Performer Assignment Architecture
Purpose
Performer assignments connect three existing records:
- a
Performer, representing the person using equipment; - a
WirelessUnit, representing the assigned transmitter or microphone; and - a
MonitoringGroup, defining which operators can observe the assignment.
Django users are operators. Performers are domain records and are never authentication identities.
Domain model
PerformerAssignment owns assignment state, priority, notes, per-condition alert preferences, and the assigned_by audit reference. The database permits only one row for a performer/wireless-unit pair. Deactivation preserves that row and its history.
The model manager provides composable query helpers:
for_user(user=...)applies monitoring-group and tenant visibility;active()selects active assignments;by_monitoring_group(group=...)limits results to one group;with_performer_and_unit()loads the common relationship graph; andneeding_alerts(after=...)selects active assignments with alerting enabled.
Write boundary
Views, admin actions, and background tasks must send assignment writes through PerformerAssignmentService. Callers pass IDs and the acting user rather than pre-resolved model objects:
from micboard.services.core.performer_assignment import PerformerAssignmentServicefrom micboard.services.core.performer_assignment_dtos import CreatePerformerAssignment
assignment = PerformerAssignmentService.create_assignment( command=CreatePerformerAssignment( performer_id=performer.id, unit_id=wireless_unit.id, group_id=monitoring_group.id, priority="high", alert_on_battery_low=True, alert_on_signal_loss=True, alert_on_hardware_offline=True, ), user=request.user,)The service provides create_assignment(), update_assignment(), deactivate_assignment(), and delete_assignment(). This is deliberately a small API: scoped resolution, authorization, and lifecycle behavior stay in one place.
Access-control invariants
Every write validates all referenced objects against the acting user:
- The monitoring group must be active and assigned to the user, unless the user is a superuser.
- The performer must be returned by
Performer.objects.for_user(). - The wireless unit must be returned by
WirelessUnit.objects.for_user(). - In MSP mode, the user needs an active
operator,admin, orownermembership covering the unit’s organization and campus. - Unsupported, tenantless, or cross-tenant relationships fail closed.
viewer memberships can read in-scope assignments but cannot create, change, deactivate, or delete them. Permission failures use Django’s PermissionDenied; HTTP views translate that into a 403 response without exposing whether an out-of-scope object exists.
Superuser cross-organization access follows the host’s MICBOARD_ALLOW_CROSS_ORG_VIEW setting.
Read boundary
Request-facing reads begin with user-scoped managers:
assignments = PerformerAssignment.objects.for_user(user=request.user)performers = Performer.objects.for_user(user=request.user)Single-tenant mode exposes an unassigned performer so its first assignment can be created. MSP mode cannot infer a safe tenant for an unassigned performer, so it remains hidden until the host provides a tenant-scoped relationship.
The assignment list view combines for_user() with select_related() for performer, wireless unit, and monitoring group. Services that need chassis/location details should use with_performer_and_unit() or an equivalent explicit relationship plan rather than triggering per-row queries.
Alert preferences
Alert behavior is represented by four independent fields:
alert_on_battery_low;alert_on_signal_loss;alert_on_audio_low; andalert_on_hardware_offline.
There is no aggregate alert_enabled service argument. Callers update only the conditions they want to change. Read the four explicit model fields when evaluating alert policy; there is no parallel dictionary representation to keep synchronized.
Integration guidance
- Keep views and Huey tasks thin; they parse input and delegate to the service.
- Do not construct assignments directly from untrusted IDs.
- Do not re-export the models or service from root packages; import their defining modules.
- Use canonical queryset helpers for access checks instead of duplicating tenant predicates.
- Add access tests for different organizations, campuses, monitoring groups, and membership roles whenever assignment behavior changes.
See the repository-root Performer Quickref for current call examples.
