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 }}">&laquo; 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 &raquo;</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.