User & Skills
A User is a Care 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 | Care 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 Care 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