Django Fundamentals: Models, Views, and Admin
Django takes a different philosophical approach than Flask—instead of giving you minimal tools and letting you choose everything else, Django provides a comprehensive framework with opinions about how web applications should be built. I initially resisted Django’s “magic,” preferring Flask’s explicitness, but I’ve come to appreciate how Django’s conventions eliminate countless small decisions and let you focus on your application’s unique logic.
The power of Django becomes apparent when you need features like user management, admin interfaces, or complex database relationships. What takes dozens of lines in Flask often requires just a few lines of configuration in Django. This isn’t laziness—it’s leveraging battle-tested solutions that have been refined by thousands of developers over many years.
Django Project Structure and Setup
Django organizes code into projects and apps, where a project contains multiple apps that handle different aspects of functionality. This structure encourages modular design from the beginning, preventing the monolithic applications that can emerge from less opinionated frameworks.
# Create a new Django project
django-admin startproject blogproject
cd blogproject
# Create an app within the project
python manage.py startapp blog
python manage.py startapp accounts
This creates a structure that separates concerns clearly. The project-level directory contains configuration and routing, while each app contains the models, views, and templates specific to that functionality. This separation makes it easy to reuse apps across different projects or extract them into separate packages.
Django’s settings system centralizes configuration in a way that scales from development to production:
# blogproject/settings.py
import os
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
SECRET_KEY = os.environ.get('SECRET_KEY', 'dev-key-change-in-production')
DEBUG = os.environ.get('DEBUG', 'True').lower() == 'true'
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'blog',
'accounts',
]
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
The INSTALLED_APPS
setting tells Django which applications to load. Django’s built-in apps provide authentication, admin interface, and static file handling—features you’d need to implement or find third-party packages for in Flask.
Django Models and the ORM
Django’s ORM is more opinionated than SQLAlchemy but provides powerful features out of the box. Models define both database structure and business logic, with Django handling migrations automatically based on model changes.
# blog/models.py
from django.db import models
from django.contrib.auth.models import User
from django.urls import reverse
class Category(models.Model):
name = models.CharField(max_length=100, unique=True)
slug = models.SlugField(unique=True)
description = models.TextField(blank=True)
class Meta:
verbose_name_plural = "categories"
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('blog:category', kwargs={'slug': self.slug})
class Post(models.Model):
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
content = models.TextField()
author = models.ForeignKey(User, on_delete=models.CASCADE)
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
published = models.BooleanField(default=False)
class Meta:
ordering = ['-created_at']
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse('blog:post_detail', kwargs={'slug': self.slug})
Django models include several conventions that reduce boilerplate code. The __str__
method defines how objects appear in the admin interface and debugging output. The get_absolute_url
method provides a canonical URL for each object, which templates and views can use consistently.
Field types like SlugField
and DateTimeField
provide validation and formatting automatically. The auto_now_add
and auto_now
parameters handle timestamp management without requiring manual intervention in your views.
Django Views and URL Routing
Django views can be functions or classes, with class-based views providing more structure for common patterns. The URL routing system connects URLs to views with named patterns that can be reversed to generate URLs programmatically.
# blog/views.py
from django.shortcuts import render, get_object_or_404, redirect
from django.views.generic import ListView, DetailView
from django.contrib.auth.decorators import login_required
from .models import Post, Category
class PostListView(ListView):
model = Post
template_name = 'blog/post_list.html'
context_object_name = 'posts'
paginate_by = 10
def get_queryset(self):
return Post.objects.filter(published=True).select_related('author', 'category')
class PostDetailView(DetailView):
model = Post
template_name = 'blog/post_detail.html'
context_object_name = 'post'
def get_queryset(self):
return Post.objects.filter(published=True)
@login_required
def create_post(request):
if request.method == 'POST':
title = request.POST['title']
content = request.POST['content']
category_id = request.POST.get('category')
post = Post.objects.create(
title=title,
content=content,
author=request.user,
category_id=category_id if category_id else None
)
return redirect('blog:post_detail', slug=post.slug)
categories = Category.objects.all()
return render(request, 'blog/create_post.html', {'categories': categories})
Class-based views eliminate repetitive code for common patterns. ListView
handles pagination, context creation, and template rendering automatically. You customize behavior by overriding methods like get_queryset()
rather than writing everything from scratch.
The select_related()
method prevents N+1 query problems by fetching related objects in a single database query. This optimization is crucial for performance but easy to forget—Django’s ORM makes it simple to write inefficient queries accidentally.
Django Admin Interface
Django’s admin interface is one of its most compelling features—it provides a complete content management system with minimal configuration. For many applications, the admin interface eliminates the need to build custom administrative tools.
# blog/admin.py
from django.contrib import admin
from .models import Post, Category
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
list_display = ['name', 'slug']
prepopulated_fields = {'slug': ('name',)}
search_fields = ['name']
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_display = ['title', 'author', 'category', 'published', 'created_at']
list_filter = ['published', 'category', 'created_at']
search_fields = ['title', 'content']
prepopulated_fields = {'slug': ('title',)}
date_hierarchy = 'created_at'
def get_queryset(self, request):
return super().get_queryset(request).select_related('author', 'category')
The admin configuration provides powerful features with minimal code. list_display
controls which fields appear in the object list, while list_filter
adds sidebar filters. The prepopulated_fields
setting automatically generates slugs from titles as you type.
Custom admin methods can add computed fields or actions that provide bulk operations on selected objects. These features transform the admin from a simple CRUD interface into a powerful content management tool.
Django Templates and Context
Django’s template system separates presentation from logic more strictly than many frameworks. Templates receive context data from views and can use filters and tags to format and manipulate that data, but they can’t execute arbitrary Python code.
<!-- blog/templates/blog/post_list.html -->
{% extends 'blog/base.html' %}
{% block title %}Latest Posts - My Blog{% endblock %}
{% block content %}
<h1>Latest Posts</h1>
{% for post in posts %}
<article>
<h2><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></h2>
<p>By {{ post.author.username }} in {{ post.category.name|default:"Uncategorized" }}</p>
<p>{{ post.content|truncatewords:50 }}</p>
<small>{{ post.created_at|date:"F j, Y" }}</small>
</article>
{% empty %}
<p>No posts yet.</p>
{% endfor %}
{% if is_paginated %}
<div class="pagination">
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}">« Previous</a>
{% endif %}
<span>Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}">Next »</a>
{% endif %}
</div>
{% endif %}
{% endblock %}
Django templates provide built-in filters like truncatewords
and date
that handle common formatting needs. The {% empty %}
clause in for loops provides fallback content when lists are empty, eliminating the need for separate conditional checks.
The {% url %}
tag generates URLs using the same named patterns defined in your URL configuration. This creates maintainable templates that don’t break when URL structures change.
Moving Forward
In our next part, we’ll explore Django’s forms system and how it integrates with models to provide automatic validation and rendering. You’ll learn about Django’s CSRF protection, form widgets, and how to create custom forms that handle complex validation scenarios.
We’ll also dive into Django’s user authentication system, which provides more built-in functionality than Flask’s approach. Django’s auth system includes user registration, password reset, and permission management out of the box, demonstrating how Django’s “batteries included” philosophy accelerates development.