Skip to content

Add parameter to pass in a list of Role names for use in export #295

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions docs/usage/exporting.rst
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,44 @@ results without having to specify ``role`` in the export function.
}]
}

Optionally a list of roles can be provided for export instead. The combination
of the all the roles will be made and used to filter fields. Blacklist roles
override whitelist roles in this case.

::

class User(Model):
id = IntType(default=42)
name = StringType()
email = StringType()
password = StringType()

class Options:
roles = {
'public': whitelist('id', 'name', 'email'),
'admin': whitelist('password')
'no_private': blacklist('password', 'email')
}

The ``password`` field will combined with the ``id``, ``name`` and ``email``
fields when ``roles`` is set to both ``public`` and ``admin``.

>>> user.to_primitive(roles=['public', 'admin'])
{
'id': 42,
'name': 'Arthur',
'email': '[email protected]',
'password': 'dolphins'
}

Adding in the ``no_private`` role will then blacklist the ``email`` and
``password`` fields.

>>> user.to_primitive(roles=['public', 'admin', 'no_private'])
{
'id': 42,
'name': 'Arthur'
}


.. _exporting_serializable:
Expand Down
8 changes: 4 additions & 4 deletions schematics/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,18 +292,18 @@ def convert(self, raw_data, **kw):
def to_native(self, role=None, context=None):
return to_native(self.__class__, self, role=role, context=context)

def to_primitive(self, role=None, context=None):
def to_primitive(self, role=None, context=None, roles=None):
"""Return data as it would be validated. No filtering of output unless
role is defined.

:param role:
Filter output by a specific role

"""
return to_primitive(self.__class__, self, role=role, context=context)
return to_primitive(self.__class__, self, role=role, context=context, roles=roles)

def serialize(self, role=None, context=None):
return self.to_primitive(role=role, context=context)
def serialize(self, role=None, context=None, roles=None):
return self.to_primitive(role=role, context=context, roles=roles)

def flatten(self, role=None, prefix=""):
"""
Expand Down
60 changes: 52 additions & 8 deletions schematics/transforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ def import_loop(cls, instance_or_dict, field_converter, context=None,


def export_loop(cls, instance_or_dict, field_converter,
role=None, raise_error_on_role=False, print_none=False):
role=None, raise_error_on_role=False, print_none=False, roles=None):
"""
The export_loop function is intended to be a general loop definition that
can be used for any form of data shaping, such as application of roles or
Expand All @@ -144,12 +144,24 @@ def export_loop(cls, instance_or_dict, field_converter,
:param print_none:
This function overrides ``serialize_when_none`` values found either on
``cls`` or an instance.
:param roles:
A list of roles, will determine a dynamic role aggregated from a list of
individual roles. Blacklisted fields will override whitelisted fields.
Supplying this parameter supersedes the role parameter.
"""
data = {}

# Translate `role` into `gottago` function
gottago = wholelist()
if hasattr(cls, '_options') and role in cls._options.roles:

if hasattr(cls, '_options') and roles:
if raise_error_on_role:
for r in roles:
if not cls._options.roles.get(r, None):
error_msg = u'%s Model has no role "%s"'
raise ValueError(error_msg % (cls.__name__, r))
gottago = combinedlist([cls._options.roles.get(r, None) for r in roles])
elif hasattr(cls, '_options') and role in cls._options.roles:
gottago = cls._options.roles[role]
elif role and raise_error_on_role:
error_msg = u'%s Model has no role "%s"'
Expand Down Expand Up @@ -374,6 +386,37 @@ def blacklist(*field_list):
return Role(Role.blacklist, field_list)


def combinedlist(roles):
"""
Returns a function that operates as a whitelist for the combined set of
fields in each of the specified roles. Blacklisted fields will override
whitelisted fields.

:param roles: list of Roles
"""
combined_roles = None

for role in roles:
if role and ((hasattr(role.function, "func_name") and
role.function.func_name == "whitelist")
or (hasattr(role.function, "__name__") and
role.function.__name__ == "whitelist")):
if combined_roles:
combined_roles = combined_roles + role
else:
combined_roles = role
if not combined_roles:
combined_roles = wholelist()
for role in roles:
if role and ((hasattr(role.function, "func_name") and
role.function.func_name == "blacklist")
or (hasattr(role.function, "__name__") and
role.function.__name__ == "blacklist")):
combined_roles = combined_roles - role

return combined_roles


###
# Import and export functions
###
Expand All @@ -393,16 +436,16 @@ def field_converter(field, value, mapping=None):


def to_native(cls, instance_or_dict, role=None, raise_error_on_role=True,
context=None):
context=None, roles=None):
field_converter = lambda field, value: field.to_native(value,
context=context)
data = export_loop(cls, instance_or_dict, field_converter,
role=role, raise_error_on_role=raise_error_on_role)
role=role, raise_error_on_role=raise_error_on_role, roles=roles)
return data


def to_primitive(cls, instance_or_dict, role=None, raise_error_on_role=True,
context=None):
context=None, roles=None):
"""
Implements serialization as a mechanism to convert ``Model`` instances into
dictionaries keyed by field_names with the converted data as the values.
Expand All @@ -422,18 +465,19 @@ def to_primitive(cls, instance_or_dict, role=None, raise_error_on_role=True,
:param raise_error_on_role:
This parameter enforces strict behavior which requires substructures
to have the same role definition as their parent structures.
:param roles: list of Roles to combine and use to filter fields.
"""
field_converter = lambda field, value: field.to_primitive(value,
context=context)
data = export_loop(cls, instance_or_dict, field_converter,
role=role, raise_error_on_role=raise_error_on_role)
role=role, raise_error_on_role=raise_error_on_role, roles=roles)
return data


def serialize(cls, instance_or_dict, role=None, raise_error_on_role=True,
context=None):
context=None, roles=None):
return to_primitive(cls, instance_or_dict, role, raise_error_on_role,
context)
context, roles=roles)


EMPTY_LIST = "[]"
Expand Down
75 changes: 75 additions & 0 deletions tests/test_serialize.py
Original file line number Diff line number Diff line change
Expand Up @@ -911,3 +911,78 @@ class Options:
},
]
}


def test_multiple_roles():

class Child(Model):
id = IntType(default=84)
name = StringType()
ssn = StringType()

class Options:
roles = {
'create': whitelist('name', 'ssn'),
'public': whitelist('id', 'name'),
'no_private': blacklist('ssn')
}

class User(Model):
id = IntType(default=42)
name = StringType()
email = StringType()
password = StringType()
child = ModelType(Child)

class Options:
roles = {
'create': whitelist('name', 'email', 'password', 'child'),
'public': whitelist('id', 'name', 'email'),
'no_private': blacklist('email', 'password', 'child')
}

child = Child({'name': 'Random', 'ssn': '000-11-1212'})
user = User({'name': 'Arthur', 'email': '[email protected]',
'password': 'dolphins', 'child': child})

d = user.serialize(roles=['public'])

assert d == {
'id': 42,
'name': 'Arthur',
'email': '[email protected]'
}

d = user.serialize(roles=['public', 'create'])

assert d == {
'id': 42,
'name': 'Arthur',
'email': '[email protected]',
'password': 'dolphins',
'child': {
'id': 84,
'name': 'Random',
'ssn': '000-11-1212'
}
}

d = user.serialize(roles=['public', 'create', 'no_private'])

assert d == {
'id': 42,
'name': 'Arthur'
}

try:
user.serialize(roles=['public', 'create', 'NOT_A_ROLE'])
except ValueError as ve:
assert ve.args[0] == 'User Model has no role "NOT_A_ROLE"'

d = user.to_primitive(roles=['public', 'create', 'no_private'])

assert d == {
'id': 42,
'name': 'Arthur'
}