DEV Community

ngemuantony
ngemuantony

Posted on • Edited on

Building a Project Budget Manager with Django - Part 5: Templates and Documentation

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 
Enter fullscreen mode Exit fullscreen mode

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> 
Enter fullscreen mode Exit fullscreen mode

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 %} 
Enter fullscreen mode Exit fullscreen mode

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 %} 
Enter fullscreen mode Exit fullscreen mode

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> 
Enter fullscreen mode Exit fullscreen mode

Documentation Best Practices

  1. 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 """ 
Enter fullscreen mode Exit fullscreen mode
  1. 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 
Enter fullscreen mode Exit fullscreen mode
  1. 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') 
Enter fullscreen mode Exit fullscreen mode

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') 
Enter fullscreen mode Exit fullscreen mode

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') 
Enter fullscreen mode Exit fullscreen mode

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)