Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add labels to User and Project metadata #4497

Merged
merged 14 commits into from
Jul 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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