mirror of
https://github.com/Mailu/Mailu.git
synced 2025-06-02 23:17:26 +02:00
order yaml data and allow callback on import
- in yaml the primary key is now always first - calling a function on import allows import to be more verbose - skip "fetches" when empty
This commit is contained in:
parent
8213d044b2
commit
65b1ad46d9
@ -3,11 +3,12 @@
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
from collections import OrderedDict
|
||||||
from textwrap import wrap
|
from textwrap import wrap
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
from marshmallow import pre_load, post_dump, fields, Schema
|
from marshmallow import pre_load, post_load, post_dump, fields, Schema
|
||||||
from marshmallow.exceptions import ValidationError
|
from marshmallow.exceptions import ValidationError
|
||||||
from marshmallow_sqlalchemy import SQLAlchemyAutoSchemaOpts
|
from marshmallow_sqlalchemy import SQLAlchemyAutoSchemaOpts
|
||||||
from flask_marshmallow import Marshmallow
|
from flask_marshmallow import Marshmallow
|
||||||
@ -25,6 +26,12 @@ ma = Marshmallow()
|
|||||||
|
|
||||||
### yaml render module ###
|
### yaml render module ###
|
||||||
|
|
||||||
|
# allow yaml module to dump OrderedDict
|
||||||
|
yaml.add_representer(
|
||||||
|
OrderedDict,
|
||||||
|
lambda cls, data: cls.represent_mapping('tag:yaml.org,2002:map', data.items())
|
||||||
|
)
|
||||||
|
|
||||||
class RenderYAML:
|
class RenderYAML:
|
||||||
""" Marshmallow YAML Render Module
|
""" Marshmallow YAML Render Module
|
||||||
"""
|
"""
|
||||||
@ -62,6 +69,7 @@ class RenderYAML:
|
|||||||
'Dumper': SpacedDumper,
|
'Dumper': SpacedDumper,
|
||||||
'default_flow_style': False,
|
'default_flow_style': False,
|
||||||
'allow_unicode': True,
|
'allow_unicode': True,
|
||||||
|
'sort_keys': False,
|
||||||
}
|
}
|
||||||
@classmethod
|
@classmethod
|
||||||
def dumps(cls, *args, **kwargs):
|
def dumps(cls, *args, **kwargs):
|
||||||
@ -195,6 +203,8 @@ class BaseOpts(SQLAlchemyAutoSchemaOpts):
|
|||||||
def __init__(self, meta, ordered=False):
|
def __init__(self, meta, ordered=False):
|
||||||
if not hasattr(meta, 'sqla_session'):
|
if not hasattr(meta, 'sqla_session'):
|
||||||
meta.sqla_session = models.db.session
|
meta.sqla_session = models.db.session
|
||||||
|
if not hasattr(meta, 'ordered'):
|
||||||
|
meta.ordered = True
|
||||||
super(BaseOpts, self).__init__(meta, ordered=ordered)
|
super(BaseOpts, self).__init__(meta, ordered=ordered)
|
||||||
|
|
||||||
class BaseSchema(ma.SQLAlchemyAutoSchema):
|
class BaseSchema(ma.SQLAlchemyAutoSchema):
|
||||||
@ -206,13 +216,12 @@ class BaseSchema(ma.SQLAlchemyAutoSchema):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
""" Schema config """
|
""" Schema config """
|
||||||
model = None
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|
||||||
# context?
|
# context?
|
||||||
context = kwargs.get('context', {})
|
context = kwargs.get('context', {})
|
||||||
flags = set([key for key, value in context.items() if value is True])
|
flags = {key for key, value in context.items() if value is True}
|
||||||
|
|
||||||
# compile excludes
|
# compile excludes
|
||||||
exclude = set(kwargs.get('exclude', []))
|
exclude = set(kwargs.get('exclude', []))
|
||||||
@ -234,7 +243,7 @@ class BaseSchema(ma.SQLAlchemyAutoSchema):
|
|||||||
|
|
||||||
# exclude default values
|
# exclude default values
|
||||||
if not context.get('full'):
|
if not context.get('full'):
|
||||||
for column in self.Meta.model.__table__.columns:
|
for column in getattr(self.Meta, 'model').__table__.columns:
|
||||||
if column.name not in exclude:
|
if column.name not in exclude:
|
||||||
self._exclude_by_value.setdefault(column.name, []).append(
|
self._exclude_by_value.setdefault(column.name, []).append(
|
||||||
None if column.default is None else column.default.arg
|
None if column.default is None else column.default.arg
|
||||||
@ -250,20 +259,48 @@ class BaseSchema(ma.SQLAlchemyAutoSchema):
|
|||||||
# init SQLAlchemyAutoSchema
|
# init SQLAlchemyAutoSchema
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
@post_dump
|
# init order
|
||||||
def _remove_skip_values(self, data, many, **kwargs): # pylint: disable=unused-argument
|
if hasattr(self.Meta, 'order'):
|
||||||
|
# use user-defined order
|
||||||
|
self._order = list(reversed(getattr(self.Meta, 'order')))
|
||||||
|
else:
|
||||||
|
# default order is: primary_key + other keys alphabetically
|
||||||
|
self._order = list(sorted(self.fields.keys()))
|
||||||
|
primary = self.opts.model.__table__.primary_key.columns.values()[0].name
|
||||||
|
self._order.remove(primary)
|
||||||
|
self._order.reverse()
|
||||||
|
self._order.append(primary)
|
||||||
|
|
||||||
|
@pre_load
|
||||||
|
def _track_import(self, data, many, **kwargs): # pylint: disable=unused-argument
|
||||||
|
call = self.context.get('callback')
|
||||||
|
if call is not None:
|
||||||
|
call(self=self, data=data, many=many, **kwargs)
|
||||||
|
return data
|
||||||
|
|
||||||
|
@post_dump
|
||||||
|
def _hide_and_order(self, data, many, **kwargs): # pylint: disable=unused-argument
|
||||||
|
|
||||||
|
# order output
|
||||||
|
for key in self._order:
|
||||||
|
try:
|
||||||
|
data.move_to_end(key, False)
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# stop early when not excluding/hiding
|
||||||
if not self._exclude_by_value and not self._hide_by_context:
|
if not self._exclude_by_value and not self._hide_by_context:
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
# exclude items or hide values
|
||||||
full = self.context.get('full')
|
full = self.context.get('full')
|
||||||
return {
|
return type(data)([
|
||||||
key: '<hidden>' if key in self._hide_by_context else value
|
(key, '<hidden>' if key in self._hide_by_context else value)
|
||||||
for key, value in data.items()
|
for key, value in data.items()
|
||||||
if full or key not in self._exclude_by_value or value not in self._exclude_by_value[key]
|
if full or key not in self._exclude_by_value or value not in self._exclude_by_value[key]
|
||||||
}
|
])
|
||||||
|
|
||||||
# TODO: remove LazyString and change model (IMHO comment should not be nullable)
|
# TODO: remove LazyStringField and change model (IMHO comment should not be nullable)
|
||||||
comment = LazyStringField()
|
comment = LazyStringField()
|
||||||
|
|
||||||
|
|
||||||
@ -336,13 +373,14 @@ class UserSchema(BaseSchema):
|
|||||||
exclude_by_value = {
|
exclude_by_value = {
|
||||||
'forward_destination': [[]],
|
'forward_destination': [[]],
|
||||||
'tokens': [[]],
|
'tokens': [[]],
|
||||||
|
'fetches': [[]],
|
||||||
'manager_of': [[]],
|
'manager_of': [[]],
|
||||||
'reply_enddate': ['2999-12-31'],
|
'reply_enddate': ['2999-12-31'],
|
||||||
'reply_startdate': ['1900-01-01'],
|
'reply_startdate': ['1900-01-01'],
|
||||||
}
|
}
|
||||||
|
|
||||||
@pre_load
|
@pre_load
|
||||||
def _handle_password(self, data, many, **kwargs): # pylint: disable=unused-argument
|
def _handle_email_and_password(self, data, many, **kwargs): # pylint: disable=unused-argument
|
||||||
data = handle_email(data)
|
data = handle_email(data)
|
||||||
if 'password' in data:
|
if 'password' in data:
|
||||||
if 'password_hash' in data or 'hash_scheme' in data:
|
if 'password_hash' in data or 'hash_scheme' in data:
|
||||||
@ -358,16 +396,23 @@ class UserSchema(BaseSchema):
|
|||||||
elif 'password_hash' in data and 'hash_scheme' in data:
|
elif 'password_hash' in data and 'hash_scheme' in data:
|
||||||
if data['hash_scheme'] not in self.Meta.model.scheme_dict:
|
if data['hash_scheme'] not in self.Meta.model.scheme_dict:
|
||||||
raise ValidationError(f'invalid password scheme {scheme!r}')
|
raise ValidationError(f'invalid password scheme {scheme!r}')
|
||||||
data['password'] = '{'+data['hash_scheme']+'}'+ data['password_hash']
|
data['password'] = f'{{{data["hash_scheme"]}}}{data["password_hash"]}'
|
||||||
del data['hash_scheme']
|
del data['hash_scheme']
|
||||||
del data['password_hash']
|
del data['password_hash']
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
# TODO: verify password (should this be done in model?)
|
||||||
|
# scheme, hashed = re.match('^(?:{([^}]+)})?(.*)$', self.password).groups()
|
||||||
|
# if not scheme...
|
||||||
|
# ctx = passlib.context.CryptContext(schemes=[scheme], default=scheme)
|
||||||
|
# try:
|
||||||
|
# ctx.verify('', hashed)
|
||||||
|
# =>? ValueError: hash could not be identified
|
||||||
|
|
||||||
tokens = fields.Nested(TokenSchema, many=True)
|
tokens = fields.Nested(TokenSchema, many=True)
|
||||||
fetches = fields.Nested(FetchSchema, many=True)
|
fetches = fields.Nested(FetchSchema, many=True)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class AliasSchema(BaseSchema):
|
class AliasSchema(BaseSchema):
|
||||||
""" Marshmallow schema for Alias model """
|
""" Marshmallow schema for Alias model """
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -381,7 +426,7 @@ class AliasSchema(BaseSchema):
|
|||||||
}
|
}
|
||||||
|
|
||||||
@pre_load
|
@pre_load
|
||||||
def _handle_password(self, data, many, **kwargs): # pylint: disable=unused-argument
|
def _handle_email(self, data, many, **kwargs): # pylint: disable=unused-argument
|
||||||
return handle_email(data)
|
return handle_email(data)
|
||||||
|
|
||||||
destination = CommaSeparatedListField()
|
destination = CommaSeparatedListField()
|
||||||
@ -408,9 +453,20 @@ class MailuSchema(Schema):
|
|||||||
class Meta:
|
class Meta:
|
||||||
""" Schema config """
|
""" Schema config """
|
||||||
render_module = RenderYAML
|
render_module = RenderYAML
|
||||||
|
ordered = True
|
||||||
|
order = ['config', 'domains', 'users', 'aliases', 'relays']
|
||||||
|
|
||||||
|
@post_dump(pass_many=True)
|
||||||
|
def _order(self, data : OrderedDict, many : bool, **kwargs): # pylint: disable=unused-argument
|
||||||
|
for key in reversed(self.Meta.order):
|
||||||
|
try:
|
||||||
|
data.move_to_end(key, False)
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
return data
|
||||||
|
|
||||||
|
config = fields.Nested(ConfigSchema, many=True)
|
||||||
domains = fields.Nested(DomainSchema, many=True)
|
domains = fields.Nested(DomainSchema, many=True)
|
||||||
relays = fields.Nested(RelaySchema, many=True)
|
|
||||||
users = fields.Nested(UserSchema, many=True)
|
users = fields.Nested(UserSchema, many=True)
|
||||||
aliases = fields.Nested(AliasSchema, many=True)
|
aliases = fields.Nested(AliasSchema, many=True)
|
||||||
config = fields.Nested(ConfigSchema, many=True)
|
relays = fields.Nested(RelaySchema, many=True)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user