From 70a1c79f81d4ceecba42e541947b14a2e9980bff Mon Sep 17 00:00:00 2001 From: Alexander Graf Date: Mon, 15 Feb 2021 22:57:37 +0100 Subject: [PATCH] handle prune and delete for lists and backrefs --- core/admin/mailu/manage.py | 13 ++++++++--- core/admin/mailu/schemas.py | 43 ++++++++++++++++++++++++++++++++++--- 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/core/admin/mailu/manage.py b/core/admin/mailu/manage.py index a20c7d6d..05eae010 100644 --- a/core/admin/mailu/manage.py +++ b/core/admin/mailu/manage.py @@ -478,9 +478,16 @@ def config_import(verbose=0, secrets=False, quiet=False, color=False, update=Fal if verbose >= 1: log('Modified', target, f'{str(target)!r} dkim_key: {before!r} -> {after!r}') - def track_serialize(obj, item): + def track_serialize(obj, item, backref=None): """ callback function to track import """ - # hide secrets + # called for backref modification? + if backref is not None: + log('Modified', item, '{target!r} {key}: {before!r} -> {after!r}'.format(**backref)) + return + # verbose? + if not verbose >= 2: + return + # hide secrets in data data = logger[obj.opts.model].hide(item) if 'hash_password' in data: data['password'] = HIDDEN @@ -501,7 +508,7 @@ def config_import(verbose=0, secrets=False, quiet=False, color=False, update=Fal 'import': True, 'update': update, 'clear': not update, - 'callback': track_serialize if verbose >= 2 else None, + 'callback': track_serialize, } # register listeners diff --git a/core/admin/mailu/schemas.py b/core/admin/mailu/schemas.py index 8e91b4aa..3e15ee26 100644 --- a/core/admin/mailu/schemas.py +++ b/core/admin/mailu/schemas.py @@ -14,6 +14,7 @@ from marshmallow import pre_load, post_load, post_dump, fields, Schema from marshmallow.utils import ensure_text_type from marshmallow.exceptions import ValidationError from marshmallow_sqlalchemy import SQLAlchemyAutoSchemaOpts +from marshmallow_sqlalchemy.fields import RelatedList from flask_marshmallow import Marshmallow @@ -39,8 +40,6 @@ ma = Marshmallow() # - when modifying, nothing is required (only the primary key, but this key is in the uri) # - the primary key from post data must not differ from the key in the uri # - when creating all fields without default or auto-increment are required -# TODO: what about deleting list items and prung lists? -# - domain.alternatives, user.forward_destination, user.manager_of, aliases.destination # TODO: validate everything! @@ -652,7 +651,7 @@ class BaseSchema(ma.SQLAlchemyAutoSchema): if '__delete__' in data: # deletion of non-existent item requested raise ValidationError( - f'item not found: {data[self._primary]!r}', + f'item to delete not found: {data[self._primary]!r}', field_name=f'?.{self._primary}', ) @@ -665,6 +664,44 @@ class BaseSchema(ma.SQLAlchemyAutoSchema): # delete instance when marked if '__delete__' in data: self.opts.sqla_session.delete(instance) + # delete item from lists or prune lists + # currently: domain.alternatives, user.forward_destination, + # user.manager_of, aliases.destination + for key, value in data.items(): + if isinstance(value, list): + new_value = set(value) + # handle list pruning + if '-prune-' in value: + value.remove('-prune-') + new_value.remove('-prune-') + else: + for old in getattr(instance, key): + # using str() is okay for now (see above) + new_value.add(str(old)) + # handle item deletion + for item in value: + if item.startswith('-'): + new_value.remove(item) + try: + new_value.remove(item[1:]) + except KeyError as exc: + raise ValidationError( + f'item to delete not found: {item[1:]!r}', + field_name=f'?.{key}', + ) from exc + # deduplicate and sort list + data[key] = sorted(new_value) + # log backref modification not catched by hook + if isinstance(self.fields[key], RelatedList): + if callback := self.context.get('callback'): + callback(self, instance, { + 'key': key, + 'target': str(instance), + 'before': [str(v) for v in getattr(instance, key)], + 'after': data[key], + }) + + # add attributes required for validation from db # TODO: this will cause validation errors if value from database does not validate