Access & roles
User & Skills
A User is a MANSOQ account — authentication, profile, clinician credentials, and notification/MFA data. The Django model is the storage layer. The Pydantic resource specs in care/emr/resources/user/ and care/emr/resources/mfa/ are the API…
3.1 · versioned documentation · 11 min read
A User is a MANSOQ account — authentication, profile, clinician credentials, and notification/MFA data. The Django model is the storage layer. The Pydantic resource specs in care/emr/resources/user/ and care/emr/resources/mfa/ are the API layer: they define enums, validation, the shape of the opaque JSONFields, and the separate request/response schemas you write to and read from.
Source: care/users/models.py, care/facility/models/patient.py, care/emr/resources/user/spec.py, care/emr/resources/mfa/spec.py
Models
| Model | Purpose |
|---|---|
User |
MANSOQ account: authentication, profile, clinician credentials, and notification/MFA data |
Skill |
Instance-wide skill/competency definition |
UserSkill |
Through model linking a User to a Skill |
UserFlag |
Feature flag scoped to a single User |
PlugConfig |
Slug-keyed JSON configuration for installed plugs |
MobileOTP |
One-time password issued against a phone number |
The base class determines which audit fields each model carries:
Userextends Django'sAbstractUser, adding its ownexternal_idand a soft-deletedeletedflag. It is not anEMRBaseModel.Skill,UserSkill, andMobileOTPextendBaseModel(external_id,created_date,modified_date, soft-delete viadeleted).UserFlagextendsBaseFlag, aBaseModelsubclass that adds aflagfield plus a cache-invalidatingsave().PlugConfigextends plaindjango.db.models.Model— no audit or soft-delete fields.
User fields
User inherits password, email, first_name, last_name, is_staff, is_active, is_superuser, last_login, and date_joined from AbstractUser. The fields below are MANSOQ additions or overrides. Queries run through CustomUserManager, which drops deleted=True rows by default; get_entire_queryset() bypasses that filter.
Identity & profile
| Field | Type | Notes |
|---|---|---|
external_id |
UUIDField |
Unique, indexed; stable public identifier. Surfaced as id in every read spec |
username |
CharField(150) |
Unique; model-validated by UsernameValidator. On create, the API additionally enforces ^[a-zA-Z0-9_-]{3,}$ and global uniqueness, including deleted users |
user_type |
CharField(100) |
Nullable; free-text role label (e.g. administrator). No user spec exposes it — set internally or by create_superuser |
prefix |
CharField(10) |
Name prefix (e.g. Dr.); spec caps length at 10. Optional in specs |
suffix |
CharField(50) |
Name suffix; spec caps length at 50. Optional in specs |
gender |
CharField(100) |
Nullable in DB. Write specs constrain it to GenderChoices and require it; read specs return it as a plain string. See GenderChoices values |
old_gender |
IntegerField |
Legacy GENDER_CHOICES (1 Male, 2 Female, 3 Non-binary); nullable. No user spec exposes it |
date_of_birth |
DateField |
Nullable. Read-only (nullable string) in CurrentUserRetrieveSpec |
profile_picture_url |
CharField(500) |
Storage key, not a URL. Read specs return the resolved URL via read_profile_picture_url(), not the raw key |
created_by |
FK → User (self, SET_NULL) |
Account creator; related_name="users_created". Serialized in UserRetrieveSpec as a cached nested UserSpec dict (nullable) |
deleted |
BooleanField |
Soft-delete flag (default False); read-only in UserSpec |
verified |
BooleanField |
Default False; read-only in CurrentUserRetrieveSpec |
is_service_account |
BooleanField |
Default False; marks machine/integration accounts. Settable on create, read in UserRetrieveSpec |
Contact
| Field | Type | Notes |
|---|---|---|
phone_number |
CharField(14) |
Model: mobile_or_landline_number_validator. Required in specs, capped at 14 chars; must be globally unique on create |
alt_phone_number |
CharField(14) |
Nullable; model mobile_validator. Read-only string in CurrentUserRetrieveSpec |
video_connect_link |
URLField |
Nullable; tele-consult link. No user spec exposes it |
Clinician credentials
| Field | Type | Notes |
|---|---|---|
qualification |
TextField |
Nullable. Read-only (nullable string) in CurrentUserRetrieveSpec |
doctor_experience_commenced_on |
DateField |
Nullable; experience is derived from this. Read-only (nullable string) in CurrentUserRetrieveSpec |
doctor_medical_council_registration |
CharField(255) |
Nullable; council registration number. Read-only (nullable string) in CurrentUserRetrieveSpec |
weekly_working_hours |
IntegerField |
Nullable; model-validated 0–168. Read-only (nullable string) in CurrentUserRetrieveSpec |
Organization & facility
| Field | Type | Notes |
|---|---|---|
geo_organization |
FK → emr.Organization (SET_NULL) |
Geographic/administrative org. Write specs accept a UUID4 and resolve it to an Organization with org_type="govt" (404 otherwise). Read in UserRetrieveSpec as a nested OrganizationReadSpec dict. Listed in UserBaseSpec.__exclude__, so it never round-trips through the generic field copy |
home_facility |
FK → facility.Facility (PROTECT) |
Primary facility. No user spec exposes it |
skills |
ManyToManyField → Skill |
Through UserSkill |
cached_role_orgs |
JSONField |
Nullable; cached role/organization map. Lazily populated by get_cached_role_orgs() and surfaced as role_orgs in read specs. Platform-maintained — do not write directly |
Notifications, MFA & preferences
| Field | Type | Notes |
|---|---|---|
pf_endpoint |
TextField |
Web-push endpoint; nullable. Read-only in CurrentUserRetrieveSpec |
pf_p256dh |
TextField |
Web-push key; nullable. Read-only in CurrentUserRetrieveSpec |
pf_auth |
TextField |
Web-push auth secret; nullable. Read-only in CurrentUserRetrieveSpec |
totp_secret |
TextField |
Nullable; TOTP seed. Never serialized; written only via the MFA setup/verify flow |
mfa_settings |
JSONField |
Default {}. Shape: { "totp": { "enabled": bool, ... } }. is_mfa_enabled() reads mfa_settings["totp"]["enabled"]; surfaced as the boolean mfa_enabled in UserSpec. Clients never write it directly |
preferences |
JSONField |
Default {}; open per-user UI/app preferences bag. Read-only dict in CurrentUserRetrieveSpec |
REQUIRED_FIELDS = ["email"], and the model is managed by CustomUserManager.
Enum values
GenderChoices values
Bound to gender on every write spec (UserUpdateSpec, UserCreateSpec). Defined in care/emr/resources/patient/spec.py and reused here; read specs return gender as a free string.
| Value |
|---|
male |
female |
non_binary |
transgender |
create_superuser() sets gender="non_binary", which is a member of this enum. The model's legacy integer GENDER_CHOICES (1 Male, 2 Female, 3 Non-binary) is separate and unused by the API.
LoginMethod values
Used by MFALoginRequest.method in care/emr/resources/mfa/spec.py.
| Value | Meaning |
|---|---|
totp |
Authenticator app one-time code |
backup |
Single-use backup recovery code |
Resource specs (API schema)
Every user spec builds on EMRResource (care/emr/resources/base.py), which provides serialize (DB → pydantic) and de_serialize (pydantic → DB) plus the perform_extra_serialization / perform_extra_deserialization hooks. UserBaseSpec sets __model__ = User and __exclude__ = ["geo_organization"].
| Spec class | Role | Fields / behaviour |
|---|---|---|
UserBaseSpec |
shared base | id, first_name, last_name, phone_number (max 14), prefix (max 10, optional), suffix (max 50, optional) |
UserUpdateSpec |
write · update | Base + gender (GenderChoices, required), phone_number (max 14), geo_organization (UUID4, optional). De-serialize resolves geo_organization to an Organization with org_type="govt" (404 if missing) |
UserCreateSpec |
write · create | Extends UserUpdateSpec + password (optional), username, email, is_service_account (default False), role_orgs: list[UserRoleOrgCreateSpec]. Validates username pattern/uniqueness, phone uniqueness, email format/uniqueness, and password strength. De-serialize stashes role_orgs on the instance and calls set_password() |
UserRoleOrgCreateSpec |
write · nested | organization: UUID4, role: UUID4 — one role-in-organization assignment supplied at user creation |
UserSpec |
read · list/summary | Base + last_login, profile_picture_url, gender (string), username, mfa_enabled (default False), phone_number, deleted (default False), role_orgs: dict. @cacheable(use_base_manager=True) — cached, and resolves deleted users. Sets id from external_id, resolves the picture URL, computes mfa_enabled, loads role_orgs |
UserRetrieveSpec |
read · detail | Extends UserSpec + geo_organization: dict, created_by: dict | None, email, flags: list[str], is_service_account. Serializes created_by (cached UserSpec), geo_organization (OrganizationReadSpec), and flags (get_all_flags()) |
CurrentUserRetrieveSpec |
read · self (/users/getcurrentuser-style) |
Extends UserRetrieveSpec + is_superuser, qualification, doctor_experience_commenced_on, doctor_medical_council_registration, weekly_working_hours, alt_phone_number, date_of_birth (all nullable strings), verified, pf_endpoint/pf_p256dh/pf_auth, organizations: list[dict], facilities: list[dict], permissions: list[str], preferences: dict. Computes the caller's organizations, facilities (excluding deleted), and resolved permission slugs |
PublicUserReadSpec |
read · public | Base + last_login, profile_picture_url, gender, username, role_orgs: list[dict]. Minimal unauthenticated projection |
The password-reset flow uses plain-BaseModel request/response DTOs (not EMRResource, no model binding), all in user/spec.py:
| DTO | Shape |
|---|---|
ResetPasswordCheckRequest |
{ token: str } |
ResetPasswordConfirmRequest |
{ token: str, password: str } |
ResetPasswordResponse |
{ detail: str } |
ResetPasswordRequestTokenRequest |
{ username: str } |
MFA specs
Plain-BaseModel DTOs in care/emr/resources/mfa/spec.py. They carry no model binding and drive the TOTP/backup-code flow that writes totp_secret and mfa_settings.
| DTO | Direction | Shape |
|---|---|---|
TOTPSetupResponse |
response | { uri: str, secret_key: str } |
TOTPVerifyRequest |
request | { code: str } |
TOTPVerifyResponse |
response | { backup_codes: list[str] } |
PasswordVerifyRequest |
request | { password: str } |
MFALoginRequest |
request | { method: LoginMethod, code: str, temp_token: str } |
MFALoginResponse |
response | { access: str, refresh: str } (JWT pair) |
Validation rules (write specs)
| Field | Rule | Source |
|---|---|---|
username |
Matches ^[a-zA-Z0-9_-]{3,}$; must not already exist (checked against the entire queryset, including deleted) |
UserCreateSpec.validate_username |
phone_number |
Must not already exist on any user | UserCreateSpec.validate_phone_number |
email |
Must not already exist; must pass Django validate_email |
UserCreateSpec.validate_user_email |
password |
If provided, must pass Django validate_password (else "Password is too weak"); None is allowed |
UserCreateSpec.validate_password |
gender |
Must be a GenderChoices member |
type annotation |
geo_organization |
Must reference an existing Organization with org_type="govt" |
UserUpdateSpec.perform_extra_deserialization |
Server-maintained behaviour
role_orgson create —UserCreateSpec.perform_extra_deserializationcopiesrole_orgsontoobj._role_orgsso the view can apply role/organization assignments after the user is saved. It is not a DB column.set_password— create hashes the supplied password viaobj.set_password(self.password), or sets an unusable password when it isNone.- Cache —
UserSpecis@cacheable(use_base_manager=True). Saving aUserinvalidates the cache viapost_save, andcreated_byis read throughmodel_from_cache(UserSpec, ...).use_base_manager=Truelets cached lookups resolve soft-deleted users. - Computed read fields —
id(fromexternal_id),profile_picture_url(resolved),mfa_enabled,role_orgs,flags,geo_organization,created_by, and the current-userorganizations/facilities/permissionsare all populated inperform_extra_serialization, never copied straight from columns.
Shared spec types
Defined in care/emr/resources/base.py and care/emr/resources/common/. No user spec binds these period and contact types; they belong to the shared resource vocabulary documented here for cross-reference.
| Type | Shape | Source |
|---|---|---|
PeriodSpec |
{ start: datetime | None, end: datetime | None } — both must be timezone-aware; start ≤ end enforced |
base.py |
Period |
{ id: str?, start: datetime?, end: datetime? }, extra="forbid" |
common/period.py |
ContactPoint |
{ system: ContactPointSystemChoices, value: str, use: ContactPointUseChoices } |
common/contact_point.py |
PhoneNumber |
E.164 string (PhoneNumberValidator) |
base.py |
ContactPointSystemChoices: phone, fax, email, pager, url, sms, other. ContactPointUseChoices: home, work, temp, old, mobile.
Related models
Skill
Instance-wide competency definition. Extends BaseModel.
name → CharField(255), unique
description → TextField (nullable, default "")
UserSkill
Through model joining User and Skill. Extends BaseModel.
user → FK User (CASCADE, nullable)
skill → FK Skill (CASCADE, nullable)
The unique_user_skill constraint blocks duplicate (skill, user) pairs among non-deleted rows.
UserFlag
Feature flag attached to a single user. Extends BaseFlag (a BaseModel subclass with a flag field).
user → FK User (CASCADE)
flag → CharField(1024) (inherited from BaseFlag)
flag_type = FlagType.USER. The unique_user_flag constraint blocks duplicate (user, flag) pairs among non-deleted rows. Flags are read through UserFlag.check_user_has_flag(user_id, flag_name) and get_all_flags(user_id), both cache-backed (TTL 1 day). UserRetrieveSpec.flags surfaces these names as a list[str].
PlugConfig
Slug-keyed configuration for installed plugs. Extends plain models.Model (no audit/soft-delete fields).
slug → CharField(255), unique
meta → JSONField (default {})
MobileOTP
Defined in care/facility/models/patient.py. One-time password issued against a phone number for verification flows. Extends BaseModel.
is_used → BooleanField (default False)
phone_number → CharField(14) (mobile_or_landline_number_validator)
otp → CharField(10)
Methods & save behaviour
User overrides neither save() nor delete(). Its helpers back the computed read fields:
get_cached_role_orgs()— returnscached_role_orgsif set; otherwise loads fromOrganizationUser.get_cached_role_orgs(self.id)and persists it viasave(update_fields=["cached_role_orgs"]). Read specs surface the result asrole_orgs.read_profile_picture_url()— resolvesprofile_picture_urlagainstFACILITY_CDNor the S3 bucket endpoint; returnsNonewhen unset. Backs every read spec'sprofile_picture_url.is_mfa_enabled()—Truewhenmfa_settings["totp"]["enabled"]is set; surfaced asmfa_enabled.full_name(property) — joinsprefix,get_full_name(), andsuffix.check_username_exists(username)(static) — checks across the entire (including deleted) queryset; used byUserCreateSpec.validate_username.get_all_flags()— delegates toUserFlag.get_all_flags(self.id); surfaced asflagsinUserRetrieveSpec.
CustomUserManager adds:
get_queryset()filtersdeleted=False;get_entire_queryset()returns all rows.create_superuser()forcesphone_number="+919696969696",gender="non_binary", anduser_type="administrator".make_random_password()generates a secure password guaranteeing lower/upper/digit composition.
UserFlag.save() (via BaseFlag) validates the flag name against the registry and invalidates the per-flag and all-flags cache keys on every write.
API integration notes
- The public identifier is
external_id, returned asid. Never key on the integer PK orusernamealone. CustomUserManagerhonoursdeleted: soft-deleted users drop out of normal queries, but cachedUserSpeclookups use the base manager (use_base_manager=True) and can still resolve them — for example as acreated_byreference.- Authorization is not stored on
User. Access resolves through the roles/permissions/organization system, andcached_role_orgsis a platform-maintained cache — clients must not write it. New users supplyrole_orgs(organization + role UUIDs) at create time. mfa_settings,preferences, andPlugConfig.metaare open JSON bags for evolving config without migrations.mfa_settingsis mutated by the MFA flow (mfa/spec.py), not by user create/update.is_service_accountdistinguishes machine/integration accounts from human users.- Write through
UserCreateSpec/UserUpdateSpec; read throughUserSpec(list),UserRetrieveSpec(detail),CurrentUserRetrieveSpec(self), andPublicUserReadSpec(public). Picture URLs, MFA status, roles, flags, and nested org/creator objects are all computed server-side.
Related
- Reference: Role, Permission, Permission association
- Reference: Organization, Facility
- Concept: Patient (
GenderChoicesis shared with the patient resource) - Base: Base model
- Source: users/models.py, facility/models/patient.py, emr/resources/user/spec.py, emr/resources/mfa/spec.py, emr/resources/base.py
Source evidence
References are pinned to the private MANSOQ source repository. Repository access is required to open these links.