Custom User Management
- Create a custom user app (users/models.py):
from django.contrib.auth.models import AbstractUser from django.db import models class CustomUser(AbstractUser): """ Custom user model with additional fields """ department = models.CharField(max_length=100, blank=True) position = models.CharField(max_length=100, blank=True) def get_full_name(self): return f"{self.first_name} {self.last_name}"
- Update settings to use custom user (config/settings/base.py):
AUTH_USER_MODEL = 'users.CustomUser'
- Create user forms (users/forms.py):
from django.contrib.auth.forms import UserCreationForm, UserChangeForm from .models import CustomUser class CustomUserCreationForm(UserCreationForm): class Meta: model = CustomUser fields = ('username', 'email', 'department', 'position') class CustomUserChangeForm(UserChangeForm): class Meta: model = CustomUser fields = ('username', 'email', 'department', 'position')
Admin Dashboard Customization
- Create a custom admin dashboard (app/admin.py):
from django.contrib import admin from django.db.models import Sum from django.utils.html import format_html from .models import Project, Expense, ProjectComment @admin.register(Project) class ProjectAdmin(admin.ModelAdmin): list_display = ('title', 'created_by', 'status', 'total_budget', 'budget_usage', 'created_at') list_filter = ('status', 'created_at') search_fields = ('title', 'description', 'created_by__username') readonly_fields = ('created_at', 'updated_at') def budget_usage(self, obj): total_expenses = obj.get_total_expenses() percentage = (total_expenses / obj.total_budget * 100) if obj.total_budget else 0 color = 'green' if percentage <= 75 else 'orange' if percentage <= 100 else 'red' return format_html( '<div style="color: {};">{:.1f}% (${:,.2f} / ${:,.2f})</div>', color, percentage, total_expenses, obj.total_budget ) budget_usage.short_description = 'Budget Usage' @admin.register(Expense) class ExpenseAdmin(admin.ModelAdmin): list_display = ('description', 'project', 'amount', 'category', 'date') list_filter = ('category', 'date', 'project') search_fields = ('description', 'project__title') date_hierarchy = 'date' def get_queryset(self, request): qs = super().get_queryset(request) if not request.user.is_superuser: qs = qs.filter(project__created_by=request.user) return qs
Utility Functions
- Create email utilities (app/utils/emails.py):
from django.core.mail import send_mail from django.template.loader import render_to_string from django.conf import settings def send_project_status_notification(project, recipient_list): """ Send email notification when project status changes Args: project: Project instance that was updated recipient_list: List of email addresses to notify """ context = { 'project': project, 'status': project.get_status_display(), } # Render email templates html_message = render_to_string( 'account/email/project_status_update.html', context ) plain_message = render_to_string( 'account/email/project_status_update.txt', context ) send_mail( subject=f'Project Status Update: {project.title}', message=plain_message, html_message=html_message, from_email=settings.DEFAULT_FROM_EMAIL, recipient_list=recipient_list )
JavaScript Integration
- Create project form handling (static/js/project-form.js):
document.addEventListener('DOMContentLoaded', function() { // Initialize date pickers const datePickers = document.querySelectorAll('input[type="date"]'); datePickers.forEach(picker => { // Add any date picker initialization here }); // Budget calculation const budgetInput = document.getElementById('id_total_budget'); const expenseInputs = document.querySelectorAll('.expense-amount'); function updateTotalExpenses() { const total = Array.from(expenseInputs) .reduce((sum, input) => sum + (parseFloat(input.value) || 0), 0); document.getElementById('total-expenses').textContent = total.toFixed(2); const budget = parseFloat(budgetInput.value) || 0; const remaining = budget - total; const remainingElement = document.getElementById('budget-remaining'); remainingElement.textContent = remaining.toFixed(2); remainingElement.classList.toggle('text-red-600', remaining < 0); remainingElement.classList.toggle('text-green-600', remaining >= 0); } expenseInputs.forEach(input => { input.addEventListener('change', updateTotalExpenses); }); budgetInput.addEventListener('change', updateTotalExpenses); });
- HTMX Integration (templates/projects/detail.html):
{% extends "layout/dashboard/layout.html" %} {% load static %} {% block extra_head %} <script src="{% static 'js/htmx.min.js' %}" defer></script> {% endblock %} {% block content %} <div class="container mx-auto px-4"> <!-- Quick Add Expense Form --> <form hx-post="{% url 'expense_create' project.id %}" hx-target="#expense-list" hx-swap="afterbegin" class="mb-8"> {% csrf_token %} <div class="flex gap-4"> <input type="text" name="description" placeholder="Expense description" class="form-input flex-1"> <input type="number" name="amount" placeholder="Amount" class="form-input w-32"> <button type="submit" class="btn-primary"> Add Expense </button> </div> </form> <!-- Expense List --> <div id="expense-list"> {% for expense in expenses %} {% include "expenses/_expense_item.html" %} {% endfor %} </div> </div> {% endblock %}
Tailwind Configuration
- Update tailwind.config.js:
/** @type {import('tailwindcss').Config} */ module.exports = { content: [ "./templates/**/*.html", "./static/**/*.js", ], theme: { extend: { colors: { primary: { 50: '#f0f9ff', 100: '#e0f2fe', // ... other shades 900: '#0c4a6e', }, }, spacing: { '128': '32rem', }, }, }, plugins: [ require('@tailwindcss/forms'), ], }
- Create build script in package.json:
{ "scripts": { "build": "tailwindcss -i ./static/css/input.css -o ./static/css/output.css --watch" }, "dependencies": { "tailwindcss": "^3.0.0", "@tailwindcss/forms": "^0.5.0" } }
Testing
- Create tests for models (app/tests/test_models.py):
from django.test import TestCase from django.contrib.auth import get_user_model from app.models import Project, Expense from decimal import Decimal User = get_user_model() class ProjectTests(TestCase): def setUp(self): self.user = User.objects.create_user( username='testuser', password='testpass123' ) self.project = Project.objects.create( title='Test Project', description='Test Description', created_by=self.user, total_budget=Decimal('1000.00') ) def test_project_creation(self): self.assertEqual(self.project.title, 'Test Project') self.assertEqual(self.project.status, 'draft') self.assertEqual(self.project.get_total_expenses(), Decimal('0')) def test_budget_calculations(self): Expense.objects.create( project=self.project, description='Test Expense', amount=Decimal('500.00'), category='materials', created_by=self.user ) self.assertEqual(self.project.get_total_expenses(), Decimal('500.00')) self.assertEqual(self.project.get_budget_remaining(), Decimal('500.00')) self.assertFalse(self.project.is_over_budget())
Security Enhancements
- Add security middleware (config/settings/production.py):
MIDDLEWARE += [ 'django.middleware.security.SecurityMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] # Security settings SECURE_BROWSER_XSS_FILTER = True SECURE_CONTENT_TYPE_NOSNIFF = True X_FRAME_OPTIONS = 'DENY' CSRF_COOKIE_SECURE = True SESSION_COOKIE_SECURE = True SECURE_SSL_REDIRECT = True SECURE_HSTS_SECONDS = 31536000 # 1 year SECURE_HSTS_INCLUDE_SUBDOMAINS = True SECURE_HSTS_PRELOAD = True
Resources
- Django Testing Documentation
- HTMX Documentation
- Tailwind CSS Documentation
- Django Security Best Practices
This article is part of the "Building a Project Budget Manager with Django" series. Check out Part 1, Part 2,Part 3, Part 4, and Part 5 if you haven't already!
Top comments (0)