Source code for invenio_records.systemfields.relations.field
# -*- coding: utf-8 -*-
#
# This file is part of Invenio.
# Copyright (C) 2020-2022 CERN.
#
# Invenio is free software; you can redistribute it and/or modify it
# under the terms of the MIT License; see LICENSE file for more details.
"""Relations system field."""
from werkzeug.utils import cached_property
from ..base import SystemField
from .mapping import RelationsMapping
from .relations import RelationBase
#
# System field
#
[docs]class RelationsField(SystemField):
"""Relations field for connections to external entities."""
[docs] def __init__(self, **fields):
"""Initialize the field."""
super().__init__()
assert all(isinstance(f, RelationBase) for f in fields.values())
self._original_fields = fields
[docs] def __getattr__(self, name):
"""Get a field definition."""
if name in self._fields:
return self._fields[name]
raise AttributeError
[docs] def __iter__(self):
"""Iterate over the configured fields."""
return iter(getattr(self, f) for f in self._fields)
[docs] def __contains__(self, name):
"""Return if a field exists in the configured fields."""
return name in self._fields
#
# Properties
#
@cached_property
def _fields(self):
"""Get the fields."""
return self._original_fields
#
# Helpers
#
[docs] def obj(self, instance):
"""Get the relations object."""
# Check cache
obj = self._get_cache(instance)
if obj:
return obj
obj = RelationsMapping(record=instance, fields=self._fields)
self._set_cache(instance, obj)
return obj
#
# Data descriptor
#
[docs] def __get__(self, record, owner=None):
"""Accessing the attribute."""
# Class access
if record is None:
return self
return self.obj(record)
[docs] def __set__(self, instance, values):
"""Setting the attribute."""
obj = self.obj(instance)
for k, v in values.items():
setattr(obj, k, v)
#
# Record extension
#
[docs] def pre_commit(self, record):
"""Initialise the model field."""
self.obj(record).validate()
self.obj(record).clean()
[docs]class MultiRelationsField(RelationsField):
"""Relations field for connections to external entities.
It allows to define nested relation fields. For example:
.. code-block:: python
class Record:
relations = MultiRelationsField(
field_one=PIDListRelation(
"metadata.field_one",
...
),
inner=RelationsField(
inner_field=PIDListRelation(
"metadata.inner_field",
...
),
)
)
"""
[docs] def __init__(self, **fields):
"""Initialize the field.
The nested RelationFields will be flattened to the root.
In the example above, the relations field has a field (field_one)
and a nested RelationsField with a field (inner_field). However,
both of them will be accessed through the relations field.
.. code-block:: python
relations.field_one
relations.inner_field # correct
relations.inner.inner_field # incorrect
"""
super().__init__() # no fields passed since validation happens in this class
assert all(
isinstance(f, RelationBase) or isinstance(f, RelationsField)
for f in fields.values()
)
self._original_fields = fields
self._relation_fields = set()
@cached_property
def _fields(self):
"""Mutates self._fields to include all nested fields."""
fields = {}
self._relation_fields = set()
for key, field in self._original_fields.items():
if isinstance(field, RelationBase):
fields[key] = field
elif isinstance(field, RelationsField):
self._relation_fields.add(key)
for inner_name, inner_field in field._fields.items():
if inner_name not in fields:
fields[inner_name] = inner_field
return fields
#
# Data descriptor
#
[docs] def __set__(self, instance, values):
"""Setting the attribute."""
obj = self.obj(instance)
for k, v in values.items():
if k in self._relation_fields:
for kk, vv in v.items():
setattr(obj, kk, vv)
else:
setattr(obj, k, v)