Conventions¶
Decisions that hold across the codebase. If you're adding something and you're not sure how, the answer is probably here.
Identifiers¶
- Every model has a UUID PK. Period.
- Slugs are used for human URLs (org, tenant, tag). Generated via
slugifyon save.
Tenant scoping¶
- Every domain model has
tenant = FK(Tenant, on_delete=CASCADE). - Every view starts with
tenant = _get_active_tenant(request). - Every queryset starts with
.filter(tenant=tenant). - Never trust a UUID in a URL to belong to the current tenant. Use
get_object_or_404(Model, pk=pk, tenant=tenant).
VRF handling¶
Prefix.vrfandIPAddress.vrfare nullable (NULL= Global).- Uniqueness uses
nulls_distinct=Falseso NULL behaves like a value. - Children inherit parent's VRF on create (form sets initial).
Naming¶
| Thing | Convention | Example |
|---|---|---|
| Models | PascalCase singular |
Prefix, IPAddress |
| ViewSets / functions | snake_case |
prefix_create, prefixes_list |
| URLs | kebab-case | /prefixes/<uuid>/edit/ |
| URL names | <resource> or <resource>-<action> |
api:prefix-detail, api:prefixes-export-csv |
| Templates | <app>/<resource>.html or <app>/<resource>_<action>.html |
api/prefixes.html, api/prefix_detail.html |
| Partial templates | leading _ |
api/_shell.html, api/_prefix_row.html |
| Internal helpers | leading _ |
_get_active_tenant, _autospawn_gateway |
Forms¶
- ModelForms inherit a
tenant=kwarg in__init__to scope select querysets. - All validation in
clean_<field>orclean()— never in the view. - Show clean errors that suggest what to do next: not just "duplicate", but
"A prefix with CIDR
Xalready exists in VRF 'production'. Pick a different VRF or CIDR."
Linked objects on detail pages¶
Detail pages show their related objects as inline tables, not "View X →"
links that navigate away. Reuse the embedded-table components
(EmbeddedDeviceTable, EmbeddedDeviceTypeTable, and EmbeddedIpTable /
EmbeddedRackTable / EmbeddedClusterTable in embedded-tables.tsx): each
takes a filter object of /api/ query params and renders a DataTable in
a tab. The list endpoints carry the matching filters (device device_type/
role/platform/manufacturer/site/location; ip role/status/vrf; rack
role/location; cluster type/group). A count-stat-only page gains a
drill-in tab; a "View X" link becomes the table itself.
Picking objects (SPA)¶
Never hand-roll a FormCombobox + ?picker=1 options block for a domain
object. Use the generic <ObjectPicker spec …/>
(frontend/src/components/object-picker.tsx) or, better, one of its presets —
DevicePicker, RackPicker, VlanPicker, PrefixPicker,
IpPicker (the last two prove the no-name case — optionLabel renders
CIDR / address · DNS). Each is a searchable
combobox (shared compact-picker cache) plus a sliders button opening an
advanced-search dialog: free text + server-side filter selects + a paginated
result table (250/page), all driven by an ObjectPickerSpec (endpoints,
filters, columns, optional row/option ghosting). The Field contract matches
FormCombobox, so call sites swap 1:1; excludeIds hides rows, quickAdd
forwards a "+" control. New picker = ~60-line spec file; the DevicePicker
extras (VC ghosting via ?with_vc=1) show how type-specific behavior rides on
optionState/rowState.
/api/devices/ supports these filter params (all tenant/RBAC-scoped): search,
site, location, device_type, role, status, rack, platform,
manufacturer (via the device's type),
region (includes descendant regions — Region is a plain adjacency-list
tree, walked in Python since there's no MPTT), and tag (repeatable, AND
semantics). Add ?picker=1 for the compact {id, name} shape; omit it for the
full list serializer the advanced table needs. A generic <ObjectPicker> can
later be extracted from DevicePicker for other object types.
Faceplate rendering (SPA)¶
The front-panel drawing is millimetre-true. All physical constants live in
frontend/src/lib/faceplate-geometry.ts (CONNECTOR_MM per connector family,
PANEL_MM EIA-310 numbers, familyForType(slug) mapping the NetBox-style
type taxonomy → connector family, renderTemplateName mirroring the backend's
{position} resolver; {module} resolves to the module bay's position at
install time — see render_module_name). Layout logic lives in
frontend/src/lib/faceplate-layout.ts: one JSON doc schema (FaceplateDoc
v1: front/rear group arrays → slots referencing component-template names,
never pixels) is both computed by autoLayout() (front only) and saved by the
Faceplate builder to DeviceType.faceplate (JSONB, null = auto; validated in
DeviceTypeSerializer.validate_faceplate). resolveLayout(doc, side, …)
matches one side against a device's components; unmatched slots render as
ghosts, uncovered interfaces append as auto groups on the front; a port lives
on exactly one side. Groups carry an optional u (1-based rack-unit lane;
multi-U types get one builder lane + "+" zone per U and render stacked lanes). Never hardcode port pixel sizes — extend
CONNECTOR_MM/familyForType instead. The eighth component kind, Aux
port (AuxPort/AuxPortTemplate, aux_port_types taxonomy), models
USB/video/SD/grounding connectors.
Templates¶
- Pages extend
api/_shell.html(sidebar + topbar shell). - Repeated row markup → extract to
_<thing>_row.htmlpartial. - Inline Lucide-style SVGs at 16×16 (
h-4 w-4), stroke 2, currentColor. - Class strings on UI components match
/CLAUDE.mdsnippets exactly.
Mockup ↔ template¶
- Mockups in
design/*.htmlare the design source of truth. - When a mockup is approved, port to a Django template at
api/templates/api/. - Class strings should match the mockup verbatim — if they diverge, update the mockup or open a design issue.
Comments¶
- Lean: don't write comments that re-state code.
- Do write the why when it isn't obvious — a workaround, a constraint, a deliberate departure from a common convention. The "deliberate departure from all-or-nothing CSV import" comment in
_import_rowsis the model.
Migrations¶
- Generate via
makemigrations core api. - For schema changes that aren't compatible with existing rows (new non-null FK):
- Drop the dev DB (
DROP DATABASE danbyte; CREATE DATABASE danbyte OWNER danbyte;) - Remove the affected migrations
- Re-
makemigrations+migrate seed_demo
- Drop the dev DB (
- This is a dev shortcut. Production migrations need data migrations + nullable-first-then-non-null patterns.
Seed data¶
seed_demois opt-in (python manage.py seed_demo), idempotent, and usesget_or_create.- Don't put seed data in migrations.
- Don't ship seed data with prod.
Tests¶
- Currently sparse. Add focused tests when fixing a bug; broader test coverage is a deferred quality-of-life round.