Create a Model
Models are a source of information about the behaviors of your data.
Each model maps to a single database table, where each attribute of the class represents a database field.
A_base/models.py:
from django.db import models
# All Models inherit from django.db.models.Model
class TeamMember(models.Model):
# CharField - Text with a maximum length
name = models.CharField(max_length=100)
role = models.CharField(max_length=100)
# TextField - Text with no maximum length
bio = models.TextField()
# DateField - Stores a date
join_date = models.DateField(auto_now_add=True)
# __str__ - How to display the object in the admin
def __str__(self):
return f"{self.name} - {self.role}"
# Meta - Other information about the model
class Meta:
ordering = ['join_date']
Migrate to the Database
Now that we have the models.py
code, we need to create the database table.
Django comes with a migration system that tracks the changes made to models and inserts them into the database.
Step 1 - Make Migrations:
python manage.py makemigrations
Step 2 - Migrate:
python manage.py migrate
All of the apps from INSTALLED_APPS that we saw in _core/settings.py, including our A_base app will migrate all of their database tables.
Django is Database Agnostic, but it uses sqlite3 by default, automatically creating a db.sqlite3 file in your project's directory.
Introduction to Django Admin
Unlock Django's Built-In Superpowers: The Admin Interface
What is the Admin UI?
○ A pre-built dashboard for managing data (CRUD operations).
○ Automatically generated from your models.
○ Only accessible to users with staff
or superuser
privileges.
Why Use It?
○ Rapidly test database entries during development.
○ Avoid building a custom admin panel from scratch.
○ Manage users, groups, permissions, and app data.
Key Features
○ Search, filter, and sort records.
○ Bulk actions (delete, update).
○ Audit logs (if enabled).
from .models import TeamMember
# Simplest way to register a model
admin.site.register(TeamMember)
python manage.py createsuperuser
Minimal coding needed for basic use—Django does the heavy lifting!
Customizing the Admin
Tailor the Admin to Your Needs
Custom Admin Classes
• Subclass admin.ModelAdmin
to override defaults.
Common Customizations
• list_display
: Show specific fields
• prepopulated_fields
: Auto-generate slugs.
• readonly_fields
: Prevent edits (e.g., join_date
).
from django.contrib import admin
from .models import TeamMember
class TeamMemberAdmin(admin.ModelAdmin):
list_display = ("name", "role", "join_date") # Columns in list view
search_fields = ("name", "role") # Add a search bar
list_filter = ("join_date",) # Filter by date
readonly_fields = ("join_date",) # Make join_date read-only
admin.site.register(TeamMember, TeamMemberAdmin)
Create an 'about' page
Create a new function in A_base/views.py:
from .models import TeamMember
def about_view(request):
team_members = TeamMember.objects.all()
context = {'team_members': team_members}
template = "A_base/about.html"
return render(
request,
template,
context
)
TeamMember.objects.all()
Retrieves a list of every TeamMember instance in your database.
Create a new url path in A_base/urls.py:
urlpatterns = [
path("", views.base_view, name="base"),
path("about/", views.about_view, name="about"),
]
New Template to show Team Members
A_base/templates/A_base/about.html:
{% extends "A_base/base.html" %}
{% block content %}
<h1>About</h1>
<p>This is the about page</p>
<h2>Team Members</h2>
{% for member in team_members %}
<strong>{{ member.name }}</strong>
<span style="opacity: 0.5;">{{ member.role }}</span>
<p style="text-indent: 16px;">{{ member.bio }}</p>
{% endfor %}
{% endblock %}
% extends "A_base/base.html" %
The base html file this extends from. The % block content %} part of this template will alter the same-named block of the extended base template.
% for member in team_members %
Accesses the 'team_members' variable from the view's context. The for loop will create the paragraph & strong element for each member in this list.
{ member.name/.role/.bio }
Accesses and shows the 'name', 'role', and 'bio' fields of each member in the 'team_members' list.
% endfor % and % endblock %
Be sure to end your for loops and blocks, or else you will get an error when you try to load this page.
style="opacity: 0.5;" and "text-indent: 16px;"
A little taste of CSS. Take note of the syntax, the variable name followed by a colon(:) then the value then a semicolon (;)
Preview of our (mostly unstyled) website
Update the nav links in A_base/base.html
% verbatim %}
<nav>
<ul>
<li><a href="% url 'base' %}">Home</a></li>
<li><a href="% url 'about' %}">About</a></li>
</ul>
</nav>
% endverbatim %}
% url 'base' % and % url 'about' %
The 'url' tag will search for the urlpatterns we have defined in A_base/urls.py by their 'name' attribute:
urlpatterns = [
path("", views.base_view, name="base"),
path("about/", views.about_view, name="about"),
]
Notice how the Nav and the Footer are still there, even though it's a new html file? That's because A_base/about.html has extended from A_base/base.html!
Create a New App
Apps are Modular. Lets create a new app for new models.
Type this into your terminal:
python manage.py startapp A_projects
Remember to add to _core/settings.py
#_core/settings.py
INSTALLED_APPS = [
"A_projects",
]
Update _core/urls.py
#_core/urls.py
urlpatterns = [
path("admin/", admin.site.urls),
path("", include("A_base.urls")),
path("projects/", include("A_projects.urls")),
]
Model Relationships
Team Members & Projects: Many-to-Many Relationships
Add a new model to A_projects/models.py
from django.template.defaultfilters import slugify
from A_base.models import TeamMember
class Project(models.Model):
name = models.CharField(max_length=100, unique=True, blank=False)
description = models.TextField()
# ManyToManyField - Many objects can be related to one another
members = models.ManyToManyField(
TeamMember,
related_name='projects', # used to access related objects
blank=True # Allows empty projects
)
start_date = models.DateField(auto_now_add=True)
# SlugField - Used to create a URL
slug = models.SlugField(max_length=100, unique=True, blank=True)
# this function is called when the object is saved
def save(self, *args, **kwargs):
# ensures that there is a slug
if not self.slug:
self.slug = slugify(self.name)
super().save(*args, **kwargs)
class Meta:
ordering = ['start_date']
def save(self, args, kwargs):
This function will be called whenever an instance of this model gets saves to a database. In this case, we ensure that the instance has a slug based on the name.
super().save(args, kwargs)
super() accesses the class that this extends from (models.Model), then we call this parent class' save function AFTER we do our custom save logic. This ensures the model is properly saved.
related_name='projects'
Allows team_member.projects.all() to get all projects for a member.
SlugField and slugify
A slug is a string that is URL-friendly, instead of "This is my Title", slugify transforms it to "this-is-my-title"
Admin Integration
Managing Relationships in Django Admin
Update A_projects/admin.py
from .models import Project
class ProjectAdmin(admin.ModelAdmin):
list_display = ("name", "start_date")
filter_horizontal = ("members",)
prepopulated_fields = {"slug": ("name",)}
search_fields = ("name",)
readonly_fields = ("start_date",)
admin.site.register(Project, ProjectAdmin)
prepopulated_fields
makes a slug auto-generate as you type a name
filter_horizontal
makes the creation UI for handling relationships much better:
![Admin interface showing horizontal filter with two columns - Available members and Chosen members with arrow controls between them]
Hold down "Control", or "Command" on a Mac, to select more than one.
Querying Relationships
Update A_base/templates/A_base/about.html inside the members loop, below the bio
% verbatim %}
<p style="text-indent: 16px;"></p>
% if member.projects.all.count == 1 %}
<p> project</p>
% elif member.projects.all.count > 1 %}
<p> projects</p>
% else %}
<p>No projects</p>
% endif %}
% for project in member.projects.all %}
<p>
<a href="#"></a>
</p>
% endfor %}
% endverbatim %}
% if % and % elif % and % else %
Just like Python, you can use comparison operators like == or > to check conditions!
.projects.all.count
This returns the amount of all the member's projects (remember related_name)
Slug-Based Detail View
Creating Friendly URLs with Slugs
Update A_projects/urls.py
from django.urls import path
from . import views
urlpatterns = [
path(
"<slug:slug>/",
views.project_detail_view,
name="project_detail"
),
]
URL Parameters ():
Matches URL patterns like /projects/my-awesome-project/
slug type ensures URL-safe format (auto-created via slugify)
Create the project_detail_view at A_base/views.py
from django.shortcuts import render, get_object_or_404
from .models import Project
def project_detail_view(request, slug):
project = get_object_or_404(
Project,
slug=slug
)
context = {'project': project}
template = "A_projects/project_detail.html"
return render(
request,
template,
context
)
This view takes the slug from the URL parameter. It then uses this slug to find the corresponding project.
If that project is not found, get_object_or_404 will automatically return an error page.
Project Detail Template
Building the Project Page
Create A_base/templates/A_base/project_detail.html
% verbatim %}
% extends "A_base/base.html" %}
% endverbatim %}
% verbatim %}
% block content %}
<h1></h1>
<p></p>
<h2>Members</h2>
% for member in project.members.all %}
<strong></strong>
<span style="opacity: 0.5;"></span>
<p style="text-indent: 16px;"></p>
% endfor %}
% endblock %}
% endverbatim %}
Update the url in the projects for loop on about.html
% verbatim %}
% for project in member.projects.all %}
<p>
<a href="% url 'project_detail' project.slug %}"></a>
</p>
% endfor %}
% endverbatim %}
This will automatically populate the href with the correct project's full URL based on the slug
Splitting up the templates
Notice we had to write the member info twice? Let's fix that!
A_projects/project_detail.html
A new folder in your templates called "snippets"
Create templates/A_base/snippets/member_info.html:
% verbatim %}
<strong></strong>
<span style="opacity: 0.5;"></span>
<p style="text-indent: 16px;"></p>
% endverbatim %}
Now instead of rewriting this every time we want to show member info, we can use the % include % tag
A_base/about.html:
% verbatim %}
% for member in team_members %}
% include "A_base/snippets/member_info.html" %}
% endfor %}
% endverbatim %}
A_projects/project_detail.html:
% verbatim %}
% for member in project.members.all %}
% include "A_base/snippets/member_info.html" %}
% endfor %}
% endverbatim %}
Splitting your templates up like this is a good habit to get into.
Not only do you no longer have to rewrite the same code, if you ever want to edit how the members are displayed, you now only have to edit one single file rather than two.
Create a list view
A view to list all the projects
Update A_projects/urls.py
urlpatterns = [
path("", views.project_list_view, name="project_list"),
path(
"projects/<slug:slug>/",
views.project_detail_view,
name="project_detail"
),
]
In A_projects/views.py, create a project_list_view
def project_list_view(request):
projects = Project.objects.all()
context = {'projects': projects}
template = "A_projects/project_list.html"
return render(
request,
template,
context
)
Hopefully you are noticing a pattern
First, we create the view/url
Next, we will create the template
Create a list view - 2
A view to list all the projects
Create A_base/templates/A_base/project_list.html
% verbatim %}
% extends "A_base/base.html" %}
% block content %}
<h1>Projects</h1>
% for project in projects %}
<h2><a href="% url 'project_detail' project.slug %}">
</a></h2>
<p></p>
<p> members</p>
% endfor %}
% endblock %}
% endverbatim %}
Add a new link to the nav bar in base.html
<nav>
<ul>
<li><a href="% url 'base' %}">Home</a></li>
<li><a href="% url 'about' %}">About</a></li>
<li><a href="% url 'project_list' %}">Projects</a></li>
</ul>
</nav>
Now our navigation includes links to all main sections of our site.
Model Relationships: ForeignKey
Tasks & Projects: One-to-Many Relationships
Add a new model to A_base/models.py
class Task(models.Model):
STATUS_CHOICES = [
('PENDING', 'Pending'),
('IN_PROGRESS', 'In Progress'),
('COMPLETED', 'Completed'),
('CANCELED', 'Canceled'),
]
name = models.CharField(max_length=100, default="New Task")
description = models.TextField(blank=True)
# ForeignKey - Creates a One-to-Many relationship
project = models.ForeignKey(
Project,
on_delete=models.CASCADE, # If project is deleted, delete related tasks
related_name='tasks' # Access via project.tasks.all()
)
# CharField with choices
status = models.CharField(
max_length=20,
choices=STATUS_CHOICES, # Limits input to these choices
default='PENDING'
)
due_date = models.DateField(null=True, blank=True)
class Meta:
ordering = ['due_date']
def __str__(self):
return f"{self.name} - {self.project.name}"
Choices vs. ForeignKey:
- Choices: Good for predefined options that won't change often (statuses, priorities)
- ForeignKey: Good for relationships to other database tables that may grow
After creating this model, run:
python manage.py makemigrations
python manage.py migrate
Model Relationships: OneToOneField
Extending Django's User Model
Modify the TeamMember model in A_base/models.py to link with Django's built-in User model:
from django.db import models
from django.contrib.auth import get_user_model
# Get the currently active User model
User = get_user_model()
class TeamMember(models.Model):
# Link to Django's User model using best practice
user = models.OneToOneField(
User,
on_delete=models.CASCADE,
related_name='team_profile',
null=True,
blank=True,
)
name = models.CharField(max_length=100)
role = models.CharField(max_length=100)
bio = models.TextField()
join_date = models.DateField(auto_now_add=True)
def __str__(self):
return f"{self.name} - {self.role}"
class Meta:
ordering = ['join_date']
OneToOneField vs. ForeignKey
- OneToOneField: Exactly one related object in each direction
- ForeignKey: Many records can point to the same related object
After modifying the model, run:
python manage.py makemigrations
python manage.py migrate
Enhanced Admin for OneToOne Relationship
Editing the TeamMember A_Base/Admin Interface
from django.contrib import admin
from .models import TeamMember
@admin.register(TeamMember)
class TeamMemberAdmin(admin.ModelAdmin):
list_display = ('name', 'role', 'join_date', 'linked_user')
list_filter = ('role', 'join_date')
search_fields = ('name', 'role', 'user__username')
# Custom field to display in list view
def linked_user(self, obj):
if obj.user:
return obj.user.username
return '(No user linked)'
linked_user.short_description = 'User Account'
# Customize fields shown during editing
fieldsets = (
('Basic Information', {
'fields': ('name', 'role', 'bio')
}),
('User Account', {
'fields': ('user',),
'description': 'Link this team member to a Django user account'
}),
)
# Add autocomplete for User field
autocomplete_fields = ['user']
-
Decorator Registration (
@admin.register(TeamMember)
):
- Alternative toadmin.site.register()
that allows cleaner code
- Registers the model and its admin class in one step -
Custom Methods for Display (
linked_user
):
- Create methods inside admin classes to format or process data
- Useshort_description
to set column header text
- Ideal for derived fields or formatting complex data -
Related Lookups (
search_fields = ('user__username',)
):
- The double-underscore syntax accesses fields in related models
- Enable searching across relationships without extra code -
Fieldsets for Organized Forms:
- Group related fields under logical headings
- Add descriptions to provide context for editors
- Create a more structured editing experience -
Autocomplete for Better UX (
autocomplete_fields
):
- Converts select fields to searchable dropdowns
- Critical for models with many possible relations
- Prevents overwhelming admins with too many choices
Admin Inline Models
Edit Related Tasks Within Projects
Update A_projects/admin.py to add inline task editing to projects:
from django.contrib import admin
from .models import Project, Task
# Create an inline admin model for tasks
class TaskInline(admin.TabularInline):
model = Task
extra = 1 # How many empty forms to show
fields = ('name', 'status', 'due_date')
@admin.register(Project)
class ProjectAdmin(admin.ModelAdmin):
list_display = ("name", "start_date")
filter_horizontal = ("members",)
prepopulated_fields = {"slug": ("name",)}
search_fields = ("name",)
readonly_fields = ("start_date",)
# Add the inline to the Project admin
inlines = [TaskInline]
# No need to register Task separately if we only want to edit it
# within the context of a Project
-
TabularInline vs StackedInline:
-TabularInline
: Compact, table-like format (good for many items)
-StackedInline
: Each related object gets a full form layout (more space) -
Parent-Child Editing:
- The foreign key relationship is automatically handled
- Tasks are always associated with the correct project -
No Need for Separate Registration:
- You don't need to register Task withadmin.site.register()
- This prevents cluttering the admin index with models better edited inline