Patrol Sync

Recommendations for synchronizing patrol-related data for offline use. Aimed at mobile clients that want to minimize downloads and the number of checks to detect changes.

This guide covers patrol types (the catalog of types, like “Foot”, “Vehicle”), the tracked-by schema (who can be reported as segment leader), the patrol configuration (teams and the asset / member rosters), and patrols and patrol segments (the actual patrol instances and their segments). Patrol types — the list, the detail, the schema routes and the revision history — the tracked-by schema, the patrol configuration routes and segment list/detail support conditional GET; patrol list/detail do not. The patrol types list and detail support both ETag / If-None-Match and Last-Modified / If-Modified-Since; the patrol type schema routes, the revision history, the tracked-by schema, the patrol configuration routes and segment list/detail support ETag / If-None-Match only.

Note: Two kinds of ETag appear below. Shareable ETags are derived from data that is identical for every caller — the patrol types list and detail, the raw schema routes (pre_render absent or false), the revision history, segment list and detail, and the patrol configuration (config/default) — so a stored value can be reused across users and profiles. Per-user ETags are derived from a response that depends on the caller: the tracked-by schema (salted with the requesting user’s ID and the user-profile header when present, because the set of selectable leaders depends on the caller’s permissions), any schema route called with pre_render=true (rendered $ref choices are resolved against the caller’s permissions) and the resolved patrol configuration (config/default/resolved, whose rosters are scoped to the caller). The segment and patrol configuration ETags are unsalted hashes of the served body; the resolved one is per-user only because its body is. Cache per-user ETags per authenticated identity; never share them between users or profiles.

Note: When you send both If-None-Match and If-Modified-Since, the server evaluates If-None-Match and ignores If-Modified-Since (RFC 9110 §13.2.2). Prefer If-None-Match — it detects changes that a one-second-resolution timestamp can miss.

Note: The web UI currently supports only one patrol segment per patrol. The API allows multiple segments per patrol, but clients that aim for parity with the web UI should treat one segment per patrol as the supported case.


Patrol Types (catalog for offline)

Patrol types define the kinds of patrols (e.g. Foot, Vehicle). Sync the list for offline so the app can create patrols and show type metadata (display name, icon, priority, etc.).

Single patrol type (detail)

GET /api/v1.0/activity/patrols/types/<patrol_type_id_or_value>

The path segment accepts either the patrol type UUID or its unique value slug — one route serves both, matching the v2 event-types endpoint. (The route regex is DRF’s default [^/.]+, so a value containing / or . is not addressable here; the same constraint applies to v2.)

Supports conditional GET via ETag / If-None-Match and via Last-Modified / If-Modified-Since, where Last-Modified is that type’s own updated_at. Use when you need to refresh or check a single type; for full catalog sync, the list endpoint with conditional GET is simpler.

The include_schema parameter is supported here as well as on the list, with the same semantics: the type’s stored schema document, which is the empty envelope until one is written. include_schema=true yields a different ETag than the no-param request, so the two response shapes never share a cache entry. The include_schema=true ETag also covers the schema content itself, so editing only a type’s schema invalidates it. Note that the default, schema-less ETag is not insulated from schema writes in general: it covers updated_at, which an ordinary PUT/PATCH of the schema bumps. It stays put only for a schema write that does not touch updated_at.

Like the list, the detail route hides deactivated types by default: a GET for a type with is_active=false returns 404 Not Found unless you pass ?include_inactive=true. The write methods (PUT, PATCH, DELETE) are unaffected and always reach the type, deactivated or not.

Patrol type response fields

Field

Writable

Description

id

No

UUID primary key.

value

Yes

Unique string identifier for this type (e.g. "foot").

display

Yes

Human-readable label.

ordernum

No

Sort order. Nullable. Read-only here; reorder via the rank endpoint.

icon

Yes

Raw stored icon identifier. Empty string when no explicit icon has been set.

icon_id

No

Resolved icon identifier. Equals icon when an icon is stored; falls back to value when icon is empty and a static marker image exists for that value, otherwise "generic_rep". Use icon_id for display — it handles the fallback. Write icon when setting or clearing an icon.

default_priority

Yes

Default priority for patrols of this type.

is_active

Yes

Whether this type is available for new patrols.

schema

Yes

The type’s {"json": ..., "ui": ...} schema document. Only present in GET responses when include_schema=true. Defaults to the empty schema envelope.

Patrol type write endpoints

Method

URL

Description

POST

/api/v1.0/activity/patrols/types

Create a patrol type.

PUT

/api/v1.0/activity/patrols/types/<id>

Replace a patrol type.

PATCH

/api/v1.0/activity/patrols/types/<id>

Partially update a patrol type.

DELETE

/api/v1.0/activity/patrols/types/<id>

Delete a patrol type.

<id> in the table above also accepts the type’s unique value slug in place of the UUID — the same dual id/value lookup as the GET detail endpoint applies to PUT, PATCH, and DELETE.

value must be unique per tenant. Attempting to create a duplicate returns 400 Bad Request. Required permissions: add_patroltype, change_patroltype, or delete_patroltype respectively.

Reordering: POST .../patrols/types/<id_or_value>/rank reorders the catalog (see Ranking). A rank write rewrites the target’s ordernum and advances its updated_at; when the write triggers a rebalance, every renumbered sibling row gets a fresh updated_at too. Both fields are in the ETag field set, so If-None-Match observes any reorder, Last-Modified / If-Modified-Since moves forward for the whole catalog, and updated_since returns every row whose position changed. If-None-Match remains the recommended validator: it detects changes a one-second-resolution timestamp can miss.

Writing a schema

schema is an optional body field on POST, PUT, and PATCH. It takes the same {"json": ..., "ui": ...} document shape as the v2 event-type schema.

  • Omitting it is a no-op. On POST the new type gets the empty schema envelope; on PUT and PATCH the stored document is left exactly as it was. Clients that never send schema are unaffected by this field.

  • Sending it replaces the stored document wholesale — it is not merged into the existing one.

  • The document is validated against the schema meta-schema (JSON Schema draft 2020-12; json.$schema must be https://json-schema.org/draft/2020-12/schema), and every ui entry that names a section must reference a key that exists in ui.sections. A document that fails any of these checks returns 400 Bad Request and nothing is saved.

  • "schema": null is rejected with 400 Bad Request; omit the key instead.

  • The stored document may differ from what you sent. Accepted documents are normalized on save: additionalProperties: false becomes unevaluatedProperties: false, and unevaluatedItems: false is added to collection-field arrays that omit it. The write response does not show this — read the type back with GET ?include_schema=true to see exactly what was stored.

  • Write responses never include schema. POST, PUT, and PATCH responses omit the key entirely, even when the request wrote one; include_schema is honoured on GET (list and detail) only. Follow a write with a conditional GET ?include_schema=true if you need to confirm the stored document.

  • The served document is the raw stored one, exactly as saved. It is not a rendered schema: choice lists and $ref are not expanded (the same distinction as the v2 event-type include_schema). Use the schema endpoints below with pre_render=true when you need the rendered form.

  • Envelope-root extras are rejected. readonly, icon_id, and image_url alongside json / ui return 400 Bad Request under the schema error key: they are event-type-only accommodations, and for a patrol type icon_id / image_url are read-only derived fields of the type itself. auto-generate is accepted at the root (it is the cross-owner auto-generation marker) but no patrol-type auto-generation exists today, so it is stored and served with no effect.

List all patrol type schemas

GET /api/v1.0/activity/patrols/types/schemas

Returns one entry per patrol type — the bulk companion to the single-type endpoint below, and the endpoint to use for a full schema sync. Query parameter pre_render=true returns rendered schemas (choice lists expanded, $ref resolved); without it, schemas are raw. include_inactive and updated_since behave exactly as on the patrol types list.

The body is {"count": <n>, "results": [...]}, where each result carries value, status (success / partial / failure), schema, and — on anything but success — an errors array whose entries have a stable machine-readable code plus human-readable message and hints. A single unreadable document therefore never fails the batch: the response is 207 Multi-Status when any entry failed, and 200 OK only when every entry succeeded.

The response carries an ETag (no Last-Modified) and honours If-None-Match: a match answers 304 Not Modified with no body. Without pre_render the ETag is a digest of the stored documents, computed in the database alongside the served rows, so the validator and the body always describe the same state; a 304 saves the bandwidth and the body build, not the database read. It is shareable across users. With pre_render=true it is a hash of the rendered body, so it also moves when the data behind a $ref changes (a new choice value, a renamed subject); it is per-user and must not be shared. Only a 200 carries an ETag: a 207 batch has no validator, so re-request it unconditionally until every entry succeeds. pre_render, include_inactive and updated_since each yield their own ETag, and so does s_format when pre_render=true — replay the exact request.

Single patrol type schema

GET /api/v1.0/activity/patrols/types/<patrol_type_id_or_value>/schema

Returns the schema for one patrol type. Like the detail route, the path segment accepts either the UUID or the unique value slug, and a deactivated type returns 404 Not Found unless you pass ?include_inactive=true.

Query parameters

Parameter

Description

pre_render

When true, resolve $ref entries and expand dynamic choice lists inline. Default false, which serves the raw stored document.

s_format

Shape of expanded choice fields when pre_render=true: oneOf (default, for compatibility with v2.x mobile clients) or enum (the enum + x-enumExtra shape). Ignored when pre_render is false; an unrecognized value returns 400 Bad Request, but only when pre_render is truthy.

The response body is the bare {"json": ..., "ui": ...} document — the same shape include_schema=true puts under the schema key on the list and detail routes. A patrol type that has never had a schema written to it serves the empty schema envelope. Note that pre_render=true also drops the document root’s unevaluatedProperties: false, so that data captured under an older revision of the schema still validates.

Only the $ref sources the requesting user is allowed to read are resolved. An unresolvable reference does not fail the request — the property renders as an empty schema, so validate rendered content rather than relying on the status code alone.

If the stored document cannot be served at all (it is empty, or it is not a {json, ui} object), the response is 422 Unprocessable Entity with a bare {"errors": [...]} body using the same error entries as the bulk endpoint.

The response carries an ETag (no Last-Modified): a hash of the served document, shareable across users without pre_render and per-user with pre_render=true, and 304 Not Modified on an If-None-Match match. A 422 carries no validator. include_inactive does not change the body, so it does not change the ETag.

Patrol type updates (revision history)

GET /api/v1.0/activity/patrols/types/<patrol_type_id_or_value>/updates

Returns the revision history for one patrol type, newest first and paginated (count / next / previous / results, with page and page_size) — unlike the patrol types list, which is a bare array. Each entry carries time, action, user, updated_fields, and sequence; a create reports no updated_fields (it is a full snapshot, not a diff), and every later entry names only the fields that changed. Use when you need to show or sync change history; not required for basic catalog + schema sync. Each page carries an ETag (no Last-Modified) — a hash of the page as served, so it moves when a revision is added and when a listed user’s display details change — and If-None-Match is honoured with 304 Not Modified. The ETag is shareable across users; each page / page_size combination has its own.

The path segment accepts the UUID or the value slug, as the detail route does, and that route’s deactivated-type rule applies here too: 404 unless ?include_inactive=true. Requires view_patroltype or view_patrol, the same as the read endpoints.


Tracked-by schema (segment leaders / “reported by”)

When creating or editing patrol segments, clients often need the list of subjects (and optionally users) that can be selected as segment leader (“tracked by”). This is provided by the tracked-by schema endpoint. It has an ETag so you can avoid re-downloading when the set of possible leaders has not changed.

Endpoint

GET /api/v1.0/activity/patrols/trackedby

Returns metadata describing the schema for “tracked by” (e.g. subject list). Requires view_patrol; the subjects offered are then limited to the ones the caller can see. The ETag is derived from the set of subjects available to the user. Use If-None-Match for conditional GET; 304 Not Modified means the schema/options are unchanged.

Optimizing “do I need to download?”

  1. Store the ETag from the last successful response.

  2. On the next sync, send If-None-Match: your stored ETag value.

  3. 304 — keep your cached schema; 200 — replace cache and store the new ETag.


Patrol configuration (teams and rosters)

The teams, assets and members a patrol form offers come from the tenant’s patrol configuration. See Patrol configuration for the response shapes, the preview-feature gate and the permission.

Endpoints

GET /api/v1.0/activity/patrols/config/default
GET /api/v1.0/activity/patrols/config/default/resolved

Both routes carry an ETag (no Last-Modified) and honour If-None-Match with 304 Not Modified. The config/default ETag is shareable across users. The config/default/resolved ETag is per-user: its rosters are scoped to the caller’s subject permissions and user-profile, so cache it per identity. Besides membership, name and activity changes to the roster subjects and changes to the teams, the resolved ETag also moves when a roster subject’s status icon changes (for example its device’s radio state).


Patrols (list and detail)

Patrols are the actual patrol instances (with one or more segments). The list and detail endpoints do not support ETag or Last-Modified. Use query parameters to limit results and paginate; for incremental-style sync you can use the filter with a date_range to fetch patrols overlapping a time window.

Endpoints

GET /api/v1.0/activity/patrols
GET /api/v1.0/activity/patrols/<patrol_id>

List query parameters

Parameter

Description

filter

JSON object for advanced filtering. Example: {"date_range":{"lower":"2020-09-16T00:00:00.000Z"}, "text":"search text"}. Use date_range with lower/upper (ISO date/time) to restrict by patrol time; supports incremental-style sync by requesting patrols overlapping a given window.

exclude_empty_patrols

If true, exclude patrols that have no patrol segments. Default is false.

status

Filter by state(s). Allowed values depend on StateFilters (e.g. scheduled, active, done, overdue). Can be repeated.

Responses are paginated. There is no ETag on the list or detail; you cannot use conditional GET to skip re-downloading. To minimize data transfer, use filter (e.g. date_range) and pagination, and store the last sync time or last requested range for incremental pulls.

List order

The default (page-number) list is ordered by state group first, then by serial_number descending (newest first) within each group:

  1. open

  2. done

  3. cancelled

  4. any other state

The order is deterministic, so walking the pages returns each patrol exactly once, as long as the data does not change while you page. Within a state group the serial order matches the order of the use_cursor=true mode, which orders the whole list by serial_number descending without grouping by state.

Patrols without a serial_number are not listed in either mode. They can still be fetched by UUID from the detail endpoint.

Single patrol

Detail returns one patrol by UUID. No ETag; refetch when you need the latest state.

Include flags (list and detail)

Both endpoints accept four flags that drop parts of the payload. Every flag defaults to true, so a request without them gets the full payload. This is unlike the events list, where include_notes and include_related_events default to false. The flags are the cheapest way to cut a patrol response down: a client that only shows the patrol list can pass all four as false.

Parameter

When false

include_updates

The top-level updates key is omitted, and every nested updates list (on segments, notes and files) is returned as [].

include_notes

The notes key is omitted. A PATCH/PUT sent with this flag still stores the notes in its body; only the response leaves them out.

include_files

The files key is omitted.

include_segment_events

Each segment’s events key is omitted. When true, each segment event carries contains (one level deep), as before.

last_activity_at

Every patrol carries a read-only last_activity_at: the latest of the patrol’s own updated_at and the updated_at of each of its segments. Editing a segment does not change the patrol’s updated_at, so use last_activity_at to tell whether the patrol or any of its segments has changed. Linking events to a segment, or unlinking them, does not change last_activity_at.

Patrol request payloads (POST, PATCH, PUT)

Create patrol: POST /api/v1.0/activity/patrols

Update patrol: PATCH or PUT /api/v1.0/activity/patrols/<patrol_id>

Request body (JSON). All fields are optional on create; omit fields you are not changing on update.

Field

Type

Description

objective

string

Patrol objective (text). Can be blank/null.

priority

integer

Priority. Allowed values: 0 (Gray), 100 (Green), 200 (Amber), 300 (Red). Default is 0.

state

string

Patrol state: "open", "done", or "cancelled". Default is "open".

title

string

Patrol title. Max 255 characters. Can be blank/null.

notes

array

List of note objects. Each note: { "text": "..." }. Optional id for updates (to update an existing note); on PATCH/PUT the id must name a note of this patrol, otherwise 400 (unknown, or a note of another patrol).

patrol_segments

array

List of segment objects (see Patrol segment request payloads below). When creating a patrol you can include segments in this array; do not send patrol inside each segment (server sets it). Include id in a segment to update an existing segment; on PATCH/PUT the id must name a segment of this patrol, otherwise 400 (a segment cannot be moved between patrols).

Validation: scheduled_start must be earlier than scheduled_end in any segment. The stored state follows from the patrol’s segments and can differ from the one you send; see Patrol state rules.


Patrol segments (list and detail)

Patrol segments are the legs of a patrol (each has a type, time range, leader, etc.). The web UI supports only one segment per patrol; the API may return or accept multiple segments per patrol, but the web UI does not. List and detail carry an ETag (no Last-Modified) and honour If-None-Match with 304 Not Modified.

Endpoints

GET  /api/v1.0/activity/patrols/segments
GET  /api/v1.0/activity/patrols/segments/<segment_id>

List is paginated. The ETag is shareable across users holding view_patrol. It is a hash of the body as served — absolute URLs included — so store it per base URL and replay the exact request. On the list, each include_pauses / page / page_size combination has its own ETag, and because every page carries the total count, every page’s ETag moves whenever a segment that list includes is created or deleted, on any page — a paused leg moves only the include_pauses=true variants. The ETag also moves when a linked event changes, when a revision author’s display name changes, and when the segment leader’s status icon changes. A 304 saves bandwidth, not server work — the body is still built to be hashed — so do not poll aggressively.

Segment–events

GET /api/v1.0/activity/patrols/segments/<patrol_segment_id>/events

Returns events linked to a patrol segment. No ETag.

Patrol segment request payloads (POST, PATCH, PUT)

Segments can be created or updated in two ways: nested in a patrol (in patrol_segments when posting to POST /patrols or PATCH/PUT /patrols/<id>) or standalone via the segments list.

Create segment (standalone): POST /api/v1.0/activity/patrols/segments

Update segment (standalone): PATCH or PUT /api/v1.0/activity/patrols/segments/<segment_id>

When nested in a patrol payload, omit patrol (the server sets it). When posting to the segments endpoint, patrol is required (patrol UUID).

Field

Type

Required (standalone)

Description

patrol

UUID

Yes (standalone only)

Patrol ID. Omit when segment is nested in a patrol body. Fixed after create: a different patrol on PATCH/PUT is a 400; resending the same one is accepted.

patrol_type

string

No

Patrol type value (e.g. "foot", "vehicle"). From patrol types catalog.

leader

object

No

Segment leader (who is tracked). Object: { "content_type": "<model>", "id": "<uuid>" }. Allowed content_type: "observations.subject", "accounts.user", "activity.community". Use the tracked-by schema to get valid options. Can be null.

scheduled_start

string

No

ISO 8601 date/time. Scheduled start.

scheduled_end

string

No

ISO 8601 date/time. Scheduled end. Must be after scheduled_start if both are set.

time_range

object

No

Actual patrol time. { "start_time": "<iso>", "end_time": "<iso>" }. Either may be null, but end_time needs a start_time, must be strictly after it, and a leg that has a start_time cannot have it cleared. See time range validation.

start_location

object

No

Start point. { "latitude": <number>, "longitude": <number> }.

end_location

object

No

End point. Same shape as start_location.

id

UUID

No

Include when updating an existing segment (nested or standalone).

Read-only in responses (do not send when creating/updating): image_url, icon_id, events.

A standalone POST may carry a client-generated id, so an offline client can name the segment before it syncs. If a segment with that id already exists — typically a retried POST whose first response was lost — the request returns 409 Conflict with {"error_message": "..."}. Nothing is created and the existing segment is left unchanged. To change the existing segment, PATCH or PUT it. The same applies to POST /api/v1.0/activity/event/<event_id>/segments.


Patrol state rules

A patrol’s state depends on all of its legs (segments), whatever order they come back in. A leg is running when its time_range has a start_time and no end_time; it has ended when its end_time is in the past; it has a planned end when its end_time is in the future. A scheduled end is the separate scheduled_end field. A new or never-started leg sent with neither start_time nor end_time is stored with no time range and does not count as running; a leg that has started cannot have its start_time cleared. A leg counts as added or updated only when the write creates it or changes one of its own fields; resending a leg unchanged writes nothing, and changing only its members or assets does not count. A patrol is saved by any POST, PATCH or PUT on the patrol endpoints, with or without patrol_segments. A save that changes none of the patrol’s own fields (such as state, title, priority or objective) adds nothing to the patrol’s updates and by itself sends no realtime das.patrol.update; a leg, note or roster change in the same request still sends one. The auto-end check is a server task (periodically_maintain_patrol_state) that runs every minute.

#

Given

When

Then

1

An open patrol whose legs have all ended, none with a scheduled end

The auto-end check runs

The patrol is done.

2

An open patrol with a running leg or a leg with a planned end

The auto-end check runs

The patrol stays open.

3

An open patrol

A user ends it (the web UI closes all legs)

The patrol is done.

4

An open patrol

A user cancels it

The patrol is cancelled.

5

An open patrol with ended and running legs, in any order

The patrol is saved

The patrol stays open.

6

A done patrol

A running leg is added or updated

The patrol is open.

7

A done patrol with no running leg

A leg with a planned end is added or updated

The patrol stays done.

8

A done patrol with a running leg

The patrol is saved

The patrol is open.

9

A cancelled patrol

A running leg is added or updated

The patrol is open.

10

A cancelled patrol with no running leg

A leg with a planned end is added or updated

The patrol stays cancelled.

11

A cancelled patrol

The patrol is saved

The patrol stays cancelled.

12

Any patrol

A leg is closed through the segment endpoints (.../patrols/segments)

No change. Only the auto-end check or a patrol save ends a patrol.

13

An open patrol whose legs have all ended

The patrol is saved (for example, a PATCH that closes the last running leg)

The patrol is done.

14

Any patrol

One POST .../patrols or PATCH/PUT .../patrols/<patrol_id> sets "state": "cancelled" and adds or updates a running leg

The patrol is open: the running leg wins over the requested state. To cancel, close the leg in the same request, or leave the running leg out of patrol_segments.

15

A cancelled patrol with a running leg

Only that leg’s members or assets change, nested in PATCH .../patrols/<patrol_id> or on PATCH .../patrols/segments/<segment_id>

The patrol stays cancelled. (A done patrol with a running leg reopens on the nested write, by row 8, because that write saves the patrol.)

Repairing patrols damaged by the old auto-end check

Before ERA-14077, the auto-end check saved multi-leg patrols every minute and could leave a patrol done while a leg was still running. Each of those saves wrote a patrol revision that patrol history never shows, up to about 1.37 million on one patrol. The repair_patrol_auto_end_damage management command cleans both up, once the fix is deployed:

  • It deletes patrol update revisions whose stored change holds nothing but updated_at. Every other revision is kept: created, deleted and relation-deleted ones, and every update that records a field, including fields patrol history does not show (priority, objective, provenance, serial_number).

  • It reopens every done patrol that still has a running leg (row 8), which writes a patrol revision and sends das.patrol.update. It never closes an open patrol (that is left to the auto-end check, which honours a leg’s scheduled end) and never changes a cancelled one.

It only reports by default. Run it for one tenant first and read the counts it prints per tenant, then re-run with --apply:

python manage.py repair_patrol_auto_end_damage --tenant <domain-or-id>
python manage.py repair_patrol_auto_end_damage --tenant <domain-or-id> --apply
python manage.py repair_patrol_auto_end_damage --apply   # every tenant

Revisions are deleted in batches of --batch-size (default 1000), each its own statement, so a run can be interrupted and started again. A tenant whose settings cannot be loaded is skipped and makes the command exit non-zero.


Patrol sub-resources (notes, files)

Patrol notes and files are nested under a patrol:

GET /api/v1.0/activity/patrols/<patrol_id>/notes
GET /api/v1.0/activity/patrols/<patrol_id>/notes/<note_id>
GET /api/v1.0/activity/patrols/<patrol_id>/files
GET /api/v1.0/activity/patrols/<patrol_id>/files/...

These endpoints do not expose ETag or Last-Modified. Refetch as needed.

Note request payloads: Create note with POST .../patrols/<patrol_id>/notes with body { "text": "..." }. Update with PATCH/PUT .../notes/<note_id>. The patrol is implied by the URL.

File request payloads: Create/update patrol files via the files endpoint; the payload typically includes usercontent_id, usercontent_type, and optionally comment, ordernum. See the API or schema for the full file upload format.


Permissions

How each /api/v1.0/activity/patrol* route is authorized. Permissions are activity.* codenames.

Routes

Class

Read (GET / HEAD / OPTIONS)

Write

patrols, patrols/<id>

PatrolObjectPermissions

view_patrol, plus leader visibility (see below)

POST add_patrol, PUT/PATCH change_patrol, DELETE delete_patrol; detail writes also pass the leader check

patrols/segments, patrols/segments/<id>

PatrolObjectPermissions

view_patrol; the detail also runs the leader check on the segment’s patrol, the list does not filter by leader

Same *_patrol codenames as patrols

patrols/<id>/notes[/<note_id>], patrols/<id>/files/..., patrols/<id>/file/...

PatrolObjectPermissions

view_patrol only; no leader check

Same *_patrol codenames as patrols

patrols/segments/<id>/events

EventsPermissions (event-category)

Any authenticated caller; events are filtered to the categories the caller can read. No patrol permission is checked

POST needs the <category>_create permission of the posted event_type’s category (or its geo-permission within the configured radius)

patrols/types, patrols/types/<id_or_value>, .../schema, patrols/types/schemas, .../updates

PatrolTypePermissions

view_patroltype or view_patrol

POST add_patroltype, PUT/PATCH change_patroltype, DELETE delete_patroltype

patrols/types/<id_or_value>/rank

PatrolTypeRankPermissions

—

POST change_patroltype

patrols/segments/types/default, .../default/schema

PatrolTypePermissions

view_patroltype or view_patrol

PUT change_patroltype

patrols/config/default, .../default/resolved

ViewPatrolPermission

view_patrol, after the patrol_schemas preview gate (see Order of checks)

— (read-only; other verbs answer 405 to a permitted caller)

patrols/trackedby

ViewPatrolPermission

view_patrol

— (POST answers 405 to a permitted caller)

Notes

  • *_patrol everywhere. PatrolObjectPermissions always checks the Patrol model’s codenames, so segment, note and file writes need add/change/delete_patrol, not a segment, note or file permission.

  • Leader visibility. On a patrol or segment detail, the caller must be able to see the leader subject of the patrol’s latest segment, or be that leader; a patrol with no segments, or a leader that is not a subject, passes. The patrols list applies a looser filter: a patrol is listed if it has no segments, or any of its segments has no leader, a visible subject as leader, or a user as leader.

  • Segment types have no permissions of their own. They are read and written exactly like patrol types; view_segmenttype and change_segmenttype are not consulted. See Segment schemas.

  • Caller-scoped content. The resolved patrol configuration lists only subjects the caller can see (see Patrol configuration), and the tracked-by schema offers only the leaders the caller can see.

  • pre_render=true. A rendered schema resolves each $ref against the caller’s own permissions. A source the caller cannot read renders as an empty choice list ("title": "Empty schema") under a 200, never a 403, so two users can get different bodies (and ETags) from the same route.

  • Order of checks on patrols/config/default. (1) patrol_schemas preview gate, 404 for everyone while off; (2) authentication, 401; (3) ViewPatrolPermission, 403 without view_patrol; (4) configuration-name lookup, 404 for any name but default. The permission check precedes the name lookup, so a caller without view_patrol gets 403 for every name and cannot probe names through the 404.

  • 404, not 403, when a preview feature is off. The segment-type and patrol-configuration routes are gated on the tenant’s patrol_schemas preview feature. The gate runs before authentication and permissions, so while it is off every caller, including an anonymous one, gets 404. A 403 means the feature is on and the caller lacks the permission.

  • Superusers pass every check above. Anonymous callers get 401.

Django admin: patrol configuration

The configuration the API serves is edited on the ACTIVITY › Patrol Configuration admin page. Its permissions are independent of the API’s: editing keys on the model’s own activity.*patrolconfiguration codenames, not on view_patrol, so a user can edit the configuration without being able to read patrols, and vice versa.

Action

Requires

Notes

Page exists

PATROL_ENABLED setting

AdminFeatureFlag unregisters the model when the setting is off, for every user

View

view_patrolconfiguration or change_patrolconfiguration

Django default; view alone opens the page read-only

Change rosters

change_patrolconfiguration

Django default; no override on the admin class

Add

add_patrolconfiguration, and only while the tenant has no row

Per-tenant singleton; once the row exists the add action is gone for everyone

Delete

—

Always refused, superusers included

Add / change / delete a team (inline)

change_patrolconfiguration

PatrolConfigurationTeamInline defers to the configuration’s codenames; view_patrolconfiguration shows teams read-only. PatrolConfigurationTeam has no permissions of its own and the permission set editor offers none

The patrol_schemas preview feature does not change who may edit. While it is off, the page hides the asset roster field and the teams inline and leaves the member roster editable for anyone holding change_patrolconfiguration.


Summary for mobile clients

Goal

Recommendation

Fewest “has anything changed?” checks

Use conditional GET where supported: patrol types (list, detail, schema routes, revision history), tracked-by schema, patrol segments (list, detail) and patrol configuration (config/default, config/default/resolved). Send If-None-Match.

Patrol types catalog

GET …/patrols/types with If-None-Match; treat 304 as “no download needed”. One list call is enough for the full catalog. Returns active types only by default — add include_inactive=true if you need deactivated types.

Tracked-by / segment leaders

GET …/patrols/trackedby with If-None-Match; 304 means schema/options unchanged.

Patrol type change history

GET .../patrols/types/<patrol_type_id_or_value>/updates — paginated; each page has an ETag, send If-None-Match. Not required for basic catalog + schema sync; use only if your client shows or reconciles change history.

Patrols list

No ETag. Use filter (e.g. date_range with lower/upper) and pagination to limit data. Store last sync time or range for incremental-style fetches.

Patrol detail

No ETag. Refetch when you need the latest state.

Segment detail

GET …/patrols/segments/<segment_id> with If-None-Match; 304 means the segment is unchanged. Shareable across users.

Patrol type schemas

GET …/patrols/types/schemas for the whole catalog, or GET …/patrols/types/<id_or_value>/schema for one type. Add pre_render=true when you need choice lists and $ref resolved — the list/detail include_schema option never renders. Both have an ETag: shareable without pre_render, per-user with it; a 304 means your cached schema is current, rendered choices included.

Where conditional GET works

Patrol types list and detail, via ETag / If-None-Match or Last-Modified / If-Modified-Since; patrol type schema routes, revision history, the tracked-by schema, segment list/detail and the patrol configuration routes, via ETag / If-None-Match only. Prefer If-None-Match everywhere. Patrols support neither.