diff --git a/.travis.yml b/.travis.yml index 999b7bb..e55d5fe 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,9 @@ language: python python: - - "3.4" - "3.5" - "3.6" env: - - DJANGO_VERSION=1.10 - DJANGO_VERSION=1.11 - DJANGO_VERSION=2.0 diff --git a/chamber/forms/fields.py b/chamber/forms/fields.py index bd2609f..59672c4 100644 --- a/chamber/forms/fields.py +++ b/chamber/forms/fields.py @@ -33,6 +33,8 @@ class PriceField(DecimalField): def __init__(self, *args, **kwargs): currency = kwargs.pop('currency', ugettext('CZK')) + kwargs.setdefault('max_digits', 10) + kwargs.setdefault('decimal_places', 2) if 'widget' not in kwargs: kwargs['widget'] = PriceNumberInput(currency) super().__init__(*args, **kwargs) diff --git a/chamber/models/__init__.py b/chamber/models/__init__.py index ea0390c..86162ee 100644 --- a/chamber/models/__init__.py +++ b/chamber/models/__init__.py @@ -2,9 +2,13 @@ from itertools import chain +from distutils.version import StrictVersion + +import django from django.db import models, transaction from django.db.models.base import ModelBase from django.utils.translation import ugettext_lazy as _ +from django.utils.functional import cached_property from chamber.exceptions import PersistenceException from chamber.patch import Options @@ -267,7 +271,7 @@ class SmartModelBase(ModelBase): def __new__(cls, name, bases, attrs): - new_cls = super(SmartModelBase, cls).__new__(cls, name, bases, attrs) + new_cls = super().__new__(cls, name, bases, attrs) for dispatcher in new_cls.dispatchers: dispatcher.connect(new_cls) return new_cls @@ -442,6 +446,18 @@ def delete(self, *args, **kwargs): def refresh_from_db(self, *args, **kwargs): super().refresh_from_db(*args, **kwargs) + for key, value in self.__class__.__dict__.items(): + if isinstance(value, cached_property): + self.__dict__.pop(key, None) + self.is_adding = False + self.is_changing = True + self.changed_fields = DynamicChangedFields(self) + + if StrictVersion(django.get_version()) < StrictVersion('2.0'): + for field in [f for f in self._meta.get_fields() if f.is_relation]: + if field.get_cache_name() in self.__dict__: + del self.__dict__[field.get_cache_name()] + return self def change(self, **changed_fields): diff --git a/chamber/models/fields.py b/chamber/models/fields.py index caa36e7..6d341e3 100644 --- a/chamber/models/fields.py +++ b/chamber/models/fields.py @@ -20,7 +20,6 @@ from chamber.models.humanized_helpers import price_humanized from chamber.utils.datastructures import SequenceChoicesEnumMixin, SubstatesChoicesNumEnum - try: from sorl.thumbnail import ImageField as OriginImageField except ImportError: @@ -149,11 +148,11 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def pre_save(self, model_instance, add): - if add or hasattr(model_instance, 'changed_fields') and self.copy_field_name in model_instance.changed_fields: + if self.copy_field_name in model_instance.changed_fields: setattr( model_instance, self.attname, getattr(model_instance, self.copy_field_name) - if add else model_instance.initial_values[self.copy_field_name] + if model_instance.is_adding else model_instance.initial_values[self.copy_field_name] ) return super().pre_save(model_instance, add) @@ -164,17 +163,17 @@ class SubchoicesPositiveIntegerField(models.PositiveIntegerField): def __init__(self, *args, **kwargs): self.enum = kwargs.pop('enum', None) - self.subchoices_field_name = kwargs.pop('subchoices_field_name', None) + self.supchoices_field_name = kwargs.pop('supchoices_field_name', None) assert self.enum is None or isinstance(self.enum, SubstatesChoicesNumEnum) if self.enum: kwargs['choices'] = self.enum.choices super().__init__(*args, **kwargs) - def _get_subvalue(self, model_instance): - return getattr(model_instance, self.subchoices_field_name) + def _get_supvalue(self, model_instance): + return getattr(model_instance, self.supchoices_field_name) def clean(self, value, model_instance): - if self.enum and self._get_subvalue(model_instance) not in self.enum.categories: + if self.enum and self._get_supvalue(model_instance) not in self.enum.categories: return None else: return super().clean(value, model_instance) @@ -184,7 +183,7 @@ def _raise_error_if_value_should_be_empty(self, value, subvalue): raise ValidationError(ugettext('Value must be empty')) def _raise_error_if_value_not_allowed(self, value, subvalue, model_instance): - allowed_values = self.enum.get_allowed_states(getattr(model_instance, self.subchoices_field_name)) + allowed_values = self.enum.get_allowed_states(getattr(model_instance, self.supchoices_field_name)) if subvalue in self.enum.categories and value not in allowed_values: raise ValidationError(ugettext('Allowed choices are {}.').format( ', '.join(('{} ({})'.format(*(self.enum.get_label(val), val)) for val in allowed_values)) @@ -194,8 +193,8 @@ def validate(self, value, model_instance): if not self.enum: return - self._raise_error_if_value_should_be_empty(value, self._get_subvalue(model_instance)) - self._raise_error_if_value_not_allowed(value, self._get_subvalue(model_instance), model_instance) + self._raise_error_if_value_should_be_empty(value, self._get_supvalue(model_instance)) + self._raise_error_if_value_not_allowed(value, self._get_supvalue(model_instance), model_instance) class EnumSequenceFieldMixin: @@ -212,11 +211,10 @@ def __init__(self, *args, **kwargs): def validate(self, value, model_instance): super().validate(value, model_instance) if self.enum: - prev_value = (not model_instance._state.adding and model_instance.initial_values[self.attname]) or None + prev_value = model_instance.initial_values[self.attname] if model_instance.is_changing else None allowed_next_values = self.enum.get_allowed_next_states(prev_value, model_instance) - - if ((self.name in model_instance.changed_fields or model_instance._state.adding) and - value not in allowed_next_values): + if ((self.name in model_instance.changed_fields or model_instance.is_adding) and + value not in allowed_next_values): raise ValidationError( ugettext('Allowed choices are {}.').format( ', '.join(('{} ({})'.format(*(self.enum.get_label(val), val)) for val in allowed_next_values)))) @@ -234,22 +232,27 @@ class PriceField(DecimalField): def __init__(self, *args, **kwargs): self.currency = kwargs.pop('currency', ugettext('CZK')) - default_kwargs = { + super().__init__(*args, **{ 'decimal_places': 2, 'max_digits': 10, - 'humanized': lambda val, inst, field: price_humanized(val, inst, currency=field.currency) - } - default_kwargs.update(kwargs) - super().__init__(*args, **default_kwargs) + 'humanized': lambda val, inst, field: price_humanized(val, inst, currency=field.currency), + **kwargs + }) def formfield(self, **kwargs): - default_kwargs = { - 'form_class': chamber_fields.PriceField, - 'currency': self.currency, - } - default_kwargs.update(kwargs) + return super(DecimalField, self).formfield( + **{ + 'form_class': chamber_fields.PriceField, + 'currency': self.currency, + **kwargs + } + ) - return super().formfield(**default_kwargs) + def deconstruct(self): + name, path, args, kwargs = super().deconstruct() + del kwargs['max_digits'] + del kwargs['decimal_places'] + return name, path, args, kwargs class PositivePriceField(PriceField): diff --git a/chamber/patch.py b/chamber/patch.py index 0c76a89..5dc6743 100644 --- a/chamber/patch.py +++ b/chamber/patch.py @@ -14,7 +14,7 @@ def __get__(self, instance=None, owner=None): class OptionsBase(type): def __new__(cls, *args, **kwargs): - new_class = super(OptionsBase, cls).__new__(cls, *args, **kwargs) + new_class = super().__new__(cls, *args, **kwargs) if new_class.model_class and new_class.meta_name: setattr(new_class.model_class, new_class.meta_name, OptionsLazy(new_class.meta_name, new_class)) return new_class diff --git a/chamber/shortcuts.py b/chamber/shortcuts.py index 9439e7c..e629dfe 100644 --- a/chamber/shortcuts.py +++ b/chamber/shortcuts.py @@ -68,7 +68,7 @@ def change(obj, **changed_fields): return obj -def change_and_save(obj, update_only_changed_fields=False, **changed_fields): +def change_and_save(obj, update_only_changed_fields=False, save_kwargs=None, **changed_fields): """ Changes a given `changed_fields` on object, saves it and returns changed object. """ @@ -78,7 +78,7 @@ def change_and_save(obj, update_only_changed_fields=False, **changed_fields): if update_only_changed_fields and not isinstance(obj, SmartModel): raise TypeError('update_only_changed_fields can be used only with SmartModel') - save_kwargs = {} + save_kwargs = save_kwargs if save_kwargs is not None else {} if update_only_changed_fields: save_kwargs['update_only_changed_fields'] = True @@ -93,13 +93,14 @@ def bulk_change(iterable, **changed_fields): return [change(obj, **changed_fields) for obj in iterable] -def bulk_change_and_save(iterable, update_only_changed_fields=False, **changed_fields): +def bulk_change_and_save(iterable, update_only_changed_fields=False, save_kwargs=None, **changed_fields): """ Changes a given `changed_fields` on each object in a given `iterable`, saves objects and returns the changed objects. """ return [ - change_and_save(obj, update_only_changed_fields=update_only_changed_fields, **changed_fields) + change_and_save(obj, update_only_changed_fields=update_only_changed_fields, save_kwargs=save_kwargs, + **changed_fields) for obj in iterable ] diff --git a/chamber/utils/datastructures.py b/chamber/utils/datastructures.py index 1698840..df5324e 100644 --- a/chamber/utils/datastructures.py +++ b/chamber/utils/datastructures.py @@ -133,6 +133,12 @@ def __init__(self, categories): def get_allowed_states(self, category): return self.categories.get(category, ()) + def get_category(self, key): + for category, items in self.categories.items(): + if key in items: + return category + return None + class SequenceChoicesEnumMixin: @@ -150,15 +156,17 @@ def __init__(self, items, initial_states=None): self.sequence_graph = {getattr(self, item[0]): item[-1] for item in items} def _get_first_choices(self, items): - return tuple(getattr(self, key) for key in self.initial_states) if self.initial_states else self.all + return tuple(getattr(self, key) for key in self.initial_states) if self.initial_states else self def get_allowed_next_states(self, state, instance): if not state: return self.first_choices else: states_or_callable = self.sequence_graph.get(state) - states = (states_or_callable(instance) if hasattr(states_or_callable, '__call__') - else list(states_or_callable)) + states = ( + states_or_callable(instance) if hasattr(states_or_callable, '__call__') + else list(states_or_callable) + ) return tuple(getattr(self, next_choice) for next_choice in states) diff --git a/chamber/utils/transaction.py b/chamber/utils/transaction.py index 30ac95b..3045c00 100644 --- a/chamber/utils/transaction.py +++ b/chamber/utils/transaction.py @@ -189,8 +189,7 @@ class InstanceOneTimeOnSuccessHandler(OneTimeOnSuccessHandler): def _get_instance(self): instance = self.kwargs_list[0]['instance'] - instance.refresh_from_db() - return instance + return instance.__class__.objects.get(pk=instance.pk) def _get_unique_id(self): instance = self.kwargs_list[0]['instance'] diff --git a/example/dj/apps/test_chamber/models.py b/example/dj/apps/test_chamber/models.py index d2d1b03..de3a0f7 100644 --- a/example/dj/apps/test_chamber/models.py +++ b/example/dj/apps/test_chamber/models.py @@ -76,7 +76,7 @@ class TestFieldsModel(chamber_models.SmartModel): decimal = chamber_fields.DecimalField(null=True, blank=True, min=3, max=10, max_digits=5, decimal_places=3) state = models.IntegerField(null=True, blank=False, choices=STATE.choices, default=STATE.OK) state_reason = chamber_models.SubchoicesPositiveIntegerField(null=True, blank=True, enum=STATE_REASON, - subchoices_field_name='state', + supchoices_field_name='state', default=STATE_REASON.SUB_OK_1) state_prev = chamber_models.PrevValuePositiveIntegerField(verbose_name=_('previous state'), null=False, blank=False, copy_field_name='state', choices=STATE.choices,