Aug 8 '20

< all posts

Django Diaries - CRUD

#webdev#django

Reading Time: 5 mins

This is Part 3 of Django Diaries. Today we will learn about CRUD functionality in Django.

Again this is all based on what I learned from Corey Schafers Youtube Series. The code for this article from the tutorial is here: (Numbers 10)

So far we have seen function based views. Django also has class based views; these has more inbuilt functionality. There are a couple types:

  • List Views
  • Update Views
  • Delete Views
  • Create Views

List View Example

# This is the view we had for the home page:
def home(request):
    context = {
        'posts': Post.objects.all()
    }
    return render(request, "blog/home.html", context)

# Now converted to a class View.
from django.views.generic import ListView

class PostListView(ListView):
    model = Post
    template_name = "blog/home.html"  
    context_object_name = 'posts'  
    ordering = ['-date_posted']

First we specify which model the view should be looking at (this needs to be imported). Class views look for templates with specific naming conventions: <app>/<model>_<view_type>.html, So to load our template we would call it post_list.html however if we want to use something else we specify a template_name. We then need to pass the context, by default the list view calls this object, but we would have to change this in the template, so we can set a context_object_name. Now by default the latest database update is at the bottom, so we can add an attribute called ordering by setting it to 'date_posted' (which is a field in the model) it will order from oldest to newest thus if we set '-date_posted' the posts will be ordered from newest to oldest.

This view could have been two lines of code if we'd have stuck to the conventions.

Finally we need to load this view in the blog app URLs file. We use the as_view() method to load a class based view.

from .views import

urlpatterns = [
    path('', PostListView.as_view(), name="blog-home")
]

Now lets make a view to show the details for each blog post. This is like clicking a post to see the whole article.

from django.views.generic import DetailView

class PostDetailView(DetailView):
    model = Post
    # Default blog/post_detail.html

# Then set the url pattern
from .views import PostDetailView

urlpatterns = [
    path('post/<int:pk>/', PostDetailView.as_view(), name="post-detail")
]

Notice the angle brackets <> here we set variable in the URL, here it is expecting the primary key this should be an integer. Django handles this by looking it up in the model. We can them create a template by convention:

blog/post_detail.html

Alt Text

This is similar to the home view, The for loop was removed and because the view expects the model to be called 'object' the post variable was replaced with 'object', so from 'post.title' to 'object.title'

Now if we go to /post/1 we will see blog one. Then we can add these links to the anchor tags in the home view. Alt Text

Creating a new Post

For creating django has set up a view. We need a form for the title and the content of the post:

views.py

from django.views.generic import CreateView

class PostCreateView(CreateView):
    model = Post
    fields = ['title','content']
    
    def form_valid(self, form):
        # get current logged in user
        form.instance.author = self.request.user
        # run parent form valid method
        return super().form_valid(form)
        # Finally redirect after submit

Then import this in URLs:

path("post/new/", PostCreateView.as_view(), name="post-create"),

This view expects the template to be called model_form.html so if your model is called post this should be called post_form.html.

We then use the form valid method to set the author. If we submit the form, the post will be created but django doesn't know where to redirect the user when the post is created. So back in the post model we define a get absolute URL method.

from django.db import models
from django.utils import timezone
from django.contrib.auth.models import User
from django.urls import reverse

class Post(models.Model):
    title = models.CharField(max_length=100)
    content = models.TextField()
    date_posted = models.DateTimeField(default=timezone.now)
    author = models.ForeignKey(User, on_delete=models.CASCADE)

    def __str__(self):
        return f'{self.title} from {self.author}'

    # Redirect will redirect to route and reverse will return 
    # the url as a string and let the view redirect.

    def get_absolute_url(self):
        return reverse("post-detail", kwargs={'pk': self.pk})

In the template we can then add the form and add a crispy filter. We also need to make sure the user can't create a post of they aren't logged in. With functional views we used a decorator but we can't decorate a class, instead we import a mixin and let the view inherit it.

blog/views.py

from django.contrib.auth.mixins import LoginRequiredMixin

class PostCreateView(LoginRequiredMixin, CreateView):
    model = Post
    fields = ['title', 'content']

    def form_valid(self, form):
        form.instance.author = self.request.user
        return super().form_valid(form)

Note that mixins have to be placed in the brackets before the view.

Updating

class PostUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
    model = Post
    fields = ['title', 'content']

    def form_valid(self, form):
        form.instance.author = self.request.user
        return super().form_valid(form)
        # Finally redirect after submit

Add to URLs

path("post/<int:pk>/edit/", PostUpdateView.as_view(), name="post-update")

We provide the primary key so that django knows what to update, it will also use the same template as the create view. However we should make sure only the author of the post can update the post. This is where another mixin comes in:

from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin

class PostUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
    model = Post
    fields = ['title', 'content']

    def form_valid(self, form):
        form.instance.author = self.request.user
        return super().form_valid(form)

    def test_func(self):
        post = self.get_object()
        if self.request.user == post.author:
            return True
        return False

The test_func method is what the user passes test mixin uses to validate. We can use the get_object() method to get the post. Then if the user who requested to update the post isn't the author the server returns 403 or forbidden.

Delete

We import DeleteView as we did the others. Here we need to make sure the user is logged in and is the post author. We can copy this from the update view.

class PostDeleteView(LoginRequiredMixin, UserPassesTestMixin, DeleteView):
    model = Post

    success_url = '/'

    def test_func(self):
        post = self.get_object()
        if self.request.user == post.author:
            return True
        return False

The only new attribute here is the success_url

Add to URLs:

path("post/<int:pk>/delete/", PostDeleteView.as_view(), name="post-delete")

The delete view looks for a _confirm_delete.html template.

blog/post_confirm_delete.html Alt Text

This doesn't have a default form, so all we do is add a confirmation button.

Now we can add all the links. We can check of the user is authenticated show a create post button and if not show a login and register button. Then on the post, we can check if the author (object.author) is the current logged in user (user) then show an update and delete button.

Thank you for reading part 3. Up next Django Pagination.


✨If you would like to follow my day to day development journey be sure to check my Instagram.