create changes on wrapped model save

This commit is contained in:
Laura Klünder 2017-06-13 03:31:10 +02:00
parent 4269f64326
commit e84028ffa8
3 changed files with 96 additions and 37 deletions

View file

@ -1,3 +1,4 @@
import json
import typing
from django.apps import apps
@ -82,7 +83,7 @@ class ChangeSet(models.Model):
author = None
if isinstance(obj, str):
return ModelWrapper(self, apps.get_model('mapdata', obj), author)
if issubclass(obj, models.Model):
if isinstance(obj, type) and issubclass(obj, models.Model):
return ModelWrapper(self, obj, author)
if isinstance(obj, models.Model):
return ModelInstanceWrapper(self, obj, author)
@ -91,28 +92,26 @@ class ChangeSet(models.Model):
def _new_change(self, author, **kwargs):
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)
print(repr(change))
change.save()
# 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_create(self, obj, author=None):
change = self._new_change(author=author, action='create', model_class=type(obj._obj))
obj.pk = 'c%d' % change.pk
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_update(self, obj, name, value, author):
return self._new_change(author=author, action='update', obj=obj,
field_name=name, field_value=json.dumps(value, ensure_ascii=False))
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 = (
@ -148,11 +147,14 @@ class Change(models.Model):
@property
def model_class(self) -> typing.Type[models.Model]:
if self.model_name is None:
raise TypeError('model_name is not set, can not get model')
return None
return apps.get_model('mapdata', self.model_name)
@model_class.setter
def model_class(self, value: typing.Type[models.Model]):
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:
@ -163,43 +165,43 @@ class Change(models.Model):
@property
def obj(self) -> models.Model:
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.')
if self._set_object is not None:
if isinstance(self._set_object, self.model_class) and self._set_object.pk != self.existing_object_pk:
return self._set_object
self._set_object = None
self._set_object = 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.created_object
raise NotImplementedError
raise TypeError('existing_model_pk or created_object have to be set.')
@obj.setter
def obj(self, value: models.Model):
if isinstance(value, Change):
if value.changeset_id != self.changeset_id:
if isinstance(value, ModelInstanceWrapper) and isinstance(value.pk, str):
if value._changeset.id != self.changeset.pk:
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.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 = value.__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
def clean(self):
if self.action == 'delchange':
@ -232,7 +234,7 @@ class Change(models.Model):
raise ValidationError('%s must not be set if action is create or delete.' % field_name)
def save(self, *args, **kwargs):
self.full_clean()
self.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:
@ -243,9 +245,9 @@ class Change(models.Model):
raise TypeError('change objects can not be deleted directly.')
def __repr__(self):
result = '<Change on ChangeSet #'+str(self.changeset_id)+': '
result = '<Change #%s on ChangeSet #%s: ' % (str(self.pk), str(self.changeset_id))
if self.action == 'create':
result += 'Create object of type '+repr(self.model_class)
result += 'Create '+repr(self.model_class.__name__)
elif self.action == 'update':
result += 'Update object '+repr(self.obj)+': '+self.field_name+'='+self.field_value
elif self.action == 'delete':

View file

@ -157,7 +157,7 @@ def edit(request, pk=None, model=None, level=None, space=None, on_top_of=None, e
'geometry_url': '/api/editor/geometries/?level='+str(level.primary_level_pk),
})
elif hasattr(model, 'level'):
if obj:
if not new:
level = obj.level
ctx.update({
'level': level,
@ -165,7 +165,7 @@ def edit(request, pk=None, model=None, level=None, space=None, on_top_of=None, e
'geometry_url': '/api/editor/geometries/?level='+str(level.primary_level_pk),
})
elif hasattr(model, 'space'):
if obj:
if not new:
space = obj.space
ctx.update({
'level': space.level,
@ -184,7 +184,7 @@ def edit(request, pk=None, model=None, level=None, space=None, on_top_of=None, e
})
if request.method == 'POST':
if obj is not None and request.POST.get('delete') == '1':
if not new and request.POST.get('delete') == '1':
# Delete this mapitem!
if request.POST.get('delete_confirm') == '1':
obj.delete()
@ -198,7 +198,7 @@ def edit(request, pk=None, model=None, level=None, space=None, on_top_of=None, e
ctx['obj_title'] = obj.title
return render(request, 'editor/delete.html', ctx)
form = model.EditorForm(instance=obj, data=request.POST, request=request)
form = model.EditorForm(instance=model() if new else obj, data=request.POST, request=request)
if form.is_valid():
# Update/create objects
obj = form.save(commit=False)
@ -226,7 +226,8 @@ def edit(request, pk=None, model=None, level=None, space=None, on_top_of=None, e
obj.on_top_of = on_top_of
obj.save()
form.save_m2m()
# form.save_m2m()
# request.changeset.changes.all().delete()
return redirect(ctx['back_url'])
else:

View file

@ -1,9 +1,10 @@
from django.db import models
from django.db.models import Manager
from django.db.models.fields.related_descriptors import ForwardManyToOneDescriptor
class BaseWrapper:
_not_wrapped = ('_changeset', '_author', '_obj', '_changes_qs')
_not_wrapped = ('_changeset', '_author', '_obj', '_changes_qs', '_initial_values')
_allowed_callables = ('', )
def __init__(self, changeset, obj, author=None):
@ -58,7 +59,7 @@ class ModelWrapper(BaseWrapper):
return self._obj is other
def __call__(self, **kwargs):
instance = self._wrap_instance(self._value())
instance = self._wrap_instance(self._obj())
for name, value in kwargs.items():
setattr(instance, name, value)
return instance
@ -67,6 +68,17 @@ class ModelWrapper(BaseWrapper):
class ModelInstanceWrapper(BaseWrapper):
_allowed_callables = ('full_clean', 'validate_unique')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._initial_values = {}
for field in self._obj._meta.get_fields():
if field.related_model is None:
if field.primary_key:
continue
self._initial_values[field] = getattr(self, field.name)
elif (field.many_to_one or field.one_to_one) and not field.primary_key:
self._initial_values[field] = getattr(self, field.name).pk
def __eq__(self, other):
if type(other) == ModelWrapper:
if type(self._obj) is not type(other._obj): # noqa
@ -75,6 +87,50 @@ class ModelInstanceWrapper(BaseWrapper):
return False
return self.pk == other.pk
def __setattr__(self, name, value):
if name in self._not_wrapped:
return super().__setattr__(name, value)
class_value = getattr(type(self._obj), name, None)
if isinstance(class_value, ForwardManyToOneDescriptor) and value is not None:
if not isinstance(value, ModelInstanceWrapper):
raise ValueError('value has to be None or ModelInstanceWrapper')
setattr(self._obj, name, value._obj)
setattr(self._obj, class_value.cache_name, value)
return
super().__setattr__(name, value)
def __repr__(self):
cls_name = self._obj.__class__.__name__
if self.pk is None:
return '<%s (unsaved) with Changeset #%d>' % (cls_name, self._changeset.pk)
elif isinstance(self.pk, int):
return '<%s #%d (existing) with Changeset #%d>' % (cls_name, self.pk, self._changeset.pk)
elif isinstance(self.pk, str):
return '<%s #%s (created) from Changeset #%d>' % (cls_name, self.pk, self._changeset.pk)
raise TypeError
def save(self, author=None):
if self.pk is None:
self._changeset.add_create(self, author=author)
for field, initial_value in self._initial_values.items():
new_value = getattr(self._obj, field.name)
if field.related_model:
if new_value.pk != initial_value.pk:
self._changeset.add_update(self, name=field.name, value=new_value.pk, author=author)
continue
if new_value == initial_value:
continue
if field.name == 'titles':
for lang in (set(initial_value.keys()) | set(new_value.keys())):
new_title = new_value.get(lang, '')
if new_title != initial_value.get(lang, ''):
self._changeset.add_update(self, name='title_'+lang, value=new_title, author=author)
continue
self._changeset.add_update(self, name=field.name, value=new_value, author=author)
class ChangesQuerySet():
def __init__(self, changeset, model, author):