Restricting access: login_required and LoginRequiredMixin
Every view built since part 9 is reachable by anyone who knows the URL, logged in or not. This part closes that: requiring login for anything that changes data, and restricting editing further to a post's own author.
LoginRequiredMixin
# blog/views.py
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic.edit import CreateView, UpdateView, DeleteView
class PostCreateView(LoginRequiredMixin, CreateView):
model = Post
form_class = PostForm
template_name = "blog/post_form.html"
success_url = reverse_lazy("post_list")
def form_valid(self, form):
form.instance.author = self.request.user
return super().form_valid(form)LoginRequiredMixin has to come before CreateView in the class's parent list — Python resolves methods left to right through multiple inheritance, and the mixin needs to intercept the request before CreateView's own logic runs. Add the same mixin, first in line, to PostUpdateView and PostDeleteView.
An anonymous request to any of these now redirects to the login page instead of running the view — by default, to whatever LOGIN_URL is set to in settings.py (it defaults to /accounts/login/, which already matches the URL from part 16).
The function-based equivalent
from django.contrib.auth.decorators import login_required
@login_required
def some_function_view(request):
...@login_required is the same protection as LoginRequiredMixin, for a function-based view instead of a class-based one — this project doesn't need it, since every view left needing protection is already class-based, but it's the name to reach for in a project with function views.
Restricting edits to a post's own author
Login alone isn't enough — right now, any logged-in user can edit or delete any post, not just their own. UpdateView and DeleteView both call get_queryset() to find the object to work with; overriding it is how to scope that lookup:
class PostUpdateView(LoginRequiredMixin, UpdateView):
model = Post
form_class = PostForm
template_name = "blog/post_form.html"
slug_field = "slug"
slug_url_kwarg = "slug"
def get_queryset(self):
return Post.objects.filter(author=self.request.user)
def get_success_url(self):
return reverse_lazy("post_detail", kwargs={"slug": self.object.slug})Narrowing the queryset to author=self.request.user means a logged-in user editing someone else's post slug gets a 404, not the other user's post — the lookup genuinely finds nothing, since it was never looking outside their own posts to begin with. Apply the same get_queryset() override to PostDeleteView.
Only hiding the edit/delete links from users who shouldn't see them (the {% if user == post.author %} template check from part 15) and treating that as the actual protection. A template check controls what's shown — it does nothing to stop a direct request to the URL. The get_queryset() override above is the real enforcement; the template check is just a courtesy that keeps a logged-in user from seeing links that would 404 anyway.
Next: the pieces that make the site look and feel finished — starting with CSS and JavaScript, served correctly.