wrap around entire django ORM in all editor views
This commit is contained in:
parent
9e58a662e0
commit
7e78bf0550
4 changed files with 224 additions and 20 deletions
|
@ -31,7 +31,9 @@ class MapitemFormMixin(ModelForm):
|
|||
self.initial['geometry'] = json.dumps(mapping(self.instance.geometry), separators=(',', ':'))
|
||||
|
||||
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'].queryset = LocationGroup.objects.all()
|
||||
|
||||
# parse titles
|
||||
self.titles = None
|
||||
|
|
|
@ -9,6 +9,8 @@ from django.utils.functional import cached_property
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import ungettext_lazy
|
||||
|
||||
from c3nav.editor.wrappers import ModelInstanceWrapper, ModelWrapper
|
||||
|
||||
|
||||
class ChangeSet(models.Model):
|
||||
created = models.DateTimeField(auto_now_add=True, verbose_name=_('created'))
|
||||
|
@ -23,6 +25,10 @@ class ChangeSet(models.Model):
|
|||
verbose_name_plural = _('Change Sets')
|
||||
default_related_name = 'changesets'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.default_author = None
|
||||
|
||||
@classmethod
|
||||
def qs_base(cls):
|
||||
return cls.objects.prefetch_related('changes').select_related('author')
|
||||
|
@ -39,6 +45,7 @@ class ChangeSet(models.Model):
|
|||
|
||||
changeset = qs.filter(pk=changeset_pk).first()
|
||||
if changeset is not None:
|
||||
changeset.default_author = request.user
|
||||
if changeset.author_id is None and request.user.is_authenticated():
|
||||
changeset.author = request.user
|
||||
changeset.save()
|
||||
|
@ -50,12 +57,14 @@ class ChangeSet(models.Model):
|
|||
changeset = qs_base.filter(Q(author=request.user)).order_by('-created').first()
|
||||
if changeset is not None:
|
||||
request.session['changeset_pk'] = changeset.pk
|
||||
changeset.default_author = request.user
|
||||
return changeset
|
||||
|
||||
new_changeset.author = request.user
|
||||
|
||||
new_changeset.save()
|
||||
request.session['changeset_pk'] = new_changeset.pk
|
||||
new_changeset.default_author = request.user
|
||||
return new_changeset
|
||||
|
||||
@cached_property
|
||||
|
@ -66,6 +75,44 @@ class ChangeSet(models.Model):
|
|||
def count_display(self):
|
||||
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):
|
||||
ACTIONS = (
|
||||
|
@ -136,12 +183,13 @@ class Change(models.Model):
|
|||
@obj.setter
|
||||
def obj(self, value: models.Model):
|
||||
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.')
|
||||
if value.action != 'create':
|
||||
raise ValueError('value is a Change instance but has action not set to create')
|
||||
self.model_class = value.model_class
|
||||
self.created_object = value
|
||||
self.created_object_id = value.pk
|
||||
self.existing_object_pk = None
|
||||
return
|
||||
|
||||
|
@ -183,13 +231,24 @@ class Change(models.Model):
|
|||
if getattr(self, field_name) is not None:
|
||||
raise ValidationError('%s must not be set if action is create or delete.' % field_name)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.full_clean()
|
||||
if self.pk is not None:
|
||||
raise TypeError('change objects can not be edited.')
|
||||
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 save(self, *args, **kwargs):
|
||||
self.full_clean()
|
||||
if self.pk is not None:
|
||||
raise TypeError('change objects can not be edited.')
|
||||
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 delete(self, *args, **kwargs):
|
||||
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
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
from contextlib import suppress
|
||||
from functools import wraps
|
||||
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import FieldDoesNotExist, PermissionDenied
|
||||
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 c3nav.editor.models import ChangeSet
|
||||
from c3nav.mapdata.models import Level, Space
|
||||
from c3nav.mapdata.models.base import EDITOR_FORM_MODELS
|
||||
|
||||
|
||||
|
@ -31,8 +29,7 @@ def sidebar_view(func):
|
|||
return never_cache(with_ajax_check)
|
||||
|
||||
|
||||
def child_model(model_name, kwargs=None, parent=None):
|
||||
model = apps.get_model('mapdata', model_name)
|
||||
def child_model(model, kwargs=None, parent=None):
|
||||
related_name = model._meta.default_related_name
|
||||
return {
|
||||
'title': model._meta.verbose_name_plural,
|
||||
|
@ -43,17 +40,19 @@ def child_model(model_name, kwargs=None, parent=None):
|
|||
|
||||
@sidebar_view
|
||||
def main_index(request):
|
||||
Level = request.changeset.wrap('Level')
|
||||
return render(request, 'editor/index.html', {
|
||||
'levels': Level.objects.filter(on_top_of__isnull=True),
|
||||
'child_models': [
|
||||
child_model('LocationGroup'),
|
||||
child_model('Source'),
|
||||
child_model(request.changeset.wrap('LocationGroup')),
|
||||
child_model(request.changeset.wrap('Source')),
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
@sidebar_view
|
||||
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)
|
||||
|
||||
return render(request, 'editor/level.html', {
|
||||
|
@ -62,7 +61,7 @@ def level_detail(request, pk):
|
|||
'level_url': 'editor.levels.detail',
|
||||
'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')],
|
||||
'levels_on_top': level.levels_on_top.all(),
|
||||
'geometry_url': '/api/editor/geometries/?level='+str(level.primary_level_pk),
|
||||
|
@ -71,13 +70,14 @@ def level_detail(request, pk):
|
|||
|
||||
@sidebar_view
|
||||
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)
|
||||
|
||||
return render(request, 'editor/space.html', {
|
||||
'level': space.level,
|
||||
'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')],
|
||||
'geometry_url': '/api/editor/geometries/?space='+pk,
|
||||
})
|
||||
|
@ -85,9 +85,12 @@ 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 = EDITOR_FORM_MODELS[model]
|
||||
model = request.changeset.wrap(EDITOR_FORM_MODELS[model])
|
||||
related_name = model._meta.default_related_name
|
||||
|
||||
Level = request.changeset.wrap('Level')
|
||||
Space = request.changeset.wrap('Space')
|
||||
|
||||
obj = None
|
||||
if pk is not None:
|
||||
# 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
|
||||
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'):
|
||||
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
|
||||
ctx = {
|
||||
'path': request.path,
|
||||
|
|
136
src/c3nav/editor/wrappers.py
Normal file
136
src/c3nav/editor/wrappers.py
Normal 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
|
Loading…
Add table
Add a link
Reference in a new issue