create changes on wrapped model save
This commit is contained in:
parent
4269f64326
commit
e84028ffa8
3 changed files with 96 additions and 37 deletions
|
@ -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':
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue