Segment schemas (the default segment type)¶
A patrol segment’s user-defined fields are shaped by a segment schema: one document per site (per tenant), applying to every patrol segment regardless of its patrol type.
The schema is owned by a SegmentType row. SegmentType is a pseudo-singleton
— a real table that could hold many rows, but in practice exactly one row per
tenant is allowed to exist, reserved under the value default. The model
mirrors v2 EventType and PatrolType (same {json, ui} envelope, same
meta-schema validation, same revision auditing); the only structural difference
is that there is no create or delete surface.
Availability. These endpoints are gated on the per-tenant
patrol_schemaspreview feature. A tenant without it gets404from every route below — the surface is indistinguishable from one that was never deployed. Segment types have no permissions of their own: they use the patrol-type permissions. Reads requireactivity.view_patroltypeoractivity.view_patrol(anyone who can read patrols needs the segment schema to record segments);PUTrequiresactivity.change_patroltype.activity.view_segmenttypeandactivity.change_segmenttypeare not consulted.
Endpoints¶
GET /api/v1.0/activity/patrols/segments/types/default/ retrieve
PUT /api/v1.0/activity/patrols/segments/types/default/ replace the schema
GET /api/v1.0/activity/patrols/segments/types/default/schema/ the schema document
default is the only accepted path segment. Any other value — including the
row’s own UUID — returns 404; the literal is baked into the URL pattern, so the
rejection happens at the resolver.
POST and DELETE return 405. This table cannot gain or lose records through
the API.
GET .../types/default/¶
Returns the record. By default the schema field is omitted:
{
"id": "3f1c…",
"value": "default",
"display": "Default"
}
Pass ?include_schema=true to get the document inline:
{
"id": "3f1c…",
"value": "default",
"display": "Default",
"schema": {
"json": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {},
"required": [],
"unevaluatedProperties": false
},
"ui": {"fields": {}, "headers": {}, "order": [], "sections": {}}
}
}
A tenant that has never written a segment schema serves the canonical empty
envelope shown above, never null — the same behaviour as PatrolType.schema
with an unset schema. That envelope is the stored default, not something the API
synthesizes on read, so it is ready to be edited as-is.
The response carries ETag and Last-Modified; both support conditional GET.
The include_schema=true ETag is distinct from the default one and moves when
the document changes.
PUT .../types/default/¶
Replaces the schema document. The body is validated against the shared
meta-schema (JSON Schema draft 2020-12) plus the ui.sections / ui.headers /
ui.order parent-reference contract.
{"schema": {"json": {...}, "ui": {...}}}
Omitting
schemaleaves the stored document untouched."schema": nullis a400, not a silent no-op.valueanddisplayare reserved. A body may echo their current values — so aGETresponse can bePUTstraight back — but any change is a400naming the reserved value. Nothing is written when that happens; a rejected identity change does not let an accompanyingschemathrough.Of the four envelope-root keys the shared meta-schema tolerates for
EventType’s benefit, onlyauto-generateis accepted here.readonly,icon_idandimage_urlare rejected with a400under theschemakey.
Every accepted write is recorded as a revision (SegmentTypeRevision), including
the timestamp-only change a re-save of an identical document produces.
GET .../types/default/schema/¶
Returns the schema document itself (not the record wrapper), with the shared schema-endpoint semantics used by v2 event types:
Query param |
Meaning |
|---|---|
|
Dereference |
|
Choice-field shape when |
A 200 carries the bare document. A document-level problem (no schema, a broken
envelope, corrupt storage) returns 422 with structured errors:
{"errors": [{
"category": "validation",
"code": "schema_structure_error",
"message": "Schema missing 'json' or 'ui' top-level key.",
"hints": [{"message": "A schema document must be a JSON object with top-level \"json\" and \"ui\" sections — update the segment type schema via the API.", "action_type": "fix", "details": {}}],
"context": null
}]}
The hints name only the API on purpose: the Django admin row for segment schemas is a link out to the form builder, with no editable schema widget, so the validated API is the only write path.
The response carries an ETag (no Last-Modified) and honours If-None-Match
with 304. It is a hash of the served document: shareable across users without
pre_render; with pre_render=true it moves when the data behind a $ref
changes and is per-user, so cache it per authenticated identity. Only a 200
carries an ETag; the 422 above has none. pre_render yields its own ETag, and
so does s_format when pre_render=true.
How the default row comes to exist¶
Lazily, on first access, and by exactly one mechanism: SegmentTypeViewSet.get_object
does a get_or_create. The first request to any of the routes above creates the row
(and its added revision); every later request finds it. There is no seeding
migration — every read and write of a segment schema goes through this viewset (the
admin row is a link out to the form builder, which drives the same API), so a
deploy-time fleet-wide write would buy nothing a tenant’s own first request does not.
Consequences worth knowing:
The row’s
idis a freshuuid4per tenant. Ids are deliberately not shared across tenants here, unlike thePatrolTyperows seeded byactivity/migrations/0096— so the shared-primary-key write hazard that applies to patrol types does not apply to segment types.A tenant whose
patrol_schemaspreview feature is off never reaches theget_or_create: the gate runs first, so no row is created for a dark tenant.The creation is idempotent and tenant-scoped.
SegmentType.objectsis tenant-scoped, so the read half only sees the caller’s own row, and the(das_tenant, value)unique constraint turns a concurrent double-create into a loser-raises thatget_or_createretries as a read.A tenant that has never called the API has no row at all. Nothing else reads
SegmentType, so that is not observable anywhere.
Admin¶
The ACTIVITY section of /admin carries a link-only row, Patrol Types and
Segments, which opens the form builder’s patrol-types page at
/admin/form-builder/patrol-types. Like the existing “Event Types and Categories
2.0 (Beta)” row it is a proxy model with every write permission denied; the row
is present only when PATROL_ENABLED is on.