Skip to content

Commit

Permalink
Merge 414c1fa into 0afc16a
Browse files Browse the repository at this point in the history
  • Loading branch information
matllubos committed Jan 15, 2019
2 parents 0afc16a + 414c1fa commit 3eb54d7
Show file tree
Hide file tree
Showing 9 changed files with 66 additions and 39 deletions.
2 changes: 0 additions & 2 deletions .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

Expand Down
2 changes: 2 additions & 0 deletions chamber/forms/fields.py
Expand Up @@ -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)
18 changes: 17 additions & 1 deletion chamber/models/__init__.py
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down
53 changes: 28 additions & 25 deletions chamber/models/fields.py
Expand Up @@ -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:
Expand Down Expand Up @@ -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)

Expand All @@ -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)
Expand All @@ -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))
Expand All @@ -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:
Expand All @@ -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))))
Expand All @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion chamber/patch.py
Expand Up @@ -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
Expand Down
9 changes: 5 additions & 4 deletions chamber/shortcuts.py
Expand Up @@ -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.
"""
Expand All @@ -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

Expand All @@ -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
]

Expand Down
14 changes: 11 additions & 3 deletions chamber/utils/datastructures.py
Expand Up @@ -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:

Expand All @@ -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)


Expand Down
3 changes: 1 addition & 2 deletions chamber/utils/transaction.py
Expand Up @@ -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']
Expand Down
2 changes: 1 addition & 1 deletion example/dj/apps/test_chamber/models.py
Expand Up @@ -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,
Expand Down

0 comments on commit 3eb54d7

Please sign in to comment.