AI Tutor Gemini

Week 3: Views, URLs & Templates

Page
Style
Download Notes
Month 1: The Foundations (Weeks 1-4)
Module 1

Week 3: Views, URLs & Templates

Week 3: Views, URLs & Templates

Professional Django Developer Bootcamp

Learning Objectives

By the end of this week, students will be able to:

  1. Master Django's view system (function-based and class-based views)
  2. Implement advanced URL routing with parameters and patterns
  3. Use Django's template language with tags, filters, and custom functionality
  4. Create dynamic, interactive web pages
  5. Implement a complete commenting system
  6. Practice feature branch workflow with Git
  7. Build reusable template components

Day 1: Django Views Deep Dive

Session 1: Understanding Django Views (2 hours)

What is a Django View?

A view is a Python function or class that:

  1. Receives a web request
  2. Processes the request (often with models/database)
  3. Returns a web response (HTML, JSON, redirect, etc.)
# Basic view structure
def my_view(request):
    # Process request
    context = {'data': 'some value'}
    # Return response
    return render(request, 'template.html', context)

Function-Based Views vs Class-Based Views

Function-Based Views (FBVs):

from django.shortcuts import render, get_object_or_404
from .models import Post

def post_detail(request, slug):
    post = get_object_or_404(Post, slug=slug)
    return render(request, 'blog/post_detail.html', {'post': post})

Class-Based Views (CBVs):

from django.views.generic import DetailView
from .models import Post

class PostDetailView(DetailView):
    model = Post
    template_name = 'blog/post_detail.html'
    context_object_name = 'post'

Common View Patterns

Let's expand our blog with more sophisticated views in blog/views.py:

from django.shortcuts import render, get_object_or_404, redirect
from django.contrib import messages
from django.core.paginator import Paginator
from django.db.models import Q, Count
from django.http import JsonResponse
from django.views.decorators.http import require_POST
from .models import Post, Comment
from .forms import CommentForm  # We'll create this later

def post_list(request):
    """Enhanced post list with search and filtering"""
    posts = Post.objects.filter(status='published').annotate(
        comment_count=Count('comments')
    )
    
    # Search functionality
    search_query = request.GET.get('search')
    if search_query:
        posts = posts.filter(
            Q(title__icontains=search_query) | 
            Q(content__icontains=search_query)
        )
    
    # Category filtering (we'll add categories later)
    category = request.GET.get('category')
    if category:
        posts = posts.filter(category__slug=category)
    
    # Pagination
    paginator = Paginator(posts, 6)
    page_number = request.GET.get('page')
    posts = paginator.get_page(page_number)
    
    context = {
        'posts': posts,
        'search_query': search_query,
        'title': 'Blog Posts',
    }
    return render(request, 'blog/post_list.html', context)

def post_detail(request, slug):
    """Enhanced post detail with comment form"""
    post = get_object_or_404(Post, slug=slug, status='published')
    comments = post.comments.filter(is_approved=True).order_by('created_at')
    comment_form = CommentForm()
    
    context = {
        'post': post,
        'comments': comments,
        'comment_form': comment_form,
        'title': post.title,
    }
    return render(request, 'blog/post_detail.html', context)

@require_POST
def add_comment(request, slug):
    """Handle comment submission via AJAX"""
    post = get_object_or_404(Post, slug=slug, status='published')
    form = CommentForm(request.POST)
    
    if form.is_valid():
        comment = form.save(commit=False)
        comment.post = post
        comment.save()
        
        # Return JSON response for AJAX
        if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
            return JsonResponse({
                'success': True,
                'message': 'Comment submitted successfully! It will appear after approval.'
            })
        
        messages.success(request, 'Comment submitted successfully!')
        return redirect('blog:post_detail', slug=slug)
    
    # Handle form errors
    if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
        return JsonResponse({
            'success': False,
            'errors': form.errors
        })
    
    messages.error(request, 'Please correct the errors below.')
    return redirect('blog:post_detail', slug=slug)

def archive(request, year=None, month=None):
    """Blog archive by year and month"""
    posts = Post.objects.filter(status='published')
    
    if year:
        posts = posts.filter(published_at__year=year)
        if month:
            posts = posts.filter(published_at__month=month)
    
    posts = posts.order_by('-published_at')
    
    context = {
        'posts': posts,
        'year': year,
        'month': month,
        'title': f'Archive for {year}' + (f'/{month}' if month else ''),
    }
    return render(request, 'blog/archive.html', context)

Recommended Videos:

  1. Django Views Explained (18 min)
  2. Function vs Class Based Views (22 min)

Session 2: HTTP Methods and Request Handling (2 hours)

Understanding HTTP Methods

def contact_view(request):
    """Handle both GET and POST for contact form"""
    if request.method == 'POST':
        # Process form submission
        name = request.POST.get('name')
        email = request.POST.get('email')
        message = request.POST.get('message')
        
        # Validate and save (we'll add form validation later)
        if name and email and message:
            # Save contact message
            messages.success(request, 'Message sent successfully!')
            return redirect('blog:contact')
        else:
            messages.error(request, 'Please fill in all fields.')
    
    # GET request - show the form
    return render(request, 'blog/contact.html')

Request Object Properties

def debug_request(request):
    """Explore request object properties"""
    context = {
        'method': request.method,
        'path': request.path,
        'get_params': request.GET,
        'post_data': request.POST,
        'user': request.user,
        'is_authenticated': request.user.is_authenticated,
        'session': dict(request.session),
    }
    return render(request, 'blog/debug.html', context)

Working with Forms

Create blog/forms.py:

from django import forms
from .models import Comment

class CommentForm(forms.ModelForm):
    """Form for adding comments to blog posts"""
    
    class Meta:
        model = Comment
        fields = ['name', 'email', 'content']
        widgets = {
            'name': forms.TextInput(attrs={
                'class': 'form-control',
                'placeholder': 'Your name',
                'required': True,
            }),
            'email': forms.EmailInput(attrs={
                'class': 'form-control',
                'placeholder': 'your.email@example.com',
                'required': True,
            }),
            'content': forms.Textarea(attrs={
                'class': 'form-control',
                'placeholder': 'Share your thoughts...',
                'rows': 4,
                'required': True,
            }),
        }
    
    def clean_content(self):
        """Custom validation for comment content"""
        content = self.cleaned_data['content']
        if len(content) < 10:
            raise forms.ValidationError("Comment must be at least 10 characters long.")
        return content

class ContactForm(forms.Form):
    """Contact form for the contact page"""
    name = forms.CharField(
        max_length=100,
        widget=forms.TextInput(attrs={
            'class': 'form-control',
            'placeholder': 'Your name'
        })
    )
    email = forms.EmailField(
        widget=forms.EmailInput(attrs={
            'class': 'form-control',
            'placeholder': 'your.email@example.com'
        })
    )
    subject = forms.CharField(
        max_length=200,
        widget=forms.TextInput(attrs={
            'class': 'form-control',
            'placeholder': 'Subject'
        })
    )
    message = forms.CharField(
        widget=forms.Textarea(attrs={
            'class': 'form-control',
            'rows': 5,
            'placeholder': 'Your message'
        })
    )

Recommended Videos:

  1. Django Forms Tutorial (25 min)
  2. Handling POST Requests in Django (15 min)

Day 2: Advanced URL Routing

Session 3: URL Patterns and Parameters (2 hours)

URL Pattern Types

Django URL patterns can capture different types of parameters:

from django.urls import path, re_path
from . import views

app_name = 'blog'

urlpatterns = [
    # Basic patterns
    path('', views.post_list, name='post_list'),
    path('about/', views.about, name='about'),
    
    # String parameters
    path('post/<slug:slug>/', views.post_detail, name='post_detail'),
    path('category/<str:category>/', views.posts_by_category, name='posts_by_category'),
    
    # Integer parameters
    path('archive/<int:year>/', views.archive, name='archive_year'),
    path('archive/<int:year>/<int:month>/', views.archive, name='archive_month'),
    
    # Multiple parameters
    path('post/<slug:slug>/comment/<int:comment_id>/', views.comment_detail, name='comment_detail'),
    
    # Regular expressions for complex patterns
    re_path(r'^archive/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})/$', views.archive, name='archive_regex'),
    
    # AJAX endpoints
    path('ajax/add-comment/<slug:slug>/', views.add_comment, name='add_comment'),
    path('ajax/like-post/<int:post_id>/', views.like_post, name='like_post'),
]

URL Parameter Types

  1. <str:name>: Matches any non-empty string (default)
  2. <int:id>: Matches positive integers
  3. <slug:slug>: Matches slug strings (letters, numbers, hyphens, underscores)
  4. <uuid:id>: Matches UUID strings
  5. <path:path>: Matches any non-empty string, including path separators

Advanced URL Features

# blog/urls.py with advanced features
from django.urls import path, include
from . import views

app_name = 'blog'

# Main URL patterns
urlpatterns = [
    path('', views.post_list, name='post_list'),
    path('search/', views.search, name='search'),
    path('contact/', views.contact, name='contact'),
    
    # Post-related URLs
    path('post/', include([
        path('<slug:slug>/', views.post_detail, name='post_detail'),
        path('<slug:slug>/like/', views.like_post, name='like_post'),
        path('<slug:slug>/share/', views.share_post, name='share_post'),
    ])),
    
    # Archive URLs
    path('archive/', include([
        path('', views.archive, name='archive'),
        path('<int:year>/', views.archive, name='archive_year'),
        path('<int:year>/<int:month>/', views.archive, name='archive_month'),
        path('<int:year>/<int:month>/<int:day>/', views.archive, name='archive_day'),
    ])),
    
    # API endpoints
    path('api/', include([
        path('posts/', views.api_posts, name='api_posts'),
        path('comments/', views.api_comments, name='api_comments'),
    ])),
]

URL Namespacing and Reversing

# In views.py
from django.urls import reverse
from django.shortcuts import redirect

def some_view(request):
    # Reverse URL by name
    url = reverse('blog:post_list')
    
    # Reverse with parameters
    url = reverse('blog:post_detail', kwargs={'slug': 'my-post'})
    
    # Redirect to a named URL
    return redirect('blog:post_list')

# In templates
# {% url 'blog:post_detail' slug=post.slug %}

Recommended Videos:

  1. Django URL Routing Deep Dive (20 min)
  2. Django URL Parameters Tutorial (15 min)

Day 3: Django Template System

Session 4: Template Tags, Filters, and Logic (2 hours)

Template Tags

Template tags provide logic in templates:

<!-- blog/templates/blog/post_list.html -->
{% extends 'blog/base.html' %}
{% load blog_extras %}  <!-- Custom template tags -->

{% block content %}
<div class="search-bar">
    <form method="get" class="search-form">
        <input type="text" name="search" value="{{ search_query }}" 
               placeholder="Search posts..." class="form-control">
        <button type="submit" class="btn btn-primary">Search</button>
    </form>
</div>

{% if search_query %}
    <div class="search-results">
        <h3>Search results for "{{ search_query }}"</h3>
        <small>{{ posts.paginator.count }} post{{ posts.paginator.count|pluralize }} found</small>
    </div>
{% endif %}

<div class="post-grid">
    {% for post in posts %}
        <article class="post-card">
            <h2>
                <a href="{% url 'blog:post_detail' slug=post.slug %}">
                    {{ post.title|title }}
                </a>
            </h2>
            
            <div class="post-meta">
                <span class="author">By {{ post.author.get_full_name|default:post.author.username }}</span>
                <span class="date">{{ post.published_at|date:"M d, Y" }}</span>
                <span class="comments">{{ post.comment_count }} comment{{ post.comment_count|pluralize }}</span>
            </div>
            
            <div class="post-excerpt">
                {% if post.excerpt %}
                    {{ post.excerpt }}
                {% else %}
                    {{ post.content|truncatewords:30|linebreaks }}
                {% endif %}
            </div>
            
            <div class="post-tags">
                {% for tag in post.tags.all %}
                    <span class="tag">{{ tag.name }}</span>
                {% empty %}
                    <span class="no-tags">No tags</span>
                {% endfor %}
            </div>
            
            <a href="{% url 'blog:post_detail' slug=post.slug %}" class="read-more">
                Read More →
            </a>
        </article>
    {% empty %}
        <div class="no-posts">
            {% if search_query %}
                <p>No posts found matching "{{ search_query }}".</p>
                <a href="{% url 'blog:post_list' %}">View all posts</a>
            {% else %}
                <p>No blog posts yet. Check back soon!</p>
            {% endif %}
        </div>
    {% endfor %}
</div>

<!-- Pagination with enhanced styling -->
{% if posts.has_other_pages %}
    <nav class="pagination-nav">
        <ul class="pagination">
            {% if posts.has_previous %}
                <li><a href="?{% if search_query %}search={{ search_query }}&{% endif %}page=1">« First</a></li>
                <li><a href="?{% if search_query %}search={{ search_query }}&{% endif %}page={{ posts.previous_page_number }}">‹ Previous</a></li>
            {% endif %}
            
            {% for num in posts.paginator.page_range %}
                {% if posts.number == num %}
                    <li class="active"><span>{{ num }}</span></li>
                {% elif num > posts.number|add:'-3' and num < posts.number|add:'3' %}
                    <li><a href="?{% if search_query %}search={{ search_query }}&{% endif %}page={{ num }}">{{ num }}</a></li>
                {% endif %}
            {% endfor %}
            
            {% if posts.has_next %}
                <li><a href="?{% if search_query %}search={{ search_query }}&{% endif %}page={{ posts.next_page_number }}">Next ›</a></li>
                <li><a href="?{% if search_query %}search={{ search_query }}&{% endif %}page={{ posts.paginator.num_pages }}">Last »</a></li>
            {% endif %}
        </ul>
    </nav>
{% endif %}
{% endblock %}

Template Filters

Built-in and custom filters for data formatting:

<!-- Common Django template filters -->
{{ post.title|upper }}                    <!-- UPPERCASE -->
{{ post.content|length }}                 <!-- Character count -->
{{ post.published_at|timesince }} ago     <!-- "3 days ago" -->
{{ post.content|truncatewords:20 }}       <!-- First 20 words -->
{{ post.content|linebreaks }}             <!-- Convert \n to <br> -->
{{ post.author.email|default:"No email" }} <!-- Default value -->
{{ post.comment_count|pluralize:"s" }}     <!-- Pluralization -->
{{ post.content|slugify }}                <!-- URL-friendly version -->

<!-- Chaining filters -->
{{ post.title|lower|title }}              <!-- "My Post Title" -->
{{ post.content|striptags|truncatewords:15 }} <!-- Remove HTML, then truncate -->

Custom Template Tags and Filters

Create blog/templatetags/__init__.py and blog/templatetags/blog_extras.py:

# blog/templatetags/blog_extras.py
from django import template
from django.utils.safestring import mark_safe
from django.db.models import Count
from ..models import Post
import markdown

register = template.Library()

@register.simple_tag
def recent_posts(count=5):
    """Get recent published posts"""
    return Post.objects.filter(status='published').order_by('-published_at')[:count]

@register.simple_tag
def popular_posts(count=5):
    """Get posts with most comments"""
    return Post.objects.filter(status='published').annotate(
        comment_count=Count('comments')
    ).order_by('-comment_count')[:count]

@register.inclusion_tag('blog/tags/post_sidebar.html')
def post_sidebar():
    """Render sidebar with recent and popular posts"""
    recent = Post.objects.filter(status='published').order_by('-published_at')[:3]
    popular = Post.objects.filter(status='published').annotate(
        comment_count=Count('comments')
    ).order_by('-comment_count')[:3]
    
    return {
        'recent_posts': recent,
        'popular_posts': popular,
    }

@register.filter(name='markdown')
def markdown_format(text):
    """Convert markdown to HTML"""
    return mark_safe(markdown.markdown(text))

@register.filter
def reading_time(content):
    """Calculate reading time based on word count"""
    word_count = len(content.split())
    reading_time = word_count // 200  # Average reading speed
    return max(1, reading_time)  # Minimum 1 minute

Create the sidebar template blog/templates/blog/tags/post_sidebar.html:

<div class="sidebar">
    <div class="widget">
        <h3>Recent Posts</h3>
        <ul class="post-list">
            {% for post in recent_posts %}
                <li>
                    <a href="{% url 'blog:post_detail' slug=post.slug %}">
                        {{ post.title }}
                    </a>
                    <small>{{ post.published_at|date:"M d" }}</small>
                </li>
            {% endfor %}
        </ul>
    </div>
    
    <div class="widget">
        <h3>Popular Posts</h3>
        <ul class="post-list">
            {% for post in popular_posts %}
                <li>
                    <a href="{% url 'blog:post_detail' slug=post.slug %}">
                        {{ post.title }}
                    </a>
                    <small>{{ post.comment_count }} comment{{ post.comment_count|pluralize }}</small>
                </li>
            {% endfor %}
        </ul>
    </div>
</div>

Recommended Videos:

  1. Django Template Tags and Filters (28 min)
  2. Custom Template Tags in Django (20 min)

Day 4: Interactive Features & Git Branching

Session 5: Adding Comment System with AJAX (2 hours)

Enhanced Post Detail Template

Update blog/templates/blog/post_detail.html:

{% extends 'blog/base.html' %}
{% load blog_extras %}

{% block title %}{{ post.title }} - My Django Blog{% endblock %}

{% block content %}
<article class="post-detail">
    <header class="post-header">
        <h1>{{ post.title }}</h1>
        <div class="post-meta">
            <span class="author">By {{ post.author.get_full_name|default:post.author.username }}</span>
            <span class="date">{{ post.published_at|date:"F d, Y" }}</span>
            <span class="reading-time">{{ post.content|reading_time }} min read</span>
            {% if post.updated_at > post.created_at %}
                <span class="updated">(Updated: {{ post.updated_at|date:"M d, Y" }})</span>
            {% endif %}
        </div>
    </header>
    
    <div class="post-content">
        {{ post.content|markdown }}
    </div>
    
    <footer class="post-footer">
        <div class="post-actions">
            <button id="like-btn" data-post-id="{{ post.id }}" class="btn btn-outline">
                ❤️ Like (<span id="like-count">0</span>)
            </button>
            <button class="btn btn-outline" onclick="copyPostUrl()">
                🔗 Share
            </button>
        </div>
    </footer>
</article>

<!-- Comments Section -->
<section class="comments-section">
    <h3>Comments ({{ comments.count }})</h3>
    
    <!-- Comment Form -->
    <div class="comment-form-wrapper">
        <h4>Leave a Comment</h4>
        <form id="comment-form" method="post" action="{% url 'blog:add_comment' slug=post.slug %}">
            {% csrf_token %}
            <div class="form-group">
                {{ comment_form.name.label_tag }}
                {{ comment_form.name }}
                <div class="error-message" id="error-name"></div>
            </div>
            
            <div class="form-group">
                {{ comment_form.email.label_tag }}
                {{ comment_form.email }}
                <div class="error-message" id="error-email"></div>
            </div>
            
            <div class="form-group">
                {{ comment_form.content.label_tag }}
                {{ comment_form.content }}
                <div class="error-message" id="error-content"></div>
            </div>
            
            <button type="submit" class="btn btn-primary">Submit Comment</button>
        </form>
        <div id="form-messages"></div>
    </div>
    
    <!-- Comments List -->
    <div class="comments-list">
        {% for comment in comments %}
            <div class="comment">
                <div class="comment-header">
                    <strong>{{ comment.name }}</strong>
                    <small>{{ comment.created_at|date:"F d, Y \a\t g:i A" }}</small>
                </div>
                <div class="comment-content">
                    {{ comment.content|linebreaks }}
                </div>
            </div>
        {% empty %}
            <p class="no-comments">No comments yet. Be the first to comment!</p>
        {% endfor %}
    </div>
</section>

{% post_sidebar %}

<script>
// AJAX comment submission
document.getElementById('comment-form').addEventListener('submit', function(e) {
    e.preventDefault();
    
    const formData = new FormData(this);
    const messageDiv = document.getElementById('form-messages');
    
    // Clear previous errors
    document.querySelectorAll('.error-message').forEach(el => el.textContent = '');
    
    fetch(this.action, {
        method: 'POST',
        body: formData,
        headers: {
            'X-Requested-With': 'XMLHttpRequest',
            'X-CSRFToken': formData.get('csrfmiddlewaretoken')
        }
    })
    .then(response => response.json())
    .then(data => {
        if (data.success) {
            messageDiv.innerHTML = `<div class="alert alert-success">${data.message}</div>`;
            this.reset(); // Clear form
        } else {
            // Display field errors
            for (const [field, errors] of Object.entries(data.errors)) {
                const errorEl = document.getElementById(`error-${field}`);
                if (errorEl) {
                    errorEl.textContent = errors.join(', ');
                }
            }
        }
    })
    .catch(error => {
        messageDiv.innerHTML = '<div class="alert alert-error">An error occurred. Please try again.</div>';
    });
});

// Copy post URL function
function copyPostUrl() {
    navigator.clipboard.writeText(window.location.href).then(() => {
        alert('Post URL copied to clipboard!');
    });
}
</script>

<style>
.post-detail {
    max-width: 800px;
    margin: 0 auto;
}

.post-header {
    border-bottom: 1px solid #eee;
    padding-bottom: 1rem;
    margin-bottom: 2rem;
}

.post-meta {
    color: #666;
    font-size: 0.9em;
    margin-top: 0.5rem;
}

.post-meta span {
    margin-right: 1rem;
}

.post-content {
    line-height: 1.8;
    margin-bottom: 2rem;
}

.post-actions {
    display: flex;
    gap: 1rem;
    margin: 2rem 0;
}

.btn {
    padding: 0.5rem 1rem;
    border: 1px solid #ddd;
    background: white;
    cursor: pointer;
    border-radius: 4px;
    text-decoration: none;
    display: inline-block;
}

.btn-primary {
    background: #007bff;
    color: white;
    border-color: #007bff;
}

.btn-outline:hover {
    background: #f8f9fa;
}

.comment-form-wrapper {
    background: #f8f9fa;
    padding: 2rem;
    border-radius: 8px;
    margin-bottom: 2rem;
}

.form-group {
    margin-bottom: 1rem;
}

.form-group label {
    display: block;
    margin-bottom: 0.5rem;
    font-weight: bold;
}

.form-control {
    width: 100%;
    padding: 0.5rem;
    border: 1px solid #ddd;
    border-radius: 4px;
    font-size: 1rem;
}

.comment {
    border-bottom: 1px solid #eee;
    padding: 1rem 0;
}

.comment-header {
    margin-bottom: 0.5rem;
}

.error-message {
    color: #dc3545;
    font-size: 0.875rem;
    margin-top: 0.25rem;
}

.alert {
    padding: 1rem;
    margin: 1rem 0;
    border-radius: 4px;
}

.alert-success {
    background: #d4edda;
    color: #155724;
    border: 1px solid #c3e6cb;
}

.alert-error {
    background: #f8d7da;
    color: #721c24;
    border: 1px solid #f5c6cb;
}
</style>
{% endblock %}

Session 6: Git Feature Branch Workflow (2 hours)

Understanding Git Branching

Branches allow you to work on features independently:

# View all branches
git branch

# Create a new branch for commenting feature
git checkout -b feature/commenting-system

# Or using newer syntax
git switch -c feature/commenting-system

# Switch between branches
git checkout main
git checkout feature/commenting-system

# Or using newer syntax
git switch main
git switch feature/commenting-system

Practical Branching Workflow

# 1. Start from main branch
git checkout main
git pull origin main  # Get latest changes

# 2. Create feature branch
git checkout -b feature/commenting-system

# 3. Work on your feature (make changes, add files)
# Edit models.py, views.py, templates, etc.

# 4. Add and commit changes
git add .
git commit -m "Add AJAX commenting system

- Create CommentForm with validation
- Add AJAX comment submission
- Implement comment approval system
- Add real-time form validation
- Style comment section with CSS"

# 5. Push feature branch to GitHub
git push -u origin feature/commenting-system

# 6. Continue working with more commits
git add .
git commit -m "Add comment moderation in admin

- Enhance CommentAdmin with bulk actions
- Add comment approval notifications
- Implement spam detection filters"

git push origin feature/commenting-system

Merging Feature Branch

# 1. Switch to main branch
git checkout main

# 2. Merge feature branch
git merge feature/commenting-system

# 3. Push merged changes
git push origin main

# 4. Delete feature branch (optional)
git branch -d feature/commenting-system
git push origin --delete feature/commenting-system

Recommended Videos:

  1. Git Branching and Merging (25 min)
  2. Git Feature Branch Workflow (18 min)

Day 5: Project Enhancement & Testing

Project 3: Enhanced Blog Features

Step 1: Add Categories to Posts

Update blog/models.py:

class Category(models.Model):
    name = models.CharField(max_length=100, unique=True)
    slug = models.SlugField(max_length=100, unique=True)
    description = models.TextField(blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    
    class Meta:
        verbose_name_plural = 'Categories'
        ordering = ['name']
    
    def __str__(self):
        return self.name
    
    def get_absolute_url(self):
        return reverse('blog:posts_by_category', kwargs={'slug': self.slug})

# Update Post model to include category
class Post(models.Model):
    # ... existing fields ...
    category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, blank=True)
    # ... rest of the model ...

Create and apply migrations:

python manage.py makemigrations
python manage.py migrate

Step 2: Add Search Functionality

Update blog/views.py with enhanced search:

from django.db.models import Q

def search(request):
    """Advanced search functionality"""
    query = request.GET.get('q', '').strip()
    category = request.GET.get('category')
    date_from = request.GET.get('date_from')
    date_to = request.GET.get('date_to')
    
    posts = Post.objects.filter(status='published')
    
    if query:
        posts = posts.filter(
            Q(title__icontains=query) |
            Q(content__icontains=query) |
            Q(excerpt__icontains=query) |
            Q(author__first_name__icontains=query) |
            Q(author__last_name__icontains=query)
        ).distinct()
    
    if category:
        posts = posts.filter(category__slug=category)
    
    if date_from:
        posts = posts.filter(published_at__date__gte=date_from)
    
    if date_to:
        posts = posts.filter(published_at__date__lte=date_to)
    
    posts = posts.order_by('-published_at')
    
    # Pagination
    paginator = Paginator(posts, 8)
    page_number = request.GET.get('page')
    posts = paginator.get_page(page_number)
    
    context = {
        'posts': posts,
        'query': query,
        'category': category,
        'date_from': date_from,
        'date_to': date_to,
        'categories': Category.objects.all(),
        'title': f'Search results for "{query}"' if query else 'Search',
    }
    return render(request, 'blog/search.html', context)

Step 3: Create Search Template

Create blog/templates/blog/search.html:

{% extends 'blog/base.html' %}

{% block title %}{{ title }} - My Django Blog{% endblock %}

{% block content %}
<div class="search-page">
    <h1>Search Blog Posts</h1>
    
    <!-- Advanced Search Form -->
    <form method="get" class="search-form advanced-search">
        <div class="search-row">
            <div class="search-field">
                <label for="q">Search terms:</label>
                <input type="text" name="q" value="{{ query }}" 
                       placeholder="Enter keywords..." class="form-control">
            </div>
            
            <div class="search-field">
                <label for="category">Category:</label>
                <select name="category" class="form-control">
                    <option value="">All Categories</option>
                    {% for cat in categories %}
                        <option value="{{ cat.slug }}" {% if category == cat.slug %}selected{% endif %}>
                            {{ cat.name }}
                        </option>
                    {% endfor %}
                </select>
            </div>
        </div>
        
        <div class="search-row">
            <div class="search-field">
                <label for="date_from">From date:</label>
                <input type="date" name="date_from" value="{{ date_from }}" class="form-control">
            </div>
            
            <div class="search-field">
                <label for="date_to">To date:</label>
                <input type="date" name="date_to" value="{{ date_to }}" class="form-control">
            </div>
        </div>
        
        <div class="search-actions">
            <button type="submit" class="btn btn-primary">Search</button>
            <a href="{% url 'blog:search' %}" class="btn btn-outline">Clear</a>
        </div>
    </form>
    
    {% if query or category or date_from or date_to %}
        <div class="search-results-info">
            <h3>Search Results</h3>
            <p>Found {{ posts.paginator.count }} post{{ posts.paginator.count|pluralize }} 
               {% if query %}matching "{{ query }}"{% endif %}
               {% if category %}in {{ categories|first }}{% endif %}
            </p>
        </div>
        
        <!-- Results -->
        <div class="search-results">
            {% for post in posts %}
                <article class="search-result">
                    <h3>
                        <a href="{% url 'blog:post_detail' slug=post.slug %}">
                            {{ post.title }}
                        </a>
                    </h3>
                    
                    <div class="post-meta">
                        {% if post.category %}
                            <span class="category">{{ post.category.name }}</span>
                        {% endif %}
                        <span class="date">{{ post.published_at|date:"M d, Y" }}</span>
                        <span class="author">by {{ post.author.get_full_name|default:post.author.username }}</span>
                    </div>
                    
                    <p class="excerpt">
                        {{ post.excerpt|default:post.content|truncatewords:25 }}
                    </p>
                    
                    <a href="{% url 'blog:post_detail' slug=post.slug %}" class="read-more">
                        Read More →
                    </a>
                </article>
            {% empty %}
                <div class="no-results">
                    <p>No posts found matching your search criteria.</p>
                    <p><a href="{% url 'blog:post_list' %}">Browse all posts</a></p>
                </div>
            {% endfor %}
        </div>
        
        <!-- Pagination -->
        {% if posts.has_other_pages %}
            <div class="pagination">
                <!-- Preserve search parameters in pagination -->
                {% if posts.has_previous %}
                    <a href="?{% if query %}q={{ query }}&{% endif %}{% if category %}category={{ category }}&{% endif %}page={{ posts.previous_page_number }}">
                        ‹ Previous
                    </a>
                {% endif %}
                
                <span>Page {{ posts.number }} of {{ posts.paginator.num_pages }}</span>
                
                {% if posts.has_next %}
                    <a href="?{% if query %}q={{ query }}&{% endif %}{% if category %}category={{ category }}&{% endif %}page={{ posts.next_page_number }}">
                        Next ›
                    </a>
                {% endif %}
            </div>
        {% endif %}
    {% else %}
        <div class="search-help">
            <h3>Search Tips</h3>
            <ul>
                <li>Use keywords from post titles or content</li>
                <li>Filter by category to narrow results</li>
                <li>Use date ranges to find posts from specific periods</li>
                <li>Search terms are case-insensitive</li>
            </ul>
        </div>
    {% endif %}
</div>

<style>
.search-form {
    background: #f8f9fa;
    padding: 2rem;
    border-radius: 8px;
    margin-bottom: 2rem;
}

.search-row {
    display: flex;
    gap: 1rem;
    margin-bottom: 1rem;
}

.search-field {
    flex: 1;
}

.search-field label {
    display: block;
    margin-bottom: 0.5rem;
    font-weight: bold;
}

.search-actions {
    display: flex;
    gap: 1rem;
}

.search-result {
    border-bottom: 1px solid #eee;
    padding: 1.5rem 0;
}

.search-result h3 {
    margin-bottom: 0.5rem;
}

.search-result .post-meta {
    color: #666;
    font-size: 0.9em;
    margin-bottom: 1rem;
}

.search-result .post-meta span {
    margin-right: 1rem;
}

.category {
    background: #e9ecef;
    padding: 0.25rem 0.5rem;
    border-radius: 3px;
    font-size: 0.8em;
}
</style>
{% endblock %}

Week 3 Assignment

Requirements:

  1. ✅ Create feature branch for commenting system
  2. ✅ Implement AJAX comment submission with form validation
  3. ✅ Add search functionality with multiple filters
  4. ✅ Create custom template tags and filters
  5. ✅ Enhance URL routing with parameters
  6. ✅ Add categories to blog posts
  7. ✅ Implement responsive design improvements

Git Workflow Assignment:

# Complete this workflow:
1. git checkout -b feature/commenting-system
2. Implement commenting features
3. git add . && git commit -m "Add commenting system"
4. git push -u origin feature/commenting-system
5. Create Pull Request on GitHub
6. Merge to main branch

Bonus Challenges:

  1. Add like/unlike functionality for posts
  2. Implement comment replies (nested comments)
  3. Create a contact form with email sending
  4. Add social media sharing buttons
  5. Implement infinite scroll pagination
  6. Create a sitemap for SEO

Testing Your Features:

  1. Comment System: Submit comments via AJAX
  2. Search: Test different search combinations
  3. Categories: Create categories and assign to posts
  4. Templates: Verify custom tags work correctly
  5. URLs: Test all URL patterns with parameters

Week 3 Summary

Outstanding work! You've now mastered Django's presentation layer and built sophisticated interactive features:

Advanced Views: Function-based and class-based view patterns

URL Routing: Complex URL patterns with parameters

Template System: Tags, filters, and custom functionality

AJAX Integration: Interactive features without page reload

Git Branching: Professional feature branch workflow

Search System: Full-text search with multiple filters

Custom Tags: Reusable template components

Key Skills Developed:

  1. Django view architecture and HTTP request handling
  2. Advanced URL routing and parameter capturing
  3. Template inheritance and component reusability
  4. JavaScript integration with Django backends
  5. Git branching and collaboration workflows
  6. Form handling and AJAX submissions
  7. Database querying with Q objects

Next week, we'll dive deep into Django's authentication system! You'll learn how to implement user registration, login/logout, password management, and user profiles. We'll also explore Django's powerful permissions system and create user-specific features for your blog.

Preparation for Week 4:

  1. Ensure your commenting feature branch is merged
  2. Test all search and filtering functionality
  3. Verify your GitHub repository is up to date
  4. Review Django's User model documentation
  5. Familiarize yourself with Django's authentication views