Skip to content

Commit

Permalink
Merge pull request #64 from druids/AddUpdateOnlyChangedFieldsToTheSma…
Browse files Browse the repository at this point in the history
…rtModelSaveHelpers

Added update_only_changed_fields to the save helpers
  • Loading branch information
matllubos committed Jan 6, 2019
2 parents f66ef87 + 71b36a9 commit c9115e3
Show file tree
Hide file tree
Showing 13 changed files with 279 additions and 70 deletions.
2 changes: 1 addition & 1 deletion chamber/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def handle(self, *args, **kwargs):
class BulkImportCSVCommand(ImportCSVCommandMixin, BulkCSVImporter, BaseCommand):

def __init__(self, *args, **kwargs):
super(BulkImportCSVCommand, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
self.bar = None

def _pre_import_rows(self, row_count):
Expand Down
2 changes: 1 addition & 1 deletion chamber/exceptions/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
class PersistenceException(Exception):

def __init__(self, message=None):
super(PersistenceException, self).__init__()
super().__init__()
self.message = message

def __str__(self):
Expand Down
8 changes: 4 additions & 4 deletions chamber/forms/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ def __init__(self, *args, **kwargs):
self.step = kwargs.pop('step', 'any')
self.min = kwargs.pop('min', None)
self.max = kwargs.pop('max', None)
super(DecimalField, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)

def widget_attrs(self, widget):
attrs = super(DecimalField, self).widget_attrs(widget)
attrs = super().widget_attrs(widget)
attrs['step'] = self.step
if self.min is not None:
attrs['min'] = self.min
Expand All @@ -23,7 +23,7 @@ def widget_attrs(self, widget):
class PriceNumberInput(forms.NumberInput):

def __init__(self, currency, *args, **kwargs):
super(PriceNumberInput, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
self.placeholder = currency


Expand All @@ -35,4 +35,4 @@ def __init__(self, *args, **kwargs):
currency = kwargs.pop('currency', ugettext('CZK'))
if 'widget' not in kwargs:
kwargs['widget'] = PriceNumberInput(currency)
super(PriceField, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
2 changes: 1 addition & 1 deletion chamber/importers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def simple_count(filename, encoding):
class DummyOutputStream(io.StringIO):

def write(self, *args, **kwargs):
super(DummyOutputStream, self).write(*args)
super().write(*args)


class AbstractCSVImporter:
Expand Down
74 changes: 49 additions & 25 deletions chamber/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
import copy

import collections

from itertools import chain

from django.core.exceptions import ValidationError
from django.db import models, transaction
from django.db.models.base import ModelBase
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import force_text

from chamber.exceptions import PersistenceException
from chamber.patch import Options
Expand Down Expand Up @@ -51,6 +47,10 @@ class UnknownSingleton:
def __repr__(self):
return 'unknown'

def __bool__(self):
return False


Unknown = UnknownSingleton()


Expand Down Expand Up @@ -94,6 +94,10 @@ def initial_values(self):
def current_values(self):
raise NotImplementedError

@property
def changed_values(self):
return {k: value_change.current for k, value_change in self.diff.items()}

@property
def diff(self):
d1 = self.initial_values
Expand Down Expand Up @@ -159,7 +163,7 @@ class DynamicChangedFields(ChangedFields):

def __init__(self, instance):
super().__init__(
self._get_unknown_dict(instance) if instance._state.adding else self._get_instance_dict(instance)
self._get_unknown_dict(instance) if instance.is_adding else self._get_instance_dict(instance)
)
self.instance = instance

Expand Down Expand Up @@ -247,12 +251,12 @@ def fast_distinct(self):
"""
return self.model.objects.filter(pk__in=self.values_list('pk', flat=True))

def change_and_save(self, **chaned_fields):
def change_and_save(self, update_only_changed_fields=False, **chaned_fields):
"""
Changes a given `changed_fields` on each object in the queryset, saves objects
and returns the changed objects in the queryset.
"""
bulk_change_and_save(self, **chaned_fields)
bulk_change_and_save(self, update_only_changed_fields=update_only_changed_fields, **chaned_fields)
return self.filter()


Expand All @@ -276,10 +280,20 @@ class SmartModel(AuditModel, metaclass=SmartModelBase):
dispatchers = []

def __init__(self, *args, **kwargs):
super(SmartModel, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
self.is_adding = True
self.is_changing = False
self.changed_fields = DynamicChangedFields(self)
self.post_save = Signal(self)

@classmethod
def from_db(cls, db, field_names, values):
new = super().from_db(db, field_names, values)
new.is_adding = False
new.is_changing = True
new.changed_fields = DynamicChangedFields(new)
return new

@property
def has_changed(self):
return bool(self.changed_fields)
Expand All @@ -299,7 +313,7 @@ def full_clean(self, exclude=None, *args, **kwargs):

if errors:
raise ValidationError(errors)
super(SmartModel, self).full_clean(exclude=exclude, *args, **kwargs)
super().full_clean(exclude=exclude, *args, **kwargs)

def _clean_save(self, *args, **kwargs):
self._persistence_clean(*args, **kwargs)
Expand Down Expand Up @@ -339,9 +353,8 @@ def _pre_save(self, *args, **kwargs):
def _call_pre_save(self, *args, **kwargs):
self._pre_save(*args, **kwargs)

def _save(self, is_cleaned_pre_save=None, is_cleaned_post_save=None, force_insert=False, force_update=False,
using=None, update_fields=None, *args, **kwargs):

def _save(self, update_only_changed_fields=False, is_cleaned_pre_save=None, is_cleaned_post_save=None,
force_insert=False, force_update=False, using=None, update_fields=None, *args, **kwargs):
is_cleaned_pre_save = (
self._smart_meta.is_cleaned_pre_save if is_cleaned_pre_save is None else is_cleaned_pre_save
)
Expand All @@ -351,21 +364,26 @@ def _save(self, is_cleaned_pre_save=None, is_cleaned_post_save=None, force_inser

origin = self.__class__

change = not self._state.adding
kwargs.update(self._get_save_extra_kwargs())

self._call_pre_save(change, self.changed_fields, *args, **kwargs)
self._call_pre_save(self.is_changing, self.changed_fields, *args, **kwargs)
if is_cleaned_pre_save:
self._clean_pre_save(*args, **kwargs)
dispatcher_pre_save.send(sender=origin, instance=self, change=change,
dispatcher_pre_save.send(sender=origin, instance=self, change=self.is_changing,
changed_fields=self.changed_fields.get_static_changes(),
*args, **kwargs)
super(SmartModel, self).save(force_insert=force_insert, force_update=force_update, using=using,
if not update_fields and update_only_changed_fields:
update_fields = list(self.changed_fields.keys()) + ['changed_at']
# remove primary key from updating fields
if self._meta.pk.name in update_fields:
update_fields.remove(self._meta.pk.name)
super().save(force_insert=force_insert, force_update=force_update, using=using,
update_fields=update_fields)
self._call_post_save(change, self.changed_fields, *args, **kwargs)

self._call_post_save(self.is_changing, self.changed_fields, *args, **kwargs)
if is_cleaned_post_save:
self._clean_post_save(*args, **kwargs)
dispatcher_post_save.send(sender=origin, instance=self, change=change,
dispatcher_post_save.send(sender=origin, instance=self, change=self.is_changing,
changed_fields=self.changed_fields.get_static_changes(),
*args, **kwargs)
self.post_save.send()
Expand All @@ -376,12 +394,17 @@ def _post_save(self, *args, **kwargs):
def _call_post_save(self, *args, **kwargs):
self._post_save(*args, **kwargs)

def save(self, *args, **kwargs):
def save_simple(self, *args, **kwargs):
super().save(*args, **kwargs)

def save(self, update_only_changed_fields=False, *args, **kwargs):
if self._smart_meta.is_save_atomic:
with transaction.atomic():
self._save(*args, **kwargs)
self._save(update_only_changed_fields=update_only_changed_fields, *args, **kwargs)
else:
self._save(*args, **kwargs)
self._save(update_only_changed_fields=update_only_changed_fields, *args, **kwargs)
self.is_adding = False
self.is_changing = True
self.changed_fields = DynamicChangedFields(self)

def _pre_delete(self, *args, **kwargs):
Expand All @@ -400,7 +423,7 @@ def _delete(self, is_cleaned_pre_delete=None, is_cleaned_post_delete=None, *args
if is_cleaned_pre_delete:
self._clean_pre_delete(*args, **kwargs)

super(SmartModel, self).delete(*args, **kwargs)
super().delete(*args, **kwargs)

self._post_delete(*args, **kwargs)

Expand Down Expand Up @@ -430,13 +453,14 @@ def change(self, **changed_fields):
change(self, **changed_fields)
return self

def change_and_save(self, **changed_fields):
def change_and_save(self, update_only_changed_fields=False, **changed_fields):
"""
Changes a given `changed_fields` on this object, saves it and returns itself.
:param changed_fields: fields to change
:param update_only_changed_fields: only changed fields will be updated in the database.
:param changed_fields: fields to change.
:return: self
"""
change_and_save(self, **changed_fields)
change_and_save(self, update_only_changed_fields=update_only_changed_fields, **changed_fields)
return self

class Meta:
Expand Down
7 changes: 3 additions & 4 deletions chamber/models/dispatchers.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def _validate_init_params(self):

def __init__(self, handler, property_name, signal=None):
self.property_name = property_name
super(PropertyDispatcher, self).__init__(handler, signal)
super().__init__(handler, signal)

def _can_dispatch(self, instance, **kwargs):
return getattr(instance, self.property_name)
Expand All @@ -78,16 +78,15 @@ class StateDispatcher(BaseDispatcher):
"""

def _validate_init_params(self):
super(StateDispatcher, self)._validate_init_params()
super()._validate_init_params()
if self.field_value not in {value for value, _ in self.enum.choices}:
raise ImproperlyConfigured('Enum of FieldDispatcher does not contain {}.'.format(self.field_value))

def __init__(self, handler, enum, field, field_value, signal=None):
self.enum = enum
self.field = field
self.field_value = field_value

super(StateDispatcher, self).__init__(handler, signal=signal)
super().__init__(handler, signal=signal)

def _can_dispatch(self, instance, change, changed_fields, *args, **kwargs):
return (
Expand Down
30 changes: 15 additions & 15 deletions chamber/models/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def __init__(self, *args, **kwargs):
kwargs['validators'].append(MinValueValidator(self.min))
if self.max is not None:
kwargs['validators'].append(MaxValueValidator(self.max))
super(DecimalField, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)

def formfield(self, **kwargs):
defaults = {
Expand All @@ -48,7 +48,7 @@ def formfield(self, **kwargs):
'max': self.max,
}
defaults.update(kwargs)
return super(DecimalField, self).formfield(**defaults)
return super().formfield(**defaults)


class RestrictedFileValidator:
Expand Down Expand Up @@ -106,7 +106,7 @@ class RestrictedFileFieldMixin:
def __init__(self, *args, **kwargs):
max_upload_size = kwargs.pop('max_upload_size', config.CHAMBER_MAX_FILE_UPLOAD_SIZE) * 1024 * 1024
allowed_content_types = kwargs.pop('allowed_content_types', None)
super(RestrictedFileFieldMixin, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)
self.validators.append(RestrictedFileValidator(max_upload_size))
if allowed_content_types:
self.validators = tuple(self.validators) + (
Expand All @@ -120,7 +120,7 @@ def generate_filename(self, instance, filename):
"""
from unidecode import unidecode

return super(RestrictedFileFieldMixin, self).generate_filename(instance, unidecode(force_text(filename)))
return super().generate_filename(instance, unidecode(force_text(filename)))


class FileField(RestrictedFileFieldMixin, OriginFileField):
Expand All @@ -131,7 +131,7 @@ class ImageField(RestrictedFileFieldMixin, OriginImageField):

def __init__(self, *args, **kwargs):
allowed_content_types = kwargs.pop('allowed_content_types', config.CHAMBER_DEFAULT_IMAGE_ALLOWED_CONTENT_TYPES)
super(ImageField, self).__init__(allowed_content_types=allowed_content_types, *args, **kwargs)
super().__init__(allowed_content_types=allowed_content_types, *args, **kwargs)


def generate_random_upload_path(instance, filename):
Expand All @@ -146,7 +146,7 @@ class PrevValuePositiveIntegerField(models.PositiveIntegerField):

def __init__(self, *args, **kwargs):
self.copy_field_name = kwargs.pop('copy_field_name', None)
super(PrevValuePositiveIntegerField, self).__init__(*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:
Expand All @@ -155,7 +155,7 @@ def pre_save(self, model_instance, add):
getattr(model_instance, self.copy_field_name)
if add else model_instance.initial_values[self.copy_field_name]
)
return super(PrevValuePositiveIntegerField, self).pre_save(model_instance, add)
return super().pre_save(model_instance, add)


class SubchoicesPositiveIntegerField(models.PositiveIntegerField):
Expand All @@ -168,7 +168,7 @@ def __init__(self, *args, **kwargs):
assert self.enum is None or isinstance(self.enum, SubstatesChoicesNumEnum)
if self.enum:
kwargs['choices'] = self.enum.choices
super(SubchoicesPositiveIntegerField, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)

def _get_subvalue(self, model_instance):
return getattr(model_instance, self.subchoices_field_name)
Expand All @@ -177,7 +177,7 @@ def clean(self, value, model_instance):
if self.enum and self._get_subvalue(model_instance) not in self.enum.categories:
return None
else:
return super(SubchoicesPositiveIntegerField, self).clean(value, model_instance)
return super().clean(value, model_instance)

def _raise_error_if_value_should_be_empty(self, value, subvalue):
if self.enum and subvalue not in self.enum.categories and value is not None:
Expand Down Expand Up @@ -207,10 +207,10 @@ def __init__(self, *args, **kwargs):
assert self.enum is None or isinstance(self.enum, SequenceChoicesEnumMixin)
if self.enum:
kwargs['choices'] = self.enum.choices
super(EnumSequenceFieldMixin, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)

def validate(self, value, model_instance):
super(EnumSequenceFieldMixin, self).validate(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
allowed_next_values = self.enum.get_allowed_next_states(prev_value, model_instance)
Expand Down Expand Up @@ -240,7 +240,7 @@ def __init__(self, *args, **kwargs):
'humanized': lambda val, inst, field: price_humanized(val, inst, currency=field.currency)
}
default_kwargs.update(kwargs)
super(PriceField, self).__init__(*args, **default_kwargs)
super().__init__(*args, **default_kwargs)

def formfield(self, **kwargs):
default_kwargs = {
Expand All @@ -249,17 +249,17 @@ def formfield(self, **kwargs):
}
default_kwargs.update(kwargs)

return super(PriceField, self).formfield(**default_kwargs)
return super().formfield(**default_kwargs)


class PositivePriceField(PriceField):

def __init__(self, *args, **kwargs):
kwargs['validators'] = kwargs.get('validators', [])
kwargs['validators'].append(MinValueValidator(Decimal('0.00')))
super(PositivePriceField, self).__init__(*args, **kwargs)
super().__init__(*args, **kwargs)

def deconstruct(self):
name, path, args, kwargs = super(PositivePriceField, self).deconstruct()
name, path, args, kwargs = super().deconstruct()
del kwargs['validators']
return name, path, args, kwargs

0 comments on commit c9115e3

Please sign in to comment.