replace Change with ChangedObject (wrapper integration still missing)

This commit is contained in:
Laura Klünder 2017-06-27 03:20:50 +02:00
parent 0ef51ac147
commit 9d5811bc15
12 changed files with 457 additions and 855 deletions

View file

@ -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')

View file

@ -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

View file

@ -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')]),
),
]

View file

@ -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 = '<Change #%s on ChangeSet #%s: ' % (str(self.pk), str(self.changeset_id))
if self.action == 'create':
result += 'Create '+repr(self.model_name)
elif self.action == 'update':
result += ('Update '+repr(self.model_name)+' #'+str(self.obj_pk)+': ' +
self.field_name+'='+self.field_value)
elif self.action == 'restore':
result += ('Restore '+repr(self.model_name)+' #'+str(self.obj_pk)+': '+self.field_name)
elif self.action == 'delete':
result += 'Delete object '+repr(self.model_name)+' #'+str(self.obj_pk)
elif self.action == 'm2m_add':
result += ('Update (m2m) object '+repr(self.model_name)+' #'+str(self.obj_pk)+': ' +
self.field_name+'.add('+self.field_value+')')
elif self.action == 'm2m_remove':
result += ('Update (m2m) object '+repr(self.model_name)+' #'+str(self.obj_pk)+': ' +
self.field_name+'.remove('+self.field_value+')')
result += '>'
return result
def serialize(self):
result = OrderedDict((
('id', self.pk),
('author', self.author_id),
('created', None if self.created is None else self.created.isoformat()),
('action', self.action),
('object_type', self.model_class.__name__.lower()),
('object_id', ('c'+str(self.pk)) if self.action == 'create' else self.obj_pk),
))
if self.action in ('update', 'm2m_add', 'm2m_remove'):
result.update(OrderedDict((
('name', self.field_name),
('value', json.loads(self.field_value)),
)))
return result

View file

@ -0,0 +1,157 @@
import typing
from itertools import chain
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.utils.translation import ugettext_lazy as _
from c3nav.editor.utils import is_created_pk
from c3nav.editor.wrappers import ModelInstanceWrapper
from c3nav.mapdata.fields import JSONField
class ChangedObject(models.Model):
changeset = models.ForeignKey('editor.ChangeSet', on_delete=models.CASCADE, verbose_name=_('Change Set'))
created = models.DateTimeField(auto_now_add=True, verbose_name=_('created'))
last_update = models.DateTimeField(auto_now=True, verbose_name=_('last update'))
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
existing_object_pk = models.PositiveIntegerField(null=True, verbose_name=_('id of existing object'))
updated_fields = JSONField(default={}, verbose_name=_('updated fields'))
m2m_added = JSONField(default={}, verbose_name=_('added m2m values'))
m2m_removed = JSONField(default={}, verbose_name=_('removed m2m values'))
deleted = models.BooleanField(default=False, verbose_name=_('new field value'))
class Meta:
verbose_name = _('Changed object')
verbose_name_plural = _('Changed objects')
default_related_name = 'changed_objects_set'
unique_together = ('changeset', 'content_type', 'existing_object_pk')
ordering = ['created', 'pk']
def __init__(self, *args, **kwargs):
model_class = kwargs.pop('model_class', None)
super().__init__(*args, **kwargs)
self._set_object = None
self._m2m_added_cache = {name: set(values) for name, values in self.m2m_added}
self._m2m_removed_cache = {name: set(values) for name, values in self.m2m_added}
if model_class is not None:
self.model_class = model_class
@property
def model_class(self) -> 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 '<ChangedObject #%s on ChangeSet #%s>' % (str(self.pk), str(self.changeset_id))

View file

@ -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:

View file

@ -12,13 +12,58 @@
<a class="btn btn-default btn-xs" href="">
{% trans 'Edit' %}
</a>
<a class="btn btn-default btn-xs" href="{% url 'editor.changesets.history' pk=changeset.pk %}">
{% trans 'View History' %}
</a>
</p>
{% bootstrap_messages %}
{% include 'editor/fragment_change_groups.html' %}
{% for group in grouped_changes %}
<table class="table table-condensed table-h-bordered change-group">
<thead>
<tr>
<th colspan="3">
{% if group.edit_url %}
<a class="btn btn-default btn-xs pull-right" data-force-next-zoom href="{{ group.edit_url }}">
{% trans 'Edit' %}
</a>
{% endif %}
{% if group.obj_title %}
{{ group.obj_title }} <small>({{ group.obj }})</small>
{% else %}
{{ group.obj }}
{% endif %}
</th>
</tr>
</thead>
<tbody>
{% for change in group.changes %}
<tr>
<td class="text-{{ change.class }}"><i class="glyphicon glyphicon-{{ change.icon }}"></i></td>
<td{% if change.discarded %} class="text-muted"{% endif %}>
{% if change.value %}<u>{% else %}<em>{% endif %}{{ change.title }}{% if change.value %}</u>:{% else %}</em>{% endif %}
{{ change.value }}
</td>
<td class="text-muted">
{% if change.apply_problem %}
<i class="glyphicon glyphicon-alert text-danger" data-toggle="tooltip" data-placement="left" title="{{ change.apply_problem }}"></i>
{% elif change.has_no_effect %}
<i class="glyphicon glyphicon-ok text-success" data-toggle="tooltip" data-placement="left" title="{% trans 'This change has no effect. You can restore it.' %}"></i>
{% endif %}
{% if change.author and change.author != changeset.author %}
<i class="glyphicon glyphicon-user text-muted" data-toggle="tooltip" data-placement="left" title="{{ change.author.username }}"></i>
{% endif %}
{% if change.can_restore %}
<button type="submit" name="restore" value="{{ change.pk }}" class="btn btn-xs btn-default" data-toggle="tooltip" data-placement="left" title="{% trans 'Restore original state' %}">
<i class="glyphicon glyphicon-share-alt"></i>
</button>
{% endif %}
{% if change.created %}
<i class="glyphicon glyphicon-time text-muted" data-toggle="tooltip" data-placement="left" title="{{ change.created }}"></i>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endfor %}
{% buttons %}
<button type="submit" class="btn btn-danger" name="delete" value="1">{% trans 'Delete' %}</button>

View file

@ -1,13 +0,0 @@
{% load bootstrap3 %}
{% load i18n %}
{% include 'editor/fragment_modal_close.html' %}
<h3>{{ changeset.title }}</h3>
<h4>{% trans 'Changeset History' %}</h4>
<form method="post" action="{{ request.path }}">
{% csrf_token %}
<p><a href="{% url 'editor.changesets.detail' pk=changeset.pk %}">« {% trans 'back to changeset' %}</a></p>
{% bootstrap_messages %}
{% include 'editor/fragment_change_groups.html' %}
</form>

View file

@ -1,51 +0,0 @@
{% load i18n %}
{% for group in grouped_changes %}
<table class="table table-condensed table-h-bordered change-group">
<thead>
<tr>
<th colspan="3">
{% if group.edit_url %}
<a class="btn btn-default btn-xs pull-right" data-force-next-zoom href="{{ group.edit_url }}">
{% trans 'Edit' %}
</a>
{% endif %}
{% if group.obj_title %}
{{ group.obj_title }} <small>({{ group.obj }})</small>
{% else %}
{{ group.obj }}
{% endif %}
</th>
</tr>
</thead>
<tbody>
{% for change in group.changes %}
<tr>
<td class="text-{{ change.class }}"><i class="glyphicon glyphicon-{{ change.icon }}"></i></td>
<td{% if change.discarded %} class="text-muted"{% endif %}>
{% if change.value %}<u>{% else %}<em>{% endif %}{{ change.title }}{% if change.value %}</u>:{% else %}</em>{% endif %}
{{ change.value }}
</td>
<td class="text-muted">
{% if change.apply_problem %}
<i class="glyphicon glyphicon-alert text-danger" data-toggle="tooltip" data-placement="left" title="{{ change.apply_problem }}"></i>
{% elif change.has_no_effect %}
<i class="glyphicon glyphicon-ok text-success" data-toggle="tooltip" data-placement="left" title="{% trans 'This change has no effect. You can restore it.' %}"></i>
{% endif %}
{% if change.author and change.author != changeset.author %}
<i class="glyphicon glyphicon-user text-muted" data-toggle="tooltip" data-placement="left" title="{{ change.author.username }}"></i>
{% endif %}
{% if change.can_restore %}
<button type="submit" name="restore" value="{{ change.pk }}" class="btn btn-xs btn-default" data-toggle="tooltip" data-placement="left" title="{% trans 'Restore original state' %}">
<i class="glyphicon glyphicon-share-alt"></i>
</button>
{% endif %}
{% if change.created %}
<i class="glyphicon glyphicon-time text-muted" data-toggle="tooltip" data-placement="left" title="{{ change.created }}"></i>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endfor %}

View file

@ -37,8 +37,6 @@ urlpatterns = [
url(r'^levels/(?P<on_top_of>c?[0-9]+)/levels_on_top/create$', edit, name='editor.levels_on_top.create',
kwargs={'model': 'Level'}),
url(r'^changesets/(?P<pk>[0-9]+)/$', changeset_detail, name='editor.changesets.detail'),
url(r'^changesets/(?P<pk>[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'),
]

View file

@ -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)

View file

@ -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,