Introduction
In this part, we'll set up user authentication and create our database models. We'll use Django's built-in authentication system and create custom models for our project management system. This guide assumes you're a beginner, so we'll explain each step in detail.
Setting Up Authentication
Django provides a robust authentication system out of the box. Let's configure it:
- First, ensure these apps are in your
INSTALLED_APPS
(they should be there by default):
# config/settings.py
INSTALLED_APPS = [
# ...
'django.contrib.auth', # Core authentication framework
'django.contrib.contenttypes', # Django content type system
# ...
]
# Authentication settings
LOGIN_REDIRECT_URL = 'dashboard' # Where to redirect after login
LOGOUT_REDIRECT_URL = 'home' # Where to redirect after logout
LOGIN_URL = 'login' # URL name for the login page
- Create user-related URLs:
# config/urls.py
from django.contrib import admin
from django.urls import path, include
from django.contrib.auth import views as auth_views
urlpatterns = [
path('admin/', admin.site.urls),
# Authentication URLs
path('login/', auth_views.LoginView.as_view(
template_name='account/login.html', # Our custom login template
redirect_authenticated_user=True # Redirect if already logged in
), name='login'),
path('logout/', auth_views.LogoutView.as_view(), name='logout'),
path('password-change/', auth_views.PasswordChangeView.as_view(
template_name='account/password_change.html',
success_url='/password-change/done/'
), name='password_change'),
path('password-change/done/', auth_views.PasswordChangeDoneView.as_view(
template_name='account/password_change_done.html'
), name='password_change_done'),
# Our app URLs
path('', include('app.urls')),
]
Creating the Models
Let's create our database models. We'll need models for Projects, Categories, and Expenses:
# app/models.py
from django.db import models
from django.conf import settings
from django.urls import reverse
from django.utils import timezone
from decimal import Decimal
class Category(models.Model):
"""
Categories for organizing expenses (e.g., 'Marketing', 'Development', 'Operations')
Fields:
name: The category name
description: Optional description of the category
created_at: When the category was created
created_by: User who created the category
"""
name = models.CharField(
max_length=100,
unique=True,
help_text="Enter a category name (e.g., Marketing)"
)
description = models.TextField(
blank=True,
help_text="Optional: Provide more details about this category"
)
created_at = models.DateTimeField(
auto_now_add=True,
help_text="When this category was created"
)
created_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.PROTECT, # Don't delete category if user is deleted
related_name='categories',
help_text="User who created this category"
)
class Meta:
verbose_name_plural = "Categories"
ordering = ['name'] # Sort alphabetically by name
def __str__(self):
"""String representation of the category"""
return self.name
def get_absolute_url(self):
"""Get the URL for this category's detail view"""
return reverse('category_detail', kwargs={'pk': self.pk})
class Project(models.Model):
"""
Main project model for tracking budgets and expenses
Fields:
name: Project name
description: Project description
budget: Total budget allocated
start_date: When the project starts
end_date: When the project ends
status: Current project status
created_by: User who created the project
team_members: Users assigned to this project
"""
# Status choices for projects
STATUS_CHOICES = [
('planning', 'Planning'),
('in_progress', 'In Progress'),
('completed', 'Completed'),
('on_hold', 'On Hold'),
('cancelled', 'Cancelled'),
]
name = models.CharField(
max_length=200,
help_text="Enter the project name"
)
description = models.TextField(
help_text="Describe the project and its objectives"
)
budget = models.DecimalField(
max_digits=10,
decimal_places=2,
help_text="Total budget allocated for this project"
)
start_date = models.DateField(
help_text="When does this project start?"
)
end_date = models.DateField(
help_text="When should this project be completed?"
)
status = models.CharField(
max_length=20,
choices=STATUS_CHOICES,
default='planning',
help_text="Current status of the project"
)
created_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.PROTECT,
related_name='created_projects',
help_text="User who created this project"
)
team_members = models.ManyToManyField(
settings.AUTH_USER_MODEL,
related_name='assigned_projects',
blank=True,
help_text="Users assigned to this project"
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['-created_at'] # Newest first
def __str__(self):
"""String representation of the project"""
return f"{self.name} ({self.get_status_display()})"
def get_absolute_url(self):
"""Get the URL for this project's detail view"""
return reverse('project_detail', kwargs={'pk': self.pk})
def get_budget_remaining(self):
"""
Calculate remaining budget
Returns:
Decimal: Amount of budget remaining
"""
total_expenses = self.expenses.aggregate(
total=models.Sum('amount')
)['total'] or Decimal('0')
return self.budget - total_expenses
def is_over_budget(self):
"""Check if project has exceeded its budget"""
return self.get_budget_remaining() < 0
def get_completion_percentage(self):
"""
Calculate project completion percentage based on expenses
Returns:
float: Percentage of budget used (0-100)
"""
if self.budget <= 0:
return 0
used = (self.budget - self.get_budget_remaining()) / self.budget * 100
return min(used, 100) # Cap at 100%
class Expense(models.Model):
"""
Track individual expenses within a project
Fields:
project: Project this expense belongs to
category: Type of expense
description: What this expense is for
amount: How much was spent
date: When it was spent
receipt: Optional receipt image
"""
project = models.ForeignKey(
Project,
on_delete=models.CASCADE, # Delete expenses if project is deleted
related_name='expenses',
help_text="Project this expense belongs to"
)
category = models.ForeignKey(
Category,
on_delete=models.PROTECT,
related_name='expenses',
help_text="Type of expense"
)
description = models.TextField(
help_text="What is this expense for?"
)
amount = models.DecimalField(
max_digits=10,
decimal_places=2,
help_text="Amount spent"
)
date = models.DateField(
default=timezone.now,
help_text="When was this expense incurred?"
)
receipt = models.ImageField(
upload_to='receipts/',
blank=True,
null=True,
help_text="Upload a receipt image (optional)"
)
created_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.PROTECT,
related_name='expenses',
help_text="User who recorded this expense"
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['-date', '-created_at'] # Most recent first
def __str__(self):
"""String representation of the expense"""
return f"{self.category.name}: {self.amount} ({self.date})"
def get_absolute_url(self):
"""Get the URL for this expense's detail view"""
return reverse('expense_detail', kwargs={'pk': self.pk})
def is_recent(self):
"""Check if expense is from the last 7 days"""
return timezone.now().date() - self.date <= timezone.timedelta(days=7)
Model Forms
Create forms for our models:
# app/forms.py
from django import forms
from .models import Project, Expense, Category
class ProjectForm(forms.ModelForm):
"""
Form for creating and editing projects
Features:
- Date picker widgets for start and end dates
- Multi-select for team members
- Custom validation for dates and budget
"""
class Meta:
model = Project
fields = [
'name', 'description', 'budget',
'start_date', 'end_date', 'status',
'team_members'
]
widgets = {
# Use date picker widgets
'start_date': forms.DateInput(attrs={'type': 'date'}),
'end_date': forms.DateInput(attrs={'type': 'date'}),
# Use multi-select for team members
'team_members': forms.SelectMultiple(attrs={
'class': 'select2', # For enhanced select widget
}),
}
def clean(self):
"""
Custom validation:
- End date must be after start date
- Budget must be positive
"""
cleaned_data = super().clean()
start_date = cleaned_data.get('start_date')
end_date = cleaned_data.get('end_date')
budget = cleaned_data.get('budget')
if start_date and end_date and end_date < start_date:
raise forms.ValidationError(
"End date cannot be before start date"
)
if budget and budget <= 0:
raise forms.ValidationError(
"Budget must be greater than zero"
)
return cleaned_data
class ExpenseForm(forms.ModelForm):
"""
Form for recording expenses
Features:
- Date picker for expense date
- File upload for receipts
- Custom validation for amount
"""
class Meta:
model = Expense
fields = [
'project', 'category', 'description',
'amount', 'date', 'receipt'
]
widgets = {
'date': forms.DateInput(attrs={'type': 'date'}),
'description': forms.Textarea(attrs={'rows': 3}),
}
def __init__(self, *args, **kwargs):
"""Initialize form with user's projects only"""
user = kwargs.pop('user', None)
super().__init__(*args, **kwargs)
if user:
# Show only projects the user is involved with
self.fields['project'].queryset = Project.objects.filter(
models.Q(created_by=user) |
models.Q(team_members=user)
).distinct()
def clean_amount(self):
"""Ensure expense amount is positive"""
amount = self.cleaned_data.get('amount')
if amount and amount <= 0:
raise forms.ValidationError(
"Expense amount must be greater than zero"
)
return amount
Admin Interface
Customize the admin interface for our models:
# app/admin.py
from django.contrib import admin
from .models import Project, Expense, Category
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
"""Admin interface for categories"""
list_display = ['name', 'created_by', 'created_at']
search_fields = ['name', 'description']
list_filter = ['created_at']
@admin.register(Project)
class ProjectAdmin(admin.ModelAdmin):
"""Admin interface for projects"""
list_display = [
'name', 'status', 'budget',
'start_date', 'end_date', 'created_by'
]
list_filter = ['status', 'start_date', 'end_date']
search_fields = ['name', 'description']
filter_horizontal = ['team_members']
# Custom columns
def get_budget_used(self, obj):
"""Show percentage of budget used"""
return f"{obj.get_completion_percentage():.1f}%"
get_budget_used.short_description = "Budget Used"
@admin.register(Expense)
class ExpenseAdmin(admin.ModelAdmin):
"""Admin interface for expenses"""
list_display = [
'project', 'category', 'amount',
'date', 'created_by'
]
list_filter = ['project', 'category', 'date']
search_fields = [
'description', 'project__name',
'category__name'
]
date_hierarchy = 'date'
Migrations
After creating our models, we need to create and apply migrations:
# Create migrations
python manage.py makemigrations app
# Review the migrations (optional but recommended)
python manage.py sqlmigrate app 0001
# Apply migrations
python manage.py migrate
Testing the Models
Create some basic tests:
# app/tests/test_models.py
from django.test import TestCase
from django.contrib.auth import get_user_model
from django.utils import timezone
from decimal import Decimal
from ..models import Project, Category, Expense
class ProjectModelTests(TestCase):
"""Test cases for the Project model"""
def setUp(self):
"""Set up test data"""
# Create a test user
self.user = get_user_model().objects.create_user(
username='testuser',
password='testpass123'
)
# Create a test project
self.project = Project.objects.create(
name='Test Project',
description='A test project',
budget=Decimal('1000.00'),
start_date=timezone.now().date(),
end_date=timezone.now().date(),
created_by=self.user
)
# Create a test category
self.category = Category.objects.create(
name='Test Category',
created_by=self.user
)
def test_project_budget_calculations(self):
"""Test budget calculations"""
# Create some expenses
Expense.objects.create(
project=self.project,
category=self.category,
amount=Decimal('300.00'),
description='Test Expense 1',
created_by=self.user
)
Expense.objects.create(
project=self.project,
category=self.category,
amount=Decimal('200.00'),
description='Test Expense 2',
created_by=self.user
)
# Test calculations
self.assertEqual(
self.project.get_budget_remaining(),
Decimal('500.00')
)
self.assertEqual(
self.project.get_completion_percentage(),
50.0
)
self.assertFalse(self.project.is_over_budget())
Next Steps
In Part 3, we'll create views and templates to interact with our models. We'll build:
- Project listing and detail views
- Forms for creating and editing projects
- Expense tracking interface
- Dashboard with budget summaries
Common Issues and Solutions
-
Migration conflicts
- Delete all migrations and start fresh
- Use
python manage.py migrate --fake
if needed
-
Related name conflicts
- Ensure unique related_name for each relationship
- Use descriptive names like 'created_projects'
-
Form validation errors
- Check form.errors in views
- Use clean() method for cross-field validation
Additional Resources
This article is part of the "Building a Project Budget Manager with Django" series. Check out Part 1 if you haven't already!