Source code for common.fields

"""Common field types."""

from typing import Union

from dateutil.relativedelta import relativedelta
from django.contrib.postgres.fields import DateRangeField
from django.contrib.postgres.fields import DateTimeRangeField
from django.db import models
from django.db.models.expressions import RawSQL
from django.forms import ModelChoiceField
from django.urls import reverse_lazy
from psycopg.types.range import DateRange
from psycopg.types.range import Range

from common import validators
from common.util import TaricDateRange
from common.util import TaricDateTimeRange
from common.widgets import AutocompleteWidget


def get_next_by_max(field):
    return lambda: RawSQL(
        sql=f'SELECT COALESCE(MAX("{field.column}"), 0) + 1 FROM "{field.model._meta.db_table}"',
        params=[],
    )


[docs]class NumericSID(models.PositiveIntegerField): def __init__(self, *args, **kwargs): kwargs["editable"] = False kwargs["validators"] = [validators.NumericSIDValidator()] kwargs["db_index"] = True kwargs.setdefault("default", get_next_by_max(self)) super().__init__(*args, **kwargs)
[docs] def deconstruct(self): name, path, args, kwargs = super().deconstruct() del kwargs["editable"] del kwargs["validators"] del kwargs["db_index"] del kwargs["default"] return name, path, args, kwargs
[docs]class SignedIntSID(models.IntegerField): def __init__(self, *args, **kwargs): kwargs["editable"] = False kwargs["db_index"] = True kwargs.setdefault("default", get_next_by_max(self)) super().__init__(*args, **kwargs)
[docs] def deconstruct(self): name, path, args, kwargs = super().deconstruct() del kwargs["editable"] del kwargs["db_index"] del kwargs["default"] return name, path, args, kwargs
[docs]class ShortDescription(models.CharField): def __init__(self, *args, **kwargs): kwargs["max_length"] = 500 kwargs["blank"] = True kwargs["null"] = True super().__init__(*args, **kwargs)
[docs] def deconstruct(self): name, path, args, kwargs = super().deconstruct() del kwargs["max_length"] del kwargs["blank"] del kwargs["null"] return name, path, args, kwargs
class LongDescription(models.TextField): def __init__(self, *args, **kwargs): kwargs["max_length"] = 20000 kwargs["blank"] = True kwargs["null"] = True super().__init__(*args, **kwargs) def deconstruct(self): name, path, args, kwargs = super().deconstruct() del kwargs["max_length"] del kwargs["blank"] del kwargs["null"] return name, path, args, kwargs
[docs]class ApplicabilityCode(models.PositiveSmallIntegerField): def __init__(self, *args, **kwargs): kwargs["choices"] = validators.ApplicabilityCode.choices super().__init__(*args, **kwargs)
[docs] def deconstruct(self): name, path, args, kwargs = super().deconstruct() del kwargs["choices"] return name, path, args, kwargs
class TaricDateRangeField(DateRangeField): """Model field that converts between `common.util.TaricDateRange` and its database representation.""" range_type = TaricDateRange def from_db_value( self, value: Union[Range, DateRange, TaricDateRange], *_args, **_kwargs, ) -> TaricDateRange: """ By default Django ignores the range_type and just returns a Psycopg DateRange. Additionally, from the Psycopg docs (https://www.psycopg.org/psycopg3/docs/basic/pgtypes.html#range-adaptation): All the PostgreSQL range types are loaded as the Range Python type, which is a Generic type and can hold bounds of different types. This method forces the conversion to a TaricDateRange and shifts the upper date to be inclusive (it is exclusive by default). """ if not value: return value if isinstance(value, TaricDateRange): # Avoid re-applying the date shift / inclusive bound change to # `upper` (i.e. a TaricDateRange instance implies a shift has # already been applied). return value lower = value.lower upper = value.upper if not value.upper_inc and not value.upper_inf: upper = upper - relativedelta(days=1) return TaricDateRange(lower=lower, upper=upper) class TaricDateTimeRangeField(DateTimeRangeField): range_type = TaricDateTimeRange class AutoCompleteField(ModelChoiceField): def __init__(self, *args, **kwargs): qs = kwargs["queryset"] prefix = getattr(qs.model, "url_pattern_name_prefix", None) if not prefix: prefix = qs.model._meta.model_name self.widget = AutocompleteWidget( attrs={ "label": kwargs.get("label", ""), "help_text": kwargs.get("help_text", ""), "source_url": reverse_lazy(f"{prefix}-list"), **kwargs.pop("attrs", {}), }, ) super().__init__(*args, **kwargs) def prepare_value(self, value): return self.to_python(value)