mirror of
				https://github.com/optim-enterprises-bv/Mailu.git
				synced 2025-11-03 19:47:52 +00:00 
			
		
		
		
	handle prune and delete for lists and backrefs
This commit is contained in:
		@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user