mirror of
https://github.com/Mailu/Mailu.git
synced 2025-05-21 22:33:16 +02:00
handle prune and delete for lists and backrefs
This commit is contained in:
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…
x
Reference in New Issue
Block a user