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!

Image of Datadog

How to Diagram Your Cloud Architecture

Cloud architecture diagrams provide critical visibility into the resources in your environment and how they’re connected. In our latest eBook, AWS Solution Architects Jason Mimick and James Wenzel walk through best practices on how to build effective and professional diagrams.

Download the Free eBook

Top comments (0)

Cloudinary image

Video API: manage, encode, and optimize for any device, channel or network condition. Deliver branded video experiences in minutes and get deep engagement insights.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay