From a43a3184e0056c23220260ffa4586a3348caf0eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Thu, 6 Jul 2017 15:06:01 +0200 Subject: [PATCH] _clean_changes --- src/c3nav/editor/api.py | 5 +- src/c3nav/editor/models/changedobject.py | 49 +++++++++++-- src/c3nav/editor/models/changeset.py | 89 ++++++++++++++++++++++-- 3 files changed, 127 insertions(+), 16 deletions(-) diff --git a/src/c3nav/editor/api.py b/src/c3nav/editor/api.py index 61ebcf43..7c9888d6 100644 --- a/src/c3nav/editor/api.py +++ b/src/c3nav/editor/api.py @@ -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()]) diff --git a/src/c3nav/editor/models/changedobject.py b/src/c3nav/editor/models/changedobject.py index 352b3f73..370f82c3 100644 --- a/src/c3nav/editor/models/changedobject.py +++ b/src/c3nav/editor/models/changedobject.py @@ -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)) diff --git a/src/c3nav/editor/models/changeset.py b/src/c3nav/editor/models/changeset.py index f06045f2..c1b3e2aa 100644 --- a/src/c3nav/editor/models/changeset.py +++ b/src/c3nav/editor/models/changeset.py @@ -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()