Using Django Forms and Models to Extend Blog Functionality
This chapter builds on a basic blog application to implement email sharing, comment management, and tagging features. Key topics covered include:
- Sending emails via Django's email framework
- Building and processing forms (standard and model-backed)
- Integrating third-party apps:
django-taggit - Advanced ORM queries using annotations and aggregations
Email Sharing via Forms and Email Framework
To enable article sharing by email, we first implement a form-based workflow:
- Create a reusable form: In
blog/forms.py, defineEmailPostFormusingdjango.forms.Form:
from django import forms
class ShareByMailForm(forms.Form):
sender_name = forms.CharField(max_length=30)
sender_email = forms.EmailField()
recipient_email = forms.EmailField()
message = forms.CharField(required=False, widget=forms.Textarea)
This form includes:
CharFieldwith max lengthEmailFieldfor email validationTextareawidget for the comment field
- Implement the view logic: In
views.py, handle both GET (show form) and POST (process and send email):
from django.shortcuts import get_object_or_404, render
from django.core.mail import send_mail
from .forms import ShareByMailForm
def share_article(request, article_pk):
article = get_object_or_404(Article, id=article_pk, status="published")
success = False
if request.method == "POST":
mail_form = ShareByMailForm(request.POST)
if mail_form.is_valid():
data = mail_form.cleaned_data
link = request.build_absolute_uri(article.get_detail_url())
subj = f"{data['sender_name']} ({data['sender_email']}) recommends '{article.title}'"
body = f"Read '{article.title}' at {link}\n\nNote: {data['message']}"
send_mail(subj, body, 'no-reply@example.com', [data['recipient_email']])
success = True
else:
mail_form = ShareByMailForm()
return render(request, 'blog/share.html', {
'article': article,
'form': mail_form,
'sent': success
})
The CSRFprotection token {% csrf_token %} is included via the template tag. Use action="." to submit to the same URL.
- Configure the URL and template: Add the URL route to
blog/urls.py:
path('<int:article_pk>/mail-share/', share_article, name='article_mail_share'),
Render the form with {{ form.as_p }}. On successful submission, display a confirmation message instead of the form.
- Email configuration options: In
settings.py, configure the email backend:
# For development: print emails to console
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
# For production: use SMTP (e.g., Gmail)
EMAIL_HOST = 'smtp.gmail.com'
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_USER = 'user@example.com'
EMAIL_HOST_PASSWORD = 'your-app-password'
Note: For Gmail, enable "Less secure app access" in Google account settings, or better, use App Passwords.
Comment System with ModelForms
Implement a comment system using the following steps:
- Define the model: In
models.py:
from django.db.models import CASCADE, DateTimeField, BooleanField, CharField, ForeignKey, TextField
class ArticleComment(models.Model):
article = ForeignKey(Article, on_delete=CASCADE, related_name='comments')
author = CharField(max_length=100)
email = CharField(max_length=254)
content = TextField()
created_at = DateTimeField(auto_now_add=True)
updated_at = DateTimeField(auto_now=True)
is_active = BooleanField(default=True)
class Meta:
ordering = ['created_at']
def __str__(self):
return f"Comment by {self.author} on {self.article}"
Run migrations to add the blog_articlecomment table to the database.
- Register with admin: In
admin.py:
from django.contrib import admin
from .models import ArticleComment
@admin.register(ArticleComment)
class CommentAdmin(admin.ModelAdmin):
list_display = ('author', 'email', 'article', 'created_at', 'is_active')
list_filter = ('is_active', 'created_at')
search_fields = ('author', 'email', 'content')
- Create a ModelForm: In
forms.py:
from django import forms
from .models import ArticleComment
class CommentForm(forms.ModelForm):
class Meta:
model = ArticleComment
fields = ('author', 'email', 'content')
- Extend
post_detailto manage comments: Modifyviews.py:
from django.shortcuts import get_object_or_404, render
from .forms import CommentForm
from .models import Article, ArticleComment
def article_detail(request, year, month, day, slug):
article = get_object_or_404(
Article,
slug=slug,
status='published',
publish__year=year,
publish__month=month,
publish__day=day
)
active_comments = article.comments.filter(is_active=True)
comment_submit_todo = None
if request.method == 'POST':
cform = CommentForm(request.POST)
if cform.is_valid():
comment_submit_todo = cform.save(commit=False)
comment_submit_todo.article = article
comment_submit_todo.save()
else:
cform = CommentForm()
return render(request, 'blog/detail.html', {
'article': article,
'comments': active_comments,
'comment_form': cform,
'new_comment': comment_submit_todo
})
Here, commit=False allows setting the foreign key before saving.
- Render comments and form in
detail.html:
{# Count and document number of comments #}
{% with comments|length as total %}
<h2>{{ total }} comment{{ total|pluralize }}</h2>
{% endwith %}
{# Display existing comments #}
{% for comment in comments %}
<div class="comment">
<p><strong>{{ comment.author }}</strong> — {{ comment.created_at }}</p>
<p>{{ comment.content|linebreaks }}</p>
</div>
{% empty %}
<p>No comments yet.</p>
{% endfor %}
{# Comment submission form #}
{% if new_comment %}
<h3>Your comment has been posted.</h3>
{% else %}
<h3>Leave a reply</h3>
<form method="post">
{{ comment_form.as_p }}
{% csrf_token %}
<button type="submit">Post comment</button>
</form>
{% endif %}
Tagging with django-taggit
- Install and configure:
pip install django-taggit
Add to INSTALLED_APPS:
INSTALLED_APPS = [
# ...
'taggit',
]
- Attach tags to the article model:
from taggit.managers import TaggableManager
class Article(models.Model):
title = ...
body = ...
tags = TaggableManager()
Run migrations to create taggit_tag and taggit_taggeditem tables.
- Use tags in Django shell:
>>> from blog.models import Article
>>> art = Article.objects.get(id=1)
>>> art.tags.add('django', 'web', 'tutorial')
>>> art.tags.all()
# => <QuerySet [<Tag: django>, <Tag: web>, <Tag: tutorial>]>
- Integrate tags in listing and detail views:
Update post_list to accept a tag_slug parameter:
from django.shortcuts import get_object_or_404, render
from taggit.models import Tag
from .models import Article
def article_list(request, tag_slug=None):
articles = Article.published.all()
active_tag = None
if tag_slug:
active_tag = get_object_or_404(Tag, slug=tag_slug)
articles = articles.filter(tags__in=[active_tag])
# pagination logic...
return render(request, 'blog/list.html', {
'articles': articles,
'tag': active_tag
})
Add URL pattern:
path('tag/<slug:tag_slug>/', article_list, name='articles_by_tag'),
Modify the template to render tags as links:
<p class="tags">
Tags:
{% for t in object.tags.all %}
<a href="{% url "blog:articles_by_tag" t.slug %}">{{ t.name }}</a>{% if not forloop.last %}, {% endif %}
{% endfor %}
</p>
- Suggest similar articles using shared tags: In
article_detailview:
from django.db.models import Count
def article_detail_view(request, year, month, day, slug):
article = get_object_or_404(
Article,
slug=slug,
status='published',
publish__year=year,
publish__month=month,
publish__day=day
)
# Get IDs of all tags attached to the current article
related_tag_ids = article.tags.values_list('id', flat=True)
# Find other published articles sharing any of those tags, excluding current
similar = Article.published.filter(tags__in=related_tag_ids).exclude(id=article.id)
# Annotate with shared tag count and order by relevance then date
rec_suggestions = similar.annotate(match_score=Count('tags')).order_by('-match_score', '-publish_date')[:4]
return render(request, 'blog/detail.html', {
'article': article,
'similar_articles': rec_suggestions
})
Display in template:
<h3>Related Articles</h3>
{% for item in similar_articles %}
<a href="{{ item.get_absolute_url }}">{{ item.title }}</a><br>
{% empty %}
No related articles found.
{% endfor %}
Key Django Features Demonstrated
- Form rendering & validation: Both manual (
forms.Form) and model-driven (ModelForm) forms - CSRF protection: Mandatory for all POST submissions
- Email sending: Using Django’s
send_mail()with customizable backends - Built-in pagination & aggregation: With
Count()andannotate() - Third-party integration: Using
django-taggitfor flexible tagging - Template utilities:
pluralize,linebreaks,with, andforloop.counter
These features form the foundation for more advanced blog and web application functionality.