Skip to main content
Version: 3.0

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

ModelPurpose
UserCare account: authentication, profile, clinician credentials, and notification/MFA data
SkillInstance-wide skill/competency definition
UserSkillThrough model linking a User to a Skill
UserFlagFeature flag scoped to a single User
PlugConfigSlug-keyed JSON configuration for installed plugs
MobileOTPOne-time password issued against a phone number

The base class determines which audit fields each model carries:

  • User extends Django's AbstractUser, adding its own external_id and a soft-delete deleted flag. It is not an EMRBaseModel.
  • Skill, UserSkill, and MobileOTP extend BaseModel (external_id, created_date, modified_date, soft-delete via deleted).
  • UserFlag extends BaseFlag, a BaseModel subclass that adds a flag field plus a cache-invalidating save().
  • PlugConfig extends plain django.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

FieldTypeNotes
external_idUUIDFieldUnique, indexed; stable public identifier. Surfaced as id in every read spec
usernameCharField(150)Unique; model-validated by UsernameValidator. On create, the API additionally enforces ^[a-zA-Z0-9_-]{3,}$ and global uniqueness, including deleted users
user_typeCharField(100)Nullable; free-text role label (e.g. administrator). No user spec exposes it — set internally or by create_superuser
prefixCharField(10)Name prefix (e.g. Dr.); spec caps length at 10. Optional in specs
suffixCharField(50)Name suffix; spec caps length at 50. Optional in specs
genderCharField(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_genderIntegerFieldLegacy GENDER_CHOICES (1 Male, 2 Female, 3 Non-binary); nullable. No user spec exposes it
date_of_birthDateFieldNullable. Read-only (nullable string) in CurrentUserRetrieveSpec
profile_picture_urlCharField(500)Storage key, not a URL. Read specs return the resolved URL via read_profile_picture_url(), not the raw key
created_byFK → User (self, SET_NULL)Account creator; related_name="users_created". Serialized in UserRetrieveSpec as a cached nested UserSpec dict (nullable)
deletedBooleanFieldSoft-delete flag (default False); read-only in UserSpec
verifiedBooleanFieldDefault False; read-only in CurrentUserRetrieveSpec
is_service_accountBooleanFieldDefault False; marks machine/integration accounts. Settable on create, read in UserRetrieveSpec

Contact

FieldTypeNotes
phone_numberCharField(14)Model: mobile_or_landline_number_validator. Required in specs, capped at 14 chars; must be globally unique on create
alt_phone_numberCharField(14)Nullable; model mobile_validator. Read-only string in CurrentUserRetrieveSpec
video_connect_linkURLFieldNullable; tele-consult link. No user spec exposes it

Clinician credentials

FieldTypeNotes
qualificationTextFieldNullable. Read-only (nullable string) in CurrentUserRetrieveSpec
doctor_experience_commenced_onDateFieldNullable; experience is derived from this. Read-only (nullable string) in CurrentUserRetrieveSpec
doctor_medical_council_registrationCharField(255)Nullable; council registration number. Read-only (nullable string) in CurrentUserRetrieveSpec
weekly_working_hoursIntegerFieldNullable; model-validated 0168. Read-only (nullable string) in CurrentUserRetrieveSpec

Organization & facility

FieldTypeNotes
geo_organizationFK → 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_facilityFK → facility.Facility (PROTECT)Primary facility. No user spec exposes it
skillsManyToManyField → SkillThrough UserSkill
cached_role_orgsJSONFieldNullable; 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

FieldTypeNotes
pf_endpointTextFieldWeb-push endpoint; nullable. Read-only in CurrentUserRetrieveSpec
pf_p256dhTextFieldWeb-push key; nullable. Read-only in CurrentUserRetrieveSpec
pf_authTextFieldWeb-push auth secret; nullable. Read-only in CurrentUserRetrieveSpec
totp_secretTextFieldNullable; TOTP seed. Never serialized; written only via the MFA setup/verify flow
mfa_settingsJSONFieldDefault {}. 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
preferencesJSONFieldDefault {}; 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.

ValueMeaning
totpAuthenticator app one-time code
backupSingle-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 classRoleFields / behaviour
UserBaseSpecshared baseid, first_name, last_name, phone_number (max 14), prefix (max 10, optional), suffix (max 50, optional)
UserUpdateSpecwrite · updateBase + 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)
UserCreateSpecwrite · createExtends 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()
UserRoleOrgCreateSpecwrite · nestedorganization: UUID4, role: UUID4 — one role-in-organization assignment supplied at user creation
UserSpecread · list/summaryBase + 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
UserRetrieveSpecread · detailExtends 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())
CurrentUserRetrieveSpecread · 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
PublicUserReadSpecread · publicBase + 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:

DTOShape
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.

DTODirectionShape
TOTPSetupResponseresponse{ uri: str, secret_key: str }
TOTPVerifyRequestrequest{ code: str }
TOTPVerifyResponseresponse{ backup_codes: list[str] }
PasswordVerifyRequestrequest{ password: str }
MFALoginRequestrequest{ method: LoginMethod, code: str, temp_token: str }
MFALoginResponseresponse{ access: str, refresh: str } (JWT pair)

Validation rules (write specs)

FieldRuleSource
usernameMatches ^[a-zA-Z0-9_-]{3,}$; must not already exist (checked against the entire queryset, including deleted)UserCreateSpec.validate_username
phone_numberMust not already exist on any userUserCreateSpec.validate_phone_number
emailMust not already exist; must pass Django validate_emailUserCreateSpec.validate_user_email
passwordIf provided, must pass Django validate_password (else "Password is too weak"); None is allowedUserCreateSpec.validate_password
genderMust be a GenderChoices membertype annotation
geo_organizationMust reference an existing Organization with org_type="govt"UserUpdateSpec.perform_extra_deserialization

Server-maintained behaviour

  • role_orgs on create — UserCreateSpec.perform_extra_deserialization copies role_orgs onto obj._role_orgs so 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 via obj.set_password(self.password), or sets an unusable password when it is None.
  • Cache — UserSpec is @cacheable(use_base_manager=True). Saving a User invalidates the cache via post_save, and created_by is read through model_from_cache(UserSpec, ...). use_base_manager=True lets cached lookups resolve soft-deleted users.
  • Computed read fields — id (from external_id), profile_picture_url (resolved), mfa_enabled, role_orgs, flags, geo_organization, created_by, and the current-user organizations/facilities/permissions are all populated in perform_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.

TypeShapeSource
PeriodSpec{ start: datetime | None, end: datetime | None } — both must be timezone-aware; start ≤ end enforcedbase.py
Period{ id: str?, start: datetime?, end: datetime? }, extra="forbid"common/period.py
ContactPoint{ system: ContactPointSystemChoices, value: str, use: ContactPointUseChoices }common/contact_point.py
PhoneNumberE.164 string (PhoneNumberValidator)base.py

ContactPointSystemChoices: phone, fax, email, pager, url, sms, other. ContactPointUseChoices: home, work, temp, old, mobile.

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() — returns cached_role_orgs if set; otherwise loads from OrganizationUser.get_cached_role_orgs(self.id) and persists it via save(update_fields=["cached_role_orgs"]). Read specs surface the result as role_orgs.
  • read_profile_picture_url() — resolves profile_picture_url against FACILITY_CDN or the S3 bucket endpoint; returns None when unset. Backs every read spec's profile_picture_url.
  • is_mfa_enabled()True when mfa_settings["totp"]["enabled"] is set; surfaced as mfa_enabled.
  • full_name (property) — joins prefix, get_full_name(), and suffix.
  • check_username_exists(username) (static) — checks across the entire (including deleted) queryset; used by UserCreateSpec.validate_username.
  • get_all_flags() — delegates to UserFlag.get_all_flags(self.id); surfaced as flags in UserRetrieveSpec.

CustomUserManager adds:

  • get_queryset() filters deleted=False; get_entire_queryset() returns all rows.
  • create_superuser() forces phone_number="+919696969696", gender="non_binary", and user_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 as id. Never key on the integer PK or username alone.
  • CustomUserManager honours deleted: soft-deleted users drop out of normal queries, but cached UserSpec lookups use the base manager (use_base_manager=True) and can still resolve them — for example as a created_by reference.
  • Authorization is not stored on User. Access resolves through the roles/permissions/organization system, and cached_role_orgs is a platform-maintained cache — clients must not write it. New users supply role_orgs (organization + role UUIDs) at create time.
  • mfa_settings, preferences, and PlugConfig.meta are open JSON bags for evolving config without migrations. mfa_settings is mutated by the MFA flow (mfa/spec.py), not by user create/update.
  • is_service_account distinguishes machine/integration accounts from human users.
  • Write through UserCreateSpec / UserUpdateSpec; read through UserSpec (list), UserRetrieveSpec (detail), CurrentUserRetrieveSpec (self), and PublicUserReadSpec (public). Picture URLs, MFA status, roles, flags, and nested org/creator objects are all computed server-side.