_clean_changes
This commit is contained in:
parent
8e2d986190
commit
a43a3184e0
3 changed files with 127 additions and 16 deletions
|
@ -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()])
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue