15. POST Create, Read, Update and Delete#

  • In this chapter, we will use class based view to implement Post Create, Detail, Update and Delete functions

15.1. Introduction to Class-Based Views (CBVs) vs. Function-Based Views (FBVs)#

15.1.1. Class-Based Views (CBVs):#

Advantages:

  • Reusable: You can inherit and reuse views.

  • Organized: Code is more structured, separating concerns into methods.

  • Built-in functionality: CBVs provide built-in methods for common tasks. Disadvantages:

  • Complexity: More complex to understand initially.

  • Less control: Overriding built-in behavior can be complex.

15.1.2. Function-Based Views (FBVs):#

Advantages:

  • Simplicity: Easier to understand and use.

  • Flexibility: More straightforward for simple views. Disadvantages:

  • Less reusable: Harder to reuse and extend.

  • Less organized: Can become cluttered with increasing complexity.

15.2. Step 1: Create Post Create View#

  • To create a new post with fields for the title and content.

  • The view will ensure that the post is associated with the logged-in user.

    from django.views.generic import CreateView
    from django.contrib.auth.mixins import LoginRequiredMixin
    from .models import Post
    
    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)
    
    

15.2.1. Notes:#

  • Form Validation: The form_valid method ensures the form is valid and associates the post with the logged-in user.

  • Template Naming Convention: The template for this view should be named post_form.html by default.

15.3. Step 2: Create Post Update View#

  • To update an existing post.

  • This view uses the same template as the create view and includes privilege checks to ensure only the post author can update the post.

    from django.views.generic import UpdateView
    from django.contrib.auth.mixins import 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 post.author == self.request.user:
                return True
            return False
    
    

15.3.1. Notes:#

  • Privilege Check: The test_func method ensures that only the post author can update the post.

15.4. Step 3: Set Template for Post Create and Update Views#

  • users/templates/post_form.html

    {% extends "blog/main.html" %}
    {% load crispy_forms_tags %}
    
    {% block main-content %}
    
    <div class="container">
        <h2>Post a Blog</h2>
        {% for message in messages %}
        <div class="alert alert-{{ message.tags }}">
            {{ message }}
        </div>
        {% endfor %}
        <form method="POST">
            {% csrf_token %}
            {{ form|crispy }}
            <div class="d-flex justify-content-end mt-2">
                <button type="submit" class="btn btn-primary me-2">Post</button>
                <a href="{% url 'blog-home' %}" class="btn btn-danger">Cancel</a>
            </div>
        </form>
    </div>
    
    {% endblock main-content %}
    

15.5. Step4: Set Post Delete View#

  • To delete an existing post.

  • The view ensures only the post author can delete the post.

    from django.views.generic import DeleteView
    from django.urls import reverse_lazy
    
    class PostDeleteView(LoginRequiredMixin, UserPassesTestMixin, DeleteView):
        model = Post
        success_url = reverse_lazy('blog-home')
    
        def test_func(self):
            post = self.get_object()
            if post.author == self.request.user:
                return True
            return False
    
    

15.5.1. Notes:#

  • Success URL: The success_url attribute defines the URL to redirect to after successful deletion.

15.6. Step 5: Construct Post Confirm Delete Template#

  • users/templates/post_confirm_delete.html

    {% extends "blog/main.html" %}
    {% load crispy_forms_tags %}
    
    {% block main-content %}
    
    <div class="container">
        <h2>Delete Blog</h2>
        {% for message in messages %}
        <div class="alert alert-{{ message.tags }}">
            {{ message }}
        </div>
        {% endfor %}
        <h2>Are you sure you want to delete the post "{{ object.title }}?"</h2>
        <form method="POST">
            {% csrf_token %}
            {{ form|crispy }}
            <div class="d-flex justify-content-end mt-2">
                <button type="submit" class="btn btn-danger me-2">Delete</button>
                <a href="{% url 'post-detail' object.pk %}" class="btn btn-secondary">Cancel</a>
            </div>
        </form>
    </div>
    
    {% endblock main-content %}
    

15.7. Step 6: Set Post Detail View#

  • To display the details of a specific post.

    from django.views.generic import DetailView
    from .models import Post
    
    class PostDetailView(DetailView):
        model = Post
    

15.8. Step 7: Construct Post Detail Template#

  • users/templates/users/post_detail.html

    {% extends 'blog/main.html' %} {% block main-content%}
    
    <article class="media content-section">
        <img class="rounded-circle article-img" src="{{ object.author.userprofile.image.url }}" />
        <div class="media-body">
            <div class="article-metadata">
                <a class="mr-2" href="#">{{ object.author }}</a>
                <small class="text-muted">{{ object.date_posted|date:"F d, Y" }}</small>
    
                {% if object.author == user %}
                <a class="btn btn-secondary btn-sm mt-1 mb-1" href="{% url 'post-update' object.pk %}">Update</a>
                <a class="btn btn-danger btn-sm mt-1 mb-1" href="{% url 'post-delete' object.pk %}">Delete</a>
                {% endif %}
    
            </div>
            <h2 class="article-title">{{ object.title }}</h2>
            <p class="article-content">{{ object.content }}</p>
        </div>
    </article>
    
    {% endblock main-content %}
    

15.8.1. Notes:#

  • Privilege Check: Ensure that only the author can see the update and delete options.

15.9. Step 8: Construct URL Patterns#

  • Define the URL patterns for the post CRUD operations.

    from django.urls import path
    from .views import (
        PostDetailView,
        PostCreateView,
        PostUpdateView,
        PostDeleteView
    )
    from . import views
    
    urlpatterns = [
        ...
        path("post/<int:pk>", PostDetailView.as_view(), name='post-detail'),
        path("post/<int:pk>/update", PostUpdateView.as_view(), name='post-update'),
        path("post/<int:pk>/delete", PostDeleteView.as_view(), name='post-delete'),
        path("post/new", PostCreateView.as_view(), name='post-create'),
        ...
    ]
    
    
  • Write get_absolute_url in Post Model

    • The get_absolute_url method provides a way to generate a URL for the detail view of the object.

    • This is particularly useful in templates and views where you need to link to the detailed view of a model instance.

    • The get_absolute_url method uses the reverse function to generate the URL for the detailed view of the post.

    • The reverse function takes the name of the URL pattern (post-detail) and the primary key of the post as arguments.

    from django.db import models
    from django.contrib.auth.models import User
    from django.utils import timezone
    from django.urls import reverse
    
    class Post(models.Model):
        title = models.CharField(max_length=80)
        author = models.ForeignKey(User, on_delete=models.CASCADE)
        content = models.TextField()
        date_posted = models.DateTimeField(default=timezone.now)
    
        def __str__(self):
            return self.title
    
        def get_absolute_url(self):
            return reverse('post-detail', kwargs={'pk': self.pk})
    

15.10. Pagination#

  • For demonstration, we generate multiple Post and Comment templates

    python manage.py shell
    
  • Use django shell to load templates into our database

    import json
    from blog.models import Post, Comment
    with open('post.json') as f:
        posts_json = json.load(f)
    
    for post in posts_json:
        temp_post = Post(title = post['title'], content = post['content'],author_id = post['user_id'])
        temp_post.save()
    
    # Load Comment data
    with open('comment.json') as f:
        comments_json = json.load(f)
    
    for comment in comments_json:
        try:
            post = Post.objects.get(id=comment['post_id'])
            author = User.objects.get(id=comment['author_id'])
            temp_comment = Comment(
                post=post,
                author=author,
                content=comment['content'],
                date_posted=comment['date_posted']
            )
            temp_comment.save()
        except Post.DoesNotExist:
            print(f"Post with id {comment['post_id']} does not exist.")
        except User.DoesNotExist:
            print(f"User with id {comment['author_id']} does not exist.")
        except Exception as e:
            print(f"An error occurred: {e}")
    
    print("Comments imported successfully!")
    

15.11. Pagination#

  • Pagination is the process of dividing content into discrete pages, making it easier to navigate through large datasets.

  • Django provides built-in support for pagination through class-based views.

15.11.1. Add Pagination to PostListView#

  • To display a limited number of posts per page on the home view and add pagination controls for navigation.

    # users/views.py
    from django.views.generic import ListView
    from .models import Post
    
    ...
    
    class PostListView(ListView):
        model = Post
        template_name = 'blog/home.html'
        context_object_name = 'posts'
        paginate_by = 5
        ordering = '-date_posted'
    

15.11.2. Create a Pagination Template#

  • To provide pagination controls for navigating between pages.

  • Create a template named pagination.html and include it in the home.html view.

    <!-- pagination.html -->
    <!-- Check if pagination is enabled -->
    {% if is_paginated %}
    <div class="container text-center"> <!-- Bootstrap container and text-center class for center alignment -->
        <!-- Check if there is a previous page -->
        {% if page_obj.has_previous %}
        <a class="btn btn-outline-info mb-4" href="?page=1">First</a> <!-- Link to first page -->
        <a class="btn btn-outline-info mb-4" href="?page={{ page_obj.previous_page_number }}">Previous</a>
        <!-- Link to previous page -->
        {% endif %}
    
        <!-- Loop through each page number -->
        {% for num in page_obj.paginator.page_range %}
        <!-- Check if the current page number is equal to the looped number -->
        {% if page_obj.number == num %}
        <a class="btn btn-info mb-4" href="?page={{ num }}">{{ num }}</a> <!-- Active page number -->
    
        <!-- Check if the page number is within a range of 3 pages from the current page -->
        {% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
        <a class="btn btn-outline-info mb-4" href="?page={{ num }}">{{ num }}</a> <!-- Nearby pages -->
        {% endif %}
        {% endfor %}
    
        <!-- Check if there is a next page -->
        {% if page_obj.has_next %}
        <a class="btn btn-outline-info mb-4" href="?page={{ page_obj.next_page_number }}">Next</a>
        <!-- Link to next page -->
        <a class="btn btn-outline-info mb-4" href="?page={{ page_obj.paginator.num_pages }}">Last</a>
        <!-- Link to last page -->
        {% endif %}
    </div> <!-- Close container -->
    {% endif %}
    
  • Include the pagination template in home.html:

    <!-- home.html -->
    ...
    {% include 'blog/pagination.html' %}
    ...
    

15.12. QuerySet#

  • A QuerySet is a collection of database queries in Django.

  • It allows you to retrieve, filter, and manipulate data from the database.

15.12.1. Create a UserPosts View#

  • To list posts by a specific user and implement pagination for these posts.

  • get_queryset: The get_queryset method is used to filter the posts by the specific user passed in the URL.

  • view.kwargs: This includes the URL parameters, such as username, which are used to filter the posts.

    # users/views.py
    from django.views.generic import ListView
    from django.shortcuts import get_object_or_404
    from django.contrib.auth.models import User
    from .models import Post
    
    ...
    
    class UserPostsView(ListView):
        model = Post
        template_name = 'blog/user_posts.html'
        context_object_name = 'posts'
        paginate_by = 5
        
        def get_queryset(self):
            user = get_object_or_404(User, username=self.kwargs.get('username'))
            return Post.objects.filter(author=user).order_by('-date_posted')
    
    

15.12.2. Construct UserPosts Template#

  • Create a template named user_posts.html.

    {% extends 'blog/main.html' %}
    
    {% block main-content %}
    
    {% if is_paginated %}
    <h1 class="mb-3"> Posts by {{ view.kwargs.username }} ({{ page_obj.paginator.count }})</h1>
    {% else %}
    <h1 class="mb-3"> Posts by {{ view.kwargs.username }}</h1>
    {% endif %}
    
    {% for post in posts %}
    <article class="media content-section">
        <img class="rounded-circle article-img" src="{{ post.author.userprofile.image.url }}">
        <div class="media-body">
            <div class="article-metadata">
                <a class="mr-2" href="{% url 'user-posts' post.author.username %}">{{ post.author }}</a>
                <small class="text-muted">{{ post.date_posted|date:"F d, Y" }}</small>
            </div>
            <h2><a class="article-title" href="{% url 'post-detail' post.pk %}">{{ post.title }}</a></h2>
            <p class="article-content">{{ post.content }}</p>
        </div>
    </article>
    {% endfor %}
    
    {% include 'blog/pagination.html' %}
    
    {% endblock main-content %}
    

15.12.3. Construct URL Pattern for User Posts#

  • Define the URL pattern for the UserPostsView to filter posts by a specific user.

    urlpatterns = [
    ...,
    path("<str:username>/post", UserPostsView.as_view(), name='user-posts'),
    ...
    ]
    

15.13. Additional Resource#

15.13.1. Built-in generic views#

View Description Parameters Example
TemplateView Renders a template.
  • template_name: The name of the template to use.
  • extra_context: A dictionary of context data to add to the template context.
python
from django.views.generic import TemplateView
class HomePageView(TemplateView):
    template_name = 'home.html'
      
ListView Displays a list of objects.
  • model: The model to query.
  • template_name: The name of the template to use.
  • context_object_name: The name of the context variable to use.
  • paginate_by: Number of objects per page.
python
from django.views.generic import ListView
from .models import Post
class PostListView(ListView):
    model = Post
    template_name = 'posts/post_list.html'
    context_object_name = 'posts'
      
DetailView Displays a detail page for a single object.
  • model: The model to query.
  • template_name: The name of the template to use.
  • context_object_name: The name of the context variable to use.
python
from django.views.generic import DetailView
from .models import Post
class PostDetailView(DetailView):
    model = Post
    template_name = 'posts/post_detail.html'
    context_object_name = 'post'
      
CreateView Displays a form for creating a new object and saves the object.
  • model: The model to query.
  • form_class: The form class to use.
  • template_name: The name of the template to use.
  • success_url: URL to redirect to after success.
python
from django.views.generic import CreateView
from .models import Post
from .forms import PostForm
class PostCreateView(CreateView):
    model = Post
    form_class = PostForm
    template_name = 'posts/post_form.html'
    success_url = '/posts/'
      
UpdateView Displays a form for updating an existing object and saves the changes.
  • model: The model to query.
  • form_class: The form class to use.
  • template_name: The name of the template to use.
  • success_url: URL to redirect to after success.
python
from django.views.generic import UpdateView
from .models import Post
from .forms import PostForm
class PostUpdateView(UpdateView):
    model = Post
    form_class = PostForm
    template_name = 'posts/post_form.html'
    success_url = '/posts/'
      
DeleteView Displays a confirmation page and deletes an existing object.
  • model: The model to query.
  • template_name: The name of the template to use.
  • success_url: URL to redirect to after success.
python
from django.views.generic import DeleteView
from .models import Post
class PostDeleteView(DeleteView):
    model = Post
    template_name = 'posts/post_confirm_delete.html'
    success_url = '/posts/'
      

15.13.2. Pagination Object in Django#

Parameter/Method Purpose Example
page_obj.number Current page number {{ page_obj.number }}
page_obj.paginator Paginator object used for pagination {% if page_obj.has_previous %}
page_obj.has_previous Boolean indicating if there's a previous page {% if page_obj.has_previous %}
page_obj.previous_page_number Number of the previous page Previous
page_obj.has_next Boolean indicating if there's a next page {% if page_obj.has_next %}
page_obj.next_page_number Number of the next page Next
page_obj.paginator.num_pages Total number of pages in the paginator Total Pages: {{ page_obj.paginator.num_pages }}
page_obj.paginator.page_range Iterable range of page numbers in paginator {% for num in page_obj.paginator.page_range %}
page_obj.paginator.count Total count of items across all pages Total Items: {{ page_obj.paginator.count }}
page_obj.start_index 1-based index of the first item on the current page First Item Index: {{ page_obj.start_index }}
page_obj.end_index 1-based index of the last item on the current page Last Item Index: {{ page_obj.end_index }}

15.13.2.1. Example#

  1. Displaying Current Page Number:

Current Page: {{ page_obj.number }}
  1. Previous Page Link:

{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}">Previous</a>
{% endif %}
  1. Next Page Link:

{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">Next</a>
{% endif %}
  1. Looping through Page Numbers:

{% for num in page_obj.paginator.page_range %}
<a href="?page={{ num }}">{{ num }}</a>
{% endfor %}
  1. Displaying Total Pages:

Total Pages: {{ page_obj.paginator.num_pages }}
  1. Displaying Total Items:

Total Items: {{ page_obj.paginator.count }}
  1. Displaying Start and End Indexes:

Items {{ page_obj.start_index }}-{{ page_obj.end_index }}

15.13.3. Key Features of Django QuerySet:#

  1. Filtering (filter()): Returns a new QuerySet containing objects that match the given lookup parameters.

queryset = MyModel.objects.filter(field_name=value)
  1. Excluding (exclude()): Returns a new QuerySet excluding objects that match the given lookup parameters.

queryset = MyModel.objects.exclude(field_name=value)
  1. Get single object (get()): Returns a single object matching the given lookup parameters. Raises an exception if multiple objects are found or none match.

obj = MyModel.objects.get(field_name=value)
  1. Ordering (order_by()): Orders the QuerySet by the given field(s) in ascending or descending order.

queryset = MyModel.objects.order_by('-created_at')
  1. Slicing ([start:end]): Allows slicing of QuerySets to limit the number of results returned.

queryset = MyModel.objects.all()[5:10]
  1. Aggregation (aggregate()): Computes aggregate values (e.g., count, sum, average) across the QuerySet.

result = MyModel.objects.aggregate(total=Count('id'))
  1. Counting (count()): Returns the number of objects in the QuerySet.

count = MyModel.objects.filter(field_name=value).count()
  1. Iterating (iterator()): Returns an iterator over the QuerySet to efficiently process large result sets.

queryset = MyModel.objects.all().iterator()
for obj in queryset:
    print(obj.field_name)
  1. Deletion (delete()): Deletes the objects in the QuerySet from the database.

MyModel.objects.filter(field_name=value).delete()