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.