Source code for superdjango.db.calculated.fields

# Imports

from django.core.exceptions import ImproperlyConfigured
from django.db import models
from django.utils.translation import ugettext_lazy as _

# Exports

__all__ = (
    "CalculatedFieldMixin",
    "CalculatedFloatField",
    "CalculatedIntegerField",
    "ConcatenatedCharField",
)

# Fields


[docs]class CalculatedFieldMixin(object): """Mixin for automatically calculate the value of the field from two or more other integer fields.""" AVERAGE = "avg" MAXIMUM = "max" MINIMUM = "min" TOTAL = "sum"
[docs] def __init__(self, **kwargs): """Initialize the field. :param calculate_from: A list of field names upon which the calculation is based. :type calculate_from: list :param calculate_type: The type of calculation to perform; one of ``avg``, ``max``,``min`` or ``total``. A callable may also be given. Defaults to ``total``. :type calculate_type: str | callable If a callable is provided for``calculate_type``, it must accept a dictionary of field name field value pairs to be calculated. It must also return a value of the appropriate type. During init, the ``check_calculation_type()`` method is automatically called. You may need to override this if custom validation of the ``calculate_type`` is needed. .. note:: Calculated fields are not editable. Init will raise the following errors: - ``ImproperlyConfigured`` when no ``calculate_from`` is provided. - ``ValueError`` if the ``calculate_from`` is not given as a list or tuple. - ``ImproperlyConfigured`` when ``check_calculation_type()`` fails. """ # Calculated fields are not editable. kwargs['editable'] = False # Get the list of fields that are to be calculated. self._calculate_from = kwargs.pop("calculate_from", None) if self._calculate_from is None: raise ImproperlyConfigured("calculate_from is required for: %s" % self.__class__.__name__) # Make sure calculate_from is a list or tuple. if type(self._calculate_from) not in (list, tuple): raise ValueError("calculate_from must be a list, not: %s" % type(self._calculate_from)) # Get the type of calculation. self._calculate_type = kwargs.pop("calculate_type", self.TOTAL) if not self.check_calculation_type(): message = "Invalid calculate_type for %s: %s" raise ImproperlyConfigured(message % (self.__class__.__name__, self._calculate_type)) # Super. super().__init__(**kwargs)
# noinspection PyMethodMayBeStatic
[docs] def check_calculation_type(self): """Check that the given calculation type is supported. :rtype: bool """ if callable(self._calculate_type): return True return self._calculate_type in (self.AVERAGE, self.MAXIMUM, self.MINIMUM, self.TOTAL)
# noinspection PyUnusedLocal
[docs] def get_calculated_value(self, model_instance, add): """Get the calculated value. This method is called by the field's ``pre_save()`` method.""" _values = dict() values = [] for field_name in self._calculate_from: values.append(getattr(model_instance, field_name, 0)) _values[field_name] = getattr(model_instance, field_name, 0) if callable(self._calculate_type): return self._calculate_type(_values) if self._calculate_type == self.AVERAGE: return self._avg(values) elif self._calculate_type == self.MAXIMUM: return self._max(values) elif self._calculate_type == self.MINIMUM: return self._min(values) else: return self._sum(values)
[docs] def pre_save(self, model_instance, add): """Override to get calculated value""" return self.get_calculated_value(model_instance, add)
# noinspection PyMethodMayBeStatic def _avg(self, values): """Return the average of the given values.""" return sum(values) / len(values) # noinspection PyMethodMayBeStatic def _max(self, values): """Return the maximum of the given values.""" return max(values) # noinspection PyMethodMayBeStatic def _min(self, values): """Return the minimum of the given values.""" return min(values) # noinspection PyMethodMayBeStatic def _sum(self, values): """Return the sum of the given values.""" return sum(values)
[docs]class CalculatedFloatField(CalculatedFieldMixin, models.FloatField): """Automatically calculate the value of the field from two or more other integer fields.""" description = _("Automatically calculate the value of a field from two or more other float fields.") # TODO: Validate that the from fields are also floats. def _avg(self, values): """Return the average of the given values.""" return float(sum(values) / len(values)) def _max(self, values): """Return the maximum of the given values.""" return float(max(values)) def _min(self, values): """Return the minimum of the given values.""" return float(min(values)) def _sum(self, values): """Return the sum of the given values.""" return float(sum(values))
[docs]class CalculatedIntegerField(CalculatedFieldMixin, models.IntegerField): """Automatically calculate the value of the field from two or more other integer fields. .. code-block:: py from superdjango.db.calculated.fields import CalculatedIntegerField class Task(models.Model): pessimistic_estimate = models.IntegerField() optimistic_estimate = models.IntegerField() realistic_estimate = models.IntegerField() average_estimate = CalculatedIntegerField( _("average estimate"), calculate_from=[ "pessimistic_estimate", "optimistic_estimate", "realistic_estimate", ], calculate_type=CalculatedIntegerField.AVERAGE ) """ description = _("Automatically calculate the value of a field from two or more other integer fields.") # TODO: Validate that the from fields are also integers. def _avg(self, values): """Return the average of the given values.""" return int(sum(values) / len(values)) def _max(self, values): """Return the maximum of the given values.""" return int(max(values)) def _min(self, values): """Return the minimum of the given values.""" return int(min(values)) def _sum(self, values): """Return the sum of the given values.""" return int(sum(values))
[docs]class ConcatenatedCharField(CalculatedFieldMixin, models.CharField): """Concatenate char fields together.""" description = _("Automatically concatenate the value of a field from two or more other char fields.") # TODO: Validate that total length of from fields will fit in specified max_length.
[docs] def __init__(self, **kwargs): """Initialize a concatenated field. :param separator: The separated used to concatenate the strings. Defaults to a space. :type separator: str """ self._separator = kwargs.pop("separator", " ") super().__init__(**kwargs)
[docs] def check_calculation_type(self): """Always returns ``True``.""" return True
[docs] def get_calculated_value(self, model_instance, add): """Get the concatenated string.""" _values = dict() values = [] for field_name in self._calculate_from: values.append(str(getattr(model_instance, field_name, ""))) _values[field_name] = getattr(model_instance, field_name, "") if callable(self._calculate_type): return self._calculate_type(_values, separator=self._separator) return self._separator.join(values)