Skip to content

Commit

Permalink
[change] Updated fallback choice fields
Browse files Browse the repository at this point in the history
- Removed dependency for using get_field_value method to access
  value of a field.
- Simplified logic for choice fields
- Added FallbackDecimalField
  • Loading branch information
pandafy committed Jul 24, 2024
1 parent 593bcf3 commit 28e039b
Show file tree
Hide file tree
Showing 8 changed files with 156 additions and 118 deletions.
56 changes: 28 additions & 28 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -905,12 +905,6 @@ Model class inheriting ``UUIDModel`` which provides two additional fields:
Which use respectively ``AutoCreatedField``, ``AutoLastModifiedField`` from ``model_utils.fields``
(self-updating fields providing the creation date-time and the last modified date-time).

``openwisp_utils.base.FallBackModelMixin``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Model mixin that implements ``get_field_value`` method which can be used
to get value of fallback fields.

Custom Fields
-------------

Expand All @@ -927,10 +921,10 @@ A model field which provides a random key or token, widely used across openwisp

This field extends Django's `BooleanField <https://docs.djangoproject.com/en/4.2/ref/models/fields/#booleanfield>`_
and provides additional functionality for handling choices with a fallback value.
The field will use the **fallback value** whenever the field is set to ``None``.
The field will return the **fallback value** whenever the field is set to ``None``.

This field is particularly useful when you want to present a choice between enabled
and disabled options, with an additional "Default" option that reflects the fallback value.
and disabled options.

.. code-block:: python
Expand All @@ -940,9 +934,6 @@ and disabled options, with an additional "Default" option that reflects the fall
class MyModel(models.Model):
is_active = FallbackBooleanChoiceField(
null=True,
blank=True,
default=None,
fallback=app_settings.IS_ACTIVE_FALLBACK,
)
Expand All @@ -951,7 +942,7 @@ and disabled options, with an additional "Default" option that reflects the fall

This field extends Django's `CharField <https://docs.djangoproject.com/en/4.2/ref/models/fields/#charfield>`_
and provides additional functionality for handling choices with a fallback value.
The field will use the **fallback value** whenever the field is set to ``None``.
The field will return the **fallback value** whenever the field is set to ``None``.

.. code-block:: python
Expand All @@ -961,8 +952,6 @@ The field will use the **fallback value** whenever the field is set to ``None``.
class MyModel(models.Model):
is_first_name_required = FallbackCharChoiceField(
null=True,
blank=True,
max_length=32,
choices=(
('disabled', _('Disabled')),
Expand All @@ -977,8 +966,7 @@ The field will use the **fallback value** whenever the field is set to ``None``.

This field extends Django's `CharField <https://docs.djangoproject.com/en/4.2/ref/models/fields/#charfield>`_
and provides additional functionality for handling text fields with a fallback value.

It allows populating the form with the fallback value when the actual value is set to ``null`` in the database.
The field will return the **fallback value** whenever the field is set to ``None``.

.. code-block:: python
Expand All @@ -988,8 +976,6 @@ It allows populating the form with the fallback value when the actual value is s
class MyModel(models.Model):
greeting_text = FallbackCharField(
null=True,
blank=True,
max_length=200,
fallback=app_settings.GREETING_TEXT,
)
Expand All @@ -999,8 +985,7 @@ It allows populating the form with the fallback value when the actual value is s

This field extends Django's `URLField <https://docs.djangoproject.com/en/4.2/ref/models/fields/#urlfield>`_
and provides additional functionality for handling URL fields with a fallback value.

It allows populating the form with the fallback value when the actual value is set to ``null`` in the database.
The field will return the **fallback value** whenever the field is set to ``None``.

.. code-block:: python
Expand All @@ -1010,8 +995,6 @@ It allows populating the form with the fallback value when the actual value is s
class MyModel(models.Model):
password_reset_url = FallbackURLField(
null=True,
blank=True,
max_length=200,
fallback=app_settings.DEFAULT_PASSWORD_RESET_URL,
)
Expand All @@ -1022,7 +1005,7 @@ It allows populating the form with the fallback value when the actual value is s
This extends Django's `TextField <https://docs.djangoproject.com/en/4.2/ref/models/fields/#django.db.models.TextField>`_
and provides additional functionality for handling text fields with a fallback value.

It allows populating the form with the fallback value when the actual value is set to ``null`` in the database.
The field will returned the **fallback value** whenever the field is set to ``None``.

.. code-block:: python
Expand All @@ -1032,8 +1015,6 @@ It allows populating the form with the fallback value when the actual value is s
class MyModel(models.Model):
extra_config = FallbackTextField(
null=True,
blank=True,
max_length=200,
fallback=app_settings.EXTRA_CONFIG,
)
Expand All @@ -1044,7 +1025,7 @@ It allows populating the form with the fallback value when the actual value is s
This extends Django's `PositiveIntegerField <https://docs.djangoproject.com/en/4.2/ref/models/fields/#positiveintegerfield>`_
and provides additional functionality for handling positive integer fields with a fallback value.

It allows populating the form with the fallback value when the actual value is set to ``null`` in the database.
The field will returned the **fallback value** whenever the field is set to ``None``.

.. code-block:: python
Expand All @@ -1054,11 +1035,30 @@ It allows populating the form with the fallback value when the actual value is s
class MyModel(models.Model):
count = FallbackPositiveIntegerField(
blank=True,
null=True,
fallback=app_settings.DEFAULT_COUNT,
)
``openwisp_utils.fields.FallbackDecimalField``
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

This extends Django's `DecimalField <https://docs.djangoproject.com/en/4.2/ref/models/fields/#decimalfield>`_
and provides additional functionality for handling positive integer fields with a fallback value.

The field will returned the **fallback value** whenever the field is set to ``None``.

.. code-block:: python
from django.db import models
from openwisp_utils.fields import FallbackDecimalField
from myapp import settings as app_settings
class MyModel(models.Model):
price = FallbackDecimalField(
max_digits=4,
decimal_places=2,
fallback=app_settings.DEFAULT_price,
)
Admin utilities
---------------

Expand Down
9 changes: 0 additions & 9 deletions openwisp_utils/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,3 @@ class TimeStampedEditableModel(UUIDModel):

class Meta:
abstract = True


class FallbackModelMixin(object):
def get_field_value(self, field_name):
value = getattr(self, field_name)
field = self._meta.get_field(field_name)
if value is None and hasattr(field, 'fallback'):
return field.fallback
return value
72 changes: 28 additions & 44 deletions openwisp_utils/fields.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from django import forms
from django.db.models.fields import (
BLANK_CHOICE_DASH,
BooleanField,
CharField,
DecimalField,
PositiveIntegerField,
TextField,
URLField,
Expand Down Expand Up @@ -39,6 +41,15 @@ def __init__(


class FallbackMixin(object):
"""
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 __init__(self, fallback, *args, **kwargs):
self.fallback = fallback
opts = dict(blank=True, null=True, default=None)
Expand All @@ -50,17 +61,6 @@ def deconstruct(self):
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
Expand Down Expand Up @@ -90,16 +90,12 @@ def clean(self, 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,
"form_class": forms.BooleanField,
'widget': forms.Select(
choices=[
(
'',
_('Default') + f' ({default_value})',
),
choices=BLANK_CHOICE_DASH
+ [
(True, _('Enabled')),
(False, _('Disabled')),
]
Expand All @@ -110,14 +106,6 @@ def formfield(self, **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(
{
Expand All @@ -127,15 +115,11 @@ def formfield(self, **kwargs):
return super().formfield(**kwargs)


class FallbackPositiveIntegerField(
FallbackMixin, FallbackFromDbValueMixin, PositiveIntegerField
):
class FallbackPositiveIntegerField(FallbackMixin, PositiveIntegerField):
pass


class FallbackCharField(
FallbackMixin, FalsyValueNoneMixin, FallbackFromDbValueMixin, CharField
):
class FallbackCharField(FallbackMixin, FalsyValueNoneMixin, CharField):
"""
Populates the form with the fallback value
if the value is set to null in the database.
Expand All @@ -144,9 +128,7 @@ class FallbackCharField(
pass


class FallbackURLField(
FallbackMixin, FalsyValueNoneMixin, FallbackFromDbValueMixin, URLField
):
class FallbackURLField(FallbackMixin, FalsyValueNoneMixin, URLField):
"""
Populates the form with the fallback value
if the value is set to null in the database.
Expand All @@ -155,21 +137,23 @@ class FallbackURLField(
pass


class FallbackTextField(
FallbackMixin, FalsyValueNoneMixin, FallbackFromDbValueMixin, TextField
):
class FallbackTextField(FallbackMixin, FalsyValueNoneMixin, 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})
kwargs.update(
{
'form_class': forms.CharField,
'widget': forms.Textarea(
attrs={'rows': 2, 'cols': 34, 'style': 'width:auto'}
),
}
)
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
class FallbackDecimalField(FallbackMixin, DecimalField):
pass
1 change: 0 additions & 1 deletion openwisp_utils/metric_collection/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ def setUp(self):
# The post_migrate signal creates the first OpenwispVersion object
# and uses the actual modules installed in the Python environment.
# This would cause tests to fail when other modules are also installed.
# import ipdb; ipdb.set_trace()
OpenwispVersion.objects.update(
module_version={
'OpenWISP Version': '23.0.0a',
Expand Down
26 changes: 26 additions & 0 deletions tests/test_project/migrations/0008_book_price.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Generated by Django 4.2.7 on 2024-07-24 15:20

from django.db import migrations
import openwisp_utils.fields


class Migration(migrations.Migration):

dependencies = [
("test_project", "0007_radiusaccounting_start_time_and_more"),
]

operations = [
migrations.AddField(
model_name="book",
name="price",
field=openwisp_utils.fields.FallbackDecimalField(
blank=True,
decimal_places=2,
default=None,
fallback=20.0,
max_digits=4,
null=True,
),
),
]
13 changes: 5 additions & 8 deletions tests/test_project/models.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,19 @@
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.translation import gettext_lazy as _
from openwisp_utils.base import (
FallbackModelMixin,
KeyField,
TimeStampedEditableModel,
UUIDModel,
)
from openwisp_utils.base import KeyField, TimeStampedEditableModel, UUIDModel
from openwisp_utils.fields import (
FallbackBooleanChoiceField,
FallbackCharChoiceField,
FallbackCharField,
FallbackDecimalField,
FallbackPositiveIntegerField,
FallbackTextField,
FallbackURLField,
)


class Shelf(FallbackModelMixin, TimeStampedEditableModel):
class Shelf(TimeStampedEditableModel):
TYPES = (
('HORROR', 'HORROR'),
('FANTASY', 'FANTASY'),
Expand Down Expand Up @@ -67,6 +63,7 @@ class Book(TimeStampedEditableModel):
name = models.CharField(_('name'), max_length=64)
author = models.CharField(_('author'), max_length=64)
shelf = models.ForeignKey('test_project.Shelf', on_delete=models.CASCADE)
price = FallbackDecimalField(max_digits=4, decimal_places=2, fallback=20.0)

def __str__(self):
return self.name
Expand Down Expand Up @@ -98,7 +95,7 @@ class RadiusAccounting(models.Model):
)


class OrganizationRadiusSettings(FallbackModelMixin, models.Model):
class OrganizationRadiusSettings(models.Model):
is_active = FallbackBooleanChoiceField(
fallback=False,
)
Expand Down
Loading

0 comments on commit 28e039b

Please sign in to comment.