From cbe64265346d6927f0aeb63fe8b39e3af608fa48 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/44] 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 8ac9ecd190c0fb1742eefc66c0c8e7c3560729bf 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/44] 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 d2203667..7d7a591a 100644 --- a/invenio_vocabularies/config.py +++ b/invenio_vocabularies/config.py @@ -124,3 +124,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 d9196202..b4b17b6f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -53,6 +53,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 eaf8abfc4a2e332eadf1b846c9a841bed7861f65 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/44] 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 960d79ed6dc69735901232cbf12707c670c76938 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/44] 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 451a5758cd1c9e04bacacdefea3938dd442e8e36 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/44] 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 59fe8387836129fece0c8466df1b75842c2b995d 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/44] 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 7d7a591a..e5f24673 100644 --- a/invenio_vocabularies/config.py +++ b/invenio_vocabularies/config.py @@ -102,6 +102,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 2261cbc61115a60505733266d2b9d23d7b22f996 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/44] add missing invenio-administration dependency --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index b4b17b6f..8379f7bc 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>=5.0.0,<6.0.0 + invenio-administration>=2.0.0,<3.0.0 lxml>=4.5.0 PyYAML>=5.4.1 From 6f278886d8f4a1c7f45ab9fa21508046e3267802 Mon Sep 17 00:00:00 2001 From: Sarah Wiechers Date: Fri, 17 May 2024 11:16:16 +0200 Subject: [PATCH 08/44] 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 29cec6b29b312d1cda71ca6b7439d838069779b8 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/44] 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 e5f24673..bc729489 100644 --- a/invenio_vocabularies/config.py +++ b/invenio_vocabularies/config.py @@ -138,11 +138,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 8379f7bc..42455c02 100644 --- a/setup.cfg +++ b/setup.cfg @@ -56,6 +56,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 = @@ -70,6 +71,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 234caa8438171cb03f2478ac05efeb12401beb10 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/44] 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 bc729489..33e4ce5a 100644 --- a/invenio_vocabularies/config.py +++ b/invenio_vocabularies/config.py @@ -24,7 +24,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 42455c02..46d1d50a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -71,7 +71,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 2b62bce1209981cac9dc481d1c96454d0dc64898 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/44] 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 e7d6f0fba6dc4a445687ffb2fdd4ba9f3546a607 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/44] 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 33e4ce5a..06efa266 100644 --- a/invenio_vocabularies/config.py +++ b/invenio_vocabularies/config.py @@ -135,20 +135,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 a2ac3d2f5acf58b48420ee9434ae85c606472033 Mon Sep 17 00:00:00 2001 From: Sarah Wiechers Date: Fri, 24 May 2024 10:59:14 +0200 Subject: [PATCH 13/44] 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 | 3 +- 6 files changed, 20 insertions(+), 73 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 06efa266..80c2766b 100644 --- a/invenio_vocabularies/config.py +++ b/invenio_vocabularies/config.py @@ -111,7 +111,6 @@ "subjects", ] - VOCABULARIES_DATASTREAM_READERS = { "csv": CSVReader, "json": JsonReader, @@ -135,8 +134,7 @@ } """Data Streams writers.""" - -VOCABULARIES_SORT_OPTIONS = { +VOCABULARIES_TYPES_SORT_OPTIONS = { "name": dict( title=_("Name"), fields=["id"], @@ -146,10 +144,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 46d1d50a..6bef84cd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -56,7 +56,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 = @@ -135,7 +134,7 @@ input-file = invenio_vocabularies/translations/messages.pot output-dir = invenio_vocabularies/translations/ [isort] -profile=black +profile = black [check-manifest] ignore = From 6b6e7d322e612e0fb6da12f34e683e3b4cd31878 Mon Sep 17 00:00:00 2001 From: Sarah Wiechers Date: Fri, 24 May 2024 13:17:58 +0200 Subject: [PATCH 14/44] 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 80c2766b..df5c4057 100644 --- a/invenio_vocabularies/config.py +++ b/invenio_vocabularies/config.py @@ -139,15 +139,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 957d052de49efd586e91deca04211b0cc9db4bed Mon Sep 17 00:00:00 2001 From: Sarah Wiechers Date: Fri, 24 May 2024 13:28:21 +0200 Subject: [PATCH 15/44] 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 8c8c3bec6dbc96ba9cef85be3594eb4f4c526e2c Mon Sep 17 00:00:00 2001 From: Sarah Wiechers Date: Tue, 28 May 2024 09:23:15 +0200 Subject: [PATCH 16/44] 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 2fb11540ca31d37a76b6418c7d90e26c953ecc41 Mon Sep 17 00:00:00 2001 From: Sarah Wiechers Date: Tue, 28 May 2024 10:18:32 +0200 Subject: [PATCH 17/44] 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 df5c4057..9758b94a 100644 --- a/invenio_vocabularies/config.py +++ b/invenio_vocabularies/config.py @@ -139,7 +139,7 @@ title=_("Name"), fields=["id"], ), - "entries": dict( + "count": dict( title=_("Number of entries"), fields=["count"], ), @@ -148,6 +148,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 2c542c6903f678f90f0953e34770d6fcd21c7b52 Mon Sep 17 00:00:00 2001 From: Sarah Wiechers Date: Tue, 28 May 2024 12:59:00 +0200 Subject: [PATCH 18/44] 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 b8d3ce800c1b49f66d57da29d1cd7dd7421c23af Mon Sep 17 00:00:00 2001 From: Sarah Wiechers Date: Tue, 28 May 2024 13:38:50 +0200 Subject: [PATCH 19/44] vocabulary types: Add second list view for vocabulary items --- .../administration/views/vocabularies.py | 78 +++++++++++++++++++ invenio_vocabularies/config.py | 22 ++++++ invenio_vocabularies/services/service.py | 3 +- setup.cfg | 1 + 4 files changed, 103 insertions(+), 1 deletion(-) diff --git a/invenio_vocabularies/administration/views/vocabularies.py b/invenio_vocabularies/administration/views/vocabularies.py index f5a870b3..ccd40e1b 100644 --- a/invenio_vocabularies/administration/views/vocabularies.py +++ b/invenio_vocabularies/administration/views/vocabularies.py @@ -14,6 +14,7 @@ AdminResourceListView, ) from invenio_i18n import lazy_gettext as _ +from flask import current_app class VocabulariesListView(AdminResourceListView): @@ -44,3 +45,80 @@ class VocabulariesListView(AdminResourceListView): 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""" + + 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 = "Vocabularies_Detail" + url = "/vocabularies/" + + 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" + + # 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_TYPES_ITEMS_SEARCH" + search_facets_config_name = "VOCABULARIES_TYPES_ITEMS_FACETS" + search_sort_config_name = "VOCABULARIES_TYPES_ITEMS_SORT_OPTIONS" + + # search_config_name = "VOCABULARIES_TYPES_SEARCH" + # search_facets_config_name = "VOCABULARIES_TYPES_FACETS" + # search_sort_config_name = "VOCABULARIES_TYPES_SORT_OPTIONS" diff --git a/invenio_vocabularies/config.py b/invenio_vocabularies/config.py index 9758b94a..ba376685 100644 --- a/invenio_vocabularies/config.py +++ b/invenio_vocabularies/config.py @@ -151,3 +151,25 @@ "sort": ["name", "count"], } """Vocabulary type search configuration.""" + +VOCABULARIES_TYPES_ITEMS_SORT_OPTIONS = { + "id": dict( + title=_("Name"), + fields=["id"], + ), + "name": dict( + title=_("Name"), + fields=["id"], + ), + "created": dict( + title=_("Created"), + fields=["created"], + ) +} +"""Definitions of available Vocabulary types sort options. """ + +VOCABULARIES_TYPES_ITEMS_SEARCH = { + "facets": [], + "sort": ["id", "name", "created"], +} +"""Vocabulary type search configuration.""" diff --git a/invenio_vocabularies/services/service.py b/invenio_vocabularies/services/service.py index 6f29b6f6..001f2c64 100644 --- a/invenio_vocabularies/services/service.py +++ b/invenio_vocabularies/services/service.py @@ -246,7 +246,7 @@ class VocabularySearchOptions(SearchOptions): sort_direction_default = "asc" - sort_default_no_query = "title" + sort_default_no_query = "id" sort_direction_options = { "asc": dict( @@ -284,6 +284,7 @@ class VocabularySearchOptions(SearchOptions): pagination_options = { "default_results_per_page": 10, + "default_max_results": 20, } diff --git a/setup.cfg b/setup.cfg index 6bef84cd..205d3862 100644 --- a/setup.cfg +++ b/setup.cfg @@ -56,6 +56,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 = From ea13518a49c50ee38920bbab596f9687dc89e225 Mon Sep 17 00:00:00 2001 From: Sarah Wiechers Date: Wed, 29 May 2024 15:25:10 +0200 Subject: [PATCH 20/44] vocabulary types: Add edit view for vocabulary items --- .../administration/views/vocabularies.py | 65 +++++++++++++------ invenio_vocabularies/resources/resource.py | 13 ++++ invenio_vocabularies/services/service.py | 26 ++++++++ setup.cfg | 3 +- 4 files changed, 86 insertions(+), 21 deletions(-) diff --git a/invenio_vocabularies/administration/views/vocabularies.py b/invenio_vocabularies/administration/views/vocabularies.py index ccd40e1b..e4914840 100644 --- a/invenio_vocabularies/administration/views/vocabularies.py +++ b/invenio_vocabularies/administration/views/vocabularies.py @@ -8,13 +8,11 @@ # details. """Vocabularies admin interface.""" -from invenio_administration.views.base import ( - AdminResourceDetailView, - AdminResourceEditView, - AdminResourceListView, -) -from invenio_i18n import lazy_gettext as _ from flask import current_app +from invenio_administration.views.base import (AdminResourceEditView, + AdminResourceListView, + ) +from invenio_i18n import lazy_gettext as _ class VocabulariesListView(AdminResourceListView): @@ -50,13 +48,15 @@ class VocabulariesListView(AdminResourceListView): class VocabularyTypesDetailsView(AdminResourceListView): """Configuration for vocabularies list view.""" - def get_api_endpoint(self, pid_value=None): + def get_api_endpoint(self, vocab_type=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}" + if vocab_type in current_app.config.get( + "VOCABULARIES_CUSTOM_VOCABULARY_TYPES", [] + ): + return f"/api/{vocab_type}" else: - return f"/api/vocabularies/{pid_value}" + return f"/api/vocabularies/{vocab_type}" def get_context(self, **kwargs): """Create details view context.""" @@ -85,8 +85,8 @@ def get_context(self, **kwargs): ), } - name = "Vocabularies_Detail" - url = "/vocabularies/" + name = "vocabularies_details" + url = "/vocabularies/" api_endpoint = "/vocabularies/" @@ -95,9 +95,6 @@ def get_context(self, **kwargs): 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" # 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 @@ -107,18 +104,46 @@ def get_context(self, **kwargs): display_delete = False display_create = False - display_edit = False + display_edit = True display_search = False item_field_list = { "id": {"text": "Name", "order": 0}, - "created": {"text": "Created", "order": 1} + "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" - # search_config_name = "VOCABULARIES_TYPES_SEARCH" - # search_facets_config_name = "VOCABULARIES_TYPES_FACETS" - # search_sort_config_name = "VOCABULARIES_TYPES_SORT_OPTIONS" + +class VocabularyTypesDetailsEditView(AdminResourceEditView): + """Configuration for vocabulary item edit view.""" + + def get_api_endpoint(self, vocab_type=None, pid=None): + """overwrite get_api_endpoint to accept pid_value""" + if vocab_type in current_app.config.get( + "VOCABULARIES_CUSTOM_VOCABULARY_TYPES", [] + ): + return f"/api/{vocab_type}/{pid}" + else: + return f"/api/vocabularies/{vocab_type}/{pid}" + + name = "vocabularies_details_edit" + url = "/vocabularies///edit" + resource_config = "vocabulary_admin_resource" + pid_path = "id" + api_endpoint = "/vocabularies" + title = "Edit vocabulary item" + + list_view_name = "vocabularies_details" + + form_fields = { + "ID": { + "order": 1, + "text": _("Set ID"), + "description": _("Some ID."), + }, + "created": {"order": 2}, + "updated": {"order": 3}, + } diff --git a/invenio_vocabularies/resources/resource.py b/invenio_vocabularies/resources/resource.py index 229c7e5e..632db119 100644 --- a/invenio_vocabularies/resources/resource.py +++ b/invenio_vocabularies/resources/resource.py @@ -172,3 +172,16 @@ def read(self): ) item = self.service.read(g.identity, pid_value) return item.to_dict(), 200 + + @request_headers + @request_view_args + @request_data + @response_handler() + def update(self): + """Update an item.""" + item = self.service.update( + g.identity, + resource_requestctx.view_args["id"], + resource_requestctx.data, + ) + return item.to_dict(), 200 diff --git a/invenio_vocabularies/services/service.py b/invenio_vocabularies/services/service.py index 001f2c64..2316dd6b 100644 --- a/invenio_vocabularies/services/service.py +++ b/invenio_vocabularies/services/service.py @@ -196,6 +196,32 @@ def search(self, identity, params=None): links_item_tpl=self.links_item_tpl, ) + @unit_of_work() + def update(self, identity, id_, data, uow=None): + """Update an OAI set.""" + self.require_permission(identity, "update") + + vocabulary_item, errors = self._get_one(id=id_) + # if oai_set.system_created: + # raise OAIPMHSetNotEditable(oai_set.id) + + valid_data, errors = self.schema.load( + data, + context={"identity": identity}, + raise_errors=True, + ) + + for key, value in valid_data.items(): + setattr(vocabulary_item, key, value) + # uow.register(OAISetCommitOp(oai_set)) + + return self.result_item( + service=self, + identity=identity, + item=vocabulary_item, + links_tpl=self.links_item_tpl, + ) + def _custom_vocabulary_statistics(self): # query database for count of terms in custom vocabularies returndict = {} diff --git a/setup.cfg b/setup.cfg index 205d3862..7cef9c4d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -56,7 +56,8 @@ 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 + vocabulary_types_details = invenio_vocabularies.administration.views.vocabularies:VocabularyTypesDetailsView + vocabulary_types_details_edit = invenio_vocabularies.administration.views.vocabularies:VocabularyTypesDetailsEditView invenio_base.apps = invenio_vocabularies = invenio_vocabularies:InvenioVocabularies invenio_base.api_apps = From a5442136f98e2c5867dd1f1a1ae7bd3654235878 Mon Sep 17 00:00:00 2001 From: Sarah Wiechers Date: Wed, 29 May 2024 15:29:22 +0200 Subject: [PATCH 21/44] vocabulary types: formatting and sorting imports --- .../administration/views/vocabularies.py | 11 +++++----- invenio_vocabularies/config.py | 2 +- invenio_vocabularies/services/service.py | 22 +++++++++---------- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/invenio_vocabularies/administration/views/vocabularies.py b/invenio_vocabularies/administration/views/vocabularies.py index e4914840..d5dfe814 100644 --- a/invenio_vocabularies/administration/views/vocabularies.py +++ b/invenio_vocabularies/administration/views/vocabularies.py @@ -9,9 +9,10 @@ """Vocabularies admin interface.""" from flask import current_app -from invenio_administration.views.base import (AdminResourceEditView, - AdminResourceListView, - ) +from invenio_administration.views.base import ( + AdminResourceEditView, + AdminResourceListView, +) from invenio_i18n import lazy_gettext as _ @@ -52,7 +53,7 @@ def get_api_endpoint(self, vocab_type=None): """overwrite get_api_endpoint to accept pid_value""" if vocab_type in current_app.config.get( - "VOCABULARIES_CUSTOM_VOCABULARY_TYPES", [] + "VOCABULARIES_CUSTOM_VOCABULARY_TYPES", [] ): return f"/api/{vocab_type}" else: @@ -123,7 +124,7 @@ class VocabularyTypesDetailsEditView(AdminResourceEditView): def get_api_endpoint(self, vocab_type=None, pid=None): """overwrite get_api_endpoint to accept pid_value""" if vocab_type in current_app.config.get( - "VOCABULARIES_CUSTOM_VOCABULARY_TYPES", [] + "VOCABULARIES_CUSTOM_VOCABULARY_TYPES", [] ): return f"/api/{vocab_type}/{pid}" else: diff --git a/invenio_vocabularies/config.py b/invenio_vocabularies/config.py index ba376685..767b2dad 100644 --- a/invenio_vocabularies/config.py +++ b/invenio_vocabularies/config.py @@ -164,7 +164,7 @@ "created": dict( title=_("Created"), fields=["created"], - ) + ), } """Definitions of available Vocabulary types sort options. """ diff --git a/invenio_vocabularies/services/service.py b/invenio_vocabularies/services/service.py index 2316dd6b..e6f50198 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: @@ -254,8 +254,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=[ @@ -379,7 +379,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 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 22/44] 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 23/44] 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 24/44] 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 25/44] 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 26/44] 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 27/44] 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 28/44] 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 29/44] 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 30/44] 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 31/44] 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 32/44] 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 33/44] 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 34/44] 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 35/44] 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 36/44] 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 37/44] 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 38/44] 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 39/44] 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 40/44] 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 41/44] 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 42/44] 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 7d1f01e746893da5b9c499321ff9b88215fea50e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karl-Ulrich=20Kr=C3=A4gelin?= Date: Mon, 17 Jun 2024 08:09:39 +0200 Subject: [PATCH 43/44] missing imports --- invenio_vocabularies/administration/views/vocabularies.py | 3 ++- invenio_vocabularies/services/service.py | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/invenio_vocabularies/administration/views/vocabularies.py b/invenio_vocabularies/administration/views/vocabularies.py index c6565cc0..9beaf9cf 100644 --- a/invenio_vocabularies/administration/views/vocabularies.py +++ b/invenio_vocabularies/administration/views/vocabularies.py @@ -13,6 +13,7 @@ AdminResourceEditView, AdminResourceListView, ) +from invenio_i18n import lazy_gettext as _ class VocabulariesListView(AdminResourceListView): @@ -141,7 +142,7 @@ def get_api_endpoint(self, vocab_type=None, pid=None): form_fields = { "ID": { "order": 1, - "text": _("Set ID"), + "text": "Set ID", "description": _("Some ID."), }, "created": {"order": 2}, diff --git a/invenio_vocabularies/services/service.py b/invenio_vocabularies/services/service.py index 61e61037..11825f6a 100644 --- a/invenio_vocabularies/services/service.py +++ b/invenio_vocabularies/services/service.py @@ -20,6 +20,10 @@ SearchOptions, pagination_links, ) +from invenio_records_resources.services.base.results import ( + ServiceItemResult, + ServiceListResult, +) from invenio_records_resources.services.base.utils import map_search_params from invenio_records_resources.services.records.schema import ServiceSchemaWrapper from invenio_records_resources.services.uow import unit_of_work From b03fb4b8a4eea5c6dd13e7e32e425baf99589922 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karl-Ulrich=20Kr=C3=A4gelin?= Date: Mon, 17 Jun 2024 11:32:57 +0200 Subject: [PATCH 44/44] administration: list view after list view --- .../administration/views/vocabularies.py | 97 ++++++++++--------- invenio_vocabularies/config.py | 21 +++- invenio_vocabularies/services/config.py | 4 + 3 files changed, 72 insertions(+), 50 deletions(-) diff --git a/invenio_vocabularies/administration/views/vocabularies.py b/invenio_vocabularies/administration/views/vocabularies.py index 9beaf9cf..db8a2fbd 100644 --- a/invenio_vocabularies/administration/views/vocabularies.py +++ b/invenio_vocabularies/administration/views/vocabularies.py @@ -32,6 +32,7 @@ class VocabulariesListView(AdminResourceListView): display_search = True display_delete = False + display_create = False display_edit = False item_field_list = { @@ -47,7 +48,7 @@ class VocabulariesListView(AdminResourceListView): class VocabularyTypesDetailsView(AdminResourceListView): - """Configuration for vocabularies list view.""" + """Configuration for specific vocabularies list view after initial list view.""" def get_api_endpoint(self, vocab_type=None): """overwrite get_api_endpoint to accept pid_value""" @@ -59,40 +60,40 @@ def get_api_endpoint(self, vocab_type=None): else: return f"/api/vocabularies/{vocab_type}" - 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 = "vocabularies_details" - url = "/vocabularies/" - - api_endpoint = "/vocabularies/" + # 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 = "vocabularies-details" + url = "/vocabulary-types/" + + # api_endpoint = "/vocabularies/" # INFO name of the resource's list view name, enables navigation between detail view and list view. - list_view_name = "Vocabularies" + list_view_name = "vocabulary-types" resource_config = "vocabulary_admin_resource" search_request_headers = {"Accept": "application/json"} @@ -109,8 +110,8 @@ def get_context(self, **kwargs): display_search = False item_field_list = { - "id": {"text": "Name", "order": 0}, - "created": {"text": "Created", "order": 1}, + "id": {"text": "ID", "order": 1}, + "created": {"text": "Created", "order": 2}, } search_config_name = "VOCABULARIES_TYPES_ITEMS_SEARCH" @@ -121,30 +122,30 @@ def get_context(self, **kwargs): class VocabularyTypesDetailsEditView(AdminResourceEditView): """Configuration for vocabulary item edit view.""" - def get_api_endpoint(self, vocab_type=None, pid=None): + # Edit view for vocabulary items from a specific vocabulary type + def get_api_endpoint(vocab_type=None, pid=None): """overwrite get_api_endpoint to accept pid_value""" if vocab_type in current_app.config.get( "VOCABULARIES_CUSTOM_VOCABULARY_TYPES", [] ): return f"/api/{vocab_type}/{pid}" else: - return f"/api/vocabularies/{vocab_type}/{pid}" + return f"/api/vocabulary-types/{vocab_type}/{pid}" name = "vocabularies_details_edit" - url = "/vocabularies///edit" + url = "/vocabulary-types///edit" resource_config = "vocabulary_admin_resource" pid_path = "id" - api_endpoint = "/vocabularies" title = "Edit vocabulary item" list_view_name = "vocabularies_details" - form_fields = { - "ID": { - "order": 1, - "text": "Set ID", - "description": _("Some ID."), - }, - "created": {"order": 2}, - "updated": {"order": 3}, - } + # form_fields = { + # "ID": { + # "order": 1, + # "text": "Set ID", + # "description": _("Some ID."), + # }, + # "created": {"order": 2}, + # "updated": {"order": 3}, + # } diff --git a/invenio_vocabularies/config.py b/invenio_vocabularies/config.py index 56f6c252..f615919a 100644 --- a/invenio_vocabularies/config.py +++ b/invenio_vocabularies/config.py @@ -146,10 +146,27 @@ fields=["count"], ), } -"""Definitions of available Vocabulary types sort options. """ - VOCABULARIES_TYPES_SEARCH = { "facets": [], "sort": ["name", "count"], } + + +VOCABULARIES_TYPES_ITEMS_SORT_OPTIONS = { + "name": dict( + title=_("Name"), + fields=["id"], + ), + "created": dict( + title=_("Created"), + fields=["created"], + ), +} +VOCABULARIES_TYPES_ITEMS_SEARCH = { + "facets": [], + "sort": ["name", "created"], +} +"""Definitions of available Vocabulary types sort options. """ + + """Vocabulary type search configuration.""" diff --git a/invenio_vocabularies/services/config.py b/invenio_vocabularies/services/config.py index 9406c668..7a75f4c9 100644 --- a/invenio_vocabularies/services/config.py +++ b/invenio_vocabularies/services/config.py @@ -108,6 +108,10 @@ class VocabularyTypeSearchOptions(SearchOptions): title=_("ID"), fields=["id"], ), + "created": dict( + title=_("Created"), + fields=["created"], + ), } sort_default = "id"