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)