Introduction
In this part, we'll dive into creating beautiful and functional templates for our project using Django's template system and Tailwind CSS. We'll also focus on writing clear documentation and comments to make our code more maintainable.
Template Organization
Our templates are organized in a hierarchical structure:
templates/
├── layout/ # Base layout templates
│ ├── base.html # Main base template
│ └── dashboard/ # Dashboard-specific layouts
│ ├── base.html # Dashboard base template
│ ├── _header.html # Header component
│ ├── _sidebar.html # Sidebar component
│ └── _footer.html # Footer component
├── projects/ # Project-related templates
│ ├── list.html # Project list view
│ ├── detail.html # Project detail view
│ ├── form.html # Project create/edit form
│ └── _project_card.html # Reusable project card component
└── components/ # Reusable UI components
├── alerts.html # Alert messages
├── forms/ # Form-related components
└── modals/ # Modal dialogs
Base Template
Here's our base template with detailed comments:
<!-- templates/layout/base.html -->
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
{# Meta tags for proper rendering and SEO #}
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="{% block meta_description %}Project Budget Manager{% endblock %}">
{# Dynamic title block #}
<title>{% block title %}Project Budget Manager{% endblock %}</title>
{# Favicon #}
<link rel="icon" type="image/png" href="{% static 'img/favicon.png' %}">
{# Tailwind CSS #}
<link href="{% static 'css/output.css' %}" rel="stylesheet">
{# Custom CSS #}
{% block extra_css %}{% endblock %}
{# Alpine.js for interactivity #}
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
</head>
<body class="bg-gray-50 dark:bg-gray-900">
{# Main content wrapper #}
<div class="min-h-screen flex flex-col">
{# Content block for page-specific content #}
{% block content %}{% endblock %}
{# Footer block #}
{% block footer %}
{% include "layout/_footer.html" %}
{% endblock %}
</div>
{# Flash messages #}
{% if messages %}
<div class="fixed bottom-4 right-4 z-50">
{% for message in messages %}
<div class="alert alert-{{ message.tags }} mb-2">
{{ message }}
</div>
{% endfor %}
</div>
{% endif %}
{# JavaScript block for page-specific scripts #}
{% block extra_js %}{% endblock %}
</body>
</html>
Dashboard Layout
The dashboard layout extends the base template and adds navigation:
<!-- templates/layout/dashboard/base.html -->
{% extends "layout/base.html" %}
{% block content %}
<div class="flex h-screen bg-gray-100 dark:bg-gray-900">
{# Sidebar navigation #}
{% include "layout/dashboard/_sidebar.html" %}
{# Main content area #}
<div class="flex-1 flex flex-col overflow-hidden">
{# Top navigation bar #}
{% include "layout/dashboard/_header.html" %}
{# Main content #}
<main class="flex-1 overflow-x-hidden overflow-y-auto bg-gray-100 dark:bg-gray-900">
<div class="container mx-auto px-6 py-8">
{# Page header #}
<div class="mb-8">
<h1 class="text-2xl font-semibold text-gray-900 dark:text-white">
{% block page_title %}Dashboard{% endblock %}
</h1>
</div>
{# Page-specific content #}
{% block dashboard_content %}{% endblock %}
</div>
</main>
</div>
</div>
{% endblock %}
Form Templates
Here's an example of a form template with proper styling and validation:
<!-- templates/projects/form.html -->
{% extends "layout/dashboard/base.html" %}
{% block dashboard_content %}
<div class="bg-white dark:bg-gray-800 shadow rounded-lg p-6">
<h2 class="text-lg font-medium text-gray-900 dark:text-white mb-6">
{% if form.instance.pk %}
Edit Project
{% else %}
Create New Project
{% endif %}
</h2>
<form method="post" enctype="multipart/form-data" class="space-y-6">
{% csrf_token %}
{# Form errors #}
{% if form.non_field_errors %}
<div class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded">
{{ form.non_field_errors }}
</div>
{% endif %}
{# Project name field #}
<div>
<label for="{{ form.name.id_for_label }}"
class="block text-sm font-medium text-gray-700 dark:text-gray-300">
Project Name
</label>
<div class="mt-1">
{{ form.name }}
{% if form.name.errors %}
<p class="mt-2 text-sm text-red-600">
{{ form.name.errors.0 }}
</p>
{% endif %}
</div>
</div>
{# Project description field #}
<div>
<label for="{{ form.description.id_for_label }}"
class="block text-sm font-medium text-gray-700 dark:text-gray-300">
Description
</label>
<div class="mt-1">
{{ form.description }}
{% if form.description.errors %}
<p class="mt-2 text-sm text-red-600">
{{ form.description.errors.0 }}
</p>
{% endif %}
</div>
</div>
{# Form actions #}
<div class="flex justify-end space-x-4">
<a href="{% url 'project_list' %}"
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-50">
Cancel
</a>
<button type="submit"
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md shadow-sm hover:bg-blue-700">
{% if form.instance.pk %}Save Changes{% else %}Create Project{% endif %}
</button>
</div>
</form>
</div>
{% endblock %}
Component Templates
Create reusable components to maintain consistency:
<!-- templates/components/alerts.html -->
{% comment %}
Alert Component
Usage: {% include "components/alerts.html" with type="success" message="Operation successful" %}
Types: success, error, warning, info
{% endcomment %}
<div class="rounded-md p-4 mb-4
{% if type == 'success' %}
bg-green-50 text-green-800 border border-green-200
{% elif type == 'error' %}
bg-red-50 text-red-800 border border-red-200
{% elif type == 'warning' %}
bg-yellow-50 text-yellow-800 border border-yellow-200
{% else %}
bg-blue-50 text-blue-800 border border-blue-200
{% endif %}">
<div class="flex">
<div class="flex-shrink-0">
{% if type == 'success' %}
{# Success icon #}
<svg class="h-5 w-5 text-green-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
</svg>
{% elif type == 'error' %}
{# Error icon #}
<svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
</svg>
{% endif %}
</div>
<div class="ml-3">
<p class="text-sm">{{ message }}</p>
</div>
</div>
</div>
Documentation Best Practices
- File Headers: Add descriptive headers to all files:
"""
project_budget/app/views.py
This module contains the views for the Project Budget Manager application.
It handles all HTTP requests and returns appropriate responses.
Author: Your Name
Created: March 20, 2025
"""
- Function/Class Documentation:
def calculate_project_budget(project):
"""
Calculate the total budget and expenses for a project.
Args:
project (Project): The project instance to calculate budget for
Returns:
tuple: A tuple containing (total_budget, total_expenses, remaining_budget)
Example:
>>> project = Project.objects.get(id=1)
>>> total, expenses, remaining = calculate_project_budget(project)
"""
total_budget = project.budget_set.aggregate(Sum('amount'))['amount__sum'] or 0
total_expenses = project.expense_set.aggregate(Sum('amount'))['amount__sum'] or 0
remaining_budget = total_budget - total_expenses
return total_budget, total_expenses, remaining_budget
- Code Comments:
class ProjectListView(ListView):
model = Project
template_name = 'projects/list.html'
context_object_name = 'projects'
def get_queryset(self):
# Get the base queryset
queryset = super().get_queryset()
# Filter by user's projects only
queryset = queryset.filter(owner=self.request.user)
# Apply search filter if provided
search_query = self.request.GET.get('q')
if search_query:
queryset = queryset.filter(
Q(name__icontains=search_query) |
Q(description__icontains=search_query)
)
# Sort by status and due date
return queryset.order_by('status', 'due_date')
Template Tags and Filters
Create custom template tags for reusable functionality:
# app/templatetags/project_tags.py
from django import template
from django.template.defaultfilters import floatformat
register = template.Library()
@register.filter
def currency(value):
"""
Format a number as currency.
Usage:
{{ project.budget|currency }}
Example:
Input: 1234.5
Output: $1,234.50
"""
if value is None:
return '$0.00'
return f'${floatformat(value, 2)}'
@register.simple_tag
def get_project_status_class(status):
"""
Return the appropriate CSS class for a project status.
Usage:
{% get_project_status_class project.status as status_class %}
<span class="{{ status_class }}">{{ project.status }}</span>
"""
status_classes = {
'planning': 'bg-blue-100 text-blue-800',
'in_progress': 'bg-yellow-100 text-yellow-800',
'completed': 'bg-green-100 text-green-800',
'on_hold': 'bg-gray-100 text-gray-800',
}
return status_classes.get(status, 'bg-gray-100 text-gray-800')
Testing Templates
Create tests for your templates:
# app/tests/test_templates.py
from django.test import TestCase, Client
from django.urls import reverse
from django.contrib.auth import get_user_model
class ProjectTemplateTests(TestCase):
def setUp(self):
self.client = Client()
self.user = get_user_model().objects.create_user(
username='testuser',
password='testpass123'
)
self.client.login(username='testuser', password='testpass123')
def test_project_list_template(self):
"""Test that the project list template renders correctly"""
response = self.client.get(reverse('project_list'))
# Check response
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'projects/list.html')
# Check context
self.assertIn('projects', response.context)
# Check page content
self.assertContains(response, 'Projects')
self.assertContains(response, 'Create New Project')
Next Steps
In Part 6, we'll cover advanced features like custom user management, admin dashboard customization, and utility functions. We'll also look at JavaScript integration and Tailwind configuration.
Additional Resources
This article is part of the "Building a Project Budget Manager with Django" series. Check out Part 1, Part 2,Part 3, and Part 4 if you haven't already!
Top comments (0)