import decimal
import json
import numbers
import babel
import six
from django import forms
from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator
from django.db import models
from django.db.models import BLANK_CHOICE_DASH
from django.forms.widgets import NumberInput
from django.utils.translation import gettext_lazy as _
from jsonfield.fields import JSONField
from shuup.core.fields.tagged_json import TaggedJSONEncoder, tag_registry
from shuup.utils.django_compat import force_text
from shuup.utils.i18n import get_current_babel_locale, remove_extinct_languages
IdentifierValidator = RegexValidator("[a-z][a-z_]+")
MONEY_FIELD_DECIMAL_PLACES = 9
FORMATTED_DECIMAL_FIELD_DECIMAL_PLACES = 9
FORMATTED_DECIMAL_FIELD_MAX_DIGITS = 36
[docs]
class InternalIdentifierField(models.CharField):
[docs]
def __init__(self, **kwargs):
if "unique" not in kwargs:
raise ValueError("Error! You must explicitly set the `unique` flag for `InternalIdentifierField`s.")
kwargs.setdefault("max_length", 64)
kwargs.setdefault("blank", True)
kwargs.setdefault("null", bool(kwargs.get("blank"))) # If it's allowed to be blank, it should be null
kwargs.setdefault("verbose_name", _("internal identifier"))
kwargs.setdefault(
"help_text",
_("Do not change this value if you are not sure what you are doing."),
)
kwargs.setdefault("editable", False)
super().__init__(**kwargs)
self.validators.append(IdentifierValidator)
[docs]
def get_prep_value(self, value):
# Save `None`s instead of falsy values (such as empty strings)
# for `InternalIdentifierField`s to avoid `IntegrityError`s on unique fields.
prepared_value = super().get_prep_value(value)
if self.null:
return prepared_value or None
return prepared_value
[docs]
def deconstruct(self):
(name, path, args, kwargs) = super().deconstruct()
kwargs["null"] = self.null
kwargs["unique"] = self.unique
kwargs["blank"] = self.blank
# Irrelevant for migrations, and usually translated anyway:
kwargs.pop("verbose_name", None)
kwargs.pop("help_text", None)
return (name, path, args, kwargs)
[docs]
class CurrencyField(models.CharField):
[docs]
def __init__(self, **kwargs):
kwargs.setdefault("max_length", 4)
super().__init__(**kwargs)
[docs]
class MoneyValueField(FormattedDecimalField):
[docs]
def __init__(self, **kwargs):
kwargs.setdefault("decimal_places", MONEY_FIELD_DECIMAL_PLACES)
kwargs.setdefault("max_digits", FORMATTED_DECIMAL_FIELD_MAX_DIGITS)
super().__init__(**kwargs)
[docs]
class QuantityField(FormattedDecimalField):
[docs]
def __init__(self, **kwargs):
kwargs.setdefault("decimal_places", FORMATTED_DECIMAL_FIELD_DECIMAL_PLACES)
kwargs.setdefault("max_digits", FORMATTED_DECIMAL_FIELD_MAX_DIGITS)
kwargs.setdefault("default", 0)
super().__init__(**kwargs)
[docs]
class MeasurementField(FormattedDecimalField):
[docs]
def __init__(self, unit, **kwargs):
self.unit = unit
kwargs.setdefault("decimal_places", FORMATTED_DECIMAL_FIELD_DECIMAL_PLACES)
kwargs.setdefault("max_digits", FORMATTED_DECIMAL_FIELD_MAX_DIGITS)
kwargs.setdefault("default", 0)
super().__init__(**kwargs)
[docs]
def deconstruct(self):
parent = super()
(name, path, args, kwargs) = parent.deconstruct()
kwargs["unit"] = self.unit
return (name, path, args, kwargs)
[docs]
class LanguageFieldMixin:
LANGUAGE_CODES = remove_extinct_languages(tuple(set(babel.Locale("en").languages.keys())))
[docs]
class LanguageField(LanguageFieldMixin, models.CharField):
[docs]
def __init__(self, *args, **kwargs):
kwargs.setdefault("max_length", 10)
kwargs["choices"] = [(code, code) for code in sorted(self.LANGUAGE_CODES)]
super().__init__(*args, **kwargs)
[docs]
def get_choices(self, include_blank=True, blank_choice=BLANK_CHOICE_DASH):
locale = get_current_babel_locale()
translated_choices = [
(code, locale.languages.get(code, code)) for (code, _) in super().get_choices(include_blank, blank_choice)
]
translated_choices.sort(key=lambda pair: pair[1].lower())
return translated_choices
# https://docs.djangoproject.com/en/1.8/ref/models/fields/#django.db.models.ForeignKey.allow_unsaved_instance_assignment
[docs]
class UnsavedForeignKey(models.ForeignKey):
allow_unsaved_instance_assignment = True
[docs]
class TaggedJSONField(JSONField):
[docs]
def __init__(self, *args, **kwargs):
dump_kwargs = kwargs.setdefault("dump_kwargs", {})
dump_kwargs.setdefault("cls", TaggedJSONEncoder)
dump_kwargs.setdefault("separators", (",", ":"))
load_kwargs = kwargs.setdefault("load_kwargs", {})
load_kwargs.setdefault("object_hook", tag_registry.decode)
super().__init__(*args, **kwargs)
[docs]
class HexColorField(models.CharField):
"""
Supports hexadecimal color values: #ABC, #AABBCC, #001122AA.
"""
[docs]
def __init__(self, **kwargs):
kwargs["max_length"] = 9
super().__init__(**kwargs)
self.validators.append(RegexValidator("^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$", _("Invalid color")))
[docs]
class SeparatedValuesField(models.TextField):
"""
https://stackoverflow.com/questions/1110153/what-is-the-most-efficient-way-to-store-a-list-in-the-django-models
"""
[docs]
def __init__(self, *args, **kwargs):
self.separator = kwargs.pop("separator", ",")
super().__init__(*args, **kwargs)
[docs]
def from_db_value(self, value, expression, connection):
if isinstance(value, six.string_types):
return value.split(self.separator)
return []
[docs]
def get_db_prep_value(self, value, connection, prepared=False):
if not value:
return
if isinstance(value, list) or isinstance(value, tuple):
return self.separator.join([force_text(s) for s in value])
if isinstance(value, six.string_types):
return value
[docs]
def value_to_string(self, obj):
value = self._get_val_from_obj(obj)
return self.get_db_prep_value(value)
[docs]
def polymorphic_has_pk(obj):
if getattr(obj, "polymorphic_primary_key_name", None):
if getattr(obj, obj.polymorphic_primary_key_name, None):
return True
return False
[docs]
class PolymorphicJSONField(JSONField):
"""
Use this field when using JSONField inside a polumorphic model.
https://github.com/dmkoch/django-jsonfield/pull/193
"""
[docs]
def pre_init(self, value, obj):
try:
if obj._state.adding:
# Make sure the primary key actually exists on the object before
# checking if it's empty. This is a special case for South datamigrations
# see: https://github.com/bradjasper/django-jsonfield/issues/52
if getattr(obj, "pk", None) is not None or polymorphic_has_pk(obj):
if isinstance(value, six.string_types):
try:
return json.loads(value, **self.load_kwargs)
except ValueError as err:
raise ValidationError(_("Enter a valid JSON.")) from err
except AttributeError:
# south fake meta class doesn't create proper attributes
# see this:
# https://github.com/bradjasper/django-jsonfield/issues/52
pass
return value