Models & the admin

A Django model is a Python class that maps to a database table. Define it once and you get the table, a query API, forms and an admin screen for free.

A model worth shipping

# shop/models.py
from django.db import models
from django.urls import reverse


class Category(models.Model):
    name = models.CharField(max_length=60, unique=True)
    slug = models.SlugField(max_length=60, unique=True)

    class Meta:
        verbose_name_plural = "categories"
        ordering = ["name"]

    def __str__(self):
        return self.name


class Product(models.Model):
    name = models.CharField(max_length=120)
    slug = models.SlugField(max_length=140, unique=True)
    category = models.ForeignKey(Category, on_delete=models.PROTECT, related_name="products")
    price = models.DecimalField(max_digits=10, decimal_places=2)
    gst_rate = models.DecimalField(max_digits=4, decimal_places=2, default=18)
    in_stock = models.BooleanField(default=True)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ["-created_at"]
        indexes = [models.Index(fields=["category", "in_stock"])]
        constraints = [
            models.CheckConstraint(condition=models.Q(price__gt=0), name="price_positive"),
        ]

    def __str__(self):
        return self.name

    def price_with_gst(self):
        return self.price * (1 + self.gst_rate / 100)

    def get_absolute_url(self):
        return reverse("shop:product_detail", args=[self.slug])

CheckConstraint(condition=...) is the Django 5.1 spelling; older code used check=. Note DecimalField for money — never FloatField, which will quietly turn 1999.99 into 1999.9899999.

Choosing fields and delete behaviour

Need Field
Short text CharField(max_length=...)
Long text TextField()
Money DecimalField(max_digits, decimal_places)
Whole numbers IntegerField, PositiveIntegerField
Yes/no BooleanField
Timestamps DateTimeField(auto_now_add=True) for created, auto_now=True for updated
Links between tables ForeignKey, ManyToManyField, OneToOneField

on_delete is a business decision: CASCADE deletes the children with the parent (order items with the order), PROTECT refuses to delete a category that still has products, SET_NULL keeps the row and blanks the link.

Migrations

python manage.py makemigrations shop
python manage.py sqlmigrate shop 0001      # see the SQL before it runs
python manage.py migrate
python manage.py createsuperuser

Migration files are code: commit them, review them, and never edit a migration that has already run on a shared database — write a new one instead.

An admin people can actually work in

# shop/admin.py
from django.contrib import admin
from .models import Category, Product


@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
    list_display = ("name", "category", "price", "in_stock", "created_at")
    list_filter = ("in_stock", "category")
    search_fields = ("name", "slug")
    list_editable = ("price", "in_stock")
    list_select_related = ("category",)
    prepopulated_fields = {"slug": ("name",)}
    date_hierarchy = "created_at"
    actions = ["mark_out_of_stock"]

    @admin.action(description="Mark selected products out of stock")
    def mark_out_of_stock(self, request, queryset):
        updated = queryset.update(in_stock=False)
        self.message_user(request, f"{updated} products marked out of stock.")


admin.site.register(Category)

Ten minutes of ModelAdmin configuration often removes the need for an internal CRUD app entirely. Use admin.TabularInline to edit order lines inside the order screen.

Common mistakes

  • FloatField for prices; rounding errors show up in GST totals.
  • No __str__, so every dropdown reads "Product object (17)".
  • Forgetting related_name, then writing product_set everywhere.
  • Changing a field and not running makemigrations, so production and code drift apart.
  • Giving staff full admin access instead of a group with only the permissions they need.

Practice

Model a sweet shop: Category, Sweet (price, weight in grams, shelf life days) and Batch (sweet, made_on, quantity). Add a constraint that quantity cannot be negative, register all three in the admin with useful list_display and filters, and add an action that marks a batch as expired.

B

Written by Bablu Kumar Jha

Founder, BeeProton AI · Mumbai, Maharashtra, India