Skip to content

Commit

Permalink
templates: use marshmallow for API control
Browse files Browse the repository at this point in the history
* Creates marshmallow schema for template resources.
* Controls "public" template creation through REST API.

Co-Authored-by: Renaud Michotte <renaud.michotte@gmail.com>
  • Loading branch information
zannkukai committed Jan 25, 2023
1 parent e167fd7 commit 7f660d2
Show file tree
Hide file tree
Showing 14 changed files with 475 additions and 206 deletions.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ patron_transaction_event_id = "rero_ils.modules.patron_transaction_events.api:pa
patron_transaction_id = "rero_ils.modules.patron_transactions.api:patron_transaction_id_fetcher"
patron_type_id = "rero_ils.modules.patron_types.api:patron_type_id_fetcher"
stat_id = "rero_ils.modules.stats.api:stat_id_fetcher"
template_id = "rero_ils.modules.templates.api:template_id_minter"
template_id = "rero_ils.modules.templates.api:template_id_fetcher"
vendor_id = "rero_ils.modules.vendors.api:vendor_id_fetcher"

[tool.poetry.plugins."invenio_pidstore.minters"]
Expand Down
3 changes: 1 addition & 2 deletions rero_ils/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,6 @@
from .modules.selfcheck.permissions import seflcheck_permission_factory
from .modules.stats.api import Stat
from .modules.stats.permissions import StatisticsPermissionPolicy
from .modules.templates.api import Template
from .modules.templates.permissions import TemplatePermissionPolicy
from .modules.users.api import get_profile_countries, \
get_readonly_profile_fields
Expand Down Expand Up @@ -1532,7 +1531,7 @@ def _(x):
'application/json': 'rero_ils.modules.serializers:json_v1_search'
},
record_loaders={
'application/json': lambda: Template(request.get_json()),
'application/json': 'rero_ils.modules.templates.loaders:json_v1'
},
list_route='/templates/',
record_class='rero_ils.modules.templates.api:Template',
Expand Down
56 changes: 56 additions & 0 deletions rero_ils/modules/commons/schemas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# -*- coding: utf-8 -*-
#
# RERO ILS
# Copyright (C) 2019-2023 RERO
# Copyright (C) 2019-2023 UCLouvain
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

"""Common marshmallow schema for RERO-ILS project."""

from functools import wraps

from flask import request
from invenio_records_rest.schemas.fields import SanitizedUnicode
from marshmallow import Schema
from marshmallow.validate import Length


def http_applicable_method(*http_methods):
"""Skip the decorated function if HTTP request method isn't applicable.
:param http_methods: the list of HTTP method for which the decorated
function will be applicable. If request method isn't in this list, the
decorated function will be skipped/uncalled.
"""
def inner(func):
@wraps(func)
def wrapper(*args, **kwargs):
if request.method in http_methods:
return func(*args, **kwargs)
return wrapper
return inner


class RefSchema(Schema):
"""Schema to describe a reference to another resources."""

# TODO : find a way to validate the `$ref` using a variable pattern.
ref = SanitizedUnicode(data_key='$ref', attribute='$ref')


class NoteSchema(Schema):
"""Schema to describe a note."""

type = SanitizedUnicode()
content = SanitizedUnicode(validate=Length(1, 2000))
8 changes: 4 additions & 4 deletions rero_ils/modules/documents/loaders/marcxml.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,13 @@ def marcxml_marshmallow_loader():
"""
marcxml_records = split_stream(BytesIO(request.data))
number_of_xml_records = 0
josn_record = {}
json_record = {}
for marcxml_record in marcxml_records:
marc21json_record = create_record(marcxml_record)
josn_record = marc21.do(marc21json_record)
json_record = marc21.do(marc21json_record)
# converted records are considered as draft
josn_record['_draft'] = True
json_record['_draft'] = True
if number_of_xml_records > 0:
abort(400)
number_of_xml_records += 1
return josn_record
return json_record
4 changes: 2 additions & 2 deletions rero_ils/modules/patrons/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,8 @@ def logged_user():
patron['organisation'] = patron.organisation.dumps(
dumper=OrganisationLoggedUserDumper())
patron['libraries'] = [
{'pid': extracted_data_from_ref(library)}
for library in patron.get('libraries', [])
{'pid': pid}
for pid in patron.manageable_library_pids
]
data['patrons'].append(patron)

Expand Down
7 changes: 7 additions & 0 deletions rero_ils/modules/serializers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,19 @@
"""RERO ILS record serialization."""

from flask import json, request
from invenio_jsonschemas import current_jsonschemas
from invenio_records_rest.serializers.json import \
JSONSerializer as _JSONSerializer

from .mixins import PostprocessorMixin


def schema_from_context(_, context, data, schema):
"""Get the record's schema from context."""
record = (context or {}).get('record', {})
return record.get('$schema', current_jsonschemas.path_to_url(schema))


class JSONSerializer(_JSONSerializer, PostprocessorMixin):
"""Serializer for RERO-ILS records as JSON."""

Expand Down
7 changes: 3 additions & 4 deletions rero_ils/modules/templates/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@
from rero_ils.modules.providers import Provider
from rero_ils.modules.utils import extracted_data_from_ref

from .extensions import CleanDataDictExtension, \
TemplateVisibilityChangesExtension
from .extensions import CleanDataDictExtension
from .models import TemplateIdentifier, TemplateMetadata, TemplateVisibility

# provider
Expand Down Expand Up @@ -59,14 +58,14 @@ class Template(IlsRecord):
"""Templates class."""

_extensions = [
CleanDataDictExtension(),
TemplateVisibilityChangesExtension()
CleanDataDictExtension()
]

minter = template_id_minter
fetcher = template_id_fetcher
provider = TemplateProvider
model_cls = TemplateMetadata
schema = 'templates/template-v0.0.1.json'
pids_exist_check = {
'required': {
'org': 'organisation',
Expand Down
112 changes: 58 additions & 54 deletions rero_ils/modules/templates/extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,73 +16,77 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.

"""Template record extensions."""
from flask_login import current_user
from invenio_records.extensions import RecordExtension
from jsonschema.exceptions import ValidationError

from rero_ils.modules.users.models import UserRole


class CleanDataDictExtension(RecordExtension):
"""Defines the methods needed by an extension."""

def post_init(self, record, data, model=None, **kwargs):
"""Called after a record is initialized.
fields_to_clean = {
'documents': [
'pid'
],
'items': [
'pid',
'barcode',
'status',
'document',
'holding',
'organisation',
'library'
],
'holdings': [
'pid',
'organisation',
'library',
'document'
],
'patrons': [
'pid',
'user_id',
'patron.subscriptions',
'patron.barcode'
]
}

def _clean_record(self, record):
"""Remove fields that can have a link to other records in the database.
Remove fields that can have a link to other records in the database.
When storing a `Template`, we don't want to store possible residual
links to other resource. Fields to clean depend on `Template` type.
:param record: the record to analyze
:param data: The dict passed to the record's constructor
:param model: The model class used for initialization.
"""
fields = ['pid']
if record.get('template_type') == 'items':
fields += ['barcode', 'status', 'document', 'holding',
'organisation', 'library']

elif record.get('template_type') == 'holdings':
fields += ['organisation', 'library', 'document']
elif record.get('template_type') == 'patrons':
fields += ['user_id', 'patron.subscriptions', 'patron.barcode']
def _clean(data, keys):
"""Inner recursive function to clean data (allow dotted path).
for field in fields:
if '.' in field:
level_1, level_2 = field.split('.')
record.get('data', {}).get(level_1, {}).pop(level_2, None)
else:
record.get('data', {}).pop(field, None)
:param data: the dictionary to clean.
:param keys: the list of key to clean into the dictionary.
"""
if not data:
return
for key in keys:
if '.' in key:
root_path, child_path = key.split('.', 1)
_clean(data.get(root_path, {}), [child_path])
else:
data.pop(key, None)


class TemplateVisibilityChangesExtension(RecordExtension):
"""Disable template visibility changes depending on connected user."""

def pre_commit(self, record):
"""Called before a record is committed.
:param record: the record containing data to validate.
:raises ValidationError: If an error is detected during the validation
check. This error could be serialized to get the error message.
"""
# First, determine if a user is connected. If not, no check must be
# done about any changes (probably it's a console script/user).
from rero_ils.modules.patrons.api import current_librarian
if not current_user:
if not record.get('data'):
return
if fields := self.fields_to_clean.get(record.get('template_type')):
_clean(record['data'], fields)

# Check if visibility of the template changed. If not, we can stop
# the validation process.
original_record = record.db_record() or {}
if record.get('visibility') == original_record.get('visibility'):
return
def pre_commit(self, record):
"""Called before a record is committed."""
self._clean_record(record)

# Only lib_admin and full_permission roles can change visibility field
error_message = "You are not allowed to change template visibility"
allowed_roles = [
UserRole.FULL_PERMISSIONS,
UserRole.LIBRARY_ADMINISTRATOR
]
user_roles = set()
if current_librarian:
user_roles = set(current_librarian.get('roles'))
if not user_roles.intersection(allowed_roles):
raise ValidationError(error_message)
def pre_create(self, record):
"""Called before a record is created."""
self._clean_record(record)
# DEV NOTE :: we need to update the model to store record modification
# into the database ; otherwise this is the original data that will
# be stored into database.
if record.model:
record.model.data = record
28 changes: 28 additions & 0 deletions rero_ils/modules/templates/loaders/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
#
# RERO ILS
# Copyright (C) 2019-2022 RERO
# Copyright (C) 2019-2022 UCLouvain
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

"""Loaders for `Template` resources."""
from invenio_records_rest.loaders.marshmallow import marshmallow_loader

from ..schemas.json import TemplateMetadataSchemaV1

json_v1 = marshmallow_loader(TemplateMetadataSchemaV1)

__all__ = (
'json_v1',
)
19 changes: 19 additions & 0 deletions rero_ils/modules/templates/schemas/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
#
# RERO ILS
# Copyright (C) 2019-2022 RERO
# Copyright (C) 2019-2022 UCLouvain
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

"""Templates schemas."""
Loading

0 comments on commit 7f660d2

Please sign in to comment.