Source code for invenio_records.systemfields.relations.modelrelations
# -*- coding: utf-8 -*-
#
# This file is part of Invenio.
# Copyright (C) 2020-2021 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.
"""Model relations.
A model relation relies on database-level foreign keys to define a relationship
instead of injecting the IDs inside the record dictionary. On indexing, the
specified attributes from the related record are dumped into the index.
.. code-block:: python
# Your database model defines a foreign key to the related model.
class MyRecordModel(db.Model, RecordMetadataBase):
# FK:
user_id = db.Column(UserModel.id, ..., db.ForeignKey(...))
class MyRecord(Record)
# The database model used by the record.
model_cls = MyRecordModel
# System field defining the related_id as a model attribute (and
# ensuring the id is dumped to the index).
user_id = ModelField("user_id")
# System field defining the relation
relations = RelationsField(
related=ModelRelation(
# The related record class.
UserRecord,
# The attribute name of the ModelField.
'user_id',
# Top-level key to dump in index
'user',
# Attributes to dump (in addition to id)
keys=['email', 'username', 'profile'],
)
"""
from .errors import InvalidRelationValue
from .relations import RelationBase
from .results import RelationResult
class ModelRelationResult(RelationResult):
"""Result class for a model relation."""
def _lookup_id(self):
return getattr(self.record, self.field._model_field_name)
def validate(self):
"""Validate relation."""
# The relation is validated via database-level foreign key constraints.
return True
def dereference(self, keys=None, attrs=None):
"""Dereference the relation field object inside the record."""
# Prepare the record dictionary to look like: {'id': ...}
if self.field.key not in self.record:
id_ = self._lookup_id()
if id_ is None:
return None
else:
self.record[self.field.key] = {self.field._value_key_suffix: str(id_)}
data = self.record[self.field.key]
return self._dereference_one(data, keys or self.keys, attrs or self.attrs)
def clean(self, keys=None, attrs=None):
"""Clean the record."""
# Don't store anything inside the record committed to the database.
self.record.pop(self.field.key, None)
[docs]class ModelRelation(RelationBase):
"""Define a relation stored as a foreign key on the record's model."""
result_cls = ModelRelationResult
[docs] def __init__(self, record_cls, model_field_name, key, keys=None, attrs=None):
"""Constructor."""
self._record_cls = record_cls
self._model_field_name = model_field_name
super().__init__(key=key, keys=keys, attrs=attrs)
[docs] def resolve(self, id_):
"""Get the related object."""
return self._record_cls.get_record(id_)
[docs] def parse_value(self, value):
"""Extract id from object being set on relation."""
if isinstance(value, self._record_cls):
return value.id
elif isinstance(value, self._record_cls.model_cls):
return value.id
else:
raise InvalidRelationValue(
f"Invalid value. Expected {self._record_cls} or "
f"{self._record_cls.model_cls}"
)
[docs] def set_value(self, record, value):
"""Set an related object."""
store_value = self.parse_value(value)
# The existence of the related object is checked via database-level
# foreign key constraints.
setattr(record, self._model_field_name, store_value)
[docs] def get_value(self, record):
"""Return the resolved relation from a record."""
return self.result_cls(self, record)
[docs] def clear_value(self, record):
"""Clear the relation."""
setattr(record, self._model_field_name, None)