From 16d2e33ea84af21525179779febed1b2102337b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johnny=20Marie=CC=81thoz?= Date: Wed, 21 Apr 2021 14:24:59 +0200 Subject: [PATCH 1/2] view: change the view routes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Adds document and project detailed view in the invenio-records-ui configuration. * Adds permissions to the detailed, preview and file document views to limit the access to the public documents. * Adds front page route and the global jinja filters to the main extension application. * Adds a new werkzeug converter to limit the view matched routes to the existing organisations. Co-Authored-by: Johnny Mariéthoz --- sonar/config.py | 21 ++++- sonar/ext.py | 18 +++- sonar/modules/documents/permissions.py | 9 ++ .../documents/templates/documents/record.html | 2 +- sonar/modules/documents/views.py | 93 +++++-------------- sonar/resources/projects/views.py | 50 ++++++++++ sonar/route_converters.py | 46 +++++++++ sonar/theme/templates/sonar/frontpage.html | 2 +- .../theme/templates/sonar/partial/navbar.html | 2 +- .../templates/sonar/partial/organisation.html | 2 +- sonar/theme/views.py | 4 +- tests/api/users/test_users_query.py | 2 +- tests/ui/documents/test_documents_views.py | 38 ++++---- tests/ui/test_utils.py | 42 ++++----- 14 files changed, 208 insertions(+), 123 deletions(-) create mode 100644 sonar/resources/projects/views.py create mode 100644 sonar/route_converters.py diff --git a/sonar/config.py b/sonar/config.py index 56620fdd..0d6286b7 100644 --- a/sonar/config.py +++ b/sonar/config.py @@ -259,17 +259,27 @@ def _(x): SECURITY_REGISTER_USER_TEMPLATE = 'sonar/accounts/signup.html' RECORDS_UI_ENDPOINTS = { + 'doc': { + 'pid_type': 'doc', + 'route': '//documents/', + 'view_imp': 'sonar.modules.documents.views:detail', + 'record_class': 'sonar.modules.documents.api:DocumentRecord', + 'template': 'documents/record.html', + 'permission_factory_imp': 'sonar.modules.documents.permissions:only_public' + }, 'doc_previewer': { 'pid_type': 'doc', 'route': '/documents//preview/', 'view_imp': 'invenio_previewer.views:preview', - 'record_class': 'sonar.modules.documents.api:DocumentRecord' + 'record_class': 'sonar.modules.documents.api:DocumentRecord', + 'permission_factory_imp': 'sonar.modules.documents.permissions:only_public' }, 'doc_files': { 'pid_type': 'doc', 'route': '/documents//files/', 'view_imp': 'invenio_records_files.utils:file_download_ui', - 'record_class': 'invenio_records_files.api:Record' + 'record_class': 'invenio_records_files.api:Record', + 'permission_factory_imp': 'sonar.modules.documents.permissions:only_public' }, 'depo_previewer': { 'pid_type': 'depo', @@ -295,6 +305,13 @@ def _(x): 'view_imp': 'invenio_records_files.utils:file_download_ui', 'record_class': 'invenio_records_files.api:Record' }, + 'proj': { + 'pid_type': 'proj', + 'route': '//projects/', + 'view_imp': 'sonar.resources.projects.views:detail', + 'record_class': 'sonar.resources.projects.api:Record', + 'template': 'sonar/projects/detail.html' + } } """Records UI for sonar.""" diff --git a/sonar/ext.py b/sonar/ext.py index bbbc8e05..bee930e3 100644 --- a/sonar/ext.py +++ b/sonar/ext.py @@ -20,7 +20,7 @@ from __future__ import absolute_import, print_function import jinja2 -from flask import current_app +from flask import current_app, render_template from flask_bootstrap import Bootstrap from flask_security import user_registered from flask_wiki import Wiki @@ -41,6 +41,7 @@ RecordService as ProjectRecordService from . import config_sonar +from .route_converters import OrganisationCodeConverter def utility_processor(): @@ -77,6 +78,7 @@ def init_app(self, app): """Flask application initialization.""" self.init_config(app) self.create_resources() + self.init_views(app) app.extensions['sonar'] = self @@ -105,6 +107,20 @@ def init_config(self, app): if k.startswith('SONAR_APP_'): app.config.setdefault(k, getattr(config_sonar, k)) + def init_views(self, app): + """Initialize the main flask views.""" + app.url_map.converters['org_code'] = OrganisationCodeConverter + + @app.route('/') + def index(view): + """Homepage.""" + return render_template('sonar/frontpage.html') + + @app.template_filter() + def nl2br(string): + r"""Replace \n to
.""" + return string.replace('\n', '
') + def create_resources(self): """Create resources.""" # Initialize the project resource with the corresponding service. diff --git a/sonar/modules/documents/permissions.py b/sonar/modules/documents/permissions.py index 0cbb81a8..322f3074 100644 --- a/sonar/modules/documents/permissions.py +++ b/sonar/modules/documents/permissions.py @@ -112,3 +112,12 @@ def delete(cls, user, record): # Same rules as read return cls.read(user, record) + +def only_public(record, *args, **kwargs): + """Allow access only for public tagged documents.""" + + def can(self): + if record.get('hiddenFromPublic'): + return False + return True + return type('OnlyPublicDocument', (), {'can': can})() diff --git a/sonar/modules/documents/templates/documents/record.html b/sonar/modules/documents/templates/documents/record.html index 4f16bad5..81d51548 100644 --- a/sonar/modules/documents/templates/documents/record.html +++ b/sonar/modules/documents/templates/documents/record.html @@ -186,7 +186,7 @@
    {% for project in record.projects %}
  • - + {{ project.name }} {%- if project.funding_organisations -%} diff --git a/sonar/modules/documents/views.py b/sonar/modules/documents/views.py index e950c013..44509766 100644 --- a/sonar/modules/documents/views.py +++ b/sonar/modules/documents/views.py @@ -40,7 +40,7 @@ __name__, template_folder='templates', static_folder='static', - url_prefix='/') + url_prefix='/') """Blueprint used for loading templates and static assets The sole purpose of this blueprint is to ensure that Invenio can find the @@ -49,56 +49,23 @@ """ -@blueprint.url_defaults -def default_view_code(endpoint, values): - """Add default view code.""" - values.setdefault('view', - current_app.config.get('SONAR_APP_DEFAULT_ORGANISATION')) - - -@blueprint.before_request -def store_organisation(): - """Add organisation record to global variables.""" - view = request.view_args.get( - 'view', current_app.config.get('SONAR_APP_DEFAULT_ORGANISATION')) - - if view != current_app.config.get('SONAR_APP_DEFAULT_ORGANISATION'): - organisation = OrganisationRecord.get_record_by_pid(view) - - if not organisation or not organisation.get('isShared'): - abort(404) - - g.organisation = organisation.dumps() - - -@blueprint.route('/') -def index(view='global'): - """Homepage.""" - return render_template('sonar/frontpage.html') - - -@blueprint.route('/search/documents') -def search(view='global'): +@blueprint.route('//search/documents') +def search(view): """Search results page.""" return render_template('sonar/search.html') -@blueprint.route('/documents/') -def detail(pid_value, view='global'): - """Document detail page.""" - record = DocumentRecord.get_record_by_pid(pid_value) - - if not record or record.get('hiddenFromPublic'): - abort(404) +def detail(pid, record, template=None, **kwargs): + r"""Document detailed view. - # Send signal when record is viewed - pid = PersistentIdentifier.get('doc', pid_value) - record_viewed.send( - current_app._get_current_object(), - pid=pid, - record=record, - ) + Sends record_viewed signal and renders template. + :param pid: PID object. + :param record: Record object. + :param template: Template to render. + :param \*\*kwargs: Additional view arguments based on URL rule. + :returns: The rendered template. + """ # Add restriction, link and thumbnail to files if record.get('_files'): # Check if organisation's record forces to point file to an external @@ -122,38 +89,20 @@ def detail(pid_value, view='global'): # Resolve $ref properties record = record.replace_refs() - return render_template('documents/record.html', - pid=pid_value, + # Send signal when record is viewed + record_viewed.send( + current_app._get_current_object(), + pid=pid, + record=record, + ) + + return render_template(template, + pid=pid, record=record, schema_org_data=schema_org_data, google_scholar_data=google_scholar_data) -@blueprint.route('/projects/') -def project_detail(pid_value, view='global'): - """Project detail view. - - :param pid_value: Project PID. - :param view: Organisation's view. - :returns: Rendered template. - """ - try: - service = sonar.service('projects') - result = service.result_item(service, g.identity, - service.record_cls.pid.resolve(pid_value)) - except Exception: - abort(404) - - return render_template('sonar/projects/detail.html', - pid=pid_value, - record=result.data['metadata']) - - -@blueprint.app_template_filter() -def nl2br(string): - r"""Replace \n to
    .""" - return string.replace('\n', '
    ') - @blueprint.app_template_filter() def title_format(title, language): diff --git a/sonar/resources/projects/views.py b/sonar/resources/projects/views.py new file mode 100644 index 00000000..7c7906d1 --- /dev/null +++ b/sonar/resources/projects/views.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +# +# Swiss Open Access Repository +# Copyright (C) 2021 RERO +# +# 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 . + +"""Projects views.""" + +from flask import current_app, g, render_template +from invenio_records_ui.signals import record_viewed + +from sonar.proxies import sonar + + +def detail(pid, record, template=None, **kwargs): + r"""Project detail view. + + Sends record_viewed signal and renders template. + + :param pid: PID object. + :param record: Record object. + :param template: Template to render. + :param \*\*kwargs: Additional view arguments based on URL rule. + :returns: The rendered template. + """ + service = sonar.service('projects') + item = service.result_item(service, g.identity, + record) + + # Send signal when record is viewed + record_viewed.send( + current_app._get_current_object(), + pid=pid, + record=record, + ) + + return render_template(template, + pid=pid, + record=item.data['metadata']) diff --git a/sonar/route_converters.py b/sonar/route_converters.py new file mode 100644 index 00000000..9787445c --- /dev/null +++ b/sonar/route_converters.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# +# Swiss Open Access Repository +# Copyright (C) 2021 RERO +# +# 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 . + +"""Werkzeug Route Converters.""" + +from flask import current_app, g, request +from werkzeug.routing import BaseConverter, ValidationError + +from .modules.organisations.api import OrganisationRecord + + +class OrganisationCodeConverter(BaseConverter): + """Werkzeug Organistaion code converter.""" + + # any word + regex = r"\w+" + + def to_python(self, value): + """Check that the value is a known organisation view code. + + :param value: the URL param value. + :returns: the URL param value. + """ + if g.get('organisation'): + g.pop('organisation') + if value == current_app.config.get('SONAR_APP_DEFAULT_ORGANISATION'): + return value + organisation = OrganisationRecord.get_record_by_pid(value) + if not organisation or not organisation.get('isShared'): + raise ValidationError + g.organisation = organisation.dumps() + return value diff --git a/sonar/theme/templates/sonar/frontpage.html b/sonar/theme/templates/sonar/frontpage.html index 5346decd..0607cc6d 100755 --- a/sonar/theme/templates/sonar/frontpage.html +++ b/sonar/theme/templates/sonar/frontpage.html @@ -26,7 +26,7 @@
    + href="{{ url_for('index', view=view_code if g.get('organisation', {}).get('isDedicated') else config.SONAR_APP_DEFAULT_ORGANISATION) }}"> {% if g.get('organisation', {}).get('isDedicated') %} {% set thumbnail = g.organisation | record_image_url %} {% if thumbnail %} diff --git a/sonar/theme/templates/sonar/partial/navbar.html b/sonar/theme/templates/sonar/partial/navbar.html index 1db8c77c..2dc2d13b 100644 --- a/sonar/theme/templates/sonar/partial/navbar.html +++ b/sonar/theme/templates/sonar/partial/navbar.html @@ -18,7 +18,7 @@