wrap around entire django ORM in all editor views

This commit is contained in:
Laura Klünder 2017-06-12 22:56:39 +02:00
parent 9e58a662e0
commit 7e78bf0550
4 changed files with 224 additions and 20 deletions

View file

@ -31,7 +31,9 @@ class MapitemFormMixin(ModelForm):
self.initial['geometry'] = json.dumps(mapping(self.instance.geometry), separators=(',', ':')) self.initial['geometry'] = json.dumps(mapping(self.instance.geometry), separators=(',', ':'))
if 'groups' in self.fields: if 'groups' in self.fields:
LocationGroup = self.request.changeset.wrap('LocationGroup')
self.fields['groups'].label_from_instance = lambda obj: obj.title_for_forms self.fields['groups'].label_from_instance = lambda obj: obj.title_for_forms
self.fields['groups'].queryset = LocationGroup.objects.all()
# parse titles # parse titles
self.titles = None self.titles = None

View file

@ -9,6 +9,8 @@ from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ungettext_lazy from django.utils.translation import ungettext_lazy
from c3nav.editor.wrappers import ModelInstanceWrapper, ModelWrapper
class ChangeSet(models.Model): class ChangeSet(models.Model):
created = models.DateTimeField(auto_now_add=True, verbose_name=_('created')) created = models.DateTimeField(auto_now_add=True, verbose_name=_('created'))
@ -23,6 +25,10 @@ class ChangeSet(models.Model):
verbose_name_plural = _('Change Sets') verbose_name_plural = _('Change Sets')
default_related_name = 'changesets' default_related_name = 'changesets'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.default_author = None
@classmethod @classmethod
def qs_base(cls): def qs_base(cls):
return cls.objects.prefetch_related('changes').select_related('author') return cls.objects.prefetch_related('changes').select_related('author')
@ -39,6 +45,7 @@ class ChangeSet(models.Model):
changeset = qs.filter(pk=changeset_pk).first() changeset = qs.filter(pk=changeset_pk).first()
if changeset is not None: if changeset is not None:
changeset.default_author = request.user
if changeset.author_id is None and request.user.is_authenticated(): if changeset.author_id is None and request.user.is_authenticated():
changeset.author = request.user changeset.author = request.user
changeset.save() changeset.save()
@ -50,12 +57,14 @@ class ChangeSet(models.Model):
changeset = qs_base.filter(Q(author=request.user)).order_by('-created').first() changeset = qs_base.filter(Q(author=request.user)).order_by('-created').first()
if changeset is not None: if changeset is not None:
request.session['changeset_pk'] = changeset.pk request.session['changeset_pk'] = changeset.pk
changeset.default_author = request.user
return changeset return changeset
new_changeset.author = request.user new_changeset.author = request.user
new_changeset.save() new_changeset.save()
request.session['changeset_pk'] = new_changeset.pk request.session['changeset_pk'] = new_changeset.pk
new_changeset.default_author = request.user
return new_changeset return new_changeset
@cached_property @cached_property
@ -66,6 +75,44 @@ class ChangeSet(models.Model):
def count_display(self): def count_display(self):
return ungettext_lazy('%(num)d Change', '%(num)d Changes', 'num') % {'num': self.undeleted_changes_count} return ungettext_lazy('%(num)d Change', '%(num)d Changes', 'num') % {'num': self.undeleted_changes_count}
def wrap(self, obj, author=None):
if author is None:
author = self.default_author
if not author.is_authenticated():
author = None
if isinstance(obj, str):
return ModelWrapper(self, apps.get_model('mapdata', obj), author)
if issubclass(obj, models.Model):
return ModelWrapper(self, obj, author)
if isinstance(obj, models.Model):
return ModelInstanceWrapper(self, obj, author)
raise ValueError
def _new_change(self, author, **kwargs):
change = Change(changeset=self)
change.changeset_id = self.pk
if author is not None and author.is_authenticated():
change.author = author
for name, value in kwargs.items():
setattr(change, name, value)
print(repr(change))
return change
def add_create(self, author, model_class):
return self._new_change(author, action='create', model_class=model_class)
def add_update(self, author, obj, name, value):
return self._new_change(author, action='update', obj=obj, field_name=name, field_value=str(value))
def add_delete(self, author, obj):
return self._new_change(author, action='delete', obj=obj)
def update_object(self, author, obj, values):
if not values:
return
for name, value in values.items():
self.add_update(author, obj, name, value)
class Change(models.Model): class Change(models.Model):
ACTIONS = ( ACTIONS = (
@ -136,12 +183,13 @@ class Change(models.Model):
@obj.setter @obj.setter
def obj(self, value: models.Model): def obj(self, value: models.Model):
if isinstance(value, Change): if isinstance(value, Change):
if self.created_object.changeset_id != self.changeset_id: if value.changeset_id != self.changeset_id:
raise ValueError('value is a Change instance but belongs to a different changeset.') raise ValueError('value is a Change instance but belongs to a different changeset.')
if value.action != 'create': if value.action != 'create':
raise ValueError('value is a Change instance but has action not set to create') raise ValueError('value is a Change instance but has action not set to create')
self.model_class = value.model_class self.model_class = value.model_class
self.created_object = value self.created_object = value
self.created_object_id = value.pk
self.existing_object_pk = None self.existing_object_pk = None
return return
@ -183,13 +231,24 @@ class Change(models.Model):
if getattr(self, field_name) is not None: if getattr(self, field_name) is not None:
raise ValidationError('%s must not be set if action is create or delete.' % field_name) raise ValidationError('%s must not be set if action is create or delete.' % field_name)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
self.full_clean() self.full_clean()
if self.pk is not None: if self.pk is not None:
raise TypeError('change objects can not be edited.') raise TypeError('change objects can not be edited.')
if self.changeset.proposed is not None or self.changeset.applied is not None: if self.changeset.proposed is not None or self.changeset.applied is not None:
raise TypeError('can not add change object to uneditable changeset.') raise TypeError('can not add change object to uneditable changeset.')
super().save(*args, **kwargs) super().save(*args, **kwargs)
def delete(self, *args, **kwargs): def delete(self, *args, **kwargs):
raise TypeError('change objects can not be deleted directly.') raise TypeError('change objects can not be deleted directly.')
def __repr__(self):
result = '<Change on ChangeSet #'+str(self.changeset_id)+': '
if self.action == 'create':
result += 'Create object of type '+repr(self.model_class)
elif self.action == 'update':
result += 'Update object '+repr(self.obj)+': '+self.field_name+'='+self.field_value
elif self.action == 'delete':
result += 'Delete object '+repr(self.obj)
result += '>'
return result

View file

@ -1,7 +1,6 @@
from contextlib import suppress from contextlib import suppress
from functools import wraps from functools import wraps
from django.apps import apps
from django.conf import settings from django.conf import settings
from django.core.exceptions import FieldDoesNotExist, PermissionDenied from django.core.exceptions import FieldDoesNotExist, PermissionDenied
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
@ -12,7 +11,6 @@ from django.utils.translation import ugettext_lazy as _
from django.views.decorators.cache import never_cache from django.views.decorators.cache import never_cache
from c3nav.editor.models import ChangeSet from c3nav.editor.models import ChangeSet
from c3nav.mapdata.models import Level, Space
from c3nav.mapdata.models.base import EDITOR_FORM_MODELS from c3nav.mapdata.models.base import EDITOR_FORM_MODELS
@ -31,8 +29,7 @@ def sidebar_view(func):
return never_cache(with_ajax_check) return never_cache(with_ajax_check)
def child_model(model_name, kwargs=None, parent=None): def child_model(model, kwargs=None, parent=None):
model = apps.get_model('mapdata', model_name)
related_name = model._meta.default_related_name related_name = model._meta.default_related_name
return { return {
'title': model._meta.verbose_name_plural, 'title': model._meta.verbose_name_plural,
@ -43,17 +40,19 @@ def child_model(model_name, kwargs=None, parent=None):
@sidebar_view @sidebar_view
def main_index(request): def main_index(request):
Level = request.changeset.wrap('Level')
return render(request, 'editor/index.html', { return render(request, 'editor/index.html', {
'levels': Level.objects.filter(on_top_of__isnull=True), 'levels': Level.objects.filter(on_top_of__isnull=True),
'child_models': [ 'child_models': [
child_model('LocationGroup'), child_model(request.changeset.wrap('LocationGroup')),
child_model('Source'), child_model(request.changeset.wrap('Source')),
], ],
}) })
@sidebar_view @sidebar_view
def level_detail(request, pk): def level_detail(request, pk):
Level = request.changeset.wrap('Level')
level = get_object_or_404(Level.objects.select_related('on_top_of').prefetch_related('levels_on_top'), pk=pk) 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', { return render(request, 'editor/level.html', {
@ -62,7 +61,7 @@ def level_detail(request, pk):
'level_url': 'editor.levels.detail', 'level_url': 'editor.levels.detail',
'level_as_pk': True, 'level_as_pk': True,
'child_models': [child_model(model_name, kwargs={'level': pk}, parent=level) 'child_models': [child_model(request.changeset.wrap(model_name), kwargs={'level': pk}, parent=level)
for model_name in ('Building', 'Space', 'Door')], for model_name in ('Building', 'Space', 'Door')],
'levels_on_top': level.levels_on_top.all(), 'levels_on_top': level.levels_on_top.all(),
'geometry_url': '/api/editor/geometries/?level='+str(level.primary_level_pk), 'geometry_url': '/api/editor/geometries/?level='+str(level.primary_level_pk),
@ -71,13 +70,14 @@ def level_detail(request, pk):
@sidebar_view @sidebar_view
def space_detail(request, level, pk): def space_detail(request, level, pk):
Space = request.changeset.wrap('Space')
space = get_object_or_404(Space.objects.select_related('level'), level__id=level, pk=pk) space = get_object_or_404(Space.objects.select_related('level'), level__id=level, pk=pk)
return render(request, 'editor/space.html', { return render(request, 'editor/space.html', {
'level': space.level, 'level': space.level,
'space': space, 'space': space,
'child_models': [child_model(model_name, kwargs={'space': pk}, parent=space) 'child_models': [child_model(request.changeset.wrap(model_name), kwargs={'space': pk}, parent=space)
for model_name in ('Hole', 'Area', 'Stair', 'Obstacle', 'LineObstacle', 'Column', 'Point')], for model_name in ('Hole', 'Area', 'Stair', 'Obstacle', 'LineObstacle', 'Column', 'Point')],
'geometry_url': '/api/editor/geometries/?space='+pk, 'geometry_url': '/api/editor/geometries/?space='+pk,
}) })
@ -85,9 +85,12 @@ def space_detail(request, level, pk):
@sidebar_view @sidebar_view
def edit(request, pk=None, model=None, level=None, space=None, on_top_of=None, explicit_edit=False): def edit(request, pk=None, model=None, level=None, space=None, on_top_of=None, explicit_edit=False):
model = EDITOR_FORM_MODELS[model] model = request.changeset.wrap(EDITOR_FORM_MODELS[model])
related_name = model._meta.default_related_name related_name = model._meta.default_related_name
Level = request.changeset.wrap('Level')
Space = request.changeset.wrap('Space')
obj = None obj = None
if pk is not None: if pk is not None:
# Edit existing map item # Edit existing map item
@ -246,10 +249,14 @@ def edit(request, pk=None, model=None, level=None, space=None, on_top_of=None, e
@sidebar_view @sidebar_view
def list_objects(request, model=None, level=None, space=None, explicit_edit=False): def list_objects(request, model=None, level=None, space=None, explicit_edit=False):
model = EDITOR_FORM_MODELS[model]
if not request.resolver_match.url_name.endswith('.list'): if not request.resolver_match.url_name.endswith('.list'):
raise ValueError('url_name does not end with .list') raise ValueError('url_name does not end with .list')
model = request.changeset.wrap(EDITOR_FORM_MODELS[model])
Level = request.changeset.wrap('Level')
Space = request.changeset.wrap('Space')
# noinspection PyProtectedMember # noinspection PyProtectedMember
ctx = { ctx = {
'path': request.path, 'path': request.path,

View file

@ -0,0 +1,136 @@
from django.db import models
from django.db.models import Manager
class BaseWrapper:
_not_wrapped = ('_changeset', '_author', '_obj', '_changes_qs')
_allowed_callables = ('', )
def __init__(self, changeset, obj, author=None):
self._changeset = changeset
self._author = author
self._obj = obj
def _wrap_model(self, model):
return ModelWrapper(self._changeset, model, self._author)
def _wrap_instance(self, instance):
return ModelInstanceWrapper(self._changeset, instance, self._author)
def _wrap_manager(self, manager):
return ManagerWrapper(self._changeset, manager, self._author)
def _wrap_queryset(self, queryset):
return QuerySetWrapper(self._changeset, queryset, self._author)
def __getattr__(self, name):
value = getattr(self._obj, name)
if isinstance(value, Manager):
value = self._wrap_manager(value)
elif isinstance(value, type) and issubclass(value, models.Model) and value._meta.app_label == 'mapdata':
value = self._wrap_model(value)
elif isinstance(value, models.Model) and value._meta.app_label == 'mapdata':
value = self._wrap_instance(value)
elif isinstance(value, type) and issubclass(value, Exception):
pass
elif callable(value) and name not in self._allowed_callables:
raise TypeError('Can not call %s.%s wrapped!' % (self._obj, name))
# print(self._obj, name, type(value), value)
return value
def __setattr__(self, name, value):
if name in self._not_wrapped:
return super().__setattr__(name, value)
return setattr(self._obj, name, value)
def __delattr__(self, name):
return delattr(self._obj, name)
class ModelWrapper(BaseWrapper):
_allowed_callables = ('EditorForm', )
def __eq__(self, other):
if type(other) == ModelWrapper:
return self._obj is other._obj
return self._obj is other
def __call__(self, **kwargs):
instance = self._wrap_instance(self._value())
for name, value in kwargs.items():
setattr(instance, name, value)
return instance
class ModelInstanceWrapper(BaseWrapper):
def __eq__(self, other):
if type(other) == ModelWrapper:
if type(self._obj) is not type(other._obj): # noqa
return False
elif type(self._obj) is not type(other):
return False
return self.pk == other.pk
class ChangesQuerySet():
def __init__(self, changeset, model, author):
self._changeset = changeset
self._model = model
self._author = author
class BaseQueryWrapper(BaseWrapper):
def __init__(self, changeset, obj, author=None, changes_qs=None):
super().__init__(changeset, obj, author)
if changes_qs is None:
changes_qs = ChangesQuerySet(changeset, obj.model, author)
self._changes_qs = changes_qs
def _wrap_queryset(self, queryset, changes_qs=None):
if changes_qs is None:
changes_qs = self._changes_qs
return QuerySetWrapper(self._changeset, queryset, self._author, changes_qs)
def all(self):
return self._wrap_queryset(self._obj.all())
def select_related(self, *args, **kwargs):
return self._wrap_queryset(self._obj.select_related(*args, **kwargs))
def prefetch_related(self, *args, **kwargs):
return self._wrap_queryset(self._obj.prefetch_related(*args, **kwargs))
def get(self, **kwargs):
return self._wrap_instance(self._obj.get(**kwargs))
def order_by(self, *args):
return self._wrap_queryset(self._obj.order_by(*args))
def filter(self, *args, **kwargs):
kwargs = {name: (value._obj if isinstance(value, ModelInstanceWrapper) else value)
for name, value in kwargs.items()}
return self._wrap_queryset(self._obj.filter(*args, **kwargs))
def count(self):
return self._obj.count()
def values_list(self, *args, flat=False):
return self._obj.values_list(*args, flat=flat)
def __iter__(self):
return iter([instance for instance in self._obj])
def iterator(self):
return iter(self)
def __len__(self):
return len(self._obj)
class ManagerWrapper(BaseQueryWrapper):
pass
class QuerySetWrapper(BaseQueryWrapper):
pass