POST Create, Read, Update and Delete (CRUD)¶
- In this chapter, we will use class based view to implement Post Create, Detail, Update and Delete functions
Introduction to Class-Based Views (CBVs) vs. Function-Based Views (FBVs)¶
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.
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.
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)
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.
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
Notes:¶
- Privilege Check: The
test_func
method ensures that only the post author can update the post.
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 %}
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
Notes:¶
- Success URL: The
success_url
attribute defines the URL to redirect to after successful deletion.
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 %}
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
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 %}
Notes:¶
- Privilege Check: Ensure that only the author can see the update and delete options.
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
andviews
where you need to link to the detailed view of a model instance. - The
get_absolute_url
method uses thereverse
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 theprimary 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})
- The
Pagination¶
For demonstration, we generate multiple Post and Comment templates
python manage.py shell
Use
django shell
to load templates into our databaseimport 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!")
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.
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'
Create a Pagination Template¶
To provide pagination controls for navigating between pages.
Create a template named
pagination.html
and include it in thehome.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' %} ...
QuerySet¶
- A
QuerySet
is a collection of database queries in Django. - It allows you to retrieve, filter, and manipulate data from the database.
Create a UserPosts View¶
To list posts by a specific user and implement pagination for these posts.
get_queryset
: Theget_queryset
method is used to filter the posts by the specific user passed in the URL.view.kwargs
: This includes theURL parameters
, such asusername
, 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')
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 %}
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'), ... ]
Additional Resource¶
Built-in generic views¶
View | Description | Parameters | Example |
---|---|---|---|
TemplateView | Renders a template. |
|
python from django.views.generic import TemplateView class HomePageView(TemplateView): template_name = 'home.html' |
ListView | Displays a list of objects. |
|
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. |
|
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. |
|
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. |
|
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. |
|
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/' |
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 }} |
Example¶
- Displaying Current Page Number:
Current Page: {{ page_obj.number }}
- Previous Page Link:
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}">Previous</a>
{% endif %}
- Next Page Link:
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">Next</a>
{% endif %}
- Looping through Page Numbers:
{% for num in page_obj.paginator.page_range %}
<a href="?page={{ num }}">{{ num }}</a>
{% endfor %}
- Displaying Total Pages:
Total Pages: {{ page_obj.paginator.num_pages }}
- Displaying Total Items:
Total Items: {{ page_obj.paginator.count }}
- Displaying Start and End Indexes:
Items {{ page_obj.start_index }}-{{ page_obj.end_index }}
Key Features of Django QuerySet:¶
- Filtering (
filter()
): Returns a new QuerySet containing objects that match the given lookup parameters.
queryset = MyModel.objects.filter(field_name=value)
- Excluding (
exclude()
): Returns a new QuerySet excluding objects that match the given lookup parameters.
queryset = MyModel.objects.exclude(field_name=value)
- 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)
- Ordering (
order_by()
): Orders the QuerySet by the given field(s) in ascending or descending order.
queryset = MyModel.objects.order_by('-created_at')
- Slicing (
[start:end]
): Allows slicing of QuerySets to limit the number of results returned.
queryset = MyModel.objects.all()[5:10]
- Aggregation (
aggregate()
): Computes aggregate values (e.g.,count
,sum
,average
) across the QuerySet.
result = MyModel.objects.aggregate(total=Count('id'))
- Counting (
count()
): Returns the number of objects in the QuerySet.
count = MyModel.objects.filter(field_name=value).count()
- 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)
- Deletion (
delete()
): Deletes the objects in the QuerySet from the database.
MyModel.objects.filter(field_name=value).delete()