DEV Community

ngemuantony
ngemuantony

Posted on • Edited on

Building a Project Budget Manager with Django - Part 6: Advanced Features

Custom User Management

  1. 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}"
Enter fullscreen mode Exit fullscreen mode
  1. Update settings to use custom user (config/settings/base.py):
AUTH_USER_MODEL = 'users.CustomUser'
Enter fullscreen mode Exit fullscreen mode
  1. 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')
Enter fullscreen mode Exit fullscreen mode

Admin Dashboard Customization

  1. 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
Enter fullscreen mode Exit fullscreen mode

Utility Functions

  1. 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
    )
Enter fullscreen mode Exit fullscreen mode

JavaScript Integration

  1. 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);
});
Enter fullscreen mode Exit fullscreen mode
  1. 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 %}
Enter fullscreen mode Exit fullscreen mode

Tailwind Configuration

  1. 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'),
  ],
}
Enter fullscreen mode Exit fullscreen mode
  1. 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"
  }
}
Enter fullscreen mode Exit fullscreen mode

Testing

  1. 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())
Enter fullscreen mode Exit fullscreen mode

Security Enhancements

  1. 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
Enter fullscreen mode Exit fullscreen mode

Resources


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!

Heroku

Deploy with ease. Manage efficiently. Scale faster.

Leave the infrastructure headaches to us, while you focus on pushing boundaries, realizing your vision, and making a lasting impression on your users.

Get Started

Top comments (0)

AWS Security LIVE!

Join us for AWS Security LIVE!

Discover the future of cloud security. Tune in live for trends, tips, and solutions from AWS and AWS Partners.

Learn More

👋 Kindness is contagious

If this post resonated with you, feel free to hit ❤️ or leave a quick comment to share your thoughts!

Okay