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!
Top comments (0)