From 9d5811bc1575f8f789f8a6f47a5d046c6627ac83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Kl=C3=BCnder?= Date: Tue, 27 Jun 2017 03:20:50 +0200 Subject: [PATCH] replace Change with ChangedObject (wrapper integration still missing) --- src/c3nav/editor/api.py | 6 +- src/c3nav/editor/forms.py | 4 +- .../migrations/0005_auto_20170627_0027.py | 62 +++ src/c3nav/editor/models/change.py | 309 --------------- src/c3nav/editor/models/changedobject.py | 157 ++++++++ src/c3nav/editor/models/changeset.py | 362 ++++-------------- .../editor/templates/editor/changeset.html | 53 ++- .../templates/editor/changeset_history.html | 13 - .../editor/fragment_change_groups.html | 51 --- src/c3nav/editor/urls.py | 2 - src/c3nav/editor/views/changes.py | 267 +++++-------- src/c3nav/editor/views/edit.py | 26 +- 12 files changed, 457 insertions(+), 855 deletions(-) create mode 100644 src/c3nav/editor/migrations/0005_auto_20170627_0027.py delete mode 100644 src/c3nav/editor/models/change.py create mode 100644 src/c3nav/editor/models/changedobject.py delete mode 100644 src/c3nav/editor/templates/editor/changeset_history.html delete mode 100644 src/c3nav/editor/templates/editor/fragment_change_groups.html diff --git a/src/c3nav/editor/api.py b/src/c3nav/editor/api.py index fa76d31c..9862462e 100644 --- a/src/c3nav/editor/api.py +++ b/src/c3nav/editor/api.py @@ -46,7 +46,7 @@ class EditorViewSet(ViewSet): return results def _get_levels_pk(self, request, level): - Level = request.changeset.wrap('Level') + Level = request.changeset.wrap_model('Level') levels_under = () levels_on_top = () lower_level = level.lower(Level).first() @@ -63,8 +63,8 @@ class EditorViewSet(ViewSet): def geometries(self, request, *args, **kwargs): request.changeset = ChangeSet.get_for_request(request) - Level = request.changeset.wrap('Level') - Space = request.changeset.wrap('Space') + Level = request.changeset.wrap_model('Level') + Space = request.changeset.wrap_model('Space') level = request.GET.get('level') space = request.GET.get('space') diff --git a/src/c3nav/editor/forms.py b/src/c3nav/editor/forms.py index ee316b60..4774164c 100644 --- a/src/c3nav/editor/forms.py +++ b/src/c3nav/editor/forms.py @@ -29,7 +29,7 @@ class MapitemFormMixin(ModelForm): self.initial['geometry'] = json.dumps(mapping(self.instance.geometry), separators=(',', ':')) if 'groups' in self.fields: - LocationGroup = self.request.changeset.wrap('LocationGroup') + LocationGroup = self.request.changeset.wrap_model('LocationGroup') self.fields['groups'].label_from_instance = lambda obj: obj.title_for_forms self.fields['groups'].queryset = LocationGroup.objects.all() @@ -71,7 +71,7 @@ class MapitemFormMixin(ModelForm): for slug in self.add_redirect_slugs: self.fields['slug'].run_validators(slug) - LocationSlug = self.request.changeset.wrap('LocationSlug') + LocationSlug = self.request.changeset.wrap_model('LocationSlug') for slug in LocationSlug.objects.filter(slug__in=self.add_redirect_slugs).values_list('slug', flat=True)[:1]: raise ValidationError( _('Can not add redirecting slug “%s”: it is already used elsewhere.') % slug diff --git a/src/c3nav/editor/migrations/0005_auto_20170627_0027.py b/src/c3nav/editor/migrations/0005_auto_20170627_0027.py new file mode 100644 index 00000000..c0833613 --- /dev/null +++ b/src/c3nav/editor/migrations/0005_auto_20170627_0027.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.2 on 2017-06-27 00:27 +from __future__ import unicode_literals + +import c3nav.mapdata.fields +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('editor', '0004_auto_20170620_0934'), + ] + + operations = [ + migrations.CreateModel( + name='ChangedObject', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True, verbose_name='created')), + ('last_update', models.DateTimeField(auto_now=True, verbose_name='last update')), + ('existing_object_pk', models.PositiveIntegerField(null=True, verbose_name='id of existing object')), + ('updated_fields', c3nav.mapdata.fields.JSONField(default={}, verbose_name='updated fields')), + ('m2m_added', c3nav.mapdata.fields.JSONField(default={}, verbose_name='added m2m values')), + ('m2m_removed', c3nav.mapdata.fields.JSONField(default={}, verbose_name='removed m2m values')), + ('deleted', models.BooleanField(default=False, verbose_name='new field value')), + ('changeset', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='changed_objects_set', to='editor.ChangeSet', verbose_name='Change Set')), + ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='changed_objects_set', to='contenttypes.ContentType')), + ], + options={ + 'verbose_name': 'Changed object', + 'verbose_name_plural': 'Changed objects', + 'ordering': ['created', 'pk'], + 'default_related_name': 'changed_objects_set', + }, + ), + migrations.RemoveField( + model_name='change', + name='author', + ), + migrations.RemoveField( + model_name='change', + name='changeset', + ), + migrations.RemoveField( + model_name='change', + name='created_object', + ), + migrations.RemoveField( + model_name='change', + name='discarded_by', + ), + migrations.DeleteModel( + name='Change', + ), + migrations.AlterUniqueTogether( + name='changedobject', + unique_together=set([('changeset', 'content_type', 'existing_object_pk')]), + ), + ] diff --git a/src/c3nav/editor/models/change.py b/src/c3nav/editor/models/change.py deleted file mode 100644 index c99266b0..00000000 --- a/src/c3nav/editor/models/change.py +++ /dev/null @@ -1,309 +0,0 @@ -import json -import typing -from collections import OrderedDict - -from django.apps import apps -from django.conf import settings -from django.core.exceptions import ObjectDoesNotExist, ValidationError -from django.db import models -from django.db.models import FieldDoesNotExist, Q -from django.utils.translation import ugettext_lazy as _ - -from c3nav.editor.utils import get_current_obj, is_created_pk -from c3nav.editor.wrappers import ModelInstanceWrapper - - -class Change(models.Model): - ACTIONS = ( - ('create', _('create object')), - ('delete', _('delete object')), - ('update', _('update attribute')), - ('restore', _('restore attribute')), - ('m2m_add', _('add many to many relation')), - ('m2m_remove', _('add many to many relation')), - ) - changeset = models.ForeignKey('editor.ChangeSet', on_delete=models.CASCADE, verbose_name=_('Change Set')) - author = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, on_delete=models.PROTECT, verbose_name=_('Author')) - created = models.DateTimeField(auto_now_add=True, verbose_name=_('created')) - action = models.CharField(max_length=16, choices=ACTIONS, verbose_name=_('action')) - discarded_by = models.OneToOneField('Change', null=True, on_delete=models.CASCADE, related_name='discards', - verbose_name=_('discarded by other change')) - model_name = models.CharField(max_length=50, verbose_name=_('model name')) - existing_object_pk = models.PositiveIntegerField(null=True, verbose_name=_('id of existing object')) - created_object = models.ForeignKey('Change', null=True, on_delete=models.CASCADE, related_name='changed_by', - verbose_name=_('changed object')) - field_name = models.CharField(max_length=50, null=True, verbose_name=_('field name')) - field_value = models.TextField(null=True, verbose_name=_('new field value')) - - class Meta: - verbose_name = _('Change') - verbose_name_plural = _('Changes') - default_related_name = 'changes' - ordering = ['created', 'pk'] - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self._set_object = None - - @property - def model_class(self) -> typing.Optional[typing.Type[models.Model]]: - if not self.model_name: - return None - return apps.get_model('mapdata', self.model_name) - - @model_class.setter - def model_class(self, value: typing.Optional[typing.Type[models.Model]]): - if value is None: - self.model_name = None - return - if not issubclass(value, models.Model): - raise ValueError('value is not a django model') - if value._meta.abstract: - raise ValueError('value is an abstract model') - if value._meta.app_label != 'mapdata': - raise ValueError('value is not a mapdata model') - self.model_name = value.__name__ - - @property - def obj_pk(self) -> typing.Union[int, str]: - if self.existing_object_pk is not None: - return self.existing_object_pk - if self.created_object_id is not None: - return 'c'+str(self.created_object_id) - if self.action == 'create': - return 'c'+str(self.pk) - raise TypeError('existing_model_pk or created_object have to be set.') - - def other_changes(self): - """ - get queryset of other active changes on the same object - """ - qs = self.changeset.changes.filter(~Q(pk=self.pk), model_name=self.model_name, discarded_by__isnull=True) - if self.existing_object_pk is not None: - return qs.filter(existing_object_pk=self.existing_object_pk) - if self.action == 'create': - return qs.filter(created_object_id=self.pk) - return qs.filter(Q(pk=self.created_object_id) | Q(created_object_id=self.created_object_id)) - - @property - def obj(self) -> ModelInstanceWrapper: - if self._set_object is not None: - return self._set_object - - if self.existing_object_pk is not None: - if self.created_object is not None: - raise TypeError('existing_object_pk and created_object can not both be set.') - self._set_object = self.changeset.wrap(self.model_class.objects.get(pk=self.existing_object_pk)) - # noinspection PyTypeChecker - return self._set_object - elif self.created_object is not None: - if self.created_object.model_class != self.model_class: - raise TypeError('created_object model and change model do not match.') - if self.created_object.changeset_id != self.changeset_id: - raise TypeError('created_object belongs to a different changeset.') - return self.changeset.get_created_object(self.model_class, self.created_object_id) - raise TypeError('existing_model_pk or created_object have to be set.') - - @obj.setter - def obj(self, value: typing.Union[models.Model, ModelInstanceWrapper]): - if not isinstance(value, ModelInstanceWrapper): - value = self.changeset.wrap(value) - - if is_created_pk(value.pk): - if value._changeset.id != self.changeset.pk: - raise ValueError('value is a Change instance but belongs to a different changeset.') - self.model_class = type(value._obj) - self.created_object = Change.objects.get(pk=value.pk[1:]) - self.created_object_id = int(value.pk[1:]) - self.existing_object_pk = None - self._set_object = value - return - - model_class_before = self.model_class - self.model_class = type(value._obj) if isinstance(value, ModelInstanceWrapper) else type(value) - if value.pk is None: - self.model_class = model_class_before - raise ValueError('object is not saved yet and cannot be referenced') - self.existing_object_pk = value.pk - self.created_object = None - self._set_object = value - - @property - def field(self): - return self.model_class._meta.get_field(self.field_name) - - def check_apply_problem(self): - if self.discarded_by_id is not None or self.action in ('create', 'restore'): - return None - - try: - model = self.model_class - except LookupError: - return _('Type %(model_name)s does not exist any more.' % {'model_name': self.model_name}) - - try: - obj = get_current_obj(model, self.obj_pk) - except model.DoesNotExist: - return _('This %(model_title)s does not exist any more.' % {'model_title': model._meta.verbose_name}) - - if self.action == 'delete': - return None - - if self.action == 'update' and self.field_name.startswith('title_'): - return None - - try: - field = self.field - except FieldDoesNotExist: - return _('This field does not exist any more.') - value = json.loads(self.field_value) - - if self.action == 'update': - if field.many_to_many: - return _('This is a m2m field, so it can\'t be updated.') - if field.many_to_one: - if value is None and not field.null: - return _('This field has to be set.') - try: - self.changeset.wrap(field.related_model).objects.get(pk=value) - except ObjectDoesNotExist: - return (_('Referenced %(model_title)s does not exist any more.') % - {'model_title': field.related_model._meta.verbose_name}) - return None - if field.is_relation: - raise NotImplementedError - try: - field.clean(value, obj) - except ValidationError as e: - return str(e) - return None - - if self.action in ('m2m_add', 'm2m_remove'): - if not field.many_to_many: - return _('This is not a m2m field, so it can\'t be trated as such.') - try: - self.changeset.wrap(field.related_model).objects.get(pk=value) - except ObjectDoesNotExist: - return _('Referenced object does not exist any more.') - return None - - @property - def can_restore(self): - if self.discarded_by_id is not None: - return False - - if self.action == 'delete': - return not is_created_pk(self.obj_pk) - - if self.action == 'create': - return False - - try: - obj = get_current_obj(self.model_class, self.obj_pk) - field = self.field - except (LookupError, ObjectDoesNotExist, FieldDoesNotExist): - return True - - if field.many_to_one: - return field.null - - if field.name == 'geometry': - return False - - try: - field.clean(field.get_prep_value(getattr(obj, field.name)), obj) - except ValidationError: - return False - - return True - - def check_has_no_effect(self): - if self.discarded_by_id is not None or self.action in ('create', 'restore'): - return False - - if self.action == 'delete': - if is_created_pk(self.obj_pk): - return False - - try: - current_obj = get_current_obj(self.model_class, self.obj_pk) - except (LookupError, ObjectDoesNotExist): - return not is_created_pk(self.obj_pk) - - if self.action == 'delete': - return False - - if self.field_name.startswith('title_'): - return self.field_value == current_obj.titles.get(self.field_name[6:], '') - - try: - field = self.field - except FieldDoesNotExist: - return True - - if self.action == 'update': - if field.many_to_one: - return self.field_value == getattr(current_obj, field.attname) - if field.is_relation: - raise NotImplementedError - return self.field_value == field.get_prep_value(getattr(current_obj, field.name)) - - if self.action == 'm2m_add': - if is_created_pk(self.field_value): - return False - return getattr(current_obj, field.name).filter(pk=self.field_value).exists() - - if self.action == 'm2m_remove': - if is_created_pk(self.field_value): - return True - return not getattr(current_obj, field.name).filter(pk=self.field_value).exists() - - def restore(self, author): - if not self.can_restore: - return - - def save(self, *args, **kwargs): - if self.pk is not None: - raise TypeError('change objects can not be edited (use update to set discarded_by)') - if self.changeset.proposed is not None or self.changeset.applied is not None: - raise TypeError('can not add change object to uneditable changeset.') - super().save(*args, **kwargs) - - def delete(self, *args, **kwargs): - raise TypeError('change objects can not be deleted directly.') - - def __repr__(self): - result = ' typing.Optional[typing.Type[models.Model]]: + return self.content_type.model_class() + + @model_class.setter + def model_class(self, value: typing.Optional[typing.Type[models.Model]]): + self.content_type = ContentType.objects.get_for_model(value) + + @property + def obj_pk(self) -> typing.Union[int, str]: + if not self.is_created: + return self.existing_object_pk + return 'c'+str(self.pk) + + @property + def obj(self) -> ModelInstanceWrapper: + return self.get_obj(get_foreign_objects=True) + + @property + def is_created(self): + return self.existing_object_pk is None + + def get_obj(self, get_foreign_objects=False) -> ModelInstanceWrapper: + model = self.model_class + + if not self.is_created: + if self._set_object is None: + self._set_object = self.changeset.wrap_instance(model.objects.get(pk=self.existing_object_pk)) + + # noinspection PyTypeChecker + return self._set_object + + pk = self.obj_pk + + obj = model() + obj.pk = pk + if hasattr(model._meta.pk, 'related_model'): + setattr(obj, model._meta.pk.related_model._meta.pk.attname, pk) + obj._state.adding = False + + for name, value in self.updated_fields.items(): + if name.startswith('title_'): + if value: + obj.titles[name[6:]] = value + continue + + field = model._meta.get_field(name) + + if field.many_to_many: + continue + + if field.many_to_one: + setattr(obj, field.attname, value) + if is_created_pk(value): + setattr(obj, field.get_cache_name(), self.changeset.get_created_object(field.related_model, value)) + elif get_foreign_objects: + related_obj = self.changeset.wrap_model(field.related_model).objects.get(pk=value) + setattr(obj, field.get_cache_name(), related_obj) + continue + + setattr(obj, name, field.to_python(value)) + return self.changeset.wrap_instance(obj) + + def add_relevant_object_pks(self, object_pks): + object_pks.setdefault(self.model_class, set()).add(self.obj_pk) + 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: + 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) + + def update_changeset_cache(self): + if self.pk is None: + return + + model = self.model_class + pk = self.obj_pk + + self.changeset.changed_objects.setdefault(model, {})[pk] = self + + if self.is_created: + if not self.deleted: + self.changeset.created_objects.setdefault(model, {})[pk] = self.updated_fields + self.changeset.ever_created_objects.setdefault(model, {})[pk] = self.updated_fields + else: + if not self.deleted: + self.changeset.updated_existing.setdefault(model, {})[pk] = self.updated_fields + self.changeset.deleted_existing.setdefault(model, set()).discard(pk) + else: + self.changeset.updated_existing.setdefault(model, {}).pop(pk, None) + self.changeset.deleted_existing.setdefault(model, set()).add(pk) + + if not self.deleted: + self.changeset.m2m_added.get(model, {})[pk] = self._m2m_added_cache + self.changeset.m2m_removed.get(model, {})[pk] = self._m2m_removed_cache + else: + self.changeset.m2m_added.get(model, {}).pop(pk, None) + self.changeset.m2m_removed.get(model, {}).pop(pk, None) + + def save(self, *args, **kwargs): + self.m2m_added = {name: tuple(values) for name, values in self._m2m_added_cache} + self.m2m_removed = {name: tuple(values) for name, values in self._m2m_added_cache} + if self.changeset.proposed is not None or self.changeset.applied is not None: + raise TypeError('can not add change object to uneditable changeset.') + super().save(*args, **kwargs) + if not self.changeset.fill_changes_cache(): + self.update_changeset_cache() + + def delete(self, *args, **kwargs): + raise TypeError('change objects can not be deleted directly.') + + def __repr__(self): + return '' % (str(self.pk), str(self.changeset_id)) diff --git a/src/c3nav/editor/models/changeset.py b/src/c3nav/editor/models/changeset.py index 0b14f3bb..63950f75 100644 --- a/src/c3nav/editor/models/changeset.py +++ b/src/c3nav/editor/models/changeset.py @@ -1,18 +1,19 @@ -import json from collections import OrderedDict +from itertools import chain +from operator import attrgetter from django.apps import apps from django.conf import settings -from django.core.serializers.json import DjangoJSONEncoder -from django.db import models, transaction -from django.db.models import Q +from django.contrib.contenttypes.models import ContentType +from django.db import models +from django.db.models import Max, Q from django.urls import reverse from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ungettext_lazy -from c3nav.editor.models.change import Change -from c3nav.editor.utils import get_current_obj, get_field_value, is_created_pk -from c3nav.editor.wrappers import ModelInstanceWrapper, ModelWrapper +from c3nav.editor.models.changedobject import ChangedObject +from c3nav.editor.utils import is_created_pk +from c3nav.editor.wrappers import ModelWrapper from c3nav.mapdata.models import LocationSlug from c3nav.mapdata.models.locations import LocationRedirect from c3nav.mapdata.utils.models import get_submodels @@ -35,7 +36,8 @@ class ChangeSet(models.Model): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.default_author = None - self.changes_qs = None + self.changed_objects = None + self.ever_created_objects = {} self.created_objects = {} self.updated_existing = {} @@ -111,129 +113,53 @@ class ChangeSet(models.Model): """ Wrap Objects """ - def wrap(self, obj, author=None): - """ - Wrap the given object in a changeset wrapper. - :param obj: A model, a model instance or the name of a model as a string. - :param author: Author to which ne changes will be assigned. This changesets default author (if set) if None. - """ - self.parse_changes() - if author is None: - author = self.default_author - if author is not None and not author.is_authenticated: - author = None - if isinstance(obj, str): - return ModelWrapper(self, apps.get_model('mapdata', obj), author) - if isinstance(obj, type) and issubclass(obj, models.Model): - return ModelWrapper(self, obj, author) - if isinstance(obj, models.Model): - return ModelWrapper(self, type(obj), author).create_wrapped_model_class()(self, obj, author) - raise ValueError + def wrap_model(self, model): + if isinstance(model, str): + model = apps.get_model('mapdata', model) + assert isinstance(model, type) and issubclass(model, models.Model) + return ModelWrapper(self, model) - """ - Parse Changes - """ - def relevant_changes(self): - """ - Get all changes of this queryset that have not been discarded and do not restore original data. - You should not call this, but instead call parse_changes(), it will store the result in self.changes_qs. - """ - qs = self.changes.filter(discarded_by__isnull=True).exclude(action='restore') - qs = qs.exclude(action='delete', created_object_id__isnull=False) - return qs + def wrap_instance(self, instance): + assert isinstance(instance, models.Model) + return self.wrap_model(instance.__class__).create_wrapped_model_class()(self, instance) - def parse_changes(self, get_history=False): - """ - Parse changes of this changeset so they can be reflected when querying data. - Only executable once, if changes are added later they are automatically parsed. - This method gets automatically called when parsed changes are needed or when adding a new change. - The queryset used/created by this method can be found in changes_qs afterwards. - :param get_history: Whether to get all changes (True) or only relevant ones (False) - """ - if self.pk is None or self.changes_qs is not None: - return + def relevant_changed_objects(self): + return self.changed_objects_set.exclude(existing_object_pk__isnull=True, deleted=True) - if get_history: - self.changes_qs = self.changes.all() + def fill_changes_cache(self, include_deleted_created=False): + """ + Get all changed objects and fill this ChangeSet's changes cache. + Only executable once, if something is changed later the cache will be automatically updated. + This method gets called automatically when the cache is needed. + Only call it if you need to set include_deleted_created to True. + :param include_deleted_created: Fetch created objects that were deleted. + :rtype: True if the method was executed, else False + """ + if self.pk is None or self.changed_objects is not None: + return False + + if include_deleted_created: + qs = self.changed_objects_set.all() else: - self.changes_qs = self.relevant_changes() + qs = self.relevant_changed_objects() - # noinspection PyTypeChecker - for change in self.changes_qs: - self._parse_change(change) + self.changed_objects = {} + for change in qs: + change.update_changeset_cache() - def _parse_change(self, change): - self._last_change_pk = change.pk - - model = change.model_class - pk = change.obj_pk - if change.action == 'create': - new = {} - self.created_objects.setdefault(model, {})[pk] = new - self.ever_created_objects.setdefault(model, {})[pk] = new - return - elif change.action == 'delete': - if not is_created_pk(pk): - self.deleted_existing.setdefault(model, set()).add(pk) - else: - self.created_objects[model].pop(pk) - self.m2m_added.get(model, {}).pop(pk, None) - self.m2m_removed.get(model, {}).pop(pk, None) - return - - name = change.field_name - - if change.action == 'restore' and change.field_value is None: - if is_created_pk(pk): - self.created_objects[model][pk].pop(name, None) - else: - self.updated_existing.setdefault(model, {}).setdefault(pk, {}).pop(name, None) - return - - value = json.loads(change.field_value) - if change.action == 'update': - if is_created_pk(pk): - self.created_objects[model][pk][name] = value - else: - self.updated_existing.setdefault(model, {}).setdefault(pk, {})[name] = value - - if change.action == 'restore': - self.m2m_removed.get(model, {}).get(pk, {}).get(name, set()).discard(value) - self.m2m_added.get(model, {}).get(pk, {}).get(name, set()).discard(value) - elif change.action == 'm2m_add': - m2m_removed = self.m2m_removed.get(model, {}).get(pk, {}).get(name, ()) - if value in m2m_removed: - m2m_removed.remove(value) - self.m2m_added.setdefault(model, {}).setdefault(pk, {}).setdefault(name, set()).add(value) - elif change.action == 'm2m_remove': - m2m_added = self.m2m_added.get(model, {}).get(pk, {}).get(name, ()) - if value in m2m_added: - m2m_added.discard(value) - self.m2m_removed.setdefault(model, {}).setdefault(pk, {}).setdefault(name, set()).add(value) + return True """ Analyse Changes """ def get_objects(self): - if self.changes_qs is None: + if self.changed_objects is None: raise TypeError # collect pks of relevant objects object_pks = {} - for change in self.changes_qs: - object_pks.setdefault(change.model_class, set()).add(change.obj_pk) - model = None - if change.action == 'update': - if change.model_class == LocationRedirect: - if change.field_name == 'target': - object_pks.setdefault(LocationSlug, set()).add(json.loads(change.field_value)) - continue - elif not change.field_name.startswith('title_'): - model = getattr(change.field, 'related_model', None) - if change.action in ('m2m_add', 'm2m_remove'): - model = change.field.related_model - if model is not None: - object_pks.setdefault(model, set()).add(json.loads(change.field_value)) + for change in chain(self.changed_objects.values()): + change.add_relevant_object_pks(object_pks) # retrieve relevant objects objects = {} @@ -266,58 +192,38 @@ class ChangeSet(models.Model): r = tuple((pk, values[name]) for pk, values in self.updated_existing.get(model, {}).items() if name in values) return r - def get_created_object(self, model, pk, author=None, get_foreign_objects=False, allow_deleted=False): + def get_changed_object(self, model, pk=None): + self.fill_changes_cache() + + objects = tuple(obj for obj in ((submodel, self.changed_objects.get(submodel, {}).get(pk, None)) + for submodel in get_submodels(model)) if obj[1] is not None) + if len(objects) > 1: + raise model.MultipleObjectsReturned + if objects: + return objects[0] + + if is_created_pk(pk): + raise model.DoesNotExist + + return ChangedObject(changeset=self, model_class=model, existing_obj_pk=pk) + + def get_created_object(self, model, pk, get_foreign_objects=False, allow_deleted=False): """ Gets a created model instance. :param model: model class :param pk: primary key - :param author: overwrite default author for changes made to that model :param get_foreign_objects: whether to fetch foreign objects and not just set their id to field.attname :param allow_deleted: return created objects that have already been deleted (needs get_history=True) :return: a wrapped model instance """ - self.parse_changes() + self.fill_changes_cache() if issubclass(model, ModelWrapper): model = model._obj - objects = self.ever_created_objects if allow_deleted else self.created_objects - - objects = tuple(obj for obj in ((submodel, objects.get(submodel, {}).get(pk, None)) - for submodel in get_submodels(model)) if obj[1] is not None) - if not objects: + object = self.get_changed_object(model, pk) + if object.deleted and not allow_deleted: raise model.DoesNotExist - if len(objects) > 1: - raise model.MultipleObjectsReturned - - model, data = objects[0] - - obj = model() - obj.pk = pk - if hasattr(model._meta.pk, 'related_model'): - setattr(obj, model._meta.pk.related_model._meta.pk.attname, pk) - obj._state.adding = False - - for name, value in data.items(): - if name.startswith('title_'): - if value: - obj.titles[name[6:]] = value - continue - - field = model._meta.get_field(name) - - if field.many_to_many: - continue - - if field.many_to_one: - setattr(obj, field.attname, value) - if is_created_pk(value): - setattr(obj, field.get_cache_name(), self.get_created_object(field.related_model, value)) - elif get_foreign_objects: - setattr(obj, field.get_cache_name(), self.wrap(field.related_model.objects.get(pk=value))) - continue - - setattr(obj, name, field.to_python(value)) - return self.wrap(obj, author=author) + return object.get_obj(get_foreign_objects=get_foreign_objects) def get_created_pks(self, model) -> set: """ @@ -327,144 +233,29 @@ class ChangeSet(models.Model): model = model._obj return set(self.created_objects.get(model, {}).keys()) - """ - add changes - """ - def _new_change(self, author, **kwargs): - if self.pk is None: - if self.session_id is None: - try: - # noinspection PyUnresolvedReferences - session = self.request.session - if session.session_key is None: - session.save() - self.session_id = session.session_key - except AttributeError: - pass # ok, so lets keep it this way - self.save() - self.parse_changes() - change = Change(changeset=self) - change.changeset_id = self.pk - author = self.default_author if author is None else author - if author is not None and author.is_authenticated: - change.author = author - for name, value in kwargs.items(): - setattr(change, name, value) - change.save() - self._parse_change(change) - return change - - def add_create(self, obj, author=None): - """ - Creates a new object in this changeset. Called when a new ModelInstanceWrapper is saved. - """ - change = self._new_change(author=author, action='create', model_class=type(obj._obj)) - obj.pk = 'c%d' % change.pk - - def _add_value(self, action, obj, name, value, author=None): - return self._new_change(author=author, action=action, obj=obj, field_name=name, - field_value=json.dumps(value, ensure_ascii=False, cls=DjangoJSONEncoder)) - - def add_restore(self, obj, name, value=None, author=None): - """ - Restore a models field value (= remove it from the changeset). - """ - return self._new_change(author=author, action='restore', obj=obj, field_name=name, field_value=value) - - def add_update(self, obj, name, value, author=None): - """ - Update a models field value. Called when a ModelInstanceWrapper is saved. - """ - if isinstance(obj, ModelInstanceWrapper): - obj = obj._obj - model = type(obj) - with transaction.atomic(): - current_obj = get_current_obj(model, obj.pk) - current_value = get_field_value(current_obj, name) - - if current_value != value: - change = self._add_value('update', obj, name, value, author) - else: - change = self.add_restore(obj, name, author) - change.other_changes().filter(field_name=name).update(discarded_by=change) - return change - - def add_m2m_add(self, obj, name, value, author=None): - """ - Add an object to a m2m relation. Called by ManyRelatedManagerWrapper. - """ - if isinstance(obj, ModelInstanceWrapper): - obj = obj._obj - with transaction.atomic(): - if is_created_pk(obj.pk) or is_created_pk(value) or not getattr(obj, name).filter(pk=value).exists(): - change = self._add_value('m2m_add', obj, name, value, author) - else: - change = self.add_restore(obj, name, value, author) - change.other_changes().filter(field_name=name, field_value=change.field_value).update(discarded_by=change) - return change - - def add_m2m_remove(self, obj, name, value, author=None): - """ - Remove an object from a m2m reltation. Called by ManyRelatedManagerWrapper. - """ - if isinstance(obj, ModelInstanceWrapper): - obj = obj._obj - with transaction.atomic(): - if is_created_pk(obj.pk) or is_created_pk(value) or not getattr(obj, name).filter(pk=value).exists(): - change = self.add_restore(obj, name, value, author) - else: - change = self._add_value('m2m_remove', obj, name, value, author) - change.other_changes().filter(field_name=name, field_value=change.field_value).update(discarded_by=change) - return change - - def add_delete(self, obj, author=None): - """ - Delete an object. Called by ModelInstanceWrapper.delete(). - """ - with transaction.atomic(): - change = self._new_change(author=author, action='delete', obj=obj) - change.other_changes().update(discarded_by=change) - return change - """ Methods for display """ @property - def changes_count(self): + def changed_objects_count(self): """ - Get the number of relevant changes. Does not need a query if changes are already parsed. + Get the number of changed objects. Does not need a query if cache is already filled. """ - if self.changes_qs is None: - return self.relevant_changes().exclude(model_name='LocationRedirect', action='update').count() + if self.changed_objects is None: + location_redirect_type = ContentType.objects.get_for_model(LocationRedirect) + return self.relevant_changed_objects().exclude(content_type=location_redirect_type).count() - result = 0 - - for model, objects in self.created_objects.items(): - result += len(objects) - if model == LocationRedirect: - continue - result += sum(len(values) for values in objects.values()) - - for objects in self.updated_existing.values(): - result += sum(len(values) for values in objects.values()) - - result += sum(len(objs) for objs in self.deleted_existing.values()) - - for m2m in self.m2m_added, self.m2m_removed: - for objects in m2m.values(): - for obj in objects.values(): - result += sum(len(values) for values in obj.values()) - - return result + return sum((len(objects) for model, objects in self.changed_objects.items() if model != LocationRedirect)) @property def count_display(self): """ - Get “%d changes” display text. + Get “%d changed objects” display text. """ if self.pk is None: - return _('No changes') - return ungettext_lazy('%(num)d change', '%(num)d changes', 'num') % {'num': self.changes_count} + return _('No changed objects') + return (ungettext_lazy('%(num)d changed object', '%(num)d changed objects', 'num') % + {'num': self.changed_objects_count}) @property def title(self): @@ -472,11 +263,18 @@ class ChangeSet(models.Model): return '' return _('Changeset #%d') % self.pk + @property + def last_update(self): + if self.changed_objects is None: + return self.relevant_changed_objects().aggregate(Max('last_update'))['last_update__max'] + + return max(chain(*self.changed_objects.values()), key=attrgetter('last_update')) + @property def cache_key(self): if self.pk is None: return None - return str(self.pk)+'-'+str(self._last_change_pk) + return str(self.pk)+'-'+str(self.last_update) def get_absolute_url(self): if self.pk is None: diff --git a/src/c3nav/editor/templates/editor/changeset.html b/src/c3nav/editor/templates/editor/changeset.html index 6a187d82..7457bdb8 100644 --- a/src/c3nav/editor/templates/editor/changeset.html +++ b/src/c3nav/editor/templates/editor/changeset.html @@ -12,13 +12,58 @@ {% trans 'Edit' %} - - {% trans 'View History' %} -

{% bootstrap_messages %} -{% include 'editor/fragment_change_groups.html' %} +{% for group in grouped_changes %} + + + + + + + + {% for change in group.changes %} + + + + {% if change.value %}{% else %}{% endif %}{{ change.title }}{% if change.value %}:{% else %}{% endif %} + {{ change.value }} + + + + {% endfor %} + +
+ {% if group.edit_url %} + + {% trans 'Edit' %} + + {% endif %} + {% if group.obj_title %} + {{ group.obj_title }} ({{ group.obj }}) + {% else %} + {{ group.obj }} + {% endif %} +
+ {% if change.apply_problem %} + + {% elif change.has_no_effect %} + + {% endif %} + {% if change.author and change.author != changeset.author %} + + {% endif %} + {% if change.can_restore %} + + {% endif %} + {% if change.created %} + + {% endif %} +
+{% endfor %} {% buttons %} diff --git a/src/c3nav/editor/templates/editor/changeset_history.html b/src/c3nav/editor/templates/editor/changeset_history.html deleted file mode 100644 index b37685c3..00000000 --- a/src/c3nav/editor/templates/editor/changeset_history.html +++ /dev/null @@ -1,13 +0,0 @@ -{% load bootstrap3 %} -{% load i18n %} - -{% include 'editor/fragment_modal_close.html' %} -

{{ changeset.title }}

-

{% trans 'Changeset History' %}

-
-{% csrf_token %} -

« {% trans 'back to changeset' %}

-{% bootstrap_messages %} - -{% include 'editor/fragment_change_groups.html' %} -
diff --git a/src/c3nav/editor/templates/editor/fragment_change_groups.html b/src/c3nav/editor/templates/editor/fragment_change_groups.html deleted file mode 100644 index ea73716e..00000000 --- a/src/c3nav/editor/templates/editor/fragment_change_groups.html +++ /dev/null @@ -1,51 +0,0 @@ -{% load i18n %} - -{% for group in grouped_changes %} - - - - - - - - {% for change in group.changes %} - - - - {% if change.value %}{% else %}{% endif %}{{ change.title }}{% if change.value %}:{% else %}{% endif %} - {{ change.value }} - - - - {% endfor %} - -
- {% if group.edit_url %} - - {% trans 'Edit' %} - - {% endif %} - {% if group.obj_title %} - {{ group.obj_title }} ({{ group.obj }}) - {% else %} - {{ group.obj }} - {% endif %} -
- {% if change.apply_problem %} - - {% elif change.has_no_effect %} - - {% endif %} - {% if change.author and change.author != changeset.author %} - - {% endif %} - {% if change.can_restore %} - - {% endif %} - {% if change.created %} - - {% endif %} -
-{% endfor %} diff --git a/src/c3nav/editor/urls.py b/src/c3nav/editor/urls.py index a774a94f..2fe8fe7b 100644 --- a/src/c3nav/editor/urls.py +++ b/src/c3nav/editor/urls.py @@ -37,8 +37,6 @@ urlpatterns = [ url(r'^levels/(?Pc?[0-9]+)/levels_on_top/create$', edit, name='editor.levels_on_top.create', kwargs={'model': 'Level'}), url(r'^changesets/(?P[0-9]+)/$', changeset_detail, name='editor.changesets.detail'), - url(r'^changesets/(?P[0-9]+)/history$', changeset_detail, name='editor.changesets.history', - kwargs={'show_history': True}), url(r'^login$', login_view, name='editor.login'), url(r'^logout$', logout_view, name='editor.logout'), ] diff --git a/src/c3nav/editor/views/changes.py b/src/c3nav/editor/views/changes.py index b9b3a59b..b6035531 100644 --- a/src/c3nav/editor/views/changes.py +++ b/src/c3nav/editor/views/changes.py @@ -1,4 +1,3 @@ -import json from operator import itemgetter from django.conf import settings @@ -11,11 +10,10 @@ from django.utils.translation import ugettext_lazy as _ from c3nav.editor.models import ChangeSet from c3nav.editor.utils import is_created_pk from c3nav.editor.views.base import sidebar_view -from c3nav.mapdata.models.locations import LocationRedirect, LocationSlug @sidebar_view -def changeset_detail(request, pk, show_history=False): +def changeset_detail(request, pk): can_edit = True changeset = request.changeset if str(pk) != str(request.changeset.pk): @@ -32,7 +30,7 @@ def changeset_detail(request, pk, show_history=False): change.restore(request.user if request.user.is_authenticated else None) messages.success(request, _('Original state has been restored!')) - if not show_history and request.POST.get('delete') == '1': + elif request.POST.get('delete') == '1': if request.POST.get('delete_confirm') == '1': changeset.delete() return redirect(reverse('editor.index')) @@ -42,48 +40,19 @@ def changeset_detail(request, pk, show_history=False): 'obj_title': changeset.title, }) - ctx = group_changes(changeset, can_edit=can_edit, show_history=show_history) - - if show_history: - return render(request, 'editor/changeset_history.html', ctx) - - return render(request, 'editor/changeset.html', ctx) - - -def group_changes(changeset, can_edit=False, show_history=False): - changeset.parse_changes(get_history=show_history) + changeset.fill_changes_cache(include_deleted_created=True) objects = changeset.get_objects() - if show_history: - grouped_changes = [] - for obj in objects: - if is_created_pk(obj.pk): - obj.titles = {} - grouped_changes = [] if show_history else {} - changes = [] - last_obj = None - for change in changeset.changes_qs: - pk = change.obj_pk - obj = objects[change.model_class][pk] - if change.model_class == LocationRedirect: - if change.action not in ('create', 'delete'): - continue - change.action = 'm2m_add' if change.action == 'create' else 'm2m_remove' - change.field_name = 'redirects' - change.field_value = obj.slug - pk = obj.target_id - obj = objects[LocationSlug][pk] + changed_objects_data = [] + + for model, changed_objects in changeset.changed_objects.items(): + for changed_object in changed_objects: + pk = changed_object.obj_pk + obj = objects[model][pk] - if obj != last_obj and not show_history and pk in grouped_changes: - # noinspection PyTypeChecker - changes = grouped_changes[pk]['changes'] - elif obj != last_obj: - changes = [] obj_desc = _('%(model)s #%(id)s') % {'model': obj.__class__._meta.verbose_name, 'id': pk} if is_created_pk(pk): - if show_history: - obj_desc = _('%s (created)') % obj_desc obj_still_exists = pk in changeset.created_objects[obj.__class__] else: obj_still_exists = pk not in changeset.deleted_existing.get(obj.__class__, ()) @@ -98,166 +67,112 @@ def group_changes(changeset, can_edit=False, show_history=False): edit_url = reverse('editor.' + obj.__class__._meta.default_related_name + '.edit', kwargs=reverse_kwargs) - change_group = { + changes = [] + changed_object_data = { 'model': obj.__class__, 'model_title': obj.__class__._meta.verbose_name, 'obj': obj_desc, 'obj_title': obj.title if obj.titles else None, 'changes': changes, 'edit_url': edit_url, + 'order': changed_object.created, } - if show_history: - grouped_changes.append(change_group) - else: - change_group['order'] = (0, int(pk[1:])) if is_created_pk(pk) else (1, int(pk)) - grouped_changes[pk] = change_group + changed_objects_data.append(changed_object_data) - last_obj = obj + form_fields = changeset.wrap(type(obj)).EditorForm._meta.fields - form = changeset.wrap(type(obj)).EditorForm - - change_data = { - 'pk': change.pk, - 'author': change.author, - 'discarded': change.discarded_by_id is not None, - 'apply_problem': change.check_apply_problem(), - 'has_no_effect': change.check_has_no_effect(), - } - if show_history: - change_data.update({ - 'created': _('created at %(datetime)s') % {'datetime': date_format(change.created, 'DATETIME_FORMAT')}, - }) - if not show_history or change.action == 'delete' and can_edit: - change_data.update({ - 'can_restore': change.can_restore, - }) - changes.append(change_data) - - if change.action == 'create': - change_data.update({ - 'icon': 'plus', - 'class': 'success', - 'title': _('created'), - 'order': (0, ), - }) - elif change.action == 'delete': - change_data.update({ - 'icon': 'minus', - 'class': 'danger', - 'title': _('deleted'), - 'order': (9, ), - }) - elif change.action == 'update': - change_data.update({ - 'icon': 'option-vertical', - 'class': 'muted', - }) - if change.field_name == 'geometry': - change_data.update({ - 'icon': 'map-marker', - 'class': 'info', - 'title': _('edited geometry'), - 'order': (8, ), + if changed_object.is_created: + changes.append({ + 'icon': 'plus', + 'class': 'success', + 'title': _('created'), }) - else: - if change.field_name.startswith('title_'): - lang = change.field_name[6:] - field_title = _('Title (%(lang)s)') % {'lang': dict(settings.LANGUAGES).get(lang, lang)} - field_value = str(json.loads(change.field_value)) - if field_value: - obj.titles[lang] = field_value - else: - obj.titles.pop(lang, None) + + update_changes = [] + + for name, value in changed_object.updated_fields: + change_data = { + 'icon': 'option-vertical', + 'class': 'muted', + } + if name == 'geometry': change_data.update({ - 'order': (4, tuple(code for code, title in settings.LANGUAGES).index(lang)), + 'icon': 'map-marker', + 'class': 'info', + 'title': _('edited geometry'), + 'order': (8,), }) else: - field = change.field - field_title = field.verbose_name - field_value = field.to_python(json.loads(change.field_value)) - if field.related_model is not None: - field_value = objects[field.related_model][field_value].title - order = 5 - if change.field_name == 'slug': - order = 1 - if change.field_name not in form._meta.fields: - order = 0 - change_data.update({ - 'order': (order, form._meta.fields.index(change.field_name) if order else 1), - }) - if not field_value: - change_data.update({ - 'title': _('remove %(field_title)s') % {'field_title': field_title}, - }) - else: - change_data.update({ - 'title': field_title, - 'value': field_value, - }) - elif change.action == 'restore': - change_data.update({ - 'icon': 'share-alt', - 'class': 'muted', - }) - if change.field_name == 'geometry': - change_data.update({ - 'icon': 'map-marker', - 'title': _('reverted geometry'), - }) - else: - if change.field_name.startswith('title_'): - lang = change.field_name[6:] - field_title = _('Title (%(lang)s)') % {'lang': dict(settings.LANGUAGES).get(lang, lang)} - else: - field = change.field - field_title = field.verbose_name - model = getattr(field, 'related_model', None) - if model is not None: + if name.startswith('title_'): + lang = name[6:] + field_title = _('Title (%(lang)s)') % {'lang': dict(settings.LANGUAGES).get(lang, lang)} + field_value = str(value) + if field_value: + obj.titles[lang] = field_value + else: + obj.titles.pop(lang, None) change_data.update({ - 'value': objects[model][json.loads(change.field_value)].title + 'order': (4, tuple(code for code, title in settings.LANGUAGES).index(lang)), }) - change_data.update({ - 'title': _('reverted %(field_title)s') % {'field_title': field_title}, + else: + field = model._meta.get_field(name) + field_title = field.verbose_name + field_value = field.to_python(value) + if field.related_model is not None: + field_value = objects[field.related_model][field_value].title + order = 5 + if name == 'slug': + order = 1 + if name not in form_fields: + order = 0 + change_data.update({ + 'order': (order, form_fields.index(name) if order else 1), + }) + if not field_value: + change_data.update({ + 'title': _('remove %(field_title)s') % {'field_title': field_title}, + }) + else: + change_data.update({ + 'title': field_title, + 'value': field_value, + }) + update_changes.append(change_data) + + changes.extend(sorted(update_changes, key=itemgetter('order'))) + + for m2m_mode in ('m2m_added', 'm2m_removed'): + m2m_list = getattr(changed_object, m2m_mode).items() + for name, values in sorted(m2m_list, key=lambda name, value: form_fields.index(name)): + field = model._meta.get_field(name) + for value in values: + change_data.update({ + 'icon': 'chevron-right' if m2m_mode == 'm2m_added' else 'chevron-left', + 'class': 'info', + 'title': field.verbose_name, + 'value': objects[field.related_model][value].title, + }) + + if changed_object.deleted: + changes.append({ + 'icon': 'minus', + 'class': 'danger', + 'title': _('deleted'), + 'order': (9,), }) - elif change.action in ('m2m_add', 'm2m_remove'): - change_data.update({ - 'icon': 'chevron-right' if change.action == 'm2m_add' else 'chevron-left', - 'class': 'info', - }) - if change.field_name == 'redirects': - change_data.update({ - 'title': _('Redirecting slugs'), - 'value': change.field_value, - 'order': (6, -1, (change.action == 'm2m_remove')), - }) - else: - field = obj.__class__._meta.get_field(change.field_name) - change_data.update({ - 'title': field.verbose_name, - 'value': objects[field.related_model][json.loads(change.field_value)].title, - 'order': (6, form._meta.fields.index(change.field_name), - (change.action == 'm2m_remove')), - }) - else: - change_data.update({ - 'title': '???', - 'order': (10, ) - }) + changed_objects_data = sorted(changed_objects_data, key=itemgetter('order')) + if changeset.author: desc = _('created at %(datetime)s by') % {'datetime': date_format(changeset.created, 'DATETIME_FORMAT')} else: desc = _('created at %(datetime)s') % {'datetime': date_format(changeset.created, 'DATETIME_FORMAT')} - if not show_history: - grouped_changes = sorted(grouped_changes.values(), key=itemgetter('order')) - for group in grouped_changes: - group['changes'] = sorted(group['changes'], key=itemgetter('order')) - ctx = { 'pk': changeset.pk, 'changeset': changeset, 'desc': desc, - 'grouped_changes': grouped_changes, + 'changed_objects': changed_objects_data, } - return ctx + + return render(request, 'editor/changeset.html', ctx) diff --git a/src/c3nav/editor/views/edit.py b/src/c3nav/editor/views/edit.py index 12dcebd4..b669753d 100644 --- a/src/c3nav/editor/views/edit.py +++ b/src/c3nav/editor/views/edit.py @@ -19,19 +19,19 @@ def child_model(model, kwargs=None, parent=None): @sidebar_view def main_index(request): - Level = request.changeset.wrap('Level') + Level = request.changeset.wrap_model('Level') return render(request, 'editor/index.html', { 'levels': Level.objects.filter(on_top_of__isnull=True), 'child_models': [ - child_model(request.changeset.wrap('LocationGroup')), - child_model(request.changeset.wrap('Source')), + child_model(request.changeset.wrap_model('LocationGroup')), + child_model(request.changeset.wrap_model('Source')), ], }) @sidebar_view def level_detail(request, pk): - Level = request.changeset.wrap('Level') + Level = request.changeset.wrap_model('Level') level = get_object_or_404(Level.objects.select_related('on_top_of').prefetch_related('levels_on_top'), pk=pk) return render(request, 'editor/level.html', { @@ -40,7 +40,7 @@ def level_detail(request, pk): 'level_url': 'editor.levels.detail', 'level_as_pk': True, - 'child_models': [child_model(request.changeset.wrap(model_name), kwargs={'level': pk}, parent=level) + 'child_models': [child_model(request.changeset.wrap_model(model_name), kwargs={'level': pk}, parent=level) for model_name in ('Building', 'Space', 'Door')], 'levels_on_top': level.levels_on_top.all(), 'geometry_url': '/api/editor/geometries/?level='+str(level.primary_level_pk), @@ -49,14 +49,14 @@ def level_detail(request, pk): @sidebar_view def space_detail(request, level, pk): - Space = request.changeset.wrap('Space') + Space = request.changeset.wrap_model('Space') space = get_object_or_404(Space.objects.select_related('level'), level__pk=level, pk=pk) return render(request, 'editor/space.html', { 'level': space.level, 'space': space, - 'child_models': [child_model(request.changeset.wrap(model_name), kwargs={'space': pk}, parent=space) + 'child_models': [child_model(request.changeset.wrap_model(model_name), kwargs={'space': pk}, parent=space) for model_name in ('Hole', 'Area', 'Stair', 'Obstacle', 'LineObstacle', 'Column', 'Point')], 'geometry_url': '/api/editor/geometries/?space='+pk, }) @@ -64,11 +64,11 @@ def space_detail(request, level, pk): @sidebar_view def edit(request, pk=None, model=None, level=None, space=None, on_top_of=None, explicit_edit=False): - model = request.changeset.wrap(model) + model = request.changeset.wrap_model(model) related_name = model._meta.default_related_name - Level = request.changeset.wrap('Level') - Space = request.changeset.wrap('Space') + Level = request.changeset.wrap_model('Level') + Space = request.changeset.wrap_model('Space') obj = None if pk is not None: @@ -229,10 +229,10 @@ def list_objects(request, model=None, level=None, space=None, explicit_edit=Fals if not request.resolver_match.url_name.endswith('.list'): raise ValueError('url_name does not end with .list') - model = request.changeset.wrap(model) + model = request.changeset.wrap_model(model) - Level = request.changeset.wrap('Level') - Space = request.changeset.wrap('Space') + Level = request.changeset.wrap_model('Level') + Space = request.changeset.wrap_model('Space') ctx = { 'path': request.path,