Data Modeling in Django

Module 23: Web Frameworks II (Python) - Wednesday, Lecture 2

Introduction to Django Models

Django models are the heart of any Django application, serving as the definitive source for your data structure and business logic. A model represents a database table, and each attribute of the model represents a database field.

What makes Django's model system powerful is that it:

The Blueprint Analogy

Think of Django models as blueprints for a building. Just as a blueprint defines the structure, dimensions, and characteristics of a building before construction begins, a Django model defines the structure, fields, and behaviors of your data before it's stored in the database. Once the blueprint (model) is finalized, construction (database creation) can proceed, resulting in a physical building (database table) that matches the specifications.

Basic Model Structure

Django models are Python classes that subclass django.db.models.Model. Each model class represents a database table, and each attribute represents a database field.

A Simple Model Example

from django.db import models

class Book(models.Model):
    title = models.CharField(max_length=200)
    author = models.CharField(max_length=100)
    description = models.TextField()
    publication_date = models.DateField()
    price = models.DecimalField(max_digits=6, decimal_places=2)
    is_published = models.BooleanField(default=True)
    cover_image = models.ImageField(upload_to='covers/', blank=True, null=True)
    
    def __str__(self):
        return self.title

When Django processes this model:

  1. It creates a database table named appname_book (you can customize the table name)
  2. It creates columns for each field in the model
  3. It adds an id field as a primary key automatically (unless you specify a different field)
  4. It creates a database-abstraction API accessible through the model class and instances
classDiagram class Model { +objects +save() +delete() +__str__() } class Book { +id: AutoField +title: CharField +author: CharField +description: TextField +publication_date: DateField +price: DecimalField +is_published: BooleanField +cover_image: ImageField +__str__() } Model <|-- Book

Field Types

Django provides a rich set of field types to represent different types of data:

Common Field Types

Field Type Database Type Description Example
CharField VARCHAR String field with a maximum length title = models.CharField(max_length=100)
TextField TEXT Unlimited text field content = models.TextField()
IntegerField INTEGER Integer field age = models.IntegerField()
DecimalField DECIMAL Fixed-precision decimal number price = models.DecimalField(max_digits=6, decimal_places=2)
BooleanField BOOLEAN True/False field is_active = models.BooleanField(default=True)
DateField DATE Date field (without time) birth_date = models.DateField()
DateTimeField DATETIME Date and time field created_at = models.DateTimeField(auto_now_add=True)
EmailField VARCHAR CharField that validates as an email address email = models.EmailField()
FileField VARCHAR File upload field document = models.FileField(upload_to='documents/')
ImageField VARCHAR Image upload field with validation photo = models.ImageField(upload_to='photos/')
URLField VARCHAR CharField that validates as a URL website = models.URLField()
SlugField VARCHAR CharField that only allows letters, numbers, underscores, and hyphens slug = models.SlugField()
JSONField JSON Stores JSON-encoded data metadata = models.JSONField()

Real-World Example: User Profile Model

from django.db import models
from django.contrib.auth.models import User

class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    bio = models.TextField(blank=True)
    birth_date = models.DateField(null=True, blank=True)
    profile_image = models.ImageField(upload_to='profile_pics/', default='profile_pics/default.jpg')
    location = models.CharField(max_length=100, blank=True)
    website = models.URLField(blank=True)
    joined_at = models.DateTimeField(auto_now_add=True)
    preferences = models.JSONField(default=dict)
    
    def __str__(self):
        return f"{self.user.username}'s profile"

Field Options

Each field type takes a set of field-specific arguments. There are also common arguments available to all field types:

Common Field Options

Option Description Example
null If True, Django will store empty values as NULL in the database age = models.IntegerField(null=True)
blank If True, the field is allowed to be blank in forms bio = models.TextField(blank=True)
default The default value for the field is_active = models.BooleanField(default=True)
help_text Additional help text to be displayed with the form widget username = models.CharField(help_text="Enter your username")
primary_key If True, this field is the primary key for the model id = models.UUIDField(primary_key=True, default=uuid.uuid4)
unique If True, this field must be unique throughout the table email = models.EmailField(unique=True)
verbose_name A human-readable name for the field first_name = models.CharField(verbose_name="First Name")
validators A list of validators to run for this field code = models.CharField(validators=[validate_code])
choices A list of choices for this field status = models.CharField(choices=STATUS_CHOICES)
db_index If True, a database index will be created for this field last_name = models.CharField(db_index=True)

Example: Using choices

class Book(models.Model):
    GENRE_CHOICES = [
        ('SCI', 'Science Fiction'),
        ('DRA', 'Drama'),
        ('MYS', 'Mystery'),
        ('ROM', 'Romance'),
        ('HOR', 'Horror'),
        ('THR', 'Thriller'),
        ('NON', 'Non-Fiction'),
    ]
    
    title = models.CharField(max_length=200)
    genre = models.CharField(max_length=3, choices=GENRE_CHOICES, default='MYS')
    
    def get_genre_display(self):
        return dict(self.GENRE_CHOICES)[self.genre]

With choices, Django provides a get_<fieldname>_display() method that returns the "human-readable" value of the field:

book = Book.objects.get(id=1)
print(book.genre)  # "MYS"
print(book.get_genre_display())  # "Mystery"

Model Relationships

Django provides three types of relationships between models:

1. One-to-Many (ForeignKey)

A many-to-one relationship. For example, many books can have the same author.

class Author(models.Model):
    name = models.CharField(max_length=100)
    bio = models.TextField()
    
    def __str__(self):
        return self.name

class Book(models.Model):
    title = models.CharField(max_length=200)
    author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name='books')
    # other fields...
    
    def __str__(self):
        return self.title

The on_delete parameter specifies what happens when the referenced object is deleted:

The related_name parameter allows you to access the relationship from the related object:

# Get all books by an author
author = Author.objects.get(name="J.K. Rowling")
books = author.books.all()  # This works because of related_name='books'

2. Many-to-Many (ManyToManyField)

A many-to-many relationship. For example, a book can have multiple categories, and a category can contain multiple books.

class Category(models.Model):
    name = models.CharField(max_length=50)
    
    def __str__(self):
        return self.name

class Book(models.Model):
    title = models.CharField(max_length=200)
    # other fields...
    categories = models.ManyToManyField(Category, related_name='books')
    
    def __str__(self):
        return self.title

You can add a custom "through" model to include additional fields in the relationship:

class BookCategory(models.Model):
    book = models.ForeignKey(Book, on_delete=models.CASCADE)
    category = models.ForeignKey(Category, on_delete=models.CASCADE)
    added_on = models.DateTimeField(auto_now_add=True)
    featured = models.BooleanField(default=False)

class Book(models.Model):
    # other fields...
    categories = models.ManyToManyField(Category, through='BookCategory', related_name='books')

3. One-to-One (OneToOneField)

A one-to-one relationship. For example, a user has one profile, and a profile belongs to one user.

from django.contrib.auth.models import User

class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    bio = models.TextField(blank=True)
    birth_date = models.DateField(null=True, blank=True)
    # other fields...
    
    def __str__(self):
        return f"{self.user.username}'s profile"
erDiagram AUTHOR ||--o{ BOOK : writes BOOK }o--o{ CATEGORY : belongs_to USER ||--|| PROFILE : has AUTHOR { int id string name text bio } BOOK { int id string title int author_id } CATEGORY { int id string name } USER { int id string username string email } PROFILE { int id int user_id text bio date birth_date }

Creating and Modifying Models

After defining your models, you need to create the corresponding database tables. Django uses a migration system to track changes to your models and apply them to the database schema.

Migration Workflow

  1. Add or modify model classes in models.py
  2. Create migrations with python manage.py makemigrations
  3. Apply migrations with python manage.py migrate
graph LR A[Define/Modify Models] --> B[python manage.py makemigrations] B -->|Creates migration files| C[Migration Files] C --> D[python manage.py migrate] D -->|Updates database schema| E[(Database)]

Understanding Migrations

Migrations are Django's way of propagating changes to your models into your database schema. They're like version control for your database schema, allowing you to:

Migration Commands

# Create migrations for all apps with model changes
python manage.py makemigrations

# Create migrations for a specific app
python manage.py makemigrations myapp

# Apply all pending migrations
python manage.py migrate

# Apply migrations for a specific app
python manage.py migrate myapp

# Show migration status
python manage.py showmigrations

# Show the SQL that would be executed by a migration
python manage.py sqlmigrate myapp 0001

# Revert a specific migration
python manage.py migrate myapp 0001

Migration Best Practices

  • Create migrations in small, incremental steps
  • Include migrations in version control
  • Test migrations before applying them to production
  • Use meaningful names for migrations: python manage.py makemigrations --name=create_book_model
  • Consider data migrations when changing field types: python manage.py makemigrations --empty myapp

Model Inheritance

Django supports three types of model inheritance:

1. Abstract Base Classes

Create a parent class with common fields that won't be used directly.

class BaseItem(models.Model):
    name = models.CharField(max_length=100)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    
    class Meta:
        abstract = True  # This model won't be used to create a database table
        
class Book(BaseItem):
    author = models.CharField(max_length=100)
    isbn = models.CharField(max_length=13)
    
class DVD(BaseItem):
    director = models.CharField(max_length=100)
    duration = models.IntegerField()  # In minutes

This creates two tables: app_book and app_dvd, each with its own fields plus the fields from BaseItem.

2. Multi-table Inheritance

Each model has its own database table, with a link to the parent table.

class Product(models.Model):
    name = models.CharField(max_length=100)
    price = models.DecimalField(max_digits=10, decimal_places=2)
    description = models.TextField()
    
class Book(Product):  # Inherits from Product
    author = models.CharField(max_length=100)
    pages = models.IntegerField()
    
class Electronics(Product):  # Also inherits from Product
    brand = models.CharField(max_length=100)
    warranty_period = models.IntegerField()  # In months

This creates three tables: app_product, app_book, and app_electronics. The child tables have a one-to-one relationship with the parent table.

3. Proxy Models

Create a different interface for the same underlying database table.

class Person(models.Model):
    name = models.CharField(max_length=100)
    date_of_birth = models.DateField()
    
    def get_age(self):
        today = date.today()
        return today.year - self.date_of_birth.year - (
            (today.month, today.day) < (self.date_of_birth.month, self.date_of_birth.day)
        )

class Student(Person):
    class Meta:
        proxy = True  # Uses the same database table as Person
        
    def enroll(self, course):
        # Student-specific behavior
        pass
        
class Teacher(Person):
    class Meta:
        proxy = True  # Also uses the same database table as Person
        
    def assign_grade(self, student, grade):
        # Teacher-specific behavior
        pass

This creates only one table: app_person. The proxy models provide different behaviors but use the same underlying table.

Model Meta Options

The Meta class within a model provides metadata about the model, such as ordering, constraints, and table names.

class Book(models.Model):
    title = models.CharField(max_length=200)
    author = models.ForeignKey(Author, on_delete=models.CASCADE)
    publication_date = models.DateField()
    
    class Meta:
        # Database table name (default: app_book)
        db_table = 'books'
        
        # Default ordering
        ordering = ['-publication_date', 'title']
        
        # Unique constraint
        unique_together = ['title', 'author']
        
        # Index definitions
        indexes = [
            models.Index(fields=['publication_date']),
            models.Index(fields=['author', 'title']),
        ]
        
        # Permissions
        permissions = [
            ('can_publish', 'Can publish books'),
            ('can_unpublish', 'Can unpublish books'),
        ]
        
        # Verbose names for the model
        verbose_name = 'Book'
        verbose_name_plural = 'Books'

Common Meta Options

Option Description Example
db_table The name of the database table db_table = 'books'
ordering Default ordering for queries ordering = ['-created_at']
unique_together Sets of fields that must be unique together unique_together = ['slug', 'author']
indexes Database indexes for the model indexes = [models.Index(fields=['title'])]
constraints Database constraints constraints = [models.CheckConstraint(...)]
abstract Whether the model is abstract abstract = True
proxy Whether the model is a proxy model proxy = True
verbose_name Human-readable singular name verbose_name = 'Blog Post'
verbose_name_plural Human-readable plural name verbose_name_plural = 'Blog Posts'
permissions Extra permissions for the model permissions = [('publish_post', 'Can publish posts')]
default_permissions Default permissions for the model default_permissions = ('add', 'change', 'delete')

Model Methods

Models can include methods to encapsulate business logic and common operations:

Common Model Methods

from django.utils import timezone
from django.urls import reverse

class Post(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    author = models.ForeignKey('auth.User', on_delete=models.CASCADE)
    created_at = models.DateTimeField(auto_now_add=True)
    published_at = models.DateTimeField(null=True, blank=True)
    is_published = models.BooleanField(default=False)
    
    # String representation
    def __str__(self):
        return self.title
    
    # Custom method for model logic
    def publish(self):
        self.published_at = timezone.now()
        self.is_published = True
        self.save()
    
    # Method to check status
    def is_recent(self):
        if not self.published_at:
            return False
        return (timezone.now() - self.published_at).days < 7
    
    # Method to get absolute URL
    def get_absolute_url(self):
        return reverse('post_detail', kwargs={'pk': self.pk})
        
    # Method to calculate related data
    def comment_count(self):
        return self.comments.count()  # Assuming a related_name='comments' on Comment model
        
    # Method to filter related data
    def approved_comments(self):
        return self.comments.filter(approved=True)

Special Model Methods

Method Description
__str__(self) Returns a string representation of the object. Used in the admin, shell, and templates.
save(self, *args, **kwargs) Override to add custom saving behavior.
delete(self, *args, **kwargs) Override to add custom deletion behavior.
get_absolute_url(self) Returns the URL for displaying the object.
clean(self) Used for model validation. Called by forms.
full_clean(self) Calls clean on all fields and model.

Example: Overriding save() for Auto-Slugify

from django.db import models
from django.utils.text import slugify

class Article(models.Model):
    title = models.CharField(max_length=200)
    slug = models.SlugField(unique=True, max_length=200)
    content = models.TextField()
    
    def __str__(self):
        return self.title
    
    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(self.title)
            
        # Check for duplicate slugs and make unique
        original_slug = self.slug
        counter = 1
        while Article.objects.filter(slug=self.slug).exclude(pk=self.pk).exists():
            self.slug = f"{original_slug}-{counter}"
            counter += 1
            
        super().save(*args, **kwargs)

Model Managers

Model managers control the database operations for models. Each model has at least one manager, and you can create custom managers for specialized queries.

The Default Manager

By default, Django adds a manager named objects to every model:

# Using the default objects manager
all_articles = Article.objects.all()
featured_articles = Article.objects.filter(featured=True)
recent_article = Article.objects.order_by('-created_at').first()

Custom Managers

You can create custom managers to encapsulate query logic:

class PublishedManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(status='published')
    
    def recent(self):
        return self.get_queryset().order_by('-published_at')[:5]

class Article(models.Model):
    STATUS_CHOICES = [
        ('draft', 'Draft'),
        ('published', 'Published'),
    ]
    
    title = models.CharField(max_length=200)
    content = models.TextField()
    status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='draft')
    published_at = models.DateTimeField(null=True, blank=True)
    
    # Default manager
    objects = models.Manager()
    
    # Custom manager
    published = PublishedManager()
    
    def __str__(self):
        return self.title

Now you can use the custom manager:

# Get all published articles
published_articles = Article.published.all()

# Get recent published articles
recent_articles = Article.published.recent()

Manager Methods

You can also add custom methods to managers:

class BookManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset()
    
    def fiction(self):
        return self.filter(genre='fiction')
    
    def non_fiction(self):
        return self.filter(genre='non-fiction')
    
    def by_author(self, author_name):
        return self.filter(author__name__icontains=author_name)
    
    def published_this_year(self):
        from django.utils import timezone
        year = timezone.now().year
        return self.filter(publication_date__year=year)

class Book(models.Model):
    title = models.CharField(max_length=200)
    author = models.ForeignKey('Author', on_delete=models.CASCADE)
    genre = models.CharField(max_length=50)
    publication_date = models.DateField()
    
    objects = BookManager()
    
    def __str__(self):
        return self.title

Using the custom manager methods:

# Get fiction books
fiction_books = Book.objects.fiction()

# Get books by a specific author
tolkien_books = Book.objects.by_author('Tolkien')

# Get books published this year
new_books = Book.objects.published_this_year()

Querying Models

Django provides a powerful API for querying models. Here are some common query operations:

Basic Queries

# Get all records
all_books = Book.objects.all()

# Get a single record by primary key
book = Book.objects.get(pk=1)

# Filter records
fiction_books = Book.objects.filter(genre='fiction')

# Exclude records
non_fiction_books = Book.objects.exclude(genre='fiction')

# Order records
sorted_books = Book.objects.order_by('title')
recent_first = Book.objects.order_by('-publication_date')

# Limit records
first_five = Book.objects.all()[:5]
pagination = Book.objects.all()[10:20]  # Items 10-19

Field Lookups

Django's powerful field lookups allow for complex filtering:

# Exact match (case-sensitive)
Book.objects.filter(title__exact="The Hobbit")

# Case-insensitive match
Book.objects.filter(title__iexact="the hobbit")

# Contains (case-sensitive)
Book.objects.filter(title__contains="Hobbit")

# Case-insensitive contains
Book.objects.filter(title__icontains="hobbit")

# Greater than
Book.objects.filter(publication_date__gt='2020-01-01')

# Less than or equal to
Book.objects.filter(publication_date__lte='2020-12-31')

# In a list
Book.objects.filter(genre__in=['fiction', 'fantasy', 'adventure'])

# Range
Book.objects.filter(publication_date__range=('2020-01-01', '2020-12-31'))

# Startswith/endswith
Book.objects.filter(title__startswith='The')
Book.objects.filter(title__endswith='Rings')

# Null check
Book.objects.filter(summary__isnull=True)

# Regex matching
Book.objects.filter(title__regex=r'^The.*')

Complex Queries with Q Objects

For complex queries with logical operators, use Q objects:

from django.db.models import Q

# OR condition (fiction OR fantasy)
Book.objects.filter(Q(genre='fiction') | Q(genre='fantasy'))

# AND condition (fiction AND published after 2020)
Book.objects.filter(Q(genre='fiction') & Q(publication_date__gte='2020-01-01'))

# NOT condition (not fiction)
Book.objects.filter(~Q(genre='fiction'))

# Complex combinations
Book.objects.filter(
    (Q(genre='fiction') | Q(genre='fantasy')) &
    Q(publication_date__year=2020) &
    ~Q(title__contains='Harry Potter')
)

Aggregation and Annotation

Django provides methods for aggregating and annotating querysets:

from django.db.models import Count, Avg, Min, Max, Sum

# Count all books
book_count = Book.objects.count()

# Aggregations
stats = Book.objects.aggregate(
    avg_price=Avg('price'),
    max_price=Max('price'),
    min_price=Min('price'),
    total_value=Sum('price')
)

# Annotations (adds calculated fields to each object)
authors_with_book_count = Author.objects.annotate(
    book_count=Count('books')
)

# Find authors with more than 5 books
prolific_authors = Author.objects.annotate(
    book_count=Count('books')
).filter(book_count__gt=5)

Model Forms

Django's forms framework can automatically create forms from your models:

from django import forms
from .models import Book

class BookForm(forms.ModelForm):
    class Meta:
        model = Book
        fields = ['title', 'author', 'genre', 'publication_date', 'price']
        # Or exclude some fields
        # exclude = ['created_at', 'updated_at']
        
        # Customize widgets
        widgets = {
            'publication_date': forms.DateInput(attrs={'type': 'date'}),
            'genre': forms.Select(choices=Book.GENRE_CHOICES),
            'price': forms.NumberInput(attrs={'step': '0.01', 'min': '0'}),
        }
        
        # Add help text
        help_texts = {
            'title': 'Enter the full title of the book',
            'price': 'Price in USD',
        }
        
        # Custom error messages
        error_messages = {
            'title': {
                'required': 'Please enter a title for the book',
                'max_length': 'The title is too long',
            },
        }
        
        # Custom labels
        labels = {
            'publication_date': 'Release Date',
        }

Using the model form in a view:

from django.shortcuts import render, redirect, get_object_or_404
from .forms import BookForm
from .models import Book

def create_book(request):
    if request.method == 'POST':
        form = BookForm(request.POST)
        if form.is_valid():
            book = form.save()  # Save the form data to the database
            return redirect('book_detail', pk=book.pk)
    else:
        form = BookForm()
    
    return render(request, 'books/create_book.html', {'form': form})

def edit_book(request, pk):
    book = get_object_or_404(Book, pk=pk)
    
    if request.method == 'POST':
        form = BookForm(request.POST, instance=book)
        if form.is_valid():
            book = form.save()
            return redirect('book_detail', pk=book.pk)
    else:
        form = BookForm(instance=book)
    
    return render(request, 'books/edit_book.html', {'form': form, 'book': book})

Data Modeling Best Practices

Here are some best practices for designing Django models:

Schema Design

Model Organization

Performance Considerations

  • Avoid large text fields: Use CharField instead of TextField when possible
  • Limit the use of auto_now and auto_now_add: These create database writes on every save
  • Be careful with related fields: Use select_related() and prefetch_related() to optimize queries
  • Use bulk operations: bulk_create(), bulk_update(), and update() are more efficient
  • Index wisely: Don't over-index, but add indexes to fields used in filtering and sorting

Real-World Example: Blog Application Models

Let's examine a comprehensive example of models for a blog application:

from django.db import models
from django.contrib.auth.models import User
from django.urls import reverse
from django.utils import timezone
from django.utils.text import slugify

class Category(models.Model):
    name = models.CharField(max_length=50, unique=True)
    slug = models.SlugField(max_length=50, unique=True)
    description = models.TextField(blank=True)
    
    class Meta:
        verbose_name_plural = 'Categories'
        ordering = ['name']
    
    def __str__(self):
        return self.name
    
    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(self.name)
        super().save(*args, **kwargs)
    
    def get_absolute_url(self):
        return reverse('blog:category_detail', kwargs={'slug': self.slug})

class Tag(models.Model):
    name = models.CharField(max_length=50, unique=True)
    slug = models.SlugField(max_length=50, unique=True)
    
    class Meta:
        ordering = ['name']
    
    def __str__(self):
        return self.name
    
    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(self.name)
        super().save(*args, **kwargs)

class Post(models.Model):
    STATUS_CHOICES = [
        ('draft', 'Draft'),
        ('published', 'Published'),
    ]
    
    title = models.CharField(max_length=200)
    slug = models.SlugField(max_length=200, unique_for_date='published_at')
    author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='blog_posts')
    category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, related_name='posts')
    tags = models.ManyToManyField(Tag, blank=True, related_name='posts')
    content = models.TextField()
    excerpt = models.TextField(blank=True)
    featured_image = models.ImageField(upload_to='blog/%Y/%m/', blank=True)
    
    status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='draft')
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    published_at = models.DateTimeField(null=True, blank=True)
    
    featured = models.BooleanField(default=False)
    views = models.PositiveIntegerField(default=0)
    
    class Meta:
        ordering = ['-published_at', '-created_at']
        indexes = [
            models.Index(fields=['-published_at']),
            models.Index(fields=['status']),
        ]
    
    def __str__(self):
        return self.title
    
    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(self.title)
        
        if self.status == 'published' and not self.published_at:
            self.published_at = timezone.now()
            
        if not self.excerpt and self.content:
            # Create excerpt from content (first 160 characters)
            self.excerpt = self.content[:160] + '...' if len(self.content) > 160 else self.content
            
        super().save(*args, **kwargs)
    
    def get_absolute_url(self):
        return reverse('blog:post_detail', kwargs={
            'year': self.published_at.year,
            'month': self.published_at.month,
            'day': self.published_at.day,
            'slug': self.slug
        })
    
    def increment_views(self):
        self.views += 1
        self.save(update_fields=['views'])
        
class PublishedManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(status='published', published_at__lte=timezone.now())
        
    def featured(self):
        return self.get_queryset().filter(featured=True)
        
    def popular(self, limit=5):
        return self.get_queryset().order_by('-views')[:limit]

class Post(Post):  # Extending the Post model with a custom manager
    objects = models.Manager()  # The default manager
    published = PublishedManager()  # Custom manager
    
    class Meta:
        proxy = True

class Comment(models.Model):
    post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='comments')
    author_name = models.CharField(max_length=100)
    author_email = models.EmailField()
    author = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='blog_comments')
    content = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    approved = models.BooleanField(default=False)
    
    parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, related_name='replies')
    
    class Meta:
        ordering = ['created_at']
        indexes = [
            models.Index(fields=['post', 'approved']),
        ]
    
    def __str__(self):
        return f'Comment by {self.author_name} on {self.post}'
        
    def approve(self):
        self.approved = True
        self.save(update_fields=['approved'])
        
class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    bio = models.TextField(blank=True)
    profile_image = models.ImageField(upload_to='profiles/', blank=True)
    website = models.URLField(blank=True)
    social_twitter = models.CharField(max_length=255, blank=True)
    social_facebook = models.CharField(max_length=255, blank=True)
    social_instagram = models.CharField(max_length=255, blank=True)
    social_linkedin = models.CharField(max_length=255, blank=True)
    
    def __str__(self):
        return f'Profile for {self.user.username}'
        
    def get_absolute_url(self):
        return reverse('blog:author_detail', kwargs={'username': self.user.username})

This example demonstrates a comprehensive blog application model structure with:

Practice Activity

Design and implement a data model for a simple e-commerce application with the following requirements:

  1. The application should have products organized into categories
  2. Products can have multiple images and attributes (e.g., color, size)
  3. Customers can create accounts, place orders, and leave reviews
  4. Orders consist of multiple items and have different statuses
  5. The application should track inventory for each product

Create the following models:

For each model:

  1. Define appropriate fields with types and options
  2. Set up relationships between models
  3. Add at least one custom method
  4. Configure Meta options

Further Topics to Explore