Skip to content

Commit

Permalink
Merge pull request #12032 from rtibbles/demogorgon
Browse files Browse the repository at this point in the history
Add customizable demographic field entry to facility admin interface
  • Loading branch information
marcellamaki authored Apr 1, 2024
2 parents 7c52206 + 8da3a77 commit 033234c
Show file tree
Hide file tree
Showing 18 changed files with 1,324 additions and 46 deletions.
1 change: 1 addition & 0 deletions kolibri/core/auth/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,7 @@ class FacilityUserViewSet(ValuesViewset):
"id_number",
"gender",
"birth_year",
"extra_demographics",
)

field_map = {
Expand Down
153 changes: 153 additions & 0 deletions kolibri/core/auth/constants/demographics.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
from __future__ import unicode_literals

from django.core.exceptions import ValidationError
from django.utils.deconstruct import deconstructible

from kolibri.core.utils.validators import JSON_Schema_Validator
from kolibri.core.utils.validators import NoRepeatedValueJSONArrayValidator
from kolibri.utils.i18n import KOLIBRI_SUPPORTED_LANGUAGES


MALE = "MALE"
FEMALE = "FEMALE"
NOT_SPECIFIED = "NOT_SPECIFIED"
Expand All @@ -14,3 +22,148 @@
)

DEMO_FIELDS = ("gender", "birth_year", "id_number")


# '"optional":True' is obsolete but needed while we keep using an
# old json_schema_validator version compatible with python 2.7.
# "additionalProperties": False must be avoided for backwards compatibility
translations_schema = {
"type": "array",
"items": {
"type": "object",
"properties": {
# KOLIBRI_SUPPORTED_LANGUAGES is a set of strings, so we use sorted
# to coerce it to a list with a consistent ordering.
# If we don't do this, every time we initialize, Django thinks it has changed
# and will try to create a new migration.
"language": {"type": "string", "enum": sorted(KOLIBRI_SUPPORTED_LANGUAGES)},
"message": {"type": "string"},
},
},
"optional": True,
}


custom_demographic_field_schema = {
"type": "object",
"properties": {
"id": {
"type": "string",
},
"description": {"type": "string"},
"enumValues": {
"type": "array",
"items": {
"type": "object",
"properties": {
"value": {"type": "string"},
"defaultLabel": {"type": "string"},
"translations": translations_schema,
},
},
},
"translations": translations_schema,
},
}


custom_demographics_schema = {
"type": "array",
"items": custom_demographic_field_schema,
"optional": True,
}


@deconstructible
class UniqueIdsValidator(NoRepeatedValueJSONArrayValidator):
def __init__(self, custom_demographics_key):
super(UniqueIdsValidator, self).__init__(
array_key=custom_demographics_key, object_key="id"
)


unique_translations_validator = NoRepeatedValueJSONArrayValidator(
array_key="translations",
object_key="language",
)


@deconstructible
class DescriptionTranslationValidator(object):
def __init__(self, custom_demographics_key):
self.custom_demographics_key = custom_demographics_key

def __call__(self, value):
for item in value.get(self.custom_demographics_key, []):
try:
unique_translations_validator(item)
except ValidationError:
raise ValidationError(
"User facing description translations for '{} ({})' must be unique by language".format(
item["description"], item["id"]
),
code="invalid",
)
return value


unique_value_validator = NoRepeatedValueJSONArrayValidator(
object_key="value",
)


@deconstructible
class EnumValuesValidator(object):
def __init__(self, custom_demographics_key):
self.custom_demographics_key = custom_demographics_key

def __call__(self, value):
for item in value.get(self.custom_demographics_key, []):
enum_values = item.get("enumValues", [])
try:
unique_value_validator(enum_values)
except ValidationError:
raise ValidationError(
"Possible values for '{} ({})' must be unique".format(
item["description"], item["id"]
),
code="invalid",
)
return value


@deconstructible
class LabelTranslationValidator(object):
def __init__(self, custom_demographics_key):
self.custom_demographics_key = custom_demographics_key

def __call__(self, value):
for item in value.get(self.custom_demographics_key, []):
for enumValue in item.get("enumValues", []):
try:
unique_translations_validator(enumValue)
except ValidationError:
raise ValidationError(
"User facing label translations for value '{} ({})' in '{} ({})' must be unique by language".format(
enumValue["defaultLabel"],
enumValue["value"],
item["description"],
item["id"],
),
code="invalid",
)
return value


class FacilityUserDemographicValidator(JSON_Schema_Validator):
def __init__(self, custom_schema):
schema = {
"type": "object",
"properties": {},
}
for field in custom_schema:
schema["properties"][field["id"]] = {
"type": "string",
"enum": [enum["value"] for enum in field["enumValues"]],
}
super(FacilityUserDemographicValidator, self).__init__(schema)
182 changes: 182 additions & 0 deletions kolibri/core/auth/migrations/0025_custom_demographic_schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.29 on 2024-03-28 15:34
from __future__ import unicode_literals

from django.db import migrations

import kolibri.core.auth.constants.demographics
import kolibri.core.fields
import kolibri.core.utils.validators


class Migration(migrations.Migration):

dependencies = [
("kolibriauth", "0024_extend_username_length"),
]

operations = [
migrations.AddField(
model_name="facilityuser",
name="extra_demographics",
field=kolibri.core.fields.JSONField(blank=True, null=True),
),
migrations.AlterField(
model_name="facilitydataset",
name="extra_fields",
field=kolibri.core.fields.JSONField(
blank=True,
default={"facility": {}, "on_my_own_setup": False, "pin_code": ""},
null=True,
validators=[
kolibri.core.utils.validators.JSON_Schema_Validator(
{
"properties": {
"demographic_fields": {
"items": {
"properties": {
"description": {"type": "string"},
"enumValues": {
"items": {
"properties": {
"defaultLabel": {
"type": "string"
},
"translations": {
"items": {
"properties": {
"language": {
"enum": [
"ar",
"bg-bg",
"bn-bd",
"de",
"el",
"en",
"es-419",
"es-es",
"fa",
"ff-cm",
"fr-fr",
"gu-in",
"ha",
"hi-in",
"ht",
"id",
"it",
"ka",
"km",
"ko",
"mr",
"my",
"nyn",
"pt-br",
"pt-mz",
"sw-tz",
"te",
"uk",
"ur-pk",
"vi",
"yo",
"zh-hans",
],
"type": "string",
},
"message": {
"type": "string"
},
},
"type": "object",
},
"optional": True,
"type": "array",
},
"value": {"type": "string"},
},
"type": "object",
},
"type": "array",
},
"id": {"type": "string"},
"translations": {
"items": {
"properties": {
"language": {
"enum": [
"ar",
"bg-bg",
"bn-bd",
"de",
"el",
"en",
"es-419",
"es-es",
"fa",
"ff-cm",
"fr-fr",
"gu-in",
"ha",
"hi-in",
"ht",
"id",
"it",
"ka",
"km",
"ko",
"mr",
"my",
"nyn",
"pt-br",
"pt-mz",
"sw-tz",
"te",
"uk",
"ur-pk",
"vi",
"yo",
"zh-hans",
],
"type": "string",
},
"message": {"type": "string"},
},
"type": "object",
},
"optional": True,
"type": "array",
},
},
"type": "object",
},
"optional": True,
"type": "array",
},
"facility": {"optional": True, "type": "object"},
"on_my_own_setup": {
"optional": True,
"type": "boolean",
},
"pin_code": {
"optional": True,
"type": ["string", "null"],
},
},
"type": "object",
}
),
kolibri.core.auth.constants.demographics.UniqueIdsValidator(
"demographic_fields"
),
kolibri.core.auth.constants.demographics.DescriptionTranslationValidator(
"demographic_fields"
),
kolibri.core.auth.constants.demographics.EnumValuesValidator(
"demographic_fields"
),
kolibri.core.auth.constants.demographics.LabelTranslationValidator(
"demographic_fields"
),
],
),
),
]
Loading

0 comments on commit 033234c

Please sign in to comment.