From d77a180ebd902edd066f5fd10aaa2a3a17b87fdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karl-Ulrich=20Kr=C3=A4gelin?= Date: Mon, 13 May 2024 14:45:09 +0200 Subject: [PATCH 01/38] register new route --- invenio_vocabularies/resources/resource.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/invenio_vocabularies/resources/resource.py b/invenio_vocabularies/resources/resource.py index ee10a685..2ec848a8 100644 --- a/invenio_vocabularies/resources/resource.py +++ b/invenio_vocabularies/resources/resource.py @@ -54,7 +54,12 @@ class VocabulariesResourceConfig(RecordResourceConfig): blueprint_name = "vocabularies" url_prefix = "/vocabularies" - routes = {"list": "/", "item": "//", "tasks": "/tasks"} + routes = { + "list": "/", + "item": "//", + "tasks": "/tasks", + "all": "/", + } request_view_args = { "pid_value": ma.fields.Str(), @@ -89,8 +94,17 @@ def create_url_rules(self): rules.append( route("POST", routes["tasks"], self.launch), ) + # Add "vocabularies/" route + rules.append( + route("GET", routes["all"], self.get_all), + ) return rules + @response_handler(many=True) + def get_all(self): + """Get all items.""" + return {"status": 200, "message": "hello world!"}, 200 + @request_search_args @request_view_args @response_handler(many=True) From 0b5aaaa42ac7b439c678eaa62ae3e2adb28fde32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karl-Ulrich=20Kr=C3=A4gelin?= Date: Tue, 14 May 2024 10:57:49 +0200 Subject: [PATCH 02/38] Implemented first Admin view based on mock API Co-authored-by: mkloeppe --- .../administration/__init__.py | 9 ++++ .../administration/views/__init__.py | 9 ++++ .../administration/views/vocabularies.py | 38 ++++++++++++++++ invenio_vocabularies/config.py | 18 ++++++++ invenio_vocabularies/ext.py | 6 ++- invenio_vocabularies/resources/resource.py | 43 ++++++++++++++++++- .../vocabularies-list.html | 12 ++++++ setup.cfg | 2 + 8 files changed, 134 insertions(+), 3 deletions(-) create mode 100644 invenio_vocabularies/administration/__init__.py create mode 100644 invenio_vocabularies/administration/views/__init__.py create mode 100644 invenio_vocabularies/administration/views/vocabularies.py create mode 100644 invenio_vocabularies/templates/semantic-ui/invenio_vocabularies/vocabularies-list.html diff --git a/invenio_vocabularies/administration/__init__.py b/invenio_vocabularies/administration/__init__.py new file mode 100644 index 00000000..b9a61ac0 --- /dev/null +++ b/invenio_vocabularies/administration/__init__.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2023 ULB Münster. +# +# invenio-oaiharvest-config is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + +"""Invenio administration views module for vocabularies.""" diff --git a/invenio_vocabularies/administration/views/__init__.py b/invenio_vocabularies/administration/views/__init__.py new file mode 100644 index 00000000..67cac69b --- /dev/null +++ b/invenio_vocabularies/administration/views/__init__.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2023 ULB Münster. +# +# invenio-oaiharvest-config is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + +"""Invenio administration views module for OAI Harvester.""" diff --git a/invenio_vocabularies/administration/views/vocabularies.py b/invenio_vocabularies/administration/views/vocabularies.py new file mode 100644 index 00000000..2a20f23a --- /dev/null +++ b/invenio_vocabularies/administration/views/vocabularies.py @@ -0,0 +1,38 @@ +from functools import partial + +from flask import current_app +from invenio_administration.views.base import ( + AdminResourceDetailView, + AdminResourceListView, +) +from invenio_i18n import lazy_gettext as _ +from invenio_search_ui.searchconfig import search_app_config + + +class VocabulariesListView(AdminResourceListView): + """Configuration for OAI-PMH sets list view.""" + + api_endpoint = "/vocabularies/" + name = "Vocabularies" + resource_config = "resource" + search_request_headers = {"Accept": "application/json"} + title = "Vocabulary" + category = "Site management" + pid_path = "id" + icon = "exchange" + template = "invenio_administration/search.html" + + display_search = True + display_delete = False + display_edit = False + + item_field_list = { + "id": {"text": "Name", "order": 1}, + "count": {"text": "Number of entries", "order": 2}, + } + + search_config_name = "VOCABULARIES_SEARCH" + search_facets_config_name = "VOCABULARIES_FACETS" + search_sort_config_name = "VOCABULARIES_SORT_OPTIONS" + + resource_name = "resource" diff --git a/invenio_vocabularies/config.py b/invenio_vocabularies/config.py index 789963a5..3d6aa7a8 100644 --- a/invenio_vocabularies/config.py +++ b/invenio_vocabularies/config.py @@ -126,3 +126,21 @@ "yaml": YamlWriter, } """Data Streams writers.""" + +VOCABULARIES_SORT_OPTIONS = { + "name": dict( + title=_("Name"), + fields=["Name"], + ), + "entries": dict( + title=_("entries"), + fields=["entries"], + ), +} +"""Definitions of available Vocabularies sort options. """ + +VOCABULARIES_SEARCH = { + "facets": [], + "sort": ["name", "entries"], +} +"""Vocabularies search configuration.""" diff --git a/invenio_vocabularies/ext.py b/invenio_vocabularies/ext.py index 4e7ab481..755e358e 100644 --- a/invenio_vocabularies/ext.py +++ b/invenio_vocabularies/ext.py @@ -40,7 +40,8 @@ SubjectsService, SubjectsServiceConfig, ) -from .resources.resource import VocabulariesResource +from .contrib.information import InformationResource, InformationResourceConfig +from .resources.resource import VocabulariesResource, VocabulariesResourceConfig from .services.service import VocabulariesService @@ -120,6 +121,9 @@ def init_resource(self, app): service=self.subjects_service, config=SubjectsResourceConfig, ) + # self.vocabularies_resource = InformationResource( + # config=InformationResourceConfig, + # ) self.resource = VocabulariesResource( service=self.service, config=app.config["VOCABULARIES_RESOURCE_CONFIG"], diff --git a/invenio_vocabularies/resources/resource.py b/invenio_vocabularies/resources/resource.py index 2ec848a8..604c8054 100644 --- a/invenio_vocabularies/resources/resource.py +++ b/invenio_vocabularies/resources/resource.py @@ -85,7 +85,13 @@ class VocabulariesResourceConfig(RecordResourceConfig): # Resource # class VocabulariesResource(RecordResource): - """Resource for generic vocabularies.""" + """Resource for generic vocabularies. + + As stated by slint: + + > Generic vocabularies have a relatively small number of entries (<10,000) + + """ def create_url_rules(self): """Create the URL rules for the record resource.""" @@ -103,7 +109,40 @@ def create_url_rules(self): @response_handler(many=True) def get_all(self): """Get all items.""" - return {"status": 200, "message": "hello world!"}, 200 + # TODO gather information about _all_ vocabularies + # in the meantime, return a mocked response + + return { + "hits": { + "hits": [ + { + "id": "rights", + "pid_type": "v-lic", + "count": 36, + "links": { + "self": "https://docs.narodni-repozitar.cz/api/vocabularies/rights", + "self_html": "https://docs.narodni-repozitar.cz/vocabularies/rights", + }, + }, + { + "id": "funders", + "pid_type": "v-f", + "count": 75, + "name": { + "cs": "Poskytovatelé finanční podpory", + "en": "Research funders", + }, + "props": {"acronym": {"label": "Acronym"}}, + "links": { + "self": "https://docs.narodni-repozitar.cz/api/vocabularies/funders", + "self_html": "https://docs.narodni-repozitar.cz/vocabularies/funders", + }, + }, + ], + "total": 11, + }, + "links": {"self": "https://docs.narodni-repozitar.cz/api/vocabularies"}, + }, 200 @request_search_args @request_view_args diff --git a/invenio_vocabularies/templates/semantic-ui/invenio_vocabularies/vocabularies-list.html b/invenio_vocabularies/templates/semantic-ui/invenio_vocabularies/vocabularies-list.html new file mode 100644 index 00000000..3c16e9d5 --- /dev/null +++ b/invenio_vocabularies/templates/semantic-ui/invenio_vocabularies/vocabularies-list.html @@ -0,0 +1,12 @@ +{# + Copyright (C) 2024 CERN. + + Invenio App RDM is free software; you can redistribute it and/or modify it + under the terms of the MIT License; see LICENSE file for more details. + #} + {% extends "invenio_administration/search.html" %} + + {% block javascript %} + {{ super() }} + {{ webpack['invenio-vocabularies-search.js'] }} +{% endblock %} \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index a194808a..b2b8b17f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -55,6 +55,8 @@ sqlite = [options.entry_points] flask.commands = vocabularies = invenio_vocabularies.cli:vocabularies +invenio_administration.views = + vocabularies_list = invenio_vocabularies.administration.views.vocabularies:VocabulariesListView invenio_base.apps = invenio_vocabularies = invenio_vocabularies:InvenioVocabularies invenio_base.api_apps = From 4d3365b979bc9092b586475a6925922d52880c6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karl-Ulrich=20Kr=C3=A4gelin?= Date: Tue, 14 May 2024 11:08:31 +0200 Subject: [PATCH 03/38] add copyright --- invenio_vocabularies/administration/__init__.py | 5 +++-- invenio_vocabularies/administration/views/__init__.py | 7 ++++--- .../administration/views/vocabularies.py | 9 +++++++++ invenio_vocabularies/resources/resource.py | 1 + .../invenio_vocabularies/vocabularies-list.html | 2 +- 5 files changed, 18 insertions(+), 6 deletions(-) diff --git a/invenio_vocabularies/administration/__init__.py b/invenio_vocabularies/administration/__init__.py index b9a61ac0..5086b5e3 100644 --- a/invenio_vocabularies/administration/__init__.py +++ b/invenio_vocabularies/administration/__init__.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2023 ULB Münster. +# Copyright (C) 2020-2021 CERN. +# Copyright (C) 2024 Uni Münster. # -# invenio-oaiharvest-config is free software; you can redistribute it and/or +# Invenio-Vocabularies is free software; you can redistribute it and/or # modify it under the terms of the MIT License; see LICENSE file for more # details. diff --git a/invenio_vocabularies/administration/views/__init__.py b/invenio_vocabularies/administration/views/__init__.py index 67cac69b..b041d1cb 100644 --- a/invenio_vocabularies/administration/views/__init__.py +++ b/invenio_vocabularies/administration/views/__init__.py @@ -1,9 +1,10 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2023 ULB Münster. +# Copyright (C) 2020-2021 CERN. +# Copyright (C) 2024 Uni Münster. # -# invenio-oaiharvest-config is free software; you can redistribute it and/or +# Invenio-Vocabularies is free software; you can redistribute it and/or # modify it under the terms of the MIT License; see LICENSE file for more # details. -"""Invenio administration views module for OAI Harvester.""" +"""Invenio administration views module for Vocabularies.""" diff --git a/invenio_vocabularies/administration/views/vocabularies.py b/invenio_vocabularies/administration/views/vocabularies.py index 2a20f23a..b6b45325 100644 --- a/invenio_vocabularies/administration/views/vocabularies.py +++ b/invenio_vocabularies/administration/views/vocabularies.py @@ -1,3 +1,12 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2020-2021 CERN. +# Copyright (C) 2024 Uni Münster. +# +# Invenio-Vocabularies is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + from functools import partial from flask import current_app diff --git a/invenio_vocabularies/resources/resource.py b/invenio_vocabularies/resources/resource.py index 604c8054..632de560 100644 --- a/invenio_vocabularies/resources/resource.py +++ b/invenio_vocabularies/resources/resource.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # # Copyright (C) 2020-2021 CERN. +# Copyright (C) 2024 Uni Münster. # # Invenio-Vocabularies is free software; you can redistribute it and/or # modify it under the terms of the MIT License; see LICENSE file for more diff --git a/invenio_vocabularies/templates/semantic-ui/invenio_vocabularies/vocabularies-list.html b/invenio_vocabularies/templates/semantic-ui/invenio_vocabularies/vocabularies-list.html index 3c16e9d5..8b6d2e65 100644 --- a/invenio_vocabularies/templates/semantic-ui/invenio_vocabularies/vocabularies-list.html +++ b/invenio_vocabularies/templates/semantic-ui/invenio_vocabularies/vocabularies-list.html @@ -1,5 +1,5 @@ {# - Copyright (C) 2024 CERN. + Copyright (C) 2024 Uni Münster. Invenio App RDM is free software; you can redistribute it and/or modify it under the terms of the MIT License; see LICENSE file for more details. From f7404c0ef30a70d3b90ec4914ba0cbbe61507c1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karl-Ulrich=20Kr=C3=A4gelin?= Date: Tue, 14 May 2024 11:35:05 +0200 Subject: [PATCH 04/38] remove WIP import --- invenio_vocabularies/ext.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/invenio_vocabularies/ext.py b/invenio_vocabularies/ext.py index 755e358e..bae92814 100644 --- a/invenio_vocabularies/ext.py +++ b/invenio_vocabularies/ext.py @@ -40,7 +40,8 @@ SubjectsService, SubjectsServiceConfig, ) -from .contrib.information import InformationResource, InformationResourceConfig + +# from .contrib.information import InformationResource, InformationResourceConfig from .resources.resource import VocabulariesResource, VocabulariesResourceConfig from .services.service import VocabulariesService From 009be0a9b9330b7beba4d6ecab204a9e0e18135d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karl-Ulrich=20Kr=C3=A4gelin?= Date: Tue, 14 May 2024 13:05:05 +0200 Subject: [PATCH 05/38] remove more imports --- invenio_vocabularies/administration/views/vocabularies.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/invenio_vocabularies/administration/views/vocabularies.py b/invenio_vocabularies/administration/views/vocabularies.py index b6b45325..d41d144e 100644 --- a/invenio_vocabularies/administration/views/vocabularies.py +++ b/invenio_vocabularies/administration/views/vocabularies.py @@ -7,15 +7,10 @@ # modify it under the terms of the MIT License; see LICENSE file for more # details. -from functools import partial - -from flask import current_app from invenio_administration.views.base import ( - AdminResourceDetailView, AdminResourceListView, ) from invenio_i18n import lazy_gettext as _ -from invenio_search_ui.searchconfig import search_app_config class VocabulariesListView(AdminResourceListView): From fb36717ff79402639dc18388891b13e3138cad41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karl-Ulrich=20Kr=C3=A4gelin?= Date: Thu, 16 May 2024 11:45:28 +0200 Subject: [PATCH 06/38] provide API endpoint with aggregated counts for all types of vocabularies --- invenio_vocabularies/config.py | 10 ++ invenio_vocabularies/resources/resource.py | 56 ++----- invenio_vocabularies/services/permissions.py | 2 + invenio_vocabularies/services/service.py | 160 ++++++++++++++++++- 4 files changed, 183 insertions(+), 45 deletions(-) diff --git a/invenio_vocabularies/config.py b/invenio_vocabularies/config.py index 3d6aa7a8..6e0aa217 100644 --- a/invenio_vocabularies/config.py +++ b/invenio_vocabularies/config.py @@ -103,6 +103,16 @@ } """Names allowed identifier schemes.""" +# configure CUSTOM_VOCABULARY_TYPES to differentiate output. Is used in VocabulariesServiceConfig +VOCABULARIES_CUSTOM_VOCABULARY_TYPES = [ + "names", + "affiliations", + "awards", + "funders", + "subjects", +] + + VOCABULARIES_DATASTREAM_READERS = { "csv": CSVReader, "json": JsonReader, diff --git a/invenio_vocabularies/resources/resource.py b/invenio_vocabularies/resources/resource.py index 632de560..ed03f13d 100644 --- a/invenio_vocabularies/resources/resource.py +++ b/invenio_vocabularies/resources/resource.py @@ -19,6 +19,8 @@ resource_requestctx, response_handler, ) +from invenio_vocabularies.proxies import current_service +from invenio_access.permissions import system_identity from invenio_records_resources.resources import ( RecordResource, RecordResourceConfig, @@ -33,9 +35,12 @@ route, ) from invenio_records_resources.resources.records.utils import search_preference + +from invenio_vocabularies.services.service import VocabularyTypeService from marshmallow import fields from .serializer import VocabularyL10NItemSchema +import json # @@ -86,13 +91,7 @@ class VocabulariesResourceConfig(RecordResourceConfig): # Resource # class VocabulariesResource(RecordResource): - """Resource for generic vocabularies. - - As stated by slint: - - > Generic vocabularies have a relatively small number of entries (<10,000) - - """ + """Resource for generic vocabularies.""" def create_url_rules(self): """Create the URL rules for the record resource.""" @@ -107,43 +106,16 @@ def create_url_rules(self): ) return rules + @request_search_args @response_handler(many=True) def get_all(self): - """Get all items.""" - # TODO gather information about _all_ vocabularies - # in the meantime, return a mocked response - - return { - "hits": { - "hits": [ - { - "id": "rights", - "pid_type": "v-lic", - "count": 36, - "links": { - "self": "https://docs.narodni-repozitar.cz/api/vocabularies/rights", - "self_html": "https://docs.narodni-repozitar.cz/vocabularies/rights", - }, - }, - { - "id": "funders", - "pid_type": "v-f", - "count": 75, - "name": { - "cs": "Poskytovatelé finanční podpory", - "en": "Research funders", - }, - "props": {"acronym": {"label": "Acronym"}}, - "links": { - "self": "https://docs.narodni-repozitar.cz/api/vocabularies/funders", - "self_html": "https://docs.narodni-repozitar.cz/vocabularies/funders", - }, - }, - ], - "total": 11, - }, - "links": {"self": "https://docs.narodni-repozitar.cz/api/vocabularies"}, - }, 200 + """Return information about _all_ vocabularies.""" + config = current_service.config + vocabtypeservice = VocabularyTypeService(config) + identity = g.identity + hits = vocabtypeservice.search(identity) + + return hits.to_dict(), 200 @request_search_args @request_view_args diff --git a/invenio_vocabularies/services/permissions.py b/invenio_vocabularies/services/permissions.py index 6a8c7d57..2ca6d302 100644 --- a/invenio_vocabularies/services/permissions.py +++ b/invenio_vocabularies/services/permissions.py @@ -21,3 +21,5 @@ class PermissionPolicy(RecordPermissionPolicy): can_update = [SystemProcess()] can_delete = [SystemProcess()] can_manage = [SystemProcess()] + # this permission is needed for the /api/vocabularies/ endpoint + can_list_vocabularies = [SystemProcess(), AnyUser()] diff --git a/invenio_vocabularies/services/service.py b/invenio_vocabularies/services/service.py index 817d4989..fdd39f03 100644 --- a/invenio_vocabularies/services/service.py +++ b/invenio_vocabularies/services/service.py @@ -8,9 +8,10 @@ # details. """Vocabulary service.""" - +from flask import current_app from invenio_cache import current_cache from invenio_db import db +from invenio_records_resources.services.base import Service, ConditionalLink from invenio_i18n import lazy_gettext as _ from invenio_records_resources.services import ( Link, @@ -25,10 +26,14 @@ FilterParam, SuggestQueryParser, ) +from invenio_records_resources.services.base import ServiceListResult +from invenio_records_resources.services.errors import PermissionDeniedError from invenio_records_resources.services.records.schema import ServiceSchemaWrapper from invenio_records_resources.services.uow import unit_of_work +from invenio_vocabularies.proxies import current_service +from invenio_search import current_search_client from invenio_search.engine import dsl - +from invenio_records_resources.proxies import current_service_registry from ..records.api import Vocabulary from ..records.models import VocabularyType from .components import PIDComponent, VocabularyTypeComponent @@ -37,6 +42,140 @@ from .tasks import process_datastream +def is_custom_vocabulary_type(vocabulary_type, context): + """Check if the vocabulary type is a custom vocabulary type.""" + return vocabulary_type["id"] in current_app.config.get( + "VOCABULARIES_CUSTOM_VOCABULARY_TYPES", [] + ) + + +class VocabularyMetadataList(ServiceListResult): + def __init__( + self, + service, + identity, + results, + links_tpl=None, + links_item_tpl=None, + ): + """Constructor. + + :params service: a service instance + :params identity: an identity that performed the service request + :params results: the search results + """ + self._identity = identity + self._results = results + self._service = service + self._links_tpl = links_tpl + self._links_item_tpl = links_item_tpl + + def to_dict(self): + hits = list(self._results) + + for hit in hits: + if self._links_item_tpl: + hit["links"] = self._links_item_tpl.expand(self._identity, hit) + + res = { + "hits": { + "hits": hits, + "total": len(hits), + } + } + + if self._links_tpl: + res["links"] = self._links_tpl.expand(self._identity, None) + + return res + + +class VocabularyTypeService(Service): + """oarepo Vocabulary types service. + search method uses VocabularyType.query.all() + """ + + @property + def schema(self): + """Returns the data schema instance.""" + return ServiceSchemaWrapper(self, schema=self.config.schema) + + @property + def links_item_tpl(self): + """Item links template.""" + return LinksTemplate( + self.config.vocabularies_listing_item, + ) + + @property + def custom_vocabulary_names(self): + return current_app.config.get("VOCABULARIES_CUSTOM_VOCABULARY_TYPES", []) + + def search(self, identity): + """Search for vocabulary types entries.""" + self.require_permission(identity, "list_vocabularies") + + vocabulary_types = VocabularyType.query.all() + + # config_vocab_types = current_app.config["INVENIO_VOCABULARY_TYPE_METADATA"] + config_vocab_types = current_app.config.get( + "INVENIO_VOCABULARY_TYPE_METADATA", {} + ) + count_terms_agg = ( + self._generic_vocabulary_statistics() | self._custom_vocabulary_statistics() + ) + + # Extend database data with configuration & aggregation data. + results = [] + for db_vocab_type in vocabulary_types: + result = { + "id": db_vocab_type.id, + "pid_type": db_vocab_type.pid_type, + "count": count_terms_agg.get(db_vocab_type.id, 0), + } + + if db_vocab_type.id in config_vocab_types: + for k, v in config_vocab_types[db_vocab_type.id].items(): + result[k] = v + + results.append(result) + + return self.config.vocabularies_listing_resultlist_cls( + self, + identity, + results, + links_tpl=LinksTemplate({"self": Link("{+api}/vocabularies")}), + links_item_tpl=self.links_item_tpl, + ) + + def _custom_vocabulary_statistics(self): + # query database for count of terms in custom vocabularies + returndict = {} + for vocab_type in self.custom_vocabulary_names: + custom_service = current_service_registry.get(vocab_type) + record_cls = custom_service.config.record_cls + returndict[vocab_type] = record_cls.model_cls.query.count() + + return returndict + + def _generic_vocabulary_statistics(self): + # Opensearch query for generic vocabularies + config: RecordServiceConfig = current_service.config + search_opts = config.search + + search = search_opts.search_cls( + using=current_search_client, + index=config.record_cls.index.search_alias, + ) + + search.aggs.bucket("vocabularies", {"terms": {"field": "type.id", "size": 100}}) + + search_result = search.execute() + buckets = search_result.aggs.to_dict()["vocabularies"]["buckets"] + + return {bucket["key"]: bucket["doc_count"] for bucket in buckets} + + class VocabularySearchOptions(SearchOptions): """Search options.""" @@ -88,6 +227,21 @@ class VocabulariesServiceConfig(RecordServiceConfig): record_cls = Vocabulary schema = VocabularySchema task_schema = TaskSchema + vocabularies_listing_resultlist_cls = VocabularyMetadataList + + vocabularies_listing_item = { + "self": ConditionalLink( + cond=is_custom_vocabulary_type, + if_=Link( + "{+api}/{id}", + vars=lambda vocab_type, vars: vars.update({"id": vocab_type["id"]}), + ), + else_=Link( + "{+api}/vocabularies/{id}", + vars=lambda vocab_type, vars: vars.update({"id": vocab_type["id"]}), + ), + ) + } search = VocabularySearchOptions @@ -145,7 +299,7 @@ def search( params, search_preference, extra_filter=dsl.Q("term", type__id=vocabulary_type.id), - **kwargs + **kwargs, ).execute() return self.result_list( From c0377de593101cebb1983448eb8ad5503f1c2b0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karl-Ulrich=20Kr=C3=A4gelin?= Date: Fri, 17 May 2024 10:25:59 +0200 Subject: [PATCH 07/38] add missing invenio-administration dependency --- setup.cfg | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index b2b8b17f..edd81736 100644 --- a/setup.cfg +++ b/setup.cfg @@ -29,6 +29,7 @@ zip_safe = False install_requires = invenio-i18n>=2.0.0,<3.0.0 invenio-records-resources>=6.0.0,<7.0.0 + invenio-administration>=2.0.0,<3.0.0 lxml>=4.5.0 PyYAML>=5.4.1 oaipmh-scythe @ git+https://github.com/ulbmuenster/invenio-oaipmh-scythe.git @@ -136,7 +137,7 @@ input-file = invenio_vocabularies/translations/messages.pot output-dir = invenio_vocabularies/translations/ [isort] -profile=black +profile = black [check-manifest] ignore = From dd1420de5772ddd2346661dc58012fe2206418e4 Mon Sep 17 00:00:00 2001 From: Sarah Wiechers Date: Fri, 17 May 2024 11:16:16 +0200 Subject: [PATCH 08/38] Add and update docstrings --- .../administration/views/vocabularies.py | 7 +++--- invenio_vocabularies/resources/resource.py | 7 +++--- invenio_vocabularies/services/service.py | 23 ++++++++++++------- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/invenio_vocabularies/administration/views/vocabularies.py b/invenio_vocabularies/administration/views/vocabularies.py index d41d144e..e36fe45e 100644 --- a/invenio_vocabularies/administration/views/vocabularies.py +++ b/invenio_vocabularies/administration/views/vocabularies.py @@ -7,14 +7,13 @@ # modify it under the terms of the MIT License; see LICENSE file for more # details. -from invenio_administration.views.base import ( - AdminResourceListView, -) +"""Vocabularies admin interface.""" +from invenio_administration.views.base import AdminResourceListView from invenio_i18n import lazy_gettext as _ class VocabulariesListView(AdminResourceListView): - """Configuration for OAI-PMH sets list view.""" + """Configuration for vocabularies list view.""" api_endpoint = "/vocabularies/" name = "Vocabularies" diff --git a/invenio_vocabularies/resources/resource.py b/invenio_vocabularies/resources/resource.py index ed03f13d..d0c0bfaf 100644 --- a/invenio_vocabularies/resources/resource.py +++ b/invenio_vocabularies/resources/resource.py @@ -9,6 +9,8 @@ """Vocabulary resource.""" +import json + import marshmallow as ma from flask import g from flask_resources import ( @@ -19,7 +21,6 @@ resource_requestctx, response_handler, ) -from invenio_vocabularies.proxies import current_service from invenio_access.permissions import system_identity from invenio_records_resources.resources import ( RecordResource, @@ -35,12 +36,12 @@ route, ) from invenio_records_resources.resources.records.utils import search_preference +from marshmallow import fields +from invenio_vocabularies.proxies import current_service from invenio_vocabularies.services.service import VocabularyTypeService -from marshmallow import fields from .serializer import VocabularyL10NItemSchema -import json # diff --git a/invenio_vocabularies/services/service.py b/invenio_vocabularies/services/service.py index fdd39f03..7389f716 100644 --- a/invenio_vocabularies/services/service.py +++ b/invenio_vocabularies/services/service.py @@ -11,8 +11,8 @@ from flask import current_app from invenio_cache import current_cache from invenio_db import db -from invenio_records_resources.services.base import Service, ConditionalLink from invenio_i18n import lazy_gettext as _ +from invenio_records_resources.proxies import current_service_registry from invenio_records_resources.services import ( Link, LinksTemplate, @@ -21,19 +21,24 @@ SearchOptions, pagination_links, ) +from invenio_records_resources.services.base import ( + ConditionalLink, + Service, + ServiceListResult, +) +from invenio_records_resources.services.errors import PermissionDeniedError from invenio_records_resources.services.records.components import DataComponent from invenio_records_resources.services.records.params import ( FilterParam, SuggestQueryParser, ) -from invenio_records_resources.services.base import ServiceListResult -from invenio_records_resources.services.errors import PermissionDeniedError from invenio_records_resources.services.records.schema import ServiceSchemaWrapper from invenio_records_resources.services.uow import unit_of_work -from invenio_vocabularies.proxies import current_service from invenio_search import current_search_client from invenio_search.engine import dsl -from invenio_records_resources.proxies import current_service_registry + +from invenio_vocabularies.proxies import current_service + from ..records.api import Vocabulary from ..records.models import VocabularyType from .components import PIDComponent, VocabularyTypeComponent @@ -50,6 +55,8 @@ def is_custom_vocabulary_type(vocabulary_type, context): class VocabularyMetadataList(ServiceListResult): + """Ensures that vocabulary metadata is returned in the proper format.""" + def __init__( self, service, @@ -71,6 +78,7 @@ def __init__( self._links_item_tpl = links_item_tpl def to_dict(self): + """Formats result to a dict of hits.""" hits = list(self._results) for hit in hits: @@ -91,9 +99,7 @@ def to_dict(self): class VocabularyTypeService(Service): - """oarepo Vocabulary types service. - search method uses VocabularyType.query.all() - """ + """Vocabulary type service.""" @property def schema(self): @@ -109,6 +115,7 @@ def links_item_tpl(self): @property def custom_vocabulary_names(self): + """Checks whether vocabulary is a custom vocabulary.""" return current_app.config.get("VOCABULARIES_CUSTOM_VOCABULARY_TYPES", []) def search(self, identity): From ebd336314734231c3395565b2ab437c8c7f12a5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karl-Ulrich=20Kr=C3=A4gelin?= Date: Wed, 22 May 2024 12:29:33 +0200 Subject: [PATCH 09/38] dirty commit, added resources --- .../administration/views/vocabularies.py | 41 ++++++++- invenio_vocabularies/config.py | 4 +- invenio_vocabularies/ext.py | 15 +++- invenio_vocabularies/resources/__init__.py | 6 ++ invenio_vocabularies/resources/config.py | 87 +++++++++++++++++++ invenio_vocabularies/resources/resource.py | 61 +++++++++---- invenio_vocabularies/services/service.py | 12 ++- .../vocabulary-details.html | 71 +++++++++++++++ invenio_vocabularies/views.py | 7 ++ setup.cfg | 2 + 10 files changed, 277 insertions(+), 29 deletions(-) create mode 100644 invenio_vocabularies/resources/config.py create mode 100644 invenio_vocabularies/templates/semantic-ui/invenio_vocabularies/vocabulary-details.html diff --git a/invenio_vocabularies/administration/views/vocabularies.py b/invenio_vocabularies/administration/views/vocabularies.py index e36fe45e..379a31d5 100644 --- a/invenio_vocabularies/administration/views/vocabularies.py +++ b/invenio_vocabularies/administration/views/vocabularies.py @@ -8,7 +8,11 @@ # details. """Vocabularies admin interface.""" -from invenio_administration.views.base import AdminResourceListView +from invenio_administration.views.base import ( + AdminResourceListView, + AdminResourceEditView, + AdminResourceDetailView, +) from invenio_i18n import lazy_gettext as _ @@ -18,9 +22,10 @@ class VocabulariesListView(AdminResourceListView): api_endpoint = "/vocabularies/" name = "Vocabularies" resource_config = "resource" - search_request_headers = {"Accept": "application/json"} + search_request_headers = {"Accept": "application/vnd.inveniordm.v1+json"} title = "Vocabulary" category = "Site management" + # pid_path ist das mapping in welchem JSON key die ID des eintrags steht pid_path = "id" icon = "exchange" template = "invenio_administration/search.html" @@ -39,3 +44,35 @@ class VocabulariesListView(AdminResourceListView): search_sort_config_name = "VOCABULARIES_SORT_OPTIONS" resource_name = "resource" + + +class VocabularyTypesDetailsView(AdminResourceListView): + """Configuration for vocabularies list view.""" + + name = "Vocabularies_Detail" + url = "/vocabularies/" + api_endpoint = "/vocabularies//test" + + # name of the resource's list view name, enables navigation between detail view and list view. + list_view_name = "Vocabularies" + resource_config = "vocabulary_admin_resource" + search_request_headers = {"Accept": "application/json"} + title = "Vocabularies Detail" + pid_path = "id" + pid_value = "id" + # only if disabled() (as a function) its not in the sidebar, see https://github.com/inveniosoftware/invenio-administration/blob/main/invenio_administration/menu/menu.py#L54 + disabled = lambda _: True + + list_view_name = "Vocabularies" + template = "invenio_administration/search.html" + display_delete = False + display_create = False + display_edit = False + display_search = True + item_field_list = { + "id": {"text": "Name", "order": 1}, + } + search_config_name = "VOCABULARIES_SEARCH" + search_facets_config_name = "VOCABULARIES_FACETS" + search_sort_config_name = "VOCABULARIES_SORT_OPTIONS" + resource_name = "resource" diff --git a/invenio_vocabularies/config.py b/invenio_vocabularies/config.py index 6e0aa217..b2233e16 100644 --- a/invenio_vocabularies/config.py +++ b/invenio_vocabularies/config.py @@ -140,11 +140,11 @@ VOCABULARIES_SORT_OPTIONS = { "name": dict( title=_("Name"), - fields=["Name"], + fields=["id"], ), "entries": dict( title=_("entries"), - fields=["entries"], + fields=["count"], ), } """Definitions of available Vocabularies sort options. """ diff --git a/invenio_vocabularies/ext.py b/invenio_vocabularies/ext.py index bae92814..e1bb7738 100644 --- a/invenio_vocabularies/ext.py +++ b/invenio_vocabularies/ext.py @@ -42,7 +42,13 @@ ) # from .contrib.information import InformationResource, InformationResourceConfig -from .resources.resource import VocabulariesResource, VocabulariesResourceConfig + +from .resources import ( + VocabulariesResourceConfig, + VocabularyTypeResourceConfig, + VocabulariesResource, + VocabulariesAdminResource, +) from .services.service import VocabulariesService @@ -122,13 +128,14 @@ def init_resource(self, app): service=self.subjects_service, config=SubjectsResourceConfig, ) - # self.vocabularies_resource = InformationResource( - # config=InformationResourceConfig, - # ) self.resource = VocabulariesResource( service=self.service, config=app.config["VOCABULARIES_RESOURCE_CONFIG"], ) + self.vocabulary_admin_resource = VocabulariesAdminResource( + service=self.service, + config=VocabularyTypeResourceConfig, + ) def finalize_app(app): diff --git a/invenio_vocabularies/resources/__init__.py b/invenio_vocabularies/resources/__init__.py index 9c379e62..6d86c1a3 100644 --- a/invenio_vocabularies/resources/__init__.py +++ b/invenio_vocabularies/resources/__init__.py @@ -8,8 +8,14 @@ """Resources module.""" from invenio_vocabularies.resources.schema import L10NString, VocabularyL10Schema +from .config import VocabularyTypeResourceConfig, VocabulariesResourceConfig +from .resource import VocabulariesResource, VocabulariesAdminResource __all__ = ( "VocabularyL10Schema", "L10NString", + "VocabulariesResourceConfig", + "VocabularyTypeResourceConfig", + "VocabulariesAdminResource", + "VocabulariesResource", ) diff --git a/invenio_vocabularies/resources/config.py b/invenio_vocabularies/resources/config.py new file mode 100644 index 00000000..430858b4 --- /dev/null +++ b/invenio_vocabularies/resources/config.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2024 CERN. +# Copyright (C) 2024 University of Münster. +# +# Invenio-Vocabularies is free software; you can redistringibute it and/or modify it +# under the terms of the MIT License; see LICENSE file for more details. + +"""Resources config.""" + +import marshmallow as ma +from flask_resources import HTTPJSONException, ResourceConfig, create_error_handler +from invenio_records_resources.resources.errors import ErrorHandlersMixin +from invenio_records_resources.resources.records.args import SearchRequestArgsSchema +from invenio_records_resources.services.base.config import ConfiguratorMixin + + +class TasksResourceConfig(ResourceConfig, ConfiguratorMixin): + """Celery tasks resource config.""" + + # Blueprint configuration + blueprint_name = "tasks" + url_prefix = "/tasks" + routes = {"list": ""} + + +class VocabulariesSearchRequestArgsSchema(SearchRequestArgsSchema): + """Vocabularies search request parameters.""" + + active = ma.fields.Boolean() + + +class VocabulariesResourceConfig(ResourceConfig, ConfiguratorMixin): + """Vocabularies resource config.""" + + # /vocabulary - all + # Blueprint configuration + blueprint_name = "vocabularies" + url_prefix = "/vocabularies" + routes = { + "list": "", + "item": "/", + } + + # Request parsing + request_read_args = {} + request_view_args = {"vocabulary_id": ma.fields.String} + request_search_args = VocabulariesSearchRequestArgsSchema + + error_handlers = { + **ErrorHandlersMixin.error_handlers, + # TODO: Add custom error handlers here + } + + +class VocabulariesSearchRequestArgsSchema(SearchRequestArgsSchema): + """Vocabularies search request parameters.""" + + status = ma.fields.Boolean() + + +class VocabularyTypeResourceConfig(ResourceConfig, ConfiguratorMixin): + """Runs resource config.""" + + # /vocabulary/vocabulary_id + # Blueprint configuration + blueprint_name = "vocabulary_runs" + url_prefix = "" + + routes = { + "all": "/", + "list": "/vocabularies/", + "item": "/vocabularies//", + } + + # Request parsing + request_view_args = { + "vocabulary_id": ma.fields.String, + "vocabulary_type_id": ma.fields.String, + } + + request_search_args = VocabulariesSearchRequestArgsSchema + + error_handlers = { + **ErrorHandlersMixin.error_handlers, + # TODO: Add custom error handlers here + } diff --git a/invenio_vocabularies/resources/resource.py b/invenio_vocabularies/resources/resource.py index d0c0bfaf..aa21465a 100644 --- a/invenio_vocabularies/resources/resource.py +++ b/invenio_vocabularies/resources/resource.py @@ -65,7 +65,6 @@ class VocabulariesResourceConfig(RecordResourceConfig): "list": "/", "item": "//", "tasks": "/tasks", - "all": "/", } request_view_args = { @@ -92,7 +91,10 @@ class VocabulariesResourceConfig(RecordResourceConfig): # Resource # class VocabulariesResource(RecordResource): - """Resource for generic vocabularies.""" + """Resource for generic vocabularies. + + Provide the API /api/vocabularies/ + """ def create_url_rules(self): """Create the URL rules for the record resource.""" @@ -101,22 +103,22 @@ def create_url_rules(self): rules.append( route("POST", routes["tasks"], self.launch), ) - # Add "vocabularies/" route - rules.append( - route("GET", routes["all"], self.get_all), - ) - return rules - - @request_search_args - @response_handler(many=True) - def get_all(self): - """Return information about _all_ vocabularies.""" - config = current_service.config - vocabtypeservice = VocabularyTypeService(config) - identity = g.identity - hits = vocabtypeservice.search(identity) - - return hits.to_dict(), 200 + # # Add "vocabularies/" route + # rules.append( + # route("GET", routes["all"], self.get_all), + # ) + # return rules + + # @request_search_args + # @response_handler(many=True) + # def get_all(self): + # """Return information about _all_ vocabularies.""" + # config = current_service.config + # vocabtypeservice = VocabularyTypeService(config) + # identity = g.identity + # hits = vocabtypeservice.search(identity) + + # return hits.to_dict(), 200 @request_search_args @request_view_args @@ -191,3 +193,26 @@ def launch(self): """Create a task.""" self.service.launch(g.identity, resource_requestctx.data or {}) return "", 202 + + +class VocabulariesAdminResource(RecordResource): + def create_url_rules(self): + """Create the URL rules for the record resource.""" + routes = self.config.routes + rules = super().create_url_rules() + + rules.append( + route("GET", routes["list"], self.get_all_vocabulary_types), + ) + return rules + + @request_search_args + @response_handler(many=True) + def get_all_vocabulary_types(self): + """Return information about _all_ vocabularies.""" + config = current_service.config + vocabtypeservice = VocabularyTypeService(config) + identity = g.identity + hits = vocabtypeservice.search(identity) + + return hits.to_dict(), 200 diff --git a/invenio_vocabularies/services/service.py b/invenio_vocabularies/services/service.py index 7389f716..80399eb8 100644 --- a/invenio_vocabularies/services/service.py +++ b/invenio_vocabularies/services/service.py @@ -128,9 +128,13 @@ def search(self, identity): config_vocab_types = current_app.config.get( "INVENIO_VOCABULARY_TYPE_METADATA", {} ) - count_terms_agg = ( - self._generic_vocabulary_statistics() | self._custom_vocabulary_statistics() - ) + + count_terms_agg = {} + generic_stats = self._generic_vocabulary_statistics() + custom_stats = self._custom_vocabulary_statistics() + + for k in generic_stats.keys() | custom_stats.keys(): + count_terms_agg[k] = generic_stats.get(k, 0) + custom_stats.get(k, 0) # Extend database data with configuration & aggregation data. results = [] @@ -139,6 +143,8 @@ def search(self, identity): "id": db_vocab_type.id, "pid_type": db_vocab_type.pid_type, "count": count_terms_agg.get(db_vocab_type.id, 0), + "is_custom_vocabulary": db_vocab_type.id + in self.custom_vocabulary_names, } if db_vocab_type.id in config_vocab_types: diff --git a/invenio_vocabularies/templates/semantic-ui/invenio_vocabularies/vocabulary-details.html b/invenio_vocabularies/templates/semantic-ui/invenio_vocabularies/vocabulary-details.html new file mode 100644 index 00000000..4dcdeac7 --- /dev/null +++ b/invenio_vocabularies/templates/semantic-ui/invenio_vocabularies/vocabulary-details.html @@ -0,0 +1,71 @@ +{# + Copyright (C) 2024 CERN. + + Invenio App RDM is free software; you can redistribute it and/or modify it + under the terms of the MIT License; see LICENSE file for more details. + #} + + {%- from "invenio_administration/macros.html" import go_back %} + + {% extends "invenio_administration/search.html" %} + + {% block admin_main_column %} +
+ +
+ {{ go_back() }} + + {% block admin_page_content %} + + + {%- block search_app %} +
+
+ {%- endblock search_app %} + {% endblock admin_page_content %} +
+
+ {% endblock %} + + {% block javascript %} + {{ super() }} + {{ webpack['invenio-jobs-details.js'] }} + {% endblock %} \ No newline at end of file diff --git a/invenio_vocabularies/views.py b/invenio_vocabularies/views.py index 4702c9e9..c5877ac0 100644 --- a/invenio_vocabularies/views.py +++ b/invenio_vocabularies/views.py @@ -44,3 +44,10 @@ def create_names_blueprint_from_app(app): def create_subjects_blueprint_from_app(app): """Create app blueprint.""" return app.extensions["invenio-vocabularies"].subjects_resource.as_blueprint() + + +def create_list_blueprint_from_app(app): + """Create app blueprint.""" + return app.extensions[ + "invenio-vocabularies" + ].vocabulary_admin_resource.as_blueprint() diff --git a/setup.cfg b/setup.cfg index edd81736..1f305b89 100644 --- a/setup.cfg +++ b/setup.cfg @@ -58,6 +58,7 @@ flask.commands = vocabularies = invenio_vocabularies.cli:vocabularies invenio_administration.views = vocabularies_list = invenio_vocabularies.administration.views.vocabularies:VocabulariesListView + vocabulary_details = invenio_vocabularies.administration.views.vocabularies:VocabularyTypesDetailsView invenio_base.apps = invenio_vocabularies = invenio_vocabularies:InvenioVocabularies invenio_base.api_apps = @@ -72,6 +73,7 @@ invenio_base.api_blueprints = invenio_vocabularies_names = invenio_vocabularies.views:create_names_blueprint_from_app invenio_vocabularies_subjects = invenio_vocabularies.views:create_subjects_blueprint_from_app invenio_vocabularies_ext = invenio_vocabularies.views:blueprint + invenio_vocabularies_list = invenio_vocabularies.views: invenio_base.api_finalize_app = invenio_vocabularies = invenio_vocabularies.ext:api_finalize_app invenio_base.finalize_app = From 9bf7e2aa15a0df4670573286037e2fb2cc53cc9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karl-Ulrich=20Kr=C3=A4gelin?= Date: Wed, 22 May 2024 14:43:08 +0200 Subject: [PATCH 10/38] fix imports and cleanup --- .../administration/views/vocabularies.py | 8 +-- invenio_vocabularies/config.py | 2 +- invenio_vocabularies/ext.py | 4 +- invenio_vocabularies/resources/config.py | 71 ++++++++++++------- invenio_vocabularies/resources/resource.py | 47 +----------- setup.cfg | 2 +- 6 files changed, 56 insertions(+), 78 deletions(-) diff --git a/invenio_vocabularies/administration/views/vocabularies.py b/invenio_vocabularies/administration/views/vocabularies.py index 379a31d5..887f9ee5 100644 --- a/invenio_vocabularies/administration/views/vocabularies.py +++ b/invenio_vocabularies/administration/views/vocabularies.py @@ -19,10 +19,10 @@ class VocabulariesListView(AdminResourceListView): """Configuration for vocabularies list view.""" - api_endpoint = "/vocabularies/" + api_endpoint = "/vocabularies" name = "Vocabularies" resource_config = "resource" - search_request_headers = {"Accept": "application/vnd.inveniordm.v1+json"} + search_request_headers = {"Accept": "application/json"} title = "Vocabulary" category = "Site management" # pid_path ist das mapping in welchem JSON key die ID des eintrags steht @@ -43,7 +43,7 @@ class VocabulariesListView(AdminResourceListView): search_facets_config_name = "VOCABULARIES_FACETS" search_sort_config_name = "VOCABULARIES_SORT_OPTIONS" - resource_name = "resource" + resource_name = "vocabulary_admin_resource" class VocabularyTypesDetailsView(AdminResourceListView): @@ -51,7 +51,7 @@ class VocabularyTypesDetailsView(AdminResourceListView): name = "Vocabularies_Detail" url = "/vocabularies/" - api_endpoint = "/vocabularies//test" + api_endpoint = "/vocabularies/" # name of the resource's list view name, enables navigation between detail view and list view. list_view_name = "Vocabularies" diff --git a/invenio_vocabularies/config.py b/invenio_vocabularies/config.py index b2233e16..50c160d9 100644 --- a/invenio_vocabularies/config.py +++ b/invenio_vocabularies/config.py @@ -25,7 +25,7 @@ ) from .datastreams.transformers import XMLTransformer from .datastreams.writers import ServiceWriter, YamlWriter -from .resources.resource import VocabulariesResourceConfig +from .resources import VocabulariesResourceConfig from .services.service import VocabulariesServiceConfig VOCABULARIES_RESOURCE_CONFIG = VocabulariesResourceConfig diff --git a/invenio_vocabularies/ext.py b/invenio_vocabularies/ext.py index e1bb7738..0f37d49e 100644 --- a/invenio_vocabularies/ext.py +++ b/invenio_vocabularies/ext.py @@ -44,10 +44,10 @@ # from .contrib.information import InformationResource, InformationResourceConfig from .resources import ( - VocabulariesResourceConfig, VocabularyTypeResourceConfig, VocabulariesResource, VocabulariesAdminResource, + VocabulariesResourceConfig, ) from .services.service import VocabulariesService @@ -129,10 +129,12 @@ def init_resource(self, app): config=SubjectsResourceConfig, ) self.resource = VocabulariesResource( + # connects resource with the config service=self.service, config=app.config["VOCABULARIES_RESOURCE_CONFIG"], ) self.vocabulary_admin_resource = VocabulariesAdminResource( + # should connect the vocabulary_admin_resource with the config service=self.service, config=VocabularyTypeResourceConfig, ) diff --git a/invenio_vocabularies/resources/config.py b/invenio_vocabularies/resources/config.py index 430858b4..cf10889e 100644 --- a/invenio_vocabularies/resources/config.py +++ b/invenio_vocabularies/resources/config.py @@ -9,10 +9,25 @@ """Resources config.""" import marshmallow as ma -from flask_resources import HTTPJSONException, ResourceConfig, create_error_handler +from flask_resources import ( + HTTPJSONException, + ResourceConfig, + create_error_handler, + JSONSerializer, + BaseListSchema, + MarshmallowSerializer, + ResponseHandler, +) +from invenio_records_resources.resources.records.headers import etag_headers from invenio_records_resources.resources.errors import ErrorHandlersMixin from invenio_records_resources.resources.records.args import SearchRequestArgsSchema from invenio_records_resources.services.base.config import ConfiguratorMixin +from invenio_records_resources.resources import ( + RecordResource, + RecordResourceConfig, + SearchRequestArgsSchema, +) +from .serializer import VocabularyL10NItemSchema class TasksResourceConfig(ResourceConfig, ConfiguratorMixin): @@ -24,48 +39,52 @@ class TasksResourceConfig(ResourceConfig, ConfiguratorMixin): routes = {"list": ""} -class VocabulariesSearchRequestArgsSchema(SearchRequestArgsSchema): +class VocabularySearchRequestArgsSchema(SearchRequestArgsSchema): """Vocabularies search request parameters.""" + tags = ma.fields.Str() active = ma.fields.Boolean() + status = ma.fields.Boolean() -class VocabulariesResourceConfig(ResourceConfig, ConfiguratorMixin): - """Vocabularies resource config.""" +class VocabulariesResourceConfig(RecordResourceConfig): + """Vocabulary resource configuration.""" - # /vocabulary - all - # Blueprint configuration blueprint_name = "vocabularies" url_prefix = "/vocabularies" routes = { - "list": "", - "item": "/", + "list": "/", + "item": "//", + "tasks": "/tasks", } - # Request parsing - request_read_args = {} - request_view_args = {"vocabulary_id": ma.fields.String} - request_search_args = VocabulariesSearchRequestArgsSchema - - error_handlers = { - **ErrorHandlersMixin.error_handlers, - # TODO: Add custom error handlers here + request_view_args = { + "pid_value": ma.fields.Str(), + "type": ma.fields.Str(required=True), } - -class VocabulariesSearchRequestArgsSchema(SearchRequestArgsSchema): - """Vocabularies search request parameters.""" - - status = ma.fields.Boolean() + request_search_args = VocabularySearchRequestArgsSchema + + response_handlers = { + "application/json": ResponseHandler(JSONSerializer(), headers=etag_headers), + "application/vnd.inveniordm.v1+json": ResponseHandler( + MarshmallowSerializer( + format_serializer_cls=JSONSerializer, + object_schema_cls=VocabularyL10NItemSchema, + list_schema_cls=BaseListSchema, + ), + headers=etag_headers, + ), + } class VocabularyTypeResourceConfig(ResourceConfig, ConfiguratorMixin): - """Runs resource config.""" + """Vocabulary list resource config.""" # /vocabulary/vocabulary_id # Blueprint configuration - blueprint_name = "vocabulary_runs" - url_prefix = "" + blueprint_name = "vocabulary_list" + url_prefix = "/vocabularies" routes = { "all": "/", @@ -74,12 +93,12 @@ class VocabularyTypeResourceConfig(ResourceConfig, ConfiguratorMixin): } # Request parsing + request_read_args = {} request_view_args = { "vocabulary_id": ma.fields.String, "vocabulary_type_id": ma.fields.String, } - - request_search_args = VocabulariesSearchRequestArgsSchema + request_search_args = VocabularySearchRequestArgsSchema error_handlers = { **ErrorHandlersMixin.error_handlers, diff --git a/invenio_vocabularies/resources/resource.py b/invenio_vocabularies/resources/resource.py index aa21465a..7779ce7f 100644 --- a/invenio_vocabularies/resources/resource.py +++ b/invenio_vocabularies/resources/resource.py @@ -44,49 +44,6 @@ from .serializer import VocabularyL10NItemSchema -# -# Request args -# -class VocabularySearchRequestArgsSchema(SearchRequestArgsSchema): - """Add parameter to parse tags.""" - - tags = fields.Str() - - -# -# Resource config -# -class VocabulariesResourceConfig(RecordResourceConfig): - """Vocabulary resource configuration.""" - - blueprint_name = "vocabularies" - url_prefix = "/vocabularies" - routes = { - "list": "/", - "item": "//", - "tasks": "/tasks", - } - - request_view_args = { - "pid_value": ma.fields.Str(), - "type": ma.fields.Str(required=True), - } - - request_search_args = VocabularySearchRequestArgsSchema - - response_handlers = { - "application/json": ResponseHandler(JSONSerializer(), headers=etag_headers), - "application/vnd.inveniordm.v1+json": ResponseHandler( - MarshmallowSerializer( - format_serializer_cls=JSONSerializer, - object_schema_cls=VocabularyL10NItemSchema, - list_schema_cls=BaseListSchema, - ), - headers=etag_headers, - ), - } - - # # Resource # @@ -107,7 +64,7 @@ def create_url_rules(self): # rules.append( # route("GET", routes["all"], self.get_all), # ) - # return rules + return rules # @request_search_args # @response_handler(many=True) @@ -202,7 +159,7 @@ def create_url_rules(self): rules = super().create_url_rules() rules.append( - route("GET", routes["list"], self.get_all_vocabulary_types), + route("GET", routes["all"], self.get_all_vocabulary_types), ) return rules diff --git a/setup.cfg b/setup.cfg index 1f305b89..3f421250 100644 --- a/setup.cfg +++ b/setup.cfg @@ -73,7 +73,7 @@ invenio_base.api_blueprints = invenio_vocabularies_names = invenio_vocabularies.views:create_names_blueprint_from_app invenio_vocabularies_subjects = invenio_vocabularies.views:create_subjects_blueprint_from_app invenio_vocabularies_ext = invenio_vocabularies.views:blueprint - invenio_vocabularies_list = invenio_vocabularies.views: + invenio_vocabularies_list = invenio_vocabularies.views:create_list_blueprint_from_app invenio_base.api_finalize_app = invenio_vocabularies = invenio_vocabularies.ext:api_finalize_app invenio_base.finalize_app = From ff5027045252999919d1ea763cad4f65d762c8be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karl-Ulrich=20Kr=C3=A4gelin?= Date: Thu, 23 May 2024 08:44:54 +0200 Subject: [PATCH 11/38] cleanup --- .../administration/views/vocabularies.py | 3 +- invenio_vocabularies/resources/config.py | 28 ++++++++++--------- invenio_vocabularies/resources/resource.py | 27 ++++++++---------- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/invenio_vocabularies/administration/views/vocabularies.py b/invenio_vocabularies/administration/views/vocabularies.py index 887f9ee5..161bec69 100644 --- a/invenio_vocabularies/administration/views/vocabularies.py +++ b/invenio_vocabularies/administration/views/vocabularies.py @@ -19,7 +19,7 @@ class VocabulariesListView(AdminResourceListView): """Configuration for vocabularies list view.""" - api_endpoint = "/vocabularies" + api_endpoint = "/vocabularies/" name = "Vocabularies" resource_config = "resource" search_request_headers = {"Accept": "application/json"} @@ -51,6 +51,7 @@ class VocabularyTypesDetailsView(AdminResourceListView): name = "Vocabularies_Detail" url = "/vocabularies/" + # FIXME the is not expaned correctly but rather gets passed as an url encoded string like GET /api/vocabularies/%3Cpid_value%3E?q= api_endpoint = "/vocabularies/" # name of the resource's list view name, enables navigation between detail view and list view. diff --git a/invenio_vocabularies/resources/config.py b/invenio_vocabularies/resources/config.py index cf10889e..790dfd57 100644 --- a/invenio_vocabularies/resources/config.py +++ b/invenio_vocabularies/resources/config.py @@ -30,15 +30,6 @@ from .serializer import VocabularyL10NItemSchema -class TasksResourceConfig(ResourceConfig, ConfiguratorMixin): - """Celery tasks resource config.""" - - # Blueprint configuration - blueprint_name = "tasks" - url_prefix = "/tasks" - routes = {"list": ""} - - class VocabularySearchRequestArgsSchema(SearchRequestArgsSchema): """Vocabularies search request parameters.""" @@ -88,15 +79,15 @@ class VocabularyTypeResourceConfig(ResourceConfig, ConfiguratorMixin): routes = { "all": "/", - "list": "/vocabularies/", - "item": "/vocabularies//", + "list": "/vocabularies/", + "item": "/vocabularies//", } # Request parsing request_read_args = {} request_view_args = { - "vocabulary_id": ma.fields.String, - "vocabulary_type_id": ma.fields.String, + "pid_value": ma.fields.String, + "type": ma.fields.String, } request_search_args = VocabularySearchRequestArgsSchema @@ -104,3 +95,14 @@ class VocabularyTypeResourceConfig(ResourceConfig, ConfiguratorMixin): **ErrorHandlersMixin.error_handlers, # TODO: Add custom error handlers here } + response_handlers = { + "application/json": ResponseHandler(JSONSerializer(), headers=etag_headers), + "application/vnd.inveniordm.v1+json": ResponseHandler( + MarshmallowSerializer( + format_serializer_cls=JSONSerializer, + object_schema_cls=VocabularyL10NItemSchema, + list_schema_cls=BaseListSchema, + ), + headers=etag_headers, + ), + } diff --git a/invenio_vocabularies/resources/resource.py b/invenio_vocabularies/resources/resource.py index 7779ce7f..8b5489db 100644 --- a/invenio_vocabularies/resources/resource.py +++ b/invenio_vocabularies/resources/resource.py @@ -57,26 +57,12 @@ def create_url_rules(self): """Create the URL rules for the record resource.""" routes = self.config.routes rules = super().create_url_rules() + rules.append( route("POST", routes["tasks"], self.launch), ) - # # Add "vocabularies/" route - # rules.append( - # route("GET", routes["all"], self.get_all), - # ) return rules - # @request_search_args - # @response_handler(many=True) - # def get_all(self): - # """Return information about _all_ vocabularies.""" - # config = current_service.config - # vocabtypeservice = VocabularyTypeService(config) - # identity = g.identity - # hits = vocabtypeservice.search(identity) - - # return hits.to_dict(), 200 - @request_search_args @request_view_args @response_handler(many=True) @@ -173,3 +159,14 @@ def get_all_vocabulary_types(self): hits = vocabtypeservice.search(identity) return hits.to_dict(), 200 + + @request_view_args + @response_handler() + def read(self): + """Read an item.""" + pid_value = ( + resource_requestctx.view_args["type"], + resource_requestctx.view_args["pid_value"], + ) + item = self.service.read(g.identity, pid_value) + return item.to_dict(), 200 From 8965685ce059ee7801678ddbded70624dd99814a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karl-Ulrich=20Kr=C3=A4gelin?= Date: Thu, 23 May 2024 11:23:44 +0200 Subject: [PATCH 12/38] overwrite api endpoint --- .../administration/views/vocabularies.py | 32 +++++++++++++------ invenio_vocabularies/config.py | 9 +++--- 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/invenio_vocabularies/administration/views/vocabularies.py b/invenio_vocabularies/administration/views/vocabularies.py index 161bec69..7aa0e3ea 100644 --- a/invenio_vocabularies/administration/views/vocabularies.py +++ b/invenio_vocabularies/administration/views/vocabularies.py @@ -49,31 +49,45 @@ class VocabulariesListView(AdminResourceListView): class VocabularyTypesDetailsView(AdminResourceListView): """Configuration for vocabularies list view.""" + def get_api_endpoint(self, pid_value=None): + # overwrite get_api_endpoint to accept pid_value + + return f"/api/vocabularies/{pid_value}" + name = "Vocabularies_Detail" url = "/vocabularies/" - # FIXME the is not expaned correctly but rather gets passed as an url encoded string like GET /api/vocabularies/%3Cpid_value%3E?q= - api_endpoint = "/vocabularies/" + # FIXME the is not expaned correctly but rather gets passed + # as an url encoded string like GET /api/vocabularies/%3Cpid_value%3E?q= - # name of the resource's list view name, enables navigation between detail view and list view. + api_endpoint = "/vocabularies/" + + # INFO name of the resource's list view name, enables navigation between detail view and list view. list_view_name = "Vocabularies" resource_config = "vocabulary_admin_resource" search_request_headers = {"Accept": "application/json"} + # TODO The title should contain the as well + # title = f"{pid_value} Detail" title = "Vocabularies Detail" pid_path = "id" - pid_value = "id" - # only if disabled() (as a function) its not in the sidebar, see https://github.com/inveniosoftware/invenio-administration/blob/main/invenio_administration/menu/menu.py#L54 + # pid_value = "id" + # INFO only if disabled() (as a function) its not in the sidebar, see https://github.com/inveniosoftware/invenio-administration/blob/main/invenio_administration/menu/menu.py#L54 disabled = lambda _: True - list_view_name = "Vocabularies" template = "invenio_administration/search.html" + display_delete = False display_create = False display_edit = False - display_search = True + display_search = False + item_field_list = { - "id": {"text": "Name", "order": 1}, + "id": {"text": "Name", "order": 0}, + "created": {"text": "Created", "order": 1}, } search_config_name = "VOCABULARIES_SEARCH" search_facets_config_name = "VOCABULARIES_FACETS" search_sort_config_name = "VOCABULARIES_SORT_OPTIONS" - resource_name = "resource" + + # TODO what is this for? + # "defines a path to human-readable attribute of the resource (title/name etc.)" + # resource_name = "id" diff --git a/invenio_vocabularies/config.py b/invenio_vocabularies/config.py index 50c160d9..0a9a0965 100644 --- a/invenio_vocabularies/config.py +++ b/invenio_vocabularies/config.py @@ -137,20 +137,21 @@ } """Data Streams writers.""" + VOCABULARIES_SORT_OPTIONS = { "name": dict( title=_("Name"), fields=["id"], ), - "entries": dict( - title=_("entries"), - fields=["count"], + "created": dict( + title=_("created"), + fields=["created"], ), } """Definitions of available Vocabularies sort options. """ VOCABULARIES_SEARCH = { "facets": [], - "sort": ["name", "entries"], + "sort": ["name", "created"], } """Vocabularies search configuration.""" From f405b4b1974e8f455ca43e7dbeab990c8ae82534 Mon Sep 17 00:00:00 2001 From: Sarah Wiechers Date: Fri, 24 May 2024 10:59:14 +0200 Subject: [PATCH 13/38] Fixes after review. Take out additional list view for now until it is working properly. --- .../administration/__init__.py | 2 +- .../administration/views/vocabularies.py | 55 ++----------------- invenio_vocabularies/config.py | 10 ++-- invenio_vocabularies/ext.py | 2 - invenio_vocabularies/services/service.py | 21 ++++--- setup.cfg | 1 - 6 files changed, 19 insertions(+), 72 deletions(-) diff --git a/invenio_vocabularies/administration/__init__.py b/invenio_vocabularies/administration/__init__.py index 5086b5e3..f27a100e 100644 --- a/invenio_vocabularies/administration/__init__.py +++ b/invenio_vocabularies/administration/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2020-2021 CERN. +# Copyright (C) 2024 CERN. # Copyright (C) 2024 Uni Münster. # # Invenio-Vocabularies is free software; you can redistribute it and/or diff --git a/invenio_vocabularies/administration/views/vocabularies.py b/invenio_vocabularies/administration/views/vocabularies.py index 7aa0e3ea..d56133d1 100644 --- a/invenio_vocabularies/administration/views/vocabularies.py +++ b/invenio_vocabularies/administration/views/vocabularies.py @@ -25,7 +25,7 @@ class VocabulariesListView(AdminResourceListView): search_request_headers = {"Accept": "application/json"} title = "Vocabulary" category = "Site management" - # pid_path ist das mapping in welchem JSON key die ID des eintrags steht + pid_path = "id" icon = "exchange" template = "invenio_administration/search.html" @@ -39,55 +39,8 @@ class VocabulariesListView(AdminResourceListView): "count": {"text": "Number of entries", "order": 2}, } - search_config_name = "VOCABULARIES_SEARCH" - search_facets_config_name = "VOCABULARIES_FACETS" - search_sort_config_name = "VOCABULARIES_SORT_OPTIONS" + search_config_name = "VOCABULARIES_TYPES_SEARCH" + search_facets_config_name = "VOCABULARIES_TYPES_FACETS" + search_sort_config_name = "VOCABULARIES_TYPES_SORT_OPTIONS" resource_name = "vocabulary_admin_resource" - - -class VocabularyTypesDetailsView(AdminResourceListView): - """Configuration for vocabularies list view.""" - - def get_api_endpoint(self, pid_value=None): - # overwrite get_api_endpoint to accept pid_value - - return f"/api/vocabularies/{pid_value}" - - name = "Vocabularies_Detail" - url = "/vocabularies/" - # FIXME the is not expaned correctly but rather gets passed - # as an url encoded string like GET /api/vocabularies/%3Cpid_value%3E?q= - - api_endpoint = "/vocabularies/" - - # INFO name of the resource's list view name, enables navigation between detail view and list view. - list_view_name = "Vocabularies" - resource_config = "vocabulary_admin_resource" - search_request_headers = {"Accept": "application/json"} - # TODO The title should contain the as well - # title = f"{pid_value} Detail" - title = "Vocabularies Detail" - pid_path = "id" - # pid_value = "id" - # INFO only if disabled() (as a function) its not in the sidebar, see https://github.com/inveniosoftware/invenio-administration/blob/main/invenio_administration/menu/menu.py#L54 - disabled = lambda _: True - - template = "invenio_administration/search.html" - - display_delete = False - display_create = False - display_edit = False - display_search = False - - item_field_list = { - "id": {"text": "Name", "order": 0}, - "created": {"text": "Created", "order": 1}, - } - search_config_name = "VOCABULARIES_SEARCH" - search_facets_config_name = "VOCABULARIES_FACETS" - search_sort_config_name = "VOCABULARIES_SORT_OPTIONS" - - # TODO what is this for? - # "defines a path to human-readable attribute of the resource (title/name etc.)" - # resource_name = "id" diff --git a/invenio_vocabularies/config.py b/invenio_vocabularies/config.py index 0a9a0965..c78aecf3 100644 --- a/invenio_vocabularies/config.py +++ b/invenio_vocabularies/config.py @@ -112,7 +112,6 @@ "subjects", ] - VOCABULARIES_DATASTREAM_READERS = { "csv": CSVReader, "json": JsonReader, @@ -137,8 +136,7 @@ } """Data Streams writers.""" - -VOCABULARIES_SORT_OPTIONS = { +VOCABULARIES_TYPES_SORT_OPTIONS = { "name": dict( title=_("Name"), fields=["id"], @@ -148,10 +146,10 @@ fields=["created"], ), } -"""Definitions of available Vocabularies sort options. """ +"""Definitions of available Vocabulary types sort options. """ -VOCABULARIES_SEARCH = { +VOCABULARIES_TYPES_SEARCH = { "facets": [], "sort": ["name", "created"], } -"""Vocabularies search configuration.""" +"""Vocabulary type search configuration.""" diff --git a/invenio_vocabularies/ext.py b/invenio_vocabularies/ext.py index 0f37d49e..904dda63 100644 --- a/invenio_vocabularies/ext.py +++ b/invenio_vocabularies/ext.py @@ -129,12 +129,10 @@ def init_resource(self, app): config=SubjectsResourceConfig, ) self.resource = VocabulariesResource( - # connects resource with the config service=self.service, config=app.config["VOCABULARIES_RESOURCE_CONFIG"], ) self.vocabulary_admin_resource = VocabulariesAdminResource( - # should connect the vocabulary_admin_resource with the config service=self.service, config=VocabularyTypeResourceConfig, ) diff --git a/invenio_vocabularies/services/service.py b/invenio_vocabularies/services/service.py index 80399eb8..773463b5 100644 --- a/invenio_vocabularies/services/service.py +++ b/invenio_vocabularies/services/service.py @@ -58,12 +58,12 @@ class VocabularyMetadataList(ServiceListResult): """Ensures that vocabulary metadata is returned in the proper format.""" def __init__( - self, - service, - identity, - results, - links_tpl=None, - links_item_tpl=None, + self, + service, + identity, + results, + links_tpl=None, + links_item_tpl=None, ): """Constructor. @@ -124,7 +124,6 @@ def search(self, identity): vocabulary_types = VocabularyType.query.all() - # config_vocab_types = current_app.config["INVENIO_VOCABULARY_TYPE_METADATA"] config_vocab_types = current_app.config.get( "INVENIO_VOCABULARY_TYPE_METADATA", {} ) @@ -144,7 +143,7 @@ def search(self, identity): "pid_type": db_vocab_type.pid_type, "count": count_terms_agg.get(db_vocab_type.id, 0), "is_custom_vocabulary": db_vocab_type.id - in self.custom_vocabulary_names, + in self.custom_vocabulary_names, } if db_vocab_type.id in config_vocab_types: @@ -193,8 +192,8 @@ class VocabularySearchOptions(SearchOptions): """Search options.""" params_interpreters_cls = [ - FilterParam.factory(param="tags", field="tags"), - ] + SearchOptions.params_interpreters_cls + FilterParam.factory(param="tags", field="tags"), + ] + SearchOptions.params_interpreters_cls suggest_parser_cls = SuggestQueryParser.factory( fields=[ @@ -296,7 +295,7 @@ def create_type(self, identity, id, pid_type, uow=None): return type_ def search( - self, identity, params=None, search_preference=None, type=None, **kwargs + self, identity, params=None, search_preference=None, type=None, **kwargs ): """Search for vocabulary entries.""" self.require_permission(identity, "search") diff --git a/setup.cfg b/setup.cfg index 3f421250..a9aab09d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -58,7 +58,6 @@ flask.commands = vocabularies = invenio_vocabularies.cli:vocabularies invenio_administration.views = vocabularies_list = invenio_vocabularies.administration.views.vocabularies:VocabulariesListView - vocabulary_details = invenio_vocabularies.administration.views.vocabularies:VocabularyTypesDetailsView invenio_base.apps = invenio_vocabularies = invenio_vocabularies:InvenioVocabularies invenio_base.api_apps = From 3601b1511f1d94188a32ccead567f9eb56c952c6 Mon Sep 17 00:00:00 2001 From: Sarah Wiechers Date: Fri, 24 May 2024 13:17:58 +0200 Subject: [PATCH 14/38] Fixed formatting and doc strings. --- .../administration/views/vocabularies.py | 10 +++++----- invenio_vocabularies/config.py | 8 ++++---- invenio_vocabularies/ext.py | 9 ++++----- invenio_vocabularies/resources/__init__.py | 5 +++-- invenio_vocabularies/resources/config.py | 15 ++++++++------- invenio_vocabularies/resources/resource.py | 2 ++ 6 files changed, 26 insertions(+), 23 deletions(-) diff --git a/invenio_vocabularies/administration/views/vocabularies.py b/invenio_vocabularies/administration/views/vocabularies.py index d56133d1..88291f25 100644 --- a/invenio_vocabularies/administration/views/vocabularies.py +++ b/invenio_vocabularies/administration/views/vocabularies.py @@ -9,9 +9,9 @@ """Vocabularies admin interface.""" from invenio_administration.views.base import ( - AdminResourceListView, - AdminResourceEditView, AdminResourceDetailView, + AdminResourceEditView, + AdminResourceListView, ) from invenio_i18n import lazy_gettext as _ @@ -21,7 +21,7 @@ class VocabulariesListView(AdminResourceListView): api_endpoint = "/vocabularies/" name = "Vocabularies" - resource_config = "resource" + resource_config = "vocabulary_admin_resource" search_request_headers = {"Accept": "application/json"} title = "Vocabulary" category = "Site management" @@ -36,11 +36,11 @@ class VocabulariesListView(AdminResourceListView): item_field_list = { "id": {"text": "Name", "order": 1}, - "count": {"text": "Number of entries", "order": 2}, + "entries": {"text": "Number of entries", "order": 2}, } search_config_name = "VOCABULARIES_TYPES_SEARCH" search_facets_config_name = "VOCABULARIES_TYPES_FACETS" search_sort_config_name = "VOCABULARIES_TYPES_SORT_OPTIONS" - resource_name = "vocabulary_admin_resource" + # resource_name = "vocabulary_admin_resource" diff --git a/invenio_vocabularies/config.py b/invenio_vocabularies/config.py index c78aecf3..0375b903 100644 --- a/invenio_vocabularies/config.py +++ b/invenio_vocabularies/config.py @@ -141,15 +141,15 @@ title=_("Name"), fields=["id"], ), - "created": dict( - title=_("created"), - fields=["created"], + "entries": dict( + title=_("Number of entries"), + fields=["count"], ), } """Definitions of available Vocabulary types sort options. """ VOCABULARIES_TYPES_SEARCH = { "facets": [], - "sort": ["name", "created"], + "sort": ["name", "entries"], } """Vocabulary type search configuration.""" diff --git a/invenio_vocabularies/ext.py b/invenio_vocabularies/ext.py index 904dda63..53a8ce8b 100644 --- a/invenio_vocabularies/ext.py +++ b/invenio_vocabularies/ext.py @@ -40,17 +40,16 @@ SubjectsService, SubjectsServiceConfig, ) - -# from .contrib.information import InformationResource, InformationResourceConfig - from .resources import ( - VocabularyTypeResourceConfig, - VocabulariesResource, VocabulariesAdminResource, + VocabulariesResource, VocabulariesResourceConfig, + VocabularyTypeResourceConfig, ) from .services.service import VocabulariesService +# from .contrib.information import InformationResource, InformationResourceConfig + class InvenioVocabularies(object): """Invenio-Vocabularies extension.""" diff --git a/invenio_vocabularies/resources/__init__.py b/invenio_vocabularies/resources/__init__.py index 6d86c1a3..486e905e 100644 --- a/invenio_vocabularies/resources/__init__.py +++ b/invenio_vocabularies/resources/__init__.py @@ -8,8 +8,9 @@ """Resources module.""" from invenio_vocabularies.resources.schema import L10NString, VocabularyL10Schema -from .config import VocabularyTypeResourceConfig, VocabulariesResourceConfig -from .resource import VocabulariesResource, VocabulariesAdminResource + +from .config import VocabulariesResourceConfig, VocabularyTypeResourceConfig +from .resource import VocabulariesAdminResource, VocabulariesResource __all__ = ( "VocabularyL10Schema", diff --git a/invenio_vocabularies/resources/config.py b/invenio_vocabularies/resources/config.py index 790dfd57..2b1f3080 100644 --- a/invenio_vocabularies/resources/config.py +++ b/invenio_vocabularies/resources/config.py @@ -10,23 +10,24 @@ import marshmallow as ma from flask_resources import ( + BaseListSchema, HTTPJSONException, - ResourceConfig, - create_error_handler, JSONSerializer, - BaseListSchema, MarshmallowSerializer, + ResourceConfig, ResponseHandler, + create_error_handler, ) -from invenio_records_resources.resources.records.headers import etag_headers -from invenio_records_resources.resources.errors import ErrorHandlersMixin -from invenio_records_resources.resources.records.args import SearchRequestArgsSchema -from invenio_records_resources.services.base.config import ConfiguratorMixin from invenio_records_resources.resources import ( RecordResource, RecordResourceConfig, SearchRequestArgsSchema, ) +from invenio_records_resources.resources.errors import ErrorHandlersMixin +from invenio_records_resources.resources.records.args import SearchRequestArgsSchema +from invenio_records_resources.resources.records.headers import etag_headers +from invenio_records_resources.services.base.config import ConfiguratorMixin + from .serializer import VocabularyL10NItemSchema diff --git a/invenio_vocabularies/resources/resource.py b/invenio_vocabularies/resources/resource.py index 8b5489db..8bdbaa7f 100644 --- a/invenio_vocabularies/resources/resource.py +++ b/invenio_vocabularies/resources/resource.py @@ -139,6 +139,8 @@ def launch(self): class VocabulariesAdminResource(RecordResource): + """Resource for vocabularies admin interface.""" + def create_url_rules(self): """Create the URL rules for the record resource.""" routes = self.config.routes From b2a3c93d68b8a3159e720767aa11d75ea7a7b86b Mon Sep 17 00:00:00 2001 From: Sarah Wiechers Date: Fri, 24 May 2024 13:28:21 +0200 Subject: [PATCH 15/38] Fixed formatting --- invenio_vocabularies/services/service.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/invenio_vocabularies/services/service.py b/invenio_vocabularies/services/service.py index 773463b5..87e3ea52 100644 --- a/invenio_vocabularies/services/service.py +++ b/invenio_vocabularies/services/service.py @@ -58,12 +58,12 @@ class VocabularyMetadataList(ServiceListResult): """Ensures that vocabulary metadata is returned in the proper format.""" def __init__( - self, - service, - identity, - results, - links_tpl=None, - links_item_tpl=None, + self, + service, + identity, + results, + links_tpl=None, + links_item_tpl=None, ): """Constructor. @@ -143,7 +143,7 @@ def search(self, identity): "pid_type": db_vocab_type.pid_type, "count": count_terms_agg.get(db_vocab_type.id, 0), "is_custom_vocabulary": db_vocab_type.id - in self.custom_vocabulary_names, + in self.custom_vocabulary_names, } if db_vocab_type.id in config_vocab_types: @@ -192,8 +192,8 @@ class VocabularySearchOptions(SearchOptions): """Search options.""" params_interpreters_cls = [ - FilterParam.factory(param="tags", field="tags"), - ] + SearchOptions.params_interpreters_cls + FilterParam.factory(param="tags", field="tags"), + ] + SearchOptions.params_interpreters_cls suggest_parser_cls = SuggestQueryParser.factory( fields=[ @@ -295,7 +295,7 @@ def create_type(self, identity, id, pid_type, uow=None): return type_ def search( - self, identity, params=None, search_preference=None, type=None, **kwargs + self, identity, params=None, search_preference=None, type=None, **kwargs ): """Search for vocabulary entries.""" self.require_permission(identity, "search") From 0fc61ae5c8902b4b7f01db8485059cc787a34441 Mon Sep 17 00:00:00 2001 From: Sarah Wiechers Date: Tue, 28 May 2024 09:23:15 +0200 Subject: [PATCH 16/38] vocabulary types: Implement query functionality --- invenio_vocabularies/resources/resource.py | 2 +- invenio_vocabularies/services/service.py | 96 ++++++++++++++++++---- 2 files changed, 79 insertions(+), 19 deletions(-) diff --git a/invenio_vocabularies/resources/resource.py b/invenio_vocabularies/resources/resource.py index 8bdbaa7f..229c7e5e 100644 --- a/invenio_vocabularies/resources/resource.py +++ b/invenio_vocabularies/resources/resource.py @@ -158,7 +158,7 @@ def get_all_vocabulary_types(self): config = current_service.config vocabtypeservice = VocabularyTypeService(config) identity = g.identity - hits = vocabtypeservice.search(identity) + hits = vocabtypeservice.search(identity, params=resource_requestctx.args) return hits.to_dict(), 200 diff --git a/invenio_vocabularies/services/service.py b/invenio_vocabularies/services/service.py index 87e3ea52..942a75f7 100644 --- a/invenio_vocabularies/services/service.py +++ b/invenio_vocabularies/services/service.py @@ -12,6 +12,7 @@ from invenio_cache import current_cache from invenio_db import db from invenio_i18n import lazy_gettext as _ +from invenio_records_resources.pagination import Pagination from invenio_records_resources.proxies import current_service_registry from invenio_records_resources.services import ( Link, @@ -26,6 +27,7 @@ Service, ServiceListResult, ) +from invenio_records_resources.services.base.utils import map_search_params from invenio_records_resources.services.errors import PermissionDeniedError from invenio_records_resources.services.records.components import DataComponent from invenio_records_resources.services.records.params import ( @@ -36,6 +38,8 @@ from invenio_records_resources.services.uow import unit_of_work from invenio_search import current_search_client from invenio_search.engine import dsl +from sqlalchemy import asc, desc, or_ +from sqlalchemy.sql import text from invenio_vocabularies.proxies import current_service @@ -58,12 +62,13 @@ class VocabularyMetadataList(ServiceListResult): """Ensures that vocabulary metadata is returned in the proper format.""" def __init__( - self, - service, - identity, - results, - links_tpl=None, - links_item_tpl=None, + self, + service, + identity, + results, + params=None, + links_tpl=None, + links_item_tpl=None, ): """Constructor. @@ -73,10 +78,25 @@ def __init__( """ self._identity = identity self._results = results + self._params = params self._service = service self._links_tpl = links_tpl self._links_item_tpl = links_item_tpl + @property + def total(self): + """Get total number of hits.""" + return len(list(self._results)) + + @property + def pagination(self): + """Create a pagination object.""" + return Pagination( + self._params["size"], + self._params["page"], + self.total, + ) + def to_dict(self): """Formats result to a dict of hits.""" hits = list(self._results) @@ -88,17 +108,18 @@ def to_dict(self): res = { "hits": { "hits": hits, - "total": len(hits), + "total": self.total, } } - if self._links_tpl: - res["links"] = self._links_tpl.expand(self._identity, None) + if self._params: + if self._links_tpl: + res["links"] = self._links_tpl.expand(self._identity, self.pagination) return res -class VocabularyTypeService(Service): +class VocabularyTypeService(RecordService): """Vocabulary type service.""" @property @@ -118,11 +139,28 @@ def custom_vocabulary_names(self): """Checks whether vocabulary is a custom vocabulary.""" return current_app.config.get("VOCABULARIES_CUSTOM_VOCABULARY_TYPES", []) - def search(self, identity): + def search(self, identity, params=None): """Search for vocabulary types entries.""" self.require_permission(identity, "list_vocabularies") - vocabulary_types = VocabularyType.query.all() + search_params = map_search_params(self.config.search, params) + + query_param = search_params["q"] + filters = [] + + if query_param: + filters.extend([VocabularyType.id.ilike(f"%{query_param}%")]) + + vocabulary_types = ( + VocabularyType.query.filter(or_(*filters)).order_by( + search_params["sort_direction"](text(",".join(search_params["sort"]))) + ) + # .paginate( + # page=search_params["page"], + # per_page=search_params["size"], + # error_out=False, + # ) + ) config_vocab_types = current_app.config.get( "INVENIO_VOCABULARY_TYPE_METADATA", {} @@ -143,7 +181,7 @@ def search(self, identity): "pid_type": db_vocab_type.pid_type, "count": count_terms_agg.get(db_vocab_type.id, 0), "is_custom_vocabulary": db_vocab_type.id - in self.custom_vocabulary_names, + in self.custom_vocabulary_names, } if db_vocab_type.id in config_vocab_types: @@ -156,7 +194,8 @@ def search(self, identity): self, identity, results, - links_tpl=LinksTemplate({"self": Link("{+api}/vocabularies")}), + params, + links_tpl=LinksTemplate(self.config.links_search, context={"args": params}), links_item_tpl=self.links_item_tpl, ) @@ -192,8 +231,8 @@ class VocabularySearchOptions(SearchOptions): """Search options.""" params_interpreters_cls = [ - FilterParam.factory(param="tags", field="tags"), - ] + SearchOptions.params_interpreters_cls + FilterParam.factory(param="tags", field="tags"), + ] + SearchOptions.params_interpreters_cls suggest_parser_cls = SuggestQueryParser.factory( fields=[ @@ -206,10 +245,23 @@ class VocabularySearchOptions(SearchOptions): ], ) - sort_default = "bestmatch" + sort_default = "id" + + sort_direction_default = "asc" sort_default_no_query = "title" + sort_direction_options = { + "asc": dict( + title=_("Ascending"), + fn=asc, + ), + "desc": dict( + title=_("Descending"), + fn=desc, + ), + } + sort_options = { "bestmatch": dict( title=_("Best match"), @@ -227,6 +279,14 @@ class VocabularySearchOptions(SearchOptions): title=_("Oldest"), fields=["created"], ), + "id": dict( + title=_("ID"), + fields=["id"], + ) + } + + pagination_options = { + "default_results_per_page": 10, } @@ -295,7 +355,7 @@ def create_type(self, identity, id, pid_type, uow=None): return type_ def search( - self, identity, params=None, search_preference=None, type=None, **kwargs + self, identity, params=None, search_preference=None, type=None, **kwargs ): """Search for vocabulary entries.""" self.require_permission(identity, "search") From 43f25dadc4dd89e509fdf3141f71e2cac9dcaf98 Mon Sep 17 00:00:00 2001 From: Sarah Wiechers Date: Tue, 28 May 2024 10:18:32 +0200 Subject: [PATCH 17/38] vocabulary types: Show count in list view --- .../administration/views/vocabularies.py | 2 +- invenio_vocabularies/config.py | 4 ++-- invenio_vocabularies/services/service.py | 24 +++++++++---------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/invenio_vocabularies/administration/views/vocabularies.py b/invenio_vocabularies/administration/views/vocabularies.py index 88291f25..f5a870b3 100644 --- a/invenio_vocabularies/administration/views/vocabularies.py +++ b/invenio_vocabularies/administration/views/vocabularies.py @@ -36,7 +36,7 @@ class VocabulariesListView(AdminResourceListView): item_field_list = { "id": {"text": "Name", "order": 1}, - "entries": {"text": "Number of entries", "order": 2}, + "count": {"text": "Number of entries", "order": 2}, } search_config_name = "VOCABULARIES_TYPES_SEARCH" diff --git a/invenio_vocabularies/config.py b/invenio_vocabularies/config.py index 0375b903..def58178 100644 --- a/invenio_vocabularies/config.py +++ b/invenio_vocabularies/config.py @@ -141,7 +141,7 @@ title=_("Name"), fields=["id"], ), - "entries": dict( + "count": dict( title=_("Number of entries"), fields=["count"], ), @@ -150,6 +150,6 @@ VOCABULARIES_TYPES_SEARCH = { "facets": [], - "sort": ["name", "entries"], + "sort": ["name", "count"], } """Vocabulary type search configuration.""" diff --git a/invenio_vocabularies/services/service.py b/invenio_vocabularies/services/service.py index 942a75f7..c25c7c2f 100644 --- a/invenio_vocabularies/services/service.py +++ b/invenio_vocabularies/services/service.py @@ -62,13 +62,13 @@ class VocabularyMetadataList(ServiceListResult): """Ensures that vocabulary metadata is returned in the proper format.""" def __init__( - self, - service, - identity, - results, - params=None, - links_tpl=None, - links_item_tpl=None, + self, + service, + identity, + results, + params=None, + links_tpl=None, + links_item_tpl=None, ): """Constructor. @@ -181,7 +181,7 @@ def search(self, identity, params=None): "pid_type": db_vocab_type.pid_type, "count": count_terms_agg.get(db_vocab_type.id, 0), "is_custom_vocabulary": db_vocab_type.id - in self.custom_vocabulary_names, + in self.custom_vocabulary_names, } if db_vocab_type.id in config_vocab_types: @@ -231,8 +231,8 @@ class VocabularySearchOptions(SearchOptions): """Search options.""" params_interpreters_cls = [ - FilterParam.factory(param="tags", field="tags"), - ] + SearchOptions.params_interpreters_cls + FilterParam.factory(param="tags", field="tags"), + ] + SearchOptions.params_interpreters_cls suggest_parser_cls = SuggestQueryParser.factory( fields=[ @@ -282,7 +282,7 @@ class VocabularySearchOptions(SearchOptions): "id": dict( title=_("ID"), fields=["id"], - ) + ), } pagination_options = { @@ -355,7 +355,7 @@ def create_type(self, identity, id, pid_type, uow=None): return type_ def search( - self, identity, params=None, search_preference=None, type=None, **kwargs + self, identity, params=None, search_preference=None, type=None, **kwargs ): """Search for vocabulary entries.""" self.require_permission(identity, "search") From 9de7fd2d2cf5b33be024d52d56c439557b2a1cf0 Mon Sep 17 00:00:00 2001 From: Sarah Wiechers Date: Tue, 28 May 2024 12:59:00 +0200 Subject: [PATCH 18/38] vocabulary types: Implement sorting --- invenio_vocabularies/services/service.py | 51 +++++++++++------------- 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/invenio_vocabularies/services/service.py b/invenio_vocabularies/services/service.py index c25c7c2f..6f29b6f6 100644 --- a/invenio_vocabularies/services/service.py +++ b/invenio_vocabularies/services/service.py @@ -8,6 +8,8 @@ # details. """Vocabulary service.""" +from functools import partial + from flask import current_app from invenio_cache import current_cache from invenio_db import db @@ -38,8 +40,6 @@ from invenio_records_resources.services.uow import unit_of_work from invenio_search import current_search_client from invenio_search.engine import dsl -from sqlalchemy import asc, desc, or_ -from sqlalchemy.sql import text from invenio_vocabularies.proxies import current_service @@ -62,13 +62,13 @@ class VocabularyMetadataList(ServiceListResult): """Ensures that vocabulary metadata is returned in the proper format.""" def __init__( - self, - service, - identity, - results, - params=None, - links_tpl=None, - links_item_tpl=None, + self, + service, + identity, + results, + params=None, + links_tpl=None, + links_item_tpl=None, ): """Constructor. @@ -143,24 +143,21 @@ def search(self, identity, params=None): """Search for vocabulary types entries.""" self.require_permission(identity, "list_vocabularies") + vocabulary_types = VocabularyType.query.all() + search_params = map_search_params(self.config.search, params) query_param = search_params["q"] - filters = [] if query_param: - filters.extend([VocabularyType.id.ilike(f"%{query_param}%")]) + vocabulary_types = [ + voc_type + for voc_type in vocabulary_types + if (query_param in voc_type.id.lower()) + ] - vocabulary_types = ( - VocabularyType.query.filter(or_(*filters)).order_by( - search_params["sort_direction"](text(",".join(search_params["sort"]))) - ) - # .paginate( - # page=search_params["page"], - # per_page=search_params["size"], - # error_out=False, - # ) - ) + sort_direction = search_params["sort_direction"] + vocabulary_types = sort_direction(vocabulary_types) config_vocab_types = current_app.config.get( "INVENIO_VOCABULARY_TYPE_METADATA", {} @@ -181,7 +178,7 @@ def search(self, identity, params=None): "pid_type": db_vocab_type.pid_type, "count": count_terms_agg.get(db_vocab_type.id, 0), "is_custom_vocabulary": db_vocab_type.id - in self.custom_vocabulary_names, + in self.custom_vocabulary_names, } if db_vocab_type.id in config_vocab_types: @@ -231,8 +228,8 @@ class VocabularySearchOptions(SearchOptions): """Search options.""" params_interpreters_cls = [ - FilterParam.factory(param="tags", field="tags"), - ] + SearchOptions.params_interpreters_cls + FilterParam.factory(param="tags", field="tags"), + ] + SearchOptions.params_interpreters_cls suggest_parser_cls = SuggestQueryParser.factory( fields=[ @@ -254,11 +251,11 @@ class VocabularySearchOptions(SearchOptions): sort_direction_options = { "asc": dict( title=_("Ascending"), - fn=asc, + fn=partial(sorted, key=lambda t: t.id), ), "desc": dict( title=_("Descending"), - fn=desc, + fn=partial(sorted, key=lambda t: t.id, reverse=True), ), } @@ -355,7 +352,7 @@ def create_type(self, identity, id, pid_type, uow=None): return type_ def search( - self, identity, params=None, search_preference=None, type=None, **kwargs + self, identity, params=None, search_preference=None, type=None, **kwargs ): """Search for vocabulary entries.""" self.require_permission(identity, "search") From a5e1f9075e34e8072c1f29763bfa6e6e60802995 Mon Sep 17 00:00:00 2001 From: Sarah Wiechers Date: Thu, 13 Jun 2024 08:56:48 +0200 Subject: [PATCH 19/38] vocabulary type endpoint: fix formatting --- invenio_vocabularies/services/service.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/invenio_vocabularies/services/service.py b/invenio_vocabularies/services/service.py index 6f29b6f6..a8bffb0e 100644 --- a/invenio_vocabularies/services/service.py +++ b/invenio_vocabularies/services/service.py @@ -62,13 +62,13 @@ class VocabularyMetadataList(ServiceListResult): """Ensures that vocabulary metadata is returned in the proper format.""" def __init__( - self, - service, - identity, - results, - params=None, - links_tpl=None, - links_item_tpl=None, + self, + service, + identity, + results, + params=None, + links_tpl=None, + links_item_tpl=None, ): """Constructor. @@ -178,7 +178,7 @@ def search(self, identity, params=None): "pid_type": db_vocab_type.pid_type, "count": count_terms_agg.get(db_vocab_type.id, 0), "is_custom_vocabulary": db_vocab_type.id - in self.custom_vocabulary_names, + in self.custom_vocabulary_names, } if db_vocab_type.id in config_vocab_types: @@ -228,8 +228,8 @@ class VocabularySearchOptions(SearchOptions): """Search options.""" params_interpreters_cls = [ - FilterParam.factory(param="tags", field="tags"), - ] + SearchOptions.params_interpreters_cls + FilterParam.factory(param="tags", field="tags"), + ] + SearchOptions.params_interpreters_cls suggest_parser_cls = SuggestQueryParser.factory( fields=[ @@ -352,7 +352,7 @@ def create_type(self, identity, id, pid_type, uow=None): return type_ def search( - self, identity, params=None, search_preference=None, type=None, **kwargs + self, identity, params=None, search_preference=None, type=None, **kwargs ): """Search for vocabulary entries.""" self.require_permission(identity, "search") From 78bec3dd54e2b1f501ea8660bf180d13c16f7c99 Mon Sep 17 00:00:00 2001 From: Sarah Wiechers Date: Thu, 13 Jun 2024 16:37:48 +0200 Subject: [PATCH 20/38] vocabulary types endpoint: split implementation for service, config and results in different files and cleaned up code. --- .../administration/views/vocabularies.py | 8 +- invenio_vocabularies/config.py | 2 +- invenio_vocabularies/ext.py | 18 +- invenio_vocabularies/resources/resource.py | 9 +- invenio_vocabularies/services/__init__.py | 5 +- invenio_vocabularies/services/config.py | 205 ++++++++++++ invenio_vocabularies/services/results.py | 111 +++++++ invenio_vocabularies/services/service.py | 295 ++---------------- 8 files changed, 357 insertions(+), 296 deletions(-) create mode 100644 invenio_vocabularies/services/config.py create mode 100644 invenio_vocabularies/services/results.py diff --git a/invenio_vocabularies/administration/views/vocabularies.py b/invenio_vocabularies/administration/views/vocabularies.py index f5a870b3..8e9e88d1 100644 --- a/invenio_vocabularies/administration/views/vocabularies.py +++ b/invenio_vocabularies/administration/views/vocabularies.py @@ -9,21 +9,19 @@ """Vocabularies admin interface.""" from invenio_administration.views.base import ( - AdminResourceDetailView, AdminResourceEditView, AdminResourceListView, ) -from invenio_i18n import lazy_gettext as _ class VocabulariesListView(AdminResourceListView): """Configuration for vocabularies list view.""" api_endpoint = "/vocabularies/" - name = "Vocabularies" + name = "vocabulary-types" resource_config = "vocabulary_admin_resource" search_request_headers = {"Accept": "application/json"} - title = "Vocabulary" + title = "Vocabulary Types" category = "Site management" pid_path = "id" @@ -42,5 +40,3 @@ class VocabulariesListView(AdminResourceListView): search_config_name = "VOCABULARIES_TYPES_SEARCH" search_facets_config_name = "VOCABULARIES_TYPES_FACETS" search_sort_config_name = "VOCABULARIES_TYPES_SORT_OPTIONS" - - # resource_name = "vocabulary_admin_resource" diff --git a/invenio_vocabularies/config.py b/invenio_vocabularies/config.py index def58178..56f6c252 100644 --- a/invenio_vocabularies/config.py +++ b/invenio_vocabularies/config.py @@ -26,7 +26,7 @@ from .datastreams.transformers import XMLTransformer from .datastreams.writers import ServiceWriter, YamlWriter from .resources import VocabulariesResourceConfig -from .services.service import VocabulariesServiceConfig +from .services.config import VocabulariesServiceConfig VOCABULARIES_RESOURCE_CONFIG = VocabulariesResourceConfig """Configure the resource.""" diff --git a/invenio_vocabularies/ext.py b/invenio_vocabularies/ext.py index 53a8ce8b..3792f1bc 100644 --- a/invenio_vocabularies/ext.py +++ b/invenio_vocabularies/ext.py @@ -46,7 +46,8 @@ VocabulariesResourceConfig, VocabularyTypeResourceConfig, ) -from .services.service import VocabulariesService +from .services.config import VocabularyTypesServiceConfig +from .services.service import VocabulariesService, VocabularyTypeService # from .contrib.information import InformationResource, InformationResourceConfig @@ -83,6 +84,7 @@ class ServiceConfigs: funders = FundersServiceConfig names = NamesServiceConfig subjects = SubjectsServiceConfig + vocabulary_types = VocabularyTypesServiceConfig return ServiceConfigs @@ -100,9 +102,12 @@ def init_services(self, app): self.funders_service = FundersService(config=service_configs.funders) self.names_service = NamesService(config=service_configs.names) self.subjects_service = SubjectsService(config=service_configs.subjects) - self.service = VocabulariesService( + self.vocabularies_service = VocabulariesService( config=app.config["VOCABULARIES_SERVICE_CONFIG"], ) + self.vocabulary_types_service = VocabularyTypeService( + config=service_configs.vocabulary_types + ) def init_resource(self, app): """Initialize vocabulary resources.""" @@ -128,11 +133,11 @@ def init_resource(self, app): config=SubjectsResourceConfig, ) self.resource = VocabulariesResource( - service=self.service, + service=self.vocabularies_service, config=app.config["VOCABULARIES_RESOURCE_CONFIG"], ) self.vocabulary_admin_resource = VocabulariesAdminResource( - service=self.service, + service=self.vocabulary_types_service, config=VocabularyTypeResourceConfig, ) @@ -164,7 +169,8 @@ def init(app): sregistry.register(ext.funders_service, service_id="funders") sregistry.register(ext.names_service, service_id="names") sregistry.register(ext.subjects_service, service_id="subjects") - sregistry.register(ext.service, service_id="vocabularies") + sregistry.register(ext.vocabularies_service, service_id="vocabularies") + sregistry.register(ext.vocabulary_types_service, service_id="vocabulary-types") # Register indexers iregistry = app.extensions["invenio-indexer"].registry iregistry.register(ext.affiliations_service.indexer, indexer_id="affiliations") @@ -172,4 +178,4 @@ def init(app): iregistry.register(ext.funders_service.indexer, indexer_id="funders") iregistry.register(ext.names_service.indexer, indexer_id="names") iregistry.register(ext.subjects_service.indexer, indexer_id="subjects") - iregistry.register(ext.service.indexer, indexer_id="vocabularies") + iregistry.register(ext.vocabularies_service.indexer, indexer_id="vocabularies") diff --git a/invenio_vocabularies/resources/resource.py b/invenio_vocabularies/resources/resource.py index 229c7e5e..9520a42e 100644 --- a/invenio_vocabularies/resources/resource.py +++ b/invenio_vocabularies/resources/resource.py @@ -38,11 +38,10 @@ from invenio_records_resources.resources.records.utils import search_preference from marshmallow import fields -from invenio_vocabularies.proxies import current_service -from invenio_vocabularies.services.service import VocabularyTypeService - from .serializer import VocabularyL10NItemSchema +# from invenio_vocabularies.proxies import current_service + # # Resource @@ -155,10 +154,8 @@ def create_url_rules(self): @response_handler(many=True) def get_all_vocabulary_types(self): """Return information about _all_ vocabularies.""" - config = current_service.config - vocabtypeservice = VocabularyTypeService(config) identity = g.identity - hits = vocabtypeservice.search(identity, params=resource_requestctx.args) + hits = self.service.search(identity, params=resource_requestctx.args) return hits.to_dict(), 200 diff --git a/invenio_vocabularies/services/__init__.py b/invenio_vocabularies/services/__init__.py index 4fa45609..da6a91ea 100644 --- a/invenio_vocabularies/services/__init__.py +++ b/invenio_vocabularies/services/__init__.py @@ -8,9 +8,12 @@ """Services module.""" -from .service import VocabulariesService, VocabulariesServiceConfig +from .config import VocabulariesServiceConfig, VocabularyTypesServiceConfig +from .service import VocabulariesService, VocabularyTypeService __all__ = ( "VocabulariesService", + "VocabularyTypeService", "VocabulariesServiceConfig", + "VocabularyTypesServiceConfig", ) diff --git a/invenio_vocabularies/services/config.py b/invenio_vocabularies/services/config.py new file mode 100644 index 00000000..9406c668 --- /dev/null +++ b/invenio_vocabularies/services/config.py @@ -0,0 +1,205 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2024 CERN. +# +# Invenio-Vocabularies is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + +"""Vocabulary services configs.""" + +from flask import current_app +from invenio_i18n import lazy_gettext as _ +from invenio_records_resources.services import ( + Link, + LinksTemplate, + RecordService, + RecordServiceConfig, + SearchOptions, + pagination_links, +) +from invenio_records_resources.services.base import ( + ConditionalLink, + Service, + ServiceListResult, +) +from invenio_records_resources.services.records.components import DataComponent +from invenio_records_resources.services.records.params import ( + FilterParam, + SuggestQueryParser, +) +from sqlalchemy import asc, desc + +from ..records.api import Vocabulary +from . import results +from .components import PIDComponent, VocabularyTypeComponent +from .permissions import PermissionPolicy +from .schema import TaskSchema, VocabularySchema + + +def is_custom_vocabulary_type(vocabulary_type, context): + """Check if the vocabulary type is a custom vocabulary type.""" + return vocabulary_type["id"] in current_app.config.get( + "VOCABULARIES_CUSTOM_VOCABULARY_TYPES", [] + ) + + +class VocabularySearchOptions(SearchOptions): + """Search options for vocabularies.""" + + params_interpreters_cls = [ + FilterParam.factory(param="tags", field="tags"), + ] + SearchOptions.params_interpreters_cls + + suggest_parser_cls = SuggestQueryParser.factory( + fields=[ + "id.text^100", + "id.text._2gram", + "id.text._3gram", + "title.en^5", + "title.en._2gram", + "title.en._3gram", + ], + ) + + sort_default = "bestmatch" + + sort_default_no_query = "title" + + sort_options = { + "bestmatch": dict( + title=_("Best match"), + fields=["_score"], # ES defaults to desc on `_score` field + ), + "title": dict( + title=_("Title"), + fields=["title_sort"], + ), + "newest": dict( + title=_("Newest"), + fields=["-created"], + ), + "oldest": dict( + title=_("Oldest"), + fields=["created"], + ), + } + + +class VocabularyTypeSearchOptions(SearchOptions): + """Search options for vocabulary types.""" + + # TODO: Is this still necessary here? + params_interpreters_cls = [ + FilterParam.factory(param="tags", field="tags"), + ] + SearchOptions.params_interpreters_cls + + # TODO: Is this still necessary here? + suggest_parser_cls = SuggestQueryParser.factory( + fields=[ + "id.text^100", + "id.text._2gram", + "id.text._3gram", + ], + ) + + sort_options = { + "id": dict( + title=_("ID"), + fields=["id"], + ), + } + + sort_default = "id" + + sort_default_no_query = "id" + + # TODO: Check if these options are actually necessary + sort_direction_options = { + "asc": dict(title=_("Ascending"), fn=asc), + "desc": dict(title=_("Descending"), fn=desc), + } + + sort_direction_default = "asc" + + +class VocabulariesServiceConfig(RecordServiceConfig): + """Vocabulary service configuration.""" + + service_id = "vocabularies" + indexer_queue_name = "vocabularies" + permission_policy_cls = PermissionPolicy + record_cls = Vocabulary + schema = VocabularySchema + task_schema = TaskSchema + + search = VocabularySearchOptions + + components = [ + # Order of components are important! + VocabularyTypeComponent, + DataComponent, + PIDComponent, + ] + + links_item = { + "self": Link( + "{+api}/vocabularies/{type}/{id}", + vars=lambda record, vars: vars.update( + { + "id": record.pid.pid_value, + "type": record.type.id, + } + ), + ), + } + + links_search = pagination_links("{+api}/vocabularies/{type}{?args*}") + + +class VocabularyTypesServiceConfig(RecordServiceConfig): + """Vocabulary types service configuration.""" + + service_id = "vocabulary_types" + permission_policy_cls = PermissionPolicy + record_cls = Vocabulary # TODO: Is this correct? + schema = VocabularySchema + task_schema = TaskSchema + vocabularies_listing_resultlist_cls = results.VocabularyMetadataList + + vocabularies_listing_item = { + "self": ConditionalLink( + cond=is_custom_vocabulary_type, + if_=Link( + "{+api}/{id}", + vars=lambda vocab_type, vars: vars.update({"id": vocab_type["id"]}), + ), + else_=Link( + "{+api}/vocabularies/{id}", + vars=lambda vocab_type, vars: vars.update({"id": vocab_type["id"]}), + ), + ) + } + + search = VocabularyTypeSearchOptions + + components = [ + # Order of components are important! + VocabularyTypeComponent, + DataComponent, + PIDComponent, + ] + + links_item = { + "self": Link( + "{+api}/vocabularies/{type}/{id}", + vars=lambda record, vars: vars.update( + { + "id": record.pid.pid_value, + "type": record.type.id, + } + ), + ), + } + + links_search = pagination_links("{+api}/vocabularies/{type}{?args*}") diff --git a/invenio_vocabularies/services/results.py b/invenio_vocabularies/services/results.py new file mode 100644 index 00000000..8d0e6479 --- /dev/null +++ b/invenio_vocabularies/services/results.py @@ -0,0 +1,111 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2024 CERN. +# +# Invenio-Vocabularies is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + +"""Vocabulary results.""" + +from flask import current_app +from invenio_records_resources.proxies import current_service_registry +from invenio_records_resources.services import RecordServiceConfig +from invenio_records_resources.services.records.results import RecordList +from invenio_search import current_search_client + +from invenio_vocabularies.proxies import current_service + + +class VocabularyMetadataList(RecordList): + """Ensures that vocabulary metadata is returned in the proper format.""" + + @property + def total(self): + """Get total number of hits.""" + return self._results.total + + @property + def custom_vocabulary_names(self): + """Checks whether vocabulary is a custom vocabulary.""" + return current_app.config.get("VOCABULARIES_CUSTOM_VOCABULARY_TYPES", []) + + def to_dict(self): + """Formats result to a dict of hits.""" + # hits = list(self._results) + + config_vocab_types = current_app.config.get( + "INVENIO_VOCABULARY_TYPE_METADATA", {} + ) + + count_terms_agg = {} + generic_stats = self._generic_vocabulary_statistics() + custom_stats = self._custom_vocabulary_statistics() + + for k in generic_stats.keys() | custom_stats.keys(): + count_terms_agg[k] = generic_stats.get(k, 0) + custom_stats.get(k, 0) + + hits = self._results.items + + # Extend database data with configuration & aggregation data. + results = [] + for db_vocab_type in hits: + result = { + "id": db_vocab_type.id, + "pid_type": db_vocab_type.pid_type, + "count": count_terms_agg.get(db_vocab_type.id, 0), + "is_custom_vocabulary": db_vocab_type.id + in self.custom_vocabulary_names, + } + + if db_vocab_type.id in config_vocab_types: + for k, v in config_vocab_types[db_vocab_type.id].items(): + result[k] = v + + results.append(result) + + for hit in results: + if self._links_item_tpl: + hit["links"] = self._links_item_tpl.expand(self._identity, hit) + + res = { + "hits": { + "hits": results, + "total": self.total, + } + } + + if self._params: + if self._links_tpl: + res["links"] = self._links_tpl.expand(self._identity, self.pagination) + + return res + + def _custom_vocabulary_statistics(self): + # query database for count of terms in custom vocabularies + returndict = {} + for vocab_type in self.custom_vocabulary_names: + custom_service = current_service_registry.get(vocab_type) + record_cls = custom_service.config.record_cls + returndict[vocab_type] = record_cls.model_cls.query.count() + + return returndict + + def _generic_vocabulary_statistics(self): + # Opensearch query for generic vocabularies + config: RecordServiceConfig = ( + current_service.config + ) # TODO: Where to get the config from here? current_service is None + search_opts = config.search + + search = search_opts.search_cls( + using=current_search_client, + index=config.record_cls.index.search_alias, + ) + + search.aggs.bucket("vocabularies", {"terms": {"field": "type.id", "size": 100}}) + + search_result = search.execute() + buckets = search_result.aggs.to_dict()["vocabularies"]["buckets"] + + return {bucket["key"]: bucket["doc_count"] for bucket in buckets} diff --git a/invenio_vocabularies/services/service.py b/invenio_vocabularies/services/service.py index a8bffb0e..a84cb311 100644 --- a/invenio_vocabularies/services/service.py +++ b/invenio_vocabularies/services/service.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2020-2021 CERN. +# Copyright (C) 2024 CERN. # Copyright (C) 2021 Northwestern University. # # Invenio-Vocabularies is free software; you can redistribute it and/or @@ -8,14 +8,9 @@ # details. """Vocabulary service.""" -from functools import partial -from flask import current_app +import sqlalchemy as sa from invenio_cache import current_cache -from invenio_db import db -from invenio_i18n import lazy_gettext as _ -from invenio_records_resources.pagination import Pagination -from invenio_records_resources.proxies import current_service_registry from invenio_records_resources.services import ( Link, LinksTemplate, @@ -24,101 +19,15 @@ SearchOptions, pagination_links, ) -from invenio_records_resources.services.base import ( - ConditionalLink, - Service, - ServiceListResult, -) from invenio_records_resources.services.base.utils import map_search_params -from invenio_records_resources.services.errors import PermissionDeniedError -from invenio_records_resources.services.records.components import DataComponent -from invenio_records_resources.services.records.params import ( - FilterParam, - SuggestQueryParser, -) from invenio_records_resources.services.records.schema import ServiceSchemaWrapper from invenio_records_resources.services.uow import unit_of_work -from invenio_search import current_search_client from invenio_search.engine import dsl -from invenio_vocabularies.proxies import current_service - -from ..records.api import Vocabulary from ..records.models import VocabularyType -from .components import PIDComponent, VocabularyTypeComponent -from .permissions import PermissionPolicy -from .schema import TaskSchema, VocabularySchema from .tasks import process_datastream -def is_custom_vocabulary_type(vocabulary_type, context): - """Check if the vocabulary type is a custom vocabulary type.""" - return vocabulary_type["id"] in current_app.config.get( - "VOCABULARIES_CUSTOM_VOCABULARY_TYPES", [] - ) - - -class VocabularyMetadataList(ServiceListResult): - """Ensures that vocabulary metadata is returned in the proper format.""" - - def __init__( - self, - service, - identity, - results, - params=None, - links_tpl=None, - links_item_tpl=None, - ): - """Constructor. - - :params service: a service instance - :params identity: an identity that performed the service request - :params results: the search results - """ - self._identity = identity - self._results = results - self._params = params - self._service = service - self._links_tpl = links_tpl - self._links_item_tpl = links_item_tpl - - @property - def total(self): - """Get total number of hits.""" - return len(list(self._results)) - - @property - def pagination(self): - """Create a pagination object.""" - return Pagination( - self._params["size"], - self._params["page"], - self.total, - ) - - def to_dict(self): - """Formats result to a dict of hits.""" - hits = list(self._results) - - for hit in hits: - if self._links_item_tpl: - hit["links"] = self._links_item_tpl.expand(self._identity, hit) - - res = { - "hits": { - "hits": hits, - "total": self.total, - } - } - - if self._params: - if self._links_tpl: - res["links"] = self._links_tpl.expand(self._identity, self.pagination) - - return res - - class VocabularyTypeService(RecordService): """Vocabulary type service.""" @@ -134,207 +43,41 @@ def links_item_tpl(self): self.config.vocabularies_listing_item, ) - @property - def custom_vocabulary_names(self): - """Checks whether vocabulary is a custom vocabulary.""" - return current_app.config.get("VOCABULARIES_CUSTOM_VOCABULARY_TYPES", []) - def search(self, identity, params=None): """Search for vocabulary types entries.""" self.require_permission(identity, "list_vocabularies") - vocabulary_types = VocabularyType.query.all() - search_params = map_search_params(self.config.search, params) query_param = search_params["q"] + filters = [] if query_param: - vocabulary_types = [ - voc_type - for voc_type in vocabulary_types - if (query_param in voc_type.id.lower()) - ] - - sort_direction = search_params["sort_direction"] - vocabulary_types = sort_direction(vocabulary_types) - - config_vocab_types = current_app.config.get( - "INVENIO_VOCABULARY_TYPE_METADATA", {} + filters.extend([VocabularyType.id.ilike(f"%{query_param}%")]) + + vocabulary_types = ( + VocabularyType.query.filter(sa.or_(*filters)) + .order_by( + search_params["sort_direction"]( + sa.text(",".join(search_params["sort"])) + ) + ) + .paginate( + page=search_params["page"], + per_page=search_params["size"], + error_out=False, + ) ) - count_terms_agg = {} - generic_stats = self._generic_vocabulary_statistics() - custom_stats = self._custom_vocabulary_statistics() - - for k in generic_stats.keys() | custom_stats.keys(): - count_terms_agg[k] = generic_stats.get(k, 0) + custom_stats.get(k, 0) - - # Extend database data with configuration & aggregation data. - results = [] - for db_vocab_type in vocabulary_types: - result = { - "id": db_vocab_type.id, - "pid_type": db_vocab_type.pid_type, - "count": count_terms_agg.get(db_vocab_type.id, 0), - "is_custom_vocabulary": db_vocab_type.id - in self.custom_vocabulary_names, - } - - if db_vocab_type.id in config_vocab_types: - for k, v in config_vocab_types[db_vocab_type.id].items(): - result[k] = v - - results.append(result) - return self.config.vocabularies_listing_resultlist_cls( self, identity, - results, - params, + vocabulary_types, + search_params, links_tpl=LinksTemplate(self.config.links_search, context={"args": params}), links_item_tpl=self.links_item_tpl, ) - def _custom_vocabulary_statistics(self): - # query database for count of terms in custom vocabularies - returndict = {} - for vocab_type in self.custom_vocabulary_names: - custom_service = current_service_registry.get(vocab_type) - record_cls = custom_service.config.record_cls - returndict[vocab_type] = record_cls.model_cls.query.count() - - return returndict - - def _generic_vocabulary_statistics(self): - # Opensearch query for generic vocabularies - config: RecordServiceConfig = current_service.config - search_opts = config.search - - search = search_opts.search_cls( - using=current_search_client, - index=config.record_cls.index.search_alias, - ) - - search.aggs.bucket("vocabularies", {"terms": {"field": "type.id", "size": 100}}) - - search_result = search.execute() - buckets = search_result.aggs.to_dict()["vocabularies"]["buckets"] - - return {bucket["key"]: bucket["doc_count"] for bucket in buckets} - - -class VocabularySearchOptions(SearchOptions): - """Search options.""" - - params_interpreters_cls = [ - FilterParam.factory(param="tags", field="tags"), - ] + SearchOptions.params_interpreters_cls - - suggest_parser_cls = SuggestQueryParser.factory( - fields=[ - "id.text^100", - "id.text._2gram", - "id.text._3gram", - "title.en^5", - "title.en._2gram", - "title.en._3gram", - ], - ) - - sort_default = "id" - - sort_direction_default = "asc" - - sort_default_no_query = "title" - - sort_direction_options = { - "asc": dict( - title=_("Ascending"), - fn=partial(sorted, key=lambda t: t.id), - ), - "desc": dict( - title=_("Descending"), - fn=partial(sorted, key=lambda t: t.id, reverse=True), - ), - } - - sort_options = { - "bestmatch": dict( - title=_("Best match"), - fields=["_score"], # ES defaults to desc on `_score` field - ), - "title": dict( - title=_("Title"), - fields=["title_sort"], - ), - "newest": dict( - title=_("Newest"), - fields=["-created"], - ), - "oldest": dict( - title=_("Oldest"), - fields=["created"], - ), - "id": dict( - title=_("ID"), - fields=["id"], - ), - } - - pagination_options = { - "default_results_per_page": 10, - } - - -class VocabulariesServiceConfig(RecordServiceConfig): - """Vocabulary service configuration.""" - - service_id = "vocabularies" - indexer_queue_name = "vocabularies" - permission_policy_cls = PermissionPolicy - record_cls = Vocabulary - schema = VocabularySchema - task_schema = TaskSchema - vocabularies_listing_resultlist_cls = VocabularyMetadataList - - vocabularies_listing_item = { - "self": ConditionalLink( - cond=is_custom_vocabulary_type, - if_=Link( - "{+api}/{id}", - vars=lambda vocab_type, vars: vars.update({"id": vocab_type["id"]}), - ), - else_=Link( - "{+api}/vocabularies/{id}", - vars=lambda vocab_type, vars: vars.update({"id": vocab_type["id"]}), - ), - ) - } - - search = VocabularySearchOptions - - components = [ - # Order of components are important! - VocabularyTypeComponent, - DataComponent, - PIDComponent, - ] - - links_item = { - "self": Link( - "{+api}/vocabularies/{type}/{id}", - vars=lambda record, vars: vars.update( - { - "id": record.pid.pid_value, - "type": record.type.id, - } - ), - ), - } - - links_search = pagination_links("{+api}/vocabularies/{type}{?args*}") - class VocabulariesService(RecordService): """Vocabulary service.""" From fe43685650fc9986d265f8dc1704631037afd9d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karl-Ulrich=20Kr=C3=A4gelin?= Date: Fri, 14 Jun 2024 13:32:58 +0200 Subject: [PATCH 21/38] services: fix service proxy --- invenio_vocabularies/proxies.py | 2 +- invenio_vocabularies/services/results.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/invenio_vocabularies/proxies.py b/invenio_vocabularies/proxies.py index f6b9704a..434db1ec 100644 --- a/invenio_vocabularies/proxies.py +++ b/invenio_vocabularies/proxies.py @@ -19,7 +19,7 @@ def _ext_proxy(attr): ) -current_service = _ext_proxy("service") +current_service = _ext_proxy("vocabularies_service") """Proxy to the instantiated vocabulary service.""" diff --git a/invenio_vocabularies/services/results.py b/invenio_vocabularies/services/results.py index 8d0e6479..44f644f8 100644 --- a/invenio_vocabularies/services/results.py +++ b/invenio_vocabularies/services/results.py @@ -14,7 +14,7 @@ from invenio_records_resources.services.records.results import RecordList from invenio_search import current_search_client -from invenio_vocabularies.proxies import current_service +from ..proxies import current_service class VocabularyMetadataList(RecordList): From edfdf5317bc84238739002a42d9c0045e8ede5c7 Mon Sep 17 00:00:00 2001 From: Sarah Wiechers Date: Mon, 17 Jun 2024 13:44:36 +0200 Subject: [PATCH 22/38] vocabulary types administration: Fix sidebar menu name and remove create button. --- invenio_vocabularies/administration/views/vocabularies.py | 2 ++ invenio_vocabularies/services/config.py | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/invenio_vocabularies/administration/views/vocabularies.py b/invenio_vocabularies/administration/views/vocabularies.py index 8e9e88d1..88343aed 100644 --- a/invenio_vocabularies/administration/views/vocabularies.py +++ b/invenio_vocabularies/administration/views/vocabularies.py @@ -19,6 +19,7 @@ class VocabulariesListView(AdminResourceListView): api_endpoint = "/vocabularies/" name = "vocabulary-types" + menu_label = "Vocabulary Types" resource_config = "vocabulary_admin_resource" search_request_headers = {"Accept": "application/json"} title = "Vocabulary Types" @@ -31,6 +32,7 @@ class VocabulariesListView(AdminResourceListView): display_search = True display_delete = False display_edit = False + display_create = False item_field_list = { "id": {"text": "Name", "order": 1}, diff --git a/invenio_vocabularies/services/config.py b/invenio_vocabularies/services/config.py index 9406c668..c0b77af6 100644 --- a/invenio_vocabularies/services/config.py +++ b/invenio_vocabularies/services/config.py @@ -114,7 +114,6 @@ class VocabularyTypeSearchOptions(SearchOptions): sort_default_no_query = "id" - # TODO: Check if these options are actually necessary sort_direction_options = { "asc": dict(title=_("Ascending"), fn=asc), "desc": dict(title=_("Descending"), fn=desc), From 22f783571a8151c707ffdc58c3ad8431db73313e Mon Sep 17 00:00:00 2001 From: Sarah Wiechers Date: Mon, 17 Jun 2024 13:47:21 +0200 Subject: [PATCH 23/38] =?UTF-8?q?vocabulary=20types=20administration:=20Ad?= =?UTF-8?q?d=20Uni=20M=C3=BCnster=20copyright=20notice.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- invenio_vocabularies/services/config.py | 1 + invenio_vocabularies/services/results.py | 1 + invenio_vocabularies/services/service.py | 1 + 3 files changed, 3 insertions(+) diff --git a/invenio_vocabularies/services/config.py b/invenio_vocabularies/services/config.py index c0b77af6..4684b1cb 100644 --- a/invenio_vocabularies/services/config.py +++ b/invenio_vocabularies/services/config.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # # Copyright (C) 2024 CERN. +# Copyright (C) 2024 Uni Münster. # # Invenio-Vocabularies is free software; you can redistribute it and/or # modify it under the terms of the MIT License; see LICENSE file for more diff --git a/invenio_vocabularies/services/results.py b/invenio_vocabularies/services/results.py index 44f644f8..790b8085 100644 --- a/invenio_vocabularies/services/results.py +++ b/invenio_vocabularies/services/results.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # # Copyright (C) 2024 CERN. +# Copyright (C) 2024 Uni Münster. # # Invenio-Vocabularies is free software; you can redistribute it and/or # modify it under the terms of the MIT License; see LICENSE file for more diff --git a/invenio_vocabularies/services/service.py b/invenio_vocabularies/services/service.py index a84cb311..b061bb3b 100644 --- a/invenio_vocabularies/services/service.py +++ b/invenio_vocabularies/services/service.py @@ -2,6 +2,7 @@ # # Copyright (C) 2024 CERN. # Copyright (C) 2021 Northwestern University. +# Copyright (C) 2024 Uni Münster. # # Invenio-Vocabularies is free software; you can redistribute it and/or # modify it under the terms of the MIT License; see LICENSE file for more From 5657042af88ed20b5b7995386b6dc3d422550de4 Mon Sep 17 00:00:00 2001 From: Sarah Wiechers Date: Mon, 17 Jun 2024 14:40:00 +0200 Subject: [PATCH 24/38] vocabulary types administration tests: Use correct service for tests --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 7ad2e3ab..b2df7d91 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -114,7 +114,7 @@ def identity(): @pytest.fixture(scope="module") def service(app): """Vocabularies service object.""" - return app.extensions["invenio-vocabularies"].service + return app.extensions["invenio-vocabularies"].vocabularies_service @pytest.fixture() From 842f5c3c651952af325d5e2f10305f132473bde0 Mon Sep 17 00:00:00 2001 From: Sarah Wiechers Date: Mon, 24 Jun 2024 12:14:09 +0200 Subject: [PATCH 25/38] vocabulary types: cleanup --- invenio_vocabularies/ext.py | 2 -- invenio_vocabularies/resources/config.py | 2 +- invenio_vocabularies/resources/resource.py | 2 -- invenio_vocabularies/services/config.py | 40 ++++------------------ invenio_vocabularies/services/results.py | 6 ++-- invenio_vocabularies/services/service.py | 9 +---- 6 files changed, 11 insertions(+), 50 deletions(-) diff --git a/invenio_vocabularies/ext.py b/invenio_vocabularies/ext.py index 3792f1bc..e273d3cd 100644 --- a/invenio_vocabularies/ext.py +++ b/invenio_vocabularies/ext.py @@ -49,8 +49,6 @@ from .services.config import VocabularyTypesServiceConfig from .services.service import VocabulariesService, VocabularyTypeService -# from .contrib.information import InformationResource, InformationResourceConfig - class InvenioVocabularies(object): """Invenio-Vocabularies extension.""" diff --git a/invenio_vocabularies/resources/config.py b/invenio_vocabularies/resources/config.py index 2b1f3080..1a175135 100644 --- a/invenio_vocabularies/resources/config.py +++ b/invenio_vocabularies/resources/config.py @@ -75,7 +75,7 @@ class VocabularyTypeResourceConfig(ResourceConfig, ConfiguratorMixin): # /vocabulary/vocabulary_id # Blueprint configuration - blueprint_name = "vocabulary_list" + blueprint_name = "vocabulary_types" url_prefix = "/vocabularies" routes = { diff --git a/invenio_vocabularies/resources/resource.py b/invenio_vocabularies/resources/resource.py index 9520a42e..8d3cc09a 100644 --- a/invenio_vocabularies/resources/resource.py +++ b/invenio_vocabularies/resources/resource.py @@ -40,8 +40,6 @@ from .serializer import VocabularyL10NItemSchema -# from invenio_vocabularies.proxies import current_service - # # Resource diff --git a/invenio_vocabularies/services/config.py b/invenio_vocabularies/services/config.py index 4684b1cb..7ab12932 100644 --- a/invenio_vocabularies/services/config.py +++ b/invenio_vocabularies/services/config.py @@ -9,6 +9,7 @@ """Vocabulary services configs.""" +import sqlalchemy as sa from flask import current_app from invenio_i18n import lazy_gettext as _ from invenio_records_resources.services import ( @@ -29,9 +30,9 @@ FilterParam, SuggestQueryParser, ) -from sqlalchemy import asc, desc from ..records.api import Vocabulary +from ..records.models import VocabularyType from . import results from .components import PIDComponent, VocabularyTypeComponent from .permissions import PermissionPolicy @@ -90,20 +91,6 @@ class VocabularySearchOptions(SearchOptions): class VocabularyTypeSearchOptions(SearchOptions): """Search options for vocabulary types.""" - # TODO: Is this still necessary here? - params_interpreters_cls = [ - FilterParam.factory(param="tags", field="tags"), - ] + SearchOptions.params_interpreters_cls - - # TODO: Is this still necessary here? - suggest_parser_cls = SuggestQueryParser.factory( - fields=[ - "id.text^100", - "id.text._2gram", - "id.text._3gram", - ], - ) - sort_options = { "id": dict( title=_("ID"), @@ -116,8 +103,8 @@ class VocabularyTypeSearchOptions(SearchOptions): sort_default_no_query = "id" sort_direction_options = { - "asc": dict(title=_("Ascending"), fn=asc), - "desc": dict(title=_("Descending"), fn=desc), + "asc": dict(title=_("Ascending"), fn=sa.asc), + "desc": dict(title=_("Descending"), fn=sa.desc), } sort_direction_default = "asc" @@ -162,12 +149,11 @@ class VocabularyTypesServiceConfig(RecordServiceConfig): service_id = "vocabulary_types" permission_policy_cls = PermissionPolicy - record_cls = Vocabulary # TODO: Is this correct? + record_cls = VocabularyType schema = VocabularySchema - task_schema = TaskSchema - vocabularies_listing_resultlist_cls = results.VocabularyMetadataList + result_list_cls = results.VocabularyTypeList - vocabularies_listing_item = { + links_item = { "self": ConditionalLink( cond=is_custom_vocabulary_type, if_=Link( @@ -190,16 +176,4 @@ class VocabularyTypesServiceConfig(RecordServiceConfig): PIDComponent, ] - links_item = { - "self": Link( - "{+api}/vocabularies/{type}/{id}", - vars=lambda record, vars: vars.update( - { - "id": record.pid.pid_value, - "type": record.type.id, - } - ), - ), - } - links_search = pagination_links("{+api}/vocabularies/{type}{?args*}") diff --git a/invenio_vocabularies/services/results.py b/invenio_vocabularies/services/results.py index 790b8085..877270ea 100644 --- a/invenio_vocabularies/services/results.py +++ b/invenio_vocabularies/services/results.py @@ -18,8 +18,8 @@ from ..proxies import current_service -class VocabularyMetadataList(RecordList): - """Ensures that vocabulary metadata is returned in the proper format.""" +class VocabularyTypeList(RecordList): + """Ensures that vocabulary type metadata is returned in the proper format.""" @property def total(self): @@ -33,8 +33,6 @@ def custom_vocabulary_names(self): def to_dict(self): """Formats result to a dict of hits.""" - # hits = list(self._results) - config_vocab_types = current_app.config.get( "INVENIO_VOCABULARY_TYPE_METADATA", {} ) diff --git a/invenio_vocabularies/services/service.py b/invenio_vocabularies/services/service.py index b061bb3b..c8a16b21 100644 --- a/invenio_vocabularies/services/service.py +++ b/invenio_vocabularies/services/service.py @@ -37,13 +37,6 @@ def schema(self): """Returns the data schema instance.""" return ServiceSchemaWrapper(self, schema=self.config.schema) - @property - def links_item_tpl(self): - """Item links template.""" - return LinksTemplate( - self.config.vocabularies_listing_item, - ) - def search(self, identity, params=None): """Search for vocabulary types entries.""" self.require_permission(identity, "list_vocabularies") @@ -70,7 +63,7 @@ def search(self, identity, params=None): ) ) - return self.config.vocabularies_listing_resultlist_cls( + return self.config.result_list_cls( self, identity, vocabulary_types, From 6c79b8b00b56a1edf83f44eeb5e062fac4d1e792 Mon Sep 17 00:00:00 2001 From: Sarah Wiechers Date: Tue, 25 Jun 2024 13:22:01 +0200 Subject: [PATCH 26/38] vocabulary types items: add vocabulary items list view --- .../administration/views/vocabularies.py | 76 ++++++++++++++++++- invenio_vocabularies/config.py | 16 ++++ invenio_vocabularies/services/results.py | 2 +- setup.cfg | 1 + 4 files changed, 90 insertions(+), 5 deletions(-) diff --git a/invenio_vocabularies/administration/views/vocabularies.py b/invenio_vocabularies/administration/views/vocabularies.py index 88343aed..0a91c720 100644 --- a/invenio_vocabularies/administration/views/vocabularies.py +++ b/invenio_vocabularies/administration/views/vocabularies.py @@ -8,10 +8,8 @@ # details. """Vocabularies admin interface.""" -from invenio_administration.views.base import ( - AdminResourceEditView, - AdminResourceListView, -) +from invenio_administration.views.base import AdminResourceListView +from flask import current_app class VocabulariesListView(AdminResourceListView): @@ -42,3 +40,73 @@ class VocabulariesListView(AdminResourceListView): search_config_name = "VOCABULARIES_TYPES_SEARCH" search_facets_config_name = "VOCABULARIES_TYPES_FACETS" search_sort_config_name = "VOCABULARIES_TYPES_SORT_OPTIONS" + +class VocabularyDetailsListView(AdminResourceListView): + """Configuration for vocabularies list view.""" + + def get_api_endpoint(self, pid_value=None): + """overwrite get_api_endpoint to accept pid_value""" + + if pid_value in current_app.config.get("VOCABULARIES_CUSTOM_VOCABULARY_TYPES", []): + return f"/api/{pid_value}" + else: + return f"/api/vocabularies/{pid_value}" + + def get_context(self, **kwargs): + """Create details view context.""" + search_conf = self.init_search_config(**kwargs) + schema = self.get_service_schema() + serialized_schema = self._schema_to_json(schema) + pid_value = kwargs.get("pid_value", "") + return { + "search_config": search_conf, + "title": f"{pid_value} vocabulary items", + "name": self.name, + "resource_schema": serialized_schema, + "fields": self.item_field_list, + "display_search": self.display_search, + "display_create": self.display_create, + "display_edit": self.display_edit, + "display_delete": self.display_delete, + "display_read": self.display_read, + "actions": self.serialize_actions(), + "pid_path": self.pid_path, + "pid_value": pid_value, + "create_ui_endpoint": self.get_create_view_endpoint(), + "list_ui_endpoint": self.get_list_view_endpoint(), + "resource_name": ( + self.resource_name if self.resource_name else self.pid_path + ), + } + + name = "vocabulary-type-items" + url = "/vocabulary-types/" + + api_endpoint = "/vocabularies/" + + # INFO name of the resource's list view name, enables navigation between items view and types view. + list_view_name = "vocabulary-types" + + resource_config = "vocabulary_admin_resource" + search_request_headers = {"Accept": "application/json"} + + pid_path = "id" + + # INFO only if disabled() (as a function) it's not in the sidebar, see https://github.com/inveniosoftware/invenio-administration/blob/main/invenio_administration/menu/menu.py#L54 + disabled = lambda _: True + + template = "invenio_administration/search.html" + + display_delete = False + display_create = False + display_edit = True + display_search = False + + item_field_list = { + "title": {"text": "Title", "order": 0}, + "created": {"text": "Created", "order": 1} + } + + search_config_name = "VOCABULARIES_TYPES_ITEMS_SEARCH" + search_facets_config_name = "VOCABULARIES_TYPES_ITEMS_FACETS" + search_sort_config_name = "VOCABULARIES_TYPES_ITEMS_SORT_OPTIONS" diff --git a/invenio_vocabularies/config.py b/invenio_vocabularies/config.py index 56f6c252..24549e44 100644 --- a/invenio_vocabularies/config.py +++ b/invenio_vocabularies/config.py @@ -153,3 +153,19 @@ "sort": ["name", "count"], } """Vocabulary type search configuration.""" + +VOCABULARIES_TYPES_ITEMS_SORT_OPTIONS = { + "title": dict( + title=_("Title"), + fields=["title"], + ), + "created": dict( + title=_("Created"), + fields=["created"], + ), +} + +VOCABULARIES_TYPES_ITEMS_SEARCH = { + "facets": [], + "sort": ["title", "created"], +} diff --git a/invenio_vocabularies/services/results.py b/invenio_vocabularies/services/results.py index 877270ea..443b300b 100644 --- a/invenio_vocabularies/services/results.py +++ b/invenio_vocabularies/services/results.py @@ -94,7 +94,7 @@ def _generic_vocabulary_statistics(self): # Opensearch query for generic vocabularies config: RecordServiceConfig = ( current_service.config - ) # TODO: Where to get the config from here? current_service is None + ) search_opts = config.search search = search_opts.search_cls( diff --git a/setup.cfg b/setup.cfg index a9aab09d..275c73c7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -58,6 +58,7 @@ flask.commands = vocabularies = invenio_vocabularies.cli:vocabularies invenio_administration.views = vocabularies_list = invenio_vocabularies.administration.views.vocabularies:VocabulariesListView + vocabulary_types_details = invenio_vocabularies.administration.views.vocabularies:VocabularyDetailsListView invenio_base.apps = invenio_vocabularies = invenio_vocabularies:InvenioVocabularies invenio_base.api_apps = From ba10dcb529df4d1625a28e9bfb09476732da2321 Mon Sep 17 00:00:00 2001 From: Sarah Wiechers Date: Tue, 25 Jun 2024 13:32:49 +0200 Subject: [PATCH 27/38] vocabulary types items: account for different sort options on different vocabulary types --- .../administration/views/vocabularies.py | 2 +- invenio_vocabularies/config.py | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/invenio_vocabularies/administration/views/vocabularies.py b/invenio_vocabularies/administration/views/vocabularies.py index 0a91c720..42cf3824 100644 --- a/invenio_vocabularies/administration/views/vocabularies.py +++ b/invenio_vocabularies/administration/views/vocabularies.py @@ -103,7 +103,7 @@ def get_context(self, **kwargs): display_search = False item_field_list = { - "title": {"text": "Title", "order": 0}, + "id": {"text": "ID", "order": 0}, "created": {"text": "Created", "order": 1} } diff --git a/invenio_vocabularies/config.py b/invenio_vocabularies/config.py index 24549e44..f932ec7f 100644 --- a/invenio_vocabularies/config.py +++ b/invenio_vocabularies/config.py @@ -155,10 +155,18 @@ """Vocabulary type search configuration.""" VOCABULARIES_TYPES_ITEMS_SORT_OPTIONS = { + "name": dict( + title=_("Name"), + fields=["id"], + ), "title": dict( title=_("Title"), fields=["title"], ), + "subject": dict( + title=_("Subject"), + fields=["subject"], + ), "created": dict( title=_("Created"), fields=["created"], @@ -167,5 +175,5 @@ VOCABULARIES_TYPES_ITEMS_SEARCH = { "facets": [], - "sort": ["title", "created"], + "sort": ["name", "title", "subject", "created"], } From b0a4bb70caf53545cda3b64d8f18361271977f22 Mon Sep 17 00:00:00 2001 From: Sarah Wiechers Date: Tue, 25 Jun 2024 13:47:57 +0200 Subject: [PATCH 28/38] vocabulary types items: add display fields for items --- invenio_vocabularies/administration/views/vocabularies.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/invenio_vocabularies/administration/views/vocabularies.py b/invenio_vocabularies/administration/views/vocabularies.py index 42cf3824..e8a44724 100644 --- a/invenio_vocabularies/administration/views/vocabularies.py +++ b/invenio_vocabularies/administration/views/vocabularies.py @@ -103,8 +103,9 @@ def get_context(self, **kwargs): display_search = False item_field_list = { - "id": {"text": "ID", "order": 0}, - "created": {"text": "Created", "order": 1} + "name": {"text": "Name", "order": 0}, + "title['en']": {"text": "Title [en]", "order": 1}, + "created": {"text": "Created", "order": 2} } search_config_name = "VOCABULARIES_TYPES_ITEMS_SEARCH" From f210d7cee61cca5a27f84c1dbd03014bc398f06f Mon Sep 17 00:00:00 2001 From: Sarah Wiechers Date: Tue, 25 Jun 2024 14:02:31 +0200 Subject: [PATCH 29/38] vocabulary types items: add subject column --- invenio_vocabularies/administration/views/vocabularies.py | 7 +++++-- invenio_vocabularies/services/config.py | 2 ++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/invenio_vocabularies/administration/views/vocabularies.py b/invenio_vocabularies/administration/views/vocabularies.py index e8a44724..16d59b28 100644 --- a/invenio_vocabularies/administration/views/vocabularies.py +++ b/invenio_vocabularies/administration/views/vocabularies.py @@ -100,12 +100,15 @@ def get_context(self, **kwargs): display_delete = False display_create = False display_edit = True - display_search = False + display_search = True + # TODO: It would be nicer to choose the correct column depending on the vocabulary + # TODO: It would ne nicer to use the title's translation in the currently selected language and fall back to English if this doesn't exist item_field_list = { "name": {"text": "Name", "order": 0}, "title['en']": {"text": "Title [en]", "order": 1}, - "created": {"text": "Created", "order": 2} + "subject": {"text": "Subject", "order": 2}, + "created": {"text": "Created", "order": 3} } search_config_name = "VOCABULARIES_TYPES_ITEMS_SEARCH" diff --git a/invenio_vocabularies/services/config.py b/invenio_vocabularies/services/config.py index 7ab12932..f1041dbb 100644 --- a/invenio_vocabularies/services/config.py +++ b/invenio_vocabularies/services/config.py @@ -91,6 +91,8 @@ class VocabularySearchOptions(SearchOptions): class VocabularyTypeSearchOptions(SearchOptions): """Search options for vocabulary types.""" + # TODO: Currently one can only search by ID (not the displayed title/name/subject) + # TODO: Sorting by anything else leads to an error sort_options = { "id": dict( title=_("ID"), From 1cbf8077a5b0c0554263c25a8451d3af1987d4ba Mon Sep 17 00:00:00 2001 From: Sarah Wiechers Date: Tue, 25 Jun 2024 14:31:50 +0200 Subject: [PATCH 30/38] Capitalize vocabulary type name in items view --- invenio_vocabularies/administration/views/vocabularies.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invenio_vocabularies/administration/views/vocabularies.py b/invenio_vocabularies/administration/views/vocabularies.py index 16d59b28..da49bca7 100644 --- a/invenio_vocabularies/administration/views/vocabularies.py +++ b/invenio_vocabularies/administration/views/vocabularies.py @@ -60,7 +60,7 @@ def get_context(self, **kwargs): pid_value = kwargs.get("pid_value", "") return { "search_config": search_conf, - "title": f"{pid_value} vocabulary items", + "title": f"{pid_value.capitalize()} vocabulary items", "name": self.name, "resource_schema": serialized_schema, "fields": self.item_field_list, From 8cf3e43d440c9f2e77b1d450a94c1b3289f4bde2 Mon Sep 17 00:00:00 2001 From: Sarah Wiechers Date: Wed, 26 Jun 2024 15:06:10 +0200 Subject: [PATCH 31/38] vocabulary item list view: Fixed formatting and pydocstyle --- .../administration/views/vocabularies.py | 12 +++++++----- invenio_vocabularies/services/results.py | 4 +--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/invenio_vocabularies/administration/views/vocabularies.py b/invenio_vocabularies/administration/views/vocabularies.py index da49bca7..a709dc46 100644 --- a/invenio_vocabularies/administration/views/vocabularies.py +++ b/invenio_vocabularies/administration/views/vocabularies.py @@ -8,8 +8,8 @@ # details. """Vocabularies admin interface.""" -from invenio_administration.views.base import AdminResourceListView from flask import current_app +from invenio_administration.views.base import AdminResourceListView class VocabulariesListView(AdminResourceListView): @@ -41,13 +41,15 @@ class VocabulariesListView(AdminResourceListView): search_facets_config_name = "VOCABULARIES_TYPES_FACETS" search_sort_config_name = "VOCABULARIES_TYPES_SORT_OPTIONS" + class VocabularyDetailsListView(AdminResourceListView): """Configuration for vocabularies list view.""" def get_api_endpoint(self, pid_value=None): - """overwrite get_api_endpoint to accept pid_value""" - - if pid_value in current_app.config.get("VOCABULARIES_CUSTOM_VOCABULARY_TYPES", []): + """Overwrite get_api_endpoint to accept pid_value.""" + if pid_value in current_app.config.get( + "VOCABULARIES_CUSTOM_VOCABULARY_TYPES", [] + ): return f"/api/{pid_value}" else: return f"/api/vocabularies/{pid_value}" @@ -108,7 +110,7 @@ def get_context(self, **kwargs): "name": {"text": "Name", "order": 0}, "title['en']": {"text": "Title [en]", "order": 1}, "subject": {"text": "Subject", "order": 2}, - "created": {"text": "Created", "order": 3} + "created": {"text": "Created", "order": 3}, } search_config_name = "VOCABULARIES_TYPES_ITEMS_SEARCH" diff --git a/invenio_vocabularies/services/results.py b/invenio_vocabularies/services/results.py index 443b300b..8e9905d6 100644 --- a/invenio_vocabularies/services/results.py +++ b/invenio_vocabularies/services/results.py @@ -92,9 +92,7 @@ def _custom_vocabulary_statistics(self): def _generic_vocabulary_statistics(self): # Opensearch query for generic vocabularies - config: RecordServiceConfig = ( - current_service.config - ) + config: RecordServiceConfig = current_service.config search_opts = config.search search = search_opts.search_cls( From 99f299d62f563e1dcb0703863fea516523e9e89c Mon Sep 17 00:00:00 2001 From: Sarah Wiechers Date: Tue, 25 Jun 2024 17:45:19 +0200 Subject: [PATCH 32/38] vocabulary types: remove schema property from VocabularyTypeService --- invenio_vocabularies/services/config.py | 2 +- invenio_vocabularies/services/service.py | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/invenio_vocabularies/services/config.py b/invenio_vocabularies/services/config.py index f1041dbb..738a25a2 100644 --- a/invenio_vocabularies/services/config.py +++ b/invenio_vocabularies/services/config.py @@ -152,7 +152,7 @@ class VocabularyTypesServiceConfig(RecordServiceConfig): service_id = "vocabulary_types" permission_policy_cls = PermissionPolicy record_cls = VocabularyType - schema = VocabularySchema + schema = VocabularySchema # Works but should be VocabularyTypeSchema if this is defined at some point result_list_cls = results.VocabularyTypeList links_item = { diff --git a/invenio_vocabularies/services/service.py b/invenio_vocabularies/services/service.py index c8a16b21..64d84e25 100644 --- a/invenio_vocabularies/services/service.py +++ b/invenio_vocabularies/services/service.py @@ -32,11 +32,6 @@ class VocabularyTypeService(RecordService): """Vocabulary type service.""" - @property - def schema(self): - """Returns the data schema instance.""" - return ServiceSchemaWrapper(self, schema=self.config.schema) - def search(self, identity, params=None): """Search for vocabulary types entries.""" self.require_permission(identity, "list_vocabularies") From 7475321751e2ff734ddc7cec4d7efc102bd15b80 Mon Sep 17 00:00:00 2001 From: Sarah Wiechers Date: Tue, 25 Jun 2024 17:47:16 +0200 Subject: [PATCH 33/38] vocabulary types: remove unused read response handler from VocabulariesAdminResource --- invenio_vocabularies/resources/resource.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/invenio_vocabularies/resources/resource.py b/invenio_vocabularies/resources/resource.py index 8d3cc09a..8851d6d2 100644 --- a/invenio_vocabularies/resources/resource.py +++ b/invenio_vocabularies/resources/resource.py @@ -157,13 +157,3 @@ def get_all_vocabulary_types(self): return hits.to_dict(), 200 - @request_view_args - @response_handler() - def read(self): - """Read an item.""" - pid_value = ( - resource_requestctx.view_args["type"], - resource_requestctx.view_args["pid_value"], - ) - item = self.service.read(g.identity, pid_value) - return item.to_dict(), 200 From 9d4ccbb5138f292cd3008ff94a2d0f01fe4e60cd Mon Sep 17 00:00:00 2001 From: Sarah Wiechers Date: Tue, 25 Jun 2024 17:59:30 +0200 Subject: [PATCH 34/38] vocabulary types: rename route and response handler of VocabulariesAdminResource --- invenio_vocabularies/resources/config.py | 6 +----- invenio_vocabularies/resources/resource.py | 9 +++------ invenio_vocabularies/services/config.py | 2 +- 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/invenio_vocabularies/resources/config.py b/invenio_vocabularies/resources/config.py index 1a175135..7d751720 100644 --- a/invenio_vocabularies/resources/config.py +++ b/invenio_vocabularies/resources/config.py @@ -78,11 +78,7 @@ class VocabularyTypeResourceConfig(ResourceConfig, ConfiguratorMixin): blueprint_name = "vocabulary_types" url_prefix = "/vocabularies" - routes = { - "all": "/", - "list": "/vocabularies/", - "item": "/vocabularies//", - } + routes = {"list": "/"} # Request parsing request_read_args = {} diff --git a/invenio_vocabularies/resources/resource.py b/invenio_vocabularies/resources/resource.py index 8851d6d2..e61bb108 100644 --- a/invenio_vocabularies/resources/resource.py +++ b/invenio_vocabularies/resources/resource.py @@ -141,19 +141,16 @@ class VocabulariesAdminResource(RecordResource): def create_url_rules(self): """Create the URL rules for the record resource.""" routes = self.config.routes - rules = super().create_url_rules() - rules.append( - route("GET", routes["all"], self.get_all_vocabulary_types), - ) + rules = [route("GET", routes["list"], self.search)] + return rules @request_search_args @response_handler(many=True) - def get_all_vocabulary_types(self): + def search(self): """Return information about _all_ vocabularies.""" identity = g.identity hits = self.service.search(identity, params=resource_requestctx.args) return hits.to_dict(), 200 - diff --git a/invenio_vocabularies/services/config.py b/invenio_vocabularies/services/config.py index 738a25a2..3fa815f0 100644 --- a/invenio_vocabularies/services/config.py +++ b/invenio_vocabularies/services/config.py @@ -152,7 +152,7 @@ class VocabularyTypesServiceConfig(RecordServiceConfig): service_id = "vocabulary_types" permission_policy_cls = PermissionPolicy record_cls = VocabularyType - schema = VocabularySchema # Works but should be VocabularyTypeSchema if this is defined at some point + schema = VocabularySchema # Works but should be VocabularyTypeSchema if this is defined at some point result_list_cls = results.VocabularyTypeList links_item = { From 9a2a0d7172a7c261da94bb39e45d3b29e44832b3 Mon Sep 17 00:00:00 2001 From: Sarah Wiechers Date: Thu, 4 Jul 2024 10:13:32 +0200 Subject: [PATCH 35/38] vocabulary item list view: Avoid code duplication by overwriting get instead of get_context method --- .../administration/views/vocabularies.py | 32 ++++++------------- 1 file changed, 9 insertions(+), 23 deletions(-) diff --git a/invenio_vocabularies/administration/views/vocabularies.py b/invenio_vocabularies/administration/views/vocabularies.py index a709dc46..5d0b1687 100644 --- a/invenio_vocabularies/administration/views/vocabularies.py +++ b/invenio_vocabularies/administration/views/vocabularies.py @@ -54,32 +54,18 @@ def get_api_endpoint(self, pid_value=None): else: return f"/api/vocabularies/{pid_value}" - def get_context(self, **kwargs): - """Create details view context.""" - search_conf = self.init_search_config(**kwargs) - schema = self.get_service_schema() - serialized_schema = self._schema_to_json(schema) + def get(self, **kwargs): + """GET view method.""" + parent_context = super().get_context(**kwargs) + pid_value = kwargs.get("pid_value", "") - return { - "search_config": search_conf, + + parent_context.update({ "title": f"{pid_value.capitalize()} vocabulary items", - "name": self.name, - "resource_schema": serialized_schema, - "fields": self.item_field_list, - "display_search": self.display_search, - "display_create": self.display_create, - "display_edit": self.display_edit, - "display_delete": self.display_delete, - "display_read": self.display_read, - "actions": self.serialize_actions(), - "pid_path": self.pid_path, "pid_value": pid_value, - "create_ui_endpoint": self.get_create_view_endpoint(), - "list_ui_endpoint": self.get_list_view_endpoint(), - "resource_name": ( - self.resource_name if self.resource_name else self.pid_path - ), - } + }) + + return self.render(**parent_context) name = "vocabulary-type-items" url = "/vocabulary-types/" From c9f79e844dac482f2e03ed11d520574408d7ab81 Mon Sep 17 00:00:00 2001 From: Sarah Wiechers Date: Tue, 16 Jul 2024 15:24:16 +0200 Subject: [PATCH 36/38] vocabulary item list view: make view ui customizable via config.py --- .../administration/views/vocabularies.py | 49 ++++++-- invenio_vocabularies/config.py | 108 +++++++++++++++++- 2 files changed, 141 insertions(+), 16 deletions(-) diff --git a/invenio_vocabularies/administration/views/vocabularies.py b/invenio_vocabularies/administration/views/vocabularies.py index 5d0b1687..976ff5c6 100644 --- a/invenio_vocabularies/administration/views/vocabularies.py +++ b/invenio_vocabularies/administration/views/vocabularies.py @@ -10,6 +10,8 @@ """Vocabularies admin interface.""" from flask import current_app from invenio_administration.views.base import AdminResourceListView +from functools import partial +from invenio_search_ui.searchconfig import search_app_config, SortConfig, FacetsConfig class VocabulariesListView(AdminResourceListView): @@ -54,24 +56,52 @@ def get_api_endpoint(self, pid_value=None): else: return f"/api/vocabularies/{pid_value}" + def init_search_config(self, **kwargs): + """Build search view config.""" + pid_value = kwargs.get("pid_value", "") + custom_search_config = current_app.config[self.search_config_name].get(pid_value) + + if custom_search_config: + available_sort_options = current_app.config[self.search_sort_config_name] + available_facets = current_app.config.get(self.search_facets_config_name) + + return partial( + search_app_config, + config_name=self.get_search_app_name(**kwargs), + available_facets=available_facets, + sort_options=available_sort_options, + endpoint=self.get_api_endpoint(**kwargs), + headers=self.get_search_request_headers(**kwargs), + sort=SortConfig(available_sort_options, + custom_search_config["sort"], + custom_search_config["sort_default"], + custom_search_config["sort_default_no_query"]), + facets=FacetsConfig(available_facets, custom_search_config["facets"]), + ) + else: + return super().init_search_config(**kwargs) + def get(self, **kwargs): """GET view method.""" parent_context = super().get_context(**kwargs) pid_value = kwargs.get("pid_value", "") + vocab_admin_cfg = current_app.config["VOCABULARIES_ADMINISTRATION_CONFIG"] - parent_context.update({ - "title": f"{pid_value.capitalize()} vocabulary items", - "pid_value": pid_value, - }) + custom_config = vocab_admin_cfg.get(pid_value) + + if custom_config: + parent_context.update(custom_config) + else: + parent_context.update({ + "title": f"{pid_value.capitalize()} vocabulary items" + }) return self.render(**parent_context) name = "vocabulary-type-items" url = "/vocabulary-types/" - api_endpoint = "/vocabularies/" - # INFO name of the resource's list view name, enables navigation between items view and types view. list_view_name = "vocabulary-types" @@ -90,13 +120,10 @@ def get(self, **kwargs): display_edit = True display_search = True - # TODO: It would be nicer to choose the correct column depending on the vocabulary # TODO: It would ne nicer to use the title's translation in the currently selected language and fall back to English if this doesn't exist item_field_list = { - "name": {"text": "Name", "order": 0}, - "title['en']": {"text": "Title [en]", "order": 1}, - "subject": {"text": "Subject", "order": 2}, - "created": {"text": "Created", "order": 3}, + "title['en']": {"text": "Title [en]", "order": 0}, + "created": {"text": "Created", "order": 1}, } search_config_name = "VOCABULARIES_TYPES_ITEMS_SEARCH" diff --git a/invenio_vocabularies/config.py b/invenio_vocabularies/config.py index f932ec7f..d78c4aae 100644 --- a/invenio_vocabularies/config.py +++ b/invenio_vocabularies/config.py @@ -167,13 +167,111 @@ title=_("Subject"), fields=["subject"], ), - "created": dict( - title=_("Created"), - fields=["created"], - ), } +"""Definitions of available Vocabulary type items sort options. """ VOCABULARIES_TYPES_ITEMS_SEARCH = { + "affiliations": { + "facets": [], + "sort": ["name"], + "sort_default": "name", + "sort_default_no_query": "name" + }, + "awards": { + "facets": [], + "sort": ["name"], + "sort_default": "name", + "sort_default_no_query": "name" + }, + "funders": { + "facets": [], + "sort": ["name"], + "sort_default": "name", + "sort_default_no_query": "name" + }, + "names": { + "facets": [], + "sort": ["name"], + "sort_default": "name", + "sort_default_no_query": "name" + }, + "subjects": { + "facets": [], + "sort": ["subject"], + "sort_default": "subject", + "sort_default_no_query": "subject" + }, "facets": [], - "sort": ["name", "title", "subject", "created"], + "sort": ["title"], + "sort_default": "title", + "sort_default_no_query": "title" +} +"""Vocabulary type item search configurations per type and for generic vocabularies.""" + +VOCABULARIES_ADMINISTRATION_CONFIG = { + "affiliations": dict( + title="Affiliations vocabulary items", + fields={ + "name": {"text": "Name", "order": 0}, + "created": {"text": "Created", "order": 1}, + }, + display_search=True, + display_create=False, + display_delete=False, + display_edit=True, + display_read=True, + pid_path="id", + ), + "awards": dict( + title="Awards vocabulary items", + fields={ + "name": {"text": "Name", "order": 0}, + "created": {"text": "Created", "order": 1}, + }, + display_search=True, + display_create=False, + display_delete=False, + display_edit=True, + display_read=True, + pid_path="id", + ), + "funders": dict( + title="Funders vocabulary items", + fields={ + "name": {"text": "Name", "order": 0}, + "created": {"text": "Created", "order": 1}, + }, + display_search=True, + display_create=False, + display_delete=False, + display_edit=True, + display_read=True, + pid_path="id", + ), + "names": dict( + title="Names vocabulary items", + fields={ + "name": {"text": "Name", "order": 0}, + "created": {"text": "Created", "order": 1}, + }, + display_search=True, + display_create=False, + display_delete=False, + display_edit=True, + display_read=True, + pid_path="id", + ), + "subjects": dict( + title="Subjects vocabulary items", + fields={ + "subject": {"text": "Subject", "order": 0}, + "created": {"text": "Created", "order": 1}, + }, + display_search=True, + display_create=False, + display_delete=False, + display_edit=True, + display_read=True, + pid_path="id", + ) } From 75240e9064b98372fc9cd589da52ac5f6bdf7860 Mon Sep 17 00:00:00 2001 From: Sarah Wiechers Date: Tue, 16 Jul 2024 16:46:31 +0200 Subject: [PATCH 37/38] vocabulary item list view: add resource name to config --- invenio_vocabularies/administration/views/vocabularies.py | 1 + invenio_vocabularies/config.py | 5 +++++ invenio_vocabularies/services/config.py | 6 ++++-- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/invenio_vocabularies/administration/views/vocabularies.py b/invenio_vocabularies/administration/views/vocabularies.py index 976ff5c6..0570e142 100644 --- a/invenio_vocabularies/administration/views/vocabularies.py +++ b/invenio_vocabularies/administration/views/vocabularies.py @@ -109,6 +109,7 @@ def get(self, **kwargs): search_request_headers = {"Accept": "application/json"} pid_path = "id" + resource_name = "title['en']" # INFO only if disabled() (as a function) it's not in the sidebar, see https://github.com/inveniosoftware/invenio-administration/blob/main/invenio_administration/menu/menu.py#L54 disabled = lambda _: True diff --git a/invenio_vocabularies/config.py b/invenio_vocabularies/config.py index d78c4aae..adf88e90 100644 --- a/invenio_vocabularies/config.py +++ b/invenio_vocabularies/config.py @@ -221,6 +221,7 @@ display_edit=True, display_read=True, pid_path="id", + resource_name="name" ), "awards": dict( title="Awards vocabulary items", @@ -234,6 +235,7 @@ display_edit=True, display_read=True, pid_path="id", + resource_name="name" ), "funders": dict( title="Funders vocabulary items", @@ -247,6 +249,7 @@ display_edit=True, display_read=True, pid_path="id", + resource_name="name" ), "names": dict( title="Names vocabulary items", @@ -260,6 +263,7 @@ display_edit=True, display_read=True, pid_path="id", + resource_name="name" ), "subjects": dict( title="Subjects vocabulary items", @@ -273,5 +277,6 @@ display_edit=True, display_read=True, pid_path="id", + resource_name="subject" ) } diff --git a/invenio_vocabularies/services/config.py b/invenio_vocabularies/services/config.py index 3fa815f0..e43ca1ad 100644 --- a/invenio_vocabularies/services/config.py +++ b/invenio_vocabularies/services/config.py @@ -91,13 +91,15 @@ class VocabularySearchOptions(SearchOptions): class VocabularyTypeSearchOptions(SearchOptions): """Search options for vocabulary types.""" - # TODO: Currently one can only search by ID (not the displayed title/name/subject) - # TODO: Sorting by anything else leads to an error sort_options = { "id": dict( title=_("ID"), fields=["id"], ), + "count": dict( + title=_("Number of entries"), + fields=["count"], + ), } sort_default = "id" From f6ba1dcf2240e7a3d23bdc8796ed18f5936d58ae Mon Sep 17 00:00:00 2001 From: Sarah Wiechers Date: Tue, 16 Jul 2024 16:55:13 +0200 Subject: [PATCH 38/38] formatting: run isort and black --- .../administration/views/vocabularies.py | 32 ++++++++++--------- invenio_vocabularies/config.py | 24 +++++++------- 2 files changed, 29 insertions(+), 27 deletions(-) diff --git a/invenio_vocabularies/administration/views/vocabularies.py b/invenio_vocabularies/administration/views/vocabularies.py index 7d8470b1..9af9ca88 100644 --- a/invenio_vocabularies/administration/views/vocabularies.py +++ b/invenio_vocabularies/administration/views/vocabularies.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2020-2024 CERN. +# Copyright (C) 2020-2021 CERN. # Copyright (C) 2024 Uni Münster. # # Invenio-Vocabularies is free software; you can redistribute it and/or @@ -8,13 +8,11 @@ # details. """Vocabularies admin interface.""" -from flask import current_app from functools import partial -from invenio_search_ui.searchconfig import search_app_config, SortConfig, FacetsConfig -from invenio_administration.views.base import ( - AdminResourceEditView, - AdminResourceListView, -) + +from flask import current_app +from invenio_administration.views.base import AdminResourceListView +from invenio_search_ui.searchconfig import FacetsConfig, SortConfig, search_app_config class VocabulariesListView(AdminResourceListView): @@ -62,7 +60,9 @@ def get_api_endpoint(self, pid_value=None): def init_search_config(self, **kwargs): """Build search view config.""" pid_value = kwargs.get("pid_value", "") - custom_search_config = current_app.config[self.search_config_name].get(pid_value) + custom_search_config = current_app.config[self.search_config_name].get( + pid_value + ) if custom_search_config: available_sort_options = current_app.config[self.search_sort_config_name] @@ -75,10 +75,12 @@ def init_search_config(self, **kwargs): sort_options=available_sort_options, endpoint=self.get_api_endpoint(**kwargs), headers=self.get_search_request_headers(**kwargs), - sort=SortConfig(available_sort_options, - custom_search_config["sort"], - custom_search_config["sort_default"], - custom_search_config["sort_default_no_query"]), + sort=SortConfig( + available_sort_options, + custom_search_config["sort"], + custom_search_config["sort_default"], + custom_search_config["sort_default_no_query"], + ), facets=FacetsConfig(available_facets, custom_search_config["facets"]), ) else: @@ -96,9 +98,9 @@ def get(self, **kwargs): if custom_config: parent_context.update(custom_config) else: - parent_context.update({ - "title": f"{pid_value.capitalize()} vocabulary items" - }) + parent_context.update( + {"title": f"{pid_value.capitalize()} vocabulary items"} + ) return self.render(**parent_context) diff --git a/invenio_vocabularies/config.py b/invenio_vocabularies/config.py index 49df3460..dad012e4 100644 --- a/invenio_vocabularies/config.py +++ b/invenio_vocabularies/config.py @@ -176,36 +176,36 @@ "facets": [], "sort": ["name"], "sort_default": "name", - "sort_default_no_query": "name" + "sort_default_no_query": "name", }, "awards": { "facets": [], "sort": ["name"], "sort_default": "name", - "sort_default_no_query": "name" + "sort_default_no_query": "name", }, "funders": { "facets": [], "sort": ["name"], "sort_default": "name", - "sort_default_no_query": "name" + "sort_default_no_query": "name", }, "names": { "facets": [], "sort": ["name"], "sort_default": "name", - "sort_default_no_query": "name" + "sort_default_no_query": "name", }, "subjects": { "facets": [], "sort": ["subject"], "sort_default": "subject", - "sort_default_no_query": "subject" + "sort_default_no_query": "subject", }, "facets": [], "sort": ["title"], "sort_default": "title", - "sort_default_no_query": "title" + "sort_default_no_query": "title", } """Vocabulary type item search configurations per type and for generic vocabularies.""" @@ -222,7 +222,7 @@ display_edit=True, display_read=True, pid_path="id", - resource_name="name" + resource_name="name", ), "awards": dict( title="Awards vocabulary items", @@ -236,7 +236,7 @@ display_edit=True, display_read=True, pid_path="id", - resource_name="name" + resource_name="name", ), "funders": dict( title="Funders vocabulary items", @@ -250,7 +250,7 @@ display_edit=True, display_read=True, pid_path="id", - resource_name="name" + resource_name="name", ), "names": dict( title="Names vocabulary items", @@ -264,7 +264,7 @@ display_edit=True, display_read=True, pid_path="id", - resource_name="name" + resource_name="name", ), "subjects": dict( title="Subjects vocabulary items", @@ -278,6 +278,6 @@ display_edit=True, display_read=True, pid_path="id", - resource_name="subject" - ) + resource_name="subject", + ), }