Skip to content

Commit

Permalink
Merge pull request #4560 from kobotoolbox/constance-with-i18n-json-su…
Browse files Browse the repository at this point in the history
…pport

Support Django Constance settings with translatable strings
  • Loading branch information
jnm committed Aug 1, 2023
2 parents e9e6b04 + ed949ad commit ef5a307
Show file tree
Hide file tree
Showing 14 changed files with 139 additions and 63 deletions.
5 changes: 4 additions & 1 deletion hub/utils/i18n.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from django.db.models.functions import Length
from django.utils.translation import get_language, gettext as t

from kobo.apps.constance_backends.utils import to_python_object
from kpi.utils.log import logging
from ..models import SitewideMessage

Expand Down Expand Up @@ -55,7 +56,9 @@ def get_mfa_help_text(lang=None):
language = lang if lang else get_language()

try:
messages_dict = json.loads(constance.config.MFA_LOCALIZED_HELP_TEXT)
messages_dict = to_python_object(
constance.config.MFA_LOCALIZED_HELP_TEXT
)
except json.JSONDecodeError:
logging.error(
'Configuration value for MFA_LOCALIZED_HELP_TEXT has invalid '
Expand Down
4 changes: 2 additions & 2 deletions kobo/apps/accounts/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
from django.conf import settings
from django.utils.translation import gettext_lazy as t

from kobo.apps.constance_backends.utils import to_python_object
from kobo.static_lists import COUNTRIES


# Only these fields can be controlled by constance.config.USER_METADATA_FIELDS
CONFIGURABLE_METADATA_FIELDS = (
'organization',
Expand Down Expand Up @@ -78,7 +78,7 @@ def __init__(self, *args, **kwargs):

# It's easier to _remove_ unwanted fields here in the constructor
# than to add a new fields *shrug*
desired_metadata_fields = json.loads(
desired_metadata_fields = to_python_object(
constance.config.USER_METADATA_FIELDS
)
desired_metadata_fields = {
Expand Down
20 changes: 20 additions & 0 deletions kobo/apps/constance_backends/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from __future__ import annotations

import json

from typing import Union
from kpi.utils.json import LazyJSONSerializable


def to_python_object(
object_or_str: Union['LazyJSONSerializable', str]
) -> Union[list, dict]:
"""
Returns a python object from `object_or_str`.
"""
# Make constance.config.SETTING return a consistent value when SETTING is a
# LazyJSONSerializable object
if isinstance(object_or_str, LazyJSONSerializable):
return object_or_str.object

return json.loads(object_or_str)
60 changes: 37 additions & 23 deletions kobo/settings/base.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
# coding: utf-8
import json
import logging
import os
import re
Expand All @@ -16,6 +15,7 @@
from django.utils.translation import get_language_info, gettext_lazy as t
from pymongo import MongoClient

from kpi.utils.json import LazyJSONSerializable
from ..static_lists import EXTRA_LANG_INFO, SECTOR_CHOICE_DEFAULTS

env = environ.Env()
Expand Down Expand Up @@ -233,11 +233,8 @@
'Enable two-factor authentication'
),
'MFA_LOCALIZED_HELP_TEXT': (
json.dumps({
'default': (
# It's terrible, but if you change this, you must also change
# `MFA_DEFAULT_HELP_TEXT` in `static_lists.py` so that
# translators receive the new string
LazyJSONSerializable({
'default': t(
'If you cannot access your authenticator app, please enter one '
'of your backup codes instead. If you cannot access those '
'either, then you will need to request assistance by '
Expand All @@ -248,7 +245,7 @@
'a valid language code, but this entry is here to show you '
'an example of adding another message in a different language.'
)
}, indent=0), # `indent=0` at least adds newlines
}),
(
'JSON object of guidance messages presented to users when they '
'click the "Problems with the token" link after being prompted for '
Expand All @@ -260,7 +257,7 @@
'for French).'
),
# Use custom field for schema validation
'mfa_help_text_fields_jsonschema'
'i18n_text_jsonfield_schema'
),
'ASR_MT_INVITEE_USERNAMES': (
'',
Expand All @@ -275,7 +272,7 @@
'authentication mechanism'
),
'USER_METADATA_FIELDS': (
json.dumps([
LazyJSONSerializable([
{'name': 'organization', 'required': False},
{'name': 'organization_website', 'required': False},
{'name': 'sector', 'required': False},
Expand All @@ -293,10 +290,10 @@
"'sector', 'gender', 'bio', 'city', 'country', 'twitter', 'linkedin', "
"and 'instagram'",
# Use custom field for schema validation
'metadata_fields_jsonschema'
'long_metadata_fields_jsonschema'
),
'PROJECT_METADATA_FIELDS': (
json.dumps([
LazyJSONSerializable([
{'name': 'sector', 'required': False},
{'name': 'country', 'required': False},
# {'name': 'operational_purpose', 'required': False},
Expand All @@ -311,7 +308,8 @@
),
'SECTOR_CHOICES': (
'\n'.join((s[0] for s in SECTOR_CHOICE_DEFAULTS)),
"Options available for the 'sector' metadata field, one per line."
"Options available for the 'sector' metadata field, one per line.",
'long_textfield'
),
'OPERATIONAL_PURPOSE_CHOICES': (
'',
Expand All @@ -324,7 +322,7 @@
'positive_int'
),
'FREE_TIER_THRESHOLDS': (
json.dumps({
LazyJSONSerializable({
'storage': None,
'data': None,
'transcription_minutes': None,
Expand All @@ -335,15 +333,13 @@
'minutes of transcription, '
'number of translation characters',
# Use custom field for schema validation
'free_tier_threshold_jsonschema'
'free_tier_threshold_jsonschema',
),
'FREE_TIER_DISPLAY': (
json.dumps(
{
'name': None,
'feature_list': [],
}
),
LazyJSONSerializable({
'name': None,
'feature_list': [],
}),
'Free tier frontend settings: name to use for the free tier, '
'array of text strings to display on the feature list of the Plans page',
'free_tier_display_jsonschema',
Expand Down Expand Up @@ -373,13 +369,31 @@
'kpi.fields.jsonschema_form_field.FreeTierDisplayField',
{'widget': 'django.forms.Textarea'},
],
'i18n_text_jsonfield_schema': [
'kpi.fields.jsonschema_form_field.I18nTextJSONField',
{'widget': 'django.forms.Textarea'},
],
'metadata_fields_jsonschema': [
'kpi.fields.jsonschema_form_field.MetadataFieldsListField',
{'widget': 'django.forms.Textarea'},
],
'mfa_help_text_fields_jsonschema': [
'kpi.fields.jsonschema_form_field.MfaHelpTextField',
{'widget': 'django.forms.Textarea'},
'long_metadata_fields_jsonschema': [
'kpi.fields.jsonschema_form_field.MetadataFieldsListField',
{
'widget': 'django.forms.Textarea',
'widget_kwargs': {
'attrs': {'rows': 45}
}
},
],
'long_textfield': [
'django.forms.fields.CharField',
{
'widget': 'django.forms.Textarea',
'widget_kwargs': {
'attrs': {'rows': 30}
}
},
],
'positive_int': ['django.forms.fields.IntegerField', {
'min_value': 0
Expand Down
5 changes: 3 additions & 2 deletions kobo/settings/testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,9 @@
# Do not use cache with Constance in tests to avoid overwriting production
# cached values
CONSTANCE_DATABASE_CACHE_BACKEND = None
if "djstripe" not in INSTALLED_APPS:
INSTALLED_APPS += ('djstripe', "kobo.apps.stripe")

if 'djstripe' not in INSTALLED_APPS:
INSTALLED_APPS += ('djstripe', 'kobo.apps.stripe')
STRIPE_ENABLED = True

WEBPACK_LOADER['DEFAULT'][
Expand Down
10 changes: 0 additions & 10 deletions kobo/static_lists.py
Original file line number Diff line number Diff line change
Expand Up @@ -339,13 +339,3 @@
'name_local': 'Nyanja',
},
}

MFA_DEFAULT_HELP_TEXT = t(
# It's terrible, but this duplicates the default value from
# `MFA_LOCALIZED_HELP_TEXT` in django-constance because it's impossible to
# use `gettext()` or `gettext_lazy()` there.
'If you cannot access your authenticator app, please enter one '
'of your backup codes instead. If you cannot access those '
'either, then you will need to request assistance by '
'contacting [##support email##](mailto:##support email##).'
)
2 changes: 1 addition & 1 deletion kpi/fields/jsonschema_form_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ def __init__(self, *args, **kwargs):
super().__init__(*args, schema=schema, **kwargs)


class MfaHelpTextField(JsonSchemaFormField):
class I18nTextJSONField(JsonSchemaFormField):
"""
Validates that the input is an object which contains at least the 'default'
key.
Expand Down
4 changes: 3 additions & 1 deletion kpi/migrations/0051_set_free_tier_thresholds_to_default.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
from constance import config
from django.db import migrations

from kpi.utils.json import LazyJSONSerializable


def reset_free_tier_thresholds(apps, schema_editor):
# The constance defaults for FREE_TIER_THRESHOLDS changed, so we set existing config to the new default value
Expand All @@ -12,7 +14,7 @@ def reset_free_tier_thresholds(apps, schema_editor):
'transcription_minutes': None,
'translation_chars': None,
}
setattr(config, 'FREE_TIER_THRESHOLDS', json.dumps(thresholds))
setattr(config, 'FREE_TIER_THRESHOLDS', LazyJSONSerializable(thresholds))


class Migration(migrations.Migration):
Expand Down
4 changes: 2 additions & 2 deletions kpi/serializers/current_user.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# coding: utf-8
import datetime
import json
try:
from zoneinfo import ZoneInfo
except ImportError:
Expand All @@ -15,6 +14,7 @@

from hub.models import ExtraUserDetail
from kobo.apps.accounts.serializers import SocialAccountSerializer
from kobo.apps.constance_backends.utils import to_python_object
from kpi.deployment_backends.kc_access.utils import get_kc_profile_data
from kpi.deployment_backends.kc_access.utils import set_kc_require_auth
from kpi.fields import WritableJSONField
Expand Down Expand Up @@ -151,7 +151,7 @@ def validate(self, attrs):
return attrs

def validate_extra_details(self, value):
desired_metadata_fields = json.loads(
desired_metadata_fields = to_python_object(
constance.config.USER_METADATA_FIELDS
)

Expand Down
7 changes: 7 additions & 0 deletions kpi/templates/admin/constance/change_list.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{% extends "admin/constance/change_list.html" %}
{% load static %}

{% block extrastyle %}
{{ block.super }}
<link href="{% static 'admin/constance/css/kobo-constance.css' %}" rel="stylesheet" type="text/css" />
{% endblock %}
29 changes: 10 additions & 19 deletions kpi/tests/api/test_api_environment.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
# coding: utf-8
# 😇
import json

import constance
import mock
from constance.test import override_config
Expand All @@ -15,9 +13,10 @@
from model_bakery import baker
from rest_framework import status

from hub.utils.i18n import I18nUtils
from kobo.apps.accounts.mfa.models import MfaAvailableToUser
from kobo.apps.constance_backends.utils import to_python_object
from kobo.apps.hook.constants import SUBMISSION_PLACEHOLDER
from kobo.static_lists import MFA_DEFAULT_HELP_TEXT
from kpi.tests.base_test_case import BaseTestCase


Expand All @@ -37,10 +36,11 @@ def setUp(self):
'frontend_max_retry_time': constance.config.FRONTEND_MAX_RETRY_TIME,
'project_metadata_fields': lambda x: self.assertEqual(
len(x),
len(json.loads(constance.config.PROJECT_METADATA_FIELDS)),
len(to_python_object(constance.config.PROJECT_METADATA_FIELDS)),
) and self.assertIn({'name': 'organization', 'required': False}, x),
'user_metadata_fields': lambda x: self.assertEqual(
len(x), len(json.loads(constance.config.USER_METADATA_FIELDS))
len(x),
len(to_python_object(constance.config.USER_METADATA_FIELDS))
) and self.assertIn({'name': 'sector', 'required': False}, x),
'sector_choices': lambda x: self.assertGreater(
len(x), 10
Expand All @@ -62,18 +62,20 @@ def setUp(self):
'asr_mt_features_enabled': False,
'mfa_enabled': constance.config.MFA_ENABLED,
'mfa_localized_help_text': markdown(
MFA_DEFAULT_HELP_TEXT.replace(
I18nUtils.get_mfa_help_text().replace(
'##support email##', constance.config.SUPPORT_EMAIL
)
),
'mfa_code_length': settings.TRENCH_AUTH['CODE_LENGTH'],
'stripe_public_key': (
settings.STRIPE_PUBLIC_KEY if settings.STRIPE_ENABLED else None
),
'free_tier_thresholds': json.loads(
'free_tier_thresholds': to_python_object(
constance.config.FREE_TIER_THRESHOLDS
),
'free_tier_display': json.loads(constance.config.FREE_TIER_DISPLAY),
'free_tier_display': to_python_object(
constance.config.FREE_TIER_DISPLAY
),
'social_apps': [],
}

Expand Down Expand Up @@ -107,17 +109,6 @@ def test_template_context_processor(self):
result = template.render(context)
self.assertEqual(result, constance.config.TERMS_OF_SERVICE_URL)

def test_mfa_help_text_default_translation(self):
MOCK_TRANSLATION_STRING = 'hello from gettext'
with mock.patch(
'hub.utils.i18n.t', return_value=MOCK_TRANSLATION_STRING
) as mock_t:
response = self.client.get(self.url, format='json')
assert response.json()['mfa_localized_help_text'] == markdown(
MOCK_TRANSLATION_STRING
)
assert mock_t.call_args.args[0] == MFA_DEFAULT_HELP_TEXT

@override_config(MFA_ENABLED=True)
def test_mfa_value_globally_enabled(self):
self.client.login(username='someuser', password='someuser')
Expand Down

0 comments on commit ef5a307

Please sign in to comment.