handle prune and delete for lists and backrefs

master
Alexander Graf 4 years ago
parent 8929912dea
commit 70a1c79f81

@ -478,9 +478,16 @@ def config_import(verbose=0, secrets=False, quiet=False, color=False, update=Fal
if verbose >= 1: if verbose >= 1:
log('Modified', target, f'{str(target)!r} dkim_key: {before!r} -> {after!r}') 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 """ """ 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) data = logger[obj.opts.model].hide(item)
if 'hash_password' in data: if 'hash_password' in data:
data['password'] = HIDDEN data['password'] = HIDDEN
@ -501,7 +508,7 @@ def config_import(verbose=0, secrets=False, quiet=False, color=False, update=Fal
'import': True, 'import': True,
'update': update, 'update': update,
'clear': not update, 'clear': not update,
'callback': track_serialize if verbose >= 2 else None, 'callback': track_serialize,
} }
# register listeners # register listeners

@ -14,6 +14,7 @@ from marshmallow import pre_load, post_load, post_dump, fields, Schema
from marshmallow.utils import ensure_text_type from marshmallow.utils import ensure_text_type
from marshmallow.exceptions import ValidationError from marshmallow.exceptions import ValidationError
from marshmallow_sqlalchemy import SQLAlchemyAutoSchemaOpts from marshmallow_sqlalchemy import SQLAlchemyAutoSchemaOpts
from marshmallow_sqlalchemy.fields import RelatedList
from flask_marshmallow import Marshmallow 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) # - 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 # - 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 # - 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! # TODO: validate everything!
@ -652,7 +651,7 @@ class BaseSchema(ma.SQLAlchemyAutoSchema):
if '__delete__' in data: if '__delete__' in data:
# deletion of non-existent item requested # deletion of non-existent item requested
raise ValidationError( 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}', field_name=f'?.{self._primary}',
) )
@ -665,6 +664,44 @@ class BaseSchema(ma.SQLAlchemyAutoSchema):
# delete instance when marked # delete instance when marked
if '__delete__' in data: if '__delete__' in data:
self.opts.sqla_session.delete(instance) 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 # add attributes required for validation from db
# TODO: this will cause validation errors if value from database does not validate # TODO: this will cause validation errors if value from database does not validate

Loading…
Cancel
Save