_clean_changes

This commit is contained in:
Laura Klünder 2017-07-06 15:06:01 +02:00
parent 8e2d986190
commit a43a3184e0
3 changed files with 127 additions and 16 deletions

View file

@ -184,7 +184,4 @@ class ChangeSetViewSet(ReadOnlyModelViewSet):
def changes(self, request, *args, **kwargs):
changeset = self.get_object()
changeset.fill_changes_cache(include_deleted_created=True)
return Response([
obj.serialize()
for obj in chain(*(changed_objects.values() for changed_objects in changeset.changed_objects.values()))
])
return Response([obj.serialize() for obj in changeset.iter_changed_objects()])

View file

@ -87,7 +87,7 @@ class ChangedObject(models.Model):
obj._state.adding = False
return self.changeset.wrap_instance(obj)
def add_relevant_object_pks(self, object_pks):
def add_relevant_object_pks(self, object_pks, many=True):
object_pks.setdefault(self.model_class, set()).add(self.obj_pk)
for name, value in self.updated_fields.items():
if name.startswith('title_'):
@ -96,9 +96,10 @@ class ChangedObject(models.Model):
if field.is_relation:
object_pks.setdefault(field.related_model, set()).add(value)
for name, value in chain(self._m2m_added_cache.items(), self._m2m_removed_cache.items()):
field = self.model_class._meta.get_field(name)
object_pks.setdefault(field.related_model, set()).update(value)
if many:
for name, value in chain(self._m2m_added_cache.items(), self._m2m_removed_cache.items()):
field = self.model_class._meta.get_field(name)
object_pks.setdefault(field.related_model, set()).update(value)
def update_changeset_cache(self):
if self.pk is None:
@ -156,9 +157,11 @@ class ChangedObject(models.Model):
else:
raise NotImplementedError
def clean_updated_fields(self):
def clean_updated_fields(self, objects=None):
if self.is_created:
current_obj = self.model_class()
elif objects is not None:
current_obj = objects[self.model_class][self.existing_object_pk]
else:
current_obj = self.model_class.objects.get(pk=self.existing_object_pk)
@ -182,6 +185,29 @@ class ChangedObject(models.Model):
self.updated_fields = {name: value for name, value in self.updated_fields.items() if name not in delete_fields}
return delete_fields
def handle_deleted_object_pks(self, deleted_object_pks):
if self.obj_pk in deleted_object_pks[self.model_class]:
self.delete()
return False
for name, value in self.updated_fields.items():
if name.startswith('title_'):
continue
field = self.model_class._meta.get_field(name)
if field.is_relation:
if value in deleted_object_pks[field.related_model]:
self.delete()
return False
changed = False
for name, value in chain(self._m2m_added_cache.items(), self._m2m_removed_cache.items()):
field = self.model_class._meta.get_field(name)
if deleted_object_pks[field.related_model] & value:
value.difference_update(deleted_object_pks[field.related_model])
changed = True
return changed
def save_instance(self, instance):
old_updated_fields = self.updated_fields
self.updated_fields = {}
@ -219,8 +245,17 @@ class ChangedObject(models.Model):
self.deleted = True
self.save()
def m2m_set(self, name, set_pks=None):
if not self.is_created:
def clean_m2m(self, objects):
current_obj = objects[self.model_class][self.existing_object_pk]
changed = False
for name in set(self._m2m_added_cache.keys()) | set(self._m2m_removed_cache.keys()):
changed = changed or self.m2m_set(name, obj=current_obj)
return changed
def m2m_set(self, name, set_pks=None, obj=None):
if obj is not None:
pks = set(related_obj.pk for related_obj in getattr(obj, name).all())
elif not self.is_created:
field = self.model_class._meta.get_field(name)
rel_name = field.rel.related_name
pks = set(field.related_model.objects.filter(**{rel_name+'__pk': self.obj_pk}).values_list('pk', flat=True))

View file

@ -1,3 +1,4 @@
import typing
from collections import OrderedDict
from contextlib import contextmanager
from itertools import chain
@ -5,6 +6,7 @@ from itertools import chain
from django.apps import apps
from django.conf import settings
from django.core.cache import cache
from django.core.exceptions import FieldDoesNotExist
from django.db import models, transaction
from django.urls import reverse
from django.utils.http import int_to_base36
@ -158,6 +160,9 @@ class ChangeSet(models.Model):
self.deleted_existing, self.m2m_added, self.m2m_removed) = cached_cache
return True
if self.state != 'applied':
self._clean_changes()
self.changed_objects = {}
for change in qs:
change.update_changeset_cache()
@ -167,17 +172,83 @@ class ChangeSet(models.Model):
return True
def iter_changed_objects(self) -> typing.Iterable[ChangedObject]:
return chain(*(changed_objects.values() for changed_objects in self.changed_objects.values()))
def _clean_changes(self):
print('clean_changes')
changed_objects = self.changed_objects_set.all()
with self.lock_to_edit():
# delete changed objects that refer in some way to deleted objects and clean up m2m changes
object_pks = {}
for changed_object in changed_objects:
changed_object.add_relevant_object_pks(object_pks)
to_save = set()
deleted_object_pks = {}
for model, pks in object_pks.items():
pks = set(pk for pk in pks if not is_created_pk(pk))
deleted_object_pks[model] = pks - set(model.objects.filter(pk__in=pks).values_list('pk', flat=True))
for changed_object in changed_objects:
if changed_object.handle_deleted_object_pks(deleted_object_pks):
to_save.add(changed_object)
# remove deleted objects
changed_objects = [obj for obj in changed_objects if obj.pk is not None]
# clean updated fields
objects = self.get_objects(many=False, changed_objects=changed_objects, prefetch_related=('groups', ))
for changed_object in changed_objects:
if changed_object.clean_updated_fields(objects):
to_save.add(changed_object)
# clean m2m
for changed_object in changed_objects:
if changed_object.clean_m2m(objects):
to_save.add(changed_object)
# remove duplicate slugs
slugs = set()
for changed_object in changed_objects:
if issubclass(changed_object.model_class, LocationSlug):
slug = changed_object.updated_fields.get('slug', None)
if slug is not None:
if slug in slugs:
changed_object.updated_fields.pop('slug', None)
to_save.add(changed_object)
else:
slugs.add(slug)
existing_slugs = set(LocationSlug.objects.filter(slug__in=slugs).values_list('slug', flat=True))
for changed_object in changed_objects:
if issubclass(changed_object.model_class, LocationSlug):
if changed_object.updated_fields.get('slug', None) in existing_slugs:
if issubclass(changed_object.model_class, LocationRedirect):
to_save.discard(changed_object)
changed_object.delete()
else:
changed_object.updated_fields.pop('slug', None)
to_save.add(changed_object)
for changed_object in to_save:
changed_object.save(standalone=True)
"""
Analyse Changes
"""
def get_objects(self):
if self.changed_objects is None:
raise TypeError
def get_objects(self, many=True, changed_objects=None, prefetch_related=()):
if changed_objects is None:
if self.changed_objects is None:
raise TypeError
changed_objects = self.iter_changed_objects()
# collect pks of relevant objects
object_pks = {}
for change in chain(*(objects.values() for objects in self.changed_objects.values())):
change.add_relevant_object_pks(object_pks)
for change in changed_objects:
change.add_relevant_object_pks(object_pks, many=many)
# retrieve relevant objects
objects = {}
@ -186,6 +257,14 @@ class ChangeSet(models.Model):
existing_pks = pks - created_pks
model_objects = {}
if existing_pks:
qs = model.objects.filter(pk__in=existing_pks)
for prefetch in prefetch_related:
try:
model._meta.get_field(prefetch)
except FieldDoesNotExist:
pass
else:
qs = qs.prefetch_related(prefetch)
for obj in model.objects.filter(pk__in=existing_pks):
if model == LocationSlug:
obj = obj.get_child()