Source code for shoop.core.fields

# -*- coding: utf-8 -*-
# This file is part of Shoop.
#
# Copyright (c) 2012-2016, Shoop Ltd. All rights reserved.
#
# This source code is licensed under the AGPLv3 license found in the
# LICENSE file in the root directory of this source tree.
from __future__ import unicode_literals

import decimal

import babel
from django import forms
from django.core.exceptions import ImproperlyConfigured
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 ugettext_lazy as _
from jsonfield.fields import JSONField

from shoop.core.fields.tagged_json import tag_registry, TaggedJSONEncoder
from shoop.utils.i18n import get_current_babel_locale

IdentifierValidator = RegexValidator("[a-z][a-z_]+")


[docs]class InternalIdentifierField(models.CharField): def __init__(self, **kwargs): if "unique" not in kwargs: raise ValueError("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", _(u"Do not change this value if you are not sure what you're doing.")) kwargs.setdefault("editable", False) super(InternalIdentifierField, self).__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(InternalIdentifierField, self).get_prep_value(value) if self.null: return (prepared_value or None) return prepared_value
[docs] def deconstruct(self): (name, path, args, kwargs) = super(InternalIdentifierField, self).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): def __init__(self, **kwargs): kwargs.setdefault("max_length", 4) super(CurrencyField, self).__init__(**kwargs)
[docs]class FormattedDecimalFormField(forms.DecimalField): # Chrome automatically converts a step with more than 5 decimals places to scientific notation MAX_DECIMAL_PLACES_FOR_STEP = 5
[docs] def widget_attrs(self, widget): # be more lenient when setting step than the default django widget_attrs if isinstance(widget, NumberInput) and 'step' not in widget.attrs: if self.decimal_places <= self.MAX_DECIMAL_PLACES_FOR_STEP: step = format(decimal.Decimal('1') / 10 ** self.decimal_places, 'f') else: step = 'any' widget.attrs.setdefault('step', step) return super(FormattedDecimalFormField, self).widget_attrs(widget)
[docs]class FormattedDecimalField(models.DecimalField): """ DecimalField subclass to display decimal values in non-scientific format. """
[docs] def value_from_object(self, obj): value = super(FormattedDecimalField, self).value_from_object(obj) if isinstance(value, decimal.Decimal): return self.format_decimal(value)
[docs] def format_decimal(self, value, max_digits=100, exponent_limit=100): assert isinstance(value, decimal.Decimal) val = value.normalize() (sign, digits, exponent) = val.as_tuple() if exponent > exponent_limit: raise ValueError('Exponent too large for formatting: %r' % value) elif exponent < -exponent_limit: raise ValueError('Exponent too small for formatting: %r' % value) if len(digits) > max_digits: raise ValueError('Too many digits for formatting: %r' % value) return format(val, 'f')
[docs] def formfield(self, **kwargs): kwargs.setdefault("form_class", FormattedDecimalFormField) return super(FormattedDecimalField, self).formfield(**kwargs)
[docs]class MoneyValueField(FormattedDecimalField): def __init__(self, **kwargs): kwargs.setdefault("decimal_places", 9) kwargs.setdefault("max_digits", 36) super(MoneyValueField, self).__init__(**kwargs)
[docs]class QuantityField(FormattedDecimalField): def __init__(self, **kwargs): kwargs.setdefault("decimal_places", 9) kwargs.setdefault("max_digits", 36) kwargs.setdefault("default", 0) super(QuantityField, self).__init__(**kwargs)
[docs]class MeasurementField(FormattedDecimalField): KNOWN_UNITS = ("mm", "m", "kg", "g", "m3") def __init__(self, unit, **kwargs): if unit not in self.KNOWN_UNITS: raise ImproperlyConfigured("Unit %r is not a known unit." % unit) self.unit = unit kwargs.setdefault("decimal_places", 9) kwargs.setdefault("max_digits", 36) kwargs.setdefault("default", 0) super(MeasurementField, self).__init__(**kwargs)
[docs] def deconstruct(self): parent = super(MeasurementField, self) (name, path, args, kwargs) = parent.deconstruct() kwargs["unit"] = self.unit return (name, path, args, kwargs)
[docs]class LanguageField(models.CharField): # TODO: This list will include extinct languages LANGUAGE_CODES = set(babel.Locale("en").languages.keys()) def __init__(self, *args, **kwargs): kwargs.setdefault("max_length", 10) kwargs["choices"] = [(code, code) for code in sorted(self.LANGUAGE_CODES)] super(LanguageField, self).__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(LanguageField, self).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): 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(TaggedJSONField, self).__init__(*args, **kwargs)