from typing import Dict, Optional, Union from django.conf import settings from django.contrib.postgres.fields.hstore import KeyTransform from django.db.utils import IntegrityError from ..forms import LocalizedIntegerFieldForm from ..value import LocalizedIntegerValue, LocalizedValue from .field import LocalizedField class LocalizedIntegerFieldKeyTransform(KeyTransform): """Transform that selects a single key from a hstore value and casts it to an integer.""" def as_sql(self, compiler, connection): sql, params = super().as_sql(compiler, connection) return f"{sql}::integer", params class LocalizedIntegerField(LocalizedField): """Stores integers as a localized value.""" attr_class = LocalizedIntegerValue def get_transform(self, name): """Gets the transformation to apply when selecting this value. This is where the SQL expression to grab a single is added and the cast to integer so that sorting by a hstore value works as expected. """ def _transform(*args, **kwargs): return LocalizedIntegerFieldKeyTransform(name, *args, **kwargs) return _transform @classmethod def from_db_value(cls, value, *_) -> Optional[LocalizedIntegerValue]: db_value = super().from_db_value(value) if db_value is None: return db_value if isinstance(db_value, str): return int(db_value) # if we were used in an expression somehow then it might be # that we're returning an individual value or an array, so # we should not convert that into an :see:LocalizedIntegerValue if not isinstance(db_value, LocalizedValue): return db_value return cls._convert_localized_value(db_value) def to_python( self, value: Union[Dict[str, int], int, None] ) -> LocalizedIntegerValue: """Converts the value from a database value into a Python value.""" db_value = super().to_python(value) return self._convert_localized_value(db_value) def get_prep_value(self, value: LocalizedIntegerValue) -> dict: """Gets the value in a format to store into the database.""" # apply default values default_values = LocalizedIntegerValue(self.default) if isinstance(value, LocalizedIntegerValue): for lang_code, _ in settings.LANGUAGES: local_value = value.get(lang_code) if local_value is None: value.set(lang_code, default_values.get(lang_code, None)) prepped_value = super().get_prep_value(value) if prepped_value is None: return None # make sure all values are proper integers for lang_code, _ in settings.LANGUAGES: local_value = prepped_value[lang_code] try: if local_value is not None: int(local_value) except (TypeError, ValueError): raise IntegrityError( 'non-integer value in column "%s.%s" violates ' "integer constraint" % (self.name, lang_code) ) # convert to a string before saving because the underlying # type is hstore, which only accept strings prepped_value[lang_code] = ( str(local_value) if local_value is not None else None ) return prepped_value def formfield(self, **kwargs): """Gets the form field associated with this field.""" defaults = {"form_class": LocalizedIntegerFieldForm} defaults.update(kwargs) return super().formfield(**defaults) @staticmethod def _convert_localized_value( value: LocalizedValue ) -> LocalizedIntegerValue: """Converts from :see:LocalizedValue to :see:LocalizedIntegerValue.""" integer_values = {} for lang_code, _ in settings.LANGUAGES: local_value = value.get(lang_code, None) if local_value is None or local_value.strip() == "": local_value = None try: integer_values[lang_code] = int(local_value) except (ValueError, TypeError): integer_values[lang_code] = None return LocalizedIntegerValue(integer_values)