Week 3: Views, URLs & Templates
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:
- Master Django's view system (function-based and class-based views)
- Implement advanced URL routing with parameters and patterns
- Use Django's template language with tags, filters, and custom functionality
- Create dynamic, interactive web pages
- Implement a complete commenting system
- Practice feature branch workflow with Git
- 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:
- Receives a web request
- Processes the request (often with models/database)
- 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:
- Django Views Explained (18 min)
- 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:
- Django Forms Tutorial (25 min)
- 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
<str:name>: Matches any non-empty string (default)<int:id>: Matches positive integers<slug:slug>: Matches slug strings (letters, numbers, hyphens, underscores)<uuid:id>: Matches UUID strings<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:
- Django URL Routing Deep Dive (20 min)
- 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 minuteCreate 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:
- Django Template Tags and Filters (28 min)
- 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-systemPractical 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-systemMerging 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-systemRecommended Videos:
- Git Branching and Merging (25 min)
- 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 migrateStep 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:
- ✅ Create feature branch for commenting system
- ✅ Implement AJAX comment submission with form validation
- ✅ Add search functionality with multiple filters
- ✅ Create custom template tags and filters
- ✅ Enhance URL routing with parameters
- ✅ Add categories to blog posts
- ✅ 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 branchBonus Challenges:
- Add like/unlike functionality for posts
- Implement comment replies (nested comments)
- Create a contact form with email sending
- Add social media sharing buttons
- Implement infinite scroll pagination
- Create a sitemap for SEO
Testing Your Features:
- Comment System: Submit comments via AJAX
- Search: Test different search combinations
- Categories: Create categories and assign to posts
- Templates: Verify custom tags work correctly
- 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:
- Django view architecture and HTTP request handling
- Advanced URL routing and parameter capturing
- Template inheritance and component reusability
- JavaScript integration with Django backends
- Git branching and collaboration workflows
- Form handling and AJAX submissions
- 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:
- Ensure your commenting feature branch is merged
- Test all search and filtering functionality
- Verify your GitHub repository is up to date
- Review Django's User model documentation
- Familiarize yourself with Django's authentication views