Skip to content

Commit

Permalink
Merge pull request #4497 from kobotoolbox/add-label-to-user-projects-…
Browse files Browse the repository at this point in the history
…metadata

Add labels to User and Project metadata
  • Loading branch information
jnm committed Jul 27, 2023
2 parents e9e6b04 + 1533a42 commit 4951343
Show file tree
Hide file tree
Showing 5 changed files with 216 additions and 19 deletions.
2 changes: 1 addition & 1 deletion jsapp/scss/stylesheets/partials/_registration.scss
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@
}

.sector,
.name {
.full_name {
width: 48%;
float: left;
margin-top: 0px;
Expand Down
32 changes: 24 additions & 8 deletions kobo/apps/accounts/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@
from allauth.socialaccount.forms import SignupForm as BaseSocialSignupForm
from django import forms
from django.conf import settings
from django.utils.translation import get_language
from django.utils.translation import gettext_lazy as t

from kobo.static_lists import COUNTRIES


# Only these fields can be controlled by constance.config.USER_METADATA_FIELDS
CONFIGURABLE_METADATA_FIELDS = (
'full_name',
'organization',
'gender',
'sector',
Expand All @@ -29,7 +31,7 @@ def __init__(self, *args, **kwargs):


class KoboSignupMixin(forms.Form):
name = forms.CharField(
full_name = forms.CharField(
label=t('Full name'),
required=False,
)
Expand Down Expand Up @@ -86,14 +88,28 @@ def __init__(self, *args, **kwargs):
}
for field_name in list(self.fields.keys()):
if field_name not in CONFIGURABLE_METADATA_FIELDS:
# This field is not allowed to be configured
continue
if field_name not in desired_metadata_fields:

try:
desired_field = desired_metadata_fields[field_name]
except KeyError:
# This field is unwanted
self.fields.pop(field_name)
continue
else:
self.fields[field_name].required = desired_metadata_fields[
field_name
].get('required', False)

field = self.fields[field_name]
field.required = desired_field.get('required', False)

if 'label' in desired_field.keys():
try:
self.fields[field_name].label = desired_field['label'][
get_language()
]
except KeyError:
self.fields[field_name].label = desired_field['label'][
'default'
]

def clean_email(self):
email = self.cleaned_data['email']
Expand All @@ -117,7 +133,7 @@ class SocialSignupForm(KoboSignupMixin, BaseSocialSignupForm):
field_order = [
'username',
'email',
'name',
'full_name',
'gender',
'sector',
'country',
Expand All @@ -133,7 +149,7 @@ def __init__(self, *args, **kwargs):

class SignupForm(KoboSignupMixin, BaseSignupForm):
field_order = [
'name',
'full_name',
'organization',
'username',
'email',
Expand Down
152 changes: 152 additions & 0 deletions kobo/apps/accounts/tests/test_forms.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import json

import pytest
from constance.test import override_config
from django.test import TestCase, override_settings
from django.utils import translation
from model_bakery import baker
from pyquery import PyQuery

from kobo.apps.accounts.forms import SocialSignupForm


Expand All @@ -25,3 +31,149 @@ def test_social_signup_form_not_email_not_disabled(self):
pq = PyQuery(str(form))
assert (email_input := pq("[name=email]"))
assert "readonly" not in email_input[0].attrib

def test_only_configurable_fields_can_be_removed(self):
with override_config(USER_METADATA_FIELDS='{}'):
form = SocialSignupForm(sociallogin=self.sociallogin)
assert 'username' in form.fields
assert 'email' in form.fields

def test_field_without_custom_label_can_be_required(self):
with override_config(
USER_METADATA_FIELDS=json.dumps(
[{'name': 'full_name', 'required': True}]
)
):
form = SocialSignupForm(sociallogin=self.sociallogin)
assert form.fields['full_name'].required
assert form.fields['full_name'].label == 'Full name'

def test_field_with_only_default_custom_label(self):
with override_config(
USER_METADATA_FIELDS=json.dumps(
[
{
'name': 'full_name',
'required': True,
'label': {'default': 'Secret Agent ID'},
}
]
)
):
form = SocialSignupForm(sociallogin=self.sociallogin)
assert form.fields['full_name'].required
assert form.fields['full_name'].label == 'Secret Agent ID'

def test_field_with_specific_and_default_custom_labels(self):
with override_config(
USER_METADATA_FIELDS=json.dumps(
[
{
'name': 'full_name',
'required': True,
'label': {
'default': 'Secret Agent ID',
'es': 'ID de agente secreto',
},
}
]
)
):
with translation.override('es'):
form = SocialSignupForm(sociallogin=self.sociallogin)
assert form.fields['full_name'].required
assert form.fields['full_name'].label == 'ID de agente secreto'
with translation.override('en'):
form = SocialSignupForm(sociallogin=self.sociallogin)
assert form.fields['full_name'].required
assert form.fields['full_name'].label == 'Secret Agent ID'

def test_field_with_custom_label_without_default(self):
"""
The JSON schema should always require a default label, but the form
should render labels properly even if the default is missing
"""
with override_config(
USER_METADATA_FIELDS=json.dumps(
[
{
'name': 'organization',
'required': True,
'label': {
'fr': 'Organisation secrète',
},
},
]
)
):
with translation.override('fr'):
form = SocialSignupForm(sociallogin=self.sociallogin)
assert form.fields['organization'].required
assert form.fields['organization'].label == 'Organisation secrète'


def test_field_without_custom_label_can_be_optional(self):
with override_config(
USER_METADATA_FIELDS=json.dumps(
[
{
'name': 'organization',
'required': False,
},
]
)
):
form = SocialSignupForm(sociallogin=self.sociallogin)
assert not form.fields['organization'].required

def test_field_with_custom_label_can_be_optional(self):
with override_config(
USER_METADATA_FIELDS=json.dumps(
[
{
'name': 'organization',
'required': False,
'label': {
'default': 'Organization',
'fr': 'Organisation',
'es': 'Organización',
},
},
]
)
):
form = SocialSignupForm(sociallogin=self.sociallogin)
assert not form.fields['organization'].required
assert form.fields['organization'].label == 'Organization'
with translation.override('fr'):
form = SocialSignupForm(sociallogin=self.sociallogin)
assert form.fields['organization'].required == False
assert form.fields['organization'].label == 'Organisation'
with translation.override('es'):
form = SocialSignupForm(sociallogin=self.sociallogin)
assert form.fields['organization'].required == False
assert form.fields['organization'].label == 'Organización'

def test_not_supported_translation(self):
with override_config(
USER_METADATA_FIELDS=json.dumps(
[
{
'name': 'organization',
'required': False,
'label': {
'default': 'Organization',
'fr': 'Organisation',
},
},
]
)
):
with translation.override('es'):
form = SocialSignupForm(sociallogin=self.sociallogin)
assert form.fields['organization'].required == False
assert form.fields['organization'].label == 'Organization'
with translation.override('ar'):
form = SocialSignupForm(sociallogin=self.sociallogin)
assert form.fields['organization'].required == False
assert form.fields['organization'].label == 'Organization'
18 changes: 12 additions & 6 deletions kobo/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@
),
'USER_METADATA_FIELDS': (
json.dumps([
{'name': 'full_name', 'required': False},
{'name': 'organization', 'required': False},
{'name': 'organization_website', 'required': False},
{'name': 'sector', 'required': False},
Expand All @@ -289,23 +290,28 @@
]),
# The available fields are hard-coded in the front end
'Display (and optionally require) these metadata fields for users. '
"Possible fields are 'organization', 'organization_website', "
"Possible fields are 'full_name', 'organization', 'organization_website', "
"'sector', 'gender', 'bio', 'city', 'country', 'twitter', 'linkedin', "
"and 'instagram'",
"and 'instagram'.\n\n"
"To add another language, use 'some-other-language' as an example.",
# Use custom field for schema validation
'metadata_fields_jsonschema'
),
'PROJECT_METADATA_FIELDS': (
json.dumps([
{'name': 'sector', 'required': False},
{'name': 'country', 'required': False},
{'name': 'sector', 'required': False,},
{'name': 'country', 'required': False,},
{'name': 'description', 'required': False},
# {'name': 'operational_purpose', 'required': False},
# {'name': 'collects_pii', 'required': False},
]),
# The available fields are hard-coded in the front end
'Display (and optionally require) these metadata fields for projects. '
"Possible fields are 'sector', 'country', 'operational_purpose', and "
"'collects_pii'.",
"Possible fields are 'sector', 'country', 'operational_purpose', "
"'collects_pii', and 'description'\n\n"
'To add another language, follow the example below.\n\n'
"{'name': 'sector', 'required': False, 'label': {default: 'Sector', 'fr': 'Secteur'}}\n"
"'default' is a required field within the 'label' dict, but 'label' is optional.",
# Use custom field for schema validation
'metadata_fields_jsonschema'
),
Expand Down
31 changes: 27 additions & 4 deletions kpi/fields/jsonschema_form_field.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,22 @@ class MetadataFieldsListField(JsonSchemaFormField):
Validates that the input is an array of objects with "name" and "required"
properties, e.g.
[
{"name": "important_field", "required": true},
{"name": "whatever_field", "required": false},
{
"name": "important_field",
"required": true,
"label": {
"default": "Important Field",
"fr": "Champ important"
}
},
{
"name": "whatever_field",
"required": false,
"label": {
"default": "Whatever Field",
"fr": "Champ whatever"
}
},
]
"""
Expand All @@ -95,8 +109,17 @@ def __init__(self, *args, **kwargs):
'properties': {
'name': {'type': 'string'},
'required': {'type': 'boolean'},
},
},
'label': {
'type': 'object',
'uniqueItems': True,
'properties': {
'default': {'type': 'string'},
},
'required': ['default'],
'additionalProperties': True,
}
}
}
}
super().__init__(*args, schema=schema, **kwargs)

Expand Down

0 comments on commit 4951343

Please sign in to comment.