← Knowledge library

Access & roles

Organization

Organizations are MANSOQ's FHIR-aligned grouping primitive: a nested tree that groups permissions and resources. A node might represent all doctors, a Cardiology sub-team, or a governance unit, and permissions attached to a parent flow…

3.1 · versioned documentation · 9 min read

Organizations are MANSOQ's FHIR-aligned grouping primitive: a nested tree that groups permissions and resources. A node might represent all doctors, a Cardiology sub-team, or a governance unit, and permissions attached to a parent flow implicitly to every descendant. You touch them whenever you model who can do what, and where.

Source: care/emr/models/organization.py · resources/organization/spec.py · resources/organization/organization_user_spec.py

The Django model is only the storage layer. Its opaque columns — JSONFields (metadata, cached_parent_json) and arrays (parent_cache, managing_organizations) — get their structure from the resource specs under care/emr/resources/organization/, which also define the org_type enum, validation, the nested JSON returned for parent, and the read/write API schemas. See Resource specs (API schema).

Models

Model Purpose
OrganizationCommonBase Abstract base holding the tree structure, caching, and uniqueness logic shared by both organization types
Organization Instance-wide organization (permission/governance tree, not tied to a facility)
FacilityOrganization Facility-scoped organization (departments/teams within one Facility)
OrganizationUser Membership row linking a User to an Organization with a RoleModel
FacilityOrganizationUser Membership row linking a User to a FacilityOrganization with a RoleModel

Organization and FacilityOrganization extend OrganizationCommonBase, which extends EMRBaseModel — the shared MANSOQ EMR base providing external_id, audit fields, soft-delete, and history/meta JSON. OrganizationUser and FacilityOrganizationUser extend EMRBaseModel directly. OrganizationCommonBase is abstract = True and has no table of its own.

OrganizationCommonBase fields

These columns land on both the Organization and FacilityOrganization tables.

Identity & classification

Field Type Required Default Notes
name CharField(255) yes — Display name. Feeds the sibling uniqueness check (validate_uniqueness)
org_type CharField(255) yes — Free text in the DB, but writes bind to the OrganizationTypeChoices enum. See Organization type values
description TextField no null Nullable and blank-able in the DB; the write/read specs default it to ""
active BooleanField no True
system_generated BooleanField no False System-generated orgs can't be edited or deleted. Read-only on the API (only OrganizationReadSpec surfaces it)
metadata JSONField no {} (dict) Open key-value bag for deployment-specific data — no fixed schema, no migration needed. Specs pass it through verbatim as metadata: dict

Tree structure

Field Type Required Default Notes
parent FK → self no null CASCADE, related_name="children". A null parent is a root org. Written as the parent's external_id (UUID); read back as a nested JSON object — see parent read shape
root_org FK → self no null CASCADE, related_name="root". Top of the tree, derived on save by set_organization_cache()
has_children BooleanField no False Flipped to True on the parent when its first child is created. Read-only on the API

Denormalized caches

MANSOQ rebuilds these on insert so reading the tree doesn't need recursive joins. Do not set them from clients.

Field Type Default Rebuilt by Shape
level_cache IntegerField 0 set_organization_cache() Depth in the tree (parent.level_cache + 1)
parent_cache ArrayField[int] [] (list) set_organization_cache() Full ancestor internal id chain (parent.parent_cache + [parent.id])
cached_parent_json JSONField {} (dict) get_parent_json() Materialized parent record (shape below). Rebuilt after cache_expiry_days (15)

cached_parent_json is built in get_parent_json() and is {} for root orgs:

{
  id:           str   (parent.external_id, UUID as string)
  name:         str
  description:  str | null
  org_type:     str   (OrganizationTypeChoices value)
  metadata:     dict
  parent:       dict  (recursively, the parent's own cached_parent_json)
  level_cache:  int
  cache_expiry: str   (ISO datetime; cache is reused while now < cache_expiry)
}

This is the same structure OrganizationReadSpec.parent returns on read (see parent read shape).

Related models

Organization

Instance-wide organization. Adds one field on top of OrganizationCommonBase:

Field Type Default Notes
managing_organizations ArrayField[int] [] (list) Internal ids of the orgs that manage this one — the management hierarchy deciding who may administer the org and its users (a user is editable only from a managing org above them). Expanded to nested org JSON (OrganizationReadSpec) only by OrganizationRetrieveSpec

FacilityOrganization

Facility-scoped organization — departments, teams, and groups within one facility. Adds one field on top of OrganizationCommonBase:

facility → FK facility.Facility  (CASCADE)

Its org_type binds to FacilityOrganizationTypeChoices rather than OrganizationTypeChoices (see Facility organization type values), and its specs live in resources/facility_organization/. FacilityOrganizationWriteSpec requires facility + org_type + optional parent (validated to be in the same facility); FacilityOrganizationReadSpec / FacilityOrganizationRetrieveSpec mirror the instance read specs, resolving the caller's permissions through get_permission_on_facility_organization. See Facility.

OrganizationUser

Grants a user access to an organization's resources through a role — only the resources assigned to the org, not everything it owns.

organization → FK Organization (CASCADE)
user         → FK users.User   (CASCADE)
role         → FK security.RoleModel (CASCADE)

FacilityOrganizationUser

The facility-scoped equivalent of OrganizationUser.

organization → FK FacilityOrganization (CASCADE)
user         → FK users.User           (CASCADE)
role         → FK security.RoleModel    (CASCADE)

Enum & coded values

Organization type values

Organization.org_type is a CharField in the DB, but every write binds to OrganizationTypeChoices (resources/organization/spec.py).

Value Meaning
team A working / department-style grouping of people (the default)
govt A governance unit mirroring a real administrative hierarchy (State → District → …). Any user can view these to support cross-governance coordination; create/edit is superadmin-only; facilities and patients reference these nodes geographically
role A user group — a flat set of users, e.g. Volunteer, ASHA Worker. Adding an OrganizationUser to a role-typed org invalidates that user's cached_role_orgs (see OrganizationUser.save()); managed by superadmins
product_supplier Supplier organization (links to the supply chain)

Facility organization type values

FacilityOrganization.org_type binds to a separate enum — FacilityOrganizationTypeChoices (resources/facility_organization/spec.py), not OrganizationTypeChoices.

Value Meaning
root System-generated top node, one per facility (cannot be edited or deleted)
dept A department within the facility
team A team, often nested under a department
role A facility-scoped user group
other Any grouping outside the categories above

Root, govt, and role (instance) organizations, plus any system-generated organization, are restricted to superadmin management.

Resource specs (API schema)

Resource specs (EMRResource subclasses) are the API layer. serialize builds a read object from a DB row, running the perform_extra_serialization / perform_extra_user_serialization hooks; de_serialize builds a DB row from a write payload, running perform_extra_deserialization. __exclude__ lists the model fields the base serializer skips so a hook can handle them instead.

Organization specs (resources/organization/spec.py)

Spec Role Fields exposed Notes
OrganizationBaseSpec shared id, active, org_type, name, description, metadata __model__ = Organization, __exclude__ = ["parent"]. org_type bound to OrganizationTypeChoices; description defaults ""; metadata defaults {}
OrganizationUpdateSpec write · update (inherits OrganizationBaseSpec) No extra fields — parent can't change on update
OrganizationWriteSpec write · create base + parent: UUID4 | null Validates parent exists (validate_parent_organization); on create, perform_extra_deserialization resolves parent from external_id (or sets None)
OrganizationReadSpec read · list base + level_cache, system_generated, has_children, parent: dict perform_extra_serialization sets id = external_id and parent = obj.get_parent_json() — see parent read shape
OrganizationRetrieveSpec read · detail OrganizationReadSpec + permissions: list[str], managing_organizations: list[dict] perform_extra_user_serialization fills permissions (via AuthorizationController.get_permission_on_organization for the requesting user), expands managing_organizations to nested OrganizationReadSpec JSON, and adds audit users (serialize_audit_users)

parent read shape

On read, OrganizationReadSpec.parent (and OrganizationRetrieveSpec.parent) is not a UUID — it's the nested object from get_parent_json(), sharing the shape of cached_parent_json: { id, name, description, org_type, metadata, parent (recursive), level_cache, cache_expiry }, or {} for root orgs. On write, parent is instead a plain UUID4 (the parent's external_id).

Membership specs (resources/organization/organization_user_spec.py)

Spec Role Fields exposed Notes
OrganizationUserBaseSpec shared (none) __model__ = OrganizationUser, __exclude__ = ["user", "role"]
OrganizationUserUpdateSpec write · update role: UUID4 validate_role requires the RoleModel to exist; perform_extra_deserialization resolves role from external_id. Only the role can change on update
OrganizationUserWriteSpec write · create role: UUID4 + user: UUID4 Extends the update spec; validate_user requires the User to exist. On create, resolves both user and role from external_id
OrganizationUserReadSpec read id: UUID4, user: dict, role: dict user = cached UserSpec JSON (model_from_cache); role = RoleReadSpec JSON (full permissions)
OrganizationUserExtendedReadSpec read id: UUID4, role: dict, organization: dict Used by OrganizationUser.get_cached_role_orgs(). organization = OrganizationReadSpec JSON; role = RoleReadMinimalSpec JSON (no permission list)

Nested membership JSON shapes (from resources/role/spec.py, resources/user/spec.py):

role  (RoleReadSpec):        { id: UUID, name, description, is_system: bool,
                               is_archived: bool, contexts: [RoleContext],
                               permissions: [{ name, description, slug, context }] }
role  (RoleReadMinimalSpec): same as above without `permissions`
user  (UserSpec):            cached user summary JSON

Methods & save behaviour

OrganizationCommonBase.save()

On insert (no id yet), the base saves the row, then calls set_organization_cache() to populate the tree caches. Updates save normally and don't recompute caches.

set_organization_cache() side effects when a parent is set:

  1. Computes parent_cache = parent.parent_cache + [parent.id] and level_cache = parent.level_cache + 1.
  2. Derives root_org from the parent (parent.root_org, or the parent itself if the parent is a root).
  3. If the parent had no children before, flips parent.has_children = True and persists only that field.

get_parent_json()

Returns the cached parent JSON while cached_parent_json is present and cache_expiry is still in the future. Otherwise it recursively rebuilds the nested parent record, stamps a new expiry (cache_expiry_days = 15), and persists cached_parent_json. Returns {} for root orgs. This is what OrganizationReadSpec.parent exposes on every read.

validate_uniqueness(queryset, pydantic_instance, model_instance)

Classmethod enforcing name uniqueness among siblings — those sharing the same root_org and level_cache. For a new org it derives level_cache/root_org from the supplied parent (external_id); a null parent means level_cache = 0 and a null root_org. The resource layer calls it before create/update.

OrganizationUser.save()

After saving, if the linked organization's org_type is role (OrganizationTypeChoices.role), it clears user.cached_role_orgs (sets it to None and persists that field) so the user's role-org cache rebuilds on the next read.

OrganizationUser.get_cached_role_orgs(user_id) is a classmethod that serializes all of a user's role-typed org memberships via OrganizationUserExtendedReadSpec.

API integration notes

Related

Source evidence

References are pinned to the private MANSOQ source repository. Repository access is required to open these links.