Repository: openwisp/openwisp-radius (language: python)

Issue:
[feature] Add more user fields in the registration API
Depends on https://github.com/openwisp/openwisp-users/issues/221.

Given the following fields:

- first name
- last name
- location
- birth date

Add a django setting which allows to specify at a global level if the following fields should be:

1. disabled: any value sent for this field will be ignored and not saved to the DB - **this shall be the default**
2. allowed: will be accepted by the API, but it's not mandatory
3. mandatory: accepted by the API and mandatory (raise validation error if missing)

Then add new fields in `OrganizationRadiusSettings`, which mirror how the [allowed mobile prefixes](https://github.com/openwisp/openwisp-radius/issues/160) feature and the allowed radius hosts feature work, but for this new feature, eg:

- `first_name`: choice field (NULL, disabled, allowed, mandatory)
- `last_name`: choice field (NULL, disabled, allowed, mandatory)
- `location`: choice field (NULL, disabled, allowed, mandatory)
- `birth_date`: choice field (NULL, disabled, allowed, mandatory)

NULL means it takes the default value of the global settings.
The NULL value (python `None`), will be set during validation if the value used is equal to the one in the global setting, this allows us to change the global setting
and automatically update the default for all the orgs if needed.
[feature] Add more user fields in the registration API
Depends on https://github.com/openwisp/openwisp-users/issues/221.

Given the following fields:

- first name
- last name
- location
- birth date

Add a django setting which allows to specify at a global level if the following fields should be:

1. disabled: any value sent for this field will be ignored and not saved to the DB - **this shall be the default**
2. allowed: will be accepted by the API, but it's not mandatory
3. mandatory: accepted by the API and mandatory (raise validation error if missing)

Then add new fields in `OrganizationRadiusSettings`, which mirror how the [allowed mobile prefixes](https://github.com/openwisp/openwisp-radius/issues/160) feature and the allowed radius hosts feature work, but for this new feature, eg:

- `first_name`: choice field (NULL, disabled, allowed, mandatory)
- `last_name`: choice field (NULL, disabled, allowed, mandatory)
- `location`: choice field (NULL, disabled, allowed, mandatory)
- `birth_date`: choice field (NULL, disabled, allowed, mandatory)

NULL means it takes the default value of the global settings.
The NULL value (python `None`), will be set during validation if the value used is equal to the one in the global setting, this allows us to change the global setting
and automatically update the default for all the orgs if needed.


Discussion / hints:

Fix patch:
diff --git a/docs/source/images/optional_fields.png b/docs/source/images/optional_fields.png
new file mode 100644
index 00000000..0c7cf82a
Binary files /dev/null and b/docs/source/images/optional_fields.png differ
diff --git a/docs/source/user/api.rst b/docs/source/user/api.rst
index b32f7a04..0967af39 100644
--- a/docs/source/user/api.rst
+++ b/docs/source/user/api.rst
@@ -350,6 +350,10 @@ Parameters:
 Param              Description
 ===============    ===============================
 username           string
+first_name         string (*)
+last_name          string (*)
+birth_date         string (*)
+location           string (*)
 email              string
 password1          string
 password2          string
@@ -359,6 +363,10 @@ phone_number       string (*)
 (*) ``phone_number`` is required only when the organization has enabled
 SMS verification in its "Organization RADIUS Settings".
 
+(*) ``first_name``, ``last_name``, ``birth_date`` and ``location`` are optional fields 
+and can be `configured <./settings.html#openwisp-radius-optional-registration-fields>`_ 
+to be optionally allowed, ignored or required.
+
 Reset password
 --------------
 
diff --git a/docs/source/user/settings.rst b/docs/source/user/settings.rst
index 1a43f3fb..e5f325b4 100644
--- a/docs/source/user/settings.rst
+++ b/docs/source/user/settings.rst
@@ -314,6 +314,51 @@ the configuration should be simply changed to:
         'default': 'https://login.wifiservice.com/{organization}/password/reset/confirm/{uid}/{token}'
     }
 
+``OPENWISP_RADIUS_OPTIONAL_REGISTRATION_FIELDS``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+**Default**:
+
+.. code-block:: python
+
+    {
+        'first_name': 'disabled',
+        'last_name': 'disabled',
+        'birth_date': 'disabled',
+        'location': 'disabled',
+    }
+
+This setting is used to specify if the user's optional fields (``first_name``, ``last_name``, ``location`` and ``birth_date``) 
+should either be optionally allowed, ignored or required in the `User Registration API <api.html#user-registration>`_.
+It can take any of the following values:
+
+- ``disabled``: this is the **default** setting. Value of user's optional fields are not required and will not be saved to the database if supplied.
+- ``allowed``: value of user's optional fields will be save to the database but they are not required.
+- ``mandatory``: value of user's optional fields are required and will be saved to the database.
+
+For example:
+
+.. code-block:: python
+
+    OPENWISP_RADIUS_OPTIONAL_REGISTRATION_FIELDS = {
+        'first_name': 'disabled',
+        'last_name': 'disabled',
+        'birth_date': 'mandatory',
+        'location': 'allowed', 
+    }
+
+In the global setting above;
+
+- ``first_name`` and ``last_name`` fields are not required and their values if provided are ignored.
+- ``location`` field is not required but its value will be save to the database if provided.
+- ``birth_date`` field is required and a ``ValidationError`` exception is raised if its value is not provided.
+
+These fields can also be configured to behave in a similar manner at the organization level by setting them in the admin 
+as shown in the screenshot below:
+
+.. image:: /images/optional_fields.png
+    :alt: optional field setting
+
 Email related settings
 ======================
 
diff --git a/openwisp_radius/admin.py b/openwisp_radius/admin.py
index cc8efa18..862c48a8 100644
--- a/openwisp_radius/admin.py
+++ b/openwisp_radius/admin.py
@@ -24,6 +24,8 @@
 from .base.models import (
     _GET_IP_LIST_HELP_TEXT,
     _GET_MOBILE_PREFIX_HELP_TEXT,
+    _GET_OPTIONAL_FIELDS_HELP_TEXT,
+    OPTIONAL_FIELD_CHOICES,
     _encode_secret,
 )
 from .utils import load_model
@@ -42,6 +44,7 @@
 RadiusUserGroup = load_model('RadiusUserGroup')
 OrganizationRadiusSettings = load_model('OrganizationRadiusSettings')
 User = get_user_model()
+OPTIONAL_SETTINGS = app_settings.OPTIONAL_REGISTRATION_FIELDS
 
 
 class OrganizationFirstMixin(MultitenantAdminMixin):
@@ -510,6 +513,34 @@ def prepare_value(self, value):
         return super().prepare_value(value)
 
 
+class FirstNameField(forms.ChoiceField):
+    def prepare_value(self, value):
+        if not value:
+            value = OPTIONAL_SETTINGS.get('first_name')
+        return super().prepare_value(value)
+
+
+class LastNameField(forms.ChoiceField):
+    def prepare_value(self, value):
+        if not value:
+            value = OPTIONAL_SETTINGS.get('last_name')
+        return super().prepare_value(value)
+
+
+class LocationField(forms.ChoiceField):
+    def prepare_value(self, value):
+        if not value:
+            value = OPTIONAL_SETTINGS.get('location')
+        return super().prepare_value(value)
+
+
+class BirthDateField(forms.ChoiceField):
+    def prepare_value(self, value):
+        if not value:
+            value = OPTIONAL_SETTINGS.get('birth_date')
+        return super().prepare_value(value)
+
+
 class AlwaysHasChangedForm(AlwaysHasChangedMixin, forms.ModelForm):
     freeradius_allowed_hosts = AllowedHostsField(
         required=False,
@@ -521,6 +552,26 @@ class AlwaysHasChangedForm(AlwaysHasChangedMixin, forms.ModelForm):
         widget=forms.Textarea(attrs={'rows': 2, 'cols': 34}),
         help_text=_GET_MOBILE_PREFIX_HELP_TEXT,
     )
+    first_name = FirstNameField(
+        required=False,
+        help_text=_GET_OPTIONAL_FIELDS_HELP_TEXT,
+        choices=OPTIONAL_FIELD_CHOICES,
+    )
+    last_name = LastNameField(
+        required=False,
+        help_text=_GET_OPTIONAL_FIELDS_HELP_TEXT,
+        choices=OPTIONAL_FIELD_CHOICES,
+    )
+    location = LocationField(
+        required=False,
+        help_text=_GET_OPTIONAL_FIELDS_HELP_TEXT,
+        choices=OPTIONAL_FIELD_CHOICES,
+    )
+    birth_date = BirthDateField(
+        required=False,
+        help_text=_GET_OPTIONAL_FIELDS_HELP_TEXT,
+        choices=OPTIONAL_FIELD_CHOICES,
+    )
 
 
 class OrganizationRadiusSettingsInline(admin.StackedInline):
@@ -536,6 +587,10 @@ class OrganizationRadiusSettingsInline(admin.StackedInline):
                     'sms_verification',
                     'sms_sender',
                     'allowed_mobile_prefixes',
+                    'first_name',
+                    'last_name',
+                    'birth_date',
+                    'location',
                 )
             },
         ),
diff --git a/openwisp_radius/api/serializers.py b/openwisp_radius/api/serializers.py
index ac102321..22c810a8 100644
--- a/openwisp_radius/api/serializers.py
+++ b/openwisp_radius/api/serializers.py
@@ -330,6 +330,10 @@ class RegisterSerializer(
         allow_blank=True,
         default='',
     )
+    first_name = serializers.CharField(required=False)
+    last_name = serializers.CharField(required=False)
+    location = serializers.CharField(required=False)
+    birth_date = serializers.DateField(required=False)
 
     def validate_phone_number(self, phone_number):
         org = self.context['view'].organization
@@ -346,6 +350,18 @@ def validate_phone_number(self, phone_number):
             phone_number = None
         return phone_number
 
+    def validate_optional_fields(self, field_name, field_value, org):
+        field_setting = getattr(
+            org.radius_settings, field_name
+        ) or app_settings.OPTIONAL_REGISTRATION_FIELDS.get(field_name)
+        if field_setting == 'mandatory' and not field_value:
+            raise serializers.ValidationError(
+                {f'{field_name}': _('This field is required.')}
+            )
+        if field_setting == 'disabled':
+            field_value = ''
+        return field_value
+
     def save(self, request):
         adapter = get_adapter()
         user = adapter.new_user(request)
@@ -362,6 +378,12 @@ def custom_signup(self, request, user, save=True):
         if phone_number != self.fields['phone_number'].default:
             user.phone_number = phone_number
         org = self.context['view'].organization
+        for field_name in ['first_name', 'last_name', 'location', 'birth_date']:
+            value = self.validate_optional_fields(
+                field_name, self.validated_data.get(field_name, ''), org
+            )
+            if value:
+                setattr(user, field_name, value)
         if is_sms_verification_enabled(org):
             user.is_active = False
         try:
diff --git a/openwisp_radius/base/models.py b/openwisp_radius/base/models.py
index 4efb4d47..0d9c278f 100644
--- a/openwisp_radius/base/models.py
+++ b/openwisp_radius/base/models.py
@@ -53,6 +53,12 @@
 logger = logging.getLogger(__name__)
 User = get_user_model()
 
+OPTIONAL_FIELD_CHOICES = (
+    ('disabled', _('Disabled')),
+    ('allowed', _('Allowed')),
+    ('mandatory', _('Mandatory')),
+)
+
 RADOP_CHECK_TYPES = (
     ('=', '='),
     (':=', ':='),
@@ -142,6 +148,10 @@
     'Comma separated list of international mobile prefixes '
     'allowed to register via the user registration API.'
 )
+_GET_OPTIONAL_FIELDS_HELP_TEXT = _(
+    'Chose if this user field should be disabled, allowed or mandatory '
+    'during user registration.'
+)
 
 
 class AutoUsernameMixin(object):
@@ -1043,6 +1053,38 @@ class AbstractOrganizationRadiusSettings(UUIDModel):
     allowed_mobile_prefixes = models.TextField(
         null=True, blank=True, help_text=_GET_MOBILE_PREFIX_HELP_TEXT,
     )
+    first_name = models.CharField(
+        verbose_name=_('first name'),
+        help_text=_GET_OPTIONAL_FIELDS_HELP_TEXT,
+        max_length=12,
+        null=True,
+        blank=True,
+        choices=OPTIONAL_FIELD_CHOICES,
+    )
+    last_name = models.CharField(
+        verbose_name=_('last name'),
+        help_text=_GET_OPTIONAL_FIELDS_HELP_TEXT,
+        max_length=12,
+        null=True,
+        blank=True,
+        choices=OPTIONAL_FIELD_CHOICES,
+    )
+    location = models.CharField(
+        verbose_name=_('location'),
+        help_text=_GET_OPTIONAL_FIELDS_HELP_TEXT,
+        max_length=12,
+        null=True,
+        blank=True,
+        choices=OPTIONAL_FIELD_CHOICES,
+    )
+    birth_date = models.CharField(
+        verbose_name=_('birth date'),
+        help_text=_GET_OPTIONAL_FIELDS_HELP_TEXT,
+        max_length=12,
+        null=True,
+        blank=True,
+        choices=OPTIONAL_FIELD_CHOICES,
+    )
 
     class Meta:
         verbose_name = _('Organization radius settings')
@@ -1077,6 +1119,7 @@ def clean(self):
             )
         self._clean_freeradius_allowed_hosts()
         self._clean_allowed_mobile_prefixes()
+        self._clean_optional_fields()
 
     def _clean_freeradius_allowed_hosts(self):
         allowed_hosts_set = set(self.freeradius_allowed_hosts_list)
@@ -1092,7 +1135,7 @@ def _clean_freeradius_allowed_hosts(self):
             )
         elif allowed_hosts_set:
             if allowed_hosts_set == settings_allowed_hosts_set:
-                self.freeradius_allowed_hosts = ''
+                self.freeradius_allowed_hosts = None
             else:
                 try:
                     for ip_address in allowed_hosts_set:
@@ -1109,6 +1152,8 @@ def _clean_freeradius_allowed_hosts(self):
 
     def _clean_allowed_mobile_prefixes(self):
         valid_country_codes = phonenumbers.COUNTRY_CODE_TO_REGION_CODE.keys()
+        allowed_mobile_prefixes_set = set(self.allowed_mobile_prefixes_list)
+        settings_allowed_mobile_prefixes_set = set(app_settings.ALLOWED_MOBILE_PREFIXES)
         for code in self.allowed_mobile_prefixes_list:
             if not code or code[0] != '+' or int(code[1:]) not in valid_country_codes:
                 raise ValidationError(
@@ -1120,6 +1165,15 @@ def _clean_allowed_mobile_prefixes(self):
                     }
                 )
 
+        if allowed_mobile_prefixes_set == settings_allowed_mobile_prefixes_set:
+            self.allowed_mobile_prefixes = None
+
+    def _clean_optional_fields(self):
+        global_settings = app_settings.OPTIONAL_REGISTRATION_FIELDS
+        for field in ['first_name', 'last_name', 'location', 'birth_date']:
+            if getattr(self, field) == global_settings.get(field):
+                setattr(self, field, None)
+
     def save_cache(self, *args, **kwargs):
         cache.set(self.organization.pk, self.token)
         cache.set(f'ip-{self.organization.pk}', self.freeradius_allowed_hosts_list)
diff --git a/openwisp_radius/migrations/0020_added_optional_registration_fields.py b/openwisp_radius/migrations/0020_added_optional_registration_fields.py
new file mode 100644
index 00000000..eb1d2271
--- /dev/null
+++ b/openwisp_radius/migrations/0020_added_optional_registration_fields.py
@@ -0,0 +1,89 @@
+# Generated by Django 3.1.5 on 2021-01-14 19:26
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('openwisp_radius', '0019_made_phonetoken_phone_number_required'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='organizationradiussettings',
+            name='birth_date',
+            field=models.CharField(
+                blank=True,
+                choices=[
+                    ('disabled', 'Disabled'),
+                    ('allowed', 'Allowed'),
+                    ('mandatory', 'Mandatory'),
+                ],
+                help_text=(
+                    'Chose if this user field should be disabled, '
+                    'allowed or mandatory during user registration.'
+                ),
+                max_length=12,
+                null=True,
+                verbose_name='birth date',
+            ),
+        ),
+        migrations.AddField(
+            model_name='organizationradiussettings',
+            name='first_name',
+            field=models.CharField(
+                blank=True,
+                choices=[
+                    ('disabled', 'Disabled'),
+                    ('allowed', 'Allowed'),
+                    ('mandatory', 'Mandatory'),
+                ],
+                help_text=(
+                    'Chose if this user field should be disabled, '
+                    'allowed or mandatory during user registration.'
+                ),
+                max_length=12,
+                null=True,
+                verbose_name='first name',
+            ),
+        ),
+        migrations.AddField(
+            model_name='organizationradiussettings',
+            name='last_name',
+            field=models.CharField(
+                blank=True,
+                choices=[
+                    ('disabled', 'Disabled'),
+                    ('allowed', 'Allowed'),
+                    ('mandatory', 'Mandatory'),
+                ],
+                help_text=(
+                    'Chose if this user field should be disabled, '
+                    'allowed or mandatory during user registration.'
+                ),
+                max_length=12,
+                null=True,
+                verbose_name='last name',
+            ),
+        ),
+        migrations.AddField(
+            model_name='organizationradiussettings',
+            name='location',
+            field=models.CharField(
+                blank=True,
+                choices=[
+                    ('disabled', 'Disabled'),
+                    ('allowed', 'Allowed'),
+                    ('mandatory', 'Mandatory'),
+                ],
+                help_text=(
+                    'Chose if this user field should be disabled, '
+                    'allowed or mandatory during user registration.'
+                ),
+                max_length=12,
+                null=True,
+                verbose_name='location',
+            ),
+        ),
+    ]
diff --git a/openwisp_radius/settings.py b/openwisp_radius/settings.py
index e15e20e4..56c7dffe 100644
--- a/openwisp_radius/settings.py
+++ b/openwisp_radius/settings.py
@@ -79,6 +79,16 @@ def get_settings_value(option, default):
 SMS_TOKEN_MAX_ATTEMPTS = get_settings_value('SMS_TOKEN_MAX_ATTEMPTS', 3)
 SMS_TOKEN_MAX_USER_DAILY = get_settings_value('SMS_TOKEN_MAX_USER_DAILY', 3)
 SMS_TOKEN_MAX_IP_DAILY = get_settings_value('SMS_TOKEN_MAX_IP_DAILY', 25)
+ALLOWED_MOBILE_PREFIXES = get_settings_value('ALLOWED_MOBILE_PREFIXES', [])
+OPTIONAL_REGISTRATION_FIELDS = get_settings_value(
+    'OPTIONAL_REGISTRATION_FIELDS',
+    {
+        'first_name': 'disabled',
+        'last_name': 'disabled',
+        'birth_date': 'disabled',
+        'location': 'disabled',
+    },
+)
 
 try:  # pragma: no cover
     assert PASSWORD_RESET_URLS
@@ -111,5 +121,3 @@ def get_settings_value(option, default):
         'lower would not be safe and higher would not be practical from '
         'a ux perspective.'
     )
-
-ALLOWED_MOBILE_PREFIXES = get_settings_value('ALLOWED_MOBILE_PREFIXES', [])


Test patch:
diff --git a/openwisp_radius/tests/test_admin.py b/openwisp_radius/tests/test_admin.py
index d9971f44..879843bc 100644
--- a/openwisp_radius/tests/test_admin.py
+++ b/openwisp_radius/tests/test_admin.py
@@ -538,7 +538,7 @@ def test_organization_radsettings_freeradius_allowed_hosts(self):
             radsetting.refresh_from_db()
             # This should fail when the value of FREERADIUS_ALLOWED_HOSTS is
             # stored in the model as well.
-            self.assertEqual(radsetting.freeradius_allowed_hosts, '')
+            self.assertEqual(radsetting.freeradius_allowed_hosts, None)
         with self.subTest('Valid IP list'):
             form_data.update(
                 {'radius_settings-0-freeradius_allowed_hosts': '127.0.0.45'}
diff --git a/openwisp_radius/tests/test_api.py b/openwisp_radius/tests/test_api.py
index 8da10f86..e834c1b0 100644
--- a/openwisp_radius/tests/test_api.py
+++ b/openwisp_radius/tests/test_api.py
@@ -50,6 +50,12 @@
 
 class TestApi(ApiTokenMixin, FileMixin, BaseTestCase):
     _test_email = 'test@openwisp.org'
+    optional_settings_params = {
+        'first_name': 'disabled',
+        'last_name': 'allowed',
+        'birth_date': 'disabled',
+        'location': 'mandatory',
+    }
 
     def setUp(self):
         cache.clear()
@@ -1106,6 +1112,46 @@ def test_register_201(self):
         self.assertTrue(user.is_member(self.default_org))
         self.assertTrue(user.is_active)
 
+    @mock.patch.object(
+        app_settings, 'OPTIONAL_REGISTRATION_FIELDS', optional_settings_params
+    )
+    def test_optional_fields_registration(self):
+        self._superuser_login()
+        url = reverse('radius:rest_register', args=[self.default_org.slug])
+        params = {
+            'username': self._test_email,
+            'email': self._test_email,
+            'password1': 'password',
+            'password2': 'password',
+            'first_name': 'first name',
+            'location': 'test location',
+            'last_name': 'last name',
+            'birth_date': '1998-08-19',
+        }
+        r = self.client.post(url, params)
+        self.assertEqual(r.status_code, 201)
+        self.assertIn('key', r.data)
+        user = User.objects.get(email=self._test_email)
+        self.assertEqual(user.first_name, '')
+        self.assertEqual(user.last_name, 'last name')
+        self.assertEqual(user.location, 'test location')
+        self.assertIsNone(user.birth_date)
+
+        with self.subTest('test org level setting'):
+            self.default_org.radius_settings.birth_date = 'mandatory'
+            self.default_org.radius_settings.full_clean()
+            self.default_org.radius_settings.save()
+            params['username'] = 'test'
+            params['email'] = 'test@gmail.com'
+            params['location'] = ''
+            r = self.client.post(url, params)
+            self.assertEqual(r.status_code, 400)
+            self.assertEqual(len(r.data.keys()), 1)
+            self.assertIn('location', r.data)
+            self.assertEqual(r.data['location'], 'This field is required.')
+            user_count = User.objects.filter(email='test@gmail.com').count()
+            self.assertEqual(user_count, 0)
+
     @capture_any_output()
     def test_register_error_missing_radius_settings(self):
         self.assertEqual(User.objects.count(), 0)
diff --git a/openwisp_radius/tests/test_models.py b/openwisp_radius/tests/test_models.py
index 87a764d1..14f92100 100644
--- a/openwisp_radius/tests/test_models.py
+++ b/openwisp_radius/tests/test_models.py
@@ -1,4 +1,5 @@
 import os
+from unittest import mock
 from uuid import UUID
 
 import swapper
@@ -10,6 +11,7 @@
 
 from openwisp_users.tests.utils import TestMultitenantAdminMixin
 
+from .. import settings as app_settings
 from ..utils import (
     DEFAULT_SESSION_TIME_LIMIT,
     DEFAULT_SESSION_TRAFFIC_LIMIT,
@@ -183,6 +185,33 @@ def test_id(self):
         self.assertIsInstance(radiuspostauth.pk, UUID)
 
 
+class TestOrganizationRadiusSettings(BaseTestCase):
+
+    optional_settings_params = {
+        'first_name': 'disabled',
+        'last_name': 'allowed',
+        'birth_date': 'disabled',
+        'location': 'mandatory',
+    }
+
+    @mock.patch.object(
+        app_settings, 'OPTIONAL_REGISTRATION_FIELDS', optional_settings_params,
+    )
+    def test_org_settings_same_globally(self):
+        org = self._get_org()
+        org.radius_settings.first_name = 'disabled'
+        org.radius_settings.last_name = 'allowed'
+        org.radius_settings.location = 'mandatory'
+        org.radius_settings.birth_date = 'disabled'
+        org.radius_settings.full_clean()
+        org.radius_settings.save()
+
+        self.assertIsNone(org.radius_settings.first_name)
+        self.assertIsNone(org.radius_settings.last_name)
+        self.assertIsNone(org.radius_settings.location)
+        self.assertIsNone(org.radius_settings.birth_date)
+
+
 class TestRadiusGroup(BaseTestCase):
     def test_group_str(self):
         g = RadiusGroup(name='entry groupname')
diff --git a/tests/openwisp2/sample_radius/migrations/0020_added_optional_registration_fields.py b/tests/openwisp2/sample_radius/migrations/0020_added_optional_registration_fields.py
new file mode 100644
index 00000000..9e02dcaa
--- /dev/null
+++ b/tests/openwisp2/sample_radius/migrations/0020_added_optional_registration_fields.py
@@ -0,0 +1,89 @@
+# Generated by Django 3.1.5 on 2021-01-14 21:52
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('sample_radius', '0005_phonetoken_phone_number'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='organizationradiussettings',
+            name='birth_date',
+            field=models.CharField(
+                blank=True,
+                choices=[
+                    ('disabled', 'Disabled'),
+                    ('allowed', 'Allowed'),
+                    ('mandatory', 'Mandatory'),
+                ],
+                help_text=(
+                    'Chose if this user field should be disabled, '
+                    'allowed or mandatory during user registration.'
+                ),
+                max_length=12,
+                null=True,
+                verbose_name='birth date',
+            ),
+        ),
+        migrations.AddField(
+            model_name='organizationradiussettings',
+            name='first_name',
+            field=models.CharField(
+                blank=True,
+                choices=[
+                    ('disabled', 'Disabled'),
+                    ('allowed', 'Allowed'),
+                    ('mandatory', 'Mandatory'),
+                ],
+                help_text=(
+                    'Chose if this user field should be disabled, '
+                    'allowed or mandatory during user registration.'
+                ),
+                max_length=12,
+                null=True,
+                verbose_name='first name',
+            ),
+        ),
+        migrations.AddField(
+            model_name='organizationradiussettings',
+            name='last_name',
+            field=models.CharField(
+                blank=True,
+                choices=[
+                    ('disabled', 'Disabled'),
+                    ('allowed', 'Allowed'),
+                    ('mandatory', 'Mandatory'),
+                ],
+                help_text=(
+                    'Chose if this user field should be disabled, '
+                    'allowed or mandatory during user registration.'
+                ),
+                max_length=12,
+                null=True,
+                verbose_name='last name',
+            ),
+        ),
+        migrations.AddField(
+            model_name='organizationradiussettings',
+            name='location',
+            field=models.CharField(
+                blank=True,
+                choices=[
+                    ('disabled', 'Disabled'),
+                    ('allowed', 'Allowed'),
+                    ('mandatory', 'Mandatory'),
+                ],
+                help_text=(
+                    'Chose if this user field should be disabled, '
+                    'allowed or mandatory during user registration.'
+                ),
+                max_length=12,
+                null=True,
+                verbose_name='location',
+            ),
+        ),
+    ]
diff --git a/tests/openwisp2/sample_radius/tests.py b/tests/openwisp2/sample_radius/tests.py
index 434351b0..12fdaccb 100644
--- a/tests/openwisp2/sample_radius/tests.py
+++ b/tests/openwisp2/sample_radius/tests.py
@@ -18,6 +18,9 @@
 )
 from openwisp_radius.tests.test_commands import TestCommands as BaseTestCommands
 from openwisp_radius.tests.test_models import TestNas as BaseTestNas
+from openwisp_radius.tests.test_models import (
+    TestOrganizationRadiusSettings as BaseTestOrganizationRadiusSettings,
+)
 from openwisp_radius.tests.test_models import (
     TestPrivateCsvFile as BaseTestPrivateCsvFile,
 )
@@ -149,6 +152,10 @@ class TestApiValidateToken(BaseTestApiValidateToken):
     pass
 
 
+class TestOrganizationRadiusSettings(BaseTestOrganizationRadiusSettings):
+    pass
+
+
 del BaseTestAdmin
 del BaseTestApi
 del BaseTestApiReject
@@ -174,3 +181,4 @@ class TestApiValidateToken(BaseTestApiValidateToken):
 del BaseTestUsersIntegration
 del BaseTestUtils
 del BaseTestUpgradeFromDjangoFreeradius
+del BaseTestOrganizationRadiusSettings