Django development standards for the Tellurium Games World of Darkness application...
The rules a change must follow to match the current architecture of this repository, and
the checklist a reviewer runs against a diff. Each rule names its reason and, where one
exists, the test that enforces it. Details live in references/; explanations live in
the project docs they link to (docs/architecture/, docs/guides/, <app>/docs/).
The code is the source of truth. If a rule here disagrees with the code or a guard test, trust the code, then fix this skill.
Not for game rules or sheet content (use wod-toolkit) or generic Django refactoring
(use django-simplifier).
accounts, characters, core, game,
items, locations, widgets). Their migrations/ hold only __init__.py;
.gitignore ignores the rest. Why: test and fresh databases build these tables from
the current models; generated files are per install.tg_schema migration: new
column, table, index or constraint, rename, data backfill, reordered chargen steps.
Number it NNNN_snake_name.py and depend on the previous one. Why: it is the only
committed path that reaches databases created before the change.tg_schema migration, import no app model and ignore the apps argument. Look
models up with tg_schema.schema.live_model / live_field, or name tables in raw SQL.
Why: a later rename must skip the old migration, not crash every migrate
(tg_schema/tests/test_schema_helpers.py enforces both).add_missing_columns), return when a model, field, table or column is gone, guard
DDL by introspection, backfill only rows that still need it, reverse with
RunPython.noop.tg_schema migration in tg_schema/tests/test_<name>.py.Human (or CharacterModel) subclasses for characters,
Group for player groups, ItemModel, LocationModel, core.models.Model for other
owned or approvable objects and most reference data, ValidatedSaveMixin +
models.Model for plain records. Why: ownership, approval, visibility, public cards
and routing are built on these bases.type (snake_case, unique in its app) and gameline (a key of
settings.GAMELINES) as class attributes on every core.models.Model subclass.
Why: GenericCharacterDetailView and characters.chargen.get_workflow dispatch on
type; theming, headings and scoped ST roles use gameline.settings.GAMELINE_CHOICES /
settings.GAMELINES for gamelines, core.constants.CharacterStatus / ImageStatus /
XPApprovalStatus for statuses. Never hard-code a gameline or status list.clean() (collect a field-keyed dict, raise one ValidationError);
save() runs full_clean(). Use save(skip_validation=True) or QuerySet.update()
only in migrations, fixtures and bulk jobs.on_delete=SET_NULL (with null=True) for links to rows that live on their own;
CASCADE only for rows that exist for their parent. Name every constraint.get_absolute_url, get_update_url and get_creation_url.
Items and locations inherit them from the registry (RegistryURLMixin); reference
models may use URLMethodsMixin.select_for_update() inside
transaction.atomic().core/route_policy_manifest.py or its registry ActionSpec.policy, never both, and no
stale entries. Why: AuthorizationMiddleware denies undeclared views, and
core/tests/security/test_route_policies.py fails on missing, doubled or stale ones.PUBLIC_READ to read, STAFF_WRITE
to write. Player objects: OBJECT_LIST, OBJECT_DETAIL, OBJECT_CREATE,
OBJECT_WRITE, OBJECT_ST_WRITE, CHARGEN_STEP. One-object state changes: ACTION.PermissionManager (always pass request=), through
core.mixins or an action's permission / has_permission, never in view bodies or
templates. Templates read the object_perms booleans.visibility
admits it (PUB, or CHR in a readable chronicle), never its private fields;
PRI requires full access; a 403 only follows a successful view check. Why: a 403 or a
different 404 confirms the object exists.owner, chronicle, gameline, status, npc, xp,
freebies_approved, approved, approved_by through a write route; everyone
else, storytellers included, goes through a dedicated action or service. Owner-editable
update views use ScopedEditFormMixin with a limited_form_class. Why: the
OBJECT_* field guard in core/access_policy.py rejects the change anyway.core.actions.ObjectActionView), and no view dispatches on posted button names
(core/tests/test_action_guard.py).form_invalid never calls form_valid (characters/tests/test_view_rules_guard.py).characters/services/, core/services/,
game/) and models make the change; views authorize, bind and render.get_object_or_404, select_related / prefetch_related and
.with_polymorphic_ctype(); stay inside the query ceilings
(core/tests/test_query_budgets.py).core.models.Model subclasses include MessageMixin (or come from
the registry). Why: it runs prepare_created_object, which sets the owner, checks
the chronicle and sets the starting status.fields = [...] or a form's Meta.fields; never
"__all__", exclude or introspection. Character CRUD lists live in
characters/forms/core/crud_fields.py and are pinned by a baseline.items/registry.py /
locations/registry.py (core/tests/test_model_registry.py). New routes use
<int:pk> and a name in the app's gameline and action namespaces
(characters:vampire:update:clan).core/tl_base.html, core/form.html, core/object.html
or an area shell. No Bootstrap, jQuery, tg-card, inline scripts, inline
style="..." or <style> blocks (the last two are ratcheted by
core/tests/test_template_policy.py). CSS goes in core/static/core/tl/tl.css,
JavaScript in <app>/static/<app>/, page data in json_script or data-*.{% block gameline %}{{ object|gameline_code }}{% endblock %}, render
fields with core/tl/field.html, dots and tracks with {% load tl %} tags.|sanitize_html for rich text, never |safe.core.cache.cache_page_per_visitor (or CachedDetailView /
CachedListView), never Django's cache_page, and only pages that read no query
string and render no CSRF token unless they hold a form. Why: cache_page serves
one visitor's page to the next, and a CSRF token keeps a page out of the shared
anonymous copy.<app>/tests/, mirroring the source path: models,
forms, views, a denial test for each new route, a migration test for each tg_schema
migration.Run the sections that match the diff. Every unchecked box is a finding.
Any model change
type unique and gameline valid on polymorphic subclasses.Meta.verbose_name, verbose_name_plural, ordering where lists need it; __str__.core.constants; no hard-coded gameline or status lists.clean() validates cross-field rules; constraints are named and have messages.on_delete and related_name chosen deliberately; hot FKs indexed.Schema change
migrations/ besides __init__.py.tg_schema migration, linear dependency, no app-model imports, apps unused.tg_schema/tests/test_<name>.py covers: adds when missing, no SQL when present,
backfill result, skip on rename.New or changed route
ObjectActionView.select_related / prefetch_related, no per-row queries in
templates.Form
readable_chronicles(user).core/tl/field.html.crud_fields.py; baseline JSON updated on purpose.Template
gameline block; uses tl tags and partials.<style>, inline scripts, Bootstrap, jQuery or tg-card.{% static %}; user text through |sanitize_html.object_perms, not role logic.mark_fragment) and varied (vary_on_htmx).Cache
cache_page_per_visitor or the cached view classes; no query-string reads;
no {% csrf_token %} on a cached page without a form.Tests and style
pre-commit run --all-files clean.| File | Read it when |
|---|---|
| models.md | Adding or changing any model, field, manager or relation |
| schema-changes.md | Anything that changes tables, columns, constraints or stored data |
| validation.md | Writing clean(), constraints, transactions, status changes |
| permissions.md | Choosing a route policy, checking object access, limited forms |
| views.md | Writing a view, action endpoint, router or htmx fragment |
| forms.md | Writing a form or a character CRUD field list |
| urls.md | Adding a route or a URL name |
| registry.md | Adding or changing an item or location type |
| templates.md | Writing a template, partial, tag use, htmx markup |
| spread.md | Spread blocks, components, tokens and page structure |
| caching.md | Caching a page, a function result or a reference list |
| testing.md | Writing or placing tests, fixtures, budgets, browser tests |
| code-style.md | Formatting, linting, imports, pre-commit |
| commands.md | Writing a management command |
| character-templates.md | Working on CharacterTemplate and its views |
| model-inventory.md | Finding where a model family lives and its base |
| domain.md | Game terms mapped to models, codes and statuses |
| deployment.md | Making a change deployable (settings, static, schema) |