diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d0c3f4b9..beaff20e 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # # This file is part of Invenio. -# Copyright (C) 2020 CERN. +# Copyright (C) 2020-2024 CERN. # Copyright (C) 2022 Graz University of Technology. # # Invenio is free software; you can redistribute it and/or modify it @@ -30,17 +30,18 @@ jobs: Tests: runs-on: ubuntu-20.04 strategy: + fail-fast: false matrix: - python-version: [3.8, 3.9] - requirements-level: [pypi] + python-version: ['3.9', '3.10', '3.11', '3.12'] db-service: [postgresql14] search-service: [opensearch2, elasticsearch7] - node-version: [16.x] + node-version: [18.x, 20.x] include: - search-service: opensearch2 SEARCH_EXTRAS: "opensearch2" - search-service: elasticsearch7 SEARCH_EXTRAS: "elasticsearch7" + env: DB: ${{ matrix.db-service }} SEARCH: ${{ matrix.search-service }} @@ -48,42 +49,32 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} + - name: Run eslint test run: ./run-js-linter.sh -i - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - - - name: Generate dependencies - run: | - pip install wheel requirements-builder - requirements-builder -e "$EXTRAS" --level=${{ matrix.requirements-level }} setup.py > .${{ matrix.requirements-level }}-${{ matrix.python-version }}-requirements.txt - - - name: Cache pip - uses: actions/cache@v2 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('.${{ matrix.requirements-level }}-${{ matrix.python-version }}-requirements.txt') }} + cache: pip + cache-dependency-path: setup.cfg - name: Install dependencies run: | - pip install -r .${{ matrix.requirements-level }}-${{ matrix.python-version }}-requirements.txt pip install ".[$EXTRAS]" pip freeze docker --version docker-compose --version - name: Run tests - run: | - ./run-tests.sh + run: ./run-tests.sh - name: Install deps for frontend tests working-directory: ./invenio_vocabularies/assets/semantic-ui/js/invenio_vocabularies diff --git a/CHANGES.rst b/CHANGES.rst index 41e1d08c..6f375d31 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,5 +1,5 @@ .. - Copyright (C) 2020-2023 CERN. + Copyright (C) 2020-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 @@ -8,6 +8,29 @@ Changes ======= +Version 3.3.0 (released 2024-04-16) + +- assets: add overridable awards and funding + +Version 3.2.0 (released 2024-03-22) + +- funding: add country and ror to funder search results +- init: move record_once to finalize_app (removes deprecation on `before_first_request`) +- installation: upgrade invenio-app + + +Version 3.1.0 (released 2024-03-05) + +- custom_fields: added subject field +- custom_fields: add pid_field to custom fields +- mappings: change "dynamic" values to string +- ci: upgrade tests matrix +- bumps react-invenio-forms + +Version 3.0.0 (released 2024-01-30) + +- installation: bump invenio-records-resources + Version 2.4.0 (2023-12-07) - schema: add validation for affiliations diff --git a/invenio_vocabularies/__init__.py b/invenio_vocabularies/__init__.py index 1a2cc653..51337af8 100644 --- a/invenio_vocabularies/__init__.py +++ b/invenio_vocabularies/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2020-2023 CERN. +# Copyright (C) 2020-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 @@ -10,6 +10,6 @@ from .ext import InvenioVocabularies -__version__ = "2.4.0" +__version__ = "3.3.0" __all__ = ("__version__", "InvenioVocabularies") diff --git a/invenio_vocabularies/assets/semantic-ui/js/invenio_vocabularies/src/contrib/forms/Funding/CustomAwardForm.js b/invenio_vocabularies/assets/semantic-ui/js/invenio_vocabularies/src/contrib/forms/Funding/CustomAwardForm.js index 764e2c1d..0161811b 100644 --- a/invenio_vocabularies/assets/semantic-ui/js/invenio_vocabularies/src/contrib/forms/Funding/CustomAwardForm.js +++ b/invenio_vocabularies/assets/semantic-ui/js/invenio_vocabularies/src/contrib/forms/Funding/CustomAwardForm.js @@ -12,17 +12,24 @@ import { TextField, RemoteSelectField } from "react-invenio-forms"; import { i18next } from "@translations/invenio_rdm_records/i18next"; import _isEmpty from "lodash/isEmpty"; +import Overridable from "react-overridable"; + function CustomAwardForm({ deserializeFunder, selectedFunding }) { function deserializeFunderToDropdown(funderItem) { let funderName = null; let funderPID = null; + let funderCountry = null; if (funderItem.name) { funderName = funderItem.name; } - if (funderItem.pid) { - funderPID = funderItem.pid; + if (funderItem.id) { + funderPID = funderItem.id; + } + + if (funderItem.country) { + funderCountry = funderItem.country; } if (!funderName && !funderPID) { @@ -30,7 +37,7 @@ function CustomAwardForm({ deserializeFunder, selectedFunding }) { } return { - text: funderName || funderPID, + text: [funderName, funderCountry, funderPID].filter((val) => val).join(", "), value: funderItem.id, key: funderItem.id, ...(funderName && { name: funderName }), @@ -48,64 +55,85 @@ function CustomAwardForm({ deserializeFunder, selectedFunding }) { return (
- { - return funders.map((funder) => - deserializeFunderToDropdown(deserializeFunder(funder)) - ); - }} - searchInput={{ - autoFocus: _isEmpty(selectedFunding), - }} - label={i18next.t("Funder")} - noQueryMessage={i18next.t("Search for funder...")} - clearable - allowAdditions={false} - multiple={false} - selectOnBlur={false} - selectOnNavigation={false} - required - search={(options) => options} - isFocused - onValueChange={({ formikProps }, selectedFundersArray) => { - if (selectedFundersArray.length === 1) { - const selectedFunder = selectedFundersArray[0]; - if (selectedFunder) { - const deserializedFunder = serializeFunderFromDropdown(selectedFunder); - formikProps.form.setFieldValue( - "selectedFunding.funder", - deserializedFunder - ); + > + { + return funders.map((funder) => + deserializeFunderToDropdown(deserializeFunder(funder)) + ); + }} + searchInput={{ + autoFocus: _isEmpty(selectedFunding), + }} + label={i18next.t("Funder")} + noQueryMessage={i18next.t("Search for funder...")} + clearable + allowAdditions={false} + multiple={false} + selectOnBlur={false} + selectOnNavigation={false} + required + search={(options) => options} + isFocused + onValueChange={({ formikProps }, selectedFundersArray) => { + if (selectedFundersArray.length === 1) { + const selectedFunder = selectedFundersArray[0]; + if (selectedFunder) { + const deserializedFunder = serializeFunderFromDropdown(selectedFunder); + formikProps.form.setFieldValue( + "selectedFunding.funder", + deserializedFunder + ); + } } - } - }} - /> - -
- {i18next.t("Award information")} ({i18next.t("optional")}) -
+ }} + /> + + +
+ {i18next.t("Award information")} ({i18next.t("optional")}) +
+
- - + + + - + + + + > + + ); diff --git a/invenio_vocabularies/assets/semantic-ui/js/invenio_vocabularies/src/contrib/forms/Funding/FundingField.js b/invenio_vocabularies/assets/semantic-ui/js/invenio_vocabularies/src/contrib/forms/Funding/FundingField.js index 819c4dd6..62d424f1 100644 --- a/invenio_vocabularies/assets/semantic-ui/js/invenio_vocabularies/src/contrib/forms/Funding/FundingField.js +++ b/invenio_vocabularies/assets/semantic-ui/js/invenio_vocabularies/src/contrib/forms/Funding/FundingField.js @@ -18,6 +18,8 @@ import FundingModal from "./FundingModal"; import { i18next } from "@translations/invenio_rdm_records/i18next"; +import Overridable from "react-overridable"; + function FundingFieldForm(props) { const { label, @@ -108,46 +110,52 @@ function FundingFieldForm(props) { ); })} - - - {i18next.t("Add award")} - - } - onAwardChange={(selectedFunding) => { - formikArrayPush(selectedFunding); - }} - mode="standard" - action="add" - deserializeAward={deserializeAward} - deserializeFunder={deserializeFunder} - computeFundingContents={computeFundingContents} - /> - - - {i18next.t("Add custom")} - - } - onAwardChange={(selectedFunding) => { - formikArrayPush(selectedFunding); - }} - mode="custom" - action="add" - deserializeAward={deserializeAward} - deserializeFunder={deserializeFunder} - computeFundingContents={computeFundingContents} - /> + + + + + {i18next.t("Add award")} + + } + onAwardChange={(selectedFunding) => { + formikArrayPush(selectedFunding); + }} + mode="standard" + action="add" + deserializeAward={deserializeAward} + deserializeFunder={deserializeFunder} + computeFundingContents={computeFundingContents} + /> + + + + + + {i18next.t("Add custom")} + + } + onAwardChange={(selectedFunding) => { + formikArrayPush(selectedFunding); + }} + mode="custom" + action="add" + deserializeAward={deserializeAward} + deserializeFunder={deserializeFunder} + computeFundingContents={computeFundingContents} + /> + ); diff --git a/invenio_vocabularies/contrib/affiliations/mappings/os-v1/affiliations/affiliation-v1.0.0.json b/invenio_vocabularies/contrib/affiliations/mappings/os-v1/affiliations/affiliation-v1.0.0.json index 891033c0..07429f3b 100644 --- a/invenio_vocabularies/contrib/affiliations/mappings/os-v1/affiliations/affiliation-v1.0.0.json +++ b/invenio_vocabularies/contrib/affiliations/mappings/os-v1/affiliations/affiliation-v1.0.0.json @@ -84,7 +84,7 @@ }, "title": { "type": "object", - "dynamic": true + "dynamic": "true" } } } diff --git a/invenio_vocabularies/contrib/affiliations/mappings/os-v2/affiliations/affiliation-v1.0.0.json b/invenio_vocabularies/contrib/affiliations/mappings/os-v2/affiliations/affiliation-v1.0.0.json index 891033c0..07429f3b 100644 --- a/invenio_vocabularies/contrib/affiliations/mappings/os-v2/affiliations/affiliation-v1.0.0.json +++ b/invenio_vocabularies/contrib/affiliations/mappings/os-v2/affiliations/affiliation-v1.0.0.json @@ -84,7 +84,7 @@ }, "title": { "type": "object", - "dynamic": true + "dynamic": "true" } } } diff --git a/invenio_vocabularies/contrib/affiliations/mappings/v7/affiliations/affiliation-v1.0.0.json b/invenio_vocabularies/contrib/affiliations/mappings/v7/affiliations/affiliation-v1.0.0.json index 891033c0..07429f3b 100644 --- a/invenio_vocabularies/contrib/affiliations/mappings/v7/affiliations/affiliation-v1.0.0.json +++ b/invenio_vocabularies/contrib/affiliations/mappings/v7/affiliations/affiliation-v1.0.0.json @@ -84,7 +84,7 @@ }, "title": { "type": "object", - "dynamic": true + "dynamic": "true" } } } diff --git a/invenio_vocabularies/contrib/awards/mappings/os-v1/awards/award-v1.0.0.json b/invenio_vocabularies/contrib/awards/mappings/os-v1/awards/award-v1.0.0.json index 22cd91fb..8fcdc343 100644 --- a/invenio_vocabularies/contrib/awards/mappings/os-v1/awards/award-v1.0.0.json +++ b/invenio_vocabularies/contrib/awards/mappings/os-v1/awards/award-v1.0.0.json @@ -47,7 +47,7 @@ }, "title": { "type": "object", - "dynamic": true + "dynamic": "true" }, "number": { "type": "keyword" diff --git a/invenio_vocabularies/contrib/awards/mappings/os-v2/awards/award-v1.0.0.json b/invenio_vocabularies/contrib/awards/mappings/os-v2/awards/award-v1.0.0.json index 22cd91fb..8fcdc343 100644 --- a/invenio_vocabularies/contrib/awards/mappings/os-v2/awards/award-v1.0.0.json +++ b/invenio_vocabularies/contrib/awards/mappings/os-v2/awards/award-v1.0.0.json @@ -47,7 +47,7 @@ }, "title": { "type": "object", - "dynamic": true + "dynamic": "true" }, "number": { "type": "keyword" diff --git a/invenio_vocabularies/contrib/awards/mappings/v7/awards/award-v1.0.0.json b/invenio_vocabularies/contrib/awards/mappings/v7/awards/award-v1.0.0.json index 22cd91fb..8fcdc343 100644 --- a/invenio_vocabularies/contrib/awards/mappings/v7/awards/award-v1.0.0.json +++ b/invenio_vocabularies/contrib/awards/mappings/v7/awards/award-v1.0.0.json @@ -47,7 +47,7 @@ }, "title": { "type": "object", - "dynamic": true + "dynamic": "true" }, "number": { "type": "keyword" diff --git a/invenio_vocabularies/contrib/funders/mappings/os-v1/funders/funder-v1.0.0.json b/invenio_vocabularies/contrib/funders/mappings/os-v1/funders/funder-v1.0.0.json index a52fc80d..2c635b57 100644 --- a/invenio_vocabularies/contrib/funders/mappings/os-v1/funders/funder-v1.0.0.json +++ b/invenio_vocabularies/contrib/funders/mappings/os-v1/funders/funder-v1.0.0.json @@ -62,7 +62,7 @@ }, "title": { "type": "object", - "dynamic": true + "dynamic": "true" } } } diff --git a/invenio_vocabularies/contrib/funders/mappings/os-v2/funders/funder-v1.0.0.json b/invenio_vocabularies/contrib/funders/mappings/os-v2/funders/funder-v1.0.0.json index a52fc80d..2c635b57 100644 --- a/invenio_vocabularies/contrib/funders/mappings/os-v2/funders/funder-v1.0.0.json +++ b/invenio_vocabularies/contrib/funders/mappings/os-v2/funders/funder-v1.0.0.json @@ -62,7 +62,7 @@ }, "title": { "type": "object", - "dynamic": true + "dynamic": "true" } } } diff --git a/invenio_vocabularies/contrib/funders/mappings/v7/funders/funder-v1.0.0.json b/invenio_vocabularies/contrib/funders/mappings/v7/funders/funder-v1.0.0.json index a52fc80d..2c635b57 100644 --- a/invenio_vocabularies/contrib/funders/mappings/v7/funders/funder-v1.0.0.json +++ b/invenio_vocabularies/contrib/funders/mappings/v7/funders/funder-v1.0.0.json @@ -62,7 +62,7 @@ }, "title": { "type": "object", - "dynamic": true + "dynamic": "true" } } } diff --git a/invenio_vocabularies/datastreams/readers.py b/invenio_vocabularies/datastreams/readers.py index 0b02128b..af7a6122 100644 --- a/invenio_vocabularies/datastreams/readers.py +++ b/invenio_vocabularies/datastreams/readers.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2021-2022 CERN. +# Copyright (C) 2021-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 @@ -23,6 +23,7 @@ from lxml.html import parse as html_parse from .errors import ReaderError +from .xml import etree_to_dict class BaseReader(ABC): @@ -206,32 +207,11 @@ def _iter(self, fp, *args, **kwargs): class XMLReader(BaseReader): """XML reader.""" - @classmethod - def _etree_to_dict(cls, tree): - d = {tree.tag: {} if tree.attrib else None} - children = list(tree) - if children: - dd = defaultdict(list) - for dc in map(cls._etree_to_dict, children): - for k, v in dc.items(): - dd[k].append(v) - d = {tree.tag: {k: v[0] if len(v) == 1 else v for k, v in dd.items()}} - if tree.attrib: - d[tree.tag].update(("@" + k, v) for k, v in tree.attrib.items()) - if tree.text: - text = tree.text.strip() - if children or tree.attrib: - if text: - d[tree.tag]["#text"] = text - else: - d[tree.tag] = text - return d - def _iter(self, fp, *args, **kwargs): """Read and parse an XML file to dict.""" # NOTE: We parse HTML, to skip XML validation and strip XML namespaces xml_tree = html_parse(fp).getroot() - record = self._etree_to_dict(xml_tree)["html"]["body"].get("record") + record = etree_to_dict(xml_tree)["html"]["body"].get("record") if not record: raise ReaderError(f"Record not found in XML entry.") diff --git a/invenio_vocabularies/datastreams/transformers.py b/invenio_vocabularies/datastreams/transformers.py index 09e74663..d4274a68 100644 --- a/invenio_vocabularies/datastreams/transformers.py +++ b/invenio_vocabularies/datastreams/transformers.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2021-2022 CERN. +# Copyright (C) 2021-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 @@ -9,11 +9,11 @@ """Transformers module.""" from abc import ABC, abstractmethod -from collections import defaultdict from lxml import etree from .errors import TransformerError +from .xml import etree_to_dict class BaseTransformer(ABC): @@ -37,34 +37,13 @@ def _xml_to_etree(cls, xml): """Converts XML to a lxml etree.""" return etree.HTML(xml) - @classmethod - def _etree_to_dict(cls, tree): - d = {tree.tag: {} if tree.attrib else None} - children = list(tree) - if children: - dd = defaultdict(list) - for dc in map(cls._etree_to_dict, children): - for k, v in dc.items(): - dd[k].append(v) - d = {tree.tag: {k: v[0] if len(v) == 1 else v for k, v in dd.items()}} - if tree.attrib: - d[tree.tag].update(("@" + k, v) for k, v in tree.attrib.items()) - if tree.text: - text = tree.text.strip() - if children or tree.attrib: - if text: - d[tree.tag]["#text"] = text - else: - d[tree.tag] = text - return d - def apply(self, stream_entry, **kwargs): """Applies the transformation to the stream entry. Requires the root element to be named "record". """ xml_tree = self._xml_to_etree(stream_entry.entry) - record = self._etree_to_dict(xml_tree)["html"]["body"].get("record") + record = etree_to_dict(xml_tree)["html"]["body"].get("record") if not record: raise TransformerError(f"Record not found in XML entry.") diff --git a/invenio_vocabularies/datastreams/xml.py b/invenio_vocabularies/datastreams/xml.py new file mode 100644 index 00000000..d26e2e22 --- /dev/null +++ b/invenio_vocabularies/datastreams/xml.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021-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. + +"""XML utils.""" + +from collections import defaultdict + + +def etree_to_dict(tree): + """Convert an ElementTree to a dictionary.""" + tag = tree.tag.split(":")[-1] # strip namespace + d = {tag: {} if tree.attrib else None} + children = list(tree) + if children: + dd = defaultdict(list) + for dc in map(etree_to_dict, children): + for k, v in dc.items(): + dd[k].append(v) + d = {tag: {k: v[0] if len(v) == 1 else v for k, v in dd.items()}} + if tree.attrib: + d[tag].update(("@" + k, v) for k, v in tree.attrib.items()) + if tree.text: + text = tree.text.strip() + if children or tree.attrib: + if text: + d[tag]["#text"] = text + else: + d[tag] = text + return d diff --git a/invenio_vocabularies/ext.py b/invenio_vocabularies/ext.py index 4c34513c..4e7ab481 100644 --- a/invenio_vocabularies/ext.py +++ b/invenio_vocabularies/ext.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # # Copyright (C) 2020-2022 CERN. +# Copyright (C) 2023 Graz University of Technology. # # 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 @@ -123,3 +124,41 @@ def init_resource(self, app): service=self.service, config=app.config["VOCABULARIES_RESOURCE_CONFIG"], ) + + +def finalize_app(app): + """Finalize app. + + NOTE: replace former @record_once decorator + """ + init(app) + + +def api_finalize_app(app): + """Api Finalize app. + + NOTE: replace former @record_once decorator + """ + init(app) + + +def init(app): + """Init app.""" + # Register services - cannot be done in extension because + # Invenio-Records-Resources might not have been initialized. + sregistry = app.extensions["invenio-records-resources"].registry + ext = app.extensions["invenio-vocabularies"] + sregistry.register(ext.affiliations_service, service_id="affiliations") + sregistry.register(ext.awards_service, service_id="awards") + 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") + # Register indexers + iregistry = app.extensions["invenio-indexer"].registry + iregistry.register(ext.affiliations_service.indexer, indexer_id="affiliations") + iregistry.register(ext.awards_service.indexer, indexer_id="awards") + 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") diff --git a/invenio_vocabularies/records/mappings/os-v1/vocabularies/vocabulary-v1.0.0.json b/invenio_vocabularies/records/mappings/os-v1/vocabularies/vocabulary-v1.0.0.json index dc81057b..fdd6525b 100644 --- a/invenio_vocabularies/records/mappings/os-v1/vocabularies/vocabulary-v1.0.0.json +++ b/invenio_vocabularies/records/mappings/os-v1/vocabularies/vocabulary-v1.0.0.json @@ -81,7 +81,7 @@ }, "title": { "type": "object", - "dynamic": true, + "dynamic": "true", "properties": { "en": { "type": "search_as_you_type", @@ -91,7 +91,7 @@ }, "description": { "type": "object", - "dynamic": true + "dynamic": "true" }, "icon": { "type": "keyword", @@ -102,7 +102,7 @@ }, "props": { "type": "object", - "dynamic": true + "dynamic": "true" } } } diff --git a/invenio_vocabularies/records/mappings/os-v2/vocabularies/vocabulary-v1.0.0.json b/invenio_vocabularies/records/mappings/os-v2/vocabularies/vocabulary-v1.0.0.json index dc81057b..fdd6525b 100644 --- a/invenio_vocabularies/records/mappings/os-v2/vocabularies/vocabulary-v1.0.0.json +++ b/invenio_vocabularies/records/mappings/os-v2/vocabularies/vocabulary-v1.0.0.json @@ -81,7 +81,7 @@ }, "title": { "type": "object", - "dynamic": true, + "dynamic": "true", "properties": { "en": { "type": "search_as_you_type", @@ -91,7 +91,7 @@ }, "description": { "type": "object", - "dynamic": true + "dynamic": "true" }, "icon": { "type": "keyword", @@ -102,7 +102,7 @@ }, "props": { "type": "object", - "dynamic": true + "dynamic": "true" } } } diff --git a/invenio_vocabularies/records/mappings/v7/vocabularies/vocabulary-v1.0.0.json b/invenio_vocabularies/records/mappings/v7/vocabularies/vocabulary-v1.0.0.json index dc81057b..fdd6525b 100644 --- a/invenio_vocabularies/records/mappings/v7/vocabularies/vocabulary-v1.0.0.json +++ b/invenio_vocabularies/records/mappings/v7/vocabularies/vocabulary-v1.0.0.json @@ -81,7 +81,7 @@ }, "title": { "type": "object", - "dynamic": true, + "dynamic": "true", "properties": { "en": { "type": "search_as_you_type", @@ -91,7 +91,7 @@ }, "description": { "type": "object", - "dynamic": true + "dynamic": "true" }, "icon": { "type": "keyword", @@ -102,7 +102,7 @@ }, "props": { "type": "object", - "dynamic": true + "dynamic": "true" } } } diff --git a/invenio_vocabularies/records/systemfields/relations.py b/invenio_vocabularies/records/systemfields/relations.py index c78543ee..0a39788a 100644 --- a/invenio_vocabularies/records/systemfields/relations.py +++ b/invenio_vocabularies/records/systemfields/relations.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2022 CERN. +# Copyright (C) 2022-2024 CERN. # # Invenio-Records-Resources is free software; you can redistribute it and/or # modify it under the terms of the MIT License; see LICENSE file for more @@ -38,7 +38,7 @@ def _fields(self): relations[cf.name] = cf.relation_cls( f"custom_fields.{cf.name}", keys=cf.field_keys, - pid_field=Vocabulary.pid.with_type_ctx(cf.vocabulary_id), + pid_field=cf.pid_field, cache_key=cf.vocabulary_id, ) diff --git a/invenio_vocabularies/services/custom_fields/__init__.py b/invenio_vocabularies/services/custom_fields/__init__.py index 1ce96f3c..69e8e08f 100644 --- a/invenio_vocabularies/services/custom_fields/__init__.py +++ b/invenio_vocabularies/services/custom_fields/__init__.py @@ -1,12 +1,16 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2022 CERN. +# Copyright (C) 2022-2024 CERN. # # Invenio-RDM-Records is free software; you can redistribute it and/or modify # it under the terms of the MIT License; see LICENSE file for more details. """Custom Fields for InvenioRDM.""" +from .subject import SUBJECT_FIELDS, SUBJECT_FIELDS_UI from .vocabulary import VocabularyCF -__all__ = "VocabularyCF" +__all__ = [ + "VocabularyCF", + "SUBJECT_FIELDS_UI" "SUBJECT_FIELDS", +] diff --git a/invenio_vocabularies/services/custom_fields/subject.py b/invenio_vocabularies/services/custom_fields/subject.py new file mode 100644 index 00000000..6ee44834 --- /dev/null +++ b/invenio_vocabularies/services/custom_fields/subject.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2024-2024 CERN. +# +# Invenio-RDM-Records is free software; you can redistribute it and/or modify +# it under the terms of the MIT License; see LICENSE file for more details. + + +"""Custom fields.""" +from invenio_i18n import lazy_gettext as _ + +from ...contrib.subjects.api import Subject +from ...contrib.subjects.schema import SubjectRelationSchema +from .vocabulary import VocabularyCF + + +class SubjectCF(VocabularyCF): + """Custom field for subjects.""" + + field_keys = ["id", "subject"] + + def __init__(self, **kwargs): + """Constructor.""" + super().__init__( + vocabulary_id="subjects", + schema=SubjectRelationSchema, + ui_schema=SubjectRelationSchema, + **kwargs + ) + self.pid_field = Subject.pid + + @property + def mapping(self): + """Return the mapping.""" + _mapping = { + "type": "object", + "properties": { + "@v": {"type": "keyword"}, + "id": {"type": "keyword"}, + "subject": {"type": "keyword"}, + }, + } + + return _mapping + + +SUBJECT_FIELDS_UI = [ + { + "section": _("Subjects"), + "fields": [ + dict( + field="subjects", + ui_widget="SubjectAutocompleteDropdown", + isGenericVocabulary=False, + props=dict( + label="Keywords and subjects", + icon="tag", + description="The subjects related to the community", + placeholder="Search for a subject by name e.g. Psychology ...", + autocompleteFrom="api/subjects", + noQueryMessage="Search for subjects...", + autocompleteFromAcceptHeader="application/vnd.inveniordm.v1+json", + required=False, + multiple=True, + clearable=True, + allowAdditions=False, + ), + ) + ], + } +] + + +SUBJECT_FIELDS = { + SubjectCF( + name="subjects", + multiple=True, + dump_options=False, + ) +} diff --git a/invenio_vocabularies/services/custom_fields/vocabulary.py b/invenio_vocabularies/services/custom_fields/vocabulary.py index f5a5e7ac..0452d1e5 100644 --- a/invenio_vocabularies/services/custom_fields/vocabulary.py +++ b/invenio_vocabularies/services/custom_fields/vocabulary.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2022 CERN. +# Copyright (C) 2022-2024 CERN. # # Invenio-RDM-Records is free software; you can redistribute it and/or modify # it under the terms of the MIT License; see LICENSE file for more details. @@ -12,6 +12,7 @@ from marshmallow import fields from ...proxies import current_service +from ...records.api import Vocabulary from ...resources.serializer import VocabularyL10NItemSchema from ...services.schema import VocabularyRelationSchema @@ -49,6 +50,7 @@ def __init__( self.sort_by = sort_by self.schema = schema self.ui_schema = ui_schema + self.pid_field = Vocabulary.pid.with_type_ctx(self.vocabulary_id) @property def mapping(self): @@ -58,7 +60,7 @@ def mapping(self): "properties": { "@v": {"type": "keyword"}, "id": {"type": "keyword"}, - "title": {"type": "object", "dynamic": True}, + "title": {"type": "object", "dynamic": "true"}, }, } diff --git a/invenio_vocabularies/views.py b/invenio_vocabularies/views.py index be150406..fcc20b78 100644 --- a/invenio_vocabularies/views.py +++ b/invenio_vocabularies/views.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # # Copyright (C) 2020-2022 CERN. +# Copyright (C) 2023 Graz University of Technology. # # 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 @@ -13,30 +14,6 @@ blueprint = Blueprint("invenio_vocabularies_ext", __name__) -@blueprint.record_once -def init(state): - """Init app.""" - app = state.app - # Register services - cannot be done in extension because - # Invenio-Records-Resources might not have been initialized. - sregistry = app.extensions["invenio-records-resources"].registry - ext = app.extensions["invenio-vocabularies"] - sregistry.register(ext.affiliations_service, service_id="affiliations") - sregistry.register(ext.awards_service, service_id="awards") - 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") - # Register indexers - iregistry = app.extensions["invenio-indexer"].registry - iregistry.register(ext.affiliations_service.indexer, indexer_id="affiliations") - iregistry.register(ext.awards_service.indexer, indexer_id="awards") - 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") - - def create_blueprint_from_app(app): """Create app blueprint.""" return app.extensions["invenio-vocabularies"].resource.as_blueprint() diff --git a/invenio_vocabularies/webpack.py b/invenio_vocabularies/webpack.py index f0281ae0..3e1cae32 100644 --- a/invenio_vocabularies/webpack.py +++ b/invenio_vocabularies/webpack.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2019-2022 CERN. +# Copyright (C) 2019-2024 CERN. # Copyright (C) 2019-2022 Northwestern University. # Copyright (C) 2022 TU Wien. # Copyright (C) 2022 Graz University of Technology. @@ -34,7 +34,7 @@ "react-dnd-html5-backend": "^11.1.0", "react-dropzone": "^11.0.0", "react-i18next": "^11.11.0", - "react-invenio-forms": "^2.0.0", + "react-invenio-forms": "^3.0.0", "react-searchkit": "^2.0.0", "yup": "^0.32.0", }, diff --git a/setup.cfg b/setup.cfg index ce6dcd84..d9196202 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2020-2022 CERN. -# Copyright (C) 2022 Graz University of Technology. +# Copyright (C) 2020-2024 CERN. +# Copyright (C) 2022-2024 Graz University of Technology. # # 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 @@ -28,14 +28,14 @@ python_requires = >=3.7 zip_safe = False install_requires = invenio-i18n>=2.0.0,<3.0.0 - invenio-records-resources>=4.0.0,<5.0.0 + invenio-records-resources>=5.0.0,<6.0.0 lxml>=4.5.0 PyYAML>=5.4.1 [options.extras_require] tests = - pytest-black>=0.3.0 - invenio-app>=1.3.3,<2.0.0 + pytest-black-ng>=0.4.0 + invenio-app>=1.4.0,<2.0.0 invenio-db[postgresql,mysql]>=1.0.14,<2.0.0 pytest-invenio>=2.1.0,<3.0.0 Sphinx>=4.5 @@ -67,6 +67,10 @@ 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_base.api_finalize_app = + invenio_vocabularies = invenio_vocabularies.ext:api_finalize_app +invenio_base.finalize_app = + invenio_vocabularies = invenio_vocabularies.ext:finalize_app invenio_db.alembic = invenio_vocabularies = invenio_vocabularies:alembic invenio_db.models = @@ -95,6 +99,7 @@ invenio_assets.webpack = invenio_i18n.translations = invenio_vocabularies = invenio_vocabularies + [build_sphinx] source-dir = docs/ build-dir = docs/_build diff --git a/tests/conftest.py b/tests/conftest.py index 617f2c60..7ad2e3ab 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -77,12 +77,12 @@ def app_config(app_config): app_config["JSONSCHEMAS_HOST"] = "localhost" app_config["BABEL_DEFAULT_LOCALE"] = "en" app_config["I18N_LANGUAGES"] = [("da", "Danish")] - app_config[ - "RECORDS_REFRESOLVER_CLS" - ] = "invenio_records.resolver.InvenioRefResolver" - app_config[ - "RECORDS_REFRESOLVER_STORE" - ] = "invenio_jsonschemas.proxies.current_refresolver_store" + app_config["RECORDS_REFRESOLVER_CLS"] = ( + "invenio_records.resolver.InvenioRefResolver" + ) + app_config["RECORDS_REFRESOLVER_STORE"] = ( + "invenio_jsonschemas.proxies.current_refresolver_store" + ) return app_config diff --git a/tests/custom_fields/test_custom_fields.py b/tests/custom_fields/test_custom_fields.py index 2d2b06b0..36168b67 100644 --- a/tests/custom_fields/test_custom_fields.py +++ b/tests/custom_fields/test_custom_fields.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # # This file is part of Invenio. -# Copyright (C) 2022 CERN. +# Copyright (C) 2022-2024 CERN. # # Invenio is free software; you can redistribute it and/or modify it # under the terms of the MIT License; see LICENSE file for more details. @@ -44,7 +44,7 @@ def test_cf_mapping(vocabulary_cf): "properties": { "@v": {"type": "keyword"}, "id": {"type": "keyword"}, - "title": {"type": "object", "dynamic": True}, + "title": {"type": "object", "dynamic": "true"}, }, } diff --git a/tests/mock_module/mappings/v6/records/record-v1.0.0.json b/tests/mock_module/mappings/v6/records/record-v1.0.0.json index e4111774..939689d8 100644 --- a/tests/mock_module/mappings/v6/records/record-v1.0.0.json +++ b/tests/mock_module/mappings/v6/records/record-v1.0.0.json @@ -38,7 +38,7 @@ }, "title": { "type": "object", - "dynamic": true + "dynamic": "true" } } } diff --git a/tests/mock_module/mappings/v7/records/record-v1.0.0.json b/tests/mock_module/mappings/v7/records/record-v1.0.0.json index 4e5203ae..5e1e8396 100644 --- a/tests/mock_module/mappings/v7/records/record-v1.0.0.json +++ b/tests/mock_module/mappings/v7/records/record-v1.0.0.json @@ -37,7 +37,7 @@ }, "title": { "type": "object", - "dynamic": true + "dynamic": "true" } } } diff --git a/tests/resources/test_resources_l10n.py b/tests/resources/test_resources_l10n.py index 50f2ac09..b55d8bde 100644 --- a/tests/resources/test_resources_l10n.py +++ b/tests/resources/test_resources_l10n.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2020-2021 CERN. +# Copyright (C) 2020-2024 CERN. # Copyright (C) 2023 Graz University of Technology. # # Invenio-Vocabularies is free software; you can redistribute it and/or @@ -122,8 +122,20 @@ def test_get(client, example_record, h, prefix, expected_da, expected_en): def test_search(client, example_record, h, prefix, expected_da, expected_en): """Test search result serialization.""" - expected_en = {"hits": {"hits": [expected_en], "total": 1}} - expected_da = {"hits": {"hits": [expected_da], "total": 1}} + expected_en = { + "hits": {"hits": [expected_en], "total": 1}, + "links": { + "self": "https://127.0.0.1:5000/api/vocabularies/resourcetypes2?page=1&size=25&sort=title" + }, + "sortBy": "title", + } + expected_da = { + "hits": {"hits": [expected_da], "total": 1}, + "links": { + "self": "https://127.0.0.1:5000/api/vocabularies/resourcetypes2?page=1&size=25&sort=title" + }, + "sortBy": "title", + } # Default locale res = client.get(f"{prefix}", headers=h)