# 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)