Enhancing a Django Blog: Email Sharing, Comments, and Tagging

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:

  1. Create a reusable form: In blog/forms.py, define EmailPostForm using django.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:

  • CharField with max length
  • EmailField for email validation
  • Textarea widget for the comment field
  1. 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.

  1. 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.

  1. 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:

  1. 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.

  1. 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')
  1. 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')
  1. Extend post_detail to manage comments: Modify views.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.

  1. 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

  1. Install and configure:
pip install django-taggit

Add to INSTALLED_APPS:

INSTALLED_APPS = [
    # ...
    'taggit',
]
  1. 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.

  1. 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>]>
  1. 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>
  1. Suggest similar articles using shared tags: In article_detail view:
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() and annotate()
  • Third-party integration: Using django-taggit for flexible tagging
  • Template utilities: pluralize, linebreaks, with, and forloop.counter

These features form the foundation for more advanced blog and web application functionality.

Thẻ: django-forms django-email django-taggit orm-annotations csrf-protection

Đăng vào ngày 20 tháng 6 lúc 19:31