Claude Fable 5.1 & GPT-6 Astra packages are live

Django

Free · MIT

Django project structure — app boundaries, models and migration discipline, queryset hygiene that avoids N+1, forms and serializers, split settings,…

237 lines9.3 KB Deepseek Backend
Target models
DeepSeek V4DeepSeek V3.2DeepSeek R1DeepSeek V3 FamilyFuture DeepSeek Models
Name
django
Category
Backend
Description
Django project structure — app boundaries, models and migration discipline, queryset hygiene that avoids N+1, forms and serializers, split settings, the security middleware that must stay on, and an admin that stays fast.
License
MIT
Author
Agent.md maintainers
Last verified
2026-09-13
Reviewed by
unreviewed

#Task boundary

  1. Implement exactly the task as stated. Do not add abstractions, options, config, or files the task did not name.
  2. Comments, identifiers, commit messages and log strings are English only.
  3. Stop when the checklist at the end passes. Do not refactor or "improve" surrounding code.
  4. Every checklist item below is backed by an assertion in a test or by pasted command output, never by a sentence.

#Purpose

Rules for a Django codebase that stays coherent past the third app. Django's batteries are good; the failure mode is fighting them — hand-rolled auth, raw SQL where the ORM would do, business logic in views.

Python-level conventions are Backend/python-conventions; REST specifics are API/rest.


#Layout

arduino
config/
  settings/base.py  dev.py  prod.py     # split, not one file with `if DEBUG`
  urls.py  asgi.py  wsgi.py
apps/
  orders/
    models.py  services.py  selectors.py  admin.py  urls.py  views.py
    migrations/  tests/
  1. Each app owns one bounded concept. If orders/models.py imports from billing/models.py and vice versa, you have one app pretending to be two.
  2. services.py holds writes (create/update with rules), selectors.py holds reads. Views parse, call, format — nothing else.
  3. DJANGO_SETTINGS_MODULE=config.settings.prod in production; dev.py imports * from base.py and overrides.

#Models and migrations

python
class Order(models.Model):
    tenant = models.ForeignKey("tenants.Tenant", on_delete=models.PROTECT)
    status = models.CharField(max_length=16, choices=Status.choices, db_index=True)
    total_cents = models.PositiveIntegerField()
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        constraints = [
            models.CheckConstraint(check=Q(total_cents__gte=0), name="order_total_nonneg"),
            models.UniqueConstraint(fields=["tenant", "external_id"], name="order_ext_id_uniq"),
        ]
  1. Every ForeignKey names its on_delete deliberately. CASCADE on a tenant or user deletes history; PROTECT makes the deletion a decision.
  2. Invariants go in Meta.constraints, enforced by the database, not only in clean(). → Database/schema-design
  3. Money is integer cents, never FloatField.
  4. makemigrations output is reviewed like code: rename operations, data migrations, and index additions on large tables (AddIndexConcurrently on Postgres) are where deploys go wrong. Never edit an applied migration.
  5. Data migrations use apps.get_model(), not the live model import — the live model may have fields the migration's schema does not.

#Querysets — the N+1 rule

python
# N+1: one query for orders, one per order for its customer
for order in Order.objects.filter(tenant=t):
    print(order.customer.email)

# Fixed
orders = (Order.objects.filter(tenant=t)
          .select_related("customer")                 # FK / one-to-one → JOIN
          .prefetch_related("items__product")         # reverse / many → 2nd query
          .only("id", "status", "customer__email"))
  1. select_related for forward foreign keys; prefetch_related for reverse relations and many-to-many. Using the wrong one either explodes the row count or issues N queries.
  2. Assert query counts in tests: with self.assertNumQueries(2):. A view that passes at 3 queries and fails at 300 in production is an N+1 you did not test.
  3. .exists() not len(qs); .count() not len(list(qs)); .update() for bulk writes instead of a loop of .save().
  4. .iterator(chunk_size=2000) for exports; a plain loop over a million rows caches every instance in memory.
  5. Never call the ORM in a template tag or a model __str__ that the admin lists — that is a hidden N+1.

#Views, forms, serializers

python
@require_POST
@login_required
def create_order(request: HttpRequest) -> HttpResponse:
    form = OrderForm(request.POST)
    if not form.is_valid():
        return render(request, "orders/new.html", {"form": form}, status=400)
    order = orders.services.create_order(actor=request.user, **form.cleaned_data)
    return redirect(order)
  1. Validation lives in a Form/ModelForm (HTML) or a DRF Serializer (JSON). Reading request.POST["x"] directly skips validation and type coercion.
  2. Views never contain business rules. A rule in a view is unavailable to the management command, the Celery task, and the test that needs it.
  3. Class-based views for CRUD that fits the generic ones; function views for anything with branching. Do not subclass View five levels deep.
  4. get_object_or_404 with the tenant filter applied — a lookup by pk alone is an IDOR. → Security/authorization

#Security middleware and settings

Django ships these on. Keep them on, and set the ones that are off:

SettingProduction value
DEBUGFalse — a True leaks settings and source on every 500
ALLOWED_HOSTSExplicit list, never ["*"]
SECRET_KEYFrom the environment; rotated if ever committed
SECURE_SSL_REDIRECT, SECURE_HSTS_SECONDSTrue, 31536000
SESSION_COOKIE_SECURE, CSRF_COOKIE_SECURETrue
SECURE_PROXY_SSL_HEADER("HTTP_X_FORWARDED_PROTO", "https") behind a proxy
CsrfViewMiddlewarePresent — @csrf_exempt is a code review item

Run python manage.py check --deploy in CI; it fails on most of the above. → Security/headers


#Admin

python
@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
    list_display = ("id", "status", "customer_email", "created_at")
    list_select_related = ("customer",)
    list_filter = ("status",)
    search_fields = ("id", "customer__email")
    raw_id_fields = ("customer",)
    readonly_fields = ("created_at",)
  1. list_select_related for every FK shown in list_display, or the changelist is one query per row.
  2. raw_id_fields/autocomplete_fields for any FK with more than a few hundred rows; the default <select> renders every one.
  3. The admin is a staff tool, not a public API. Put it on a non-default path and behind SSO or an allowlist in production.

#Testing

python
class CreateOrderTests(TestCase):
    def test_rejects_other_tenants_customer(self):
        other = CustomerFactory(tenant=TenantFactory())
        with self.assertNumQueries(1):
            with self.assertRaises(PermissionDenied):
                create_order(actor=self.user, customer_id=other.id, items=[...])

TestCase wraps each test in a transaction and rolls back — fast and isolated. Use TransactionTestCase only when testing transaction behaviour itself. Build data with factories, not fixtures files. → Testing/pytest


#Anti-patterns

Anti-patternWhy it failsFix
One settings.py with if DEBUG:Prod and dev drift silentlySplit settings modules
Business rules in viewsUnreachable from tasks and testsservices.py
on_delete=CASCADE on tenant/userDeletes historyPROTECT
Invariants only in clean()Bypassed by .update() and shellMeta.constraints
Loop over queryset touching FKsN+1select_related/prefetch_related
len(qs) for a countLoads every row.count() / .exists()
Editing an applied migrationOther environments divergeNew migration
Live model import in data migrationSchema mismatchapps.get_model()
FloatField for moneyRounding errorsInteger cents
request.POST["x"] in a viewNo validation or coercionForm / serializer
get_object_or_404(Order, pk=pk)IDOR across tenantsFilter by tenant
ALLOWED_HOSTS = ["*"]Host header attacksExplicit list
@csrf_exempt on a session-auth viewCSRFKeep the middleware
Admin FK without raw_id_fieldsRenders every row in a selectautocomplete_fields
TransactionTestCase everywhere10× slower suiteTestCase

#Checklist

  • Settings split into base/dev/prod modules
  • Each app owns one concept; no circular model imports
  • Writes go through services.py; views hold no business rules
  • Every ForeignKey sets on_delete deliberately
  • Invariants enforced with Meta.constraints
  • Money stored as integer cents
  • Migrations reviewed; applied migrations never edited
  • Data migrations use apps.get_model()
  • List views use select_related/prefetch_related; query counts asserted
  • Bulk operations use .update(), .bulk_create(), .iterator()
  • All input passes through a Form or Serializer
  • Object lookups are scoped to the tenant
  • manage.py check --deploy passes in CI
  • Admin changelists set list_select_related and raw_id_fields
  • Tests use TestCase and factories