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

Issue:
[change] Move fallback fields to openwisp-utils
See https://github.com/openwisp/openwisp-utils/issues/333.
[change] Move fallback fields to openwisp-utils
See https://github.com/openwisp/openwisp-utils/issues/333.


Discussion / hints:

Fix patch:
diff --git a/openwisp_radius/base/fields.py b/openwisp_radius/base/fields.py
deleted file mode 100644
index da08ab3f..00000000
--- a/openwisp_radius/base/fields.py
+++ /dev/null
@@ -1,133 +0,0 @@
-from django import forms
-from django.db.models.fields import BooleanField, CharField, TextField, URLField
-from django.utils.translation import gettext_lazy as _
-
-
-class FallbackMixin(object):
-    def __init__(self, *args, **kwargs):
-        self.fallback = kwargs.pop('fallback', None)
-        super().__init__(*args, **kwargs)
-
-    def deconstruct(self):
-        name, path, args, kwargs = super().deconstruct()
-        kwargs['fallback'] = self.fallback
-        return (name, path, args, kwargs)
-
-
-class FallbackFromDbValueMixin:
-    """
-    Returns the fallback value when the value of the field
-    is falsy (None or '').
-
-    It does not set the field's value to "None" when the value
-    is equal to the fallback value. This allows overriding of
-    the value when a user knows that the default will get changed.
-    """
-
-    def from_db_value(self, value, expression, connection):
-        if value is None:
-            return self.fallback
-        return value
-
-
-class FalsyValueNoneMixin:
-    """
-    If the field contains an empty string, then
-    stores "None" in the database if the field is
-    nullable.
-    """
-
-    # TDjango convention is to use the empty string, not NULL
-    # for representing "no data" in the database.
-    # https://docs.djangoproject.com/en/dev/ref/models/fields/#null
-    # We need to use NULL for fallback field here to keep
-    # the fallback logic simple. Hence, we allow only "None" (NULL)
-    # as empty value here.
-    empty_values = [None]
-
-    def clean(self, value, model_instance):
-        if not value and self.null is True:
-            return None
-        return super().clean(value, model_instance)
-
-
-class FallbackBooleanChoiceField(FallbackMixin, BooleanField):
-    def formfield(self, **kwargs):
-        default_value = _('Enabled') if self.fallback else _('Disabled')
-        kwargs.update(
-            {
-                "form_class": forms.NullBooleanField,
-                'widget': forms.Select(
-                    choices=[
-                        (
-                            '',
-                            _('Default') + f' ({default_value})',
-                        ),
-                        (True, _('Enabled')),
-                        (False, _('Disabled')),
-                    ],
-                    attrs={'data-default-value': str(self.fallback)},
-                ),
-            }
-        )
-        return super().formfield(**kwargs)
-
-
-class FallbackCharChoiceField(FallbackMixin, CharField):
-    def get_choices(self, **kwargs):
-        for choice, value in self.choices:
-            if choice == self.fallback:
-                default = value
-                break
-        kwargs.update({'blank_choice': [('', _('Default') + f' ({default})')]})
-        return super().get_choices(**kwargs)
-
-    def formfield(self, **kwargs):
-        kwargs.update(
-            {
-                "choices_form_class": forms.TypedChoiceField,
-            }
-        )
-        return super().formfield(**kwargs)
-
-
-class FallbackCharField(
-    FallbackMixin, FalsyValueNoneMixin, FallbackFromDbValueMixin, CharField
-):
-    """
-    Populates the form with the fallback value
-    if the value is set to null in the database.
-    """
-
-    pass
-
-
-class FallbackURLField(
-    FallbackMixin, FalsyValueNoneMixin, FallbackFromDbValueMixin, URLField
-):
-    """
-    Populates the form with the fallback value
-    if the value is set to null in the database.
-    """
-
-    pass
-
-
-class FallbackTextField(
-    FallbackMixin, FalsyValueNoneMixin, FallbackFromDbValueMixin, TextField
-):
-    """
-    Populates the form with the fallback value
-    if the value is set to null in the database.
-    """
-
-    def formfield(self, **kwargs):
-        kwargs.update({'form_class': FallbackTextFormField})
-        return super().formfield(**kwargs)
-
-
-class FallbackTextFormField(forms.CharField):
-    def widget_attrs(self, widget):
-        attrs = super().widget_attrs(widget)
-        attrs.update({'rows': 2, 'cols': 34, 'style': 'width:auto'})
-        return attrs
diff --git a/openwisp_radius/base/models.py b/openwisp_radius/base/models.py
index 57ded646..50534f8e 100644
--- a/openwisp_radius/base/models.py
+++ b/openwisp_radius/base/models.py
@@ -28,6 +28,12 @@
 from openwisp_radius.registration import REGISTRATION_METHOD_CHOICES
 from openwisp_users.mixins import OrgMixin
 from openwisp_utils.base import KeyField, TimeStampedEditableModel, UUIDModel
+from openwisp_utils.fields import (
+    FallbackBooleanChoiceField,
+    FallbackCharChoiceField,
+    FallbackCharField,
+    FallbackTextField,
+)
 
 from .. import exceptions
 from .. import settings as app_settings
@@ -47,12 +53,6 @@
     prefix_generate_users,
     validate_csvfile,
 )
-from .fields import (
-    FallbackBooleanChoiceField,
-    FallbackCharChoiceField,
-    FallbackCharField,
-    FallbackTextField,
-)
 from .validators import ipv6_network_validator, password_reset_url_validator
 
 logger = logging.getLogger(__name__)
diff --git a/openwisp_radius/migrations/0031_added_fallback_model_fields.py b/openwisp_radius/migrations/0031_added_fallback_model_fields.py
index 59171faf..9ac796ae 100644
--- a/openwisp_radius/migrations/0031_added_fallback_model_fields.py
+++ b/openwisp_radius/migrations/0031_added_fallback_model_fields.py
@@ -2,7 +2,7 @@
 
 from django.db import migrations
 
-import openwisp_radius.base.fields
+import openwisp_utils.fields
 
 from .. import settings as app_settings
 
@@ -17,7 +17,7 @@ class Migration(migrations.Migration):
         migrations.AlterField(
             model_name='organizationradiussettings',
             name='allowed_mobile_prefixes',
-            field=openwisp_radius.base.fields.FallbackTextField(
+            field=openwisp_utils.fields.FallbackTextField(
                 blank=True,
                 default=','.join(app_settings.ALLOWED_MOBILE_PREFIXES),
                 fallback=','.join(app_settings.ALLOWED_MOBILE_PREFIXES),
@@ -31,7 +31,7 @@ class Migration(migrations.Migration):
         migrations.AlterField(
             model_name='organizationradiussettings',
             name='birth_date',
-            field=openwisp_radius.base.fields.FallbackCharChoiceField(
+            field=openwisp_utils.fields.FallbackCharChoiceField(
                 blank=True,
                 choices=[
                     ('disabled', 'Disabled'),
@@ -53,7 +53,7 @@ class Migration(migrations.Migration):
         migrations.AlterField(
             model_name='organizationradiussettings',
             name='first_name',
-            field=openwisp_radius.base.fields.FallbackCharChoiceField(
+            field=openwisp_utils.fields.FallbackCharChoiceField(
                 blank=True,
                 choices=[
                     ('disabled', 'Disabled'),
@@ -75,7 +75,7 @@ class Migration(migrations.Migration):
         migrations.AlterField(
             model_name='organizationradiussettings',
             name='freeradius_allowed_hosts',
-            field=openwisp_radius.base.fields.FallbackTextField(
+            field=openwisp_utils.fields.FallbackTextField(
                 blank=True,
                 default=','.join(app_settings.FREERADIUS_ALLOWED_HOSTS),
                 fallback=','.join(app_settings.FREERADIUS_ALLOWED_HOSTS),
@@ -89,7 +89,7 @@ class Migration(migrations.Migration):
         migrations.AlterField(
             model_name='organizationradiussettings',
             name='last_name',
-            field=openwisp_radius.base.fields.FallbackCharChoiceField(
+            field=openwisp_utils.fields.FallbackCharChoiceField(
                 blank=True,
                 choices=[
                     ('disabled', 'Disabled'),
@@ -111,7 +111,7 @@ class Migration(migrations.Migration):
         migrations.AlterField(
             model_name='organizationradiussettings',
             name='location',
-            field=openwisp_radius.base.fields.FallbackCharChoiceField(
+            field=openwisp_utils.fields.FallbackCharChoiceField(
                 blank=True,
                 choices=[
                     ('disabled', 'Disabled'),
@@ -133,7 +133,7 @@ class Migration(migrations.Migration):
         migrations.AlterField(
             model_name='organizationradiussettings',
             name='needs_identity_verification',
-            field=openwisp_radius.base.fields.FallbackBooleanChoiceField(
+            field=openwisp_utils.fields.FallbackBooleanChoiceField(
                 blank=True,
                 default=None,
                 help_text=(
@@ -147,7 +147,7 @@ class Migration(migrations.Migration):
         migrations.AlterField(
             model_name='organizationradiussettings',
             name='password_reset_url',
-            field=openwisp_radius.base.fields.FallbackURLField(
+            field=openwisp_utils.fields.FallbackURLField(
                 blank=True,
                 default=app_settings.DEFAULT_PASSWORD_RESET_URL,
                 fallback=app_settings.DEFAULT_PASSWORD_RESET_URL,
@@ -159,7 +159,7 @@ class Migration(migrations.Migration):
         migrations.AlterField(
             model_name='organizationradiussettings',
             name='registration_enabled',
-            field=openwisp_radius.base.fields.FallbackBooleanChoiceField(
+            field=openwisp_utils.fields.FallbackBooleanChoiceField(
                 blank=True,
                 default=None,
                 help_text=(
@@ -172,7 +172,7 @@ class Migration(migrations.Migration):
         migrations.AlterField(
             model_name='organizationradiussettings',
             name='saml_registration_enabled',
-            field=openwisp_radius.base.fields.FallbackBooleanChoiceField(
+            field=openwisp_utils.fields.FallbackBooleanChoiceField(
                 blank=True,
                 default=None,
                 help_text=(
@@ -186,7 +186,7 @@ class Migration(migrations.Migration):
         migrations.AlterField(
             model_name='organizationradiussettings',
             name='sms_verification',
-            field=openwisp_radius.base.fields.FallbackBooleanChoiceField(
+            field=openwisp_utils.fields.FallbackBooleanChoiceField(
                 blank=True,
                 default=None,
                 help_text=(
@@ -201,7 +201,7 @@ class Migration(migrations.Migration):
         migrations.AlterField(
             model_name='organizationradiussettings',
             name='social_registration_enabled',
-            field=openwisp_radius.base.fields.FallbackBooleanChoiceField(
+            field=openwisp_utils.fields.FallbackBooleanChoiceField(
                 blank=True,
                 default=None,
                 help_text=(
diff --git a/openwisp_radius/migrations/0032_organizationradiussettings_sms_message.py b/openwisp_radius/migrations/0032_organizationradiussettings_sms_message.py
index 88bdc737..868c68fd 100644
--- a/openwisp_radius/migrations/0032_organizationradiussettings_sms_message.py
+++ b/openwisp_radius/migrations/0032_organizationradiussettings_sms_message.py
@@ -2,7 +2,7 @@
 
 from django.db import migrations
 
-import openwisp_radius.base.fields
+import openwisp_utils.fields
 from openwisp_radius import settings as app_settings
 
 
@@ -16,7 +16,7 @@ class Migration(migrations.Migration):
         migrations.AddField(
             model_name='organizationradiussettings',
             name='sms_message',
-            field=openwisp_radius.base.fields.FallbackTextField(
+            field=openwisp_utils.fields.FallbackTextField(
                 blank=True,
                 fallback=app_settings.SMS_MESSAGE_TEMPLATE,
                 help_text=(
diff --git a/openwisp_radius/migrations/0033_alter_organizationradiussettings_password_reset_url.py b/openwisp_radius/migrations/0033_alter_organizationradiussettings_password_reset_url.py
index d2a6a752..b9532bc1 100644
--- a/openwisp_radius/migrations/0033_alter_organizationradiussettings_password_reset_url.py
+++ b/openwisp_radius/migrations/0033_alter_organizationradiussettings_password_reset_url.py
@@ -2,8 +2,8 @@
 
 from django.db import migrations
 
-import openwisp_radius.base.fields
 import openwisp_radius.base.validators
+import openwisp_utils.fields
 from openwisp_radius import settings as app_settings
 
 
@@ -17,7 +17,7 @@ class Migration(migrations.Migration):
         migrations.AlterField(
             model_name='organizationradiussettings',
             name='password_reset_url',
-            field=openwisp_radius.base.fields.FallbackCharField(
+            field=openwisp_utils.fields.FallbackCharField(
                 blank=True,
                 default=app_settings.DEFAULT_PASSWORD_RESET_URL,
                 fallback=app_settings.DEFAULT_PASSWORD_RESET_URL,
diff --git a/openwisp_radius/migrations/0034_organizationradiussettings_coa_enabled.py b/openwisp_radius/migrations/0034_organizationradiussettings_coa_enabled.py
index 1eca3748..2af8dcfe 100644
--- a/openwisp_radius/migrations/0034_organizationradiussettings_coa_enabled.py
+++ b/openwisp_radius/migrations/0034_organizationradiussettings_coa_enabled.py
@@ -2,7 +2,7 @@
 
 from django.db import migrations
 
-import openwisp_radius.base.fields
+import openwisp_utils.fields
 from openwisp_radius import settings as app_settings
 
 
@@ -16,7 +16,7 @@ class Migration(migrations.Migration):
         migrations.AddField(
             model_name='organizationradiussettings',
             name='coa_enabled',
-            field=openwisp_radius.base.fields.FallbackBooleanChoiceField(
+            field=openwisp_utils.fields.FallbackBooleanChoiceField(
                 blank=True,
                 default=None,
                 fallback=app_settings.COA_ENABLED,


Test patch:
diff --git a/openwisp_radius/tests/test_models.py b/openwisp_radius/tests/test_models.py
index dd4a6ae4..a1757302 100644
--- a/openwisp_radius/tests/test_models.py
+++ b/openwisp_radius/tests/test_models.py
@@ -7,7 +7,6 @@
 from django.conf import settings
 from django.contrib.auth import get_user_model
 from django.core.exceptions import ValidationError
-from django.db import connection
 from django.db.models import ProtectedError
 from django.urls import reverse
 from django.utils import timezone
@@ -883,37 +882,6 @@ def test_delete_csv_file(self):
             batch.delete()
 
 
-class TestOrganizationRadiusSettings(BaseTestCase):
-    def test_fallback_field_falsy_values(self):
-        org = self._get_org()
-        rad_settings = org.radius_settings
-
-        def _verify_none_database_value(field_name):
-            setattr(rad_settings, field_name, '')
-            rad_settings.full_clean()
-            rad_settings.save()
-            with connection.cursor() as cursor:
-                cursor.execute(
-                    f'SELECT {field_name} FROM'
-                    f' {rad_settings._meta.app_label}_{rad_settings._meta.model_name}'
-                    f' WHERE id = \'{rad_settings.id.hex}\';',
-                )
-                row = cursor.fetchone()
-            self.assertEqual(row[0], None)
-
-        with self.subTest('Test "sms_message" field'):
-            _verify_none_database_value('sms_message')
-
-        with self.subTest('Test "freeradius_allowed_hosts" field'):
-            _verify_none_database_value('freeradius_allowed_hosts')
-
-        with self.subTest('Test "allowed_mobile_prefixes" field'):
-            _verify_none_database_value('allowed_mobile_prefixes')
-
-        with self.subTest('Test "password_reset_url" field'):
-            _verify_none_database_value('password_reset_url')
-
-
 class TestChangeOfAuthorization(BaseTransactionTestCase):
     def _change_radius_user_group(self, user, organization):
         rad_user_group = user.radiususergroup_set.first()
diff --git a/tests/openwisp2/sample_radius/migrations/0028_alter_organizationradiussettings_allowed_mobile_prefixes_and_more.py b/tests/openwisp2/sample_radius/migrations/0028_alter_organizationradiussettings_allowed_mobile_prefixes_and_more.py
index cbea6e62..d33a8608 100644
--- a/tests/openwisp2/sample_radius/migrations/0028_alter_organizationradiussettings_allowed_mobile_prefixes_and_more.py
+++ b/tests/openwisp2/sample_radius/migrations/0028_alter_organizationradiussettings_allowed_mobile_prefixes_and_more.py
@@ -2,7 +2,8 @@
 
 from django.db import migrations
 
-import openwisp_radius.base.fields
+import openwisp_radius
+import openwisp_utils.fields
 from openwisp_radius import settings as app_settings
 
 
@@ -16,7 +17,7 @@ class Migration(migrations.Migration):
         migrations.AlterField(
             model_name='organizationradiussettings',
             name='allowed_mobile_prefixes',
-            field=openwisp_radius.base.fields.FallbackTextField(
+            field=openwisp_utils.fields.FallbackTextField(
                 blank=True,
                 default=','.join(app_settings.ALLOWED_MOBILE_PREFIXES),
                 fallback=','.join(app_settings.ALLOWED_MOBILE_PREFIXES),
@@ -30,7 +31,7 @@ class Migration(migrations.Migration):
         migrations.AlterField(
             model_name='organizationradiussettings',
             name='birth_date',
-            field=openwisp_radius.base.fields.FallbackCharChoiceField(
+            field=openwisp_utils.fields.FallbackCharChoiceField(
                 blank=True,
                 choices=[
                     ('disabled', 'Disabled'),
@@ -52,7 +53,7 @@ class Migration(migrations.Migration):
         migrations.AlterField(
             model_name='organizationradiussettings',
             name='first_name',
-            field=openwisp_radius.base.fields.FallbackCharChoiceField(
+            field=openwisp_utils.fields.FallbackCharChoiceField(
                 blank=True,
                 choices=[
                     ('disabled', 'Disabled'),
@@ -74,7 +75,7 @@ class Migration(migrations.Migration):
         migrations.AlterField(
             model_name='organizationradiussettings',
             name='freeradius_allowed_hosts',
-            field=openwisp_radius.base.fields.FallbackTextField(
+            field=openwisp_utils.fields.FallbackTextField(
                 blank=True,
                 default=','.join(app_settings.FREERADIUS_ALLOWED_HOSTS),
                 fallback=','.join(app_settings.FREERADIUS_ALLOWED_HOSTS),
@@ -88,7 +89,7 @@ class Migration(migrations.Migration):
         migrations.AlterField(
             model_name='organizationradiussettings',
             name='last_name',
-            field=openwisp_radius.base.fields.FallbackCharChoiceField(
+            field=openwisp_utils.fields.FallbackCharChoiceField(
                 blank=True,
                 choices=[
                     ('disabled', 'Disabled'),
@@ -110,7 +111,7 @@ class Migration(migrations.Migration):
         migrations.AlterField(
             model_name='organizationradiussettings',
             name='location',
-            field=openwisp_radius.base.fields.FallbackCharChoiceField(
+            field=openwisp_utils.fields.FallbackCharChoiceField(
                 blank=True,
                 choices=[
                     ('disabled', 'Disabled'),
@@ -132,7 +133,7 @@ class Migration(migrations.Migration):
         migrations.AlterField(
             model_name='organizationradiussettings',
             name='needs_identity_verification',
-            field=openwisp_radius.base.fields.FallbackBooleanChoiceField(
+            field=openwisp_utils.fields.FallbackBooleanChoiceField(
                 blank=True,
                 default=None,
                 help_text=(
@@ -146,7 +147,7 @@ class Migration(migrations.Migration):
         migrations.AlterField(
             model_name='organizationradiussettings',
             name='password_reset_url',
-            field=openwisp_radius.base.fields.FallbackCharField(
+            field=openwisp_utils.fields.FallbackCharField(
                 blank=True,
                 default=app_settings.DEFAULT_PASSWORD_RESET_URL,
                 fallback=app_settings.DEFAULT_PASSWORD_RESET_URL,
@@ -162,7 +163,7 @@ class Migration(migrations.Migration):
         migrations.AlterField(
             model_name='organizationradiussettings',
             name='registration_enabled',
-            field=openwisp_radius.base.fields.FallbackBooleanChoiceField(
+            field=openwisp_utils.fields.FallbackBooleanChoiceField(
                 blank=True,
                 default=None,
                 help_text=(
@@ -175,7 +176,7 @@ class Migration(migrations.Migration):
         migrations.AlterField(
             model_name='organizationradiussettings',
             name='saml_registration_enabled',
-            field=openwisp_radius.base.fields.FallbackBooleanChoiceField(
+            field=openwisp_utils.fields.FallbackBooleanChoiceField(
                 blank=True,
                 default=None,
                 help_text=(
@@ -189,7 +190,7 @@ class Migration(migrations.Migration):
         migrations.AlterField(
             model_name='organizationradiussettings',
             name='sms_verification',
-            field=openwisp_radius.base.fields.FallbackBooleanChoiceField(
+            field=openwisp_utils.fields.FallbackBooleanChoiceField(
                 blank=True,
                 default=None,
                 help_text=(
@@ -204,7 +205,7 @@ class Migration(migrations.Migration):
         migrations.AlterField(
             model_name='organizationradiussettings',
             name='social_registration_enabled',
-            field=openwisp_radius.base.fields.FallbackBooleanChoiceField(
+            field=openwisp_utils.fields.FallbackBooleanChoiceField(
                 blank=True,
                 default=None,
                 help_text=(
@@ -218,7 +219,7 @@ class Migration(migrations.Migration):
         migrations.AddField(
             model_name='organizationradiussettings',
             name='sms_message',
-            field=openwisp_radius.base.fields.FallbackTextField(
+            field=openwisp_utils.fields.FallbackTextField(
                 blank=True,
                 fallback=app_settings.SMS_MESSAGE_TEMPLATE,
                 help_text=(
@@ -233,7 +234,7 @@ class Migration(migrations.Migration):
         migrations.AddField(
             model_name='organizationradiussettings',
             name='coa_enabled',
-            field=openwisp_radius.base.fields.FallbackBooleanChoiceField(
+            field=openwisp_utils.fields.FallbackBooleanChoiceField(
                 blank=True,
                 default=None,
                 fallback=app_settings.COA_ENABLED,