In this third part of our series, we'll create views and templates for our Project Budget Manager. We'll use Tailwind CSS for styling and HTMX for dynamic interactions.
Setting Up Tailwind CSS
- First, install Tailwind CSS dependencies:
npm install -D tailwindcss npx tailwindcss init
- Create a tailwind.config.js file:
/** @type {import('tailwindcss').Config} */ module.exports = { content: [ "./templates/**/*.html", "./static/**/*.js", ], theme: { extend: {}, }, plugins: [], }
- Create static/css/input.css:
@tailwind base; @tailwind components; @tailwind utilities; /* Custom styles */ @layer components { .btn-primary { @apply px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700; } .btn-secondary { @apply px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700; } .form-input { @apply mt-1 block w-full rounded-md border-gray-300 shadow-sm; } }
Creating Base Templates
- Create templates/layout/base.html:
{% load static %} <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>{% block title %}Project Budget Manager{% endblock %}</title> <link rel="stylesheet" href="{% static 'css/output.css' %}"> <script src="{% static 'js/htmx.min.js' %}" defer></script> {% block extra_head %}{% endblock %} </head> <body class="bg-gray-50"> {% include "layout/nav.html" %} <main class="container mx-auto px-4 py-8"> {% if messages %} <div class="messages mb-8"> {% for message in messages %} <div class="p-4 mb-4 rounded-lg {% if message.tags == 'success' %}bg-green-100 text-green-700{% elif message.tags == 'error' %}bg-red-100 text-red-700{% else %}bg-blue-100 text-blue-700{% endif %}"> {{ message }} </div> {% endfor %} </div> {% endif %} {% block content %}{% endblock %} </main> <footer class="bg-gray-800 text-white py-8 mt-16"> <div class="container mx-auto px-4"> <p>© {% now "Y" %} Project Budget Manager. All rights reserved.</p> </div> </footer> {% block extra_js %}{% endblock %} </body> </html>
Creating Views
- Update app/views.py with our views:
from django.shortcuts import render, redirect, get_object_or_404 from django.contrib.auth.decorators import login_required from django.contrib import messages from django.http import HttpResponse from .models import Project, Expense from .forms import ProjectForm, ExpenseForm from decimal import Decimal @login_required def dashboard(request): user_projects = Project.objects.filter( created_by=request.user ).order_by('-created_at') assigned_projects = Project.objects.filter( assigned_to=request.user ).order_by('-created_at') context = { 'user_projects': user_projects, 'assigned_projects': assigned_projects, } return render(request, 'app/dashboard.html', context) @login_required def project_list(request): projects = Project.objects.filter( created_by=request.user ).order_by('-created_at') return render(request, 'app/project_list.html', {'projects': projects}) @login_required def project_detail(request, pk): project = get_object_or_404(Project, pk=pk) expenses = project.expenses.all().order_by('-date') context = { 'project': project, 'expenses': expenses, 'total_expenses': project.get_total_expenses(), 'budget_remaining': project.get_budget_remaining(), } return render(request, 'app/project_detail.html', context) @login_required def project_create(request): if request.method == 'POST': form = ProjectForm(request.POST) if form.is_valid(): project = form.save(commit=False) project.created_by = request.user project.save() messages.success(request, 'Project created successfully.') return redirect('project_detail', pk=project.pk) else: form = ProjectForm() return render(request, 'app/project_form.html', {'form': form}) @login_required def expense_create(request, project_pk): project = get_object_or_404(Project, pk=project_pk) if request.method == 'POST': form = ExpenseForm(request.POST, request.FILES) if form.is_valid(): expense = form.save(commit=False) expense.project = project expense.created_by = request.user expense.save() if request.htmx: return HttpResponse( f'<div id="expense-{expense.id}" class="expense-item">' f'<p>{expense.description} - ${expense.amount}</p></div>' ) messages.success(request, 'Expense added successfully.') return redirect('project_detail', pk=project.pk) else: form = ExpenseForm() context = { 'form': form, 'project': project, } return render(request, 'app/expense_form.html', context)
- Create app/forms.py:
from django import forms from .models import Project, Expense class ProjectForm(forms.ModelForm): class Meta: model = Project fields = ['title', 'description', 'total_budget', 'start_date', 'end_date', 'assigned_to'] widgets = { 'start_date': forms.DateInput(attrs={'type': 'date'}), 'end_date': forms.DateInput(attrs={'type': 'date'}), } class ExpenseForm(forms.ModelForm): class Meta: model = Expense fields = ['description', 'amount', 'category', 'date', 'receipt'] widgets = { 'date': forms.DateInput(attrs={'type': 'date'}), }
Creating Templates
- Create templates/app/dashboard.html:
{% extends "layout/base.html" %} {% block title %}Dashboard - Project Budget Manager{% endblock %} {% block content %} <div class="grid grid-cols-1 md:grid-cols-2 gap-8"> <div> <h2 class="text-2xl font-bold mb-4">Your Projects</h2> {% if user_projects %} {% for project in user_projects %} <div class="bg-white p-6 rounded-lg shadow-md mb-4"> <h3 class="text-xl font-semibold mb-2"> <a href="{% url 'project_detail' pk=project.pk %}" class="text-blue-600 hover:text-blue-800"> {{ project.title }} </a> </h3> <p class="text-gray-600 mb-2">{{ project.description|truncatewords:30 }}</p> <div class="flex justify-between items-center"> <span class="text-sm text-gray-500">Budget: ${{ project.total_budget }}</span> <span class="px-3 py-1 rounded-full text-sm {% if project.status == 'approved' %}bg-green-100 text-green-800 {% elif project.status == 'pending' %}bg-yellow-100 text-yellow-800 {% elif project.status == 'rejected' %}bg-red-100 text-red-800 {% else %}bg-gray-100 text-gray-800{% endif %}"> {{ project.get_status_display }} </span> </div> </div> {% endfor %} {% else %} <p class="text-gray-600">No projects created yet.</p> {% endif %} <a href="{% url 'project_create' %}" class="btn-primary inline-block mt-4"> Create New Project </a> </div> <div> <h2 class="text-2xl font-bold mb-4">Assigned Projects</h2> {% if assigned_projects %} {% for project in assigned_projects %} <div class="bg-white p-6 rounded-lg shadow-md mb-4"> <h3 class="text-xl font-semibold mb-2"> <a href="{% url 'project_detail' pk=project.pk %}" class="text-blue-600 hover:text-blue-800"> {{ project.title }} </a> </h3> <p class="text-gray-600 mb-2">{{ project.description|truncatewords:30 }}</p> <div class="flex justify-between items-center"> <span class="text-sm text-gray-500">Created by: {{ project.created_by.get_full_name|default:project.created_by.username }}</span> <span class="px-3 py-1 rounded-full text-sm {% if project.status == 'approved' %}bg-green-100 text-green-800 {% elif project.status == 'pending' %}bg-yellow-100 text-yellow-800 {% elif project.status == 'rejected' %}bg-red-100 text-red-800 {% else %}bg-gray-100 text-gray-800{% endif %}"> {{ project.get_status_display }} </span> </div> </div> {% endfor %} {% else %} <p class="text-gray-600">No projects assigned to you.</p> {% endif %} </div> </div> {% endblock %}
Setting Up URLs
Update app/urls.py:
from django.urls import path from . import views app_name = 'app' urlpatterns = [ path('', views.dashboard, name='dashboard'), path('projects/', views.project_list, name='project_list'), path('projects/create/', views.project_create, name='project_create'), path('projects/<int:pk>/', views.project_detail, name='project_detail'), path('projects/<int:project_pk>/expenses/create/', views.expense_create, name='expense_create'), ]
Next Steps
In Part 4 of this series, we'll:
- Implement project approval workflow
- Add email notifications
- Create project reports and analytics
- Set up production deployment
Resources
This article is part of the "Building a Project Budget Manager with Django" series. Check out Part 1 and Part 2 if you haven't already!
Top comments (2)
I first encountered Django around 2009. At that time, I found Django very useful, especially for its admin functionality. However, due to various reasons, I switched back to PHP. As PHP frameworks started to emerge, particularly in recent years with the rise of the Laravel framework, I now find that PHP is still more suitable for building websites when I look back at Django.
As long as it serves the purpose, there's no problem sticking with it. At the end of the day, developers are solution-oriented and focused on addressing problems.