From a47908f5fd034492304a77036adbf0edbfca5eeb Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 3 May 2021 14:59:47 +0200 Subject: [PATCH 001/217] modification: moving ui records moving records specific ui files into subfolder records --- invenio_records_marc21/ui/__init__.py | 40 ++------------- invenio_records_marc21/ui/records/__init__.py | 51 +++++++++++++++++++ .../ui/{ => records}/decorators.py | 2 +- .../ui/{ => records}/errors.py | 0 .../ui/{ => records}/filters.py | 0 .../ui/{ => records}/records.py | 11 +--- 6 files changed, 57 insertions(+), 47 deletions(-) create mode 100644 invenio_records_marc21/ui/records/__init__.py rename invenio_records_marc21/ui/{ => records}/decorators.py (98%) rename invenio_records_marc21/ui/{ => records}/errors.py (100%) rename invenio_records_marc21/ui/{ => records}/filters.py (100%) rename invenio_records_marc21/ui/{ => records}/records.py (87%) diff --git a/invenio_records_marc21/ui/__init__.py b/invenio_records_marc21/ui/__init__.py index 6b2d2b39..8dc633d7 100644 --- a/invenio_records_marc21/ui/__init__.py +++ b/invenio_records_marc21/ui/__init__.py @@ -8,16 +8,9 @@ """Records user interface.""" from flask import Blueprint -from invenio_pidstore.errors import PIDDeletedError, PIDDoesNotExistError -from invenio_records_resources.services.errors import PermissionDeniedError -from .errors import ( - not_found_error, - record_permission_denied_error, - record_tombstone_error, -) -from .filters import pid_url, sanitize_title -from .records import marc21_index, record_detail, record_export +from .records import init_records_views +from .theme import init_theme_views # @@ -25,7 +18,6 @@ # def create_blueprint(app): """Register blueprint routes on app.""" - routes = app.config.get("INVENIO_MARC21_UI_ENDPOINTS") blueprint = Blueprint( "invenio_records_marc21", @@ -33,31 +25,7 @@ def create_blueprint(app): template_folder="../templates", ) - # Record URL rules - blueprint.add_url_rule( - routes["index"], - view_func=marc21_index, - ) - - # Record URL rules - blueprint.add_url_rule( - routes["record_detail"], - view_func=record_detail, - ) - - blueprint.add_url_rule( - routes["record_export"], - view_func=record_export, - ) - - # Register error handlers - blueprint.register_error_handler(PIDDeletedError, record_tombstone_error) - blueprint.register_error_handler(PIDDoesNotExistError, not_found_error) - blueprint.register_error_handler(KeyError, not_found_error) - blueprint.register_error_handler( - PermissionDeniedError, record_permission_denied_error - ) + blueprint = init_records_views(blueprint, app) + blueprint = init_theme_views(blueprint, app) - blueprint.add_app_template_filter(pid_url) - blueprint.add_app_template_filter(sanitize_title) return blueprint diff --git a/invenio_records_marc21/ui/records/__init__.py b/invenio_records_marc21/ui/records/__init__.py new file mode 100644 index 00000000..6ecddf84 --- /dev/null +++ b/invenio_records_marc21/ui/records/__init__.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it +# under the terms of the MIT License; see LICENSE file for more details. + +"""Records user interface.""" + +from invenio_pidstore.errors import PIDDeletedError, PIDDoesNotExistError +from invenio_records_resources.services.errors import PermissionDeniedError + +from .errors import ( + not_found_error, + record_permission_denied_error, + record_tombstone_error, +) +from .filters import pid_url, sanitize_title +from .records import record_detail, record_export + + +# +# Registration +# +def init_records_views(blueprint, app): + """Register blueprint routes on app.""" + + routes = app.config.get("INVENIO_MARC21_UI_ENDPOINTS") + + # Record URL rules + blueprint.add_url_rule( + routes["record_detail"], + view_func=record_detail, + ) + + blueprint.add_url_rule( + routes["record_export"], + view_func=record_export, + ) + + # Register error handlers + blueprint.register_error_handler(PIDDeletedError, record_tombstone_error) + blueprint.register_error_handler(PIDDoesNotExistError, not_found_error) + blueprint.register_error_handler(KeyError, not_found_error) + blueprint.register_error_handler( + PermissionDeniedError, record_permission_denied_error + ) + + blueprint.add_app_template_filter(pid_url) + blueprint.add_app_template_filter(sanitize_title) + return blueprint diff --git a/invenio_records_marc21/ui/decorators.py b/invenio_records_marc21/ui/records/decorators.py similarity index 98% rename from invenio_records_marc21/ui/decorators.py rename to invenio_records_marc21/ui/records/decorators.py index b5f848eb..94b5130c 100644 --- a/invenio_records_marc21/ui/decorators.py +++ b/invenio_records_marc21/ui/records/decorators.py @@ -15,7 +15,7 @@ from flask import g from invenio_records_resources.services.errors import PermissionDeniedError -from ..proxies import current_records_marc21 +from ...proxies import current_records_marc21 def links_config(): diff --git a/invenio_records_marc21/ui/errors.py b/invenio_records_marc21/ui/records/errors.py similarity index 100% rename from invenio_records_marc21/ui/errors.py rename to invenio_records_marc21/ui/records/errors.py diff --git a/invenio_records_marc21/ui/filters.py b/invenio_records_marc21/ui/records/filters.py similarity index 100% rename from invenio_records_marc21/ui/filters.py rename to invenio_records_marc21/ui/records/filters.py diff --git a/invenio_records_marc21/ui/records.py b/invenio_records_marc21/ui/records/records.py similarity index 87% rename from invenio_records_marc21/ui/records.py rename to invenio_records_marc21/ui/records/records.py index 4e5d7c38..785cda7b 100644 --- a/invenio_records_marc21/ui/records.py +++ b/invenio_records_marc21/ui/records/records.py @@ -11,7 +11,7 @@ from flask import abort, current_app, render_template from invenio_base.utils import obj_or_import_string -from ..resources.serializers.ui import UIJSONSerializer +from ...resources.serializers.ui import UIJSONSerializer from .decorators import pass_record, user_permissions @@ -32,15 +32,6 @@ def record_detail(record=None, files=None, pid_value=None, permissions=None): ) -def marc21_index(permissions=None): - """Marc21 index page.""" - return render_template( - "invenio_records_marc21/index.html", - permissions=permissions, - module_name=__name__, - ) - - @pass_record def record_export(record=None, export_format=None, pid_value=None, permissions=None): """Export marc21 record page view.""" From 1c0ae4e3f1513a71d344d5f95d2129edac1707ba Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Thu, 6 May 2021 14:24:17 +0200 Subject: [PATCH 002/217] bugfix: show link shema in the record renaming links_config marc21 into record to add the url to record export and using the ItemLinksSchema class --- invenio_records_marc21/resources/config.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/invenio_records_marc21/resources/config.py b/invenio_records_marc21/resources/config.py index 27ddeb51..72e177d2 100755 --- a/invenio_records_marc21/resources/config.py +++ b/invenio_records_marc21/resources/config.py @@ -13,7 +13,7 @@ from invenio_records_resources.resources import RecordResponse from invenio_records_resources.resources.records.schemas_links import ( ItemLink, - LinksSchema, + ItemLinksSchema, SearchLinksSchema, ) @@ -22,7 +22,7 @@ # # Links # -RecordLinks = LinksSchema.create( +RecordLinks = ItemLinksSchema.create( links={ "self": ItemLink(template="/api/marc21/{pid_value}"), "self_html": ItemLink(template="/marc21/{pid_value}"), @@ -30,7 +30,7 @@ ) -DraftLinks = LinksSchema.create( +DraftLinks = ItemLinksSchema.create( links={ "self": ItemLink(template="/api/marc21/{pid_value}/draft"), "self_html": ItemLink(template="/marc21/uploads/{pid_value}"), @@ -64,12 +64,12 @@ class Marc21RecordResourceConfig(RecordResourceConfig): item_route = "/marc21/" links_config = { - "marc21": RecordLinks, + "record": RecordLinks, "search": SearchLinks, } draft_links_config = { - "marc21": DraftLinks, + "record": DraftLinks, } response_handlers = record_serializers @@ -83,6 +83,9 @@ class Marc21DraftResourceConfig(DraftResourceConfig): list_route = "/marc21//draft" - item_route = "/marc21/" # None + item_route = "/marc21/" - links_config = {"marc21": DraftLinks} + links_config = { + "record": DraftLinks, + "search": SearchLinks, + } From 772069956bcbcbb833beea85269f8b28661acd70 Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Fri, 7 May 2021 07:59:28 +0200 Subject: [PATCH 003/217] modification: adding json export adding json dict to ui metadataschema for the export of records --- invenio_records_marc21/resources/serializers/ui/schema.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) mode change 100644 => 100755 invenio_records_marc21/resources/serializers/ui/schema.py diff --git a/invenio_records_marc21/resources/serializers/ui/schema.py b/invenio_records_marc21/resources/serializers/ui/schema.py old mode 100644 new mode 100755 index 908e053c..64303480 --- a/invenio_records_marc21/resources/serializers/ui/schema.py +++ b/invenio_records_marc21/resources/serializers/ui/schema.py @@ -35,7 +35,7 @@ class MetadataSchema(Schema): """Access right vocabulary.""" xml = Str(required=False) - # json = Dict(required=False) + json = Dict(required=True) @pre_dump def convert_xml(self, data, **kwargs): From 082d15a9c672020b07696d0566be51d5f5a03033 Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Fri, 7 May 2021 08:21:05 +0200 Subject: [PATCH 004/217] feature: add search engine adding react index page with search bar and created a search webpage with react --- .../invenio_records_marc21/index.html | 22 +- .../invenio_records_marc21/search.html | 76 +++++ invenio_records_marc21/ui/theme/__init__.py | 27 ++ .../search/components.js | 283 ++++++++++++++++++ .../js/invenio_records_marc21/search/index.js | 32 ++ invenio_records_marc21/ui/theme/views.py | 21 ++ invenio_records_marc21/ui/theme/webpack.py | 42 +++ setup.py | 3 + 8 files changed, 501 insertions(+), 5 deletions(-) create mode 100755 invenio_records_marc21/templates/invenio_records_marc21/search.html create mode 100755 invenio_records_marc21/ui/theme/__init__.py create mode 100755 invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/components.js create mode 100755 invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/index.js create mode 100755 invenio_records_marc21/ui/theme/views.py create mode 100755 invenio_records_marc21/ui/theme/webpack.py mode change 100644 => 100755 setup.py diff --git a/invenio_records_marc21/templates/invenio_records_marc21/index.html b/invenio_records_marc21/templates/invenio_records_marc21/index.html index 9ed79c6a..94cf49d4 100755 --- a/invenio_records_marc21/templates/invenio_records_marc21/index.html +++ b/invenio_records_marc21/templates/invenio_records_marc21/index.html @@ -9,14 +9,26 @@ {%- extends config.INVENIO_MARC21_BASE_TEMPLATE %} {%- block page_body %} +
-
-
-
+ {%- block frontpage_search %} -

{{_('Welcome to %(module_name)s', module_name=module_name)}}

+ {%- block frontpage_form %} +
+
+
-
+ + {%- endblock frontpage_form %} + {%- endblock frontpage_search %} + {%- endblock %} diff --git a/invenio_records_marc21/templates/invenio_records_marc21/search.html b/invenio_records_marc21/templates/invenio_records_marc21/search.html new file mode 100755 index 00000000..af8a97a4 --- /dev/null +++ b/invenio_records_marc21/templates/invenio_records_marc21/search.html @@ -0,0 +1,76 @@ +{# + Copyright (C) 2021 Graz University of Technology + + invenio-records-marc21 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 config.INVENIO_MARC21_BASE_TEMPLATE %} + +{%- block javascript %} + {{ super() }} + {{ webpack['invenio-records-marc21-search.js'] }} +{%- endblock %} + +{%- block page_body %} + +
+ +{%- endblock page_body %} diff --git a/invenio_records_marc21/ui/theme/__init__.py b/invenio_records_marc21/ui/theme/__init__.py new file mode 100755 index 00000000..79474cf8 --- /dev/null +++ b/invenio_records_marc21/ui/theme/__init__.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it +# under the terms of the MIT License; see LICENSE file for more details. + + +"""Marc21 Theme Package.""" + +from flask_babelex import lazy_gettext as _ +from flask_menu import current_menu + +from .views import index, search + + +# +# Registration +# +def init_theme_views(blueprint, app): + """Blueprint for the routes and resources provided by Invenio-Records-Marc21.""" + routes = app.config.get("INVENIO_MARC21_UI_ENDPOINTS") + + blueprint.add_url_rule(routes["index"], view_func=index) + blueprint.add_url_rule(routes["record_search"], view_func=search) + + return blueprint diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/components.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/components.js new file mode 100755 index 00000000..89e5af46 --- /dev/null +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/components.js @@ -0,0 +1,283 @@ +// This file is part of Invenio-Records-Marc21. +// Copyright (C) 2021 Graz University of Technology. +// +// Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it +// under the terms of the MIT License; see LICENSE file for more details. + +import React, { useState } from "react"; +import { + Button, + Card, + Checkbox, + Grid, + Icon, + Input, + Item, + Label, + List, +} from "semantic-ui-react"; +import { BucketAggregation, Toggle } from "react-searchkit"; +import _find from "lodash/find"; +import _get from "lodash/get"; +import _truncate from "lodash/truncate"; +import Overridable from "react-overridable"; +import { SearchBar } from "@js/invenio_search_ui/components"; + +export const Marc21RecordResultsListItem = ({ result, index }) => { + + const createdDate = _get( + result, + "ui.created", + "No creation date found." + ); + + const publicationDate = _get( + result, + "ui.updated", + "No update date found." + ); + const metadata = _get(result, ["ui", "metadata", "json"], []); + const description = _get(metadata, ["summary", "0", "summary"], "No description"); + const subjects = _get(metadata, "subject_added_entry_topical_term", []); + + const publication = _get(metadata, ["production_publication_distribution_manufacture_and_copyright_notice", "0"], []); + const creators = _get(publication, "name_of_producer_publisher_distributor_manufacturer", []); + + const title = _get(metadata, ["title_statement", "title"], "No title"); + const version = _get(result, "revision_id", null); + + const viewLink = `/marc21/${result.id}`; + + return ( + + + +
+ + +
+
+ {title} + + {creators.map((creator, index) => ( + + {creator} + {index < creators.length - 1 && ","} + + ))} + + + {_truncate(description, { length: 350 })} + + + {subjects.map((subject, index) => ( + + ))} + {createdDate && ( +
+ + Uploaded on {createdDate} + +
+ )} +
+
+
+ ); +}; + +export const Marc21RecordResultsGridItem = ({ result, index }) => { + const metadata = _get(result, ["ui", "metadata", "json"], []); + const description = _get(metadata, ["summary", "0", "summary"], "No description"); + return ( + + + {result.metadata.json.title_statement.title} + + {_truncate(description, { length: 200 })} + + + + ); +}; + +export const Marc21RecordSearchBarContainer = () => { + return ( + + + + ); +}; + +export const Marc21RecordSearchBarElement = ({ + placeholder: passedPlaceholder, + queryString, + onInputChange, + executeSearch, +}) => { + const placeholder = passedPlaceholder || "Search"; + const onBtnSearchClick = () => { + executeSearch(); + }; + const onKeyPress = (event) => { + if (event.key === "Enter") { + executeSearch(); + } + }; + return ( + { + onInputChange(value); + }} + value={queryString} + onKeyPress={onKeyPress} + /> + ); +}; + +export const Marc21RecordFacetsValues = ({ + bucket, + isSelected, + onFilterClicked, + getChildAggCmps, +}) => { + const childAggCmps = getChildAggCmps(bucket); + const [isActive, setisActive] = useState(false); + const hasChildren = childAggCmps && childAggCmps.props.buckets.length > 0; + const keyField = bucket.key_as_string ? bucket.key_as_string : bucket.key; + return ( + +
+ + + + {hasChildren ? ( + setisActive(!isActive)} + > + ) : null} + onFilterClicked(keyField)} + checked={isSelected} + /> +
+
+ {childAggCmps} +
+
+ ); +}; + +export const Marc21RecordFacets = ({ aggs, currentResultsState }) => { + return ( + <> + {aggs.map((agg) => { + return ( +
+ +
+ ); + })} + + ); +}; + +export const Marc21BucketAggregationElement = ({ title, containerCmp }) => { + return ( + + + {title} + + {containerCmp} + + ); +}; + +export const Marc21ToggleComponent = ({ + updateQueryFilters, + userSelectionFilters, + filterValue, + label, + title, + isChecked, +}) => { + const _isChecked = (userSelectionFilters) => { + const isFilterActive = + userSelectionFilters.filter((filter) => filter[0] === filterValue[0]) + .length > 0; + return isFilterActive; + }; + + const onToggleClicked = () => { + updateQueryFilters(filterValue); + }; + + var isChecked = _isChecked(userSelectionFilters); + return ( + + + {title} + + + + + + ); +}; + +export const Marc21CountComponent = ({ totalResults }) => { + return ; +}; + + +export function createMarc21SearchAppInit( + defaultComponents, + autoInit = true, + autoInitDataAttr = "invenio-records-marc21-search-config" +) { + const initSearchApp = (rootElement) => { + const config = JSON.parse( + rootElement.dataset[_camelCase(autoInitDataAttr)] + ); + loadComponents(config.appId, defaultComponents).then((res) => { + ReactDOM.render(, rootElement); + }); + }; + + if (autoInit) { + const searchAppElements = document.querySelectorAll( + `[data-${autoInitDataAttr}]` + ); + for (const appRootElement of searchAppElements) { + initSearchApp(appRootElement); + } + } else { + return initSearchApp; + } +} \ No newline at end of file diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/index.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/index.js new file mode 100755 index 00000000..34126f9d --- /dev/null +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/index.js @@ -0,0 +1,32 @@ +// This file is part of Invenio-Records-Marc21. +// Copyright (C) 2021 Graz University of Technology. +// +// Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it +// under the terms of the MIT License; see LICENSE file for more details. + + +import { createSearchAppInit } from "@js/invenio_search_ui"; +import { + Marc21BucketAggregationElement, + Marc21RecordFacets, + Marc21RecordFacetsValues, + Marc21RecordResultsGridItem, + Marc21RecordResultsListItem, + Marc21RecordSearchBarContainer, + Marc21RecordSearchBarElement, + Marc21ToggleComponent, + Marc21CountComponent, +} from "./components"; + + +const initSearchApp = createSearchAppInit({ + "BucketAggregation.element": Marc21BucketAggregationElement, + "BucketAggregationValues.element": Marc21RecordFacetsValues, + "ResultsGrid.item": Marc21RecordResultsGridItem, + "ResultsList.item": Marc21RecordResultsListItem, + "SearchApp.facets": Marc21RecordFacets, + "SearchApp.searchbarContainer": Marc21RecordSearchBarContainer, + "SearchBar.element": Marc21RecordSearchBarElement, + "SearchFilters.ToggleComponent": Marc21ToggleComponent, + "Count.element": Marc21CountComponent, +}); diff --git a/invenio_records_marc21/ui/theme/views.py b/invenio_records_marc21/ui/theme/views.py new file mode 100755 index 00000000..ea1cad45 --- /dev/null +++ b/invenio_records_marc21/ui/theme/views.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it +# under the terms of the MIT License; see LICENSE file for more details. + + +"""Routes for general pages provided by Invenio-Records-Marc21.""" + +from flask import render_template + + +def index(): + """Frontpage.""" + return render_template("invenio_records_marc21/index.html") + + +def search(): + """Search help guide.""" + return render_template("invenio_records_marc21/search.html") diff --git a/invenio_records_marc21/ui/theme/webpack.py b/invenio_records_marc21/ui/theme/webpack.py new file mode 100755 index 00000000..9306e7a7 --- /dev/null +++ b/invenio_records_marc21/ui/theme/webpack.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it +# under the terms of the MIT License; see LICENSE file for more details. + + +"""JS/CSS Webpack bundles for theme.""" + +from invenio_assets.webpack import WebpackThemeBundle + +theme = WebpackThemeBundle( + __name__, + "assets", + default="semantic-ui", + themes={ + "semantic-ui": dict( + entry={ + "invenio-records-marc21-search": "./js/invenio_records_marc21/search/index.js", + }, + dependencies={ + "@babel/runtime": "^7.9.0", + "formik": "^2.1.4", + "luxon": "^1.23.0", + "path": "^0.12.7", + "prop-types": "^15.7.2", + "react-dnd": "^11.1.3", + "react-dnd-html5-backend": "^11.1.3", + "react-invenio-forms": "^0.6.3", + "react-dropzone": "^11.0.3", + "yup": "^0.27.0", + "@ckeditor/ckeditor5-build-classic": "^16.0.0", + "@ckeditor/ckeditor5-react": "^2.1.0", + }, + aliases={ + "themes/marc21": "less/invenio_records_marc21/theme", + "@less/invenio_records_marc21": "less/invenio_records_marc21", + }, + ), + }, +) diff --git a/setup.py b/setup.py old mode 100644 new mode 100755 index 9899dc26..6be7c52d --- a/setup.py +++ b/setup.py @@ -107,6 +107,9 @@ "invenio_search.mappings": [ "marc21records = invenio_records_marc21.records.mappings", ], + "invenio_assets.webpack": [ + "invenio_records_marc21_theme = invenio_records_marc21.ui.theme.webpack:theme", + ], # TODO: Edit these entry points to fit your needs. # 'invenio_access.actions': [], # 'invenio_admin.actions': [], From 3c11a74185096ccb3160c8d7b7738b474e61c63b Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Fri, 7 May 2021 08:21:29 +0200 Subject: [PATCH 005/217] modification: codestyle --- invenio_records_marc21/ui/__init__.py | 1 - invenio_records_marc21/ui/records/__init__.py | 1 - 2 files changed, 2 deletions(-) diff --git a/invenio_records_marc21/ui/__init__.py b/invenio_records_marc21/ui/__init__.py index 8dc633d7..7cd9d20c 100644 --- a/invenio_records_marc21/ui/__init__.py +++ b/invenio_records_marc21/ui/__init__.py @@ -18,7 +18,6 @@ # def create_blueprint(app): """Register blueprint routes on app.""" - blueprint = Blueprint( "invenio_records_marc21", __name__, diff --git a/invenio_records_marc21/ui/records/__init__.py b/invenio_records_marc21/ui/records/__init__.py index 6ecddf84..b44d3a99 100644 --- a/invenio_records_marc21/ui/records/__init__.py +++ b/invenio_records_marc21/ui/records/__init__.py @@ -24,7 +24,6 @@ # def init_records_views(blueprint, app): """Register blueprint routes on app.""" - routes = app.config.get("INVENIO_MARC21_UI_ENDPOINTS") # Record URL rules From 0338b6ca69c88420443cb71d1aff232a899f99e0 Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Fri, 7 May 2021 08:22:01 +0200 Subject: [PATCH 006/217] modification adding js file to manifest --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index 567315ec..d4f126cb 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -38,5 +38,6 @@ recursive-include invenio_records_marc21 *.json recursive-include invenio_records_marc21 *.xml recursive-include invenio_records_marc21 *.py recursive-include invenio_records_marc21 *.csv +recursive-include invenio_records_marc21 *.js recursive-include tests *.py recursive-include tests *.json From 75ce30189de19409b9e7c057c40fed06b450e282 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 17 May 2021 13:27:01 +0200 Subject: [PATCH 007/217] modification: jsonschemas update adding jsonschema for parent marc21 record and moving common defintions of record and parent into definitions schema. --- .../marc21/definitions-v1.0.0.json | 129 ++++++++++++ .../jsonschemas/marc21/marc21-v1.0.0.json | 186 +++++++----------- .../jsonschemas/marc21/parent-v1.0.0.json | 81 ++++++++ 3 files changed, 284 insertions(+), 112 deletions(-) create mode 100644 invenio_records_marc21/records/jsonschemas/marc21/definitions-v1.0.0.json create mode 100644 invenio_records_marc21/records/jsonschemas/marc21/parent-v1.0.0.json diff --git a/invenio_records_marc21/records/jsonschemas/marc21/definitions-v1.0.0.json b/invenio_records_marc21/records/jsonschemas/marc21/definitions-v1.0.0.json new file mode 100644 index 00000000..7be40b27 --- /dev/null +++ b/invenio_records_marc21/records/jsonschemas/marc21/definitions-v1.0.0.json @@ -0,0 +1,129 @@ +{ + "identifier": { + "description": "An identifier.", + "type": "string" + }, + "identifiers": { + "description": "Identifiers object (keys being scheme, value being the identifier).", + "type": "object", + "additionalProperties": { + "$ref": "#/identifier" + } + }, + "internal-pid": { + "type": "object", + "description": "An internal persistent identifier object.", + "additionalProperties": false, + "required": [ + "pk", + "status" + ], + "properties": { + "pk": { + "description": "Primary key of the PID object.", + "type": "integer" + }, + "status": { + "description": "The status of the PID (from Invenio-PIDStore).", + "type": "string", + "enum": [ + "N", + "K", + "R", + "M", + "D" + ] + }, + "pid_type": { + "description": "The type of the persistent identifier.", + "type": "string" + }, + "obj_type": { + "description": "The type of the associated object.", + "type": "string" + } + } + }, + "external-pid": { + "type": "object", + "description": "An external persistent identifier object.", + "additionalProperties": false, + "required": [ + "identifier", + "provider" + ], + "properties": { + "identifier": { + "$ref": "#/identifier" + }, + "provider": { + "description": "The provider of the persistent identifier.", + "type": "string" + }, + "client": { + "description": "Client identifier for the specific PID.", + "type": "string" + } + } + }, + "user": { + "type": "object", + "description": "..", + "additionalProperties": false, + "properties": { + "user": { + "type": "integer" + } + } + }, + "agent": { + "description": "An agent (user, software process, community, ...).", + "oneOf": [ + { + "$ref": "#/user" + } + ] + }, + "file": { + "type": "object", + "additionalProperties": false, + "description": "A file object.", + "properties": { + "version_id": { + "description": "Object version ID.", + "type": "string" + }, + "bucket_id": { + "description": "Object verison bucket ID.", + "type": "string" + }, + "mimetype": { + "description": "File MIMEType.", + "type": "string" + }, + "uri": { + "description": "File URI.", + "type": "string" + }, + "storage_class": { + "description": "File storage class.", + "type": "string" + }, + "checksum": { + "description": "Checksum of the file.", + "type": "string" + }, + "size": { + "description": "Size of the file in bytes.", + "type": "number" + }, + "key": { + "description": "Key (filename) of the file.", + "type": "string" + }, + "file_id": { + "$ref": "#/identifier" + } + } + } +} diff --git a/invenio_records_marc21/records/jsonschemas/marc21/marc21-v1.0.0.json b/invenio_records_marc21/records/jsonschemas/marc21/marc21-v1.0.0.json index 38341ba0..65217e7f 100644 --- a/invenio_records_marc21/records/jsonschemas/marc21/marc21-v1.0.0.json +++ b/invenio_records_marc21/records/jsonschemas/marc21/marc21-v1.0.0.json @@ -1,84 +1,9 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "id": "http://localhost/schemas/marc21/marc21-v1.0.0.json", + "id": "local://marc21/marc21-v1.0.0.json", "additionalProperties": false, "title": "Invenio Records Marc21 v1.0.0", "type": "object", - - "definitions": { - - "identifier": { - "description": "An identifier.", - "type": "string" - }, - - "identifiers": { - "description": "Identifiers object (keys being scheme, value being the identifier).", - "type": "object", - "additionalProperties": {"$ref": "#/definitions/identifier"} - }, - - "internal-pid": { - "type": "object", - "description": "An internal persistent identifier object.", - "additionalProperties": false, - "required": ["pk", "status"], - "properties": { - "pk": { - "description": "Primary key of the PID object.", - "type": "integer" - }, - "status": { - "description": "The status of the PID (from Invenio-PIDStore).", - "type": "string", - "enum": [ - "N", - "K", - "R", - "M", - "D" - ] - } - } - }, - - "external-pid": { - "type": "object", - "description": "An external persistent identifier object.", - "additionalProperties": false, - "required": ["identifier", "provider"], - "properties": { - "identifier": {"$ref": "#/definitions/identifier"}, - "provider": { - "description": "The provider of the persistent identifier.", - "type": "string" - }, - "client": { - "description": "Client identifier for the specific PID.", - "type": "string" - } - } - }, - - "user": { - "type": "object", - "description": "..", - "additionalProperties": false, - "properties": { - "user": { - "type": "integer" - } - } - }, - - "agent": { - "description": "An agent (user, software process, community, ...).", - "oneOf": [ - {"$ref": "#/definitions/user"} - ] - } - }, - "properties": { "$schema": { "description": "This record's jsonschema.", @@ -88,16 +13,13 @@ "description": "Invenio record identifier (integer).", "type": "string" }, - "pid": {"$ref": "#/definitions/internal-pid"}, - - "conceptid": { - "description": "Persistent concept record identifier (alphanumeric).", - "type": "string" + "pid": { + "$ref": "local://marc21/definitions-v1.0.0.json#/internal-pid" }, - "conceptpid": {"$ref": "#/definitions/internal-pid"}, - "pids": { - "additionalProperties": {"$ref": "#/definitions/external-pid"}, + "additionalProperties": { + "$ref": "local://marc21/definitions-v1.0.0.json#/external-pid" + }, "description": "Managed persistent identifiers for a record including e.g. OAI-PMH identifier, minted DOIs and more. Managed PIDs are registered in the PIDStore" }, "metadata": { @@ -105,57 +27,97 @@ "description": "Resource metadata.", "additionalProperties": true }, - "access": { "type": "object", "description": "Record access control and ownership.", "additionalProperties": false, "properties": { - "metadata": { - "description": "Metadata visibility (true - public, false - private)", - "type": "boolean" + "description": "Metadata visibility (public or restricted)", + "type": "string", + "enum": [ + "public", + "embargoed", + "restricted" + ] }, - "files": { - "description": "Files visibility (true - public, false - private)", - "type": "boolean" + "description": "Files visibility (public or restricted)", + "type": "string", + "enum": [ + "public", + "embargoed", + "restricted" + ] }, - "owned_by": { "description": "List of user IDs that are owners of the record.", "type": "array", "minItems": 1, "uniqueItems": true, - "items": {"$ref": "#/definitions/agent"} + "items": { + "$ref": "local://marc21/definitions-v1.0.0.json#/agent" + } }, - - "embargo_date": { + "embargo": { "description": "Embargo date of record (ISO8601 formatted date time in UTC). At this time both metadata and files will be made public.", - "type": "string", - "format": "date-time" - }, - - "access_condition": { - "description": "Conditions under which access to files are granted", "type": "object", "additionalProperties": false, "properties": { - "condition": { - "type": "string", - "description": "Textual description under which conditions access is granted." + "active": { + "type": [ + "boolean", + "null" + ] + }, + "until": { + "description": "Embargo date of record (ISO8601 formatted date time in UTC). At this time both metadata and files will be made public.", + "type": [ + "string", + "null" + ] }, - "default_link_validity": { - "type": "integer", - "description": "Number of days" + "reason": { + "description": "The reason why the record is under embargo.", + "type": [ + "string", + "null" + ] } } + } + } + }, + "files": { + "type": "object", + "description": "Files associated with the record", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "description": "Set to false for metadata only records." }, - - "access_right": { - "default": "open", - "description": "TODO - Access right for record. - should be computed or moved to metadata?", - "type": "string" + "default_preview": { + "type": "string", + "description": "Key of the default previewed file." + }, + "order": { + "type": "array", + "items": { + "type": "string" + } + }, + "entries": { + "type": "object", + "additionalProperties": { + "$ref": "local://marc21/definitions-v1.0.0.json#/file" + } + }, + "meta": { + "type": "object", + "additionalProperties": { + "type": "object" + } } } } diff --git a/invenio_records_marc21/records/jsonschemas/marc21/parent-v1.0.0.json b/invenio_records_marc21/records/jsonschemas/marc21/parent-v1.0.0.json new file mode 100644 index 00000000..585facf0 --- /dev/null +++ b/invenio_records_marc21/records/jsonschemas/marc21/parent-v1.0.0.json @@ -0,0 +1,81 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "id": "local://marc21/parent-v1.0.0.json", + "title": "Invenio Parent Record Marc21 v1.0.0", + "type": "object", + "additionalProperties": false, + "properties": { + "$schema": { + "description": "JSONSchema declaration.", + "type": "string" + }, + "id": { + "description": "Persistent record identifier (alphanumeric).", + "type": "string" + }, + "pid": { + "$ref": "local://marc21/definitions-v1.0.0.json#/internal-pid" + }, + "access": { + "type": "object", + "description": "Record access control and ownership.", + "additionalProperties": false, + "properties": { + "metadata": { + "description": "Metadata visibility (public or restricted)", + "type": "string", + "enum": [ + "public", + "embargoed", + "restricted" + ] + }, + "files": { + "description": "Files visibility (public or restricted)", + "type": "string", + "enum": [ + "public", + "embargoed", + "restricted" + ] + }, + "owned_by": { + "description": "List of user IDs that are owners of the record.", + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "$ref": "local://marc21/definitions-v1.0.0.json#/agent" + } + }, + "embargo": { + "description": "Embargo date of record (ISO8601 formatted date time in UTC). At this time both metadata and files will be made public.", + "type": "object", + "additionalProperties": false, + "properties": { + "active": { + "type": [ + "boolean", + "null" + ] + }, + "until": { + "description": "Embargo date of record (ISO8601 formatted date time in UTC). At this time both metadata and files will be made public.", + "type": [ + "string", + "null" + ] + }, + "reason": { + "description": "The reason why the record is under embargo.", + "type": [ + "string", + "null" + ] + } + } + } + } + } + } +} From d8a4837ed637460c7ce4f668a15171039e2c5f0a Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 17 May 2021 13:48:15 +0200 Subject: [PATCH 008/217] modification: move record components moving records component into own module --- .../services/components/__init__.py | 19 +++++ .../services/components/access.py | 69 +++++++++++++++++++ .../services/components/metadata.py | 43 ++++++++++++ .../services/components/pid.py | 20 ++++++ 4 files changed, 151 insertions(+) create mode 100644 invenio_records_marc21/services/components/__init__.py create mode 100644 invenio_records_marc21/services/components/access.py create mode 100644 invenio_records_marc21/services/components/metadata.py create mode 100644 invenio_records_marc21/services/components/pid.py diff --git a/invenio_records_marc21/services/components/__init__.py b/invenio_records_marc21/services/components/__init__.py new file mode 100644 index 00000000..2e14e773 --- /dev/null +++ b/invenio_records_marc21/services/components/__init__.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it +# under the terms of the MIT License; see LICENSE file for more details. + +"""Marc21 record components.""" + +from .access import AccessComponent, AccessStatusEnum +from .metadata import MetadataComponent +from .pid import PIDComponent + +__all__ = ( + "AccessStatusEnum", + "AccessComponent", + "PIDComponent", + "MetadataComponent", +) diff --git a/invenio_records_marc21/services/components/access.py b/invenio_records_marc21/services/components/access.py new file mode 100644 index 00000000..1994d763 --- /dev/null +++ b/invenio_records_marc21/services/components/access.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it +# under the terms of the MIT License; see LICENSE file for more details. + +"""Marc21 AccessComponent.""" + +from enum import Enum + +from invenio_records_resources.services.records.components import ServiceComponent + + +class AccessStatusEnum(Enum): + """Enum defining access statuses.""" + + PUBLIC = "public" + + EMBARGOED = "embargoed" + + RESTRICTED = "restricted" + + # METADATA_ONLY = "metadata-only" + + @staticmethod + def list(): + return list(map(lambda c: c.value, AccessStatusEnum)) + + +class AccessComponent(ServiceComponent): + """Service component for access integration.""" + + def _default_access(self, identity, data, record, **kwargs): + """Create default access component if not pressent in the record.""" + default_access = { + "access": { + "owned_by": [{"user": identity.id}], + "metadata": AccessStatusEnum.PUBLIC.value, + "files": AccessStatusEnum.PUBLIC.value, + }, + } + if record is not None and "access" in data: + record.parent.update({"access": data.get("access")}) + record.parent.commit() + elif "access" not in data: + data.update(default_access) + record.parent.update({"access": data.get("access")}) + record.parent.commit() + + def _init_owned_by(self, identity, record, **kwargs): + """Initialize the owned by atribute in access component.""" + access_data = record.parent.get("access", {}) + if not "owned_by" in access_data and identity.id: + access_data.setdefault("owned_by", [{"user": identity.id}]) + record.parent.update(access_data) + + def create(self, identity, data=None, record=None, **kwargs): + """Add basic ownership fields to the record.""" + self._default_access(identity, data, record, **kwargs) + self._init_owned_by(identity, record, **kwargs) + + def update_draft(self, identity, data=None, record=None, **kwargs): + """Update draft handler.""" + self._default_access(identity, data, record, **kwargs) + + def update(self, identity, data=None, record=None, **kwargs): + """Update handler.""" + self._init_owned_by(identity, record, **kwargs) diff --git a/invenio_records_marc21/services/components/metadata.py b/invenio_records_marc21/services/components/metadata.py new file mode 100644 index 00000000..d9b99f5a --- /dev/null +++ b/invenio_records_marc21/services/components/metadata.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it +# under the terms of the MIT License; see LICENSE file for more details. + + +"""Marc21 record metadata component.""" + +from invenio_records_resources.services.records.components import ( + MetadataComponent as BaseMetadataComponent, +) + + +class MetadataComponent(BaseMetadataComponent): + """Service component for metadata.""" + + def create(self, identity, data=None, record=None, errors=None, **kwargs): + """Inject parsed metadata to the record.""" + record.metadata = data.get("metadata", {}) + + def update(self, identity, data=None, record=None, **kwargs): + """Inject parsed metadata to the record.""" + record.metadata = data.get("metadata", {}) + + def update_draft(self, identity, data=None, record=None, **kwargs): + """Inject parsed metadata to the record.""" + record.metadata = data.get("metadata", {}) + + def publish(self, identity, draft=None, record=None, **kwargs): + """Update draft metadata.""" + record.metadata = draft.get("metadata", {}) + + def edit(self, identity, draft=None, record=None, **kwargs): + """Update draft metadata.""" + draft.metadata = record.get("metadata", {}) + + def new_version(self, identity, draft=None, record=None, **kwargs): + """Update draft metadata.""" + draft.metadata = record.get("metadata", {}) + for f in self.new_version_skip_fields: + draft.metadata.pop(f, None) diff --git a/invenio_records_marc21/services/components/pid.py b/invenio_records_marc21/services/components/pid.py new file mode 100644 index 00000000..5e52418f --- /dev/null +++ b/invenio_records_marc21/services/components/pid.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it +# under the terms of the MIT License; see LICENSE file for more details. + +"""Marc21 external pid component.""" + +from invenio_drafts_resources.services.records.components import ( + PIDComponent as BasePIDComponent, +) + + +class PIDComponent(BasePIDComponent): + """Service component for pids integration.""" + + def create(self, identity, data=None, record=None, **kwargs): + """Create PID when record is created..""" + self.service.record_cls.pid.create(record) From 5216fc31245bc4812745347029dd712ffcaaec19 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 17 May 2021 13:50:30 +0200 Subject: [PATCH 009/217] modification: introducing parent record schema --- .../services/schemas/parent/__init__.py | 22 +++++++++++++++ .../services/schemas/parent/access.py | 27 +++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 invenio_records_marc21/services/schemas/parent/__init__.py create mode 100644 invenio_records_marc21/services/schemas/parent/access.py diff --git a/invenio_records_marc21/services/schemas/parent/__init__.py b/invenio_records_marc21/services/schemas/parent/__init__.py new file mode 100644 index 00000000..43920e15 --- /dev/null +++ b/invenio_records_marc21/services/schemas/parent/__init__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it +# under the terms of the MIT License; see LICENSE file for more details. + +"""Marc21 parent record schemas.""" + +from invenio_drafts_resources.services.records.schema import ParentSchema +from marshmallow import fields + +from .access import ParentAccessSchema + + +class Marc21ParentSchema(ParentSchema): + """Record schema.""" + + access = fields.Nested(ParentAccessSchema) + + +__all__ = ("Marc21ParentSchema",) diff --git a/invenio_records_marc21/services/schemas/parent/access.py b/invenio_records_marc21/services/schemas/parent/access.py new file mode 100644 index 00000000..1b6728a9 --- /dev/null +++ b/invenio_records_marc21/services/schemas/parent/access.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it +# under the terms of the MIT License; see LICENSE file for more details. + +"""Marc21 parent record access schemas.""" + +from flask_babelex import lazy_gettext as _ +from marshmallow import Schema, fields +from marshmallow_utils.fields import SanitizedUnicode + + +class Agent(Schema): + """An agent schema.""" + + user = fields.Integer(required=True) + + +class ParentAccessSchema(Schema): + """Access schema.""" + + metadata = SanitizedUnicode(required=True) + files = SanitizedUnicode(required=True) + status = SanitizedUnicode(dump_only=False) + owned_by = fields.List(fields.Nested(Agent)) From f445be1dceed1099e20ecbfd78880263387fba4b Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 17 May 2021 14:15:09 +0200 Subject: [PATCH 010/217] modification: update db models adding parent model and updating draft and records models. --- invenio_records_marc21/records/models.py | 44 ++++++++++++++++++------ setup.py | 3 ++ 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/invenio_records_marc21/records/models.py b/invenio_records_marc21/records/models.py index 574c8465..a31a901d 100644 --- a/invenio_records_marc21/records/models.py +++ b/invenio_records_marc21/records/models.py @@ -8,44 +8,66 @@ """Marc21 Record and Draft models.""" from invenio_db import db -from invenio_drafts_resources.records import DraftMetadataBase +from invenio_drafts_resources.records import ( + DraftMetadataBase, + ParentRecordMixin, + ParentRecordStateMixin, +) from invenio_files_rest.models import Bucket from invenio_records.models import RecordMetadataBase -from invenio_records_resources.records.models import RecordFileBase +from invenio_records_resources.records.models import FileRecordModelMixin from sqlalchemy_utils.types import UUIDType -class RecordMetadata(db.Model, RecordMetadataBase): +class ParentMetadata(db.Model, RecordMetadataBase): + """Metadata store for the parent record.""" + + __tablename__ = "marc21_parents_metadata" + __versioned__ = {} + + +class RecordMetadata(db.Model, RecordMetadataBase, ParentRecordMixin): """Represent a marc21 record metadata.""" __tablename__ = "marc21_records_metadata" - + __parent_record_model__ = ParentMetadata __versioned__ = {} bucket_id = db.Column(UUIDType, db.ForeignKey(Bucket.id)) bucket = db.relationship(Bucket) -class DraftMetadata(db.Model, DraftMetadataBase): +class DraftMetadata(db.Model, DraftMetadataBase, ParentRecordMixin): """Represent a marc21 record draft metadata.""" __tablename__ = "marc21_drafts_metadata" + __parent_record_model__ = ParentMetadata + __versioned__ = {} bucket_id = db.Column(UUIDType, db.ForeignKey(Bucket.id)) bucket = db.relationship(Bucket) -class RecordFile(db.Model, RecordFileBase): +class RecordFile(db.Model, RecordMetadataBase, FileRecordModelMixin): """File associated with a marc21 record.""" - record_model_cls = RecordMetadata - __tablename__ = "marc21_records_files" + __record_model_cls__ = RecordMetadata + __versioned__ = {} -class DraftFile(db.Model, RecordFileBase): +class DraftFile(db.Model, RecordMetadataBase, FileRecordModelMixin): """File associated with a marc21 draft.""" - record_model_cls = DraftMetadata - __tablename__ = "marc21_drafts_files" + __record_model_cls__ = DraftMetadata + __versioned__ = {} + + +class VersionsState(db.Model, ParentRecordStateMixin): + """Store for the version state of the parent record.""" + + __tablename__ = "marc21_versions_state" + __parent_record_model__ = ParentMetadata + __record_model__ = RecordMetadata + __draft_model__ = DraftMetadata diff --git a/setup.py b/setup.py index 6be7c52d..78f06369 100755 --- a/setup.py +++ b/setup.py @@ -98,6 +98,9 @@ "invenio_base.blueprints": [ "invenio_records_marc21_ui = invenio_records_marc21.ui:create_blueprint", ], + "invenio_db.models": [ + "invenio_records_marc21_model = invenio_records_marc21.records.models", + ], "invenio_i18n.translations": [ "messages = invenio_records_marc21", ], From c3f8cb8c5aced5f118a121b8be3446bef078efaf Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 17 May 2021 15:12:25 +0200 Subject: [PATCH 011/217] modification: update service update api removing internalpid using new access schema --- invenio_records_marc21/cli.py | 48 ++++++----- invenio_records_marc21/records/__init__.py | 3 +- invenio_records_marc21/records/api.py | 63 +++++++++----- invenio_records_marc21/services/config.py | 91 +++++++++++++++++++-- invenio_records_marc21/services/services.py | 22 ++--- 5 files changed, 166 insertions(+), 61 deletions(-) diff --git a/invenio_records_marc21/cli.py b/invenio_records_marc21/cli.py index d8373046..5f7d2c3d 100644 --- a/invenio_records_marc21/cli.py +++ b/invenio_records_marc21/cli.py @@ -17,8 +17,9 @@ from flask_principal import Identity from invenio_access import any_user -from .services import Marc21DraftFilesService, Marc21RecordService, Metadata -from .vocabularies import Vocabularies +from .proxies import current_records_marc21 +from .services import Metadata +from .services.components import AccessStatusEnum def system_identity(): @@ -30,8 +31,7 @@ def system_identity(): def fake_access_right(): """Generates a fake access_right.""" - vocabulary = Vocabularies.get_vocabulary("access_right") - _type = random.choice(list(vocabulary.data.keys())) + _type = random.choice(list(AccessStatusEnum)).value return _type @@ -58,23 +58,29 @@ def create_fake_metadata(filename): """Create records for demo purposes.""" metadata = Metadata() metadata.xml = _load_file(filename) + metadata_access = fake_access_right() data_acces = { - "access_right": fake_access_right(), - "embargo_date": fake_feature_date(), + "owned_by": [{"user": system_identity().id}], + "files": "public", + "metadata": metadata_access, } - - service = Marc21RecordService() - draft_files_service = Marc21DraftFilesService() + if metadata_access == AccessStatusEnum.EMBARGOED.value: + embargo = { + "embargo": { + "until": fake_feature_date(), + "active": True, + "reason": "Because I can!", + } + } + data_acces.update(embargo) + + service = current_records_marc21.records_service draft = service.create( metadata=metadata, identity=system_identity(), access=data_acces, ) - draft_files_service.update_files_options( - id_=draft.id, identity=system_identity(), data={"enabled": False} - ) - record = service.publish(id_=draft.id, identity=system_identity()) return record @@ -83,19 +89,20 @@ def create_fake_metadata(filename): def create_fake_record(filename): """Create records for demo purposes.""" data_to_use = _load_json(filename) + metadata_access = fake_access_right() data_acces = { - "access_right": fake_access_right(), - "embargo_date": fake_feature_date(), + "owned_by": [{"user": system_identity().id}], + "files": AccessStatusEnum.PUBLIC.value, + "metadata": metadata_access, } + if metadata_access == AccessStatusEnum.EMBARGOED.value: + data_acces.update({"embargo_date": fake_feature_date()}) + + service = current_records_marc21.records_service - service = Marc21RecordService() - draft_files_service = Marc21DraftFilesService() draft = service.create( data=data_to_use, identity=system_identity(), access=data_acces ) - draft_files_service.update_files_options( - id_=draft.id, identity=system_identity, data={"enabled": False} - ) record = service.publish(id_=draft.id, identity=system_identity()) @@ -109,6 +116,7 @@ def marc21(): @marc21.command("demo") +@with_appcontext @click.option( "--number", "-n", diff --git a/invenio_records_marc21/records/__init__.py b/invenio_records_marc21/records/__init__.py index e0905fd2..8ed9a156 100644 --- a/invenio_records_marc21/records/__init__.py +++ b/invenio_records_marc21/records/__init__.py @@ -8,7 +8,7 @@ """Marc21 Records module.""" -from .api import DraftFile, Marc21Draft, Marc21Record, RecordFile +from .api import DraftFile, Marc21Draft, Marc21Parent, Marc21Record, RecordFile from .models import DraftMetadata, RecordMetadata __all__ = ( @@ -16,6 +16,7 @@ "Marc21Draft", "RecordFile", "Marc21Record", + "Marc21Parent", "DraftMetadata", "RecordMetadata", ) diff --git a/invenio_records_marc21/records/api.py b/invenio_records_marc21/records/api.py index 08502a78..d0d9b459 100644 --- a/invenio_records_marc21/records/api.py +++ b/invenio_records_marc21/records/api.py @@ -10,8 +10,10 @@ from __future__ import absolute_import, print_function from invenio_drafts_resources.records import Draft, Record -from invenio_records.systemfields import ModelField -from invenio_records_resources.records.api import RecordFile as BaseRecordFile +from invenio_drafts_resources.records.api import ParentRecord as BaseParentRecord +from invenio_drafts_resources.records.systemfields import ParentField +from invenio_records.systemfields import ConstantField, ModelField +from invenio_records_resources.records.api import FileRecord as BaseFileRecord from invenio_records_resources.records.systemfields import ( FilesField, IndexField, @@ -19,7 +21,14 @@ ) from werkzeug.local import LocalProxy -from .models import DraftFile, DraftMetadata, RecordFile, RecordMetadata +from .models import ( + DraftFile, + DraftMetadata, + ParentMetadata, + RecordFile, + RecordMetadata, + VersionsState, +) from .systemfields import ( MarcDraftProvider, MarcPIDFieldContext, @@ -28,7 +37,27 @@ ) -class DraftFile(BaseRecordFile): +# +# Parent record API +# +class Marc21Parent(BaseParentRecord): + """Parent record.""" + + versions_model_cls = VersionsState + model_cls = ParentMetadata + + schema = ConstantField("$schema", "local://marc21/parent-v1.0.0.json") + + pid = PIDField( + key="id", + provider=MarcDraftProvider, + context_cls=MarcPIDFieldContext, + resolver_cls=MarcResolver, + delete=False, + ) + + +class DraftFile(BaseFileRecord): """Marc21 file associated with a marc21 draft model.""" model_cls = DraftFile @@ -39,11 +68,15 @@ class Marc21Draft(Draft): """Marc21 draft API.""" model_cls = DraftMetadata + versions_model_cls = VersionsState + parent_record_cls = Marc21Parent index = IndexField( "marc21records-drafts-marc21-v1.0.0", search_alias="marc21records-marc21" ) + parent = ParentField(Marc21Parent, create=True, soft_delete=False, hard_delete=True) + pid = PIDField( key="id", provider=MarcDraftProvider, @@ -52,14 +85,6 @@ class Marc21Draft(Draft): delete=False, ) - conceptpid = PIDField( - key="conceptid", - provider=MarcDraftProvider, - context_cls=MarcPIDFieldContext, - resolver_cls=MarcResolver, - delete=False, - ) - files = FilesField( store=False, file_cls=DraftFile, @@ -71,7 +96,7 @@ class Marc21Draft(Draft): bucket = ModelField(dump=False) -class RecordFile(BaseRecordFile): +class RecordFile(BaseFileRecord): """Marc21 record file API.""" model_cls = RecordFile @@ -82,25 +107,21 @@ class Marc21Record(Record): """Define API for Marc21 create and manipulate.""" model_cls = RecordMetadata + versions_model_cls = VersionsState + parent_record_cls = Marc21Parent index = IndexField( "marc21records-marc21-marc21-v1.0.0", search_alias="marc21records-marc21" ) + parent = ParentField(Marc21Parent, create=True, soft_delete=False, hard_delete=True) + pid = PIDField( key="id", provider=MarcRecordProvider, - delete=False, context_cls=MarcPIDFieldContext, resolver_cls=MarcResolver, - ) - - conceptpid = PIDField( - key="conceptid", - provider=MarcRecordProvider, delete=False, - context_cls=MarcPIDFieldContext, - resolver_cls=MarcResolver, ) files = FilesField( diff --git a/invenio_records_marc21/services/config.py b/invenio_records_marc21/services/config.py index 3c391329..fa56288e 100644 --- a/invenio_records_marc21/services/config.py +++ b/invenio_records_marc21/services/config.py @@ -8,33 +8,95 @@ """Marc21 Record Service config.""" -from invenio_drafts_resources.services.records import RecordDraftServiceConfig +from flask import current_app +from invenio_drafts_resources.services.records.config import ( + RecordServiceConfig, + SearchDraftsOptions, + SearchOptions, + is_record, +) +from invenio_records_resources.services import ConditionalLink, FileServiceConfig from invenio_records_resources.services.files.config import FileServiceConfig -from invenio_records_resources.services.records.components import MetadataComponent +from invenio_records_resources.services.records.links import RecordLink from invenio_records_resources.services.records.search import terms_filter -from ..records import Marc21Draft, Marc21Record -from .components import AccessComponent, PIDComponent +from ..records import Marc21Draft, Marc21Parent, Marc21Record +from .components import AccessComponent, MetadataComponent, PIDComponent from .permissions import Marc21RecordPermissionPolicy -from .schemas import Marc21RecordSchema, MetadataSchema +from .schemas import Marc21RecordSchema +from .schemas.parent import Marc21ParentSchema -class Marc21RecordServiceConfig(RecordDraftServiceConfig): +class Marc21SearchOptions(SearchOptions): + """Search options for record search.""" + + facets_options = dict( + aggs={ + "title": { + "terms": {"field": "metadata.json.title_statement.title"}, + }, + "access_right": { + "terms": {"field": "access.metadata"}, + }, + }, + post_filters={ + "title": terms_filter( + "metadata.json.title_statement.title", + ), + "access_right": terms_filter("access.metadata"), + }, + ) + + +class Marc21SearchDraftsOptions(SearchDraftsOptions): + """Search options for drafts search.""" + + facets_options = dict( + aggs={ + "resource_type": { + "terms": {"field": "metadata"}, + "aggs": {}, + }, + "access_right": { + "terms": {"field": "access.metadata"}, + }, + "is_published": { + "terms": {"field": "is_published"}, + }, + }, + post_filters={ + "access_right": terms_filter("access.metadata"), + "is_published": terms_filter("is_published"), + }, + ) + + +class Marc21RecordServiceConfig(RecordServiceConfig): """Marc21 record service config.""" # Record class record_cls = Marc21Record # Draft class draft_cls = Marc21Draft + # Parent class + parent_record_cls = Marc21Parent + # Schemas schema = Marc21RecordSchema + schema_parent = Marc21ParentSchema + # TODO: ussing from invenio-permissions permission_policy_cls = Marc21RecordPermissionPolicy search_facets_options = dict( - aggs={}, + aggs={ + "is_published": { + "terms": {"field": "is_published"}, + }, + }, post_filters={ - "access_right": terms_filter("access.access_right"), + "access_right": terms_filter("access.status"), + "is_published": terms_filter("is_published"), }, ) @@ -44,6 +106,19 @@ class Marc21RecordServiceConfig(RecordDraftServiceConfig): PIDComponent, ] + links_item = { + "self": ConditionalLink( + cond=is_record, + if_=RecordLink("{+api}/marc21/{id}"), + else_=RecordLink("{+api}/marc21/{id}/draft"), + ), + "self_html": ConditionalLink( + cond=is_record, + if_=RecordLink("{+ui}/marc21/{id}"), + else_=RecordLink("{+ui}/uploads/{id}"), + ), + } + # # Record files diff --git a/invenio_records_marc21/services/services.py b/invenio_records_marc21/services/services.py index 8ccdb60b..52ca80a1 100644 --- a/invenio_records_marc21/services/services.py +++ b/invenio_records_marc21/services/services.py @@ -9,10 +9,11 @@ """Marc21 Record Service.""" -from invenio_drafts_resources.services.records import RecordDraftService -from invenio_records_resources.services.files.service import RecordFileService +from invenio_drafts_resources.services.records import RecordService +from invenio_records_resources.services.files.service import FileService from invenio_records_resources.services.records.results import RecordItem +from .components import AccessStatusEnum from .config import ( Marc21DraftFilesServiceConfig, Marc21RecordFilesServiceConfig, @@ -51,7 +52,7 @@ def xml(self, xml: str): self._xml = xml -class Marc21RecordService(RecordDraftService): +class Marc21RecordService(RecordService): """Marc21 record service.""" config_name = "MARC21_RECORDS_SERVICE_CONFIG" @@ -70,9 +71,9 @@ def _create_data(self, identity, data, metadata, access=None): if "access" not in data: default_access = { "access": { - "metadata": False, "owned_by": [{"user": identity.id}], - "access_right": "open", + "metadata": AccessStatusEnum.PUBLIC.value, + "files": AccessStatusEnum.PUBLIC.value, }, } if access is not None: @@ -81,7 +82,7 @@ def _create_data(self, identity, data, metadata, access=None): return data def create( - self, identity, data=None, metadata=Metadata(), links_config=None, access=None + self, identity, data=None, metadata=Metadata(), access=None ) -> RecordItem: """Create a draft record. @@ -92,7 +93,7 @@ def create( :param dict access: provide access additional information """ data = self._create_data(identity, data, metadata, access) - return super().create(identity, data, links_config) + return super().create(identity, data) def update_draft( self, @@ -100,7 +101,6 @@ def update_draft( identity, data=None, metadata=Metadata(), - links_config=None, revision_id=None, access=None, ): @@ -113,13 +113,13 @@ def update_draft( :param dict access: provide access additional information """ data = self._create_data(identity, data, metadata, access) - return super().update_draft(id_, identity, data, links_config, revision_id) + return super().update_draft(id_, identity, data, revision_id) # # Record files # -class Marc21RecordFilesService(RecordFileService): +class Marc21RecordFilesService(FileService): """Marc21 record files service.""" config_name = "MARC21_RECORDS_FILES_SERVICE_CONFIG" @@ -129,7 +129,7 @@ class Marc21RecordFilesService(RecordFileService): # # Draft files # -class Marc21DraftFilesService(RecordFileService): +class Marc21DraftFilesService(FileService): """Marc21 draft files service.""" config_name = "MARC21_DRAFT_FILES_SERVICE_CONFIG" From 6a43fa017cea9cdcf9f431f798949ca6a4b53bc3 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 17 May 2021 15:22:25 +0200 Subject: [PATCH 012/217] modification: schemas adding file schema and update access schema. --- .../services/schemas/__init__.py | 42 ++++----- .../services/schemas/access.py | 92 ++++++++++++------- .../services/schemas/files.py | 34 +++++++ .../services/schemas/versions.py | 24 +++++ 4 files changed, 136 insertions(+), 56 deletions(-) create mode 100644 invenio_records_marc21/services/schemas/files.py create mode 100644 invenio_records_marc21/services/schemas/versions.py diff --git a/invenio_records_marc21/services/schemas/__init__.py b/invenio_records_marc21/services/schemas/__init__.py index 4dc90140..2db41f1a 100644 --- a/invenio_records_marc21/services/schemas/__init__.py +++ b/invenio_records_marc21/services/schemas/__init__.py @@ -7,48 +7,42 @@ """Marc21 record schemas.""" -from invenio_records_resources.services.records.schema import RecordSchema +from invenio_records_resources.services.records.schema import BaseRecordSchema from marshmallow import EXCLUDE, INCLUDE, Schema, fields, missing, post_dump +from marshmallow_utils.fields import NestedAttribute from .access import AccessSchema +from .files import FilesSchema from .metadata import MetadataSchema +from .parent import Marc21ParentSchema from .pids import PIDSchema +from .versions import VersionsSchema -class AttributeAccessorFieldMixin: - """Marshmallow field mixin for attribute-based serialization.""" - - def get_value(self, obj, attr, accessor=None, default=missing): - """Return the value for a given key from an object attribute.""" - attribute = getattr(self, "attribute", None) - check_key = attr if attribute is None else attribute - return getattr(obj, check_key, default) - - -class NestedAttribute(fields.Nested, AttributeAccessorFieldMixin): - """Nested object attribute field.""" - - -class Marc21RecordSchema(RecordSchema): +class Marc21RecordSchema(BaseRecordSchema): """Record schema.""" - class Meta: - """Meta class.""" - - unknown = EXCLUDE - id = fields.Str() # pid - conceptid = fields.Str() - # conceptpid pids = fields.List(NestedAttribute(PIDSchema)) + + parent = NestedAttribute(Marc21ParentSchema, dump_only=True) + metadata = NestedAttribute(MetadataSchema) access = NestedAttribute(AccessSchema) - # files = NestedAttribute(FilesSchema, dump_only=True) + files = NestedAttribute(FilesSchema, dump_only=True) + created = fields.Str(dump_only=True) updated = fields.Str(dump_only=True) revision = fields.Integer(dump_only=True) + versions = NestedAttribute(VersionsSchema, dump_only=True) + + is_published = fields.Boolean(dump_only=True) + + # Add version to record schema + # versions = NestedAttribute(VersionsSchema, dump_only=True) + @post_dump def default_nested(self, data, many, **kwargs): """Serialize metadata as empty dict for partial drafts. diff --git a/invenio_records_marc21/services/schemas/access.py b/invenio_records_marc21/services/schemas/access.py index 979f8966..2023e8f8 100644 --- a/invenio_records_marc21/services/schemas/access.py +++ b/invenio_records_marc21/services/schemas/access.py @@ -9,53 +9,81 @@ import arrow from flask_babelex import lazy_gettext as _ -from marshmallow import ( - Schema, - ValidationError, - fields, - validate, - validates, - validates_schema, -) +from marshmallow import Schema, ValidationError, fields, validates, validates_schema from marshmallow_utils.fields import ISODateString, SanitizedUnicode +from marshmallow_utils.fields.nestedattr import NestedAttribute -from .utils import validate_entry +from ..components import AccessStatusEnum -class AccessConditionSchema(Schema): - """Access condition schema. +class Agent(Schema): + """An agent schema.""" - Conditions under which access to files are granted. - """ + user = fields.Integer(required=True) - condition = fields.String() - default_link_validity = fields.Integer() +class EmbargoSchema(Schema): + """Schema for an embargo on the record.""" -class Agent(Schema): - """An agent schema.""" + active = fields.Bool(allow_none=True, missing=None) + until = ISODateString(allow_none=True, missing=None) + reason = SanitizedUnicode(allow_none=True, missing=None) - user = fields.Integer(required=True) + @validates_schema + def validate_embargo(self, data, **kwargs): + """Validate that the properties are consistent with each other.""" + if data.get("until") is not None: + until_date = arrow.get(data.get("until")) + else: + until_date = None + + if data.get("active", False): + if until_date is None or until_date < arrow.utcnow(): + raise ValidationError( + _( + "Embargo end date must be set to a future date " + "if active is True." + ), + field_name="until", + ) + + elif data.get("active", None) is not None: + if until_date is not None and until_date > arrow.utcnow(): + raise ValidationError( + _( + "Embargo end date must be unset or in the past " + "if active is False." + ), + field_name="until", + ) class AccessSchema(Schema): """Access schema.""" - metadata = fields.Bool(required=True) + metadata = SanitizedUnicode(required=True) + files = SanitizedUnicode(required=True) + embargo = NestedAttribute(EmbargoSchema) + status = SanitizedUnicode(dump_only=False) owned_by = fields.List(fields.Nested(Agent)) - access_right = SanitizedUnicode(required=True) - embargo_date = ISODateString() - access_condition = fields.Nested(AccessConditionSchema) - - @validates("embargo_date") - def validate_embargo_date(self, value): - """Validate that embargo date is in the future.""" - if arrow.get(value).date() <= arrow.utcnow().date(): + + def validate_protection_value(self, value, field_name): + """Check that the protection value is valid.""" + if value not in AccessStatusEnum.list(): raise ValidationError( - _("Embargo date must be in the future."), field_names=["embargo_date"] + _("'{}' must be either '{}', '{}' or '{}'").format( + field_name, + *AccessStatusEnum.list(), + ), + "record", ) - @validates_schema - def validate_access_right(self, data, **kwargs): - """Validate that access right is one of the allowed ones.""" - validate_entry("access_right", data) + @validates("metadata") + def validate_record_protection(self, value): + """Validate the record protection value.""" + self.validate_protection_value(value, "record") + + @validates("files") + def validate_files_protection(self, value): + """Validate the files protection value.""" + self.validate_protection_value(value, "files") diff --git a/invenio_records_marc21/services/schemas/files.py b/invenio_records_marc21/services/schemas/files.py new file mode 100644 index 00000000..e3ecb9d5 --- /dev/null +++ b/invenio_records_marc21/services/schemas/files.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2020 CERN. +# Copyright (C) 2020 Northwestern University. +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or modify +# it under the terms of the MIT License; see LICENSE file for more details. + +"""Marc21 record files schemas.""" + +from marshmallow import Schema, fields +from marshmallow_utils.fields import SanitizedUnicode + + +class FileSchema(Schema): + """File schema.""" + + type = fields.String() + checksum = fields.String() + size = fields.Integer() + key = SanitizedUnicode() + version_id = SanitizedUnicode() + bucket_id = SanitizedUnicode() + mimetype = SanitizedUnicode() + storage_class = SanitizedUnicode() + + +class FilesSchema(Schema): + """Files metadata schema.""" + + enabled = fields.Bool() + default_preview = SanitizedUnicode() + order = fields.List(SanitizedUnicode()) diff --git a/invenio_records_marc21/services/schemas/versions.py b/invenio_records_marc21/services/schemas/versions.py new file mode 100644 index 00000000..cd4c25ae --- /dev/null +++ b/invenio_records_marc21/services/schemas/versions.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 TU Wien. +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or modify +# it under the terms of the MIT License; see LICENSE file for more details. + +"""Marc21 record schema version.""" + +from invenio_drafts_resources.services.records.schema import ( + VersionsSchema as VersionsSchemaBase, +) +from marshmallow_utils.permissions import FieldPermissionsMixin + + +class VersionsSchema(VersionsSchemaBase, FieldPermissionsMixin): + """Version schema with field-level permissions.""" + + field_load_permissions = {} + + field_dump_permissions = { + "is_latest_draft": "edit", + } From 1a07db851747ecba809774c7ca03e46b3b6e24da Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 17 May 2021 15:45:29 +0200 Subject: [PATCH 013/217] modification: removing new_version in MetadataComponent --- invenio_records_marc21/services/components/metadata.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/invenio_records_marc21/services/components/metadata.py b/invenio_records_marc21/services/components/metadata.py index d9b99f5a..13577f85 100644 --- a/invenio_records_marc21/services/components/metadata.py +++ b/invenio_records_marc21/services/components/metadata.py @@ -35,9 +35,3 @@ def publish(self, identity, draft=None, record=None, **kwargs): def edit(self, identity, draft=None, record=None, **kwargs): """Update draft metadata.""" draft.metadata = record.get("metadata", {}) - - def new_version(self, identity, draft=None, record=None, **kwargs): - """Update draft metadata.""" - draft.metadata = record.get("metadata", {}) - for f in self.new_version_skip_fields: - draft.metadata.pop(f, None) From 1f0185a20c4d16e5d3b69ace27913ef7a5ed900b Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 17 May 2021 15:47:53 +0200 Subject: [PATCH 014/217] modification: update services in ext --- invenio_records_marc21/ext.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/invenio_records_marc21/ext.py b/invenio_records_marc21/ext.py index 99f38d4d..62c40e52 100755 --- a/invenio_records_marc21/ext.py +++ b/invenio_records_marc21/ext.py @@ -10,8 +10,14 @@ from __future__ import absolute_import, print_function from . import config -from .resources import Marc21DraftResource, Marc21RecordResource -from .services import Marc21RecordService +from .services import ( + Marc21DraftFilesServiceConfig, + Marc21RecordFilesServiceConfig, + Marc21RecordPermissionPolicy, + Marc21RecordService, + Marc21RecordServiceConfig, +) + class InvenioRecordsMARC21(object): @@ -25,7 +31,7 @@ def __init__(self, app=None): def init_app(self, app): """Flask application initialization.""" self.init_config(app) - self.init_resource(app) + self.init_services(app) app.extensions["invenio-records-marc21"] = self def init_config(self, app): @@ -48,10 +54,17 @@ def init_config(self, app): app.config.setdefault(n, {}) app.config[n].update(getattr(config, k)) - def init_resource(self, app): - """Initialize resources.""" + def init_services(self, app): + """Initialize services.""" + service_config = Marc21RecordServiceConfig + service_config.permission_policy_cls = obj_or_import_string( + app.config.get("RECORDS_PERMISSIONS_RECORD_POLICY1"), + default=Marc21RecordPermissionPolicy, + ) self.records_service = Marc21RecordService( - config=app.config.get(Marc21RecordService.config_name), + config=service_config, + files_service=FileService(Marc21RecordFilesServiceConfig), + draft_files_service=FileService(Marc21DraftFilesServiceConfig), ) self.records_resource = Marc21RecordResource( From 59d9e03b8db5d36657aedabc529ece5fa69aa00c Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 17 May 2021 15:52:14 +0200 Subject: [PATCH 015/217] build(setup): update dependencies --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 78f06369..2bba7121 100755 --- a/setup.py +++ b/setup.py @@ -54,8 +54,8 @@ "invenio-i18n>=1.3.0", "dojson>=1.4.0", "invenio-records-rest>=1.5.0,<2.0.0", - "invenio-drafts-resources>=0.9.7,<0.10.0", - "invenio-vocabularies>=0.3.3,<0.4.0", + "invenio-drafts-resources>=0.11.0,<0.12.0", + "invenio-vocabularies>=0.5.0,<0.7.0", ] packages = find_packages() From 4b2c8f9ba73e2c3ff494bdf66fdd529f0895c399 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 17 May 2021 15:54:39 +0200 Subject: [PATCH 016/217] modification: removing vocabularies in module --- .../resources/serializers/ui/schema.py | 2 - .../vocabularies/__init__.py | 20 --- .../vocabularies/access_right.csv | 5 - .../vocabularies/access_right.py | 36 ----- invenio_records_marc21/vocabularies/views.py | 16 --- .../vocabularies/vocabularies.py | 83 ------------ .../vocabularies/vocabulary.py | 126 ------------------ 7 files changed, 288 deletions(-) delete mode 100644 invenio_records_marc21/vocabularies/__init__.py delete mode 100644 invenio_records_marc21/vocabularies/access_right.csv delete mode 100644 invenio_records_marc21/vocabularies/access_right.py delete mode 100644 invenio_records_marc21/vocabularies/views.py delete mode 100644 invenio_records_marc21/vocabularies/vocabularies.py delete mode 100644 invenio_records_marc21/vocabularies/vocabulary.py diff --git a/invenio_records_marc21/resources/serializers/ui/schema.py b/invenio_records_marc21/resources/serializers/ui/schema.py index 64303480..585f4c23 100755 --- a/invenio_records_marc21/resources/serializers/ui/schema.py +++ b/invenio_records_marc21/resources/serializers/ui/schema.py @@ -16,8 +16,6 @@ from marshmallow.fields import Dict, Method, Nested, Str from marshmallow_utils.fields import FormatDate as BaseFormatDatetime -from invenio_records_marc21.vocabularies import Vocabularies - FormatDatetime = partial(BaseFormatDatetime, locale=get_locale) diff --git a/invenio_records_marc21/vocabularies/__init__.py b/invenio_records_marc21/vocabularies/__init__.py deleted file mode 100644 index a8beb2b5..00000000 --- a/invenio_records_marc21/vocabularies/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2021 Graz University of Technology. -# -# Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it -# under the terms of the MIT License; see LICENSE file for more details. - -"""Models for Invenio Marc21 Records.""" - -# TODO: remove vocabularies after invenio-vocabularies is released (fixme) - -from .access_right import AccessRightVocabulary -from .vocabularies import Vocabularies -from .vocabulary import Vocabulary - -__all__ = ( - "AccessRightVocabulary", - "Vocabularies", - "Vocabulary", -) diff --git a/invenio_records_marc21/vocabularies/access_right.csv b/invenio_records_marc21/vocabularies/access_right.csv deleted file mode 100644 index d02d164a..00000000 --- a/invenio_records_marc21/vocabularies/access_right.csv +++ /dev/null @@ -1,5 +0,0 @@ -access_right,access_right_name,icon,notes -open, Open Access, lock open -embargoed, Embargoed, ban -restricted, Restricted, key -closed, Private, lock diff --git a/invenio_records_marc21/vocabularies/access_right.py b/invenio_records_marc21/vocabularies/access_right.py deleted file mode 100644 index 14fd1142..00000000 --- a/invenio_records_marc21/vocabularies/access_right.py +++ /dev/null @@ -1,36 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2021 Graz University of Technology. -# -# Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it -# under the terms of the MIT License; see LICENSE file for more details. - - -"""Contributor Role Vocabulary.""" - -from .vocabulary import Vocabulary - - -class AccessRightVocabulary(Vocabulary): - """Encapsulates all access right choices. - - NOTE: Perhaps a stretch of Vocabulary, but very useful. - """ - - key_field = "access_right" - - @property - def readable_key(self): - """Returns the key to readable values for this vocabulary.""" - return "access_right_name" - - @property - def vocabulary_name(self): - """Returns the human readable name for this vocabulary.""" - return "access right" - - def get_entry_by_dict(self, dict_key): - """Returns a vocabulary entry as an OrderedDict.""" - if isinstance(dict_key, str): - dict_key = {self.key_field: dict_key} - return super().get_entry_by_dict(dict_key) diff --git a/invenio_records_marc21/vocabularies/views.py b/invenio_records_marc21/vocabularies/views.py deleted file mode 100644 index 5f8ded17..00000000 --- a/invenio_records_marc21/vocabularies/views.py +++ /dev/null @@ -1,16 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2021 Graz University of Technology. -# -# Invenio-Records-Marc21 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 vocabularies views.""" - - -def create_subjects_blueprint_from_app(app): - """Create app blueprint.""" - return app.extensions["invenio-records-marc21"].subjects_resource.as_blueprint( - "vocabularies-subjects" - ) diff --git a/invenio_records_marc21/vocabularies/vocabularies.py b/invenio_records_marc21/vocabularies/vocabularies.py deleted file mode 100644 index 663bb88b..00000000 --- a/invenio_records_marc21/vocabularies/vocabularies.py +++ /dev/null @@ -1,83 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2021 Graz University of Technology. -# -# Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it -# under the terms of the MIT License; see LICENSE file for more details. - - -"""Models for Invenio Marc21 Records.""" - -from os.path import dirname, join - -from flask import current_app -from flask_babelex import lazy_gettext as _ - -from .access_right import AccessRightVocabulary - - -class Vocabularies: - """Interface to vocabulary data.""" - - this_dir = dirname(__file__) - vocabularies = { - # NOTE: dotted keys should parallel MetadataSchemaV1 fields - "access_right": { - "path": join(this_dir, "access_right.csv"), - "class": AccessRightVocabulary, - "object": None, - }, - } - - @classmethod - def get_vocabulary(cls, key): - """Returns the Vocabulary object corresponding to the path. - - :param key: string of dotted subkeys for Vocabulary object. - """ - vocabulary_dict = cls.vocabularies.get(key) - - if not vocabulary_dict: - return None - - obj = vocabulary_dict.get("object") - - if not obj: - custom_vocabulary_dict = current_app.config.get( - "MARC21_RECORDS_CUSTOM_VOCABULARIES", {} - ).get(key, {}) - - path = custom_vocabulary_dict.get("path") or vocabulary_dict.get("path") - - # Only predefined classes for now - VocabularyClass = vocabulary_dict["class"] - obj = VocabularyClass(path) - vocabulary_dict["object"] = obj - - return obj - - @classmethod - def clear(cls): - """Clears loaded vocabularies.""" - for value in cls.vocabularies.values(): - value["object"] = None - - @classmethod - def dump(cls): - """Returns a json-compatible dict of options for frontend. - - The current shape is influenced by current frontend, but it's flexible - enough to withstand the test of time (new frontend would be able to - change it easily). - """ - options = {} - for key in cls.vocabularies: - result = options - for i, dotkey in enumerate(key.split(".")): - if i == (len(key.split(".")) - 1): - result[dotkey] = cls.get_vocabulary(key).dump_options() - else: - result.setdefault(dotkey, {}) - result = result[dotkey] - - return options diff --git a/invenio_records_marc21/vocabularies/vocabulary.py b/invenio_records_marc21/vocabularies/vocabulary.py deleted file mode 100644 index 672899d4..00000000 --- a/invenio_records_marc21/vocabularies/vocabulary.py +++ /dev/null @@ -1,126 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2021 Graz University of Technology. -# -# Invenio-Records-Marc21 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.""" - -import csv -from collections import OrderedDict, defaultdict - -from flask_babelex import lazy_gettext as _ - - -def hierarchized_rows(dict_reader): - """Yields filled OrderedDict rows according to csv hierarchy. - - Idea is to have the csv rows: - - fooA, barA-1, bazA-1 - , barA-2, bazA-2 - fooB, barB-1, bazB-1 - , , bazB-2 - - map to these rows - - fooA, barA-1, bazA-1 - fooA, barA-2, bazA-2 - fooB, barB-1, bazB-1 - fooB, barB-1, bazB-2 - - This makes it easy for subject matter experts to fill the csv in - their spreadsheet software, while also allowing hierarchy of data - a-la yaml and extensibility for other conversions or data down the road. - """ - prev_row = defaultdict(lambda: "") - - for row in dict_reader: # row is an OrderedDict in fieldnames order - current_row = row - for field in row: - if not current_row[field]: - current_row[field] = prev_row[field] - else: - break - prev_row = current_row - yield current_row - - -class Vocabulary(object): - """Abstracts common vocabulary functionality.""" - - key_field = None # Children need to fill - - def __init__(self, path): - """Constructor.""" - self.path = path - self._load_data() - - @property - def readable_key(self): - """Returns the key to readable values for this vocabulary.""" - raise NotImplementedError() - - @property - def vocabulary_name(self): - """Returns the human readable name for this vocabulary.""" - raise NotImplementedError() - - def key(self, row): - """Returns the primary key of the row. - - row: dict-like - returns: serializable - """ - return row.get(self.key_field) - - def _load_data(self): - """Sets self.data with the filled rows.""" - with open(self.path) as f: - reader = csv.DictReader(f, skipinitialspace=True) - # NOTE: We use an OrderedDict to preserve on file row order - self.data = OrderedDict( - [ - # NOTE: unfilled cells return '' (empty string) - (self.key(row), row) - for row in hierarchized_rows(reader) - ] - ) - - def get_entry_by_dict(self, dict_key): - """Returns a vocabulary entry as an OrderedDict.""" - return self.data.get(self.key(dict_key)) - - def get_title_by_dict(self, dict_key): - """Returns the vocabulary entry's human readable name.""" - entry = self.get_entry_by_dict(dict_key) - - # NOTE: translations could also be done via the CSV file directly - return entry.get(self.readable_key) - - def get_invalid(self, dict_key): - """Returns the user facing error message for the given dict key.""" - choices = sorted([self.key(e) for e in self.data.values()]) - return {self.key_field: [_(f"Invalid value. Choose one of {choices}.")]} - - def dump_options(self): - """Returns json-compatible dict of options for roles. - - The current shape is influenced by current frontend, but it's flexible - enough to withstand the test of time (new frontend would be able to - adapt it to their needs easily). - - TODO: Be attentive to generalization for all vocabularies. - """ - options = [ - { - **({"icon": entry["icon"]} if entry.get("icon") else {}), - "text": _(entry.get(self.readable_key)), - "value": key, - } - for key, entry in self.data.items() - ] - - return options From 99a3eb71d86b8a7e0ea4234dffaf77071ed47f9a Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Thu, 20 May 2021 08:39:14 +0200 Subject: [PATCH 017/217] modification: update schema access testset --- tests/services/schemas/test_access.py | 46 +++++++++++++++------------ 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/tests/services/schemas/test_access.py b/tests/services/schemas/test_access.py index 17792967..aa38c112 100644 --- a/tests/services/schemas/test_access.py +++ b/tests/services/schemas/test_access.py @@ -10,41 +10,47 @@ import pytest from flask_babelex import lazy_gettext as _ +from marshmallow import ValidationError from marshmallow.exceptions import ValidationError from invenio_records_marc21.services.schemas.access import AccessSchema -from .test_utils import assert_raises_messages + +def _assert_raises_messages(lambda_expression, expected_messages): + with pytest.raises(ValidationError) as e: + lambda_expression() + + messages = e.value.normalized_messages() + assert expected_messages == messages -def test_valid_full(vocabulary_clear): +def test_valid_full(): valid_full = { - "metadata": False, + "metadata": "public", "owned_by": [{"user": 1}], - "embargo_date": "2120-10-06", - "access_right": "open", - "access_condition": { - "condition": "because it is needed", - "default_link_validity": 30, + "embargo": { + "until": "2120-10-06", + "active": True, + "reason": "Because I can!" }, + "files": "public", } assert valid_full == AccessSchema().load(valid_full) -def test_invalid_access_right(vocabulary_clear): +def test_invalid_access_right(): invalid_access_right = { - "metadata": False, + "files": "public", "owned_by": [{"user": 1}], - "access_right": "invalid value", + "metadata": "invalid value", } - assert_raises_messages( + _assert_raises_messages( lambda: AccessSchema().load(invalid_access_right), { - "access_right": [ + "metadata": [ _( - "Invalid value. Choose one of ['closed', 'embargoed', " - "'open', 'restricted']." + "'record' must be either 'public', 'embargoed' or 'restricted'" ) ] }, @@ -56,18 +62,18 @@ def test_invalid_access_right(vocabulary_clear): [ ( { - "metadata": False, - "access_right": "open", + "metadata": "public", + "files": "public", "owned_by": [1], }, "owned_by", ), ( - {"metadata": False, "owned_by": [{"user": 1}]}, - "access_right", + {"metadata": "public", "owned_by": [{"user": 1}]}, + "files", ), ( - {"owned_by": [{"user": 1}], "access_right": "open"}, + {"owned_by": [{"user": 1}], "files": "public"}, "metadata", ), ], From d9b452c0ed64838e90f583ae57dbf1e8466ea968 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Thu, 20 May 2021 10:19:18 +0200 Subject: [PATCH 018/217] tests: update access schema testset --- tests/services/schemas/test_access.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/tests/services/schemas/test_access.py b/tests/services/schemas/test_access.py index aa38c112..0f52155d 100644 --- a/tests/services/schemas/test_access.py +++ b/tests/services/schemas/test_access.py @@ -26,13 +26,9 @@ def _assert_raises_messages(lambda_expression, expected_messages): def test_valid_full(): valid_full = { - "metadata": "public", + "metadata": "embargoed", "owned_by": [{"user": 1}], - "embargo": { - "until": "2120-10-06", - "active": True, - "reason": "Because I can!" - }, + "embargo": {"until": "2120-10-06", "active": True, "reason": "Because I can!"}, "files": "public", } assert valid_full == AccessSchema().load(valid_full) @@ -49,9 +45,7 @@ def test_invalid_access_right(): lambda: AccessSchema().load(invalid_access_right), { "metadata": [ - _( - "'record' must be either 'public', 'embargoed' or 'restricted'" - ) + _("'record' must be either 'public', 'embargoed' or 'restricted'") ] }, ) From 45392adba010c668954747490a858c872ac59bc1 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Thu, 20 May 2021 11:58:35 +0200 Subject: [PATCH 019/217] modification: move acces schema for marc21 records --- invenio_records_marc21/services/config.py | 3 +- .../services/schemas/access/__init__.py | 18 +++++++ .../schemas/{access.py => access/embargo.py} | 48 ++--------------- .../{parent/access.py => access/parent.py} | 10 ++-- .../services/schemas/access/record.py | 54 +++++++++++++++++++ .../services/schemas/parent/__init__.py | 22 -------- tests/services/conftest.py | 18 +++---- tests/services/test_create_record.py | 9 ++-- 8 files changed, 96 insertions(+), 86 deletions(-) create mode 100644 invenio_records_marc21/services/schemas/access/__init__.py rename invenio_records_marc21/services/schemas/{access.py => access/embargo.py} (50%) rename invenio_records_marc21/services/schemas/{parent/access.py => access/parent.py} (67%) create mode 100644 invenio_records_marc21/services/schemas/access/record.py delete mode 100644 invenio_records_marc21/services/schemas/parent/__init__.py diff --git a/invenio_records_marc21/services/config.py b/invenio_records_marc21/services/config.py index fa56288e..6ac5f6a8 100644 --- a/invenio_records_marc21/services/config.py +++ b/invenio_records_marc21/services/config.py @@ -23,8 +23,7 @@ from ..records import Marc21Draft, Marc21Parent, Marc21Record from .components import AccessComponent, MetadataComponent, PIDComponent from .permissions import Marc21RecordPermissionPolicy -from .schemas import Marc21RecordSchema -from .schemas.parent import Marc21ParentSchema +from .schemas import Marc21ParentSchema, Marc21RecordSchema class Marc21SearchOptions(SearchOptions): diff --git a/invenio_records_marc21/services/schemas/access/__init__.py b/invenio_records_marc21/services/schemas/access/__init__.py new file mode 100644 index 00000000..bd6dde3d --- /dev/null +++ b/invenio_records_marc21/services/schemas/access/__init__.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it +# under the terms of the MIT License; see LICENSE file for more details. + +"""Marc21 parent record schemas.""" + +from .embargo import EmbargoSchema +from .parent import ParentAccessSchema +from .record import AccessSchema + +__all__ = ( + "ParentAccessSchema", + "AccessSchema", + "EmbargoSchema", +) diff --git a/invenio_records_marc21/services/schemas/access.py b/invenio_records_marc21/services/schemas/access/embargo.py similarity index 50% rename from invenio_records_marc21/services/schemas/access.py rename to invenio_records_marc21/services/schemas/access/embargo.py index 2023e8f8..2d096d97 100644 --- a/invenio_records_marc21/services/schemas/access.py +++ b/invenio_records_marc21/services/schemas/access/embargo.py @@ -5,27 +5,18 @@ # Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it # under the terms of the MIT License; see LICENSE file for more details. -"""Marc21 record schemas.""" +"""Marc21 embargo access schemas.""" import arrow from flask_babelex import lazy_gettext as _ -from marshmallow import Schema, ValidationError, fields, validates, validates_schema -from marshmallow_utils.fields import ISODateString, SanitizedUnicode -from marshmallow_utils.fields.nestedattr import NestedAttribute - -from ..components import AccessStatusEnum - - -class Agent(Schema): - """An agent schema.""" - - user = fields.Integer(required=True) +from marshmallow import Schema, ValidationError, validates_schema +from marshmallow_utils.fields import Bool, ISODateString, SanitizedUnicode class EmbargoSchema(Schema): """Schema for an embargo on the record.""" - active = fields.Bool(allow_none=True, missing=None) + active = Bool(allow_none=True, missing=None) until = ISODateString(allow_none=True, missing=None) reason = SanitizedUnicode(allow_none=True, missing=None) @@ -56,34 +47,3 @@ def validate_embargo(self, data, **kwargs): ), field_name="until", ) - - -class AccessSchema(Schema): - """Access schema.""" - - metadata = SanitizedUnicode(required=True) - files = SanitizedUnicode(required=True) - embargo = NestedAttribute(EmbargoSchema) - status = SanitizedUnicode(dump_only=False) - owned_by = fields.List(fields.Nested(Agent)) - - def validate_protection_value(self, value, field_name): - """Check that the protection value is valid.""" - if value not in AccessStatusEnum.list(): - raise ValidationError( - _("'{}' must be either '{}', '{}' or '{}'").format( - field_name, - *AccessStatusEnum.list(), - ), - "record", - ) - - @validates("metadata") - def validate_record_protection(self, value): - """Validate the record protection value.""" - self.validate_protection_value(value, "record") - - @validates("files") - def validate_files_protection(self, value): - """Validate the files protection value.""" - self.validate_protection_value(value, "files") diff --git a/invenio_records_marc21/services/schemas/parent/access.py b/invenio_records_marc21/services/schemas/access/parent.py similarity index 67% rename from invenio_records_marc21/services/schemas/parent/access.py rename to invenio_records_marc21/services/schemas/access/parent.py index 1b6728a9..5819134f 100644 --- a/invenio_records_marc21/services/schemas/parent/access.py +++ b/invenio_records_marc21/services/schemas/access/parent.py @@ -9,13 +9,16 @@ from flask_babelex import lazy_gettext as _ from marshmallow import Schema, fields -from marshmallow_utils.fields import SanitizedUnicode +from marshmallow_utils.fields import Integer, List, SanitizedUnicode +from marshmallow_utils.fields.nestedattr import NestedAttribute + +from .embargo import EmbargoSchema class Agent(Schema): """An agent schema.""" - user = fields.Integer(required=True) + user = Integer(required=True) class ParentAccessSchema(Schema): @@ -24,4 +27,5 @@ class ParentAccessSchema(Schema): metadata = SanitizedUnicode(required=True) files = SanitizedUnicode(required=True) status = SanitizedUnicode(dump_only=False) - owned_by = fields.List(fields.Nested(Agent)) + embargo = NestedAttribute(EmbargoSchema) + owned_by = List(fields.Nested(Agent)) diff --git a/invenio_records_marc21/services/schemas/access/record.py b/invenio_records_marc21/services/schemas/access/record.py new file mode 100644 index 00000000..d3d67f7d --- /dev/null +++ b/invenio_records_marc21/services/schemas/access/record.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it +# under the terms of the MIT License; see LICENSE file for more details. + +"""Marc21 record schemas.""" + +import arrow +from flask_babelex import lazy_gettext as _ +from marshmallow import Schema, ValidationError, validates, validates_schema +from marshmallow_utils.fields import Integer, List, SanitizedUnicode +from marshmallow_utils.fields.nestedattr import NestedAttribute + +from ...components import AccessStatusEnum +from .embargo import EmbargoSchema + + +class Agent(Schema): + """An agent schema.""" + + user = Integer(required=True) + + +class AccessSchema(Schema): + """Access schema.""" + + metadata = SanitizedUnicode(required=True) + files = SanitizedUnicode(required=True) + embargo = NestedAttribute(EmbargoSchema) + status = SanitizedUnicode(dump_only=False) + owned_by = List(fields.Nested(Agent)) + + def validate_protection_value(self, value, field_name): + """Check that the protection value is valid.""" + if value not in AccessStatusEnum.list(): + raise ValidationError( + _("'{}' must be either '{}', '{}' or '{}'").format( + field_name, + *AccessStatusEnum.list(), + ), + "record", + ) + + @validates("metadata") + def validate_record_protection(self, value): + """Validate the record protection value.""" + self.validate_protection_value(value, "record") + + @validates("files") + def validate_files_protection(self, value): + """Validate the files protection value.""" + self.validate_protection_value(value, "files") diff --git a/invenio_records_marc21/services/schemas/parent/__init__.py b/invenio_records_marc21/services/schemas/parent/__init__.py deleted file mode 100644 index 43920e15..00000000 --- a/invenio_records_marc21/services/schemas/parent/__init__.py +++ /dev/null @@ -1,22 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2021 Graz University of Technology. -# -# Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it -# under the terms of the MIT License; see LICENSE file for more details. - -"""Marc21 parent record schemas.""" - -from invenio_drafts_resources.services.records.schema import ParentSchema -from marshmallow import fields - -from .access import ParentAccessSchema - - -class Marc21ParentSchema(ParentSchema): - """Record schema.""" - - access = fields.Nested(ParentAccessSchema) - - -__all__ = ("Marc21ParentSchema",) diff --git a/tests/services/conftest.py b/tests/services/conftest.py index e783354c..584ba6ce 100644 --- a/tests/services/conftest.py +++ b/tests/services/conftest.py @@ -21,8 +21,11 @@ from invenio_vocabularies.services.service import VocabulariesService from invenio_records_marc21.records import Marc21Draft -from invenio_records_marc21.services import Marc21RecordService, Metadata -from invenio_records_marc21.vocabularies import Vocabularies +from invenio_records_marc21.services import ( + Marc21RecordService, + Marc21RecordServiceConfig, + Metadata, +) @pytest.fixture(scope="module") @@ -31,15 +34,6 @@ def create_app(instance_path): return create_api -@pytest.fixture(scope="function") -def vocabulary_clear(app): - """Clears the Vocabulary singleton and pushes an application context. - - NOTE: app fixture pushes an application context - """ - Vocabularies.clear() - - @pytest.fixture() def identity_simple(): """Simple identity fixture.""" @@ -51,7 +45,7 @@ def identity_simple(): @pytest.fixture() def service(appctx): """Service instance.""" - return Marc21RecordService() + return Marc21RecordService(config=Marc21RecordServiceConfig()) @pytest.fixture() diff --git a/tests/services/test_create_record.py b/tests/services/test_create_record.py index 1564d4e5..01d976d2 100644 --- a/tests/services/test_create_record.py +++ b/tests/services/test_create_record.py @@ -14,7 +14,10 @@ from invenio_access import any_user from invenio_records_marc21.records import DraftMetadata, RecordMetadata -from invenio_records_marc21.services import Marc21RecordService +from invenio_records_marc21.services import ( + Marc21RecordService, + Marc21RecordServiceConfig, +) def _assert_fields_exists(fields, data): @@ -35,7 +38,7 @@ def marc21(): def test_create_with_service(app, marc21, identity_simple): - service = Marc21RecordService() + service = Marc21RecordService(config=Marc21RecordServiceConfig) draft = service.create(data=marc21, identity=identity_simple, access=None) @@ -127,7 +130,7 @@ def empty_data(): ) def test_create_with_access(app, empty_data, identity_simple, access): - service = Marc21RecordService() + service = Marc21RecordService(config=Marc21RecordServiceConfig) draft = service.create( data=empty_data, identity=identity_simple, access=access["input"] ) From 24b4c5962cb0c2526e8ac0947b338fe613b94106 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Thu, 20 May 2021 11:59:34 +0200 Subject: [PATCH 020/217] bugfix: move record file below record --- invenio_records_marc21/records/api.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/invenio_records_marc21/records/api.py b/invenio_records_marc21/records/api.py index d0d9b459..26d6a5e3 100644 --- a/invenio_records_marc21/records/api.py +++ b/invenio_records_marc21/records/api.py @@ -57,13 +57,6 @@ class Marc21Parent(BaseParentRecord): ) -class DraftFile(BaseFileRecord): - """Marc21 file associated with a marc21 draft model.""" - - model_cls = DraftFile - record_cls = LocalProxy(lambda: Marc21Draft) - - class Marc21Draft(Draft): """Marc21 draft API.""" @@ -96,11 +89,11 @@ class Marc21Draft(Draft): bucket = ModelField(dump=False) -class RecordFile(BaseFileRecord): - """Marc21 record file API.""" +class DraftFile(BaseFileRecord): + """Marc21 file associated with a marc21 draft model.""" - model_cls = RecordFile - record_cls = LocalProxy(lambda: Marc21Record) + model_cls = DraftFile + record_cls = LocalProxy(lambda: Marc21Draft) class Marc21Record(Record): @@ -134,3 +127,10 @@ class Marc21Record(Record): bucket_id = ModelField(dump=False) bucket = ModelField(dump=False) + + +class RecordFile(BaseFileRecord): + """Marc21 record file API.""" + + model_cls = RecordFile + record_cls = LocalProxy(lambda: Marc21Record) From 8a193fed4b81c5d11d8cf9e43cedbb5554eeabe5 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Thu, 20 May 2021 12:00:59 +0200 Subject: [PATCH 021/217] modification: add parent schema to init file --- .../services/schemas/__init__.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/invenio_records_marc21/services/schemas/__init__.py b/invenio_records_marc21/services/schemas/__init__.py index 2db41f1a..7e3f286f 100644 --- a/invenio_records_marc21/services/schemas/__init__.py +++ b/invenio_records_marc21/services/schemas/__init__.py @@ -7,18 +7,24 @@ """Marc21 record schemas.""" +from invenio_drafts_resources.services.records.schema import ParentSchema from invenio_records_resources.services.records.schema import BaseRecordSchema from marshmallow import EXCLUDE, INCLUDE, Schema, fields, missing, post_dump from marshmallow_utils.fields import NestedAttribute -from .access import AccessSchema +from .access import AccessSchema, ParentAccessSchema from .files import FilesSchema from .metadata import MetadataSchema -from .parent import Marc21ParentSchema from .pids import PIDSchema from .versions import VersionsSchema +class Marc21ParentSchema(ParentSchema): + """Record schema.""" + + access = fields.Nested(ParentAccessSchema) + + class Marc21RecordSchema(BaseRecordSchema): """Record schema.""" @@ -58,4 +64,7 @@ def default_nested(self, data, many, **kwargs): return data -__all__ = ("Marc21RecordSchema",) +__all__ = ( + "Marc21RecordSchema", + "Marc21ParentSchema", +) From b6feea84033f00f44ab0fa7fda5dd79025539a4d Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Thu, 20 May 2021 12:02:46 +0200 Subject: [PATCH 022/217] bugfix(test): removing conceptid in testset --- tests/records/test_jsonschema.py | 12 +++--------- tests/services/test_record_service.py | 13 ++----------- 2 files changed, 5 insertions(+), 20 deletions(-) diff --git a/tests/records/test_jsonschema.py b/tests/records/test_jsonschema.py index 7364ff49..a7fdf462 100644 --- a/tests/records/test_jsonschema.py +++ b/tests/records/test_jsonschema.py @@ -86,15 +86,9 @@ def test_id(appctx): assert fails({"id": 1}) -def test_conceptid(appctx): - """Test conceptid.""" - assert validates({"conceptid": "12345-abcd"}) - assert fails({"conceptid": {"id": "val"}}) - - -@pytest.mark.parametrize("prop", ["pid", "conceptpid"]) -def test_pid_conceptpid(appctx, prop): - """Test pid/conceptpid.""" +@pytest.mark.parametrize("prop", ["pid"]) +def test_pid(appctx, prop): + """Test pid.""" pid = { "pk": 1, "status": "R", diff --git a/tests/services/test_record_service.py b/tests/services/test_record_service.py index 72cdb436..bce3709b 100755 --- a/tests/services/test_record_service.py +++ b/tests/services/test_record_service.py @@ -36,13 +36,11 @@ def test_create_draft(app, service, identity_simple, metadata): assert draft.id # files attribute in record causes at create change the revision_id twice - assert draft._record.revision_id == 2 + assert draft._record.revision_id == 3 # Check for pid and parent pid assert draft["id"] - assert draft["conceptid"] assert draft._record.pid.status == PIDStatus.NEW - assert draft._record.conceptpid.status == PIDStatus.NEW def test_create_empty_draft(app, service, identity_simple): @@ -56,9 +54,7 @@ def test_create_empty_draft(app, service, identity_simple): draft_dict = draft.to_dict() assert draft["id"] - assert draft["conceptid"] assert draft._record.pid.status == PIDStatus.NEW - assert draft._record.conceptpid.status == PIDStatus.NEW def test_read_draft(app, service, identity_simple, metadata): @@ -103,7 +99,6 @@ def test_publish_draft(app, service, identity_simple, metadata): """ record = _create_and_publish(service, metadata, identity_simple) assert record._record.pid.status == PIDStatus.REGISTERED - assert record._record.conceptpid.status == PIDStatus.REGISTERED # Check draft deleted with pytest.raises(NoResultFound): @@ -114,7 +109,6 @@ def test_publish_draft(app, service, identity_simple, metadata): assert record.id assert record._record.pid.status == PIDStatus.REGISTERED - assert record._record.conceptpid.status == PIDStatus.REGISTERED def _test_metadata(metadata, metadata2): @@ -192,18 +186,15 @@ def test_create_publish_new_version(app, service, identity_simple, metadata): # files attribute in record causes at create change the revision_id twice assert draft._record.revision_id == 2 - assert draft["conceptid"] == record["conceptid"] assert draft["id"] != record["id"] assert draft._record.pid.status == PIDStatus.NEW - assert draft._record.conceptpid.status == PIDStatus.REGISTERED # Publish it record_2 = service.publish(draft.id, identity_simple) assert record_2.id assert record_2._record.pid.status == PIDStatus.REGISTERED - assert record_2._record.conceptpid.status == PIDStatus.REGISTERED + # files attribute in record causes at create change the revision_id twice assert record_2._record.revision_id == 1 - assert record_2["conceptid"] == record["conceptid"] assert record_2["id"] != record["id"] From 3b5e34ca4cf19ef68d8d7d41ce024fb9b257d6b6 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Thu, 20 May 2021 12:08:00 +0200 Subject: [PATCH 023/217] modification: removing unused vocabulary functions --- tests/records/conftest.py | 6 ------ tests/services/conftest.py | 2 -- 2 files changed, 8 deletions(-) diff --git a/tests/records/conftest.py b/tests/records/conftest.py index caa739a7..c391a8c7 100644 --- a/tests/records/conftest.py +++ b/tests/records/conftest.py @@ -15,8 +15,6 @@ import pytest from invenio_app.factory import create_api -from invenio_records_marc21.vocabularies import Vocabularies - @pytest.fixture(scope="module") def create_app(instance_path): @@ -24,7 +22,3 @@ def create_app(instance_path): return create_api -@pytest.fixture(scope="function") -def vocabulary_clear(appctx): - """Clears the Vocabulary singleton and pushes an appctx.""" - Vocabularies.clear() diff --git a/tests/services/conftest.py b/tests/services/conftest.py index 584ba6ce..1acd1dff 100644 --- a/tests/services/conftest.py +++ b/tests/services/conftest.py @@ -17,8 +17,6 @@ from flask_principal import Identity from invenio_access import any_user from invenio_app.factory import create_api -from invenio_vocabularies.records.models import VocabularyType -from invenio_vocabularies.services.service import VocabulariesService from invenio_records_marc21.records import Marc21Draft from invenio_records_marc21.services import ( From ebf80c14ed2d0915ecfd5e831c355c88f33b4028 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Thu, 20 May 2021 12:11:11 +0200 Subject: [PATCH 024/217] bugfix(tests): using local jsonschema resolver --- tests/conftest.py | 18 +++++++++++++++++- tests/records/fields/conftest.py | 10 ++++++++-- tests/records/test_jsonschema.py | 2 +- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 6b4d1b4b..c6c9b6ff 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -33,7 +33,23 @@ def celery_config(): return {} -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") +def app_config(app_config): + """Application config fixture.""" + app_config[ + "RECORDS_REFRESOLVER_CLS" + ] = "invenio_records.resolver.InvenioRefResolver" + app_config[ + "RECORDS_REFRESOLVER_STORE" + ] = "invenio_jsonschemas.proxies.current_refresolver_store" + + # Variable not used. We set it to silent warnings + app_config["JSONSCHEMAS_HOST"] = "not-used" + + return app_config + + +@pytest.fixture(scope="module") def app(base_app, database): """Application with just a database. diff --git a/tests/records/fields/conftest.py b/tests/records/fields/conftest.py index 2e68ff04..4dadbf2f 100644 --- a/tests/records/fields/conftest.py +++ b/tests/records/fields/conftest.py @@ -19,13 +19,16 @@ from invenio_db import InvenioDB from invenio_files_rest import InvenioFilesREST from invenio_files_rest.models import Location +from invenio_jsonschemas import InvenioJSONSchemas +from invenio_records import InvenioRecords from invenio_records_marc21 import InvenioRecordsMARC21 -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def create_app(instance_path): """Application factory fixture for use with pytest-invenio.""" + def _create_app(**config): app_ = Flask( __name__, @@ -36,10 +39,11 @@ def _create_app(**config): InvenioFilesREST(app_) InvenioRecordsMARC21(app_) return app_ + return _create_app -@pytest.fixture(scope='module') +@pytest.fixture(scope="module") def testapp(base_app, database): """Application with just a database. @@ -51,4 +55,6 @@ def testapp(base_app, database): database.session.add(location_obj) database.session.commit() + InvenioRecords(base_app) + InvenioJSONSchemas(base_app) yield base_app diff --git a/tests/records/test_jsonschema.py b/tests/records/test_jsonschema.py index a7fdf462..55c6c14c 100644 --- a/tests/records/test_jsonschema.py +++ b/tests/records/test_jsonschema.py @@ -23,7 +23,7 @@ # def validates(data): """Assertion function used to validate according to the schema.""" - data["$schema"] = "https://localhost/schemas/marc21/marc21-v1.0.0.json" + data["$schema"] = "local://marc21/marc21-v1.0.0.json" Record(data).validate() return True From 46e241f34e70c8e3c0dbfbe90c35422d01e280ad Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Thu, 20 May 2021 12:13:01 +0200 Subject: [PATCH 025/217] modification: remove component file components moved into own module --- invenio_records_marc21/services/components.py | 40 ------------------- 1 file changed, 40 deletions(-) delete mode 100644 invenio_records_marc21/services/components.py diff --git a/invenio_records_marc21/services/components.py b/invenio_records_marc21/services/components.py deleted file mode 100644 index da8d43a0..00000000 --- a/invenio_records_marc21/services/components.py +++ /dev/null @@ -1,40 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2021 Graz University of Technology. -# -# Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it -# under the terms of the MIT License; see LICENSE file for more details. - - -"""Marc21 service components.""" - -from invenio_drafts_resources.services.records.components import ( - PIDComponent as BasePIDComponent, -) -from invenio_records_resources.services.records.components import ServiceComponent - - -class AccessComponent(ServiceComponent): - """Service component for access integration.""" - - def create(self, identity, data=None, record=None, **kwargs): - """Add basic ownership fields to the record.""" - validated_data = data.get("access", {}) - if identity.id: - validated_data.setdefault("owned_by", [{"user": identity.id}]) - record.update({"access": validated_data}) - - def update(self, identity, data=None, record=None, **kwargs): - """Update handler.""" - validated_data = data.get("access", {}) - if identity.id: - validated_data.setdefault("owned_by", [{"user": identity.id}]) - record.update({"access": validated_data}) - - -class PIDComponent(BasePIDComponent): - """Service component for pids integration.""" - - def create(self, identity, data=None, record=None, **kwargs): - """Create PID when record is created..""" - self.service.record_cls.pid.create(record) From 51e999b23b50f9cc5a98c974135011dee62f9c25 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Thu, 20 May 2021 12:22:36 +0200 Subject: [PATCH 026/217] modification(tests): tests clean up --- tests/conftest.py | 1 - .../records/fields/test_systemfield_files.py | 6 ++--- tests/records/test-record.json | 26 +++++++++---------- tests/services/test_create_record.py | 3 --- 4 files changed, 15 insertions(+), 21 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index c6c9b6ff..1619ee27 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,7 +12,6 @@ fixtures are available. """ -import shutil import tempfile import pytest diff --git a/tests/records/fields/test_systemfield_files.py b/tests/records/fields/test_systemfield_files.py index 3971215d..fc9f68a8 100644 --- a/tests/records/fields/test_systemfield_files.py +++ b/tests/records/fields/test_systemfield_files.py @@ -10,7 +10,7 @@ from io import BytesIO -from invenio_files_rest.models import Bucket, FileInstance, Location, ObjectVersion +from invenio_files_rest.models import Bucket, FileInstance, ObjectVersion from invenio_records.systemfields import ModelField from invenio_records_resources.records.systemfields.files import FilesField @@ -100,7 +100,7 @@ def test_record_file_update(testapp, db): assert models.RecordFile.query.count() == 0 assert FileInstance.query.count() == 1 - assert ObjectVersion.query.count() == 2 + assert ObjectVersion.query.count() == 0 assert Bucket.query.count() == 1 assert len(record.files) == 0 assert "test.pdf" not in record.files @@ -145,7 +145,7 @@ def test_record_files_clear(testapp, db): assert models.RecordFile.query.count() == 0 assert FileInstance.query.count() == 2 - assert ObjectVersion.query.count() == 4 + assert ObjectVersion.query.count() == 0 assert Bucket.query.count() == 1 assert len(record.files) == 0 assert "f1.pdf" not in record.files diff --git a/tests/records/test-record.json b/tests/records/test-record.json index 02bd9aff..12d5c6d5 100644 --- a/tests/records/test-record.json +++ b/tests/records/test-record.json @@ -1,16 +1,14 @@ { - "access": { - "owned_by": [{ - "user": 1 - }], - "embargo_date": "2022-02-28", - "access_right": "open", - "access_condition": { - "condition": "Medical doctors.", - "default_link_validity": 30 - } - }, - "metadata": { - "xml": "990079940640203331 AT-OBV20170703041800.0cr 100504|1932AC08088803AC08088803" - } + "access": { + "owned_by": [ + { + "user": 1 + } + ], + "metadata": "public", + "files": "public" + }, + "metadata": { + "xml": "990079940640203331 AT-OBV20170703041800.0cr 100504|1932AC08088803AC08088803" + } } diff --git a/tests/services/test_create_record.py b/tests/services/test_create_record.py index 01d976d2..da3fbe7a 100644 --- a/tests/services/test_create_record.py +++ b/tests/services/test_create_record.py @@ -10,10 +10,7 @@ from datetime import date, timedelta import pytest -from flask_principal import Identity -from invenio_access import any_user -from invenio_records_marc21.records import DraftMetadata, RecordMetadata from invenio_records_marc21.services import ( Marc21RecordService, Marc21RecordServiceConfig, From ccbdb6f834ce3b8864a51ff2541a95a3cfd34954 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Thu, 20 May 2021 12:40:38 +0200 Subject: [PATCH 027/217] bugfix: fields marshmallow instead of marshmallow_utils --- invenio_records_marc21/services/schemas/access/embargo.py | 3 ++- invenio_records_marc21/services/schemas/access/parent.py | 3 ++- invenio_records_marc21/services/schemas/access/record.py | 5 +++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/invenio_records_marc21/services/schemas/access/embargo.py b/invenio_records_marc21/services/schemas/access/embargo.py index 2d096d97..6769cc43 100644 --- a/invenio_records_marc21/services/schemas/access/embargo.py +++ b/invenio_records_marc21/services/schemas/access/embargo.py @@ -10,7 +10,8 @@ import arrow from flask_babelex import lazy_gettext as _ from marshmallow import Schema, ValidationError, validates_schema -from marshmallow_utils.fields import Bool, ISODateString, SanitizedUnicode +from marshmallow.fields import Bool +from marshmallow_utils.fields import ISODateString, SanitizedUnicode class EmbargoSchema(Schema): diff --git a/invenio_records_marc21/services/schemas/access/parent.py b/invenio_records_marc21/services/schemas/access/parent.py index 5819134f..fb664b6a 100644 --- a/invenio_records_marc21/services/schemas/access/parent.py +++ b/invenio_records_marc21/services/schemas/access/parent.py @@ -9,7 +9,8 @@ from flask_babelex import lazy_gettext as _ from marshmallow import Schema, fields -from marshmallow_utils.fields import Integer, List, SanitizedUnicode +from marshmallow.fields import Integer, List +from marshmallow_utils.fields import SanitizedUnicode from marshmallow_utils.fields.nestedattr import NestedAttribute from .embargo import EmbargoSchema diff --git a/invenio_records_marc21/services/schemas/access/record.py b/invenio_records_marc21/services/schemas/access/record.py index d3d67f7d..0295c7e7 100644 --- a/invenio_records_marc21/services/schemas/access/record.py +++ b/invenio_records_marc21/services/schemas/access/record.py @@ -10,7 +10,8 @@ import arrow from flask_babelex import lazy_gettext as _ from marshmallow import Schema, ValidationError, validates, validates_schema -from marshmallow_utils.fields import Integer, List, SanitizedUnicode +from marshmallow.fields import Integer, List, Nested +from marshmallow_utils.fields import SanitizedUnicode from marshmallow_utils.fields.nestedattr import NestedAttribute from ...components import AccessStatusEnum @@ -30,7 +31,7 @@ class AccessSchema(Schema): files = SanitizedUnicode(required=True) embargo = NestedAttribute(EmbargoSchema) status = SanitizedUnicode(dump_only=False) - owned_by = List(fields.Nested(Agent)) + owned_by = List(Nested(Agent)) def validate_protection_value(self, value, field_name): """Check that the protection value is valid.""" From ee50435c8ae5de5156243f70aa76bf5a211353e3 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Thu, 20 May 2021 13:06:35 +0200 Subject: [PATCH 028/217] modification(tests): update root keys --- tests/services/test_create_record.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/services/test_create_record.py b/tests/services/test_create_record.py index da3fbe7a..f1a03e7a 100644 --- a/tests/services/test_create_record.py +++ b/tests/services/test_create_record.py @@ -41,21 +41,26 @@ def test_create_with_service(app, marc21, identity_simple): root_fields = [ "id", - "conceptid", + "versions", + "links", + "is_published", + "parent", + "revision_id", "created", "updated", "metadata", - "access", ] expected = {"metadata": {}} _assert_fields_exists(root_fields, draft.data) _assert_fields(["metadata"], draft.data, expected) + assert not draft["is_published"] record = service.publish(id_=draft.id, identity=identity_simple) assert record _assert_fields_exists(root_fields, record.data) _assert_fields(["metadata"], record.data, expected) + assert record["is_published"] @pytest.fixture() From 68172ca24fe48a7b2a08e7fc95cd810e98262d9b Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Thu, 20 May 2021 14:25:36 +0200 Subject: [PATCH 029/217] modification: clean up unused code --- .../resources/serializers/ui/schema.py | 9 -------- .../services/components/access.py | 3 ++- .../services/schemas/utils.py | 22 +++++++++---------- tests/records/conftest.py | 2 -- tests/services/test_record_service.py | 12 ++++------ 5 files changed, 17 insertions(+), 31 deletions(-) mode change 100755 => 100644 invenio_records_marc21/resources/serializers/ui/schema.py diff --git a/invenio_records_marc21/resources/serializers/ui/schema.py b/invenio_records_marc21/resources/serializers/ui/schema.py old mode 100755 new mode 100644 index 585f4c23..bc8684b2 --- a/invenio_records_marc21/resources/serializers/ui/schema.py +++ b/invenio_records_marc21/resources/serializers/ui/schema.py @@ -82,15 +82,6 @@ def get_aggs(self, obj_list): if not aggs: return missing - for name, agg in aggs.items(): - vocab = Vocabularies.get_vocabulary(name) - if not vocab: - continue - - buckets = agg.get("buckets") - if buckets: - apply_labels(vocab, buckets) - return aggs diff --git a/invenio_records_marc21/services/components/access.py b/invenio_records_marc21/services/components/access.py index 1994d763..c79196fb 100644 --- a/invenio_records_marc21/services/components/access.py +++ b/invenio_records_marc21/services/components/access.py @@ -25,6 +25,7 @@ class AccessStatusEnum(Enum): @staticmethod def list(): + """List all access statuses.""" return list(map(lambda c: c.value, AccessStatusEnum)) @@ -51,7 +52,7 @@ def _default_access(self, identity, data, record, **kwargs): def _init_owned_by(self, identity, record, **kwargs): """Initialize the owned by atribute in access component.""" access_data = record.parent.get("access", {}) - if not "owned_by" in access_data and identity.id: + if "owned_by" not in access_data and identity.id: access_data.setdefault("owned_by", [{"user": identity.id}]) record.parent.update(access_data) diff --git a/invenio_records_marc21/services/schemas/utils.py b/invenio_records_marc21/services/schemas/utils.py index be9148ec..aa5748c3 100644 --- a/invenio_records_marc21/services/schemas/utils.py +++ b/invenio_records_marc21/services/schemas/utils.py @@ -11,18 +11,18 @@ from marshmallow.schema import SchemaMeta from marshmallow_utils.fields import NestedAttribute -from ...vocabularies import Vocabularies +# from ...vocabularies import Vocabularies -def validate_entry(vocabulary_key, entry_key): - """Validates if an entry is valid for a vocabulary. +# def validate_entry(vocabulary_key, entry_key): +# """Validates if an entry is valid for a vocabulary. - :param vocabulary_key: str, Vocabulary key - :param entry_key: str, specific entry key +# :param vocabulary_key: str, Vocabulary key +# :param entry_key: str, specific entry key - raises marshmallow.ValidationError if entry is not valid. - """ - vocabulary = Vocabularies.get_vocabulary(vocabulary_key) - obj = vocabulary.get_entry_by_dict(entry_key) - if not obj: - raise ValidationError(vocabulary.get_invalid(entry_key)) +# raises marshmallow.ValidationError if entry is not valid. +# """ +# vocabulary = Vocabularies.get_vocabulary(vocabulary_key) +# obj = vocabulary.get_entry_by_dict(entry_key) +# if not obj: +# raise ValidationError(vocabulary.get_invalid(entry_key)) diff --git a/tests/records/conftest.py b/tests/records/conftest.py index c391a8c7..91b57aa8 100644 --- a/tests/records/conftest.py +++ b/tests/records/conftest.py @@ -20,5 +20,3 @@ def create_app(instance_path): """Application factory fixture.""" return create_api - - diff --git a/tests/services/test_record_service.py b/tests/services/test_record_service.py index bce3709b..39149d64 100755 --- a/tests/services/test_record_service.py +++ b/tests/services/test_record_service.py @@ -126,10 +126,6 @@ def test_update_draft(app, service, identity_simple, metadata, metadata2): draft.id, identity=identity_simple, metadata=metadata2 ) - assert draft.id == update_draft.id - _test_metadata( - to_marc21.do(update_draft["metadata"]["json"]), create_record(metadata.xml) - ) # Check the updates where savedif "json" in data: read_draft = service.read_draft(id_=draft.id, identity=identity_simple) @@ -154,13 +150,13 @@ def test_mutiple_edit(base_app, service, identity_simple, metadata): assert draft.id == marcid assert draft._record.fork_version_id == record._record.revision_id # files attribute in record causes at create change the revision_id twice - assert draft._record.revision_id == 5 + assert draft._record.revision_id == 6 draft = service.edit(marcid, identity_simple) assert draft.id == marcid assert draft._record.fork_version_id == record._record.revision_id # files attribute in record causes at create change the revision_id twice - assert draft._record.revision_id == 5 + assert draft._record.revision_id == 6 # Publish it to check the increment in version_id record = service.publish(marcid, identity_simple) @@ -170,7 +166,7 @@ def test_mutiple_edit(base_app, service, identity_simple, metadata): assert draft._record.fork_version_id == record._record.revision_id # files attribute in record causes at create change the revision_id twice # create(2), soft-delete, undelete, update - assert draft._record.revision_id == 8 + assert draft._record.revision_id == 9 def test_create_publish_new_version(app, service, identity_simple, metadata): @@ -185,7 +181,7 @@ def test_create_publish_new_version(app, service, identity_simple, metadata): draft = service.new_version(marcid, identity_simple) # files attribute in record causes at create change the revision_id twice - assert draft._record.revision_id == 2 + assert draft._record.revision_id == 3 assert draft["id"] != record["id"] assert draft._record.pid.status == PIDStatus.NEW From a64a43841d8d6b4aa8d3eb8186cdaf207d5b4575 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Thu, 20 May 2021 14:28:00 +0200 Subject: [PATCH 030/217] modification: build api endpoints build api endpoints with each entrypoint --- invenio_records_marc21/config.py | 12 +++-- invenio_records_marc21/ui/records/__init__.py | 4 +- invenio_records_marc21/ui/theme/__init__.py | 4 +- invenio_records_marc21/views.py | 45 ++++++++----------- setup.py | 6 ++- tests/conftest.py | 4 +- 6 files changed, 37 insertions(+), 38 deletions(-) diff --git a/invenio_records_marc21/config.py b/invenio_records_marc21/config.py index 29513c32..c1d0ee79 100644 --- a/invenio_records_marc21/config.py +++ b/invenio_records_marc21/config.py @@ -24,11 +24,15 @@ } INVENIO_MARC21_UI_ENDPOINTS = { - "index": "/marc21/", - "record_search": "/marc21/search", - "record_detail": "/marc21/", - "record_export": "/marc21//export/", + "record-detail": "/", + "record-export": "//export/", } + +INVENIO_MARC21_UI_THEME_ENDPOINTS = { + "index": "/", + "record-search": "/search", +} + """Records UI for invenio-records-marc21.""" SEARCH_UI_JSTEMPLATE_RESULTS = "templates/invenio_records_marc21/results.html" diff --git a/invenio_records_marc21/ui/records/__init__.py b/invenio_records_marc21/ui/records/__init__.py index b44d3a99..7a06dc84 100644 --- a/invenio_records_marc21/ui/records/__init__.py +++ b/invenio_records_marc21/ui/records/__init__.py @@ -28,12 +28,12 @@ def init_records_views(blueprint, app): # Record URL rules blueprint.add_url_rule( - routes["record_detail"], + routes["record-detail"], view_func=record_detail, ) blueprint.add_url_rule( - routes["record_export"], + routes["record-export"], view_func=record_export, ) diff --git a/invenio_records_marc21/ui/theme/__init__.py b/invenio_records_marc21/ui/theme/__init__.py index 79474cf8..2ae71329 100755 --- a/invenio_records_marc21/ui/theme/__init__.py +++ b/invenio_records_marc21/ui/theme/__init__.py @@ -19,9 +19,9 @@ # def init_theme_views(blueprint, app): """Blueprint for the routes and resources provided by Invenio-Records-Marc21.""" - routes = app.config.get("INVENIO_MARC21_UI_ENDPOINTS") + routes = app.config.get("INVENIO_MARC21_UI_THEME_ENDPOINTS") blueprint.add_url_rule(routes["index"], view_func=index) - blueprint.add_url_rule(routes["record_search"], view_func=search) + blueprint.add_url_rule(routes["record-search"], view_func=search) return blueprint diff --git a/invenio_records_marc21/views.py b/invenio_records_marc21/views.py index 4318d5ab..ca7264e1 100644 --- a/invenio_records_marc21/views.py +++ b/invenio_records_marc21/views.py @@ -9,33 +9,26 @@ from __future__ import absolute_import, print_function -from flask import Blueprint -blueprint = Blueprint( - "invenio_records_marc21", - __name__, - template_folder="templates", - static_folder="static", -) -"""Blueprint used for loading templates and static assets +def create_record_bp(app): + """Create records blueprint.""" + ext = app.extensions["invenio-records-marc21"] + return ext.record_resource.as_blueprint() -The sole purpose of this blueprint is to ensure that Invenio can find the -templates and static files located in the folders of the same names next to -this file. -""" +def create_record_files_bp(app): + """Create records files blueprint.""" + ext = app.extensions["invenio-records-marc21"] + return ext.record_files_resource.as_blueprint() -def create_records_blueprint(app): - """Create records blueprint.""" - ext = app.extensions - return ext["invenio-records-marc21"].records_resource.as_blueprint( - "marc21_records_resource" - ) - - -def create_drafts_blueprint(app): - """Create drafts blueprint.""" - ext = app.extensions - return ext["invenio-records-marc21"].drafts_resource.as_blueprint( - "marc21_draft_resource" - ) + +def create_draft_files_bp(app): + """Create draft files blueprint.""" + ext = app.extensions["invenio-records-marc21"] + return ext.draft_files_resource.as_blueprint() + + +def create_parent_record_links_bp(app): + """Create parent record links blueprint.""" + ext = app.extensions["invenio-records-marc21"] + return ext.parent_record_links_resource.as_blueprint() diff --git a/setup.py b/setup.py index 2bba7121..8c9a30e2 100755 --- a/setup.py +++ b/setup.py @@ -92,8 +92,10 @@ "invenio_records_marc21 = invenio_records_marc21:InvenioRecordsMARC21", ], "invenio_base.api_blueprints": [ - "invenio_records_marc21_record = invenio_records_marc21.views:create_records_blueprint", - "invenio_records_marc21_draft = invenio_records_marc21.views:create_drafts_blueprint", + "invenio_records_marc21_record = invenio_records_marc21.views:create_record_bp", + "invenio_records_marc21_record_files = invenio_records_marc21.views:create_record_files_bp", + "invenio_records_marc21_draft_files = invenio_records_marc21.views:create_draft_files_bp", + "invenio_records_marc21_parent_links = invenio_records_marc21.views:create_parent_record_links_bp", ], "invenio_base.blueprints": [ "invenio_records_marc21_ui = invenio_records_marc21.ui:create_blueprint", diff --git a/tests/conftest.py b/tests/conftest.py index 1619ee27..3abcb855 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -20,7 +20,7 @@ from invenio_files_rest.models import Location from invenio_records_marc21 import InvenioRecordsMARC21 -from invenio_records_marc21.views import blueprint +from invenio_records_marc21.views import create_record_bp @pytest.fixture(scope="module") @@ -72,7 +72,7 @@ def factory(**config): app.config.update(**config) Babel(app) InvenioRecordsMARC21(app) - app.register_blueprint(blueprint) + create_record_bp(app) return app return factory From b8d70afb733a1712dca1743f2a8cc2feca980565 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Thu, 20 May 2021 14:39:45 +0200 Subject: [PATCH 031/217] bugfix: import external modules --- invenio_records_marc21/ext.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/invenio_records_marc21/ext.py b/invenio_records_marc21/ext.py index 62c40e52..5367fc42 100755 --- a/invenio_records_marc21/ext.py +++ b/invenio_records_marc21/ext.py @@ -9,6 +9,11 @@ from __future__ import absolute_import, print_function +import six +from invenio_records_resources.resources import FileResource +from invenio_records_resources.services import FileService +from werkzeug.utils import import_string + from . import config from .services import ( Marc21DraftFilesServiceConfig, @@ -19,6 +24,19 @@ ) +def obj_or_import_string(value, default=None): + """Import string or return object. + + :params value: Import path or class object to instantiate. + :params default: Default object to return if the import fails. + :returns: The imported object. + """ + if isinstance(value, six.string_types): + return import_string(value) + elif value: + return value + return default + class InvenioRecordsMARC21(object): """Invenio-Records-Marc21 extension.""" From 1c7bcbea19cf658f3d3ac954ac8faee5c6699f50 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Thu, 20 May 2021 14:43:57 +0200 Subject: [PATCH 032/217] modification: update resources update record resources and load in ext. configuring endpoints and update required record permission --- invenio_records_marc21/ext.py | 29 +++- invenio_records_marc21/resources/__init__.py | 14 +- invenio_records_marc21/resources/config.py | 133 ++++++++++-------- invenio_records_marc21/resources/resources.py | 41 ++++-- invenio_records_marc21/services/config.py | 13 +- .../services/permissions.py | 8 ++ .../invenio_records_marc21/search.html | 2 +- invenio_records_marc21/ui/__init__.py | 4 +- .../ui/records/decorators.py | 12 +- tests/services/test_create_record.py | 1 + 10 files changed, 173 insertions(+), 84 deletions(-) diff --git a/invenio_records_marc21/ext.py b/invenio_records_marc21/ext.py index 5367fc42..94d8b69c 100755 --- a/invenio_records_marc21/ext.py +++ b/invenio_records_marc21/ext.py @@ -15,6 +15,14 @@ from werkzeug.utils import import_string from . import config +from .resources import ( + Marc21DraftFilesResourceConfig, + Marc21ParentRecordLinksResource, + Marc21ParentRecordLinksResourceConfig, + Marc21RecordFilesResourceConfig, + Marc21RecordResource, + Marc21RecordResourceConfig, +) from .services import ( Marc21DraftFilesServiceConfig, Marc21RecordFilesServiceConfig, @@ -50,6 +58,7 @@ def init_app(self, app): """Flask application initialization.""" self.init_config(app) self.init_services(app) + self.init_resources(app) app.extensions["invenio-records-marc21"] = self def init_config(self, app): @@ -85,12 +94,22 @@ def init_services(self, app): draft_files_service=FileService(Marc21DraftFilesServiceConfig), ) - self.records_resource = Marc21RecordResource( + def init_resources(self, app): + """Initialize resources.""" + self.record_resource = Marc21RecordResource( service=self.records_service, - config=app.config.get(Marc21RecordResource.config_name), + config=Marc21RecordResourceConfig, ) - self.drafts_resource = Marc21DraftResource( - service=self.records_service, - config=app.config.get(Marc21DraftResource.config_name), + self.record_files_resource = FileResource( + service=self.records_service.files, config=Marc21RecordFilesResourceConfig + ) + + self.draft_files_resource = FileResource( + service=self.records_service.draft_files, + config=Marc21DraftFilesResourceConfig, + ) + + self.parent_record_links_resource = Marc21ParentRecordLinksResource( + service=self.records_service, config=Marc21ParentRecordLinksResourceConfig ) diff --git a/invenio_records_marc21/resources/__init__.py b/invenio_records_marc21/resources/__init__.py index aea627eb..c9ac74f2 100755 --- a/invenio_records_marc21/resources/__init__.py +++ b/invenio_records_marc21/resources/__init__.py @@ -8,9 +8,19 @@ """Invenio Marc21 module to create REST APIs.""" -from .resources import Marc21DraftResource, Marc21RecordResource +from .config import ( + Marc21DraftFilesResourceConfig, + Marc21ParentRecordLinksResourceConfig, + Marc21RecordFilesResourceConfig, + Marc21RecordResourceConfig, +) +from .resources import Marc21ParentRecordLinksResource, Marc21RecordResource __all__ = ( - "Marc21DraftResource", "Marc21RecordResource", + "Marc21DraftFilesResourceConfig", + "Marc21RecordFilesResourceConfig", + "Marc21RecordResourceConfig", + "Marc21ParentRecordLinksResourceConfig", + "Marc21ParentRecordLinksResource", ) diff --git a/invenio_records_marc21/resources/config.py b/invenio_records_marc21/resources/config.py index 72e177d2..9ba602b8 100755 --- a/invenio_records_marc21/resources/config.py +++ b/invenio_records_marc21/resources/config.py @@ -7,85 +7,104 @@ """Resources configuration.""" - -from flask_resources.serializers import JSONSerializer -from invenio_drafts_resources.resources import DraftResourceConfig, RecordResourceConfig -from invenio_records_resources.resources import RecordResponse -from invenio_records_resources.resources.records.schemas_links import ( - ItemLink, - ItemLinksSchema, - SearchLinksSchema, +import marshmallow as ma +from flask_resources import ( + JSONDeserializer, + JSONSerializer, + RequestBodyParser, + ResponseHandler, ) +from flask_resources.serializers import JSONSerializer +from invenio_drafts_resources.resources import RecordResourceConfig +from invenio_records_resources.resources.files import FileResourceConfig +from invenio_records_resources.resources.records.args import SearchRequestArgsSchema from .serializers.ui import UIJSONSerializer -# -# Links -# -RecordLinks = ItemLinksSchema.create( - links={ - "self": ItemLink(template="/api/marc21/{pid_value}"), - "self_html": ItemLink(template="/marc21/{pid_value}"), - } -) +record_serializer = { + "application/json": ResponseHandler(UIJSONSerializer()), + "application/vnd.inveniomarc21.v1+json": ResponseHandler(UIJSONSerializer()), +} +url_prefix = "/marc21" + +record_ui_routes = { + "search" : f"{url_prefix}", + "list": f"{url_prefix}/list", + "item": f"{url_prefix}/", + "item-versions": f"{url_prefix}//versions", + "item-latest": f"{url_prefix}//versions/latest", + "item-draft": f"{url_prefix}//draft", + "item-publish": f"{url_prefix}//draft/actions/publish", + "item-files-import": f"{url_prefix}//draft/actions/files-import", +} -DraftLinks = ItemLinksSchema.create( - links={ - "self": ItemLink(template="/api/marc21/{pid_value}/draft"), - "self_html": ItemLink(template="/marc21/uploads/{pid_value}"), - "publish": ItemLink( - template="/api/marc21/{pid_value}/draft/actions/publish", - permission="publish", - ), - } -) +class Marc21RecordResourceConfig(RecordResourceConfig): + """Marc21 Record resource configuration.""" -SearchLinks = SearchLinksSchema.create(template="/api/marc21{?params*}") + blueprint_name = "marc21_records" + url_prefix = url_prefix -# -# Response handlers -# -record_serializers = { - "application/json": RecordResponse(UIJSONSerializer()), - "application/vnd.inveniomarc21.v1+json": RecordResponse(UIJSONSerializer()), -} + default_accept_mimetype = "application/json" + response_handlers = record_serializer -# -# marc21 -# -class Marc21RecordResourceConfig(RecordResourceConfig): - """Record resource configuration.""" + request_view_args = { + "pid_value": ma.fields.Str(), + "pid_type": ma.fields.Str(), + } + links_config = {} - list_route = "/marc21" + routes = record_ui_routes - item_route = "/marc21/" + # Request parsing + request_args = SearchRequestArgsSchema + request_view_args = {"pid_value": ma.fields.Str()} + request_headers = {"if_match": ma.fields.Int()} + request_body_parsers = {"application/json": RequestBodyParser(JSONDeserializer())} - links_config = { - "record": RecordLinks, - "search": SearchLinks, + request_view_args = { + "pid_value": ma.fields.Str(), + "pid_type": ma.fields.Str(), } - draft_links_config = { - "record": DraftLinks, - } - response_handlers = record_serializers +class Marc21RecordFilesResourceConfig(FileResourceConfig): + """Bibliographic record files resource config.""" + + allow_upload = False + blueprint_name = "marc21_files" + url_prefix = f"{url_prefix}/" + + links_config = {} # -# Drafts and draft actions +# Draft files # -class Marc21DraftResourceConfig(DraftResourceConfig): - """Draft resource configuration.""" +class Marc21DraftFilesResourceConfig(FileResourceConfig): + """Bibliographic record files resource config.""" + + blueprint_name = "marc21_draft_files" + url_prefix = f"{url_prefix}//draft" + + +class Marc21ParentRecordLinksResourceConfig(RecordResourceConfig): + """User records resource configuration.""" - list_route = "/marc21//draft" + blueprint_name = "marc21_access" - item_route = "/marc21/" + url_prefix = f"{url_prefix}//access" - links_config = { - "record": DraftLinks, - "search": SearchLinks, + routes = { + "search" : "", + "list": "/links", + "item": "/links/", } + + links_config = {} + + request_view_args = {"pid_value": ma.fields.Str(), "link_id": ma.fields.Str()} + + response_handlers = {"application/json": ResponseHandler(JSONSerializer())} diff --git a/invenio_records_marc21/resources/resources.py b/invenio_records_marc21/resources/resources.py index 8857da70..513da4d8 100755 --- a/invenio_records_marc21/resources/resources.py +++ b/invenio_records_marc21/resources/resources.py @@ -1,5 +1,9 @@ # -*- coding: utf-8 -*- # +# +# Copyright (C) 2020-2021 CERN. +# Copyright (C) 2020 Northwestern University. +# Copyright (C) 2021 TU Wien. # Copyright (C) 2021 Graz University of Technology. # # Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it @@ -8,7 +12,8 @@ """Marc21 Record Resource.""" -from invenio_drafts_resources.resources import DraftResource, RecordResource +from flask_resources import route +from invenio_drafts_resources.resources import RecordResource from . import config @@ -22,12 +27,32 @@ class Marc21RecordResource(RecordResource): config_name = "MARC21_RECORDS_RECORD_CONFIG" default_config = config.Marc21RecordResourceConfig + def p(self, route): + """Prefix a route with the URL prefix.""" + return f"{self.config.url_prefix}{route}" -# -# Drafts -# -class Marc21DraftResource(DraftResource): - """Bibliographic record draft resource.""" + def create_url_rules(self): + """Create the URL rules for the record resource.""" + routes = self.config.routes + url_rules = super(RecordResource, self).create_url_rules() + return url_rules + + +class Marc21ParentRecordLinksResource(RecordResource): + """Secret links resource.""" + + def create_url_rules(self): + """Create the URL rules for the record resource.""" + + def p(route): + """Prefix a route with the URL prefix.""" + return f"{self.config.url_prefix}{route}" - config_name = "MARC21_RECORDS_DRAFT_CONFIG" - default_config = config.Marc21DraftResourceConfig + routes = self.config.routes + return [ + route("GET", p(routes["list"]), self.search), + route("POST", p(routes["list"]), self.create), + route("GET", p(routes["item"]), self.read), + route("PUT", p(routes["item"]), self.update), + route("DELETE", p(routes["item"]), self.delete), + ] diff --git a/invenio_records_marc21/services/config.py b/invenio_records_marc21/services/config.py index 6ac5f6a8..f7a91b60 100644 --- a/invenio_records_marc21/services/config.py +++ b/invenio_records_marc21/services/config.py @@ -15,7 +15,11 @@ SearchOptions, is_record, ) -from invenio_records_resources.services import ConditionalLink, FileServiceConfig +from invenio_records_resources.services import ( + ConditionalLink, + FileServiceConfig, + pagination_links, +) from invenio_records_resources.services.files.config import FileServiceConfig from invenio_records_resources.services.records.links import RecordLink from invenio_records_resources.services.records.search import terms_filter @@ -99,6 +103,13 @@ class Marc21RecordServiceConfig(RecordServiceConfig): }, ) + links_search = pagination_links("{+api}/marc21{?args*}") + + links_search_drafts = pagination_links("{+api}/marc21/draft{?args*}") + + links_search_versions = pagination_links( + "{+api}/marc21/{id}/versions{?args*}") + components = [ MetadataComponent, AccessComponent, diff --git a/invenio_records_marc21/services/permissions.py b/invenio_records_marc21/services/permissions.py index e1043050..4fe54969 100644 --- a/invenio_records_marc21/services/permissions.py +++ b/invenio_records_marc21/services/permissions.py @@ -31,10 +31,18 @@ class Marc21RecordPermissionPolicy(RecordPermissionPolicy): can_publish = [AnyUser()] can_read = [AnyUser()] can_update = [AnyUser()] + can_new_version = [AnyUser()] + can_edit = [AnyUser()] + # Draft permissions can_read_draft = [AnyUser()] can_delete_draft = [AnyUser()] can_update_draft = [AnyUser()] + can_search_drafts = [AnyUser()] + can_draft_read_files = [AnyUser()] + can_draft_create_files = [AnyUser()] + can_draft_update_files = [AnyUser()] + can_draft_delete_files = [AnyUser()] # Files permissions can_read_files = [AnyUser()] diff --git a/invenio_records_marc21/templates/invenio_records_marc21/search.html b/invenio_records_marc21/templates/invenio_records_marc21/search.html index af8a97a4..385579fd 100755 --- a/invenio_records_marc21/templates/invenio_records_marc21/search.html +++ b/invenio_records_marc21/templates/invenio_records_marc21/search.html @@ -56,7 +56,7 @@ "headers": { "Accept": "application/vnd.inveniomarc21.v1+json" }, - "url": "/api/marc21", + "url": "/api/marc21/list", "withCredentials": true } }, diff --git a/invenio_records_marc21/ui/__init__.py b/invenio_records_marc21/ui/__init__.py index 7cd9d20c..e367b23d 100644 --- a/invenio_records_marc21/ui/__init__.py +++ b/invenio_records_marc21/ui/__init__.py @@ -22,9 +22,9 @@ def create_blueprint(app): "invenio_records_marc21", __name__, template_folder="../templates", + url_prefix="/marc21", ) - blueprint = init_records_views(blueprint, app) blueprint = init_theme_views(blueprint, app) - + blueprint = init_records_views(blueprint, app) return blueprint diff --git a/invenio_records_marc21/ui/records/decorators.py b/invenio_records_marc21/ui/records/decorators.py index 94b5130c..c4877d8c 100644 --- a/invenio_records_marc21/ui/records/decorators.py +++ b/invenio_records_marc21/ui/records/decorators.py @@ -20,12 +20,12 @@ def links_config(): """Get the record links config.""" - return current_records_marc21.records_resource.config.links_config + return current_records_marc21.record_resource.config.links_config def draft_links_config(): """Get the drafts links config.""" - return current_records_marc21.records_resource.config.draft_links_config + return current_records_marc21.record_resource.config.draft_links_config def service(): @@ -39,9 +39,7 @@ def pass_record(f): @wraps(f) def view(**kwargs): pid_value = kwargs.get("pid_value") - record = service().read( - id_=pid_value, identity=g.identity, links_config=links_config() - ) + record = service().read(id_=pid_value, identity=g.identity) kwargs["record"] = record return f(**kwargs) @@ -54,9 +52,7 @@ def pass_draft(f): @wraps(f) def view(**kwargs): pid_value = kwargs.get("pid_value") - draft = service().read_draft( - id_=pid_value, identity=g.identity, links_config=draft_links_config() - ) + draft = service().read_draft(id_=pid_value, identity=g.identity) draft._record.relations.dereference() kwargs["draft"] = draft return f(**kwargs) diff --git a/tests/services/test_create_record.py b/tests/services/test_create_record.py index f1a03e7a..ccfcce90 100644 --- a/tests/services/test_create_record.py +++ b/tests/services/test_create_record.py @@ -7,6 +7,7 @@ """Module tests.""" + from datetime import date, timedelta import pytest From 4e9dbcd6b91e088d29b9fa56565e38b8bb6c9495 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Thu, 27 May 2021 09:33:35 +0200 Subject: [PATCH 033/217] modification: validate embargo present validate embargo schema is present in the record if metadata is set to embargoed --- .../services/schemas/access/parent.py | 19 +++++++++++++++---- .../services/schemas/access/record.py | 16 +++++++++++++--- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/invenio_records_marc21/services/schemas/access/parent.py b/invenio_records_marc21/services/schemas/access/parent.py index fb664b6a..7926464a 100644 --- a/invenio_records_marc21/services/schemas/access/parent.py +++ b/invenio_records_marc21/services/schemas/access/parent.py @@ -8,11 +8,11 @@ """Marc21 parent record access schemas.""" from flask_babelex import lazy_gettext as _ -from marshmallow import Schema, fields -from marshmallow.fields import Integer, List -from marshmallow_utils.fields import SanitizedUnicode -from marshmallow_utils.fields.nestedattr import NestedAttribute +from marshmallow import Schema, ValidationError, fields, validates_schema +from marshmallow.fields import Integer, List, Nested +from marshmallow_utils.fields import NestedAttribute, SanitizedUnicode +from ...components import AccessStatusEnum from .embargo import EmbargoSchema @@ -30,3 +30,14 @@ class ParentAccessSchema(Schema): status = SanitizedUnicode(dump_only=False) embargo = NestedAttribute(EmbargoSchema) owned_by = List(fields.Nested(Agent)) + + @validates_schema + def validate_embargo(self, data, **kwargs): + """Validate that the properties are consistent with each other.""" + metadata = data.get("metadata", "") + embargo = data.get("embargo", None) + if AccessStatusEnum.EMBARGOED.value == metadata and not embargo: + raise ValidationError( + _("Embargo schema must be set if metadata is Embargoed"), + field_name="embargo", + ) diff --git a/invenio_records_marc21/services/schemas/access/record.py b/invenio_records_marc21/services/schemas/access/record.py index 0295c7e7..7634ccf6 100644 --- a/invenio_records_marc21/services/schemas/access/record.py +++ b/invenio_records_marc21/services/schemas/access/record.py @@ -11,8 +11,7 @@ from flask_babelex import lazy_gettext as _ from marshmallow import Schema, ValidationError, validates, validates_schema from marshmallow.fields import Integer, List, Nested -from marshmallow_utils.fields import SanitizedUnicode -from marshmallow_utils.fields.nestedattr import NestedAttribute +from marshmallow_utils.fields import NestedAttribute, SanitizedUnicode from ...components import AccessStatusEnum from .embargo import EmbargoSchema @@ -47,7 +46,18 @@ def validate_protection_value(self, value, field_name): @validates("metadata") def validate_record_protection(self, value): """Validate the record protection value.""" - self.validate_protection_value(value, "record") + self.validate_protection_value(value, "metadata") + + @validates_schema + def validate_embargo(self, data, **kwargs): + """Validate that the properties are consistent with each other.""" + metadata = data.get("metadata", "") + embargo = data.get("embargo", "") + if AccessStatusEnum.EMBARGOED.value == metadata and not embargo: + raise ValidationError( + _("Embargo must be set if metadata is Embargoed"), + field_name="embargo", + ) @validates("files") def validate_files_protection(self, value): From e83f311a82a30b4a1aa55a8533830a6685f4e9ed Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Thu, 27 May 2021 09:34:14 +0200 Subject: [PATCH 034/217] tests: update tests to new record structure --- tests/services/schemas/test_access.py | 2 +- tests/services/test_create_record.py | 44 +++++++++------------------ 2 files changed, 15 insertions(+), 31 deletions(-) diff --git a/tests/services/schemas/test_access.py b/tests/services/schemas/test_access.py index 0f52155d..ccf860e7 100644 --- a/tests/services/schemas/test_access.py +++ b/tests/services/schemas/test_access.py @@ -45,7 +45,7 @@ def test_invalid_access_right(): lambda: AccessSchema().load(invalid_access_right), { "metadata": [ - _("'record' must be either 'public', 'embargoed' or 'restricted'") + _("'metadata' must be either 'public', 'embargoed' or 'restricted'") ] }, ) diff --git a/tests/services/test_create_record.py b/tests/services/test_create_record.py index ccfcce90..b8a769f8 100644 --- a/tests/services/test_create_record.py +++ b/tests/services/test_create_record.py @@ -75,57 +75,42 @@ def empty_data(): [ { "input": { - "access_right": "open", + "metadata": "public", }, "expect": { "access": { - "metadata": False, + "metadata": "public", "owned_by": [{"user": 1}], - "access_right": "open", + "files": "public", }, }, }, { "input": { - "access_right": "closed", + "metadata": "restricted", }, "expect": { "access": { - "metadata": False, + "files": "public", "owned_by": [{"user": 1}], - "access_right": "closed", + "metadata": "restricted", }, }, }, { "input": { - "access_right": "embargoed", - "embargo_date": (date.today() + timedelta(days=2)).strftime("%Y-%m-%d"), - }, - "expect": { - "access": { - "metadata": False, - "owned_by": [{"user": 1}], - "access_right": "embargoed", - "embargo_date": (date.today() + timedelta(days=2)).strftime( - "%Y-%m-%d" - ), + "metadata": "embargoed", + "embargo": { + "until": (date.today() + timedelta(days=2)).strftime("%Y-%m-%d"), + "active": True, + "reason": "Because I can!", }, }, - }, - { - "input": { - "access_right": "restricted", - "embargo_date": (date.today() + timedelta(days=3)).strftime("%Y-%m-%d"), - }, "expect": { "access": { - "metadata": False, + "files": "public", "owned_by": [{"user": 1}], - "access_right": "restricted", - "embargo_date": (date.today() + timedelta(days=3)).strftime( - "%Y-%m-%d" - ), + "metadata": "embargoed" }, }, }, @@ -138,9 +123,8 @@ def test_create_with_access(app, empty_data, identity_simple, access): data=empty_data, identity=identity_simple, access=access["input"] ) record = service.publish(id_=draft.id, identity=identity_simple) - _assert_fields( ["access"], - record.data, + record.data["parent"], access["expect"], ) From 874f2d8045cb5a802c96807817bd042a5c4d56d1 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 7 Jun 2021 10:02:46 +0200 Subject: [PATCH 035/217] style: rupdate formatting --- invenio_records_marc21/resources/config.py | 4 ++-- tests/services/test_create_record.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/invenio_records_marc21/resources/config.py b/invenio_records_marc21/resources/config.py index 9ba602b8..2a50662a 100755 --- a/invenio_records_marc21/resources/config.py +++ b/invenio_records_marc21/resources/config.py @@ -29,7 +29,7 @@ url_prefix = "/marc21" record_ui_routes = { - "search" : f"{url_prefix}", + "search": f"{url_prefix}", "list": f"{url_prefix}/list", "item": f"{url_prefix}/", "item-versions": f"{url_prefix}//versions", @@ -98,7 +98,7 @@ class Marc21ParentRecordLinksResourceConfig(RecordResourceConfig): url_prefix = f"{url_prefix}//access" routes = { - "search" : "", + "search": "", "list": "/links", "item": "/links/", } diff --git a/tests/services/test_create_record.py b/tests/services/test_create_record.py index b8a769f8..e7537b57 100644 --- a/tests/services/test_create_record.py +++ b/tests/services/test_create_record.py @@ -110,7 +110,7 @@ def empty_data(): "access": { "files": "public", "owned_by": [{"user": 1}], - "metadata": "embargoed" + "metadata": "embargoed", }, }, }, From d37c169960e4a719d18dea5b2feafab7ba914334 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 7 Jun 2021 10:04:04 +0200 Subject: [PATCH 036/217] build: update dependencies versions --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 8c9a30e2..10434773 100755 --- a/setup.py +++ b/setup.py @@ -54,8 +54,8 @@ "invenio-i18n>=1.3.0", "dojson>=1.4.0", "invenio-records-rest>=1.5.0,<2.0.0", - "invenio-drafts-resources>=0.11.0,<0.12.0", - "invenio-vocabularies>=0.5.0,<0.7.0", + "invenio-drafts-resources>=0.12.0,<0.13.0", + "invenio-vocabularies>=0.6.0,<0.7.0", ] packages = find_packages() From 845a53cbc9f0ba5245a5b7a8d67d4299dbc15965 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 7 Jun 2021 10:09:25 +0200 Subject: [PATCH 037/217] bugfix(search): update record search config updating the record search config moved search from invenio_records_resources.services.records.search to invenio_records_resources.services.records.facets --- invenio_records_marc21/services/config.py | 82 +++++++++-------------- 1 file changed, 32 insertions(+), 50 deletions(-) diff --git a/invenio_records_marc21/services/config.py b/invenio_records_marc21/services/config.py index f7a91b60..dbbfd49d 100644 --- a/invenio_records_marc21/services/config.py +++ b/invenio_records_marc21/services/config.py @@ -9,6 +9,7 @@ """Marc21 Record Service config.""" from flask import current_app +from flask_babelex import gettext as _ from invenio_drafts_resources.services.records.config import ( RecordServiceConfig, SearchDraftsOptions, @@ -21,57 +22,51 @@ pagination_links, ) from invenio_records_resources.services.files.config import FileServiceConfig +from invenio_records_resources.services.records.facets import TermsFacet from invenio_records_resources.services.records.links import RecordLink -from invenio_records_resources.services.records.search import terms_filter from ..records import Marc21Draft, Marc21Parent, Marc21Record -from .components import AccessComponent, MetadataComponent, PIDComponent +from .components import ( + AccessComponent, + AccessStatusEnum, + MetadataComponent, + PIDComponent, +) from .permissions import Marc21RecordPermissionPolicy from .schemas import Marc21ParentSchema, Marc21RecordSchema +access_right_facet = TermsFacet( + field="access.metadata", + label=_("Access status"), + value_labels={ + AccessStatusEnum.PUBLIC.value: _("Public"), + AccessStatusEnum.EMBARGOED.value: _("Embargoed"), + AccessStatusEnum.RESTRICTED.value: _("Restricted"), + }, +) + +is_published_facet = TermsFacet( + field="is_published", + label=_("State"), + value_labels={"true": _("Published"), "false": _("Unpublished")}, +) + class Marc21SearchOptions(SearchOptions): """Search options for record search.""" - facets_options = dict( - aggs={ - "title": { - "terms": {"field": "metadata.json.title_statement.title"}, - }, - "access_right": { - "terms": {"field": "access.metadata"}, - }, - }, - post_filters={ - "title": terms_filter( - "metadata.json.title_statement.title", - ), - "access_right": terms_filter("access.metadata"), - }, - ) + facets = { + "access_right": access_right_facet, + } class Marc21SearchDraftsOptions(SearchDraftsOptions): """Search options for drafts search.""" - facets_options = dict( - aggs={ - "resource_type": { - "terms": {"field": "metadata"}, - "aggs": {}, - }, - "access_right": { - "terms": {"field": "access.metadata"}, - }, - "is_published": { - "terms": {"field": "is_published"}, - }, - }, - post_filters={ - "access_right": terms_filter("access.metadata"), - "is_published": terms_filter("is_published"), - }, - ) + facets = { + "access_right": access_right_facet, + "is_published": is_published_facet, + } class Marc21RecordServiceConfig(RecordServiceConfig): @@ -91,24 +86,11 @@ class Marc21RecordServiceConfig(RecordServiceConfig): # TODO: ussing from invenio-permissions permission_policy_cls = Marc21RecordPermissionPolicy - search_facets_options = dict( - aggs={ - "is_published": { - "terms": {"field": "is_published"}, - }, - }, - post_filters={ - "access_right": terms_filter("access.status"), - "is_published": terms_filter("is_published"), - }, - ) - links_search = pagination_links("{+api}/marc21{?args*}") links_search_drafts = pagination_links("{+api}/marc21/draft{?args*}") - links_search_versions = pagination_links( - "{+api}/marc21/{id}/versions{?args*}") + links_search_versions = pagination_links("{+api}/marc21/{id}/versions{?args*}") components = [ MetadataComponent, From d6578952cf9a2c6860f9993f74a37a962b6c604d Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 7 Jun 2021 13:52:54 +0200 Subject: [PATCH 038/217] modification: marc21 metadata update service metadata object allowing user create advanced records with marc21 flavor --- invenio_records_marc21/cli.py | 4 +- invenio_records_marc21/services/__init__.py | 2 - .../services/record/__init__.py | 19 +++ .../services/record/fields/__init__.py | 21 +++ .../services/record/fields/control.py | 29 ++++ .../services/record/fields/data.py | 36 +++++ .../services/record/fields/leader.py | 54 +++++++ .../services/record/fields/sub.py | 28 ++++ .../services/record/metadata.py | 128 +++++++++++++++ .../services/record/schema/MARC21slim.xsd | 150 ++++++++++++++++++ invenio_records_marc21/services/services.py | 36 +---- 11 files changed, 470 insertions(+), 37 deletions(-) create mode 100644 invenio_records_marc21/services/record/__init__.py create mode 100644 invenio_records_marc21/services/record/fields/__init__.py create mode 100644 invenio_records_marc21/services/record/fields/control.py create mode 100644 invenio_records_marc21/services/record/fields/data.py create mode 100644 invenio_records_marc21/services/record/fields/leader.py create mode 100644 invenio_records_marc21/services/record/fields/sub.py create mode 100644 invenio_records_marc21/services/record/metadata.py create mode 100644 invenio_records_marc21/services/record/schema/MARC21slim.xsd diff --git a/invenio_records_marc21/cli.py b/invenio_records_marc21/cli.py index 5f7d2c3d..da155c6b 100644 --- a/invenio_records_marc21/cli.py +++ b/invenio_records_marc21/cli.py @@ -18,8 +18,8 @@ from invenio_access import any_user from .proxies import current_records_marc21 -from .services import Metadata from .services.components import AccessStatusEnum +from .services.record import Marc21Metadata def system_identity(): @@ -56,7 +56,7 @@ def _load_json(filename): def create_fake_metadata(filename): """Create records for demo purposes.""" - metadata = Metadata() + metadata = Marc21Metadata() metadata.xml = _load_file(filename) metadata_access = fake_access_right() data_acces = { diff --git a/invenio_records_marc21/services/__init__.py b/invenio_records_marc21/services/__init__.py index 013ce735..e9ff1322 100644 --- a/invenio_records_marc21/services/__init__.py +++ b/invenio_records_marc21/services/__init__.py @@ -18,12 +18,10 @@ Marc21DraftFilesService, Marc21RecordFilesService, Marc21RecordService, - Metadata, RecordItem, ) __all__ = ( - "Metadata", "Marc21RecordService", "Marc21DraftFilesService", "Marc21RecordFilesService", diff --git a/invenio_records_marc21/services/record/__init__.py b/invenio_records_marc21/services/record/__init__.py new file mode 100644 index 00000000..a7e302ad --- /dev/null +++ b/invenio_records_marc21/services/record/__init__.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it +# under the terms of the MIT License; see LICENSE file for more details. + + +"""Marc21 field class.""" + +from .fields import ControlField, DataField, SubField +from .metadata import Marc21Metadata + +__all__ = ( + "Marc21Metadata", + "ControlField", + "DataField", + "SubField", +) diff --git a/invenio_records_marc21/services/record/fields/__init__.py b/invenio_records_marc21/services/record/fields/__init__.py new file mode 100644 index 00000000..e046be71 --- /dev/null +++ b/invenio_records_marc21/services/record/fields/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it +# under the terms of the MIT License; see LICENSE file for more details. + + +"""Marc21 field class.""" + +from .control import ControlField +from .data import DataField +from .leader import LeaderField +from .sub import SubField + +__all__ = ( + "ControlField", + "DataField", + "LeaderField", + "SubField", +) diff --git a/invenio_records_marc21/services/record/fields/control.py b/invenio_records_marc21/services/record/fields/control.py new file mode 100644 index 00000000..59241d2e --- /dev/null +++ b/invenio_records_marc21/services/record/fields/control.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it +# under the terms of the MIT License; see LICENSE file for more details. + +"""Marc21 control field class.""" + + +from os import linesep + + +class ControlField(object): + """ControlField class representing the controlfield HTML tag in MARC21 XML.""" + + def __init__(self, tag: str = "", value: str = ""): + """Default constructor of the class.""" + self.tag = tag + self.value = value + + def to_xml_tag(self, tagsep: str = linesep, indent: int = 4) -> str: + """Get the Marc21 Controlfield XML tag as string.""" + controlfield_tag = " " * indent + controlfield_tag += f'{self.value}' + controlfield_tag += " " * indent + controlfield_tag += "" + controlfield_tag += tagsep + return controlfield_tag diff --git a/invenio_records_marc21/services/record/fields/data.py b/invenio_records_marc21/services/record/fields/data.py new file mode 100644 index 00000000..91103090 --- /dev/null +++ b/invenio_records_marc21/services/record/fields/data.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it +# under the terms of the MIT License; see LICENSE file for more details. + +"""Marc21 data field class.""" + + +from os import linesep + + +class DataField(object): + """DataField class representing the datafield HTML tag in MARC21 XML.""" + + def __init__(self, tag: str = "", ind1: str = " ", ind2: str = " "): + """Default constructor of the class.""" + self.tag = tag + self.ind1 = ind1 + self.ind2 = ind2 + self.subfields = list() + + def to_xml_tag(self, tagsep: str = linesep, indent: int = 4) -> str: + """Get the Marc21 Datafield XML tag as string.""" + datafield_tag = " " * indent + datafield_tag += ( + f'' + ) + datafield_tag += tagsep + for subfield in self.subfields: + datafield_tag += subfield.to_xml_tag(tagsep, indent) + datafield_tag += " " * indent + datafield_tag += "" + datafield_tag += tagsep + return datafield_tag diff --git a/invenio_records_marc21/services/record/fields/leader.py b/invenio_records_marc21/services/record/fields/leader.py new file mode 100644 index 00000000..68147c03 --- /dev/null +++ b/invenio_records_marc21/services/record/fields/leader.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it +# under the terms of the MIT License; see LICENSE file for more details. + +"""Marc21 leader field class.""" + + +from os import linesep + + +class LeaderField(object): + """LeaderField class representing the leaderfield HTML tag in MARC21 XML.""" + + def __init__(self, **kwargs): + """Default constructor of the class.""" + self.length = kwargs.get("length", "00000") # 00-04 + self.status = kwargs.get("status", "n") # 05 + self.type = kwargs.get("type", "a") # 06 + self.level = kwargs.get("level", "m") # 07 + self.control = kwargs.get("control", " ") # 08 + self.charset = kwargs.get("charset", "a") # 09 + + self.ind_count = kwargs.get("ind_count", "2") # 10 + self.sub_count = kwargs.get("sub_count", "2") # 11 + self.address = kwargs.get("address", "00000") # 12-16 + self.encoding = kwargs.get("encoding", "z") # 17 + self.description = kwargs.get("description", "c") # 18 + self.multipart_resource_record_level = kwargs.get( + "multipart_resource_record_level", "a" + ) # 19 + self.length_field_position = kwargs.get("length_field_position", "4") # 20 + self.length_starting_character_position_portion = kwargs.get( + "length_starting_character_position_portion", "5" + ) # 21 + self.length_implementation_defined_portion = kwargs.get( + "length_implementation_defined_portion", "0" + ) # 22 + self.undefined = kwargs.get("undefined", "0") # 23 + + def to_xml_tag(self, tagsep: str = linesep, indent: int = 4) -> str: + """Get the Marc21 Leaderfield XML tag as string.""" + leader = " " * indent + leader += "" + leader += f"{self.length}{self.status}{self.type}{self.level}{self.control}{self.charset}" + leader += f"{self.ind_count}{self.sub_count}{self.address}{self.encoding}{self.description}" + leader += f"{self.multipart_resource_record_level}{self.length_field_position}" + leader += f"{self.length_starting_character_position_portion}{self.length_implementation_defined_portion}" + leader += f"{self.undefined}" + leader += "" + leader += tagsep + return leader diff --git a/invenio_records_marc21/services/record/fields/sub.py b/invenio_records_marc21/services/record/fields/sub.py new file mode 100644 index 00000000..42bae7f0 --- /dev/null +++ b/invenio_records_marc21/services/record/fields/sub.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it +# under the terms of the MIT License; see LICENSE file for more details. + +"""Marc21 sub field class.""" + + +from os import linesep + + +class SubField(object): + """SubField class representing the subfield HTML tag in MARC21 XML.""" + + def __init__(self, code: str = "", value: str = ""): + """Default constructor of the class.""" + self.code = code + self.value = value + + def to_xml_tag(self, tagsep: str = linesep, indent: int = 4) -> str: + """Get the Marc21 Subfield XML tag as string.""" + subfield_tag = 2 * " " * indent + subfield_tag += f'{self.value}' + subfield_tag += f"" + subfield_tag += tagsep + return subfield_tag diff --git a/invenio_records_marc21/services/record/metadata.py b/invenio_records_marc21/services/record/metadata.py new file mode 100644 index 00000000..c210ecc2 --- /dev/null +++ b/invenio_records_marc21/services/record/metadata.py @@ -0,0 +1,128 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it +# under the terms of the MIT License; see LICENSE file for more details. + +"""Marc21 record class.""" + +from io import StringIO +from os import linesep +from os.path import dirname, join + +from lxml import etree + +from .fields import DataField, LeaderField, SubField + + +class Marc21Metadata(object): + """MARC21 Record class to facilitate storage of records in MARC21 format.""" + + def __init__(self, leader: LeaderField = LeaderField()): + """Default constructor of the class.""" + self._xml = "" + self._json = {} + self.leader = leader + self.controlfields = list() + self.datafields = list() + + @property + def json(self): + """Metadata json getter method.""" + return self._json + + @json.setter + def json(self, json: dict): + """Metadata json setter method.""" + if not isinstance(json, dict): + raise TypeError("json must be from type dict") + self._json = json + + @property + def xml(self): + """Metadata xml getter method.""" + self._to_string() + return self._xml + + @xml.setter + def xml(self, xml: str): + """Metadata xml setter method.""" + if not isinstance(xml, str): + raise TypeError("xml must be from type str") + + self._to_xml_tree(xml) + self._xml = xml + + def _to_xml_tree(self, xml: str): + """Xml to internal representation method.""" + test = etree.parse(StringIO(xml)) + for element in test.iter(): + if element.tag == "datafield": + self.datafields.append(DataField(**element.attrib)) + elif element.tag == "subfield": + self.datafields[-1].subfields.append( + SubField(**element.attrib, value=element.text) + ) + + def _to_string(self, tagsep: str = linesep, indent: int = 4) -> str: + """Get a pretty-printed XML string of the record.""" + self._xml = "" + self._xml += '' + self._xml += tagsep + if self.leader: + self._xml += self.leader.to_xml_tag(tagsep, indent) + for controlfield in self.controlfields: + self._xml += controlfield.to_xml_tag(tagsep, indent) + for datafield in self.datafields: + self._xml += datafield.to_xml_tag(tagsep, indent) + self._xml += "" + + def contains(self, ref_df: DataField, ref_sf: SubField) -> bool: + """Return True if record contains reference datafield, which contains reference subfield.""" + for df in self.datafields: + if ( + df.tag == ref_df.tag and df.ind1 == ref_df.ind1 and df.ind2 == ref_df.ind2 + ): + for sf in df.subfields: + if sf.code == ref_sf.code and sf.value == ref_sf.value: + return True + return False + + def add_value( + self, + tag: str = "", + ind1: str = " ", + ind2: str = " ", + code: str = "", + value: str = "", + ) -> None: + """Add value to record for given datafield and subfield.""" + datafield = DataField(tag, ind1, ind2) + subfield = SubField(code, value) + datafield.subfields.append(subfield) + self.datafields.append(datafield) + + def add_unique_value( + self, + tag: str = "", + ind1: str = " ", + ind2: str = " ", + code: str = "", + value: str = "", + ) -> None: + """Add value to record if it doesn't already contain it.""" + datafield = DataField(tag, ind1, ind2) + subfield = SubField(code, value) + if not self.contains(datafield, subfield): + datafield.subfields.append(subfield) + self.datafields.append(datafield) + + def is_valid_marc21_xml_string(self) -> bool: + """Validate the record against a Marc21XML Schema.""" + with open( + join(dirname(__file__), "schema", "MARC21slim.xsd"), "r", encoding="utf-8" + ) as fp: + marc21xml_schema = etree.XMLSchema(etree.parse(fp)) + marc21xml = etree.parse(StringIO(self.xml)) + return marc21xml_schema.validate(marc21xml) diff --git a/invenio_records_marc21/services/record/schema/MARC21slim.xsd b/invenio_records_marc21/services/record/schema/MARC21slim.xsd new file mode 100644 index 00000000..dfb8498e --- /dev/null +++ b/invenio_records_marc21/services/record/schema/MARC21slim.xsd @@ -0,0 +1,150 @@ + + + + + MARCXML: The MARC 21 XML Schema + Prepared by Corey Keith + + May 21, 2002 - Version 1.0 - Initial Release + +********************************************** +Changes. + +August 4, 2003 - Version 1.1 - +Removed import of xml namespace and the use of xml:space="preserve" attributes on the leader and controlfields. + Whitespace preservation in these subfields is accomplished by the use of xsd:whiteSpace value="preserve" + +May 21, 2009 - Version 1.2 - +in subfieldcodeDataType the pattern + "[\da-z!"#$%&'()*+,-./:;<=>?{}_^`~\[\]\\]{1}" + changed to: + "[\dA-Za-z!"#$%&'()*+,-./:;<=>?{}_^`~\[\]\\]{1}" + i.e "A-Z" added after "[\d" before "a-z" to allow upper case. This change is for consistency with the documentation. + +************************************************************ + This schema supports XML markup of MARC21 records as specified in the MARC documentation (see www.loc.gov). It allows tags with + alphabetics and subfield codes that are symbols, neither of which are as yet used in the MARC 21 communications formats, but are + allowed by MARC 21 for local data. The schema accommodates all types of MARC 21 records: bibliographic, holdings, bibliographic + with embedded holdings, authority, classification, and community information. + + + + + record is a top level container element for all of the field elements which compose the record + + + + + collection is a top level container element for 0 or many records + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MARC21 Leader, 24 bytes + + + + + + + + + + + + + + + + MARC21 Fields 001-009 + + + + + + + + + + + + + + + + + + + + + + MARC21 Variable Data Fields 010-999 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/invenio_records_marc21/services/services.py b/invenio_records_marc21/services/services.py index 52ca80a1..e6e57b06 100644 --- a/invenio_records_marc21/services/services.py +++ b/invenio_records_marc21/services/services.py @@ -19,37 +19,7 @@ Marc21RecordFilesServiceConfig, Marc21RecordServiceConfig, ) - - -class Metadata: - """Marc21 Metadata object.""" - - _json = {} - _xml = "" - - @property - def json(self): - """Metadata json getter method.""" - return self._json - - @json.setter - def json(self, json: dict): - """Metadata json setter method.""" - if not isinstance(json, dict): - raise TypeError("json must be from type dict") - self._json = json - - @property - def xml(self): - """Metadata xml getter method.""" - return self._xml - - @xml.setter - def xml(self, xml: str): - """Metadata xml setter method.""" - if not isinstance(xml, str): - raise TypeError("xml must be from type str") - self._xml = xml +from .record import Marc21Metadata class Marc21RecordService(RecordService): @@ -82,7 +52,7 @@ def _create_data(self, identity, data, metadata, access=None): return data def create( - self, identity, data=None, metadata=Metadata(), access=None + self, identity, data=None, metadata=Marc21Metadata(), access=None ) -> RecordItem: """Create a draft record. @@ -100,7 +70,7 @@ def update_draft( id_, identity, data=None, - metadata=Metadata(), + metadata=Marc21Metadata(), revision_id=None, access=None, ): From 9cc833cf1d9fa7ea0d5fe5ddda5edeaa73816fa6 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 7 Jun 2021 13:53:25 +0200 Subject: [PATCH 039/217] build(setup): add lxml module dependency --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 10434773..d64b30b0 100755 --- a/setup.py +++ b/setup.py @@ -53,6 +53,7 @@ install_requires = [ "invenio-i18n>=1.3.0", "dojson>=1.4.0", + "lxml>=4.6.2", "invenio-records-rest>=1.5.0,<2.0.0", "invenio-drafts-resources>=0.12.0,<0.13.0", "invenio-vocabularies>=0.6.0,<0.7.0", From f02f7b5c7812bc3fef857375e8a1454d393e527a Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 7 Jun 2021 13:54:50 +0200 Subject: [PATCH 040/217] refactor(Marc21Metadata): import Marc21Metadata --- tests/services/conftest.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/tests/services/conftest.py b/tests/services/conftest.py index 1acd1dff..5b41da6c 100644 --- a/tests/services/conftest.py +++ b/tests/services/conftest.py @@ -22,8 +22,8 @@ from invenio_records_marc21.services import ( Marc21RecordService, Marc21RecordServiceConfig, - Metadata, ) +from invenio_records_marc21.services.record import Marc21Metadata @pytest.fixture(scope="module") @@ -57,19 +57,14 @@ def example_record(app, db): @pytest.fixture() def metadata(): """Input data (as coming from the view layer).""" - metadata = Metadata() - metadata.xml = "\ - laborum sunt ut nulla\ - " + metadata = Marc21Metadata() + metadata.add_value(tag="245", ind1="1", ind2="0", value="laborum sunt ut nulla") return metadata @pytest.fixture() def metadata2(): """Input data (as coming from the view layer).""" - metadata = Metadata() - metadata.xml = "\ - \ - nulla sunt laborum\ - " + metadata = Marc21Metadata() + metadata.add_value(tag="245", ind1="1", ind2="0", value="nulla sunt laborum") return metadata From 6a05a2230733bef74d5e5fa52c54588406b50a13 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 7 Jun 2021 13:55:47 +0200 Subject: [PATCH 041/217] tests(Marc21Metadata): add Marc21Metadata testset adding testset for marc21 metadata object and fields --- tests/services/record/conftest.py | 16 ++ tests/services/record/fields/conftest.py | 40 ++++ .../record/fields/test_controlfield.py | 44 +++++ .../services/record/fields/test_datafield.py | 52 ++++++ .../record/fields/test_leaderfield.py | 107 +++++++++++ tests/services/record/fields/test_subfield.py | 43 +++++ tests/services/record/test_marc21_metadata.py | 172 ++++++++++++++++++ 7 files changed, 474 insertions(+) create mode 100644 tests/services/record/conftest.py create mode 100644 tests/services/record/fields/conftest.py create mode 100644 tests/services/record/fields/test_controlfield.py create mode 100644 tests/services/record/fields/test_datafield.py create mode 100644 tests/services/record/fields/test_leaderfield.py create mode 100644 tests/services/record/fields/test_subfield.py create mode 100644 tests/services/record/test_marc21_metadata.py diff --git a/tests/services/record/conftest.py b/tests/services/record/conftest.py new file mode 100644 index 00000000..51161203 --- /dev/null +++ b/tests/services/record/conftest.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + + +"""Pytest configuration. + +See https://pytest-invenio.readthedocs.io/ for documentation on which test +fixtures are available. +""" + +import pytest diff --git a/tests/services/record/fields/conftest.py b/tests/services/record/fields/conftest.py new file mode 100644 index 00000000..ff5ec3c5 --- /dev/null +++ b/tests/services/record/fields/conftest.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + + +"""Pytest configuration. + +See https://pytest-invenio.readthedocs.io/ for documentation on which test +fixtures are available. +""" + +import pytest + + +@pytest.fixture() +def leader_kwargs(): + """Input data (as coming from the view layer).""" + leader = {} + leader["length"] = "00000" + leader["status"] = "n" + leader["type"] = "a" + leader["level"] = "m" + leader["control"] = " " + leader["charset"] = "a" + + leader["ind_count"] = "2" + leader["sub_count"] = "2" + leader["address"] = "00000" + leader["encoding"] = "z" + leader["description"] = "c" + leader["multipart_resource_record_level"] = "a" + leader["length_field_position"] = "4" + leader["length_starting_character_position_portion"] = "5" + leader["length_implementation_defined_portion"] = "0" + leader["undefined"] = "0" + return leader diff --git a/tests/services/record/fields/test_controlfield.py b/tests/services/record/fields/test_controlfield.py new file mode 100644 index 00000000..aa694262 --- /dev/null +++ b/tests/services/record/fields/test_controlfield.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + + +"""Pytest configuration. + +See https://pytest-invenio.readthedocs.io/ for documentation on which test +fixtures are available. +""" + +import pytest + +from invenio_records_marc21.services.record import ControlField + + +def test_controlfield(): + controlfield = ControlField() + assert controlfield.tag == "" + assert controlfield.value == "" + + controlfield = ControlField("12") + assert controlfield.tag == "12" + assert controlfield.value == "" + + controlfield = ControlField(tag="123", value="laborum sunt ut nulla") + assert controlfield.tag == "123" + assert controlfield.value == "laborum sunt ut nulla" + + +def test_controlfield_to_xml(): + controlfield = ControlField(tag="123", value="laborum sunt ut nulla") + xml = controlfield.to_xml_tag() + assert 'laborum sunt ut nulla' in xml + assert '' in xml + assert xml.startswith(" ") + assert xml.endswith("\n") + + xml = controlfield.to_xml_tag(tagsep="", indent=0) + assert 'laborum sunt ut nulla' == xml diff --git a/tests/services/record/fields/test_datafield.py b/tests/services/record/fields/test_datafield.py new file mode 100644 index 00000000..1590fb52 --- /dev/null +++ b/tests/services/record/fields/test_datafield.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + + +"""Pytest configuration. + +See https://pytest-invenio.readthedocs.io/ for documentation on which test +fixtures are available. +""" + +import pytest + +from invenio_records_marc21.services.record import DataField, SubField + + +def test_datafield(): + datafield = DataField() + assert datafield.tag == "" + assert datafield.ind1 == " " + assert datafield.ind2 == " " + assert datafield.subfields == list() + + datafield = DataField("laborum", "a", "b") + assert datafield.tag == "laborum" + assert datafield.ind1 == "a" + assert datafield.ind2 == "b" + + subfield = SubField(code="123", value="laborum sunt ut nulla") + datafield.subfields.append(subfield) + assert len(datafield.subfields) == 1 + + +def test_datafield_to_xml(): + subfield = SubField(code="123", value="laborum sunt ut nulla") + datafield = DataField("laborum") + datafield.subfields.append(subfield) + + xml = datafield.to_xml_tag() + assert '\n' in xml + assert 'laborum sunt ut nulla' in xml + assert xml.startswith(" ") + assert xml.endswith("\n") + + xml = datafield.to_xml_tag(tagsep="", indent=0) + assert xml.startswith( + 'laborum sunt ut nulla' + ) diff --git a/tests/services/record/fields/test_leaderfield.py b/tests/services/record/fields/test_leaderfield.py new file mode 100644 index 00000000..0718e686 --- /dev/null +++ b/tests/services/record/fields/test_leaderfield.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + + +"""Pytest configuration. + +See https://pytest-invenio.readthedocs.io/ for documentation on which test +fixtures are available. +""" + +import pytest + +from invenio_records_marc21.services.record.fields import LeaderField + + +def _assert_field_value(fields, object, expected): + for key in fields: + assert getattr(object, key) == expected[key] + + +def test_leaderfield(leader_kwargs): + leaderfield = LeaderField() + _assert_field_value(leader_kwargs.keys(), leaderfield, leader_kwargs) + + leader_kwargs["length"] = "99999" + leaderfield = LeaderField(**leader_kwargs) + _assert_field_value(leader_kwargs.keys(), leaderfield, leader_kwargs) + + leader_kwargs["status"] = "d" + leaderfield = LeaderField(**leader_kwargs) + _assert_field_value(leader_kwargs.keys(), leaderfield, leader_kwargs) + + leader_kwargs["type"] = "g" + leaderfield = LeaderField(**leader_kwargs) + _assert_field_value(leader_kwargs.keys(), leaderfield, leader_kwargs) + + leader_kwargs["level"] = "c" + leaderfield = LeaderField(**leader_kwargs) + _assert_field_value(leader_kwargs.keys(), leaderfield, leader_kwargs) + + leader_kwargs["control"] = "a" + leaderfield = LeaderField(**leader_kwargs) + _assert_field_value(leader_kwargs.keys(), leaderfield, leader_kwargs) + + leader_kwargs["charset"] = " " + leaderfield = LeaderField(**leader_kwargs) + _assert_field_value(leader_kwargs.keys(), leaderfield, leader_kwargs) + + leader_kwargs["ind_count"] = "6" + leaderfield = LeaderField(**leader_kwargs) + _assert_field_value(leader_kwargs.keys(), leaderfield, leader_kwargs) + + leader_kwargs["sub_count"] = "6" + leaderfield = LeaderField(**leader_kwargs) + _assert_field_value(leader_kwargs.keys(), leaderfield, leader_kwargs) + + leader_kwargs["ind_count"] = "6" + leaderfield = LeaderField(**leader_kwargs) + _assert_field_value(leader_kwargs.keys(), leaderfield, leader_kwargs) + + leader_kwargs["address"] = "99999" + leaderfield = LeaderField(**leader_kwargs) + _assert_field_value(leader_kwargs.keys(), leaderfield, leader_kwargs) + + leader_kwargs["encoding"] = "u" + leaderfield = LeaderField(**leader_kwargs) + _assert_field_value(leader_kwargs.keys(), leaderfield, leader_kwargs) + + leader_kwargs["description"] = "a" + leaderfield = LeaderField(**leader_kwargs) + _assert_field_value(leader_kwargs.keys(), leaderfield, leader_kwargs) + + leader_kwargs["multipart_resource_record_level"] = " " + leaderfield = LeaderField(**leader_kwargs) + _assert_field_value(leader_kwargs.keys(), leaderfield, leader_kwargs) + + leader_kwargs["length_field_position"] = "9" + leaderfield = LeaderField(**leader_kwargs) + _assert_field_value(leader_kwargs.keys(), leaderfield, leader_kwargs) + + leader_kwargs["length_starting_character_position_portion"] = "9" + leaderfield = LeaderField(**leader_kwargs) + _assert_field_value(leader_kwargs.keys(), leaderfield, leader_kwargs) + + leader_kwargs["length_implementation_defined_portion"] = "9" + leaderfield = LeaderField(**leader_kwargs) + _assert_field_value(leader_kwargs.keys(), leaderfield, leader_kwargs) + + leader_kwargs["undefined"] = "9" + leaderfield = LeaderField(**leader_kwargs) + _assert_field_value(leader_kwargs.keys(), leaderfield, leader_kwargs) + + +def test_leaderfield_to_xml(leader_kwargs): + leaderfield = LeaderField(**leader_kwargs) + xml = leaderfield.to_xml_tag() + assert "00000nam a2200000zca4500" in xml + assert xml.startswith(" ") + assert xml.endswith("\n") + + xml = leaderfield.to_xml_tag(tagsep="", indent=0) + assert "00000nam a2200000zca4500" == xml diff --git a/tests/services/record/fields/test_subfield.py b/tests/services/record/fields/test_subfield.py new file mode 100644 index 00000000..2e4df78f --- /dev/null +++ b/tests/services/record/fields/test_subfield.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + + +"""Pytest configuration. + +See https://pytest-invenio.readthedocs.io/ for documentation on which test +fixtures are available. +""" + +import pytest + +from invenio_records_marc21.services.record import SubField + + +def test_subfield(): + subfield = SubField() + assert subfield.code == "" + assert subfield.value == "" + + subfield = SubField("12") + assert subfield.code == "12" + assert subfield.value == "" + + subfield = SubField(code="123", value="laborum sunt ut nulla") + assert subfield.code == "123" + assert subfield.value == "laborum sunt ut nulla" + + +def test_subfield_to_xml(): + subfield = SubField(code="123", value="laborum sunt ut nulla") + xml = subfield.to_xml_tag() + assert 'laborum sunt ut nulla' in xml + assert xml.startswith(" ") + assert xml.endswith("\n") + + xml = subfield.to_xml_tag(tagsep="", indent=0) + assert 'laborum sunt ut nulla' == xml diff --git a/tests/services/record/test_marc21_metadata.py b/tests/services/record/test_marc21_metadata.py new file mode 100644 index 00000000..a69d8888 --- /dev/null +++ b/tests/services/record/test_marc21_metadata.py @@ -0,0 +1,172 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + + +"""Pytest configuration. + +See https://pytest-invenio.readthedocs.io/ for documentation on which test +fixtures are available. +""" + +from io import StringIO +from os import linesep +from os.path import dirname, join + +import pytest +from lxml import etree + +from invenio_records_marc21.services.record import Marc21Metadata +from invenio_records_marc21.services.record.fields import ( + ControlField, + DataField, + LeaderField, + SubField, +) + + +def test_create_metadata(): + + metadata = Marc21Metadata() + assert metadata.leader.to_xml_tag() == LeaderField().to_xml_tag() + assert metadata.controlfields == list() + assert metadata.datafields == list() + + assert "" in metadata.xml + assert '' in metadata.xml + assert 'laborum sunt ut nulla' in metadata.xml + + +def test_validate_metadata(): + metadata = Marc21Metadata() + metadata.add_value( + tag="245", ind1="1", ind2="0", code="a", value="laborum sunt ut nulla" + ) + assert metadata.is_valid_marc21_xml_string() + + metadata.add_value( + tag="245", ind1="1", ind2="0", code="", value="laborum sunt ut nulla" + ) + + assert not metadata.is_valid_marc21_xml_string() + + +def test_subfield_metadata(): + metadata = Marc21Metadata() + metadata.add_value( + tag="245", ind1="1", ind2="0", code="a", value="laborum sunt ut nulla" + ) + + metadata.add_value( + tag="245", ind1="1", ind2="0", code="b", value="laborum sunt ut nulla" + ) + + assert metadata.is_valid_marc21_xml_string() + + +def test_controlfields_metadata(): + metadata = Marc21Metadata() + controlfield = ControlField(tag="123", value="laborum sunt ut nulla") + + assert len(metadata.datafields) == 0 + assert len(metadata.controlfields) == 0 + + metadata.controlfields.append(controlfield) + assert len(metadata.datafields) == 0 + assert len(metadata.controlfields) == 1 + assert 'laborum sunt ut nulla' in metadata.xml + + +def test_uniqueness_metadata(): + metadata = Marc21Metadata() + metadata.add_unique_value( + tag="245", ind1="1", ind2="0", code="a", value="laborum sunt ut nulla" + ) + + metadata.add_unique_value( + tag="245", ind1="1", ind2="0", code="a", value="laborum sunt ut nulla" + ) + + assert len(metadata.datafields) == 1 + assert len(metadata.controlfields) == 0 + assert len(metadata.datafields[0].subfields) == 1 + assert metadata.is_valid_marc21_xml_string() + + +def test_xml_type(): + metadata = Marc21Metadata() + + test = '' + metadata.xml = test + assert metadata.xml + + test = {} + with pytest.raises(TypeError): + metadata.xml = test + + +def test_json_type(): + metadata = Marc21Metadata() + + test = dict() + metadata.json = test + assert metadata.json == {} + + test = "" + with pytest.raises(TypeError): + metadata.json = test + + +def test_xml_metadata(): + metadata = Marc21Metadata() + test = DataField(tag="245", ind1="0", ind2="0") + + metadata.xml = test.to_xml_tag() + + assert metadata.leader.to_xml_tag() == LeaderField().to_xml_tag() + assert len(metadata.datafields) == 1 + assert len(metadata.controlfields) == 0 + assert len(metadata.datafields[0].subfields) == 0 + assert test.to_xml_tag() in metadata.xml + + test.subfields.append(SubField(code="a", value="Brain-Computer Interface")) + metadata = Marc21Metadata() + metadata.xml = test.to_xml_tag() + + assert len(metadata.datafields) == 1 + assert len(metadata.controlfields) == 0 + assert len(metadata.datafields[0].subfields) == 1 + assert test.to_xml_tag() in metadata.xml + + test.subfields.append(SubField(code="b", value="Subtitle field.")) + metadata = Marc21Metadata() + metadata.xml = test.to_xml_tag() + + assert len(metadata.datafields) == 1 + assert len(metadata.controlfields) == 0 + assert len(metadata.datafields[0].subfields) == 2 + assert test.to_xml_tag() in metadata.xml + + test.subfields.append(SubField(code="c", value="hrsg. von Josef Frank")) + metadata = Marc21Metadata() + metadata.xml = test.to_xml_tag() + + assert len(metadata.datafields) == 1 + assert len(metadata.controlfields) == 0 + assert len(metadata.datafields[0].subfields) == 3 + assert test.to_xml_tag() in metadata.xml From 5de625928e11d3f007e5a49d94ce534efc106aef Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 7 Jun 2021 14:14:42 +0200 Subject: [PATCH 042/217] modification: update manifest --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index d4f126cb..927655eb 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -39,5 +39,6 @@ recursive-include invenio_records_marc21 *.xml recursive-include invenio_records_marc21 *.py recursive-include invenio_records_marc21 *.csv recursive-include invenio_records_marc21 *.js +recursive-include invenio_records_marc21 *.xsd recursive-include tests *.py recursive-include tests *.json From dd12a9ed943a7bb705fcc9e7584f559adc9a116e Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Thu, 10 Jun 2021 09:29:15 +0200 Subject: [PATCH 043/217] modification: rename metadata functions renaming add_value and add_unique_value functions to emplace_field and emplace_unique_field --- invenio_records_marc21/services/record/metadata.py | 4 ++-- tests/services/conftest.py | 4 ++-- tests/services/record/fields/test_controlfield.py | 2 +- tests/services/record/test_marc21_metadata.py | 14 +++++++------- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/invenio_records_marc21/services/record/metadata.py b/invenio_records_marc21/services/record/metadata.py index c210ecc2..e9485f55 100644 --- a/invenio_records_marc21/services/record/metadata.py +++ b/invenio_records_marc21/services/record/metadata.py @@ -89,7 +89,7 @@ def contains(self, ref_df: DataField, ref_sf: SubField) -> bool: return True return False - def add_value( + def emplace_field( self, tag: str = "", ind1: str = " ", @@ -103,7 +103,7 @@ def add_value( datafield.subfields.append(subfield) self.datafields.append(datafield) - def add_unique_value( + def emplace_unique_field( self, tag: str = "", ind1: str = " ", diff --git a/tests/services/conftest.py b/tests/services/conftest.py index 5b41da6c..9fd09969 100644 --- a/tests/services/conftest.py +++ b/tests/services/conftest.py @@ -58,7 +58,7 @@ def example_record(app, db): def metadata(): """Input data (as coming from the view layer).""" metadata = Marc21Metadata() - metadata.add_value(tag="245", ind1="1", ind2="0", value="laborum sunt ut nulla") + metadata.emplace_field(tag="245", ind1="1", ind2="0", value="laborum sunt ut nulla") return metadata @@ -66,5 +66,5 @@ def metadata(): def metadata2(): """Input data (as coming from the view layer).""" metadata = Marc21Metadata() - metadata.add_value(tag="245", ind1="1", ind2="0", value="nulla sunt laborum") + metadata.emplace_field(tag="245", ind1="1", ind2="0", value="nulla sunt laborum") return metadata diff --git a/tests/services/record/fields/test_controlfield.py b/tests/services/record/fields/test_controlfield.py index aa694262..2a650066 100644 --- a/tests/services/record/fields/test_controlfield.py +++ b/tests/services/record/fields/test_controlfield.py @@ -36,7 +36,7 @@ def test_controlfield_to_xml(): controlfield = ControlField(tag="123", value="laborum sunt ut nulla") xml = controlfield.to_xml_tag() assert 'laborum sunt ut nulla' in xml - assert '' in xml + assert "" in xml assert xml.startswith(" ") assert xml.endswith("\n") diff --git a/tests/services/record/test_marc21_metadata.py b/tests/services/record/test_marc21_metadata.py index a69d8888..fc5eb276 100644 --- a/tests/services/record/test_marc21_metadata.py +++ b/tests/services/record/test_marc21_metadata.py @@ -44,7 +44,7 @@ def test_create_metadata(): ) assert 'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"' in metadata.xml - metadata.add_value( + metadata.emplace_field( tag="245", ind1="1", ind2="0", code="a", value="laborum sunt ut nulla" ) @@ -54,12 +54,12 @@ def test_create_metadata(): def test_validate_metadata(): metadata = Marc21Metadata() - metadata.add_value( + metadata.emplace_field( tag="245", ind1="1", ind2="0", code="a", value="laborum sunt ut nulla" ) assert metadata.is_valid_marc21_xml_string() - metadata.add_value( + metadata.emplace_field( tag="245", ind1="1", ind2="0", code="", value="laborum sunt ut nulla" ) @@ -68,11 +68,11 @@ def test_validate_metadata(): def test_subfield_metadata(): metadata = Marc21Metadata() - metadata.add_value( + metadata.emplace_field( tag="245", ind1="1", ind2="0", code="a", value="laborum sunt ut nulla" ) - metadata.add_value( + metadata.emplace_field( tag="245", ind1="1", ind2="0", code="b", value="laborum sunt ut nulla" ) @@ -94,11 +94,11 @@ def test_controlfields_metadata(): def test_uniqueness_metadata(): metadata = Marc21Metadata() - metadata.add_unique_value( + metadata.emplace_unique_field( tag="245", ind1="1", ind2="0", code="a", value="laborum sunt ut nulla" ) - metadata.add_unique_value( + metadata.emplace_unique_field( tag="245", ind1="1", ind2="0", code="a", value="laborum sunt ut nulla" ) From 309a6835f15fb348faf934c5c0ae88c23ac67258 Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Fri, 16 Jul 2021 14:36:50 +0200 Subject: [PATCH 044/217] bugfix: lxml parsetree adds xmlns to tag name lxml adds xml namespace to element tag name changing search of element tag names to containing datafield or subfield string. --- invenio_records_marc21/services/record/metadata.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/invenio_records_marc21/services/record/metadata.py b/invenio_records_marc21/services/record/metadata.py index e9485f55..f96dcc57 100644 --- a/invenio_records_marc21/services/record/metadata.py +++ b/invenio_records_marc21/services/record/metadata.py @@ -58,9 +58,9 @@ def _to_xml_tree(self, xml: str): """Xml to internal representation method.""" test = etree.parse(StringIO(xml)) for element in test.iter(): - if element.tag == "datafield": + if "datafield" in element.tag: self.datafields.append(DataField(**element.attrib)) - elif element.tag == "subfield": + elif "subfield" in element.tag: self.datafields[-1].subfields.append( SubField(**element.attrib, value=element.text) ) From 3de7b302024f3f85948438273338d25d67df86ea Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 19 Jul 2021 11:02:15 +0200 Subject: [PATCH 045/217] refactor: update ES mapping removing old internal id schema and add title statement in metadata and parents schema --- .../marc21records/drafts/marc21-v1.0.0.json | 129 +++++++++++++++--- .../marc21records/marc21/marc21-v1.0.0.json | 128 ++++++++++++++--- 2 files changed, 214 insertions(+), 43 deletions(-) diff --git a/invenio_records_marc21/records/mappings/v7/marc21records/drafts/marc21-v1.0.0.json b/invenio_records_marc21/records/mappings/v7/marc21records/drafts/marc21-v1.0.0.json index 615e6b3d..575588f3 100644 --- a/invenio_records_marc21/records/mappings/v7/marc21records/drafts/marc21-v1.0.0.json +++ b/invenio_records_marc21/records/mappings/v7/marc21records/drafts/marc21-v1.0.0.json @@ -3,41 +3,126 @@ "date_detection": false, "numeric_detection": false, "properties": { - "conceptid" : { - "type" : "keyword" - }, "id": { "type": "keyword" }, - "access" : { - "properties" : { - "access_right" : { - "type" : "keyword" + "access": { + "properties": { + "metadata": { + "type": "keyword" + }, + "files": { + "type": "keyword" + }, + "embargo": { + "properties": { + "active": { + "type": "boolean" + }, + "until": { + "type": "date" + }, + "reason": { + "type": "text" + } + } }, - "created_by" : { - "type" : "long" + "owned_by": { + "properties": { + "user": { + "type": "keyword" + } + } + }, + "status": { + "type": "keyword" + } + } + }, + "parent": { + "properties": { + "id": { + "type": "keyword" + }, + "access": { + "properties": { + "owned_by": { + "properties": { + "user": { + "type": "keyword" + } + } + } + } + } + } + }, + "pids": { + "properties": { + "identifier": { + "type": "keyword" }, - "files_restricted" : { - "type" : "boolean" + "scheme": { + "type": "keyword" }, - "metadata_restricted" : { - "type" : "boolean" + "client": { + "type": "keyword", + "index": false }, - "owners" : { - "type" : "long" + "provider": { + "type": "keyword", + "index": false } } }, - "metadata" : { - "properties" : { - + "has_draft": { + "type": "boolean" + }, + "metadata": { + "properties": { + "json": { + "properties": { + "title_statement": { + "properties": { + "title": { + "type": "keyword" + } + } + } + } + } } }, - "created" : { - "type" : "date" + "created": { + "type": "date" + }, + "updated": { + "type": "date" }, - "updated" : { - "type" : "date" + "is_published": { + "type": "boolean" + }, + "versions": { + "properties": { + "index": { + "type": "integer" + }, + "is_latest": { + "type": "boolean" + }, + "is_latest_draft": { + "type": "boolean" + }, + "latest_id": { + "type": "keyword" + }, + "latest_index": { + "type": "integer" + }, + "next_draft_id": { + "type": "keyword" + } + } } } } diff --git a/invenio_records_marc21/records/mappings/v7/marc21records/marc21/marc21-v1.0.0.json b/invenio_records_marc21/records/mappings/v7/marc21records/marc21/marc21-v1.0.0.json index 1f02dc1c..575588f3 100644 --- a/invenio_records_marc21/records/mappings/v7/marc21records/marc21/marc21-v1.0.0.json +++ b/invenio_records_marc21/records/mappings/v7/marc21records/marc21/marc21-v1.0.0.json @@ -3,40 +3,126 @@ "date_detection": false, "numeric_detection": false, "properties": { - "conceptid" : { - "type" : "keyword" - }, "id": { "type": "keyword" }, - "access" : { - "properties" : { - "access_right" : { - "type" : "keyword" + "access": { + "properties": { + "metadata": { + "type": "keyword" + }, + "files": { + "type": "keyword" + }, + "embargo": { + "properties": { + "active": { + "type": "boolean" + }, + "until": { + "type": "date" + }, + "reason": { + "type": "text" + } + } }, - "created_by" : { - "type" : "long" + "owned_by": { + "properties": { + "user": { + "type": "keyword" + } + } + }, + "status": { + "type": "keyword" + } + } + }, + "parent": { + "properties": { + "id": { + "type": "keyword" + }, + "access": { + "properties": { + "owned_by": { + "properties": { + "user": { + "type": "keyword" + } + } + } + } + } + } + }, + "pids": { + "properties": { + "identifier": { + "type": "keyword" }, - "files_restricted" : { - "type" : "boolean" + "scheme": { + "type": "keyword" }, - "metadata_restricted" : { - "type" : "boolean" + "client": { + "type": "keyword", + "index": false }, - "owners" : { - "type" : "long" + "provider": { + "type": "keyword", + "index": false } } }, - "metadata" : { - "properties" : { + "has_draft": { + "type": "boolean" + }, + "metadata": { + "properties": { + "json": { + "properties": { + "title_statement": { + "properties": { + "title": { + "type": "keyword" + } + } + } + } + } } }, - "created" : { - "type" : "date" + "created": { + "type": "date" + }, + "updated": { + "type": "date" }, - "updated" : { - "type" : "date" + "is_published": { + "type": "boolean" + }, + "versions": { + "properties": { + "index": { + "type": "integer" + }, + "is_latest": { + "type": "boolean" + }, + "is_latest_draft": { + "type": "boolean" + }, + "latest_id": { + "type": "keyword" + }, + "latest_index": { + "type": "integer" + }, + "next_draft_id": { + "type": "keyword" + } + } } } } From 69d5dd65ec17162b83fe7a6ac112e262915f7e3d Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 2 Aug 2021 07:05:16 +0200 Subject: [PATCH 046/217] build: update dependencies --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index d64b30b0..014d415f 100755 --- a/setup.py +++ b/setup.py @@ -55,8 +55,8 @@ "dojson>=1.4.0", "lxml>=4.6.2", "invenio-records-rest>=1.5.0,<2.0.0", - "invenio-drafts-resources>=0.12.0,<0.13.0", - "invenio-vocabularies>=0.6.0,<0.7.0", + "invenio-drafts-resources>=0.13.0,<0.14.0", + "invenio-vocabularies>=0.7.0,<0.8.0", ] packages = find_packages() From c717bd5d9f5367685452c50c959c2a652d06f5e0 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 2 Aug 2021 08:42:13 +0200 Subject: [PATCH 047/217] modification: update imports removing unused imports from semantic-ui-react and react-searchkit and adding missing imports --- .../js/invenio_records_marc21/search/components.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/components.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/components.js index 89e5af46..ae415102 100755 --- a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/components.js +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/components.js @@ -9,19 +9,21 @@ import { Button, Card, Checkbox, - Grid, Icon, Input, Item, Label, List, } from "semantic-ui-react"; -import { BucketAggregation, Toggle } from "react-searchkit"; -import _find from "lodash/find"; +import { BucketAggregation } from "react-searchkit"; import _get from "lodash/get"; import _truncate from "lodash/truncate"; import Overridable from "react-overridable"; -import { SearchBar } from "@js/invenio_search_ui/components"; +import { SearchBar, SearchApp } from "@js/invenio_search_ui/components"; + +import _camelCase from "lodash/camelCase"; +import { loadComponents } from "@js/invenio_theme/templates"; +import ReactDOM from "react-dom"; export const Marc21RecordResultsListItem = ({ result, index }) => { From 97d7c899723998acbc3e1a843a43c37ca2599ad6 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 9 Aug 2021 11:46:01 +0200 Subject: [PATCH 048/217] build: update dependencies --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 014d415f..48c86d0b 100755 --- a/setup.py +++ b/setup.py @@ -56,7 +56,7 @@ "lxml>=4.6.2", "invenio-records-rest>=1.5.0,<2.0.0", "invenio-drafts-resources>=0.13.0,<0.14.0", - "invenio-vocabularies>=0.7.0,<0.8.0", + "invenio-vocabularies>=0.8.0,<0.9.0", ] packages = find_packages() From 7b36f976991c1d49721e857caa88f86af8d7183a Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 9 Aug 2021 12:03:55 +0200 Subject: [PATCH 049/217] bugfix: use explicit record models using explicit model module for the records, since some models classes have the same name as the records --- invenio_records_marc21/records/api.py | 43 +++++++++++---------------- 1 file changed, 18 insertions(+), 25 deletions(-) diff --git a/invenio_records_marc21/records/api.py b/invenio_records_marc21/records/api.py index 26d6a5e3..ce3a49cf 100644 --- a/invenio_records_marc21/records/api.py +++ b/invenio_records_marc21/records/api.py @@ -21,14 +21,8 @@ ) from werkzeug.local import LocalProxy -from .models import ( - DraftFile, - DraftMetadata, - ParentMetadata, - RecordFile, - RecordMetadata, - VersionsState, -) +from . import models + from .systemfields import ( MarcDraftProvider, MarcPIDFieldContext, @@ -43,8 +37,8 @@ class Marc21Parent(BaseParentRecord): """Parent record.""" - versions_model_cls = VersionsState - model_cls = ParentMetadata + versions_model_cls = models.VersionsState + model_cls = models.ParentMetadata schema = ConstantField("$schema", "local://marc21/parent-v1.0.0.json") @@ -56,12 +50,18 @@ class Marc21Parent(BaseParentRecord): delete=False, ) +class DraftFile(BaseFileRecord): + """Marc21 file associated with a marc21 draft model.""" + + model_cls = models.DraftFile + record_cls = LocalProxy(lambda: Marc21Draft) + class Marc21Draft(Draft): """Marc21 draft API.""" - model_cls = DraftMetadata - versions_model_cls = VersionsState + model_cls = models.DraftMetadata + versions_model_cls = models.VersionsState parent_record_cls = Marc21Parent index = IndexField( @@ -89,18 +89,18 @@ class Marc21Draft(Draft): bucket = ModelField(dump=False) -class DraftFile(BaseFileRecord): - """Marc21 file associated with a marc21 draft model.""" +class RecordFile(BaseFileRecord): + """Marc21 record file API.""" - model_cls = DraftFile - record_cls = LocalProxy(lambda: Marc21Draft) + model_cls = models.RecordFile + record_cls = LocalProxy(lambda: Marc21Record) class Marc21Record(Record): """Define API for Marc21 create and manipulate.""" - model_cls = RecordMetadata - versions_model_cls = VersionsState + model_cls = models.RecordMetadata + versions_model_cls = models.VersionsState parent_record_cls = Marc21Parent index = IndexField( @@ -127,10 +127,3 @@ class Marc21Record(Record): bucket_id = ModelField(dump=False) bucket = ModelField(dump=False) - - -class RecordFile(BaseFileRecord): - """Marc21 record file API.""" - - model_cls = RecordFile - record_cls = LocalProxy(lambda: Marc21Record) From 81acdcc96a2926d4d1cf4499c16545ea0e694470 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 9 Aug 2021 12:26:19 +0200 Subject: [PATCH 050/217] modification: update parent access shema removing all unnecessary field and keep only owned_by field --- .../jsonschemas/marc21/parent-v1.0.0.json | 46 ------------------- .../services/schemas/access/parent.py | 6 +-- 2 files changed, 2 insertions(+), 50 deletions(-) diff --git a/invenio_records_marc21/records/jsonschemas/marc21/parent-v1.0.0.json b/invenio_records_marc21/records/jsonschemas/marc21/parent-v1.0.0.json index 585facf0..6de7a86f 100644 --- a/invenio_records_marc21/records/jsonschemas/marc21/parent-v1.0.0.json +++ b/invenio_records_marc21/records/jsonschemas/marc21/parent-v1.0.0.json @@ -21,59 +21,13 @@ "description": "Record access control and ownership.", "additionalProperties": false, "properties": { - "metadata": { - "description": "Metadata visibility (public or restricted)", - "type": "string", - "enum": [ - "public", - "embargoed", - "restricted" - ] - }, - "files": { - "description": "Files visibility (public or restricted)", - "type": "string", - "enum": [ - "public", - "embargoed", - "restricted" - ] - }, "owned_by": { "description": "List of user IDs that are owners of the record.", "type": "array", - "minItems": 1, "uniqueItems": true, "items": { "$ref": "local://marc21/definitions-v1.0.0.json#/agent" } - }, - "embargo": { - "description": "Embargo date of record (ISO8601 formatted date time in UTC). At this time both metadata and files will be made public.", - "type": "object", - "additionalProperties": false, - "properties": { - "active": { - "type": [ - "boolean", - "null" - ] - }, - "until": { - "description": "Embargo date of record (ISO8601 formatted date time in UTC). At this time both metadata and files will be made public.", - "type": [ - "string", - "null" - ] - }, - "reason": { - "description": "The reason why the record is under embargo.", - "type": [ - "string", - "null" - ] - } - } } } } diff --git a/invenio_records_marc21/services/schemas/access/parent.py b/invenio_records_marc21/services/schemas/access/parent.py index 7926464a..082be4af 100644 --- a/invenio_records_marc21/services/schemas/access/parent.py +++ b/invenio_records_marc21/services/schemas/access/parent.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # # Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it @@ -25,10 +27,6 @@ class Agent(Schema): class ParentAccessSchema(Schema): """Access schema.""" - metadata = SanitizedUnicode(required=True) - files = SanitizedUnicode(required=True) - status = SanitizedUnicode(dump_only=False) - embargo = NestedAttribute(EmbargoSchema) owned_by = List(fields.Nested(Agent)) @validates_schema From 624b18e76ff11922fca63bfc31f07ed5ce112b9e Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 9 Aug 2021 12:54:24 +0200 Subject: [PATCH 051/217] feature: add access systemfield adding the record access system field to marc21 records with the properties embargo, owners and status and the protection of the record. --- invenio_records_marc21/records/api.py | 9 +- .../records/systemfields/access/__init__.py | 34 ++++ .../records/systemfields/access/embargo.py | 124 ++++++++++++ .../systemfields/access/fields/__init__.py | 21 ++ .../systemfields/access/fields/parent.py | 144 ++++++++++++++ .../systemfields/access/fields/record.py | 187 ++++++++++++++++++ .../records/systemfields/access/owners.py | 126 ++++++++++++ .../records/systemfields/access/protection.py | 75 +++++++ .../records/systemfields/access/status.py | 27 +++ 9 files changed, 746 insertions(+), 1 deletion(-) create mode 100644 invenio_records_marc21/records/systemfields/access/__init__.py create mode 100755 invenio_records_marc21/records/systemfields/access/embargo.py create mode 100644 invenio_records_marc21/records/systemfields/access/fields/__init__.py create mode 100755 invenio_records_marc21/records/systemfields/access/fields/parent.py create mode 100755 invenio_records_marc21/records/systemfields/access/fields/record.py create mode 100755 invenio_records_marc21/records/systemfields/access/owners.py create mode 100644 invenio_records_marc21/records/systemfields/access/protection.py create mode 100644 invenio_records_marc21/records/systemfields/access/status.py diff --git a/invenio_records_marc21/records/api.py b/invenio_records_marc21/records/api.py index ce3a49cf..5f7198ad 100644 --- a/invenio_records_marc21/records/api.py +++ b/invenio_records_marc21/records/api.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # # invenio-records-marc21 is free software; you can redistribute it and/or modify it @@ -22,13 +24,13 @@ from werkzeug.local import LocalProxy from . import models - from .systemfields import ( MarcDraftProvider, MarcPIDFieldContext, MarcRecordProvider, MarcResolver, ) +from .systemfields.access import ParentRecordAccessField, RecordAccessField # @@ -49,6 +51,8 @@ class Marc21Parent(BaseParentRecord): resolver_cls=MarcResolver, delete=False, ) + access = ParentRecordAccessField() + class DraftFile(BaseFileRecord): """Marc21 file associated with a marc21 draft model.""" @@ -83,6 +87,7 @@ class Marc21Draft(Draft): file_cls=DraftFile, delete=False, ) + access = RecordAccessField() bucket_id = ModelField(dump=False) @@ -124,6 +129,8 @@ class Marc21Record(Record): delete=False, ) + access = RecordAccessField() + bucket_id = ModelField(dump=False) bucket = ModelField(dump=False) diff --git a/invenio_records_marc21/records/systemfields/access/__init__.py b/invenio_records_marc21/records/systemfields/access/__init__.py new file mode 100644 index 00000000..7475caeb --- /dev/null +++ b/invenio_records_marc21/records/systemfields/access/__init__.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it +# under the terms of the MIT License; see LICENSE file for more details. + + +"""Access module.""" + +from .embargo import Embargo +from .fields import ( + ParentRecordAccess, + ParentRecordAccessField, + RecordAccess, + RecordAccessField, +) +from .owners import Owner, Owners +from .protection import Protection +from .status import AccessStatusEnum + +__all__ = ( + "AccessStatusEnum", + "Embargo", + "ParentRecordAccessField", + "ParentRecordAccess", + "RecordAccessField", + "RecordAccess", + "Owner", + "Owners", + "Protection", +) diff --git a/invenio_records_marc21/records/systemfields/access/embargo.py b/invenio_records_marc21/records/systemfields/access/embargo.py new file mode 100755 index 00000000..0e2422a4 --- /dev/null +++ b/invenio_records_marc21/records/systemfields/access/embargo.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# +# Copyright (C) 2021 TU Wien. +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it +# under the terms of the MIT License; see LICENSE file for more details. + +"""Embargo class for the access system field.""" + +import arrow + + +class Embargo: + """Embargo class for the access system field.""" + + def __init__(self, until=None, reason=None, active=None): + """Create a new Embargo.""" + self.until = until + if isinstance(until, str): + self.until = arrow.get(until).datetime + + self.reason = reason + self._active = active + + @property + def active(self): + """Whether or not the Embargo is (still) active.""" + if self._active is not None: + return self._active + + elif self.until is None: + return False + + return arrow.utcnow().datetime < self.until + + @active.setter + def active(self, value): + """Set the Embargo's (boolean) active state.""" + self._active = bool(value) + + def lift(self): + """Update the embargo active status if it has expired. + + Returns ``True`` if the embargo was actually lifted (i.e. it was + expired but still marked as active). + """ + if self.until is not None: + if arrow.utcnow().datetime > self.until: + was_active = bool(self._active) + self.active = False + return was_active + + return False + + def clear(self): + """Clear any information if the embargo was ever active.""" + self.until = None + self.reason = None + self._active = None + + def dump(self): + """Dump the embargo as dictionary.""" + until_str = None + if self.until is not None: + until_str = self.until.strftime("%Y-%m-%d") + + return { + "active": self.active, + "until": until_str, + "reason": self.reason, + } + + def __repr__(self): + """Return repr(self).""" + if self == Embargo(): + return "" + + until_str = self.until or "n/a" + + return "<{} (active: {}, until: {}, reason: {})>".format( + type(self).__name__, self.active, until_str, self.reason + ) + + def __eq__(self, other): + """Return self == other.""" + if type(self) != type(other): + return False + + return ( + self.reason == other.reason and self.until == other.until and self.active == other.active + ) + + def __ne__(self, other): + """Return self != other.""" + return not (self == other) + + def __bool__(self): + """Return bool(self).""" + return self.active + + @classmethod + def from_dict(cls, dict_, ignore_active_value=False): + """Parse the Embargo from the given dictionary.""" + if not dict_: + return cls() + + until = dict_.get("until") + if until: + until = arrow.get(until).datetime + + reason = dict_.get("reason") + active = dict_.get("active") + if ignore_active_value: + # with ignore_active_value, the 'active' value is re-calculated + # instead of parsed + active = None + + if not until and not reason and not active: + return cls() + + return cls(until=until, reason=reason, active=active) diff --git a/invenio_records_marc21/records/systemfields/access/fields/__init__.py b/invenio_records_marc21/records/systemfields/access/fields/__init__.py new file mode 100644 index 00000000..04b87d78 --- /dev/null +++ b/invenio_records_marc21/records/systemfields/access/fields/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it +# under the terms of the MIT License; see LICENSE file for more details. + + +"""System access fields module.""" + +from .parent import ParentRecordAccess, ParentRecordAccessField +from .record import RecordAccess, RecordAccessField + +__all__ = ( + "ParentRecordAccessField", + "ParentRecordAccess", + "RecordAccessField", + "RecordAccess", +) diff --git a/invenio_records_marc21/records/systemfields/access/fields/parent.py b/invenio_records_marc21/records/systemfields/access/fields/parent.py new file mode 100755 index 00000000..86e3122a --- /dev/null +++ b/invenio_records_marc21/records/systemfields/access/fields/parent.py @@ -0,0 +1,144 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# +# Copyright (C) 2021 TU Wien. +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it +# under the terms of the MIT License; see LICENSE file for more details. + +"""Access system field.""" + +from invenio_records.systemfields import SystemField + +from ..owners import Owners + + +class ParentRecordAccess: + """Access management for all versions of a record.""" + + owners_cls = Owners + + def __init__( + self, + owned_by=None, + owners_cls=None, + ): + """Create a new Access object for a record. + + If ``owned_by`` are not specified, + a new instance of ``owners_cls`` + will be used, respectively. + :param owned_by: The set of record owners + """ + owners_cls = owners_cls or ParentRecordAccess.owners_cls + self.owned_by = owned_by if owned_by else owners_cls() + self.errors = [] + + @property + def owners(self): + """An alias for the owned_by property.""" + return self.owned_by + + def dump(self): + """Dump the field values as dictionary.""" + access = { + "owned_by": self.owned_by.dump(), + } + + return access + + def refresh_from_dict(self, access_dict): + """Re-initialize the Access object with the data in the access_dict.""" + new_access = self.from_dict(access_dict) + self.errors = new_access.errors + self.owned_by = new_access.owned_by + + @classmethod + def from_dict( + cls, + access_dict, + owners_cls=None, + ): + """Create a new Access object from the specified 'access' property. + + The new ``ParentRecordAccess`` object will be populated with new + instances from the configured classes. + If ``access_dict`` is empty, the ``ParentRecordAccess`` object will + be populated with new instances of ``owners_cls``. + """ + owners_cls = owners_cls or cls.owners_cls + errors = [] + + owners = owners_cls() + + if access_dict: + for owner_dict in access_dict.get("owned_by", []): + try: + owners.add(owners.owner_cls(owner_dict)) + except Exception as e: + errors.append(e) + + access = cls( + owned_by=owners, + ) + access.errors = errors + return access + + def __repr__(self): + """Return repr(self).""" + return ("<{} (owners: {})>").format( + type(self).__name__, + len(self.owners or []), + ) + + +class ParentRecordAccessField(SystemField): + """System field for managing record access.""" + + def __init__(self, key="access", access_obj_class=ParentRecordAccess): + """Create a new ParentRecordAccessField instance.""" + self._access_obj_class = access_obj_class + super().__init__(key=key) + + def obj(self, instance): + """Get the access object.""" + data = self.get_dictkey(instance) + obj = self._get_cache(instance) + if obj is not None and data is None: + return obj + + if data: + obj = self._access_obj_class.from_dict(data) + else: + obj = self._access_obj_class() + + self._set_cache(instance, obj) + return obj + + def set_obj(self, record, obj): + """Set the access object.""" + if isinstance(obj, dict): + obj = self._access_obj_class.from_dict(obj) + + assert isinstance(obj, self._access_obj_class) + + self._set_cache(record, obj) + + def __get__(self, record, owner=None): + """Get the record's access object.""" + if record is None: + return self + + return self.obj(record) + + def __set__(self, record, obj): + """Set the records access object.""" + self.set_obj(record, obj) + + def pre_commit(self, record): + """Dump the configured values before the record is committed.""" + obj = self.obj(record) + if obj is not None: + record["access"] = obj.dump() diff --git a/invenio_records_marc21/records/systemfields/access/fields/record.py b/invenio_records_marc21/records/systemfields/access/fields/record.py new file mode 100755 index 00000000..453c4e63 --- /dev/null +++ b/invenio_records_marc21/records/systemfields/access/fields/record.py @@ -0,0 +1,187 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# +# Copyright (C) 2021 TU Wien. +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it +# under the terms of the MIT License; see LICENSE file for more details. + +"""Access system field.""" + +from enum import Enum + +from invenio_records.systemfields import SystemField + +from ..embargo import Embargo +from ..protection import Protection +from ..status import AccessStatusEnum + + +class RecordAccess: + """Access management per record.""" + + protection_cls = Protection + embargo_cls = Embargo + + def __init__( + self, + protection=None, + embargo=None, + protection_cls=None, + embargo_cls=None, + has_files=None, + ): + """Create a new RecordAccess object for a record. + + If ``protection`` or ``embargo`` are not specified, + a new instance of ``protection_cls`` or ``embargo_cls`` + will be used, respectively. + :param protection: The record and file protection levels + :param embargo: The embargo on the record (None means no embargo) + """ + protection_cls = protection_cls or RecordAccess.protection_cls + embargo_cls = embargo_cls or RecordAccess.embargo_cls + + public = protection_cls("public", "public") + self.protection = protection if protection is not None else public + self.embargo = embargo if embargo is not None else embargo_cls() + self.has_files = has_files + self.errors = [] + + @property + def status(self): + """Record's access status.""" + status = AccessStatusEnum.RESTRICTED + + if self.embargo.active: + status = AccessStatusEnum.EMBARGOED + elif self.protection.metadata == self.protection.files == "public": + status = AccessStatusEnum.PUBLIC + + return status + + def dump(self): + """Dump the field values as dictionary.""" + access = { + "metadata": self.protection.metadata, + "files": self.protection.files, + "embargo": self.embargo.dump(), + } + + return access + + def refresh_from_dict(self, access_dict): + """Re-initialize the Access object with the data in the access_dict.""" + new_access = self.from_dict(access_dict) + self.protection = new_access.protection + self.embargo = new_access.embargo + self.errors = new_access.errors + + @classmethod + def from_dict( + cls, access_dict, protection_cls=None, embargo_cls=None, has_files=None + ): + """Create a new Access object from the specified 'access' property. + + The new ``RecordAccess`` object will be populated with new instances + from the configured classes. + If ``access_dict`` is empty, the ``Access`` object will be populated + with new instances of ``protection_cls`` and ``embargo_cls``. + """ + protection_cls = protection_cls or cls.protection_cls + embargo_cls = embargo_cls or cls.embargo_cls + errors = [] + + protection = protection_cls() + embargo = embargo_cls() + + if access_dict: + try: + protection = protection_cls( + access_dict["metadata"], access_dict["files"] + ) + except Exception as e: + errors.append(e) + + embargo_dict = access_dict.get("embargo") + if embargo_dict is not None: + embargo = embargo_cls.from_dict(embargo_dict) + + access = cls( + protection=protection, + embargo=embargo, + has_files=has_files, + ) + access.errors = errors + + return access + + def __repr__(self): + """Return repr(self).""" + protection_str = "{}/{}".format(self.protection.record, self.protection.files) + + return ("<{} (protection: {}, {})>").format( + type(self).__name__, + protection_str, + self.embargo, + ) + + +class RecordAccessField(SystemField): + """System field for managing record access.""" + + def __init__(self, key="access", access_obj_class=RecordAccess): + """Create a new RecordAccessField instance.""" + self._access_obj_class = access_obj_class + super().__init__(key=key) + + def obj(self, instance): + """Get the access object.""" + obj = self._get_cache(instance) + if obj is not None: + return obj + + data = self.get_dictkey(instance) + if data: + obj = self._access_obj_class.from_dict(data, has_files=len(instance.files)) + else: + obj = self._access_obj_class() + + self._set_cache(instance, obj) + return obj + + def set_obj(self, record, obj): + """Set the access object.""" + if isinstance(obj, dict): + obj = self._access_obj_class.from_dict(obj) + + assert isinstance(obj, self._access_obj_class) + self._set_cache(record, obj) + + def __get__(self, record, owner=None): + """Get the record's access object.""" + if record is None: + return self + return self.obj(record) + + def __set__(self, metadata, obj): + """Set the records access object.""" + self.set_obj(metadata, obj) + + def pre_commit(self, record): + """Dump the configured values before the record is committed.""" + obj = self.obj(record) + if obj is not None: + record["access"] = obj.dump() + + def post_dump(self, record, data, dumper=None): + """Called before a record is dumped.""" + if data.get("access") and isinstance(data.get("access"), dict): + data["access"]["status"] = record.access.status.value + + def pre_load(self, data, loader=None): + """Called before a record is dumped.""" + if data.get("access") and isinstance(data.get("access"), dict): + data["access"].pop("status", None) diff --git a/invenio_records_marc21/records/systemfields/access/owners.py b/invenio_records_marc21/records/systemfields/access/owners.py new file mode 100755 index 00000000..a95d2780 --- /dev/null +++ b/invenio_records_marc21/records/systemfields/access/owners.py @@ -0,0 +1,126 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# +# Copyright (C) 2021 TU Wien. +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it +# under the terms of the MIT License; see LICENSE file for more details. + +"""Owners classes for the access system field.""" + +from invenio_accounts.models import User + + +class Owner: + """An abstraction between owner entities and specifications as dicts.""" + + def __init__(self, owner): + """Create an owner from either a dictionary or a User.""" + self._entity = None + self.owner_type = None + self.owner_id = None + + if isinstance(owner, dict): + if "user" in owner: + self.owner_type = "user" + self.owner_id = owner["user"] + + else: + raise ValueError("unknown owner type: {}".format(owner)) + + elif isinstance(owner, User): + self._entity = owner + self.owner_id = owner.id + self.owner_type = "user" + + else: + raise TypeError("invalid owner type: {}".format(type(owner))) + + def dump(self): + """Dump the owner to a dictionary.""" + return {self.owner_type: self.owner_id} + + def resolve(self, raise_exc=False): + """Resolve the owner entity (e.g. User) via a database query.""" + if self._entity is None: + if self.owner_type == "user": + self._entity = User.query.get(self.owner_id) + + else: + raise ValueError("unknown owner type: {}".format(self.owner_type)) + + if self._entity is None and raise_exc: + raise LookupError("could not find owner: {}".format(self.dump())) + + return self._entity + + def __hash__(self): + """Return hash(self).""" + return hash(self.owner_type) + hash(self.owner_id) + + def __eq__(self, other): + """Return self == other.""" + if type(self) != type(other): + return False + + return self.owner_type == other.owner_type and self.owner_id == other.owner_id + + def __ne__(self, other): + """Return self != other.""" + return not self == other + + def __str__(self): + """Return str(self).""" + return str(self.resolve()) + + def __repr__(self): + """Return repr(self).""" + return repr(self.resolve()) + + +class Owners(list): + """A list of owners for a record.""" + + owner_cls = Owner + + def __init__(self, owners=None, owner_cls=None): + """Create a new list of owners.""" + self.owner_cls = owner_cls or Owners.owner_cls + for owner in owners or []: + self.add(owner) + + def add(self, owner): + """Alias for self.append(owner).""" + self.append(owner) + + def append(self, owner): + """Add the specified owner to the list of owners. + + :param owner: The record's owner (either a dict, User or Owner). + """ + if not isinstance(owner, self.owner_cls): + owner = self.owner_cls(owner) + + if owner not in self: + super().append(owner) + + def extend(self, owners): + """Add all new items from the specified owners to this list.""" + for owner in owners: + self.add(owner) + + def remove(self, owner): + """Remove the specified owner from the list of owners. + + :param owner: The record's owner (either a dict, User or Owner). + """ + if not isinstance(owner, self.owner_cls): + owner = self.owner_cls(owner) + + super().remove(owner) + + def dump(self): + """Dump the owners as a list of owner dictionaries.""" + return [owner.dump() for owner in self] diff --git a/invenio_records_marc21/records/systemfields/access/protection.py b/invenio_records_marc21/records/systemfields/access/protection.py new file mode 100644 index 00000000..61db7d2e --- /dev/null +++ b/invenio_records_marc21/records/systemfields/access/protection.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# +# Copyright (C) 2021 TU Wien. +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it +# under the terms of the MIT License; see LICENSE file for more details. + +"""Protection class for the access system field.""" + + +class Protection: + """Protection class for the access system field.""" + + def __init__(self, metadata="public", files="public"): + """Create a new protection levels instance.""" + self.set(metadata=metadata, files=files) + + def _validate_protection_level(self, level): + return level in ("public", "embargoed", "restricted") + + @property + def metadata(self): + """Get the record's overall protection level.""" + return self._metadata + + @metadata.setter + def metadata(self, value): + """Set the record's overall protection level.""" + if not self._validate_protection_level(value): + raise ValueError("unknown metadata protection level: {}".format(value)) + + if value == "restricted": + self._files = "restricted" + + self._metadata = value + + @property + def files(self): + """Get the record's files protection level.""" + return self._files + + @files.setter + def files(self, value): + """Set the record's files protection level.""" + if not self._validate_protection_level(value): + raise ValueError("unknown files protection level: {}".format(value)) + + if self.metadata == "restricted": + self._files = "restricted" + else: + self._files = value + + def set(self, metadata, files=None): + """Set the protection level for record and files.""" + self.metadata = metadata + if files is not None: + self.files = files + + def __get__(self): + """Get the protection level of the record and its files.""" + return { + "metadata": self.metadata, + "files": self.files, + } + + def __repr__(self): + """Return repr(self).""" + return "<{} (metadata: {}, files: {})>".format( + type(self).__name__, + self.metadata, + self.files, + ) diff --git a/invenio_records_marc21/records/systemfields/access/status.py b/invenio_records_marc21/records/systemfields/access/status.py new file mode 100644 index 00000000..9016c4a1 --- /dev/null +++ b/invenio_records_marc21/records/systemfields/access/status.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it +# under the terms of the MIT License; see LICENSE file for more details. + +"""Marc21 access status.""" + +from enum import Enum + + +class AccessStatusEnum(Enum): + """Enum defining access statuses.""" + + PUBLIC = "public" + + EMBARGOED = "embargoed" + + RESTRICTED = "restricted" + + @staticmethod + def list(): + """List all access statuses.""" + return list(map(lambda c: c.value, AccessStatusEnum)) From 4cb781e9f585e090f23021057464a31341af5733 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 9 Aug 2021 14:02:02 +0200 Subject: [PATCH 052/217] modification: moved access status enum --- .../services/components/access.py | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/invenio_records_marc21/services/components/access.py b/invenio_records_marc21/services/components/access.py index c79196fb..73108050 100644 --- a/invenio_records_marc21/services/components/access.py +++ b/invenio_records_marc21/services/components/access.py @@ -11,22 +11,7 @@ from invenio_records_resources.services.records.components import ServiceComponent - -class AccessStatusEnum(Enum): - """Enum defining access statuses.""" - - PUBLIC = "public" - - EMBARGOED = "embargoed" - - RESTRICTED = "restricted" - - # METADATA_ONLY = "metadata-only" - - @staticmethod - def list(): - """List all access statuses.""" - return list(map(lambda c: c.value, AccessStatusEnum)) +from ...records.systemfields.access import AccessStatusEnum class AccessComponent(ServiceComponent): From c674ab30e4315bbf40bea501c24cd7ccc72283f2 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 9 Aug 2021 14:11:47 +0200 Subject: [PATCH 053/217] feature: set access property setting the access property in a record and parent and refresh or dump and call modifications according to the defined schema --- .../services/components/access.py | 18 ++++++++++++++++++ .../services/schemas/access/record.py | 14 ++++++++++++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/invenio_records_marc21/services/components/access.py b/invenio_records_marc21/services/components/access.py index 73108050..fa5b7ca7 100644 --- a/invenio_records_marc21/services/components/access.py +++ b/invenio_records_marc21/services/components/access.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # # Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it @@ -26,11 +28,15 @@ def _default_access(self, identity, data, record, **kwargs): "files": AccessStatusEnum.PUBLIC.value, }, } + if record is not None and "access" in data: + record.update({"access": data.get("access")}) + record.access.refresh_from_dict(record.get("access")) record.parent.update({"access": data.get("access")}) record.parent.commit() elif "access" not in data: data.update(default_access) + record.access.refresh_from_dict(record.get("access")) record.parent.update({"access": data.get("access")}) record.parent.commit() @@ -53,3 +59,15 @@ def update_draft(self, identity, data=None, record=None, **kwargs): def update(self, identity, data=None, record=None, **kwargs): """Update handler.""" self._init_owned_by(identity, record, **kwargs) + + def publish(self, identity, draft=None, record=None, **kwargs): + """Update draft metadata.""" + record.access = draft.access + + def edit(self, identity, draft=None, record=None, **kwargs): + """Update draft metadata.""" + draft.access = record.access + + def new_version(self, identity, draft=None, record=None, **kwargs): + """Update draft metadata.""" + draft.access = record.access diff --git a/invenio_records_marc21/services/schemas/access/record.py b/invenio_records_marc21/services/schemas/access/record.py index 7634ccf6..a56f981d 100644 --- a/invenio_records_marc21/services/schemas/access/record.py +++ b/invenio_records_marc21/services/schemas/access/record.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # # Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it @@ -43,9 +45,17 @@ def validate_protection_value(self, value, field_name): "record", ) + def get_attribute(self, obj, key, default): + """Override from where we get attributes when serializing.""" + if key in ["metadata", "files"]: + return getattr(obj.protection, key, default) + elif key == "status": + return obj.status.value + return getattr(obj, key, default) + @validates("metadata") - def validate_record_protection(self, value): - """Validate the record protection value.""" + def validate_metadata_protection(self, value): + """Validate the metadata protection value.""" self.validate_protection_value(value, "metadata") @validates_schema From f493639a50d4248a192edd1de41fe18ec0b609a5 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 9 Aug 2021 14:29:43 +0200 Subject: [PATCH 054/217] test: test-set for access component adding a test-set for the systemfield access in a record and update the test-set for the record service. --- tests/conftest.py | 50 ++++++ tests/records/fields/conftest.py | 19 +++ .../records/fields/test_systemfield_access.py | 159 ++++++++++++++++++ tests/services/conftest.py | 22 ++- tests/services/schemas/test_access.py | 128 +++++++++++++- tests/services/test_create_record.py | 39 ++++- tests/services/test_record_service.py | 84 ++++----- 7 files changed, 442 insertions(+), 59 deletions(-) mode change 100644 => 100755 tests/conftest.py mode change 100644 => 100755 tests/records/fields/conftest.py create mode 100755 tests/records/fields/test_systemfield_access.py mode change 100644 => 100755 tests/services/conftest.py mode change 100644 => 100755 tests/services/schemas/test_access.py mode change 100644 => 100755 tests/services/test_create_record.py diff --git a/tests/conftest.py b/tests/conftest.py old mode 100644 new mode 100755 index 3abcb855..da8b256d --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # # Invenio-Records-Marc21 is free software; you can redistribute it and/or @@ -13,16 +15,64 @@ """ import tempfile +from datetime import timedelta +import arrow import pytest from flask import Flask from flask_babelex import Babel from invenio_files_rest.models import Location from invenio_records_marc21 import InvenioRecordsMARC21 +from invenio_records_marc21.proxies import current_records_marc21 +from invenio_records_marc21.records import Marc21Draft from invenio_records_marc21.views import create_record_bp +@pytest.fixture() +def embargoed_record(): + """Embargoed record.""" + embargoed_record = { + "metadata": {"json": "test"}, + "access": { + "files": "restricted", + "status": "embargoed", + "embargo": { + "active": True, + "until": (arrow.utcnow().datetime + timedelta(days=2)).strftime("%Y-%m-%d"), + "reason": None, + }, + }, + } + return embargoed_record + + +@pytest.fixture() +def marc21_record(): + """Normal record.""" + marc21_record = { + "metadata": {"json": "test"}, + "access": { + "files": "public", + "status": "public", + "metadata": "public", + "embargo": { + "active": False, + "reason": None, + }, + }, + } + return marc21_record + + +@pytest.fixture() +def example_record(app, db): + """Example record.""" + record = Marc21Draft.create({}, metadata={"title": "Test"}) + db.session.commit() + return record + + @pytest.fixture(scope="module") def celery_config(): """Override pytest-invenio fixture. diff --git a/tests/records/fields/conftest.py b/tests/records/fields/conftest.py old mode 100644 new mode 100755 index 4dadbf2f..7f7b6bcf --- a/tests/records/fields/conftest.py +++ b/tests/records/fields/conftest.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # # Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it @@ -16,6 +18,8 @@ import pytest from flask import Flask +from flask_security.utils import hash_password +from invenio_accounts import InvenioAccounts from invenio_db import InvenioDB from invenio_files_rest import InvenioFilesREST from invenio_files_rest.models import Location @@ -23,6 +27,7 @@ from invenio_records import InvenioRecords from invenio_records_marc21 import InvenioRecordsMARC21 +from invenio_records_marc21.records import Marc21Parent @pytest.fixture(scope="module") @@ -58,3 +63,17 @@ def testapp(base_app, database): InvenioRecords(base_app) InvenioJSONSchemas(base_app) yield base_app + + +@pytest.fixture(scope="module") +def appaccess(base_app, database): + """Create App systemfields.""" + InvenioRecords(base_app) + InvenioJSONSchemas(base_app) + yield base_app + + +@pytest.fixture() +def parent(app, db): + """A parent record.""" + return Marc21Parent.create({}) diff --git a/tests/records/fields/test_systemfield_access.py b/tests/records/fields/test_systemfield_access.py new file mode 100755 index 00000000..0148f2d9 --- /dev/null +++ b/tests/records/fields/test_systemfield_access.py @@ -0,0 +1,159 @@ +# +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it +# under the terms of the MIT License; see LICENSE file for more details. + +"""Test access system field.""" + + +from datetime import timedelta + +import arrow +import pytest + +from invenio_records_marc21.records import Marc21Record +from invenio_records_marc21.records.systemfields.access import ( + Embargo, + Protection, + RecordAccess, +) + +# +# Protection +# + + +def test_protection_valid(): + p = Protection("public", "public") + assert p.metadata == "public" + assert p.files == "public" + p.set("restricted", files="restricted") + assert p.metadata == "restricted" + assert p.files == "restricted" + + +def test_protection_invalid_values(): + with pytest.raises(ValueError): + Protection("invalid", "values") + + +# +# Embargo +# + + +def test_embargo_creation(): + future_date = arrow.utcnow().datetime + timedelta(days=+30) + embargo = Embargo( + until=future_date, + reason="Because I can!", + ) + assert embargo.active + + past_date = arrow.utcnow().datetime + timedelta(days=-30) + embargo = Embargo( + until=past_date, + reason="espionage", + ) + assert not embargo.active + + +def test_embargo_from_dict(): + date_feature = arrow.utcnow().datetime + timedelta(days=+30) + embargo_dict = { + "until": date_feature, + "active": True, + "reason": "espionage", + } + embargo = Embargo.from_dict(embargo_dict) + assert embargo.active + assert embargo.until == date_feature + assert embargo.reason == "espionage" + embargo_dict["until"] = date_feature.strftime("%Y-%m-%d") + assert embargo.dump() == embargo_dict + + +def test_embargo_lift(): + future_date = arrow.utcnow().datetime + timedelta(days=+30) + past_date = arrow.utcnow().datetime + timedelta(days=-30) + embargo_dict1 = {"until": future_date, "active": True, "reason": "espionage"} + embargo_dict2 = {"until": past_date, "active": True, "reason": "espionage"} + new_embargo = Embargo.from_dict(embargo_dict1) + old_embargo = Embargo.from_dict(embargo_dict2) + + assert old_embargo.lift() + assert not old_embargo.active + assert not new_embargo.lift() + assert new_embargo.active + + +# +# Record Access System Field +# + + +def test_access_field_on_record(appaccess, marc21_record, parent): + future_date = arrow.utcnow().datetime + timedelta(days=+30) + marc21_record["access"]["embargo"] = { + "until": future_date.strftime("%Y-%m-%d"), + "active": True, + "reason": "nothing in particular", + } + rec = Marc21Record.create(marc21_record, parent=parent) + + assert isinstance(rec.access, RecordAccess) + assert isinstance(rec.access.protection, Protection) + assert rec.access.protection.metadata == marc21_record["access"]["metadata"] + assert rec.access.protection.files == marc21_record["access"]["files"] + assert isinstance(rec.access.embargo, Embargo) + + +def test_access_field_update_embargo(appaccess, marc21_record, parent): + future_date = arrow.utcnow().datetime + timedelta(days=+30) + marc21_record["access"]["embargo"] = { + "until": future_date.strftime("%Y-%m-%d"), + "active": True, + "reason": "Because I can", + } + rec = Marc21Record.create(marc21_record.copy(), parent=parent) + assert rec.access.embargo + + rec.access.embargo.active = False + rec.access.embargo.reason = "can't remember" + rec.commit() + + marc21_record["access"]["embargo"]["active"] = False + marc21_record["access"]["embargo"]["reason"] = "can't remember" + + +def test_access_field_clear_embargo(appaccess, marc21_record, parent): + future_date = arrow.utcnow().datetime + timedelta(days=+30) + marc21_record["access"]["embargo"] = { + "until": future_date.strftime("%Y-%m-%d"), + "active": True, + "reason": "nothing in particular", + } + rec = Marc21Record.create(marc21_record, parent=parent) + + rec.access.embargo.clear() + assert not rec.access.embargo + + +def test_access_field_update_protection(appaccess, marc21_record, parent): + marc21_record["access"]["metadata"] = "restricted" + marc21_record["access"]["files"] = "restricted" + + rec = Marc21Record.create(marc21_record, parent=parent) + assert rec.access.protection.metadata == "restricted" + assert rec.access.protection.files == "restricted" + + rec.access.protection.set("public", "public") + rec.commit() + + assert rec["access"]["metadata"] == "public" + assert rec["access"]["files"] == "public" diff --git a/tests/services/conftest.py b/tests/services/conftest.py old mode 100644 new mode 100755 index 9fd09969..48ab0b2e --- a/tests/services/conftest.py +++ b/tests/services/conftest.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # # Invenio-Records-Marc21 is free software; you can redistribute it and/or @@ -18,7 +20,7 @@ from invenio_access import any_user from invenio_app.factory import create_api -from invenio_records_marc21.records import Marc21Draft +from invenio_records_marc21.proxies import current_records_marc21 from invenio_records_marc21.services import ( Marc21RecordService, Marc21RecordServiceConfig, @@ -46,14 +48,6 @@ def service(appctx): return Marc21RecordService(config=Marc21RecordServiceConfig()) -@pytest.fixture() -def example_record(app, db): - """Example record.""" - record = Marc21Draft.create({}, metadata={"title": "Test"}) - db.session.commit() - return record - - @pytest.fixture() def metadata(): """Input data (as coming from the view layer).""" @@ -68,3 +62,13 @@ def metadata2(): metadata = Marc21Metadata() metadata.emplace_field(tag="245", ind1="1", ind2="0", value="nulla sunt laborum") return metadata + + +@pytest.fixture() +def embargoedrecord(embargoed_record): + """Embargoed record.""" + service = current_records_marc21.records_service + + draft = service.create(identity_simple, embargoed_record) + record = service.publish(id_=draft.id, identity=identity_simple) + return record diff --git a/tests/services/schemas/test_access.py b/tests/services/schemas/test_access.py old mode 100644 new mode 100755 index ccf860e7..65ca1366 --- a/tests/services/schemas/test_access.py +++ b/tests/services/schemas/test_access.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # # Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it @@ -7,13 +9,15 @@ """Test metadata access schema.""" +from datetime import timedelta +import arrow import pytest from flask_babelex import lazy_gettext as _ from marshmallow import ValidationError from marshmallow.exceptions import ValidationError -from invenio_records_marc21.services.schemas.access import AccessSchema +from invenio_records_marc21.services.schemas.access import AccessSchema, EmbargoSchema def _assert_raises_messages(lambda_expression, expected_messages): @@ -28,7 +32,11 @@ def test_valid_full(): valid_full = { "metadata": "embargoed", "owned_by": [{"user": 1}], - "embargo": {"until": "2120-10-06", "active": True, "reason": "Because I can!"}, + "embargo": { + "until": (arrow.utcnow().datetime + timedelta(days=2)).strftime("%Y-%m-%d"), + "active": True, + "reason": "Because I can!", + }, "files": "public", } assert valid_full == AccessSchema().load(valid_full) @@ -80,3 +88,119 @@ def test_invalid(invalid_access, missing_attr): error_fields = e.value.messages.keys() assert len(error_fields) == 1 assert missing_attr in error_fields + + +def test_embargo_load_no_until_is_valid(): + expected = {"active": False, "until": None, "reason": None} + + valid_no_until = { + "active": False, + } + assert expected == EmbargoSchema().load(valid_no_until) + + valid_no_until = { + "active": False, + "until": None, + } + assert expected == EmbargoSchema().load(valid_no_until) + + +def test_embargo_dump_no_until_is_valid(): + valid_no_until = { + "active": False, + } + assert valid_no_until == EmbargoSchema().dump(valid_no_until) + + expected = { + "active": False, + } + valid_no_until = { + "active": False, + "until": None, + } + assert expected == EmbargoSchema().dump(valid_no_until) + + +@pytest.mark.parametrize( + "invalid_access,invalid_attr", + [ + ( + { + "files": "restricted", + "embargo": { + "active": True, + "until": (arrow.utcnow().datetime + timedelta(days=2)).strftime("%Y-%m-%d"), + "reason": "Because I can!", + }, + }, + "metadata", + ), + ( + { + "metadata": "public", + "embargo": { + "active": True, + "until": (arrow.utcnow().datetime + timedelta(days=2)).strftime("%Y-%m-%d"), + "reason": "Because I can!", + }, + }, + "files", + ), + ( + { + "metadata": "public", + "files": "restricted", + "embargo": { + "active": False, + "until": (arrow.utcnow().datetime + timedelta(days=2)).strftime("%Y-%m-%d"), + "reason": "Because I can!", + }, + }, + "embargo", + ), + ( + { + "metadata": "public", + "files": "restricted", + "embargo": { + "active": True, + "until": (arrow.utcnow().datetime + timedelta(days=-20)).strftime("%Y-%m-%d"), + "reason": "Because I can!", + }, + }, + "embargo", + ), + ( + { + "metadata": "invalid", + "files": "restricted", + "embargo": { + "active": False, + "until": (arrow.utcnow().datetime + timedelta(days=-20)).strftime("%Y-%m-%d"), + "reason": "Because I can!", + }, + }, + "metadata", + ), + ( + { + "metadata": "public", + "files": "invalid", + "embargo": { + "active": False, + "until": (arrow.utcnow().datetime + timedelta(days=-20)).strftime("%Y-%m-%d"), + "reason": "Because I can!", + }, + }, + "files", + ), + ], +) +def test_invalid(invalid_access, invalid_attr): + + with pytest.raises(ValidationError) as e: + AccessSchema().load(invalid_access) + + error_fields = e.value.messages.keys() + assert len(error_fields) == 1 + assert invalid_attr in error_fields diff --git a/tests/services/test_create_record.py b/tests/services/test_create_record.py old mode 100644 new mode 100755 index e7537b57..9dddbc40 --- a/tests/services/test_create_record.py +++ b/tests/services/test_create_record.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # # Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it @@ -10,6 +12,7 @@ from datetime import date, timedelta +import arrow import pytest from invenio_records_marc21.services import ( @@ -80,8 +83,12 @@ def empty_data(): "expect": { "access": { "metadata": "public", - "owned_by": [{"user": 1}], "files": "public", + "embargo": { + "active": False, + "reason": None, + }, + "status": "public", }, }, }, @@ -91,9 +98,13 @@ def empty_data(): }, "expect": { "access": { - "files": "public", - "owned_by": [{"user": 1}], + "files": "restricted", "metadata": "restricted", + "embargo": { + "active": False, + "reason": None, + }, + "status": "restricted", }, }, }, @@ -101,7 +112,7 @@ def empty_data(): "input": { "metadata": "embargoed", "embargo": { - "until": (date.today() + timedelta(days=2)).strftime("%Y-%m-%d"), + "until": (arrow.utcnow().datetime + timedelta(days=2)).strftime("%Y-%m-%d"), "active": True, "reason": "Because I can!", }, @@ -109,8 +120,15 @@ def empty_data(): "expect": { "access": { "files": "public", - "owned_by": [{"user": 1}], "metadata": "embargoed", + "embargo": { + "until": (arrow.utcnow().datetime + timedelta(days=2)).strftime( + "%Y-%m-%d" + ), + "active": True, + "reason": "Because I can!", + }, + "status": "embargoed", }, }, }, @@ -125,6 +143,15 @@ def test_create_with_access(app, empty_data, identity_simple, access): record = service.publish(id_=draft.id, identity=identity_simple) _assert_fields( ["access"], - record.data["parent"], + record.data, access["expect"], ) + _assert_fields( + ["access"], + record.data["parent"], + { + "access": { + "owned_by": [{"user": 1}], + } + }, + ) diff --git a/tests/services/test_record_service.py b/tests/services/test_record_service.py index 39149d64..01f13277 100755 --- a/tests/services/test_record_service.py +++ b/tests/services/test_record_service.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # # Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it @@ -14,6 +16,7 @@ """ import time +from collections import namedtuple import pytest from dojson.contrib.marc21.utils import create_record @@ -22,15 +25,28 @@ from invenio_pidstore.models import PIDStatus from sqlalchemy.orm.exc import NoResultFound +RunningApp = namedtuple("RunningApp", ["app", "service", "identity_simple"]) + + +@pytest.fixture() +def running_app(app, service, identity_simple): + """This fixture provides an app with the typically needed db data loaded. + + All of these fixtures are often needed together, so collecting them + under a semantic umbrella makes sense. + """ + return RunningApp(app, service, identity_simple) + + # # Operations tests # - -def test_create_draft(app, service, identity_simple, metadata): +def test_create_draft(running_app, metadata): """Test draft creation of a non-existing record.""" - # Needs `app` context because of invenio_access/permissions.py#166 - draft = service.create(identity_simple, metadata=metadata) + + service = running_app.service + draft = service.create(running_app.identity_simple, metadata=metadata) draft_dict = draft.to_dict() assert draft.id @@ -43,12 +59,14 @@ def test_create_draft(app, service, identity_simple, metadata): assert draft._record.pid.status == PIDStatus.NEW -def test_create_empty_draft(app, service, identity_simple): +def test_create_empty_draft(running_app): """Test an empty draft can be created. Errors (missing required fields) are reported, but don't prevent creation. """ input_data = {"metadata": {}} + service = running_app.service + identity_simple = running_app.identity_simple draft = service.create(identity_simple, input_data) draft_dict = draft.to_dict() @@ -57,7 +75,11 @@ def test_create_empty_draft(app, service, identity_simple): assert draft._record.pid.status == PIDStatus.NEW -def test_read_draft(app, service, identity_simple, metadata): +def test_read_draft(running_app, metadata): + """Test read a draft can be created.""" + + service = running_app.service + identity_simple = running_app.identity_simple draft = service.create(identity_simple, metadata=metadata) assert draft.id @@ -65,7 +87,11 @@ def test_read_draft(app, service, identity_simple, metadata): assert draft.id == draft_2.id -def test_delete_draft(app, service, identity_simple, metadata): +def test_delete_draft(running_app, metadata): + """Test a created draft can be deleted.""" + identity_simple = running_app.identity_simple + service = running_app.service + draft = service.create(identity=identity_simple, metadata=metadata) assert draft.id @@ -92,11 +118,13 @@ def _create_and_publish(service, metadata, identity_simple): return record -def test_publish_draft(app, service, identity_simple, metadata): +def test_publish_draft(running_app, metadata): """Test draft publishing of a non-existing record. Note that the publish action requires a draft to be created first. """ + service = running_app.service + identity_simple = running_app.identity_simple record = _create_and_publish(service, metadata, identity_simple) assert record._record.pid.status == PIDStatus.REGISTERED @@ -117,7 +145,9 @@ def _test_metadata(metadata, metadata2): assert metadata[key] == metadata2[key] -def test_update_draft(app, service, identity_simple, metadata, metadata2): +def test_update_draft(running_app, metadata, metadata2): + service = running_app.service + identity_simple = running_app.identity_simple draft = service.create(identity=identity_simple, metadata=metadata) assert draft.id @@ -137,43 +167,13 @@ def test_update_draft(app, service, identity_simple, metadata, metadata2): ) -def test_mutiple_edit(base_app, service, identity_simple, metadata): - """Test the revision_id when editing record multiple times.. - - This tests the `edit` service method. - """ - record = _create_and_publish(service, metadata, identity_simple) - marcid = record.id - - # Create new draft of said record - draft = service.edit(marcid, identity_simple) - assert draft.id == marcid - assert draft._record.fork_version_id == record._record.revision_id - # files attribute in record causes at create change the revision_id twice - assert draft._record.revision_id == 6 - - draft = service.edit(marcid, identity_simple) - assert draft.id == marcid - assert draft._record.fork_version_id == record._record.revision_id - # files attribute in record causes at create change the revision_id twice - assert draft._record.revision_id == 6 - - # Publish it to check the increment in version_id - record = service.publish(marcid, identity_simple) - - draft = service.edit(marcid, identity_simple) - assert draft.id == marcid - assert draft._record.fork_version_id == record._record.revision_id - # files attribute in record causes at create change the revision_id twice - # create(2), soft-delete, undelete, update - assert draft._record.revision_id == 9 - - -def test_create_publish_new_version(app, service, identity_simple, metadata): +def test_create_publish_new_version(running_app, metadata): """Test creating a new revision of a record. This tests the `new_version` service method. """ + service = running_app.service + identity_simple = running_app.identity_simple record = _create_and_publish(service, metadata, identity_simple) marcid = record.id From b5b6112a24a206b2f7fbd9c6a98a745049fd702f Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 9 Aug 2021 14:36:02 +0200 Subject: [PATCH 055/217] modification: date modification using arrow module for date modification in a record --- invenio_records_marc21/cli.py | 5 +++-- setup.py | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/invenio_records_marc21/cli.py b/invenio_records_marc21/cli.py index da155c6b..f68338aa 100644 --- a/invenio_records_marc21/cli.py +++ b/invenio_records_marc21/cli.py @@ -9,9 +9,10 @@ """Command-line tools for demo module.""" import json import random -from datetime import date, timedelta +from datetime import timedelta from os.path import dirname, join +import arrow import click from flask.cli import with_appcontext from flask_principal import Identity @@ -37,7 +38,7 @@ def fake_access_right(): def fake_feature_date(days=365): """Generates a fake feature_date.""" - start_date = date.today() + start_date = arrow.utcnow().datetime random_number_of_days = random.randrange(days) _date = start_date + timedelta(days=random_number_of_days) return _date.strftime("%Y-%m-%d") diff --git a/setup.py b/setup.py index 48c86d0b..c57301bf 100755 --- a/setup.py +++ b/setup.py @@ -52,6 +52,7 @@ install_requires = [ "invenio-i18n>=1.3.0", + "arrow>=1.0.0", "dojson>=1.4.0", "lxml>=4.6.2", "invenio-records-rest>=1.5.0,<2.0.0", From da03ccb2c7ef4940766d4dfec55a86c8ff42f57e Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Tue, 10 Aug 2021 07:33:29 +0200 Subject: [PATCH 056/217] bugfix: embargo date in the future creating demo records with an active embargo with a random timedelta of at least 1 day until a given stop date. --- invenio_records_marc21/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invenio_records_marc21/cli.py b/invenio_records_marc21/cli.py index f68338aa..4d894415 100644 --- a/invenio_records_marc21/cli.py +++ b/invenio_records_marc21/cli.py @@ -39,7 +39,7 @@ def fake_access_right(): def fake_feature_date(days=365): """Generates a fake feature_date.""" start_date = arrow.utcnow().datetime - random_number_of_days = random.randrange(days) + random_number_of_days = random.randrange(1, days) _date = start_date + timedelta(days=random_number_of_days) return _date.strftime("%Y-%m-%d") From 426f2e0b9ce05b09c7e65df8e9be4246f019032f Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Tue, 10 Aug 2021 09:33:18 +0200 Subject: [PATCH 057/217] test: update embargo checking update the embargo property of a record --- tests/records/fields/test_systemfield_access.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/records/fields/test_systemfield_access.py b/tests/records/fields/test_systemfield_access.py index 0148f2d9..bb157fb4 100755 --- a/tests/records/fields/test_systemfield_access.py +++ b/tests/records/fields/test_systemfield_access.py @@ -121,14 +121,17 @@ def test_access_field_update_embargo(appaccess, marc21_record, parent): "reason": "Because I can", } rec = Marc21Record.create(marc21_record.copy(), parent=parent) + assert rec.access.embargo + assert rec.access.embargo.active + assert rec.access.embargo.reason == "Because I can" rec.access.embargo.active = False rec.access.embargo.reason = "can't remember" rec.commit() - marc21_record["access"]["embargo"]["active"] = False - marc21_record["access"]["embargo"]["reason"] = "can't remember" + assert not rec["access"]["embargo"]["active"] + assert rec["access"]["embargo"]["reason"] == "can't remember" def test_access_field_clear_embargo(appaccess, marc21_record, parent): From ff819aa02d486cc7ca9fec0d76b36b322f169a69 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Tue, 10 Aug 2021 10:37:33 +0200 Subject: [PATCH 058/217] bugfix: correct import AccessStatusEnum --- invenio_records_marc21/cli.py | 2 +- invenio_records_marc21/services/components/__init__.py | 3 +-- invenio_records_marc21/services/config.py | 10 +++------- .../services/schemas/access/parent.py | 6 ++---- .../services/schemas/access/record.py | 2 +- invenio_records_marc21/services/services.py | 2 +- 6 files changed, 9 insertions(+), 16 deletions(-) diff --git a/invenio_records_marc21/cli.py b/invenio_records_marc21/cli.py index 4d894415..65782fca 100644 --- a/invenio_records_marc21/cli.py +++ b/invenio_records_marc21/cli.py @@ -19,7 +19,7 @@ from invenio_access import any_user from .proxies import current_records_marc21 -from .services.components import AccessStatusEnum +from .records.systemfields.access import AccessStatusEnum from .services.record import Marc21Metadata diff --git a/invenio_records_marc21/services/components/__init__.py b/invenio_records_marc21/services/components/__init__.py index 2e14e773..1e0a6c13 100644 --- a/invenio_records_marc21/services/components/__init__.py +++ b/invenio_records_marc21/services/components/__init__.py @@ -7,12 +7,11 @@ """Marc21 record components.""" -from .access import AccessComponent, AccessStatusEnum +from .access import AccessComponent from .metadata import MetadataComponent from .pid import PIDComponent __all__ = ( - "AccessStatusEnum", "AccessComponent", "PIDComponent", "MetadataComponent", diff --git a/invenio_records_marc21/services/config.py b/invenio_records_marc21/services/config.py index dbbfd49d..938c4356 100644 --- a/invenio_records_marc21/services/config.py +++ b/invenio_records_marc21/services/config.py @@ -8,7 +8,7 @@ """Marc21 Record Service config.""" -from flask import current_app + from flask_babelex import gettext as _ from invenio_drafts_resources.services.records.config import ( RecordServiceConfig, @@ -26,12 +26,8 @@ from invenio_records_resources.services.records.links import RecordLink from ..records import Marc21Draft, Marc21Parent, Marc21Record -from .components import ( - AccessComponent, - AccessStatusEnum, - MetadataComponent, - PIDComponent, -) +from ..records.systemfields.access import AccessStatusEnum +from .components import AccessComponent, MetadataComponent, PIDComponent from .permissions import Marc21RecordPermissionPolicy from .schemas import Marc21ParentSchema, Marc21RecordSchema diff --git a/invenio_records_marc21/services/schemas/access/parent.py b/invenio_records_marc21/services/schemas/access/parent.py index 082be4af..79a2beb6 100644 --- a/invenio_records_marc21/services/schemas/access/parent.py +++ b/invenio_records_marc21/services/schemas/access/parent.py @@ -11,11 +11,9 @@ from flask_babelex import lazy_gettext as _ from marshmallow import Schema, ValidationError, fields, validates_schema -from marshmallow.fields import Integer, List, Nested -from marshmallow_utils.fields import NestedAttribute, SanitizedUnicode +from marshmallow.fields import Integer, List -from ...components import AccessStatusEnum -from .embargo import EmbargoSchema +from ....records.systemfields.access import AccessStatusEnum class Agent(Schema): diff --git a/invenio_records_marc21/services/schemas/access/record.py b/invenio_records_marc21/services/schemas/access/record.py index a56f981d..508867ee 100644 --- a/invenio_records_marc21/services/schemas/access/record.py +++ b/invenio_records_marc21/services/schemas/access/record.py @@ -15,7 +15,7 @@ from marshmallow.fields import Integer, List, Nested from marshmallow_utils.fields import NestedAttribute, SanitizedUnicode -from ...components import AccessStatusEnum +from ....records.systemfields.access import AccessStatusEnum from .embargo import EmbargoSchema diff --git a/invenio_records_marc21/services/services.py b/invenio_records_marc21/services/services.py index e6e57b06..c1f02ce9 100644 --- a/invenio_records_marc21/services/services.py +++ b/invenio_records_marc21/services/services.py @@ -13,7 +13,7 @@ from invenio_records_resources.services.files.service import FileService from invenio_records_resources.services.records.results import RecordItem -from .components import AccessStatusEnum +from ..records.systemfields.access import AccessStatusEnum from .config import ( Marc21DraftFilesServiceConfig, Marc21RecordFilesServiceConfig, From 10ff9fc75382e4d727a2942b03a3f3102ba658f7 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Wed, 11 Aug 2021 08:12:37 +0200 Subject: [PATCH 059/217] test: access component adding testset for the access component --- .../components/test_component_access.py | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 tests/services/components/test_component_access.py diff --git a/tests/services/components/test_component_access.py b/tests/services/components/test_component_access.py new file mode 100644 index 00000000..140016e5 --- /dev/null +++ b/tests/services/components/test_component_access.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + + +"""Tests for the service AccessComponent.""" + +from datetime import timedelta + +import arrow +import pytest +from flask_principal import Identity, UserNeed + +from invenio_records_marc21.proxies import current_records_marc21 +from invenio_records_marc21.records import Marc21Record +from invenio_records_marc21.services.components import AccessComponent + + +def test_component_access_default_access(parent, identity_simple): + marc21_record = {"metadata": {"json": {}}} + record = Marc21Record.create(data=marc21_record, parent=parent) + component = AccessComponent(current_records_marc21.records_service) + component.create(identity=identity_simple, data=marc21_record, record=record) + + prot = record.access.protection + assert prot.metadata == "public" + assert prot.files == "public" + + assert len(record.parent.access.owners) > 0 + assert record.parent.access.owners[0].owner_id == identity_simple.id + + +def test_component_access_update_access_via_json( + marc21_record, parent, identity_simple +): + next_year = arrow.utcnow().datetime + timedelta(days=+365) + restricted_access = { + "metadata": "restricted", + "files": "restricted", + "embargo": { + "until": next_year.strftime("%Y-%m-%d"), + "active": True, + "reason": "Because I can.", + }, + } + marc21_record["access"] = restricted_access + + record = Marc21Record.create(marc21_record, parent=parent) + component = AccessComponent(current_records_marc21.records_service) + component.create(identity_simple, marc21_record, record) + + prot = record.access.protection + assert record.access.embargo is not None + assert "embargo" in record["access"] + assert prot.metadata == record["access"]["metadata"] == "restricted" + assert prot.files == record["access"]["files"] == "restricted" + + new_data = marc21_record.copy() + new_data["access"] = { + "metadata": "public", + "files": "public", + } + component.create(identity_simple, new_data, record) + + prot = record.access.protection + assert not record.access.embargo + assert "embargo" not in record["access"] + assert prot.metadata == record["access"]["metadata"] == "public" + assert prot.files == record["access"]["files"] == "public" From d8e323f050188e3539c1df800c5ca88d1963b424 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Wed, 11 Aug 2021 09:56:48 +0200 Subject: [PATCH 060/217] modification: move systemfield test-set moving test-set according to module structure --- tests/records/{fields => systemfields}/conftest.py | 9 --------- .../{fields => systemfields}/test_systemfield_access.py | 0 .../{fields => systemfields}/test_systemfield_files.py | 0 3 files changed, 9 deletions(-) rename tests/records/{fields => systemfields}/conftest.py (87%) rename tests/records/{fields => systemfields}/test_systemfield_access.py (100%) rename tests/records/{fields => systemfields}/test_systemfield_files.py (100%) diff --git a/tests/records/fields/conftest.py b/tests/records/systemfields/conftest.py similarity index 87% rename from tests/records/fields/conftest.py rename to tests/records/systemfields/conftest.py index 7f7b6bcf..06c111a7 100755 --- a/tests/records/fields/conftest.py +++ b/tests/records/systemfields/conftest.py @@ -18,8 +18,6 @@ import pytest from flask import Flask -from flask_security.utils import hash_password -from invenio_accounts import InvenioAccounts from invenio_db import InvenioDB from invenio_files_rest import InvenioFilesREST from invenio_files_rest.models import Location @@ -27,7 +25,6 @@ from invenio_records import InvenioRecords from invenio_records_marc21 import InvenioRecordsMARC21 -from invenio_records_marc21.records import Marc21Parent @pytest.fixture(scope="module") @@ -71,9 +68,3 @@ def appaccess(base_app, database): InvenioRecords(base_app) InvenioJSONSchemas(base_app) yield base_app - - -@pytest.fixture() -def parent(app, db): - """A parent record.""" - return Marc21Parent.create({}) diff --git a/tests/records/fields/test_systemfield_access.py b/tests/records/systemfields/test_systemfield_access.py similarity index 100% rename from tests/records/fields/test_systemfield_access.py rename to tests/records/systemfields/test_systemfield_access.py diff --git a/tests/records/fields/test_systemfield_files.py b/tests/records/systemfields/test_systemfield_files.py similarity index 100% rename from tests/records/fields/test_systemfield_files.py rename to tests/records/systemfields/test_systemfield_files.py From ccaecc7f725445d6589f3c60a26ad5ed74be1d04 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Wed, 11 Aug 2021 10:43:36 +0200 Subject: [PATCH 061/217] test: pid field test-set adding testset for the record pid field and the provider and resolver of records --- .../systemfields/test_systemfield_pid.py | 100 ++++++++++++ .../test_systemfield_providers.py | 77 +++++++++ .../systemfields/test_systemfield_resolver.py | 154 ++++++++++++++++++ 3 files changed, 331 insertions(+) create mode 100644 tests/records/systemfields/test_systemfield_pid.py create mode 100644 tests/records/systemfields/test_systemfield_providers.py create mode 100644 tests/records/systemfields/test_systemfield_resolver.py diff --git a/tests/records/systemfields/test_systemfield_pid.py b/tests/records/systemfields/test_systemfield_pid.py new file mode 100644 index 00000000..12a283b0 --- /dev/null +++ b/tests/records/systemfields/test_systemfield_pid.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + + +"""Tests for the record PIDField.""" + + +from invenio_records_resources.records.systemfields import PIDField +from sqlalchemy import inspect + +from invenio_records_marc21.records.api import Marc21Record +from invenio_records_marc21.records.models import RecordMetadata +from invenio_records_marc21.records.systemfields import ( + MarcPIDFieldContext, + MarcRecordProvider, + MarcResolver, +) + + +def test_class_attribute_pid(): + """Test that field is returned.""" + assert isinstance(Marc21Record.pid, MarcPIDFieldContext) + + +def test_record_pid_creation(testapp, db): + """Test record creation.""" + record = Marc21Record.create({}) + assert record["id"] == record.pid.pid_value + assert record["pid"]["pk"] == record.pid.id + assert record["pid"]["status"] == record.pid.status + assert record["pid"]["obj_type"] == record.pid.object_type + assert record["pid"]["pid_type"] == record.pid.pid_type + assert record.id == record.pid.object_uuid + + +def test_create_no_provider(testapp, db): + """Test creation without a provider.""" + + class Record(Marc21Record): + model_cls = RecordMetadata + pid = PIDField() + + record = Record.create({}) + assert record.pid is None + + record.pid = MarcRecordProvider.create(object_type="rec", object_uuid=record.id).pid + + assert record["id"] == record.pid.pid_value + assert record["pid"]["pk"] == record.pid.id + assert record["pid"]["status"] == record.pid.status + assert record["pid"]["obj_type"] == record.pid.object_type + assert record["pid"]["pid_type"] == record.pid.pid_type + assert record.id == record.pid.object_uuid + + +def test_create_different_key(testapp, db): + """Test creation with different key.""" + + class Record(Marc21Record): + model_cls = RecordMetadata + + pid = PIDField( + key="id", + provider=MarcRecordProvider, + context_cls=MarcPIDFieldContext, + resolver_cls=MarcResolver, + delete=False, + ) + + record = Record.create({}) + assert record["id"] == record.pid.pid_value + assert record["pid"]["pid_type"] == record.pid.pid_type + + +def test_reading_a_pid(testapp, db): + """Test reading from dict.""" + record = Marc21Record( + { + "id": "0scgd-ps972", + "pid": { + "pid_type": "marcid", + "obj_type": "rec", + "pk": 10, + "status": "R", + }, + } + ) + assert record.pid is not None + assert record["id"] == record.pid.pid_value + assert record["pid"]["pk"] == record.pid.id + assert record["pid"]["status"] == record.pid.status + assert record["pid"]["obj_type"] == record.pid.object_type + assert record["pid"]["pid_type"] == record.pid.pid_type diff --git a/tests/records/systemfields/test_systemfield_providers.py b/tests/records/systemfields/test_systemfield_providers.py new file mode 100644 index 00000000..5ff86aa1 --- /dev/null +++ b/tests/records/systemfields/test_systemfield_providers.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + + +"""Tests for the record Pid Providers.""" + + +import uuid + +import pytest +from invenio_pidstore.models import PIDStatus + +from invenio_records_marc21.records.systemfields import ( + MarcDraftProvider, + MarcRecordProvider, +) + + +def test_record_provider(app, db): + """Test MarcRecordProvider.""" + with app.app_context(): + provider = MarcRecordProvider.create() + assert provider.pid + assert provider.pid.pid_type == "marcid" + assert provider.pid.pid_value + assert provider.pid.pid_provider is None + assert provider.pid.status == PIDStatus.RESERVED + assert provider.pid.object_type is None + assert provider.pid.object_uuid is None + + # Assign to object immediately + rec_uuid = uuid.uuid4() + provider = MarcRecordProvider.create(object_type="rec", object_uuid=rec_uuid) + assert provider.pid + assert provider.pid.pid_type == "marcid" + assert provider.pid.pid_value + assert provider.pid.pid_provider is None + assert provider.pid.status == PIDStatus.REGISTERED + assert provider.pid.object_type == "rec" + assert provider.pid.object_uuid == rec_uuid + + +def test_draft_provider(app, db): + """Test MarcDraftProvider.""" + with app.app_context(): + provider = MarcDraftProvider.create() + assert provider.pid + assert provider.pid.pid_type == "marcid" + assert provider.pid.pid_value + assert provider.pid.pid_provider is None + assert provider.pid.status == PIDStatus.RESERVED + assert provider.pid.object_type is None + assert provider.pid.object_uuid is None + + # Assign to object immediately + rec_uuid = uuid.uuid4() + provider = MarcDraftProvider.create(object_type="rec", object_uuid=rec_uuid) + assert provider.pid + assert provider.pid.pid_type == "marcid" + assert provider.pid.pid_value + assert provider.pid.pid_provider is None + assert provider.pid.status == PIDStatus.NEW + assert provider.pid.object_type == "rec" + assert provider.pid.object_uuid == rec_uuid + + +def test_invalid_pid_value(app, db): + with app.app_context(): + with pytest.raises(AssertionError): + MarcRecordProvider.create(pid_value="3") diff --git a/tests/records/systemfields/test_systemfield_resolver.py b/tests/records/systemfields/test_systemfield_resolver.py new file mode 100644 index 00000000..7084bb5a --- /dev/null +++ b/tests/records/systemfields/test_systemfield_resolver.py @@ -0,0 +1,154 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + + +"""Tests for the record Pid Resolver.""" + +import uuid + +import pytest +from invenio_pidstore.errors import ( + PIDDeletedError, + PIDDoesNotExistError, + PIDMissingObjectError, + PIDRedirectedError, + PIDUnregistered, +) +from invenio_pidstore.models import PersistentIdentifier, PIDStatus + +from invenio_records_marc21.records.systemfields import MarcResolver + + +def test_marc_resolver(app, db): + """Test the class methods of PersistentIdentifier class.""" + + with app.app_context(): + rec_a = uuid.uuid4() + PersistentIdentifier.create("marcid", 1, status=PIDStatus.NEW) + + PersistentIdentifier.create("marcid", 2, status=PIDStatus.NEW, object_type="rec", object_uuid=rec_a) + PersistentIdentifier.create("marcid", 3, status=PIDStatus.RESERVED) + PersistentIdentifier.create("marcid", 4, status=PIDStatus.REGISTERED) + PersistentIdentifier.create("marcid", 5, status=PIDStatus.REGISTERED, object_type="rec", object_uuid=rec_a) + pid = PersistentIdentifier.create("marcid", 6, status=PIDStatus.DELETED) + # Create redirects + pid = PersistentIdentifier.create("marcid", 7, status=PIDStatus.REGISTERED) + pid.redirect(PersistentIdentifier.get("marcid", "3")) + + db.session.commit() + + # Start tests + resolver = MarcResolver(getter=lambda x: x) + + # Resolve non-existing pid + pytest.raises(PIDDoesNotExistError, resolver.resolve, "0scgd-ps972") + pytest.raises(PIDDoesNotExistError, resolver.resolve, "yspa2-jqz52") + + # Resolve status new + pytest.raises(PIDMissingObjectError, resolver.resolve, "1") + + resolved = resolver.resolve("2") + assert resolved + ident = resolved[0] + assert ident.pid_type == "marcid" + assert ident.pid_value == "2" + assert ident.pid_provider is None + assert ident.object_uuid == rec_a + + # Resolve status reserved + pytest.raises(PIDMissingObjectError, resolver.resolve, "3") + + # Resolve status registered + pytest.raises(PIDMissingObjectError, resolver.resolve, "4") + + resolved = resolver.resolve("5") + assert resolved + ident = resolved[0] + assert ident.pid_type == "marcid" + assert ident.pid_value == "5" + assert ident.pid_provider is None + assert ident.object_uuid == rec_a + + # Resolve status deleted + pytest.raises(PIDDeletedError, resolver.resolve, "6") + + # Resolve status redirected + try: + resolver.resolve("7") + assert False + except PIDRedirectedError as e: + assert e.destination_pid.pid_type == "marcid" + assert e.destination_pid.pid_value == "3" + + +def test_resolver_deleted_object(app, db): + """Test the class methods of PersistentIdentifier class.""" + with app.app_context(): + rec_uuid = uuid.uuid4() + records = { + rec_uuid: {"title": "test"}, + } + with db.session.begin_nested(): + pid = PersistentIdentifier.create( + "marcid", + "1", + status=PIDStatus.REGISTERED, + object_type="rec", + object_uuid=rec_uuid, + ) + + with db.session.begin_nested(): + pid.delete() + + resolver = MarcResolver( + pid_type="marcid", object_type="rec", getter=records.get + ) + + assert pytest.raises(PIDDeletedError, resolver.resolve, "1") + + +def test_resolver_not_registered_only(app, db): + """Test the resolver returns a new and reserved PID when specified.""" + + status = [PIDStatus.NEW, PIDStatus.RESERVED, PIDStatus.REGISTERED] + + with app.app_context(): + rec_a = uuid.uuid4() + # Create pids for each status with and without object + for idx, s in enumerate(status, 1): + PersistentIdentifier.create("recid", idx * 2 - 1, status=s) + PersistentIdentifier.create( + "recid", idx * 2, status=s, object_type="rec", object_uuid=rec_a + ) + + db.session.commit() + + # Start tests + resolver = MarcResolver( + pid_type="recid", + object_type="rec", + getter=lambda x: x, + registered_only=False, + ) + + # Resolve status new + pytest.raises(PIDMissingObjectError, resolver.resolve, "1") + pid, obj = resolver.resolve("2") + assert pid and obj == rec_a + + # Resolve status reserved + pytest.raises(PIDMissingObjectError, resolver.resolve, "3") + pid, obj = resolver.resolve("4") + assert pid and obj == rec_a + + # Resolve status registered + pytest.raises(PIDMissingObjectError, resolver.resolve, "5") + pid, obj = resolver.resolve("6") + assert pid and obj == rec_a From 5ed74ffbb4404cb2defe7aa4664a924c8526ea80 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Wed, 11 Aug 2021 10:49:07 +0200 Subject: [PATCH 062/217] test: add parent fixture --- tests/conftest.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index da8b256d..d45bb576 100755 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -24,8 +24,7 @@ from invenio_files_rest.models import Location from invenio_records_marc21 import InvenioRecordsMARC21 -from invenio_records_marc21.proxies import current_records_marc21 -from invenio_records_marc21.records import Marc21Draft +from invenio_records_marc21.records import Marc21Draft, Marc21Parent from invenio_records_marc21.views import create_record_bp @@ -65,6 +64,12 @@ def marc21_record(): return marc21_record +@pytest.fixture() +def parent(app, db): + """A parent record.""" + return Marc21Parent.create({}) + + @pytest.fixture() def example_record(app, db): """Example record.""" From 5d0ad8bdc503d3a3facb048abd3f27d709c071c6 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Wed, 11 Aug 2021 10:52:30 +0200 Subject: [PATCH 063/217] bugfix: protection string changing protection string to metadata --- .../records/systemfields/access/fields/record.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invenio_records_marc21/records/systemfields/access/fields/record.py b/invenio_records_marc21/records/systemfields/access/fields/record.py index 453c4e63..bb94d3fd 100755 --- a/invenio_records_marc21/records/systemfields/access/fields/record.py +++ b/invenio_records_marc21/records/systemfields/access/fields/record.py @@ -120,7 +120,7 @@ def from_dict( def __repr__(self): """Return repr(self).""" - protection_str = "{}/{}".format(self.protection.record, self.protection.files) + protection_str = "{}/{}".format(self.protection.metadata, self.protection.files) return ("<{} (protection: {}, {})>").format( type(self).__name__, From 7f1b0c134ba45d2a26afcaf8333658657b337a64 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Wed, 11 Aug 2021 14:43:36 +0200 Subject: [PATCH 064/217] modification: AccessSchema removing embardo validation since a protection class added to the marc21 record and add field_name to ValidationError if protection value for metadata and files is invalid --- .../services/schemas/access/parent.py | 13 +------------ .../services/schemas/access/record.py | 15 ++------------- 2 files changed, 3 insertions(+), 25 deletions(-) diff --git a/invenio_records_marc21/services/schemas/access/parent.py b/invenio_records_marc21/services/schemas/access/parent.py index 79a2beb6..92c974a3 100644 --- a/invenio_records_marc21/services/schemas/access/parent.py +++ b/invenio_records_marc21/services/schemas/access/parent.py @@ -7,7 +7,7 @@ # Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it # under the terms of the MIT License; see LICENSE file for more details. -"""Marc21 parent record access schemas.""" +"""Marc21 record ParentAccessSchema.""" from flask_babelex import lazy_gettext as _ from marshmallow import Schema, ValidationError, fields, validates_schema @@ -26,14 +26,3 @@ class ParentAccessSchema(Schema): """Access schema.""" owned_by = List(fields.Nested(Agent)) - - @validates_schema - def validate_embargo(self, data, **kwargs): - """Validate that the properties are consistent with each other.""" - metadata = data.get("metadata", "") - embargo = data.get("embargo", None) - if AccessStatusEnum.EMBARGOED.value == metadata and not embargo: - raise ValidationError( - _("Embargo schema must be set if metadata is Embargoed"), - field_name="embargo", - ) diff --git a/invenio_records_marc21/services/schemas/access/record.py b/invenio_records_marc21/services/schemas/access/record.py index 508867ee..d9ac950f 100644 --- a/invenio_records_marc21/services/schemas/access/record.py +++ b/invenio_records_marc21/services/schemas/access/record.py @@ -7,7 +7,7 @@ # Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it # under the terms of the MIT License; see LICENSE file for more details. -"""Marc21 record schemas.""" +"""Marc21 services record AccessSchema.""" import arrow from flask_babelex import lazy_gettext as _ @@ -42,7 +42,7 @@ def validate_protection_value(self, value, field_name): field_name, *AccessStatusEnum.list(), ), - "record", + field_name, ) def get_attribute(self, obj, key, default): @@ -58,17 +58,6 @@ def validate_metadata_protection(self, value): """Validate the metadata protection value.""" self.validate_protection_value(value, "metadata") - @validates_schema - def validate_embargo(self, data, **kwargs): - """Validate that the properties are consistent with each other.""" - metadata = data.get("metadata", "") - embargo = data.get("embargo", "") - if AccessStatusEnum.EMBARGOED.value == metadata and not embargo: - raise ValidationError( - _("Embargo must be set if metadata is Embargoed"), - field_name="embargo", - ) - @validates("files") def validate_files_protection(self, value): """Validate the files protection value.""" From 6a1f80f8bbeb2e8e309473d3e0a7c8efe571bea4 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Wed, 11 Aug 2021 14:46:13 +0200 Subject: [PATCH 065/217] test: service schemas adding testset for the access schema and metadata schema --- tests/services/schemas/__init__.py | 9 --- tests/services/schemas/access/conftest.py | 16 +++++ tests/services/schemas/access/test_record.py | 61 ++++++++++++++++++++ tests/services/schemas/conftest.py | 45 +++++++++++++++ tests/services/schemas/test_access.py | 4 +- tests/services/schemas/test_metadata.py | 57 ++++++++++++++++++ 6 files changed, 182 insertions(+), 10 deletions(-) delete mode 100644 tests/services/schemas/__init__.py create mode 100755 tests/services/schemas/access/conftest.py create mode 100755 tests/services/schemas/access/test_record.py create mode 100755 tests/services/schemas/conftest.py create mode 100644 tests/services/schemas/test_metadata.py diff --git a/tests/services/schemas/__init__.py b/tests/services/schemas/__init__.py deleted file mode 100644 index df589013..00000000 --- a/tests/services/schemas/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2021 Graz University of Technology. -# -# Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it -# under the terms of the MIT License; see LICENSE file for more details. - - -"""Tests for schemas.py .""" diff --git a/tests/services/schemas/access/conftest.py b/tests/services/schemas/access/conftest.py new file mode 100755 index 00000000..bfa82f8d --- /dev/null +++ b/tests/services/schemas/access/conftest.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + + +"""Pytest configuration. + +See https://pytest-invenio.readthedocs.io/ for documentation on which test +fixtures are available. +""" diff --git a/tests/services/schemas/access/test_record.py b/tests/services/schemas/access/test_record.py new file mode 100755 index 00000000..fc3162e6 --- /dev/null +++ b/tests/services/schemas/access/test_record.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + + +"""Test record AccessSchema.""" + + +from datetime import timedelta + +import arrow +import pytest +from flask_babelex import lazy_gettext as _ +from marshmallow import ValidationError +from marshmallow.exceptions import ValidationError + +from invenio_records_marc21.services.schemas.access import AccessSchema + + +def test_valid_full(): + valid_full = { + "metadata": "embargoed", + "owned_by": [{"user": 1}], + "embargo": { + "until": (arrow.utcnow().datetime + timedelta(days=2)).strftime("%Y-%m-%d"), + "active": True, + "reason": "Because I can!", + }, + "files": "public", + } + assert valid_full == AccessSchema().load(valid_full) + + +@pytest.mark.parametrize("value", ["public", "embargoed", "restricted"]) +def test_valid_metadata_protection(value): + assert AccessSchema().validate_metadata_protection(value) is None + + +def test_invalid_metadata_protection(): + with pytest.raises(ValidationError) as e: + AccessSchema().validate_metadata_protection("invalid") + assert e.value.messages[0] == _("'metadata' must be either 'public', 'embargoed' or 'restricted'") + assert e.value.field_name == "metadata" + + +@pytest.mark.parametrize("value", ["public", "embargoed", "restricted"]) +def test_valid_files_protection(value): + assert AccessSchema().validate_files_protection(value) is None + + +def test_invalid_files_protection(): + with pytest.raises(ValidationError) as e: + AccessSchema().validate_files_protection("invalid") + assert e.value.messages[0] == _("'files' must be either 'public', 'embargoed' or 'restricted'") + assert e.value.field_name == "files" diff --git a/tests/services/schemas/conftest.py b/tests/services/schemas/conftest.py new file mode 100755 index 00000000..ce71376b --- /dev/null +++ b/tests/services/schemas/conftest.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + + +"""Pytest configuration. + +See https://pytest-invenio.readthedocs.io/ for documentation on which test +fixtures are available. +""" + +import pytest + + +@pytest.fixture() +def full_metadata(): + """Metadata full record marc21 xml.""" + metadata = { + "xml": "990079940640203331 AT-OBV20170703041800.0cr 100504|1932AC08088803AC08088803" + } + return metadata + + +@pytest.fixture() +def min_metadata(): + """Metadata empty record marc21 xml.""" + metadata = { + "xml": "" + } + return metadata + + +@pytest.fixture() +def min_json_metadata(): + """Metadata empty record marc21 json.""" + metadata = { + "json": {} + } + return metadata diff --git a/tests/services/schemas/test_access.py b/tests/services/schemas/test_access.py index 65ca1366..a41da289 100755 --- a/tests/services/schemas/test_access.py +++ b/tests/services/schemas/test_access.py @@ -8,7 +8,9 @@ # under the terms of the MIT License; see LICENSE file for more details. -"""Test metadata access schema.""" +"""Test record AccessSchema.""" + + from datetime import timedelta import arrow diff --git a/tests/services/schemas/test_metadata.py b/tests/services/schemas/test_metadata.py new file mode 100644 index 00000000..8d5d13fb --- /dev/null +++ b/tests/services/schemas/test_metadata.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + + +"""Test for record MetadataSchema.""" + + +from dojson.contrib.marc21 import marc21 +from dojson.contrib.marc21.utils import create_record + +from invenio_records_marc21.services.schemas.metadata import MetadataSchema + + +def _test_metadata(test, expected): + assert test.keys() == expected.keys() + for key in test.keys(): + assert test[key] == expected[key] + + +def test_full_metadata_xml_schema(app, full_metadata): + """Test metadata schema.""" + + metadata = MetadataSchema() + data = metadata.load(full_metadata) + _test_metadata( + data["json"], + marc21.do(create_record(full_metadata["xml"])), + ) + assert "xml" not in data + + +def test_minimal_metadata_xml_schema(app, min_metadata): + metadata = MetadataSchema() + data = metadata.load(min_metadata) + _test_metadata( + data["json"], + marc21.do(create_record(min_metadata["xml"])), + ) + assert "xml" not in data + + +def test_minimal_metadata_json_schema(app, min_json_metadata): + metadata = MetadataSchema() + data = metadata.load(min_json_metadata) + assert data == min_json_metadata + _test_metadata( + data, + min_json_metadata, + ) + assert "xml" not in data From cc1c4e680f13d2674973480c7ceae09060381b0f Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Thu, 12 Aug 2021 09:02:57 +0200 Subject: [PATCH 066/217] feature: add record check_draft field adding to record a check field if a draft is pressent for the record --- invenio_records_marc21/records/api.py | 3 + .../records/systemfields/__init__.py | 2 + .../records/systemfields/has_draft.py | 64 +++++++++++++++++++ 3 files changed, 69 insertions(+) create mode 100644 invenio_records_marc21/records/systemfields/has_draft.py diff --git a/invenio_records_marc21/records/api.py b/invenio_records_marc21/records/api.py index 5f7198ad..385d9e09 100644 --- a/invenio_records_marc21/records/api.py +++ b/invenio_records_marc21/records/api.py @@ -25,6 +25,7 @@ from . import models from .systemfields import ( + HasDraftField, MarcDraftProvider, MarcPIDFieldContext, MarcRecordProvider, @@ -88,6 +89,7 @@ class Marc21Draft(Draft): delete=False, ) access = RecordAccessField() + has_draft = HasDraftField() bucket_id = ModelField(dump=False) @@ -130,6 +132,7 @@ class Marc21Record(Record): ) access = RecordAccessField() + has_draft = HasDraftField(Marc21Draft) bucket_id = ModelField(dump=False) diff --git a/invenio_records_marc21/records/systemfields/__init__.py b/invenio_records_marc21/records/systemfields/__init__.py index 2bcd7b56..a5ed4872 100644 --- a/invenio_records_marc21/records/systemfields/__init__.py +++ b/invenio_records_marc21/records/systemfields/__init__.py @@ -9,10 +9,12 @@ """System fields module.""" from .context import MarcPIDFieldContext +from .has_draft import HasDraftField from .providers import MarcDraftProvider, MarcRecordProvider from .resolver import MarcResolver __all__ = ( + "HasDraftField", "MarcPIDFieldContext", "MarcDraftProvider", "MarcRecordProvider", diff --git a/invenio_records_marc21/records/systemfields/has_draft.py b/invenio_records_marc21/records/systemfields/has_draft.py new file mode 100644 index 00000000..3e7dd33e --- /dev/null +++ b/invenio_records_marc21/records/systemfields/has_draft.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 CERN. +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it +# under the terms of the MIT License; see LICENSE file for more details.. + +"""Record has draft check field. + +The HasDraftField is used to check if an associated draft exists for a +a record. +""" + +from invenio_records.dictutils import dict_set +from invenio_records.systemfields import SystemField +from sqlalchemy.orm.exc import NoResultFound + + +class HasDraftField(SystemField): + """Status field which checks against an expected status.""" + + def __init__(self, draft_cls=None, key=None): + """Initialize the PIDField. + + It stores the `record.has_draft` value in the secondary storage + system's record or defaults to `False` if the `draft_cls` is not passed + e.g Draft records. + + :param key: Attribute name of the HasDraftCheckField. + :param draft_cls: The draft class to use for querying. + """ + super().__init__(key=key) + self.draft_cls = draft_cls + + # + # Data descriptor methods (i.e. attribute access) + # + def __get__(self, record, owner=None): + """Get the persistent identifier.""" + if record is None: + return self # returns the field itself. + + # If not draft_cls is passed return False + if self.draft_cls is None: + return False + + try: + self.draft_cls.get_record(record.id) + return True + except NoResultFound: + return False + + def pre_dump(self, record, data, **kwargs): + """Called before a record is dumped in a secondary storage system.""" + dict_set( + data, + self.key, + record.has_draft + ) + + def post_load(self, record, data, **kwargs): + """Called after a record is loaded from a secondary storage system.""" + record.pop(self.key, None) From 61f6bcc0bae36e42abf22e536d25739c6da68c0f Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Thu, 12 Aug 2021 09:28:31 +0200 Subject: [PATCH 067/217] feature: service lift embargo giving the marc21 service to search for embargoed records and lift the embargo if the date reached. --- .../systemfields/access/fields/record.py | 2 +- invenio_records_marc21/services/errors.py | 25 +++++++++ .../services/permissions.py | 8 ++- invenio_records_marc21/services/services.py | 52 +++++++++++++++++-- 4 files changed, 81 insertions(+), 6 deletions(-) create mode 100644 invenio_records_marc21/services/errors.py diff --git a/invenio_records_marc21/records/systemfields/access/fields/record.py b/invenio_records_marc21/records/systemfields/access/fields/record.py index bb94d3fd..00c246e5 100755 --- a/invenio_records_marc21/records/systemfields/access/fields/record.py +++ b/invenio_records_marc21/records/systemfields/access/fields/record.py @@ -145,7 +145,7 @@ def obj(self, instance): data = self.get_dictkey(instance) if data: - obj = self._access_obj_class.from_dict(data, has_files=len(instance.files)) + obj = self._access_obj_class.from_dict(data, has_files=len(instance.files or [])) else: obj = self._access_obj_class() diff --git a/invenio_records_marc21/services/errors.py b/invenio_records_marc21/services/errors.py new file mode 100644 index 00000000..6bd57146 --- /dev/null +++ b/invenio_records_marc21/services/errors.py @@ -0,0 +1,25 @@ + +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + + +"""Services exceptions.""" + + +class Marc21RecordsException(Exception): + """Base exception for Marc21Records errors.""" + + +class EmbargoNotLiftedError(Marc21RecordsException): + """Embargo could not be lifted .""" + + def __init__(self, record_id): + """Initialise error.""" + super().__init__(f"Embargo could not be lifted for record: {record_id}") diff --git a/invenio_records_marc21/services/permissions.py b/invenio_records_marc21/services/permissions.py index 4fe54969..f5f01e8b 100644 --- a/invenio_records_marc21/services/permissions.py +++ b/invenio_records_marc21/services/permissions.py @@ -1,9 +1,12 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """Permissions for Invenio Marc21 Records.""" @@ -33,6 +36,7 @@ class Marc21RecordPermissionPolicy(RecordPermissionPolicy): can_update = [AnyUser()] can_new_version = [AnyUser()] can_edit = [AnyUser()] + can_lift_embargo = [AnyUser()] # Draft permissions can_read_draft = [AnyUser()] diff --git a/invenio_records_marc21/services/services.py b/invenio_records_marc21/services/services.py index c1f02ce9..1f9afacb 100644 --- a/invenio_records_marc21/services/services.py +++ b/invenio_records_marc21/services/services.py @@ -1,14 +1,18 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """Marc21 Record Service.""" - +import arrow +from invenio_db import db from invenio_drafts_resources.services.records import RecordService from invenio_records_resources.services.files.service import FileService from invenio_records_resources.services.records.results import RecordItem @@ -19,6 +23,7 @@ Marc21RecordFilesServiceConfig, Marc21RecordServiceConfig, ) +from .errors import EmbargoNotLiftedError from .record import Marc21Metadata @@ -85,6 +90,47 @@ def update_draft( data = self._create_data(identity, data, metadata, access) return super().update_draft(id_, identity, data, revision_id) + def _lift_embargo_from(self, record): + """Lifts embargo from record or draft.""" + if not record.access.embargo.lift(): + raise EmbargoNotLiftedError(record["id"]) + record.access.protection.metadata = "public" + record.access.protection.files = "public" + + def _draft_access_field_was_modified(self, draft, record): + """Returns True if draft's access field was modified.""" + return draft.get('access') == record.get('access') + + def lift_embargo(self, _id, identity): + """Lifts embargo from the record and updates draft.""" + # Get the record + record = self.record_cls.pid.resolve(_id) + + # Check permissions + self.require_permission(identity, "lift_embargo", record=record) + + lifted_embargo_from_draft = False + # Check if record has already a draft + if record.has_draft: + draft = self.draft_cls.pid.resolve(_id, registered_only=False) + # If the draft has no modifications in the access field the + # embargo is lifted + if self._draft_access_field_was_modified(draft, record): + # Lifts embargo from draft + self._lift_embargo_from(draft) + lifted_embargo_from_draft = True + + # Lifts embargo from record + self._lift_embargo_from(record) + # Commit and index + record.commit() + if record.has_draft and lifted_embargo_from_draft: + draft.commit() + db.session.commit() + self.indexer.index(record) + if record.has_draft and lifted_embargo_from_draft: + self.indexer.index(draft) + # # Record files From 266c1014cccac06ca3216df2cfedc3128eeaf8d0 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 16 Aug 2021 07:06:56 +0200 Subject: [PATCH 068/217] feature: celery task lift embargo adding a celery task to lift an embargoed record --- invenio_records_marc21/services/tasks.py | 41 ++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 invenio_records_marc21/services/tasks.py diff --git a/invenio_records_marc21/services/tasks.py b/invenio_records_marc21/services/tasks.py new file mode 100644 index 00000000..b59ac460 --- /dev/null +++ b/invenio_records_marc21/services/tasks.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + +"""Marc21 Celery tasks.""" + +import arrow +from celery import shared_task +from flask import current_app +from invenio_access.permissions import system_identity + +from ..proxies import current_records_marc21 +from .errors import EmbargoNotLiftedError + + +@shared_task(ignore_result=True) +def update_expired_embargos(): + """Release expired embargoes.""" + service = current_records_marc21.records_service + embargoed_q = ( + "access.embargo.active:true AND access.embargo.until:" + f"{{* TO {arrow.utcnow().datetime.strftime('%Y-%m-%d')}}}" + ) + + restricted_records = service.scan(identity=system_identity, q=embargoed_q) + for restricted_record in restricted_records.hits: + try: + service.lift_embargo(_id=restricted_record["id"], identity=system_identity) + except EmbargoNotLiftedError: + current_app.logger.warning( + "Embargo from record with id" + f" {restricted_record['id']} was not" + " lifted" + ) + continue From 97b4517f1501347de9afa291a525fb2705c03d6e Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Thu, 12 Aug 2021 09:33:27 +0200 Subject: [PATCH 069/217] tests: add service lift embargo adding a test-set to lift an embargoed record --- tests/services/conftest.py | 14 +++ tests/services/test_create_record.py | 8 +- tests/services/test_record_service.py | 152 ++++++++++++++++++++++---- 3 files changed, 148 insertions(+), 26 deletions(-) diff --git a/tests/services/conftest.py b/tests/services/conftest.py index 48ab0b2e..f3e51954 100755 --- a/tests/services/conftest.py +++ b/tests/services/conftest.py @@ -14,6 +14,7 @@ See https://pytest-invenio.readthedocs.io/ for documentation on which test fixtures are available. """ +from collections import namedtuple import pytest from flask_principal import Identity @@ -34,6 +35,19 @@ def create_app(instance_path): return create_api +RunningApp = namedtuple("RunningApp", ["app", "service", "identity_simple"]) + + +@pytest.fixture() +def running_app(app, service, identity_simple): + """This fixture provides an app with the typically needed db data loaded. + + All of these fixtures are often needed together, so collecting them + under a semantic umbrella makes sense. + """ + return RunningApp(app, service, identity_simple) + + @pytest.fixture() def identity_simple(): """Simple identity fixture.""" diff --git a/tests/services/test_create_record.py b/tests/services/test_create_record.py index 9dddbc40..cb755217 100755 --- a/tests/services/test_create_record.py +++ b/tests/services/test_create_record.py @@ -37,8 +37,8 @@ def marc21(): return {"metadata": {"xml": ""}} -def test_create_with_service(app, marc21, identity_simple): - +def test_create_with_service(running_app, marc21): + identity_simple = running_app.identity_simple service = Marc21RecordService(config=Marc21RecordServiceConfig) draft = service.create(data=marc21, identity=identity_simple, access=None) @@ -134,8 +134,8 @@ def empty_data(): }, ], ) -def test_create_with_access(app, empty_data, identity_simple, access): - +def test_create_with_access(running_app, empty_data, access): + identity_simple = running_app.identity_simple service = Marc21RecordService(config=Marc21RecordServiceConfig) draft = service.create( data=empty_data, identity=identity_simple, access=access["input"] diff --git a/tests/services/test_record_service.py b/tests/services/test_record_service.py index 01f13277..f552f999 100755 --- a/tests/services/test_record_service.py +++ b/tests/services/test_record_service.py @@ -7,41 +7,28 @@ # Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it # under the terms of the MIT License; see LICENSE file for more details. -"""Service tests. +"""Tests for marc21 Service.""" -Test to add: -- Read a tombstone page -- Read with missing permissions -- Read with missing pid -""" -import time -from collections import namedtuple +from datetime import datetime +from unittest import mock +import arrow import pytest -from dojson.contrib.marc21.utils import create_record +from dateutil import tz from dojson.contrib.to_marc21 import to_marc21 from invenio_pidstore.errors import PIDDoesNotExistError from invenio_pidstore.models import PIDStatus from sqlalchemy.orm.exc import NoResultFound -RunningApp = namedtuple("RunningApp", ["app", "service", "identity_simple"]) - - -@pytest.fixture() -def running_app(app, service, identity_simple): - """This fixture provides an app with the typically needed db data loaded. - - All of these fixtures are often needed together, so collecting them - under a semantic umbrella makes sense. - """ - return RunningApp(app, service, identity_simple) - +from invenio_records_marc21.proxies import current_records_marc21 +from invenio_records_marc21.services.errors import EmbargoNotLiftedError # # Operations tests # + def test_create_draft(running_app, metadata): """Test draft creation of a non-existing record.""" @@ -157,7 +144,6 @@ def test_update_draft(running_app, metadata, metadata2): ) # Check the updates where savedif "json" in data: - read_draft = service.read_draft(id_=draft.id, identity=identity_simple) assert draft.id == update_draft.id @@ -194,3 +180,125 @@ def test_create_publish_new_version(running_app, metadata): # files attribute in record causes at create change the revision_id twice assert record_2._record.revision_id == 1 assert record_2["id"] != record["id"] + + +# Embargo lift +# +@mock.patch('arrow.utcnow') +def test_embargo_lift_without_draft( + mock_arrow, running_app, marc21_record): + identity_simple = running_app.identity_simple + service = current_records_marc21.records_service + # Add embargo to record + marc21_record["access"]["files"] = 'restricted' + marc21_record["access"]["status"] = 'embargoed' + marc21_record["access"]["embargo"] = dict( + active=True, until='2020-06-01', reason=None + ) + # We need to set the current date in the past to pass the validations + mock_arrow.return_value = arrow.get(datetime(1954, 9, 29), tz.gettz('UTC')) + draft = service.create(identity_simple, marc21_record) + record = service.publish(id_=draft.id, identity=identity_simple) + # Recover current date + mock_arrow.return_value = arrow.get(datetime.utcnow()) + + service.lift_embargo(_id=record['id'], identity=identity_simple) + record_lifted = service.record_cls.pid.resolve(record['id']) + + assert not record_lifted.access.embargo.active + assert record_lifted.access.protection.files == 'public' + assert record_lifted.access.protection.metadata == 'public' + assert record_lifted.access.status.value == 'public' + + +@mock.patch('arrow.utcnow') +def test_embargo_lift_with_draft( + mock_arrow, running_app, marc21_record): + identity_simple = running_app.identity_simple + service = current_records_marc21.records_service + # Add embargo to record + marc21_record["access"]["files"] = 'restricted' + marc21_record["access"]["status"] = 'embargoed' + marc21_record["access"]["embargo"] = dict( + active=True, until='2020-06-01', reason=None + ) + + mock_arrow.return_value = arrow.get(datetime(1954, 9, 29), tz.gettz('UTC')) + draft = service.create(identity_simple, marc21_record) + record = service.publish(id_=draft.id, identity=identity_simple) + # This draft simulates an existing one while lifting the record + ongoing_draft = service.edit(id_=draft.id, identity=identity_simple) + + mock_arrow.return_value = arrow.get(datetime.utcnow()) + + service.lift_embargo(_id=record['id'], identity=identity_simple) + record_lifted = service.record_cls.pid.resolve(record['id']) + draft_lifted = service.draft_cls.pid.resolve(ongoing_draft['id']) + + assert record_lifted.access.embargo.active is False + assert record_lifted.access.protection.files == 'public' + assert record_lifted.access.protection.metadata == 'public' + + assert draft_lifted.access.embargo.active is False + assert draft_lifted.access.protection.files == 'public' + assert draft_lifted.access.protection.metadata == 'public' + + +@mock.patch('arrow.utcnow') +def test_embargo_lift_with_updated_draft( + mock_arrow, running_app, marc21_record): + identity_simple = running_app.identity_simple + service = current_records_marc21.records_service + # Add embargo to record + marc21_record["access"]["files"] = 'restricted' + marc21_record["access"]["status"] = 'embargoed' + marc21_record["access"]["embargo"] = dict( + active=True, until='2020-06-01', reason=None + ) + # We need to set the current date in the past to pass the validations + mock_arrow.return_value = arrow.get(datetime(1954, 9, 29), tz.gettz('UTC')) + draft = service.create(identity_simple, marc21_record) + record = service.publish(id_=draft.id, identity=identity_simple) + # This draft simulates an existing one while lifting the record + service.edit(id_=draft.id, identity=identity_simple) + # Recover current date + mock_arrow.return_value = arrow.get(datetime.utcnow()) + + # Change record's title and access field to be restricted + marc21_record["metadata"]["title"] = 'Record modified by the user' + marc21_record["access"]["status"] = 'restricted' + marc21_record["access"]["embargo"] = dict( + active=False, until=None, reason=None + ) + # Update the ongoing draft with the new data simulating the user's input + ongoing_draft = service.update_draft( + id_=draft.id, identity=identity_simple, data=marc21_record) + + service.lift_embargo(_id=record['id'], identity=identity_simple) + record_lifted = service.record_cls.pid.resolve(record['id']) + draft_lifted = service.draft_cls.pid.resolve(ongoing_draft['id']) + + assert record_lifted.access.embargo.active is False + assert record_lifted.access.protection.files == 'public' + assert record_lifted.access.protection.metadata == 'public' + + assert draft_lifted.access.embargo.active is False + assert draft_lifted.access.protection.files == 'restricted' + assert draft_lifted.access.protection.metadata == 'public' + + +def test_embargo_lift_with_error(running_app, marc21_record): + identity_simple = running_app.identity_simple + service = current_records_marc21.records_service + # Add embargo to record + marc21_record["access"]["files"] = 'restricted' + marc21_record["access"]["status"] = 'embargoed' + marc21_record["access"]["embargo"] = dict( + active=True, until='3220-06-01', reason=None + ) + draft = service.create(identity_simple, marc21_record) + record = service.publish(id_=draft.id, identity=identity_simple) + + # Record should not be lifted since it didn't expire (until 3220) + with pytest.raises(EmbargoNotLiftedError): + service.lift_embargo(_id=record['id'], identity=identity_simple) From 8dee9b8bcbf841f2f03cd9409986a1f458a5334c Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Mon, 16 Aug 2021 07:26:50 +0200 Subject: [PATCH 070/217] modification: config celery tasks adding a default configuration for ambargo lift scheduled celery task and load with extension. --- invenio_records_marc21/config.py | 13 +++++++++++++ invenio_records_marc21/ext.py | 5 +++++ 2 files changed, 18 insertions(+) diff --git a/invenio_records_marc21/config.py b/invenio_records_marc21/config.py index c1d0ee79..f0b4e942 100644 --- a/invenio_records_marc21/config.py +++ b/invenio_records_marc21/config.py @@ -9,6 +9,8 @@ from __future__ import absolute_import, print_function +from celery.schedules import crontab + INVENIO_MARC21_BASE_TEMPLATE = "invenio_records_marc21/base.html" INVENIO_MARC21_REST_ENDPOINTS = {} @@ -40,3 +42,14 @@ INVENIO_MARC21_ENDPOINTS_ENABLED = True """Enable/disable automatic endpoint registration.""" + +CELERY_BEAT_SCHEDULE = { + "marc21_service_embargo_lift": { + "task": "invenio_records_marc21.services.tasks.update_expired_embargos", + "schedule": crontab( + minute="5", + hour="0", + ), + }, +} +"""Celery tasks for the module.""" diff --git a/invenio_records_marc21/ext.py b/invenio_records_marc21/ext.py index 94d8b69c..85f738eb 100755 --- a/invenio_records_marc21/ext.py +++ b/invenio_records_marc21/ext.py @@ -72,6 +72,11 @@ def init_config(self, app): app.config.setdefault(k, getattr(config, k)) elif k == "SEARCH_UI_JSTEMPLATE_RESULTS": app.config["SEARCH_UI_JSTEMPLATE_RESULTS"] = getattr(config, k) + elif k == "CELERY_BEAT_SCHEDULE": + if "CELERY_BEAT_SCHEDULE" in app.config: + app.config["CELERY_BEAT_SCHEDULE"].update(getattr(config, k)) + else: + app.config.setdefault(k, getattr(config, k)) else: for n in [ "INVENIO_MARC21_REST_ENDPOINTS", From e2da8f6b5fa7347a56f8280847313f0e2bf436c4 Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Mon, 16 Aug 2021 07:27:23 +0200 Subject: [PATCH 071/217] build: add celery dependency --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index c57301bf..0da8d5d2 100755 --- a/setup.py +++ b/setup.py @@ -55,6 +55,7 @@ "arrow>=1.0.0", "dojson>=1.4.0", "lxml>=4.6.2", + "invenio-celery>=1.2.0,<2.0.0", "invenio-records-rest>=1.5.0,<2.0.0", "invenio-drafts-resources>=0.13.0,<0.14.0", "invenio-vocabularies>=0.8.0,<0.9.0", From 45fad80a4fe15832ca717ef8b4ea6b06d4f1b601 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 16 Aug 2021 13:52:04 +0200 Subject: [PATCH 072/217] modification: rename private service function --- invenio_records_marc21/services/services.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/invenio_records_marc21/services/services.py b/invenio_records_marc21/services/services.py index 1f9afacb..47d776eb 100644 --- a/invenio_records_marc21/services/services.py +++ b/invenio_records_marc21/services/services.py @@ -97,9 +97,9 @@ def _lift_embargo_from(self, record): record.access.protection.metadata = "public" record.access.protection.files = "public" - def _draft_access_field_was_modified(self, draft, record): + def _is_draft_access_field_modified(self, draft, record): """Returns True if draft's access field was modified.""" - return draft.get('access') == record.get('access') + return draft.get("access") == record.get("access") def lift_embargo(self, _id, identity): """Lifts embargo from the record and updates draft.""" @@ -115,7 +115,7 @@ def lift_embargo(self, _id, identity): draft = self.draft_cls.pid.resolve(_id, registered_only=False) # If the draft has no modifications in the access field the # embargo is lifted - if self._draft_access_field_was_modified(draft, record): + if self._is_draft_access_field_modified(draft, record): # Lifts embargo from draft self._lift_embargo_from(draft) lifted_embargo_from_draft = True From 06bae3c6d5af19112212b8915aae4d56c9f294b1 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 16 Aug 2021 15:17:31 +0200 Subject: [PATCH 073/217] modification: remove comments --- invenio_records_marc21/services/services.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/invenio_records_marc21/services/services.py b/invenio_records_marc21/services/services.py index 47d776eb..5613ff5f 100644 --- a/invenio_records_marc21/services/services.py +++ b/invenio_records_marc21/services/services.py @@ -106,15 +106,13 @@ def lift_embargo(self, _id, identity): # Get the record record = self.record_cls.pid.resolve(_id) - # Check permissions self.require_permission(identity, "lift_embargo", record=record) lifted_embargo_from_draft = False # Check if record has already a draft if record.has_draft: draft = self.draft_cls.pid.resolve(_id, registered_only=False) - # If the draft has no modifications in the access field the - # embargo is lifted + if self._is_draft_access_field_modified(draft, record): # Lifts embargo from draft self._lift_embargo_from(draft) From 7bfbb731f1051dd8d2cc795cfb65ce540a26fec1 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 30 Aug 2021 15:59:00 +0200 Subject: [PATCH 074/217] feature: display error message if an error occurs display a custom error message in the console --- invenio_records_marc21/cli.py | 34 +++++++++++++++++++++++------- invenio_records_marc21/errors.py | 36 ++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 8 deletions(-) create mode 100644 invenio_records_marc21/errors.py diff --git a/invenio_records_marc21/cli.py b/invenio_records_marc21/cli.py index 65782fca..48c4e4fe 100644 --- a/invenio_records_marc21/cli.py +++ b/invenio_records_marc21/cli.py @@ -18,6 +18,7 @@ from flask_principal import Identity from invenio_access import any_user +from .errors import error_messages from .proxies import current_records_marc21 from .records.systemfields.access import AccessStatusEnum from .services.record import Marc21Metadata @@ -145,11 +146,28 @@ def marc21(): def demo(number, file, metadata_only): """Create number of fake records for demo purposes.""" click.secho("Creating demo records...", fg="blue") - - for _ in range(number): - if metadata_only: - create_fake_metadata(file) - else: - create_fake_record(file) - - click.secho("Created records!", fg="green") + try: + for _ in range(number): + if metadata_only: + create_fake_metadata(file) + else: + create_fake_record(file) + + click.secho("Created records!", fg="green") + except Exception as e: + _create_errormessage(e) + + +def _create_errormessage(e: Exception): + """Create an error message for CLI.""" + message = "" + + errors = error_messages.get(type(e).__name__) + for error in errors: + if error.get("args") in e.args[0]: + message = error.get("message") + break + if message: + click.secho(message, fg="red") + else: + click.secho("Error:\n" + str(e), fg="red") diff --git a/invenio_records_marc21/errors.py b/invenio_records_marc21/errors.py new file mode 100644 index 00000000..d68e8f77 --- /dev/null +++ b/invenio_records_marc21/errors.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + +"""Marc21 error messages.""" + +from flask_babelex import gettext as _ + +error_messages = { + "ProgrammingError": [ + { + "args": "UndefinedTable", + "message": _( + "The table you are looking for does not exist.\n" + "Maybe you missed to re-setup the services after the marc21 module was installed.\n" + "Try to do a invenio-cli services setup -f" + ), + }, + ], + "OperationalError": [ + { + "args": "could not connect to server", + "message": _( + "Connection to Database refused.\n" + "Maybe you missed to start the database service.\n" + "Try to do a invenio-cli services setup -f" + ), + }, + ], +} From 8864c4f0884d6d2c0303fbd1fe95545e725459bb Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 30 Aug 2021 15:59:47 +0200 Subject: [PATCH 075/217] test: invenio cli create demo records test set --- tests/conftest.py | 20 ++---- tests/test-metadata.xml | 18 ++++++ tests/test-record.json | 5 ++ tests/test_invenio_records_cli.py | 104 ++++++++++++++++++++++++++++++ 4 files changed, 132 insertions(+), 15 deletions(-) create mode 100644 tests/test-metadata.xml create mode 100644 tests/test-record.json create mode 100644 tests/test_invenio_records_cli.py diff --git a/tests/conftest.py b/tests/conftest.py index d45bb576..0788da83 100755 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -19,13 +19,10 @@ import arrow import pytest -from flask import Flask -from flask_babelex import Babel +from invenio_app.factory import create_api from invenio_files_rest.models import Location -from invenio_records_marc21 import InvenioRecordsMARC21 from invenio_records_marc21.records import Marc21Draft, Marc21Parent -from invenio_records_marc21.views import create_record_bp @pytest.fixture() @@ -38,7 +35,9 @@ def embargoed_record(): "status": "embargoed", "embargo": { "active": True, - "until": (arrow.utcnow().datetime + timedelta(days=2)).strftime("%Y-%m-%d"), + "until": (arrow.utcnow().datetime + timedelta(days=2)).strftime( + "%Y-%m-%d" + ), "reason": None, }, }, @@ -121,13 +120,4 @@ def app(base_app, database): @pytest.fixture(scope="module") def create_app(instance_path): """Application factory fixture.""" - - def factory(**config): - app = Flask("testapp", instance_path=instance_path) - app.config.update(**config) - Babel(app) - InvenioRecordsMARC21(app) - create_record_bp(app) - return app - - return factory + return create_api diff --git a/tests/test-metadata.xml b/tests/test-metadata.xml new file mode 100644 index 00000000..1773a66a --- /dev/null +++ b/tests/test-metadata.xml @@ -0,0 +1,18 @@ + + 990079940640203331 + cr + 100504|1932 + AC08088803 + + AC08088803 + + + (AT-OBV)AC08088803 + (Aleph)007994064ACC01 + (DE-599)OBVAC08088803 + + + <<Die>> Internationale Werkbundsiedlung Wien 1932 + hrsg. von Josef Frank + + diff --git a/tests/test-record.json b/tests/test-record.json new file mode 100644 index 00000000..08c5cb19 --- /dev/null +++ b/tests/test-record.json @@ -0,0 +1,5 @@ +{ + "metadata": { + "xml": "990079940640203331 AT-OBV20170703041800.0cr 100504|1932AC08088803AC08088803(AT-OBV)AC08088803(Aleph)007994064ACC01(DE-599)OBVAC08088803<<Die>> Internationale Werkbundsiedlung Wien 1932hrsg. von Josef FrankWienSchroll1932" + } +} diff --git a/tests/test_invenio_records_cli.py b/tests/test_invenio_records_cli.py new file mode 100644 index 00000000..300107c8 --- /dev/null +++ b/tests/test_invenio_records_cli.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + +"""Module cli tests.""" + + +from datetime import timedelta + +import arrow + +from invenio_records_marc21.cli import ( + create_fake_metadata, + create_fake_record, + fake_access_right, + fake_feature_date, + marc21, + system_identity, +) +from invenio_records_marc21.records.systemfields.access import AccessStatusEnum + + +def test_system_identity(): + """Test r identity for demo.""" + identity = system_identity() + assert identity.id == 1 + assert identity.provides + + +def test_fake_access_right(): + """Test random access right for demo.""" + access = fake_access_right() + assert access in AccessStatusEnum.list() + + +def test_fake_feature_date(): + """Test create future date for demo.""" + assert isinstance(fake_feature_date(), str) + date = fake_feature_date(days=30) + start_date = arrow.utcnow().datetime + assert isinstance(date, str) + date_arrow = arrow.get(date) + assert date_arrow >= arrow.get(start_date.date() + timedelta(days=1)) + assert date_arrow < arrow.get(start_date.date() + timedelta(days=30)) + + +def test_fake_demo_record_creation(app): + """Test create fake full record with marc21 service.""" + with app.app_context(): + record = create_fake_record("../tests/test-record.json") + assert record + assert record.id + + +def test_fake_demo_metadata_creation(app): + """Test create fake metadata record with marc21 service.""" + with app.app_context(): + record = create_fake_metadata("../tests/test-metadata.xml") + assert record + assert record.id + + +def test_cli_create_demo_record(app): + """Test cli marc21 demo record.""" + runner = app.test_cli_runner() + result = runner.invoke( + marc21, + [ + "demo", + "-f", + "../tests/test-record.json", + "-m", + "False", + "-n", + "10", + ], + ) + assert result.exit_code == 0 + assert result.output == "Creating demo records...\nCreated records!\n" + + +def test_cli_create_demo_metadata(app): + """Test cli marc21 demo metadata.""" + runner = app.test_cli_runner() + result = runner.invoke( + marc21, + [ + "demo", + "-f", + "../tests/test-metadata.xml", + "-m", + "True", + "-n", + "10", + ], + ) + assert result.exit_code == 0 + assert result.output == "Creating demo records...\nCreated records!\n" From fd8dabe3d728123487194d5ecd305975015d8277 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 30 Aug 2021 16:04:17 +0200 Subject: [PATCH 076/217] build: update manifest --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index 927655eb..981cdfc4 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -42,3 +42,4 @@ recursive-include invenio_records_marc21 *.js recursive-include invenio_records_marc21 *.xsd recursive-include tests *.py recursive-include tests *.json +recursive-include tests *.xml \ No newline at end of file From 19fc60d008098f28bac1016bb4f909ebed0e0ce3 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Tue, 31 Aug 2021 09:43:34 +0200 Subject: [PATCH 077/217] modification: exception handling creating a exception handler decorator to print an exception message to cli. --- invenio_records_marc21/cli.py | 36 ++++++------------- invenio_records_marc21/errors.py | 62 ++++++++++++++++++++++++++------ 2 files changed, 61 insertions(+), 37 deletions(-) diff --git a/invenio_records_marc21/cli.py b/invenio_records_marc21/cli.py index 48c4e4fe..0b9e198d 100644 --- a/invenio_records_marc21/cli.py +++ b/invenio_records_marc21/cli.py @@ -7,6 +7,7 @@ """Command-line tools for demo module.""" + import json import random from datetime import timedelta @@ -18,7 +19,7 @@ from flask_principal import Identity from invenio_access import any_user -from .errors import error_messages +from .errors import log_exceptions from .proxies import current_records_marc21 from .records.systemfields.access import AccessStatusEnum from .services.record import Marc21Metadata @@ -143,31 +144,14 @@ def marc21(): help="Provided metadata only in file", ) @with_appcontext +@log_exceptions def demo(number, file, metadata_only): """Create number of fake records for demo purposes.""" click.secho("Creating demo records...", fg="blue") - try: - for _ in range(number): - if metadata_only: - create_fake_metadata(file) - else: - create_fake_record(file) - - click.secho("Created records!", fg="green") - except Exception as e: - _create_errormessage(e) - - -def _create_errormessage(e: Exception): - """Create an error message for CLI.""" - message = "" - - errors = error_messages.get(type(e).__name__) - for error in errors: - if error.get("args") in e.args[0]: - message = error.get("message") - break - if message: - click.secho(message, fg="red") - else: - click.secho("Error:\n" + str(e), fg="red") + for _ in range(number): + if metadata_only: + create_fake_metadata(file) + else: + create_fake_record(file) + + click.secho("Created records!", fg="green") diff --git a/invenio_records_marc21/errors.py b/invenio_records_marc21/errors.py index d68e8f77..fd20259b 100644 --- a/invenio_records_marc21/errors.py +++ b/invenio_records_marc21/errors.py @@ -10,27 +10,67 @@ """Marc21 error messages.""" +import functools + +import click from flask_babelex import gettext as _ -error_messages = { +ERROR_MESSAGE_WRAPPER = { "ProgrammingError": [ { "args": "UndefinedTable", - "message": _( - "The table you are looking for does not exist.\n" - "Maybe you missed to re-setup the services after the marc21 module was installed.\n" - "Try to do a invenio-cli services setup -f" - ), + "message": "DB_TABLE_NOT_FOUND", }, ], "OperationalError": [ { "args": "could not connect to server", - "message": _( - "Connection to Database refused.\n" - "Maybe you missed to start the database service.\n" - "Try to do a invenio-cli services setup -f" - ), + "message": "SERVICES_CONNECTION_REFUSED", + }, + ], + "ConnectionError": [ + { + "args": "Connection refused.", + "message": "SERVICES_CONNECTION_REFUSED", }, ], } + +ERROR_MESSAGES = { + "DB_TABLE_NOT_FOUND": _( + "The table you are looking for does not exist.\n" + "Maybe you missed to re-setup the services after the marc21 module was installed.\n" + "Try to do a invenio-cli services setup -f" + ), + "SERVICES_CONNECTION_REFUSED": _( + "Connection to Services refused.\n" + "Maybe you missed to start the services.\n" + "Try to do a invenio-cli services setup -f" + ), +} + + +def log_exceptions(f): + """A function wrapper for catching all exceptions and log to CLI.""" + + @functools.wraps(f) + def catch_and_log(*args, **kwargs): + try: + return f(*args, **kwargs) + except Exception as e: + _create_errormessage(e) + + return catch_and_log + + +def _create_errormessage(e: Exception): + """Create an error message for CLI.""" + message = "" + errors = ERROR_MESSAGE_WRAPPER.get(type(e).__name__, {}) + for error in errors: + if error.get("args") in e.args[0]: + wrap = error.get("message", "") + message = ERROR_MESSAGES.get(wrap) + break + message = message if message else "Error:\n" + str(e) + click.secho(message, fg="red") From feca6aa6b1698b080592dfa7e624f0e9e6248a65 Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Thu, 2 Sep 2021 10:45:58 +0200 Subject: [PATCH 078/217] modification: metadata schema moving metadata schema into a submodule and remove "order" key from metadata field. --- .../resources/serializers/fields/__init__.py | 16 +++++ .../resources/serializers/fields/metadata.py | 63 +++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 invenio_records_marc21/resources/serializers/fields/__init__.py create mode 100644 invenio_records_marc21/resources/serializers/fields/metadata.py diff --git a/invenio_records_marc21/resources/serializers/fields/__init__.py b/invenio_records_marc21/resources/serializers/fields/__init__.py new file mode 100644 index 00000000..1299a084 --- /dev/null +++ b/invenio_records_marc21/resources/serializers/fields/__init__.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + + +"""Record response fields.""" + +from .metadata import MetadataSchema + +__all__ = ("MetadataSchema",) diff --git a/invenio_records_marc21/resources/serializers/fields/metadata.py b/invenio_records_marc21/resources/serializers/fields/metadata.py new file mode 100644 index 00000000..eb747f6c --- /dev/null +++ b/invenio_records_marc21/resources/serializers/fields/metadata.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + +"""Metadata field for marc21 records.""" + + +from dojson.contrib.to_marc21 import to_marc21 +from dojson.contrib.to_marc21.utils import dumps +from dojson.utils import GroupableOrderedDict +from marshmallow import Schema +from marshmallow.decorators import pre_dump +from marshmallow.fields import Dict, Str + +from ..errors import Marc21XMLConvertError + + +class MetadataSchema(Schema): + """Metadata schema.""" + + xml = Str(required=False) + json = Dict(required=True) + + def _remove_order(self, data): + """Removing order key in marc21 dict.""" + if isinstance(data, dict): + kwargs = {} + if isinstance(data, GroupableOrderedDict): + kwargs = {"with_order": False} + temp = [] + for k, v in data.items(**kwargs): + temp.append((k, self._remove_order(v))) + return dict(temp) + elif isinstance(data, (list, tuple)): + return [self._remove_order(item) for item in data] + return data + + @pre_dump + def remove_order_key(self, data, **kwargs): + """Remove order key in metadata dict .""" + remove_order = self.context.get("remove_order", False) + if "json" in data and remove_order: + data["json"] = self._remove_order(GroupableOrderedDict(data["json"])) + return data + + @pre_dump + def convert_xml(self, data, **kwargs): + """Convert json into marc21 xml.""" + marcxml = self.context.get("marcxml", False) + if "json" in data and marcxml: + try: + data["xml"] = dumps(to_marc21.do(data["json"])) + except Exception as e: + raise Marc21XMLConvertError(e) + + del data["json"] + return data From 4f43104d35a7069ab8e00063c8fe6728734ebed6 Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Thu, 2 Sep 2021 10:53:04 +0200 Subject: [PATCH 079/217] modification: marc21 record ui schema dumping extra information and converted dates for the user interface. --- .../resources/serializers/schema.py | 38 ++++++++ .../resources/serializers/ui/__init__.py | 59 +++--------- .../serializers/ui/fields/__init__.py | 20 ++++ .../resources/serializers/ui/fields/access.py | 86 +++++++++++++++++ .../resources/serializers/ui/schema.py | 95 +++---------------- 5 files changed, 170 insertions(+), 128 deletions(-) create mode 100644 invenio_records_marc21/resources/serializers/schema.py create mode 100755 invenio_records_marc21/resources/serializers/ui/fields/__init__.py create mode 100644 invenio_records_marc21/resources/serializers/ui/fields/access.py diff --git a/invenio_records_marc21/resources/serializers/schema.py b/invenio_records_marc21/resources/serializers/schema.py new file mode 100644 index 00000000..645b7ae7 --- /dev/null +++ b/invenio_records_marc21/resources/serializers/schema.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + +"""Schemas for marc21 records serializers.""" + +from functools import partial + +from marshmallow import Schema +from marshmallow.fields import Nested +from marshmallow_utils.fields import SanitizedUnicode + +from .fields import MetadataSchema + + +class Marc21Schema(Schema): + """Schema for dumping extra information for the marc21 record.""" + + id = SanitizedUnicode(data_key="id", attribute="id") + metadata = Nested(MetadataSchema, attribute="metadata") + + class Meta: + """Meta class to accept unknwon fields.""" + + additional = ( + "access", + "created", + "updated", + "links", + "files", + "versions", + ) diff --git a/invenio_records_marc21/resources/serializers/ui/__init__.py b/invenio_records_marc21/resources/serializers/ui/__init__.py index 792cd63f..94fd2585 100755 --- a/invenio_records_marc21/resources/serializers/ui/__init__.py +++ b/invenio_records_marc21/resources/serializers/ui/__init__.py @@ -1,56 +1,21 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """Record response serializers.""" -from flask_resources.serializers import JSONSerializer - -from .schema import UIListSchema, UIObjectSchema - - -class UIJSONSerializer(JSONSerializer): - """UI JSON serializer implementation.""" - - object_key = "ui" - object_schema_cls = UIObjectSchema - list_schema_cls = UIListSchema - - # - # Dump Python dictionary (obj and list) - # - def dump_obj(self, obj): - """Dump the object with extra information.""" - obj[self.object_key] = self.object_schema_cls().dump(obj) - return obj - - def dump_list(self, obj_list): - """Dump the list of objects with extra information.""" - ctx = { - "object_key": self.object_key, - "object_schema_cls": self.object_schema_cls, - } - return self.list_schema_cls(context=ctx).dump(obj_list) - - # - # Serialize to JSON (obj and list) - # - def serialize_object(self, obj): - """Dump the object into a JSON string.""" - return super().serialize_object(self.dump_obj(obj)) - - def serialize_object_list(self, obj_list): - """Dump the object list into a JSON string.""" - return super().serialize_object_list(self.dump_list(obj_list)) - - def serialize_object_to_dict(self, obj): - """Dump the object into a JSON string.""" - return self.dump_obj(obj) +from .schema import Marc21UISchema +from .serializers import Marc21UIJSONSerializer, Marc21UIXMLSerializer - def serialize_object_list_to_dict(self, obj_list): - """Dump the object list into a JSON string.""" - return self.dump_list(obj_list) +__all__ = ( + "Marc21UISchema", + "Marc21UIJSONSerializer", + "Marc21UIXMLSerializer", +) diff --git a/invenio_records_marc21/resources/serializers/ui/fields/__init__.py b/invenio_records_marc21/resources/serializers/ui/fields/__init__.py new file mode 100755 index 00000000..ab6d1b77 --- /dev/null +++ b/invenio_records_marc21/resources/serializers/ui/fields/__init__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + + +"""Record UI response fields.""" + + +from .access import AccessField, UIAccessSchema + +__all__ = ( + "AccessField", + "UIAccessSchema", +) diff --git a/invenio_records_marc21/resources/serializers/ui/fields/access.py b/invenio_records_marc21/resources/serializers/ui/fields/access.py new file mode 100644 index 00000000..b2fad836 --- /dev/null +++ b/invenio_records_marc21/resources/serializers/ui/fields/access.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# +# Copyright (C) 2020 CERN. +# Copyright (C) 2020 Northwestern University. +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + +"""Access field for UI.""" + + +from babel_edtf import format_edtf +from flask_babelex import gettext as _ +from marshmallow import fields + +from .....records.systemfields.access import AccessStatusEnum + + +class UIAccessSchema: + """Access status properties to display in the UI.""" + + def __init__(self, access): + """Build access status object.""" + self.access = access + self.access_status = AccessStatusEnum(access.get("status")) + + @property + def id(self): + """Access status id.""" + return self.access_status.value + + @property + def title(self): + """Access status title.""" + return { + AccessStatusEnum.PUBLIC: _("Public"), + AccessStatusEnum.EMBARGOED: _("Embargoed"), + AccessStatusEnum.RESTRICTED: _("Restricted"), + }.get(self.access_status) + + @property + def icon(self): + """Access status icon.""" + return { + AccessStatusEnum.PUBLIC: "unlock", + AccessStatusEnum.EMBARGOED: "outline clock", + AccessStatusEnum.RESTRICTED: "ban", + }.get(self.access_status) + + @property + def embargo_date(self): + """Embargo date.""" + until = self.access.get("embargo").get("until") + if until: + return format_edtf(until, format="long") + return until + + @property + def message_class(self): + """UI message class name.""" + return { + AccessStatusEnum.PUBLIC: "", + AccessStatusEnum.EMBARGOED: "warning", + AccessStatusEnum.RESTRICTED: "negative", + }.get(self.access_status) + + +class AccessField(fields.Field): + """Record access status.""" + + def _serialize(self, value, attr, obj, **kwargs): + """Serialise access status.""" + record_access_dict = obj.get("access") + if record_access_dict: + record_access_ui = UIAccessSchema(record_access_dict) + return { + "id": record_access_ui.id, + "title": record_access_ui.title, + "icon": record_access_ui.icon, + "embargo_date": record_access_ui.embargo_date, + "message_class": record_access_ui.message_class, + } diff --git a/invenio_records_marc21/resources/serializers/ui/schema.py b/invenio_records_marc21/resources/serializers/ui/schema.py index bc8684b2..7209e0e3 100644 --- a/invenio_records_marc21/resources/serializers/ui/schema.py +++ b/invenio_records_marc21/resources/serializers/ui/schema.py @@ -1,100 +1,33 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + +"""Schema for marc21 ui records.""" -"""Schema ui.""" -from copy import deepcopy from functools import partial -from dojson.contrib.to_marc21 import to_marc21 -from dojson.contrib.to_marc21.utils import dumps from flask_babelex import get_locale -from marshmallow import INCLUDE, Schema, missing, pre_dump -from marshmallow.fields import Dict, Method, Nested, Str from marshmallow_utils.fields import FormatDate as BaseFormatDatetime +from marshmallow_utils.fields import SanitizedUnicode -FormatDatetime = partial(BaseFormatDatetime, locale=get_locale) - - -# -# Object schema -# -class AccessRightSchema(Schema): - """Access right vocabulary.""" - - -# -# Object schema -# -class MetadataSchema(Schema): - """Access right vocabulary.""" +from ..schema import Marc21Schema +from .fields import AccessField - xml = Str(required=False) - json = Dict(required=True) - - @pre_dump - def convert_xml(self, data, **kwargs): - """Convert json into marc21 xml.""" - if "json" in data: - data["xml"] = dumps(to_marc21.do(data["json"])) - return data +FormatDatetime = partial(BaseFormatDatetime, locale=get_locale) -class UIObjectSchema(Schema): +class Marc21UISchema(Marc21Schema): """Schema for dumping extra information for the UI.""" - metadata = Nested(MetadataSchema, attribute="metadata") - - access_right = Nested(AccessRightSchema, attribute="access") + id = SanitizedUnicode(data_key="id", attribute="id") + access = AccessField(attribute="access") created = FormatDatetime(attribute="created", format="long") updated = FormatDatetime(attribute="updated", format="long") - - -# -# List schema -class UIListSchema(Schema): - """Schema for dumping extra information in the UI.""" - - class Meta: - """.""" - - unknown = INCLUDE - - hits = Method("get_hits") - aggregations = Method("get_aggs") - - def get_hits(self, obj_list): - """Apply hits transformation.""" - for obj in obj_list["hits"]["hits"]: - obj[self.context["object_key"]] = self.context["object_schema_cls"]().dump( - obj - ) - return obj_list["hits"] - - def get_aggs(self, obj_list): - """Apply aggregations transformation.""" - aggs = obj_list.get("aggregations") - if not aggs: - return missing - - return aggs - - -def apply_labels(vocab, buckets): - """Inject labels in the aggregation buckets. - - :params agg: Current aggregation object. - :params vocab: The vocabulary - """ - for bucket in buckets: - bucket["label"] = vocab.get_title_by_dict(bucket["key"]) - - # Recursively apply to subbuckets - for data in bucket.values(): - if isinstance(data, dict) and "buckets" in data: - apply_labels(vocab, data["buckets"]) From 3f5d0ad5d28fac9cf331c8724b26a24e77cc0cad Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Thu, 2 Sep 2021 11:03:27 +0200 Subject: [PATCH 080/217] feature: marc21 record serializers adding marc21 xml record serializer convert record to xml format and json serializer which removes interal order keys. --- .../resources/serializers/__init__.py | 17 +- .../resources/serializers/errors.py | 20 ++ .../resources/serializers/serializer.py | 179 ++++++++++++++++++ .../resources/serializers/ui/serializers.py | 53 ++++++ 4 files changed, 266 insertions(+), 3 deletions(-) create mode 100644 invenio_records_marc21/resources/serializers/errors.py create mode 100644 invenio_records_marc21/resources/serializers/serializer.py create mode 100644 invenio_records_marc21/resources/serializers/ui/serializers.py diff --git a/invenio_records_marc21/resources/serializers/__init__.py b/invenio_records_marc21/resources/serializers/__init__.py index 5cada7bd..7c248209 100644 --- a/invenio_records_marc21/resources/serializers/__init__.py +++ b/invenio_records_marc21/resources/serializers/__init__.py @@ -1,12 +1,23 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """Record serializers.""" from __future__ import absolute_import, print_function -__all__ = () +from .errors import Marc21XMLConvertError +from .serializer import Marc21BASESerializer, Marc21JSONSerializer, Marc21XMLSerializer + +__all__ = ( + "Marc21XMLConvertError", + "Marc21BASESerializer", + "Marc21JSONSerializer", + "Marc21XMLSerializer", +) diff --git a/invenio_records_marc21/resources/serializers/errors.py b/invenio_records_marc21/resources/serializers/errors.py new file mode 100644 index 00000000..ab89f60d --- /dev/null +++ b/invenio_records_marc21/resources/serializers/errors.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + + +"""Errors for serializers.""" + + +class SerializerError(Exception): + """Base class for serializer errors.""" + + +class Marc21XMLConvertError(SerializerError): + """Error thrown when a marc21 xml could not be converted.""" diff --git a/invenio_records_marc21/resources/serializers/serializer.py b/invenio_records_marc21/resources/serializers/serializer.py new file mode 100644 index 00000000..1918ae36 --- /dev/null +++ b/invenio_records_marc21/resources/serializers/serializer.py @@ -0,0 +1,179 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + + +"""Marc21 record response serializers.""" + +import json + +from dojson._compat import iteritems, string_types +from dojson.contrib.to_marc21 import to_marc21 +from dojson.contrib.to_marc21.utils import MARC21_NS +from dojson.utils import GroupableOrderedDict +from flask_resources.serializers import MarshmallowJSONSerializer +from lxml import etree +from lxml.builder import E, ElementMaker + +from .schema import Marc21Schema + + +class Marc21BASESerializer(MarshmallowJSONSerializer): + """Marc21 Base serializer implementation.""" + + def __init__(self, schema_cls=Marc21Schema, **options): + """Marc21 Base Serializer Constructor. + + :param schema_cls: Default Marc21Schema + :param options: Json encoding options. + """ + super().__init__(schema_cls=schema_cls, **options) + + def dump_one(self, obj): + """Dump the object into a JSON string.""" + return self._schema_cls(context=self.ctx).dump(obj) + + def dump_many(self, obj_list): + """Serialize a list of records. + + :param obj_list: List of records instance. + """ + records = obj_list["hits"]["hits"] + obj_list["hits"]["hits"] = [self.dump_one(obj) for obj in records] + return obj_list + + +class Marc21JSONSerializer(Marc21BASESerializer): + """Marc21 JSON export serializer implementation.""" + + ctx = { + "remove_order": True, + } + + def serialize_object_list(self, obj_list): + """Serialize a list of records. + + :param records: List of records instance. + """ + obj_list = self.dump_many(obj_list) + return json.dumps(obj_list, cls=self.encoder, **self.dumps_options) + + +class Marc21XMLSerializer(Marc21BASESerializer): + """Marc21 XML export serializer implementation.""" + + ctx = { + "remove_order": False, + } + + def serialize_object(self, obj): + """Serialize a single record. + + :param record: Record instance. + """ + return self.convert_record(self.dump_one(obj)) + + def serialize_object_list(self, obj_list): + """Dump the object list into a JSON string.""" + list = [] + for obj in obj_list["hits"]["hits"]: + list.append(self.serialize_object(obj)) + + return "\n".join(list) + + def _convert(self, key, data): + root = etree.Element(key) + if isinstance(data, dict): + for k, v in data.items(): + root.append(self._convert(k, v)) + elif isinstance(data, (list, tuple)): + for item in data: + root.append(self._convert(key, item)) + else: + root.text = str(data) + return root + + def convert_record(self, data): + """Convert marc21 record to xml.""" + E = ElementMaker(namespace=MARC21_NS, nsmap={"prefix": MARC21_NS}) + record = E.record() + for key, value in data.items(): + if "metadata" in key: + record.append(self.convert_metadata(to_marc21.do(value["json"]))) + continue + record.append(self._convert(key, value)) + return etree.tostring( + record, + pretty_print=True, + xml_declaration=True, + encoding="UTF-8", + ).decode("UTF-8") + + def convert_metadata(self, data): + """Convert the metadata to Marc21 xml.""" + rec = E.metadata() + + leader = data.get("leader") + if leader: + rec.append(E.leader(leader)) + + if isinstance(data, GroupableOrderedDict): + items = data.iteritems(with_order=False, repeated=True) + else: + items = iteritems(data) + + for df, subfields in items: + # Control fields + if len(df) == 3: + if isinstance(subfields, string_types): + controlfield = E.controlfield(subfields) + controlfield.attrib["tag"] = df[0:3] + rec.append(controlfield) + elif isinstance(subfields, (list, tuple, set)): + for subfield in subfields: + controlfield = E.controlfield(subfield) + controlfield.attrib["tag"] = df[0:3] + rec.append(controlfield) + else: + # Skip leader. + if df == "leader": + continue + + if not isinstance(subfields, (list, tuple, set)): + subfields = (subfields,) + + df = df.replace("_", " ") + for subfield in subfields: + if not isinstance(subfield, (list, tuple, set)): + subfield = [subfield] + + for s in subfield: + datafield = E.datafield() + datafield.attrib["tag"] = df[0:3] + datafield.attrib["ind1"] = df[3] + datafield.attrib["ind2"] = df[4] + + if isinstance(s, GroupableOrderedDict): + items = s.iteritems(with_order=False, repeated=True) + elif isinstance(s, dict): + items = iteritems(s) + else: + datafield.append(E.subfield(s)) + + items = tuple() + + for code, value in items: + if not isinstance(value, string_types): + for v in value: + datafield.append(E.subfield(v, code=code)) + else: + datafield.append(E.subfield(value, code=code)) + + rec.append(datafield) + return rec diff --git a/invenio_records_marc21/resources/serializers/ui/serializers.py b/invenio_records_marc21/resources/serializers/ui/serializers.py new file mode 100644 index 00000000..02b43242 --- /dev/null +++ b/invenio_records_marc21/resources/serializers/ui/serializers.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + + +"""Marc21 UI record response serializers.""" + + +from copy import deepcopy + +from ..serializer import Marc21BASESerializer +from .schema import Marc21UISchema + + +class Marc21UIBASESerializer(Marc21BASESerializer): + """UI Base serializer implementation.""" + + def __init__(self, object_key="ui", **options): + """Marc21 UI Base Constructor. + + :param object_key: str key dump ui specific information + """ + super().__init__(schema_cls=Marc21UISchema, **options) + self._object_key = object_key + + def dump_one(self, obj): + """Dump the object into a JSON string.""" + obj[self._object_key] = self._schema_cls(context=self.ctx).dump(deepcopy(obj)) + return obj + + +class Marc21UIJSONSerializer(Marc21UIBASESerializer): + """UI JSON serializer implementation.""" + + ctx = { + "remove_order": True, + "marcxml": False, + } + + +class Marc21UIXMLSerializer(Marc21UIBASESerializer): + """UI Marc21 xml serializer implementation.""" + + ctx = { + "remove_order": False, + "marcxml": True, + } From 05b8e4b632f8911f44f6a7c755e2c4ce45a83fa3 Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Thu, 2 Sep 2021 11:06:14 +0200 Subject: [PATCH 081/217] modification: serializer config updating the record serializer config and add the Marc21XMLSerializer --- invenio_records_marc21/config.py | 27 ++++++++++++++++++---- invenio_records_marc21/resources/config.py | 25 ++++++++++---------- 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/invenio_records_marc21/config.py b/invenio_records_marc21/config.py index c1d0ee79..603223b9 100644 --- a/invenio_records_marc21/config.py +++ b/invenio_records_marc21/config.py @@ -1,9 +1,13 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + """Default configuration.""" @@ -18,21 +22,34 @@ "json": { "name": "JSON", "serializer": ( - "invenio_records_marc21.resources.serializers.ui:" "UIJSONSerializer" + "invenio_records_marc21.resources.serializers:" "Marc21JSONSerializer" + ), + }, + "marcxml": { + "name": "MARCXML", + "serializer": ( + "invenio_records_marc21.resources.serializers:" "Marc21XMLSerializer" ), - } + }, } +"""Marc21 Record export serializers.""" + +INVENIO_MARC21_RECORD_EXPORTER_OPTIONS = { + "indent": 4, + "sort_keys": True, +} +"""Marc21 Record export options.""" INVENIO_MARC21_UI_ENDPOINTS = { "record-detail": "/", "record-export": "//export/", } +"""Marc21 Record ui endpoints.""" INVENIO_MARC21_UI_THEME_ENDPOINTS = { "index": "/", "record-search": "/search", } - """Records UI for invenio-records-marc21.""" SEARCH_UI_JSTEMPLATE_RESULTS = "templates/invenio_records_marc21/results.html" diff --git a/invenio_records_marc21/resources/config.py b/invenio_records_marc21/resources/config.py index 2a50662a..2f2964c9 100755 --- a/invenio_records_marc21/resources/config.py +++ b/invenio_records_marc21/resources/config.py @@ -8,29 +8,28 @@ """Resources configuration.""" import marshmallow as ma -from flask_resources import ( - JSONDeserializer, - JSONSerializer, - RequestBodyParser, - ResponseHandler, -) -from flask_resources.serializers import JSONSerializer +from flask_resources import JSONDeserializer, RequestBodyParser, ResponseHandler from invenio_drafts_resources.resources import RecordResourceConfig from invenio_records_resources.resources.files import FileResourceConfig from invenio_records_resources.resources.records.args import SearchRequestArgsSchema -from .serializers.ui import UIJSONSerializer +from .serializers import Marc21JSONSerializer, Marc21XMLSerializer +from .serializers.ui import Marc21UIJSONSerializer, Marc21UIXMLSerializer record_serializer = { - "application/json": ResponseHandler(UIJSONSerializer()), - "application/vnd.inveniomarc21.v1+json": ResponseHandler(UIJSONSerializer()), + "application/json": ResponseHandler(Marc21JSONSerializer()), + "application/marcxml": ResponseHandler(Marc21XMLSerializer()), + "application/vnd.inveniomarc21.v1+json": ResponseHandler(Marc21UIJSONSerializer()), + "application/vnd.inveniomarc21.v1+marcxml": ResponseHandler( + Marc21UIXMLSerializer() + ), } url_prefix = "/marc21" record_ui_routes = { - "search": f"{url_prefix}", - "list": f"{url_prefix}/list", + "search": f"{url_prefix}/search", + "list": f"{url_prefix}", "item": f"{url_prefix}/", "item-versions": f"{url_prefix}//versions", "item-latest": f"{url_prefix}//versions/latest", @@ -107,4 +106,4 @@ class Marc21ParentRecordLinksResourceConfig(RecordResourceConfig): request_view_args = {"pid_value": ma.fields.Str(), "link_id": ma.fields.Str()} - response_handlers = {"application/json": ResponseHandler(JSONSerializer())} + response_handlers = {"application/json": ResponseHandler(Marc21JSONSerializer())} From c7d17e9b704f66b00069d861c26af15867756b70 Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Thu, 2 Sep 2021 11:09:35 +0200 Subject: [PATCH 082/217] modification: update record endpoints updating to the new record serializer structure and add exporter option configuration. --- invenio_records_marc21/ui/records/records.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/invenio_records_marc21/ui/records/records.py b/invenio_records_marc21/ui/records/records.py index 785cda7b..2ad32dec 100644 --- a/invenio_records_marc21/ui/records/records.py +++ b/invenio_records_marc21/ui/records/records.py @@ -11,7 +11,7 @@ from flask import abort, current_app, render_template from invenio_base.utils import obj_or_import_string -from ...resources.serializers.ui import UIJSONSerializer +from ...resources.serializers.ui import Marc21UIJSONSerializer from .decorators import pass_record, user_permissions @@ -25,7 +25,7 @@ def record_detail(record=None, files=None, pid_value=None, permissions=None): files_dict = None if files is None else files.to_dict() return render_template( "invenio_records_marc21/record.html", - record=UIJSONSerializer().serialize_object_to_dict(record.to_dict()), + record=Marc21UIJSONSerializer().dump_one(record.to_dict()), pid=pid_value, files=files_dict, permissions=permissions, @@ -41,18 +41,21 @@ def record_export(record=None, export_format=None, pid_value=None, permissions=N if exporter is None: abort(404) - serializer = obj_or_import_string(exporter["serializer"])( - options={ + options = current_app.config.get( + "INVENIO_MARC21_RECORD_EXPORTER_OPTIONS", + { "indent": 2, "sort_keys": True, - } + }, ) + + serializer = obj_or_import_string(exporter["serializer"])(options=options) exported_record = serializer.serialize_object(record.to_dict()) return render_template( "invenio_records_marc21/records/export.html", export_format=exporter.get("name", export_format), exported_record=exported_record, - record=UIJSONSerializer().serialize_object_to_dict(record.to_dict()), + record=Marc21UIJSONSerializer().dump_one(record.to_dict()), permissions=permissions, ) From ce758d448a11bc5b98e6ac0c1826f493653eaf1d Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Thu, 2 Sep 2021 11:16:50 +0200 Subject: [PATCH 083/217] modification: record pages using specific ui information from record serializer in search results and record landing page. Adding on the export page the record_sidebar. --- .../invenio_records_marc21/record.html | 21 ++++++++++++------ .../records/details/subjects.html | 2 +- .../records/export.html | 22 +++++++++---------- .../records/helpers/authors.html | 6 ++--- .../records/helpers/description.html | 6 ++--- .../records/helpers/details.html | 2 +- .../records/helpers/side_bar.html | 5 ++--- .../records/helpers/subjects.html | 2 +- .../records/macros/detail.html | 8 ++----- .../invenio_records_marc21/search.html | 2 +- .../search/components.js | 4 ++-- 11 files changed, 41 insertions(+), 39 deletions(-) diff --git a/invenio_records_marc21/templates/invenio_records_marc21/record.html b/invenio_records_marc21/templates/invenio_records_marc21/record.html index 07fef4cf..83efa429 100755 --- a/invenio_records_marc21/templates/invenio_records_marc21/record.html +++ b/invenio_records_marc21/templates/invenio_records_marc21/record.html @@ -13,7 +13,7 @@ {%- set metadata = record.metadata %} -{%- set marcrecord = record.metadata.json %} +{%- set marcrecord = record.ui.metadata.json %} {%- block page_body %} @@ -34,11 +34,18 @@ | Version {{ record.revision_id }} {% endif %}
-
-
-
- Access Rights - +
+
+
+
+ {{ record.ui.access.title }} +
+
{{ record.ui.access.description }}

+ {% if record.access.embargo.reason %} +

{{_("Reason")}}: {{record.access.embargo.reason}}

+ {% endif%} +
+
@@ -46,7 +53,7 @@ {%- endblock record_header -%} {%- block record_title -%} -

{{ record.metadata.json.title_statement.title | sanitize_title() }}

+

{{ marcrecord.title_statement.title | safe() | sanitize_title() }}

{%- endblock record_title -%} {%- block record_content -%}
diff --git a/invenio_records_marc21/templates/invenio_records_marc21/records/details/subjects.html b/invenio_records_marc21/templates/invenio_records_marc21/records/details/subjects.html index 089123e6..5026b51b 100644 --- a/invenio_records_marc21/templates/invenio_records_marc21/records/details/subjects.html +++ b/invenio_records_marc21/templates/invenio_records_marc21/records/details/subjects.html @@ -7,7 +7,7 @@ #} -{% set subjects = metadata.get('subject_added_entry_corporate_name') %} +{% set subjects = marcrecord.get('subject_added_entry_corporate_name') %} {% if subjects %} {{_('Subjects')}}
diff --git a/invenio_records_marc21/templates/invenio_records_marc21/records/export.html b/invenio_records_marc21/templates/invenio_records_marc21/records/export.html index 15de9d5f..d6cb8a61 100644 --- a/invenio_records_marc21/templates/invenio_records_marc21/records/export.html +++ b/invenio_records_marc21/templates/invenio_records_marc21/records/export.html @@ -28,7 +28,7 @@

{{ export_format }} Export

- + {% endblock record_header %} {% block record_content %} @@ -44,18 +44,18 @@

{{ export_format }} Export

{% block record_details %}{% endblock %} {% block record_footer %} -
- - + {% endblock %} + + {% block record_sidebar %} + {%- include "invenio_records_marc21/records/helpers/side_bar.html" %} + {% endblock record_sidebar %} diff --git a/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/authors.html b/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/authors.html index 4f0ce211..ac19363e 100644 --- a/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/authors.html +++ b/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/authors.html @@ -7,9 +7,9 @@ #} {%- from "invenio_records_marc21/records/macros/authors.html" import show_authors %} -{% set production = marcrecord.production_publication_distribution_manufacture_and_copyright_notice %} -{% if production and production[0]["name_of_producer_publisher_distributor_manufacturer"] %} -{% set authors = production[0]["name_of_producer_publisher_distributor_manufacturer"] %} +{% set production = marcrecord["production_publication_distribution_manufacture_and_copyright_notice"] %} +{% if production and production["name_of_producer_publisher_distributor_manufacturer"] %} +{% set authors = production["name_of_producer_publisher_distributor_manufacturer"] %}

{{_("Rights Holder(s)")}}

{{ show_authors(authors) }} {% endif %} \ No newline at end of file diff --git a/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/description.html b/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/description.html index 4aede51a..e8328161 100755 --- a/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/description.html +++ b/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/description.html @@ -7,10 +7,10 @@ #} -{% set desciption = metadata.json.get('summary') %} +{% set desciption = marcrecord.get('summary') %} {% if desciption %} -{% if desciption is iterable and value is not string %} -{% set desciption = desciption[0].get('summary') %} +{% if desciption is iterable and desciption is not string %} +{% set desciption = desciption.get('summary') %} {% endif %}
{{_('Desciption')}}
diff --git a/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/details.html b/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/details.html index 8d414807..081d5d16 100755 --- a/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/details.html +++ b/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/details.html @@ -8,7 +8,7 @@ {%- from "invenio_records_marc21/records/macros/detail.html" import show_subtitles, show_detail, show_publisher %} -{% set details = metadata.json %} +{% set details = marcrecord %}

{{ _('Details')}}


diff --git a/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/side_bar.html b/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/side_bar.html index 30ab49aa..536872c7 100644 --- a/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/side_bar.html +++ b/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/side_bar.html @@ -13,10 +13,9 @@

{{ _('Export')}}

{%- for fmt, val in config.get("INVENIO_MARC21_RECORD_EXPORTERS", {}).items() -%} {%- set name = val.get("name", fmt) -%} {%- set export_url = url_for('invenio_records_marc21.record_export', pid_value=record.id, export_format=fmt) -%} - {{ name }} - {%- endfor -%} + {{ name }}
+ {%- endfor -%}
-
diff --git a/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/subjects.html b/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/subjects.html index e344330a..b69bafef 100755 --- a/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/subjects.html +++ b/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/subjects.html @@ -7,7 +7,7 @@ #} -{% set subjects = metadata.json.get('subject_added_entry_topical_term') %} +{% set subjects = marcrecord.get('subject_added_entry_topical_term') %} {% if subjects %}
{{_('Subjects')}}
diff --git a/invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html b/invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html index f95918a5..4d92b486 100755 --- a/invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html +++ b/invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html @@ -56,8 +56,7 @@ {% macro show_publisher(publishers) %} - {% for publisher in publishers %} - {% for key, value in publisher.items() %} + {% for key, value in publishers.items() %} {% if key == "sequence_of_statements" %} {% set key = "Sequence" %} {% elif key == "name_of_producer_publisher_distributor_manufacturer"%} @@ -69,13 +68,11 @@ {% endif %} {{ show_detail(key, value) }} {% endfor %} - {% endfor %} {%- endmacro %} {% macro show_pages(pages) %} - {% for publisher in publishers %} - {% for key, value in publisher.items() %} + {% for key, value in publishers.items() %} {% if key == "sequence_of_statements" %} {% set key = "Sequence" %} {% elif key == "name_of_producer_publisher_distributor_manufacturer"%} @@ -85,5 +82,4 @@ {% endif %} {{ show_detail(key, value) }} {% endfor %} - {% endfor %} {%- endmacro %} \ No newline at end of file diff --git a/invenio_records_marc21/templates/invenio_records_marc21/search.html b/invenio_records_marc21/templates/invenio_records_marc21/search.html index 385579fd..af8a97a4 100755 --- a/invenio_records_marc21/templates/invenio_records_marc21/search.html +++ b/invenio_records_marc21/templates/invenio_records_marc21/search.html @@ -56,7 +56,7 @@ "headers": { "Accept": "application/vnd.inveniomarc21.v1+json" }, - "url": "/api/marc21/list", + "url": "/api/marc21", "withCredentials": true } }, diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/components.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/components.js index ae415102..fa449551 100755 --- a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/components.js +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/components.js @@ -39,10 +39,10 @@ export const Marc21RecordResultsListItem = ({ result, index }) => { "No update date found." ); const metadata = _get(result, ["ui", "metadata", "json"], []); - const description = _get(metadata, ["summary", "0", "summary"], "No description"); + const description = _get(metadata, ["summary","summary"], "No description"); const subjects = _get(metadata, "subject_added_entry_topical_term", []); - const publication = _get(metadata, ["production_publication_distribution_manufacture_and_copyright_notice", "0"], []); + const publication = _get(metadata, ["production_publication_distribution_manufacture_and_copyright_notice"], []); const creators = _get(publication, "name_of_producer_publisher_distributor_manufacturer", []); const title = _get(metadata, ["title_statement", "title"], "No title"); From 321c26a161f06741b7a28d86be35b64bb8fb0c92 Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Thu, 2 Sep 2021 11:18:10 +0200 Subject: [PATCH 084/217] modification: add links to record adding record files link and access links to record serialize --- invenio_records_marc21/services/config.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/invenio_records_marc21/services/config.py b/invenio_records_marc21/services/config.py index 938c4356..8e92fc02 100644 --- a/invenio_records_marc21/services/config.py +++ b/invenio_records_marc21/services/config.py @@ -105,6 +105,12 @@ class Marc21RecordServiceConfig(RecordServiceConfig): if_=RecordLink("{+ui}/marc21/{id}"), else_=RecordLink("{+ui}/uploads/{id}"), ), + "files": ConditionalLink( + cond=is_record, + if_=RecordLink("{+api}/marc21/{id}/files"), + else_=RecordLink("{+api}/marc21/{id}/draft/files"), + ), + "access_links": RecordLink("{+api}/marc21/{id}/access/links"), } From fa2b2a3cae347bfad572abc2f5570cfa05d2e6e7 Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Thu, 2 Sep 2021 11:20:53 +0200 Subject: [PATCH 085/217] modification: doc string class name --- invenio_records_marc21/services/services.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/invenio_records_marc21/services/services.py b/invenio_records_marc21/services/services.py index c1f02ce9..4f3bd81b 100644 --- a/invenio_records_marc21/services/services.py +++ b/invenio_records_marc21/services/services.py @@ -58,7 +58,7 @@ def create( :param identity: Identity of user creating the record. :param dict data: Input data according to the data schema. - :param Metadata metadata: Input data according to the metadata schema. + :param Marc21Metadata metadata: Input data according to the metadata schema. :param links_config: Links configuration. :param dict access: provide access additional information """ @@ -78,7 +78,7 @@ def update_draft( :param identity: Identity of user creating the record. :param dict data: Input data according to the data schema. - :param Metadata metadata: Input data according to the metadata schema. + :param Marc21Metadata metadata: Input data according to the metadata schema. :param links_config: Links configuration. :param dict access: provide access additional information """ From dfdc50c2ba11a05fcb41ade72a7570010f5bb765 Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Thu, 2 Sep 2021 11:29:17 +0200 Subject: [PATCH 086/217] test(serializer): test-set adding test-set for serializer and schema --- tests/resources/serializers/conftest.py | 183 ++++++++++++++++++ .../resources/serializers/test_serializer.py | 81 ++++++++ .../serializers/ui/fields/conftest.py | 16 ++ .../serializers/ui/fields/test_ui_metadata.py | 107 ++++++++++ .../serializers/ui/test_serializers.py | 94 +++++++++ tests/services/conftest.py | 14 ++ tests/services/record/test_marc21_metadata.py | 11 +- 7 files changed, 496 insertions(+), 10 deletions(-) create mode 100755 tests/resources/serializers/conftest.py create mode 100644 tests/resources/serializers/test_serializer.py create mode 100755 tests/resources/serializers/ui/fields/conftest.py create mode 100644 tests/resources/serializers/ui/fields/test_ui_metadata.py create mode 100644 tests/resources/serializers/ui/test_serializers.py diff --git a/tests/resources/serializers/conftest.py b/tests/resources/serializers/conftest.py new file mode 100755 index 00000000..231fa07a --- /dev/null +++ b/tests/resources/serializers/conftest.py @@ -0,0 +1,183 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + + +"""Pytest configuration. + +See https://pytest-invenio.readthedocs.io/ for documentation on which test +fixtures are available. +""" + + +import pytest + + +@pytest.fixture(scope="function") +def marc21_metadata(): + """Record UI metadata.""" + return { + "json": { + "leader": { + "undefined": 0, + "record_length": 0, + "record_status": "new", + "encoding_level": "not_applicable", + "type_of_record": "language_material", + "indicator_count": 2, + "bibliographic_level": "monograph_item", + "subfield_code_count": 2, + "base_address_of_data": 0, + "character_coding_scheme": "ucs_unicode", + "descriptive_cataloging_form": "isbd_punctuation_omitteed", + "multipart_resource_record_level": "set", + "length_of_the_length_of_field_portion": 4, + "length_of_the_implementation_defined_portion": 0, + "length_of_the_starting_character_position_portion": 5, + }, + "summary": [ + { + "summary": "A wonderful serenity has taken possession of my entire soul, like these sweet mornings of spring which I enjoy with my whole heart. I am alone, and feel the charm of existence in this spot, which was created for the bliss of souls like mine. I am so happy, my dear friend, so absorbed in the exquisite sense of mere tranquil existence, that I neglect my talents. I should be incapable of drawing a single stroke at the present moment; and yet I feel that I never was a greater artist than now. When, while the lovely valley teems with vapour around me, and the meridian sun strikes the upper surface of the impenetrable foliage of my trees, and but a few stray gleams steal into the inner sanctuary, I throw myself down among the tall grass by the trickling stream; and, as I lie close to the earth, a thousand unknown plants are noticed by me: when I hear the buzz of the little world among the stalks, and grow familiar with the countless indescribable forms of the insects and flies, then I feel the presence of the Almighty, who formed us in his own image, and the breath ", + "__order__": ["summary", "display_constant_controller"], + "display_constant_controller": "Summary", + } + ], + "__order__": [ + "leader", + "title_statement", + "subject_added_entry_topical_term", + "subject_added_entry_topical_term", + "subject_added_entry_topical_term", + "subject_added_entry_topical_term", + "summary", + "production_publication_distribution_manufacture_and_copyright_notice", + "main_entry_personal_name", + ], + "title_statement": { + "title": "Proceedings of the 3rd International Brain-Computer Interface Workshop and Training Course", + "__order__": [ + "title", + "remainder_of_title", + "statement_of_responsibility", + "title_added_entry", + "nonfiling_characters", + ], + "title_added_entry": "No added entry", + "remainder_of_title": "Subtitle field.", + "nonfiling_characters": "0", + "statement_of_responsibility": "hrsg. von Josef Frank", + }, + "main_entry_personal_name": { + "__order__": ["affiliation", "type_of_personal_name_entry_element"], + "affiliation": "Institute of Solid State Physics (5130)", + "type_of_personal_name_entry_element": "Surname", + }, + "subject_added_entry_topical_term": [ + { + "__order__": [ + "miscellaneous_information", + "level_of_subject", + "thesaurus", + ], + "thesaurus": "Source not specified", + "level_of_subject": "No information provided", + "miscellaneous_information": ["Test"], + }, + { + "__order__": [ + "miscellaneous_information", + "level_of_subject", + "thesaurus", + ], + "thesaurus": "Source not specified", + "level_of_subject": "No information provided", + "miscellaneous_information": ["Invenio"], + }, + { + "__order__": [ + "miscellaneous_information", + "level_of_subject", + "thesaurus", + ], + "thesaurus": "Source not specified", + "level_of_subject": "No information provided", + "miscellaneous_information": ["TUGraz"], + }, + { + "__order__": [ + "topical_term_or_geographic_name_entry_element", + "level_of_subject", + "thesaurus", + ], + "thesaurus": "Source not specified", + "level_of_subject": "No information provided", + "topical_term_or_geographic_name_entry_element": "Marc21", + }, + ], + "production_publication_distribution_manufacture_and_copyright_notice": [ + { + "__order__": [ + "place_of_production_publication_distribution_manufacture", + "name_of_producer_publisher_distributor_manufacturer", + "name_of_producer_publisher_distributor_manufacturer", + "name_of_producer_publisher_distributor_manufacturer", + "name_of_producer_publisher_distributor_manufacturer", + "date_of_production_publication_distribution_manufacture_or_copyright_notice", + "sequence_of_statements", + ], + "sequence_of_statements": "Not applicable/No information provided/Earliest", + "name_of_producer_publisher_distributor_manufacturer": [ + "Hulk", + "Thor", + "Captain", + "Black Widow", + ], + "place_of_production_publication_distribution_manufacture": [ + "Tu Graz" + ], + "date_of_production_publication_distribution_manufacture_or_copyright_notice": [ + "2004" + ], + } + ], + } + } + + +@pytest.fixture(scope="function") +def full_record(marc21_record, marc21_metadata): + """Full record as is expected by the UI serializer.""" + marc21_record + marc21_record["id"] = "9jkx5-hx115" + marc21_record["pid"] = { + "pk": 58, + "status": "R", + "obj_type": "rec", + "pid_type": "marcid", + } + + marc21_record["files"] = {"enabled": True} + marc21_record["access"] = { + "files": "restricted", + "embargo": {"until": None, "active": False, "reason": None}, + "metadata": "restricted", + "status": "restricted", + } + marc21_record["metadata"] = marc21_metadata + + return marc21_record + + +@pytest.fixture(scope="function") +def list_records(full_record): + """Fixture list of records.""" + list_records = { + "hits": {"hits": [full_record, full_record]}, + } + return list_records diff --git a/tests/resources/serializers/test_serializer.py b/tests/resources/serializers/test_serializer.py new file mode 100644 index 00000000..f865b6ee --- /dev/null +++ b/tests/resources/serializers/test_serializer.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it +# under the terms of the MIT License; see LICENSE file for more details. + + +"""Tests for Resources marc21 serializer.""" + +import json + +from invenio_records_marc21.resources.serializers import ( + Marc21JSONSerializer, + Marc21XMLSerializer, +) +from invenio_records_marc21.resources.serializers.schema import Marc21Schema + + +def _test_key_in_serialized_obj(obj, keys, exept_keys): + for key in keys: + assert key in obj + for no_key in exept_keys: + assert no_key not in obj + + +def test_marcxml_serializer_init(): + marc = Marc21XMLSerializer() + assert marc._schema_cls == Marc21Schema + + +def test_marcxml_serializer_serialize_object(full_record): + marc = Marc21XMLSerializer() + test_keys = ["metadata", "access", "id", "files"] + exept_keys = ["pid"] + obj = marc.serialize_object(full_record) + assert isinstance(obj, str) + _test_key_in_serialized_obj(obj, test_keys, exept_keys) + assert obj.startswith("") + assert 'xmlns:prefix="http://www.loc.gov/MARC21/slim"' in obj + + +def test_marcxml_serializer_serialize_object_list(list_records): + test_keys = ["metadata", "access", "id", "files"] + exept_keys = ["pid"] + marc = Marc21XMLSerializer() + obj_list = marc.serialize_object_list(list_records) + assert isinstance(obj_list, str) + test_obj = obj_list.split(":record>\n\n") + for obj in test_obj: + assert isinstance(obj, str) + _test_key_in_serialized_obj(obj, test_keys, exept_keys) + assert obj.startswith("") + assert 'xmlns:prefix="http://www.loc.gov/MARC21/slim"' in obj + + +def test_json_serializer_init(): + marc = Marc21JSONSerializer() + assert marc._schema_cls == Marc21Schema + + +def test_json_serializer_serialize_object(full_record): + test_keys = ["metadata", "access", "id", "files"] + exept_keys = ["pid"] + marc = Marc21JSONSerializer() + obj = marc.serialize_object(full_record) + assert isinstance(obj, str) + _test_key_in_serialized_obj(obj, test_keys, exept_keys) + + +def test_json_serializer_serialize_object_list(list_records): + test_keys = ["metadata", "access", "id", "files"] + exept_keys = ["pid"] + marc = Marc21JSONSerializer() + obj_list = marc.serialize_object_list(list_records) + assert isinstance(obj_list, str) + test_obj = json.loads(obj_list) + for obj in test_obj["hits"]["hits"]: + _test_key_in_serialized_obj(obj, test_keys, exept_keys) diff --git a/tests/resources/serializers/ui/fields/conftest.py b/tests/resources/serializers/ui/fields/conftest.py new file mode 100755 index 00000000..bfa82f8d --- /dev/null +++ b/tests/resources/serializers/ui/fields/conftest.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + + +"""Pytest configuration. + +See https://pytest-invenio.readthedocs.io/ for documentation on which test +fixtures are available. +""" diff --git a/tests/resources/serializers/ui/fields/test_ui_metadata.py b/tests/resources/serializers/ui/fields/test_ui_metadata.py new file mode 100644 index 00000000..43e0d902 --- /dev/null +++ b/tests/resources/serializers/ui/fields/test_ui_metadata.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + + +"""Test for record ui MetadataSchema.""" + +from copy import deepcopy + +from dojson.contrib.marc21 import marc21 +from dojson.contrib.marc21.utils import create_record +from dojson.contrib.to_marc21 import to_marc21 +from dojson.contrib.to_marc21.utils import dumps + +from invenio_records_marc21.resources.serializers.errors import Marc21XMLConvertError +from invenio_records_marc21.resources.serializers.fields import MetadataSchema + + +def _test_metadata(test, expected, exept=["__order__"]): + # assert test.keys() == expected.keys() + for key in test.keys(): + if key not in exept: + assert test[key] == expected[key] + + +def _test_without_order(data, key="__order__"): + """Test no Key with the name in dict.""" + if isinstance(data, dict): + for k, v in data.items(): + assert not key == k + _test_without_order(v, key) + elif isinstance(data, (list, tuple)): + for item in data: + _test_without_order(item, key) + + +def test_ui_metadata_remove_order(marc21_metadata): + metadata = MetadataSchema(context={"remove_order": True}) + data = metadata.dump(marc21_metadata) + _test_without_order(data) + + +def test_ui_metadata_convert_xml(marc21_metadata): + metadata = MetadataSchema(context={"marcxml": True}) + test = deepcopy(marc21_metadata) + + data = metadata.dump(test) + assert "xml" in data + assert "json" not in data + assert isinstance(data["xml"], str) + + expect_str = dumps(to_marc21.do(marc21_metadata["json"])).decode("UTF-8") + assert expect_str == data["xml"] + + +def test_ui_metadata_default_schema(marc21_metadata): + metadata = MetadataSchema() + data = metadata.dump(marc21_metadata) + assert data == marc21_metadata + _test_metadata( + data["json"], + marc21_metadata["json"], + ) + assert "xml" not in data + + +def test_ui_metadata_xml_schema(marc21_metadata): + """Test metadata schema.""" + + metadata = MetadataSchema( + context={ + "marcxml": True, + } + ) + test = deepcopy(marc21_metadata) + data = metadata.dump(test) + assert "json" not in data + assert "xml" in data + + s = "".join(data["xml"].split("\n")[1:-1]) + test = marc21.do(create_record(s)) + _test_without_order( + test, + marc21_metadata["json"], + ) + + +def test_ui_metadata_json_schema(marc21_metadata): + metadata = MetadataSchema( + context={ + "marcxml": False, + } + ) + test = deepcopy(marc21_metadata) + data = metadata.dump(test) + assert "xml" not in data + assert "json" in data + _test_metadata( + data["json"], + marc21_metadata["json"], + ) diff --git a/tests/resources/serializers/ui/test_serializers.py b/tests/resources/serializers/ui/test_serializers.py new file mode 100644 index 00000000..1175e8fd --- /dev/null +++ b/tests/resources/serializers/ui/test_serializers.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it +# under the terms of the MIT License; see LICENSE file for more details. + + +"""Tests for Resources UI marc21 serializer.""" + + +from invenio_records_marc21.resources.serializers.ui import ( + Marc21UIJSONSerializer, + Marc21UIXMLSerializer, +) +from invenio_records_marc21.resources.serializers.ui.schema import Marc21UISchema + + +def test_ui_marcxml_serializer_init(): + marc = Marc21UIXMLSerializer() + assert marc._object_key == "ui" + assert marc._schema_cls == Marc21UISchema + + +def test_ui_marcxml_serializer_dump_one(full_record): + marc = Marc21UIXMLSerializer() + obj = marc.dump_one(full_record) + assert "json" in obj["metadata"] + assert full_record["metadata"]["json"] == obj["metadata"]["json"] + assert len(obj["metadata"]) == 1 + + assert marc._object_key in obj + obj_ui = obj[marc._object_key] + assert "metadata" in obj_ui + assert len(obj_ui["metadata"]) == 1 + assert "xml" in obj_ui["metadata"] + + +def test_ui_marcxml_serializer_dump_many(list_records): + marc = Marc21UIXMLSerializer() + obj_list = marc.dump_many(list_records) + for record, obj in zip(obj_list["hits"]["hits"], list_records["hits"]["hits"]): + assert marc._object_key in obj + + assert "metadata" in obj + assert record["metadata"]["json"] == obj["metadata"]["json"] + assert len(obj["metadata"]) == 1 + + obj_ui = obj[marc._object_key] + assert "metadata" in obj_ui + assert len(obj_ui["metadata"]) == 1 + assert "xml" in obj_ui["metadata"] + assert "json" not in obj_ui["metadata"] + + +def test_ui_json_serializer_init(): + marc = Marc21UIJSONSerializer() + assert marc._object_key == "ui" + assert marc._schema_cls == Marc21UISchema + + +def test_ui_json_serializer_dump_one(full_record): + marc = Marc21UIJSONSerializer() + obj = marc.dump_one(full_record) + + assert "json" in obj["metadata"] + assert full_record["metadata"]["json"] == obj["metadata"]["json"] + assert len(obj["metadata"]) == 1 + + assert marc._object_key in obj + obj_ui = obj[marc._object_key] + assert "metadata" in obj_ui + assert len(obj_ui["metadata"]) == 1 + assert "xml" not in obj_ui["metadata"] + assert "json" in obj_ui["metadata"] + + +def test_ui_json_serializer_dump_many(list_records): + marc = Marc21UIJSONSerializer() + obj_list = marc.dump_many(list_records) + for record, obj in zip(obj_list["hits"]["hits"], list_records["hits"]["hits"]): + assert marc._object_key in obj + + assert "metadata" in obj + assert record["metadata"]["json"] == obj["metadata"]["json"] + assert len(obj["metadata"]) == 1 + + obj_ui = obj[marc._object_key] + assert "metadata" in obj_ui + assert len(obj_ui["metadata"]) == 1 + assert "xml" not in obj_ui["metadata"] + assert "json" in obj_ui["metadata"] diff --git a/tests/services/conftest.py b/tests/services/conftest.py index 48ab0b2e..f3e51954 100755 --- a/tests/services/conftest.py +++ b/tests/services/conftest.py @@ -14,6 +14,7 @@ See https://pytest-invenio.readthedocs.io/ for documentation on which test fixtures are available. """ +from collections import namedtuple import pytest from flask_principal import Identity @@ -34,6 +35,19 @@ def create_app(instance_path): return create_api +RunningApp = namedtuple("RunningApp", ["app", "service", "identity_simple"]) + + +@pytest.fixture() +def running_app(app, service, identity_simple): + """This fixture provides an app with the typically needed db data loaded. + + All of these fixtures are often needed together, so collecting them + under a semantic umbrella makes sense. + """ + return RunningApp(app, service, identity_simple) + + @pytest.fixture() def identity_simple(): """Simple identity fixture.""" diff --git a/tests/services/record/test_marc21_metadata.py b/tests/services/record/test_marc21_metadata.py index fc5eb276..fef5b914 100644 --- a/tests/services/record/test_marc21_metadata.py +++ b/tests/services/record/test_marc21_metadata.py @@ -7,18 +7,9 @@ # details. -"""Pytest configuration. - -See https://pytest-invenio.readthedocs.io/ for documentation on which test -fixtures are available. -""" - -from io import StringIO -from os import linesep -from os.path import dirname, join +"""Tests for record MetadataSchema.""" import pytest -from lxml import etree from invenio_records_marc21.services.record import Marc21Metadata from invenio_records_marc21.services.record.fields import ( From 99f0a7b848cf1a11c04681e4f55d52e888fd1850 Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Thu, 2 Sep 2021 14:08:51 +0200 Subject: [PATCH 087/217] modification: access status to search result adding access label to search result item --- .../js/invenio_records_marc21/search/components.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/components.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/components.js index fa449551..6fc29b58 100755 --- a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/components.js +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/components.js @@ -38,6 +38,11 @@ export const Marc21RecordResultsListItem = ({ result, index }) => { "ui.updated", "No update date found." ); + const access = _get(result, ["ui", "access"], []); + const access_id = _get(access, "id", "public"); + const access_status = _get(access, "title", "Public"); + const access_icon = _get(access, "icon", "unlock"); + const metadata = _get(result, ["ui", "metadata", "json"], []); const description = _get(metadata, ["summary","summary"], "No description"); const subjects = _get(metadata, "subject_added_entry_topical_term", []); @@ -58,6 +63,12 @@ export const Marc21RecordResultsListItem = ({ result, index }) => { +
diff --git a/invenio_records_marc21/ui/records/records.py b/invenio_records_marc21/ui/records/records.py index d79e5fe1..cb0768a5 100644 --- a/invenio_records_marc21/ui/records/records.py +++ b/invenio_records_marc21/ui/records/records.py @@ -54,6 +54,7 @@ def record_export(record=None, export_format=None, pid_value=None, permissions=N return render_template( "invenio_records_marc21/records/export.html", + pid_value=pid_value, export_format=exporter.get("name", export_format), exported_record=exported_record, record=Marc21UIJSONSerializer().dump_one(record.to_dict()), From 3b731b2d30e532436e6affad25296eac2eea13c5 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Wed, 15 Sep 2021 10:30:42 +0200 Subject: [PATCH 097/217] modification: metadata field creating a custom field for marc21 metadata to convert from marcxml to a json dict and store in the database and using it in the Marc21RecordSchema. --- .../services/schemas/__init__.py | 4 +- .../services/schemas/metadata.py | 66 +++++++++++-------- 2 files changed, 42 insertions(+), 28 deletions(-) diff --git a/invenio_records_marc21/services/schemas/__init__.py b/invenio_records_marc21/services/schemas/__init__.py index 7e3f286f..b46ed90b 100644 --- a/invenio_records_marc21/services/schemas/__init__.py +++ b/invenio_records_marc21/services/schemas/__init__.py @@ -14,7 +14,7 @@ from .access import AccessSchema, ParentAccessSchema from .files import FilesSchema -from .metadata import MetadataSchema +from .metadata import MetadataField from .pids import PIDSchema from .versions import VersionsSchema @@ -34,7 +34,7 @@ class Marc21RecordSchema(BaseRecordSchema): parent = NestedAttribute(Marc21ParentSchema, dump_only=True) - metadata = NestedAttribute(MetadataSchema) + metadata = MetadataField(attribute="metadata") access = NestedAttribute(AccessSchema) files = NestedAttribute(FilesSchema, dump_only=True) diff --git a/invenio_records_marc21/services/schemas/metadata.py b/invenio_records_marc21/services/schemas/metadata.py index de23b90c..5bce1830 100644 --- a/invenio_records_marc21/services/schemas/metadata.py +++ b/invenio_records_marc21/services/schemas/metadata.py @@ -5,36 +5,50 @@ # Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it # under the terms of the MIT License; see LICENSE file for more details. -"""Marc21 record schemas.""" +"""Marc21 record metadata field.""" + + +import typing from dojson.contrib.marc21 import marc21 from dojson.contrib.marc21.utils import create_record -from marshmallow import INCLUDE, Schema, fields, post_load +from marshmallow.fields import Field -class MetadataSchema(Schema): +class MetadataField(Field): """Schema for the record metadata.""" - field_load_permissions = { - # TODO: define "can_admin" action - } - - field_dump_permissions = { - # TODO: define "can_admin" action - } - - xml = fields.Str(required=False) - json = fields.Dict(required=False) - - class Meta: - """Meta class to accept unknwon fields.""" - - unknown = INCLUDE - - @post_load - def convert_xml(self, data, **kwargs): - """Convert marc21 xml into json.""" - if "xml" in data: - data["json"] = marc21.do(create_record(data["xml"])) - del data["xml"] - return data + def _deserialize( + self, + value: typing.Any, + attr: typing.Optional[str], + data: typing.Optional[typing.Mapping[str, typing.Any]], + **kwargs + ): + """Deserialize value. Concrete :class:`Field` classes should implement this method. + + :param value: The value to be deserialized. + :param attr: The attribute/key in `data` to be deserialized. + :param data: The raw input data passed to the `Schema.load`. + :param kwargs: Field-specific keyword arguments. + :raise ValidationError: In case of formatting or validation failure. + :return: The deserialized value. + + .. versionchanged:: 2.0.0 + Added ``attr`` and ``data`` parameters. + + .. versionchanged:: 3.0.0 + Added ``**kwargs`` to signature. + """ + if "xml" in value: + value = marc21.do(create_record(value["xml"])) + return value + + def _validate(self, value): + """Perform validation on ``value``. + + Raise a :exc:`ValidationError` if validation + does not succeed. + """ + # TODO: validate the marc21 xml during loading the Schema + self._validate_all(value) From 6109f09ae18270c4062d628eab3130395ed0cd4f Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Wed, 15 Sep 2021 10:32:20 +0200 Subject: [PATCH 098/217] modification: codestyle importing specific mashmallow.fields which are used in the Marc21RecordSchema. --- .../services/schemas/__init__.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/invenio_records_marc21/services/schemas/__init__.py b/invenio_records_marc21/services/schemas/__init__.py index b46ed90b..c8908000 100644 --- a/invenio_records_marc21/services/schemas/__init__.py +++ b/invenio_records_marc21/services/schemas/__init__.py @@ -9,7 +9,8 @@ from invenio_drafts_resources.services.records.schema import ParentSchema from invenio_records_resources.services.records.schema import BaseRecordSchema -from marshmallow import EXCLUDE, INCLUDE, Schema, fields, missing, post_dump +from marshmallow.decorators import post_dump +from marshmallow.fields import Boolean, Integer, List, Nested, Str from marshmallow_utils.fields import NestedAttribute from .access import AccessSchema, ParentAccessSchema @@ -22,15 +23,15 @@ class Marc21ParentSchema(ParentSchema): """Record schema.""" - access = fields.Nested(ParentAccessSchema) + access = Nested(ParentAccessSchema) class Marc21RecordSchema(BaseRecordSchema): """Record schema.""" - id = fields.Str() + id = Str() # pid - pids = fields.List(NestedAttribute(PIDSchema)) + pids = List(NestedAttribute(PIDSchema)) parent = NestedAttribute(Marc21ParentSchema, dump_only=True) @@ -38,13 +39,13 @@ class Marc21RecordSchema(BaseRecordSchema): access = NestedAttribute(AccessSchema) files = NestedAttribute(FilesSchema, dump_only=True) - created = fields.Str(dump_only=True) - updated = fields.Str(dump_only=True) - revision = fields.Integer(dump_only=True) + created = Str(dump_only=True) + updated = Str(dump_only=True) + revision = Integer(dump_only=True) versions = NestedAttribute(VersionsSchema, dump_only=True) - is_published = fields.Boolean(dump_only=True) + is_published = Boolean(dump_only=True) # Add version to record schema # versions = NestedAttribute(VersionsSchema, dump_only=True) From f9148e1cfdc689b4c13fb6fa3764264efa49260b Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Wed, 15 Sep 2021 10:35:40 +0200 Subject: [PATCH 099/217] modification: metadata field in ui creating a custom field for marc21 ui metadata to convert from json metadata to a xml string or a json dict without the order keys. This custom field is used to for the landing page and the record exporter. --- .../resources/serializers/fields/__init__.py | 4 +-- .../resources/serializers/fields/metadata.py | 32 +++++++++++-------- .../resources/serializers/schema.py | 5 ++- .../resources/serializers/serializer.py | 2 +- 4 files changed, 24 insertions(+), 19 deletions(-) diff --git a/invenio_records_marc21/resources/serializers/fields/__init__.py b/invenio_records_marc21/resources/serializers/fields/__init__.py index 1299a084..a79264e6 100644 --- a/invenio_records_marc21/resources/serializers/fields/__init__.py +++ b/invenio_records_marc21/resources/serializers/fields/__init__.py @@ -11,6 +11,6 @@ """Record response fields.""" -from .metadata import MetadataSchema +from .metadata import MetadataField -__all__ = ("MetadataSchema",) +__all__ = ("MetadataField",) diff --git a/invenio_records_marc21/resources/serializers/fields/metadata.py b/invenio_records_marc21/resources/serializers/fields/metadata.py index eb747f6c..7c1949c8 100644 --- a/invenio_records_marc21/resources/serializers/fields/metadata.py +++ b/invenio_records_marc21/resources/serializers/fields/metadata.py @@ -10,22 +10,20 @@ """Metadata field for marc21 records.""" +import json from dojson.contrib.to_marc21 import to_marc21 from dojson.contrib.to_marc21.utils import dumps from dojson.utils import GroupableOrderedDict from marshmallow import Schema from marshmallow.decorators import pre_dump -from marshmallow.fields import Dict, Str +from marshmallow.fields import Dict, Field, Nested, Str from ..errors import Marc21XMLConvertError -class MetadataSchema(Schema): - """Metadata schema.""" - - xml = Str(required=False) - json = Dict(required=True) +class MetadataField(Field): + """Schema for the record metadata.""" def _remove_order(self, data): """Removing order key in marc21 dict.""" @@ -41,23 +39,31 @@ def _remove_order(self, data): return [self._remove_order(item) for item in data] return data - @pre_dump def remove_order_key(self, data, **kwargs): """Remove order key in metadata dict .""" remove_order = self.context.get("remove_order", False) - if "json" in data and remove_order: - data["json"] = self._remove_order(GroupableOrderedDict(data["json"])) + if data and remove_order: + data = self._remove_order(GroupableOrderedDict(data)) return data - @pre_dump def convert_xml(self, data, **kwargs): """Convert json into marc21 xml.""" marcxml = self.context.get("marcxml", False) - if "json" in data and marcxml: + if data and marcxml: try: - data["xml"] = dumps(to_marc21.do(data["json"])) + data = dumps(to_marc21.do(data)) # .decode("UTF-8") except Exception as e: raise Marc21XMLConvertError(e) - del data["json"] return data + + def _serialize(self, value, attr, obj, **kwargs): + """Serialise access status.""" + if value: + record_metadata = value + + record_metadata = self.remove_order_key(record_metadata) + record_metadata = self.convert_xml(record_metadata) + + return record_metadata + return {} diff --git a/invenio_records_marc21/resources/serializers/schema.py b/invenio_records_marc21/resources/serializers/schema.py index 645b7ae7..8b5d5859 100644 --- a/invenio_records_marc21/resources/serializers/schema.py +++ b/invenio_records_marc21/resources/serializers/schema.py @@ -13,17 +13,16 @@ from functools import partial from marshmallow import Schema -from marshmallow.fields import Nested from marshmallow_utils.fields import SanitizedUnicode -from .fields import MetadataSchema +from .fields import MetadataField class Marc21Schema(Schema): """Schema for dumping extra information for the marc21 record.""" id = SanitizedUnicode(data_key="id", attribute="id") - metadata = Nested(MetadataSchema, attribute="metadata") + metadata = MetadataField(attribute="metadata") class Meta: """Meta class to accept unknwon fields.""" diff --git a/invenio_records_marc21/resources/serializers/serializer.py b/invenio_records_marc21/resources/serializers/serializer.py index 1918ae36..5f8a6042 100644 --- a/invenio_records_marc21/resources/serializers/serializer.py +++ b/invenio_records_marc21/resources/serializers/serializer.py @@ -105,7 +105,7 @@ def convert_record(self, data): record = E.record() for key, value in data.items(): if "metadata" in key: - record.append(self.convert_metadata(to_marc21.do(value["json"]))) + record.append(self.convert_metadata(to_marc21.do(value))) continue record.append(self._convert(key, value)) return etree.tostring( From a11044868a4e24b5079a13a0dbf93be7b73c3254 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Wed, 15 Sep 2021 10:38:30 +0200 Subject: [PATCH 100/217] modification: metadata ui presentation updating the landingpage and search engine using the new metadata structure. --- .../templates/invenio_records_marc21/record.html | 4 ++-- .../js/invenio_records_marc21/search/components.js | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/invenio_records_marc21/templates/invenio_records_marc21/record.html b/invenio_records_marc21/templates/invenio_records_marc21/record.html index 83efa429..d7b3bc56 100755 --- a/invenio_records_marc21/templates/invenio_records_marc21/record.html +++ b/invenio_records_marc21/templates/invenio_records_marc21/record.html @@ -13,7 +13,7 @@ {%- set metadata = record.metadata %} -{%- set marcrecord = record.ui.metadata.json %} +{%- set marcrecord = record.ui.metadata %} {%- block page_body %} @@ -53,7 +53,7 @@ {%- endblock record_header -%} {%- block record_title -%} -

{{ marcrecord.title_statement.title | safe() | sanitize_title() }}

+

{{ marcrecord.get('title_statement').get('title') | sanitize_title() }}

{%- endblock record_title -%} {%- block record_content -%}
diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/components.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/components.js index 6fc29b58..72ae8785 100755 --- a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/components.js +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/components.js @@ -43,8 +43,8 @@ export const Marc21RecordResultsListItem = ({ result, index }) => { const access_status = _get(access, "title", "Public"); const access_icon = _get(access, "icon", "unlock"); - const metadata = _get(result, ["ui", "metadata", "json"], []); - const description = _get(metadata, ["summary","summary"], "No description"); + const metadata = _get(result, ["ui", "metadata"], []); + const description = _get(metadata, ["summary", "summary"], "No description"); const subjects = _get(metadata, "subject_added_entry_topical_term", []); const publication = _get(metadata, ["production_publication_distribution_manufacture_and_copyright_notice"], []); @@ -108,7 +108,7 @@ export const Marc21RecordResultsListItem = ({ result, index }) => { export const Marc21RecordResultsGridItem = ({ result, index }) => { const metadata = _get(result, ["ui", "metadata", "json"], []); - const description = _get(metadata, ["summary", "0", "summary"], "No description"); + const description = _get(metadata, ["summary", "summary"], "No description"); return ( From a11cb7d7ce3144f7a3f734271ef67f47ae7e7030 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Wed, 15 Sep 2021 10:39:34 +0200 Subject: [PATCH 101/217] test(metadata): metadata structure updating the testset using the new metadata custom field --- .../serializers/ui/fields/test_ui_metadata.py | 59 ++++++----- .../serializers/ui/test_serializers.py | 31 ++---- tests/services/test_record_service.py | 100 +++++++++--------- 3 files changed, 88 insertions(+), 102 deletions(-) diff --git a/tests/resources/serializers/ui/fields/test_ui_metadata.py b/tests/resources/serializers/ui/fields/test_ui_metadata.py index 43e0d902..460f5e42 100644 --- a/tests/resources/serializers/ui/fields/test_ui_metadata.py +++ b/tests/resources/serializers/ui/fields/test_ui_metadata.py @@ -17,9 +17,16 @@ from dojson.contrib.marc21.utils import create_record from dojson.contrib.to_marc21 import to_marc21 from dojson.contrib.to_marc21.utils import dumps +from marshmallow import Schema from invenio_records_marc21.resources.serializers.errors import Marc21XMLConvertError -from invenio_records_marc21.resources.serializers.fields import MetadataSchema +from invenio_records_marc21.resources.serializers.fields import MetadataField, metadata + + +class Marc21TestSchema(Schema): + """Marc21 Test Schema.""" + + metadata = MetadataField(attribute="metadata") def _test_metadata(test, expected, exept=["__order__"]): @@ -41,67 +48,61 @@ def _test_without_order(data, key="__order__"): def test_ui_metadata_remove_order(marc21_metadata): - metadata = MetadataSchema(context={"remove_order": True}) - data = metadata.dump(marc21_metadata) - _test_without_order(data) + metadata = Marc21TestSchema(context={"remove_order": True}) + data = metadata.dump({"metadata": marc21_metadata}) + _test_without_order(data["metadata"]) def test_ui_metadata_convert_xml(marc21_metadata): - metadata = MetadataSchema(context={"marcxml": True}) + metadata = Marc21TestSchema(context={"marcxml": True}) test = deepcopy(marc21_metadata) - data = metadata.dump(test) - assert "xml" in data - assert "json" not in data - assert isinstance(data["xml"], str) + data = metadata.dump({"metadata": test}) + assert isinstance(data["metadata"], bytes) - expect_str = dumps(to_marc21.do(marc21_metadata["json"])).decode("UTF-8") - assert expect_str == data["xml"] + expect_str = dumps(to_marc21.do(marc21_metadata)) + assert expect_str == data["metadata"] def test_ui_metadata_default_schema(marc21_metadata): - metadata = MetadataSchema() - data = metadata.dump(marc21_metadata) - assert data == marc21_metadata + metadata = Marc21TestSchema() + data = metadata.dump({"metadata": marc21_metadata}) + assert data["metadata"] == marc21_metadata _test_metadata( - data["json"], - marc21_metadata["json"], + data["metadata"], + marc21_metadata, ) - assert "xml" not in data def test_ui_metadata_xml_schema(marc21_metadata): """Test metadata schema.""" - metadata = MetadataSchema( + metadata = Marc21TestSchema( context={ "marcxml": True, } ) test = deepcopy(marc21_metadata) - data = metadata.dump(test) - assert "json" not in data - assert "xml" in data + data = metadata.dump({"metadata": test}) - s = "".join(data["xml"].split("\n")[1:-1]) + s = "".join(data["metadata"].decode("UTF-8").split("\n")[1:-1]) test = marc21.do(create_record(s)) _test_without_order( test, - marc21_metadata["json"], + marc21_metadata, ) def test_ui_metadata_json_schema(marc21_metadata): - metadata = MetadataSchema( + metadata = Marc21TestSchema( context={ "marcxml": False, } ) test = deepcopy(marc21_metadata) - data = metadata.dump(test) - assert "xml" not in data - assert "json" in data + data = metadata.dump({"metadata": test}) + _test_metadata( - data["json"], - marc21_metadata["json"], + data["metadata"], + marc21_metadata, ) diff --git a/tests/resources/serializers/ui/test_serializers.py b/tests/resources/serializers/ui/test_serializers.py index 1175e8fd..a3df1714 100644 --- a/tests/resources/serializers/ui/test_serializers.py +++ b/tests/resources/serializers/ui/test_serializers.py @@ -27,15 +27,13 @@ def test_ui_marcxml_serializer_init(): def test_ui_marcxml_serializer_dump_one(full_record): marc = Marc21UIXMLSerializer() obj = marc.dump_one(full_record) - assert "json" in obj["metadata"] - assert full_record["metadata"]["json"] == obj["metadata"]["json"] - assert len(obj["metadata"]) == 1 + assert isinstance(obj["metadata"], dict) + assert full_record["metadata"] == obj["metadata"] assert marc._object_key in obj obj_ui = obj[marc._object_key] assert "metadata" in obj_ui - assert len(obj_ui["metadata"]) == 1 - assert "xml" in obj_ui["metadata"] + assert isinstance(obj_ui["metadata"], bytes) def test_ui_marcxml_serializer_dump_many(list_records): @@ -45,14 +43,11 @@ def test_ui_marcxml_serializer_dump_many(list_records): assert marc._object_key in obj assert "metadata" in obj - assert record["metadata"]["json"] == obj["metadata"]["json"] - assert len(obj["metadata"]) == 1 + assert record["metadata"] == obj["metadata"] obj_ui = obj[marc._object_key] assert "metadata" in obj_ui - assert len(obj_ui["metadata"]) == 1 - assert "xml" in obj_ui["metadata"] - assert "json" not in obj_ui["metadata"] + assert isinstance(obj_ui["metadata"], bytes) def test_ui_json_serializer_init(): @@ -65,16 +60,13 @@ def test_ui_json_serializer_dump_one(full_record): marc = Marc21UIJSONSerializer() obj = marc.dump_one(full_record) - assert "json" in obj["metadata"] - assert full_record["metadata"]["json"] == obj["metadata"]["json"] - assert len(obj["metadata"]) == 1 + assert isinstance(obj["metadata"], dict) + assert full_record["metadata"] == obj["metadata"] assert marc._object_key in obj obj_ui = obj[marc._object_key] assert "metadata" in obj_ui - assert len(obj_ui["metadata"]) == 1 - assert "xml" not in obj_ui["metadata"] - assert "json" in obj_ui["metadata"] + assert isinstance(obj_ui["metadata"], dict) def test_ui_json_serializer_dump_many(list_records): @@ -84,11 +76,8 @@ def test_ui_json_serializer_dump_many(list_records): assert marc._object_key in obj assert "metadata" in obj - assert record["metadata"]["json"] == obj["metadata"]["json"] - assert len(obj["metadata"]) == 1 + assert record["metadata"] == obj["metadata"] obj_ui = obj[marc._object_key] assert "metadata" in obj_ui - assert len(obj_ui["metadata"]) == 1 - assert "xml" not in obj_ui["metadata"] - assert "json" in obj_ui["metadata"] + assert isinstance(obj_ui["metadata"], dict) diff --git a/tests/services/test_record_service.py b/tests/services/test_record_service.py index f552f999..6fcb23ee 100755 --- a/tests/services/test_record_service.py +++ b/tests/services/test_record_service.py @@ -148,8 +148,8 @@ def test_update_draft(running_app, metadata, metadata2): assert draft.id == update_draft.id _test_metadata( - to_marc21.do(update_draft["metadata"]["json"]), - to_marc21.do(read_draft["metadata"]["json"]), + to_marc21.do(update_draft["metadata"]), + to_marc21.do(read_draft["metadata"]), ) @@ -184,46 +184,44 @@ def test_create_publish_new_version(running_app, metadata): # Embargo lift # -@mock.patch('arrow.utcnow') -def test_embargo_lift_without_draft( - mock_arrow, running_app, marc21_record): +@mock.patch("arrow.utcnow") +def test_embargo_lift_without_draft(mock_arrow, running_app, marc21_record): identity_simple = running_app.identity_simple service = current_records_marc21.records_service # Add embargo to record - marc21_record["access"]["files"] = 'restricted' - marc21_record["access"]["status"] = 'embargoed' + marc21_record["access"]["files"] = "restricted" + marc21_record["access"]["status"] = "embargoed" marc21_record["access"]["embargo"] = dict( - active=True, until='2020-06-01', reason=None + active=True, until="2020-06-01", reason=None ) # We need to set the current date in the past to pass the validations - mock_arrow.return_value = arrow.get(datetime(1954, 9, 29), tz.gettz('UTC')) + mock_arrow.return_value = arrow.get(datetime(1954, 9, 29), tz.gettz("UTC")) draft = service.create(identity_simple, marc21_record) record = service.publish(id_=draft.id, identity=identity_simple) # Recover current date mock_arrow.return_value = arrow.get(datetime.utcnow()) - service.lift_embargo(_id=record['id'], identity=identity_simple) - record_lifted = service.record_cls.pid.resolve(record['id']) + service.lift_embargo(_id=record["id"], identity=identity_simple) + record_lifted = service.record_cls.pid.resolve(record["id"]) assert not record_lifted.access.embargo.active - assert record_lifted.access.protection.files == 'public' - assert record_lifted.access.protection.metadata == 'public' - assert record_lifted.access.status.value == 'public' + assert record_lifted.access.protection.files == "public" + assert record_lifted.access.protection.metadata == "public" + assert record_lifted.access.status.value == "public" -@mock.patch('arrow.utcnow') -def test_embargo_lift_with_draft( - mock_arrow, running_app, marc21_record): +@mock.patch("arrow.utcnow") +def test_embargo_lift_with_draft(mock_arrow, running_app, marc21_record): identity_simple = running_app.identity_simple service = current_records_marc21.records_service # Add embargo to record - marc21_record["access"]["files"] = 'restricted' - marc21_record["access"]["status"] = 'embargoed' + marc21_record["access"]["files"] = "restricted" + marc21_record["access"]["status"] = "embargoed" marc21_record["access"]["embargo"] = dict( - active=True, until='2020-06-01', reason=None + active=True, until="2020-06-01", reason=None ) - mock_arrow.return_value = arrow.get(datetime(1954, 9, 29), tz.gettz('UTC')) + mock_arrow.return_value = arrow.get(datetime(1954, 9, 29), tz.gettz("UTC")) draft = service.create(identity_simple, marc21_record) record = service.publish(id_=draft.id, identity=identity_simple) # This draft simulates an existing one while lifting the record @@ -231,32 +229,31 @@ def test_embargo_lift_with_draft( mock_arrow.return_value = arrow.get(datetime.utcnow()) - service.lift_embargo(_id=record['id'], identity=identity_simple) - record_lifted = service.record_cls.pid.resolve(record['id']) - draft_lifted = service.draft_cls.pid.resolve(ongoing_draft['id']) + service.lift_embargo(_id=record["id"], identity=identity_simple) + record_lifted = service.record_cls.pid.resolve(record["id"]) + draft_lifted = service.draft_cls.pid.resolve(ongoing_draft["id"]) assert record_lifted.access.embargo.active is False - assert record_lifted.access.protection.files == 'public' - assert record_lifted.access.protection.metadata == 'public' + assert record_lifted.access.protection.files == "public" + assert record_lifted.access.protection.metadata == "public" assert draft_lifted.access.embargo.active is False - assert draft_lifted.access.protection.files == 'public' - assert draft_lifted.access.protection.metadata == 'public' + assert draft_lifted.access.protection.files == "public" + assert draft_lifted.access.protection.metadata == "public" -@mock.patch('arrow.utcnow') -def test_embargo_lift_with_updated_draft( - mock_arrow, running_app, marc21_record): +@mock.patch("arrow.utcnow") +def test_embargo_lift_with_updated_draft(mock_arrow, running_app, marc21_record): identity_simple = running_app.identity_simple service = current_records_marc21.records_service # Add embargo to record - marc21_record["access"]["files"] = 'restricted' - marc21_record["access"]["status"] = 'embargoed' + marc21_record["access"]["files"] = "restricted" + marc21_record["access"]["status"] = "embargoed" marc21_record["access"]["embargo"] = dict( - active=True, until='2020-06-01', reason=None + active=True, until="2020-06-01", reason=None ) # We need to set the current date in the past to pass the validations - mock_arrow.return_value = arrow.get(datetime(1954, 9, 29), tz.gettz('UTC')) + mock_arrow.return_value = arrow.get(datetime(1954, 9, 29), tz.gettz("UTC")) draft = service.create(identity_simple, marc21_record) record = service.publish(id_=draft.id, identity=identity_simple) # This draft simulates an existing one while lifting the record @@ -265,40 +262,39 @@ def test_embargo_lift_with_updated_draft( mock_arrow.return_value = arrow.get(datetime.utcnow()) # Change record's title and access field to be restricted - marc21_record["metadata"]["title"] = 'Record modified by the user' - marc21_record["access"]["status"] = 'restricted' - marc21_record["access"]["embargo"] = dict( - active=False, until=None, reason=None - ) + marc21_record["metadata"]["title"] = "Record modified by the user" + marc21_record["access"]["status"] = "restricted" + marc21_record["access"]["embargo"] = dict(active=False, until=None, reason=None) # Update the ongoing draft with the new data simulating the user's input ongoing_draft = service.update_draft( - id_=draft.id, identity=identity_simple, data=marc21_record) + id_=draft.id, identity=identity_simple, data=marc21_record + ) - service.lift_embargo(_id=record['id'], identity=identity_simple) - record_lifted = service.record_cls.pid.resolve(record['id']) - draft_lifted = service.draft_cls.pid.resolve(ongoing_draft['id']) + service.lift_embargo(_id=record["id"], identity=identity_simple) + record_lifted = service.record_cls.pid.resolve(record["id"]) + draft_lifted = service.draft_cls.pid.resolve(ongoing_draft["id"]) assert record_lifted.access.embargo.active is False - assert record_lifted.access.protection.files == 'public' - assert record_lifted.access.protection.metadata == 'public' + assert record_lifted.access.protection.files == "public" + assert record_lifted.access.protection.metadata == "public" assert draft_lifted.access.embargo.active is False - assert draft_lifted.access.protection.files == 'restricted' - assert draft_lifted.access.protection.metadata == 'public' + assert draft_lifted.access.protection.files == "restricted" + assert draft_lifted.access.protection.metadata == "public" def test_embargo_lift_with_error(running_app, marc21_record): identity_simple = running_app.identity_simple service = current_records_marc21.records_service # Add embargo to record - marc21_record["access"]["files"] = 'restricted' - marc21_record["access"]["status"] = 'embargoed' + marc21_record["access"]["files"] = "restricted" + marc21_record["access"]["status"] = "embargoed" marc21_record["access"]["embargo"] = dict( - active=True, until='3220-06-01', reason=None + active=True, until="3220-06-01", reason=None ) draft = service.create(identity_simple, marc21_record) record = service.publish(id_=draft.id, identity=identity_simple) # Record should not be lifted since it didn't expire (until 3220) with pytest.raises(EmbargoNotLiftedError): - service.lift_embargo(_id=record['id'], identity=identity_simple) + service.lift_embargo(_id=record["id"], identity=identity_simple) From 4a46cf0bb93fb007d2dc2f1e52ba2df0e67b45ef Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Wed, 15 Sep 2021 10:40:36 +0200 Subject: [PATCH 102/217] modification: codestyle improve the docstring of the marc21 module --- invenio_records_marc21/services/__init__.py | 7 +++++-- invenio_records_marc21/services/errors.py | 1 - 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/invenio_records_marc21/services/__init__.py b/invenio_records_marc21/services/__init__.py index e9ff1322..85e17c28 100644 --- a/invenio_records_marc21/services/__init__.py +++ b/invenio_records_marc21/services/__init__.py @@ -1,9 +1,12 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """High-level API for wokring with Marc21 records, pids and search.""" diff --git a/invenio_records_marc21/services/errors.py b/invenio_records_marc21/services/errors.py index 6bd57146..ed084f4e 100644 --- a/invenio_records_marc21/services/errors.py +++ b/invenio_records_marc21/services/errors.py @@ -1,4 +1,3 @@ - # -*- coding: utf-8 -*- # # This file is part of Invenio. From 3dfa080d3cd02eb416635f658213f6bea8fb4896 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Wed, 15 Sep 2021 11:03:42 +0200 Subject: [PATCH 103/217] test(metadata): metadata schema updating the metadata field tests to the new schema. --- tests/services/schemas/test_metadata.py | 32 ++++++++++++++----------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/tests/services/schemas/test_metadata.py b/tests/services/schemas/test_metadata.py index 8d5d13fb..a0117543 100644 --- a/tests/services/schemas/test_metadata.py +++ b/tests/services/schemas/test_metadata.py @@ -9,13 +9,17 @@ # details. -"""Test for record MetadataSchema.""" - +"""Test for record MetadataField.""" from dojson.contrib.marc21 import marc21 from dojson.contrib.marc21.utils import create_record +from marshmallow import Schema + +from invenio_records_marc21.services.schemas.metadata import MetadataField -from invenio_records_marc21.services.schemas.metadata import MetadataSchema + +class Marc21TestSchema(Schema): + metadata = MetadataField(attribute="metadata") def _test_metadata(test, expected): @@ -27,31 +31,31 @@ def _test_metadata(test, expected): def test_full_metadata_xml_schema(app, full_metadata): """Test metadata schema.""" - metadata = MetadataSchema() - data = metadata.load(full_metadata) + metadata = Marc21TestSchema() + data = metadata.load({"metadata": full_metadata}) _test_metadata( - data["json"], + data["metadata"], marc21.do(create_record(full_metadata["xml"])), ) assert "xml" not in data def test_minimal_metadata_xml_schema(app, min_metadata): - metadata = MetadataSchema() - data = metadata.load(min_metadata) + metadata = Marc21TestSchema() + data = metadata.load({"metadata": min_metadata}) _test_metadata( - data["json"], + data["metadata"], marc21.do(create_record(min_metadata["xml"])), ) assert "xml" not in data def test_minimal_metadata_json_schema(app, min_json_metadata): - metadata = MetadataSchema() - data = metadata.load(min_json_metadata) - assert data == min_json_metadata + metadata = Marc21TestSchema() + data = metadata.load({"metadata": min_json_metadata}) + assert data["metadata"] == min_json_metadata _test_metadata( - data, + data["metadata"], min_json_metadata, ) - assert "xml" not in data + assert "json" not in data From af135f3f158862f09f4b3312005e6cedeaf4fae2 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Wed, 15 Sep 2021 12:08:29 +0200 Subject: [PATCH 104/217] test: reduce fixture calls set the fixture scope to module --- tests/services/conftest.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/services/conftest.py b/tests/services/conftest.py index f3e51954..339dad7d 100755 --- a/tests/services/conftest.py +++ b/tests/services/conftest.py @@ -38,7 +38,7 @@ def create_app(instance_path): RunningApp = namedtuple("RunningApp", ["app", "service", "identity_simple"]) -@pytest.fixture() +@pytest.fixture(scope="module") def running_app(app, service, identity_simple): """This fixture provides an app with the typically needed db data loaded. @@ -48,7 +48,7 @@ def running_app(app, service, identity_simple): return RunningApp(app, service, identity_simple) -@pytest.fixture() +@pytest.fixture(scope="module") def identity_simple(): """Simple identity fixture.""" i = Identity(1) @@ -56,13 +56,13 @@ def identity_simple(): return i -@pytest.fixture() +@pytest.fixture(scope="module") def service(appctx): """Service instance.""" return Marc21RecordService(config=Marc21RecordServiceConfig()) -@pytest.fixture() +@pytest.fixture(scope="session") def metadata(): """Input data (as coming from the view layer).""" metadata = Marc21Metadata() @@ -70,7 +70,7 @@ def metadata(): return metadata -@pytest.fixture() +@pytest.fixture(scope="session") def metadata2(): """Input data (as coming from the view layer).""" metadata = Marc21Metadata() From 603d847ffb0d82c160884cdd22f2f31d864223fe Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Thu, 16 Sep 2021 08:00:29 +0200 Subject: [PATCH 105/217] style: codestyle updating file permission to 644 and updating the file doc strings to a single style. --- .editorconfig | 7 +++++-- .gitignore | 10 ++++++++++ invenio_records_marc21/__init__.py | 7 +++++-- invenio_records_marc21/cli.py | 8 +++++--- invenio_records_marc21/config.py | 1 - invenio_records_marc21/ext.py | 7 +++++-- invenio_records_marc21/proxies.py | 7 +++++-- invenio_records_marc21/records/__init__.py | 8 +++++--- invenio_records_marc21/records/api.py | 5 +++-- invenio_records_marc21/records/jsonschemas/__init__.py | 7 +++++-- invenio_records_marc21/records/mappings/__init__.py | 7 +++++-- invenio_records_marc21/records/mappings/v7/__init__.py | 7 +++++-- invenio_records_marc21/records/models.py | 9 +++++++-- .../records/systemfields/__init__.py | 7 +++++-- .../records/systemfields/access/__init__.py | 6 +++--- .../records/systemfields/access/embargo.py | 5 +++-- .../records/systemfields/access/fields/parent.py | 5 +++-- .../records/systemfields/access/fields/record.py | 8 +++++--- .../records/systemfields/access/owners.py | 5 +++-- invenio_records_marc21/records/systemfields/context.py | 7 +++++-- .../records/systemfields/has_draft.py | 8 ++++++-- .../records/systemfields/providers.py | 7 +++++-- .../records/systemfields/resolver.py | 7 +++++-- invenio_records_marc21/resources/__init__.py | 8 +++++--- invenio_records_marc21/resources/config.py | 9 ++++++--- invenio_records_marc21/resources/resources.py | 6 ++++-- invenio_records_marc21/resources/serializers/errors.py | 1 - .../resources/serializers/fields/__init__.py | 1 - .../resources/serializers/serializer.py | 1 - .../resources/serializers/ui/__init__.py | 1 - .../resources/serializers/ui/fields/__init__.py | 1 - .../resources/serializers/ui/serializers.py | 1 - invenio_records_marc21/services/__init__.py | 8 +++++--- invenio_records_marc21/services/components/__init__.py | 7 +++++-- invenio_records_marc21/services/components/access.py | 5 +++-- invenio_records_marc21/services/components/metadata.py | 8 +++++--- invenio_records_marc21/services/components/pid.py | 7 +++++-- invenio_records_marc21/services/config.py | 8 +++++--- invenio_records_marc21/services/errors.py | 2 -- invenio_records_marc21/services/permissions.py | 1 - invenio_records_marc21/services/record/__init__.py | 8 +++++--- .../services/record/fields/__init__.py | 8 +++++--- .../services/record/fields/control.py | 7 +++++-- invenio_records_marc21/services/record/fields/data.py | 7 +++++-- .../services/record/fields/leader.py | 7 +++++-- invenio_records_marc21/services/record/fields/sub.py | 7 +++++-- invenio_records_marc21/services/record/metadata.py | 7 +++++-- invenio_records_marc21/services/schemas/__init__.py | 7 +++++-- .../services/schemas/access/__init__.py | 7 +++++-- .../services/schemas/access/embargo.py | 7 +++++-- .../services/schemas/access/parent.py | 5 +++-- .../services/schemas/access/record.py | 5 +++-- invenio_records_marc21/services/schemas/files.py | 8 ++++++-- invenio_records_marc21/services/schemas/metadata.py | 7 +++++-- invenio_records_marc21/services/schemas/pids.py | 7 +++++-- invenio_records_marc21/services/schemas/utils.py | 7 +++++-- invenio_records_marc21/services/schemas/versions.py | 8 ++++++-- invenio_records_marc21/services/services.py | 1 - .../templates/invenio_records_marc21/base.html | 5 ++++- .../templates/invenio_records_marc21/index.html | 5 ++++- .../templates/invenio_records_marc21/record.html | 8 +++++--- .../records/helpers/description.html | 0 .../records/helpers/details.html | 0 .../records/helpers/subjects.html | 0 .../invenio_records_marc21/records/macros/detail.html | 0 .../templates/invenio_records_marc21/results.html | 7 +++++-- .../templates/invenio_records_marc21/search.html | 0 invenio_records_marc21/ui/__init__.py | 7 +++++-- invenio_records_marc21/ui/records/__init__.py | 7 +++++-- invenio_records_marc21/ui/records/decorators.py | 8 ++++++-- invenio_records_marc21/ui/records/errors.py | 7 +++++-- invenio_records_marc21/ui/records/filters.py | 8 +++++--- invenio_records_marc21/ui/records/records.py | 8 +++++--- invenio_records_marc21/ui/theme/__init__.py | 8 +++++--- .../js/invenio_records_marc21/search/components.js | 8 +++++--- .../js/invenio_records_marc21/search/index.js | 0 invenio_records_marc21/ui/theme/views.py | 8 +++++--- invenio_records_marc21/ui/theme/webpack.py | 8 +++++--- invenio_records_marc21/version.py | 7 +++++-- invenio_records_marc21/views.py | 7 +++++-- pytest.ini | 2 ++ requirements-devel.txt | 2 ++ run-tests.sh | 3 ++- setup.cfg | 2 ++ setup.py | 2 ++ tests/api/conftest.py | 8 ++++++-- tests/conftest.py | 0 tests/records/conftest.py | 7 +++++-- tests/records/systemfields/conftest.py | 0 tests/records/systemfields/test_systemfield_access.py | 0 tests/records/systemfields/test_systemfield_files.py | 8 ++++++-- tests/records/test_jsonschema.py | 7 +++++-- tests/resources/serializers/conftest.py | 1 - tests/resources/serializers/ui/fields/conftest.py | 1 - tests/services/conftest.py | 1 - tests/services/schemas/access/conftest.py | 3 +-- tests/services/schemas/access/test_record.py | 7 ++++--- tests/services/schemas/conftest.py | 1 - tests/services/schemas/test_access.py | 6 +++--- tests/services/schemas/test_metadata.py | 1 - tests/services/schemas/test_utils.py | 8 ++++++-- tests/services/test_create_record.py | 0 tests/services/test_record_service.py | 0 tests/test_invenio_records_marc21.py | 2 ++ 104 files changed, 361 insertions(+), 177 deletions(-) mode change 100755 => 100644 invenio_records_marc21/ext.py mode change 100755 => 100644 invenio_records_marc21/records/systemfields/access/embargo.py mode change 100755 => 100644 invenio_records_marc21/records/systemfields/access/fields/parent.py mode change 100755 => 100644 invenio_records_marc21/records/systemfields/access/fields/record.py mode change 100755 => 100644 invenio_records_marc21/records/systemfields/access/owners.py mode change 100755 => 100644 invenio_records_marc21/resources/__init__.py mode change 100755 => 100644 invenio_records_marc21/resources/config.py mode change 100755 => 100644 invenio_records_marc21/resources/resources.py mode change 100755 => 100644 invenio_records_marc21/resources/serializers/ui/__init__.py mode change 100755 => 100644 invenio_records_marc21/resources/serializers/ui/fields/__init__.py mode change 100755 => 100644 invenio_records_marc21/templates/invenio_records_marc21/base.html mode change 100755 => 100644 invenio_records_marc21/templates/invenio_records_marc21/index.html mode change 100755 => 100644 invenio_records_marc21/templates/invenio_records_marc21/record.html mode change 100755 => 100644 invenio_records_marc21/templates/invenio_records_marc21/records/helpers/description.html mode change 100755 => 100644 invenio_records_marc21/templates/invenio_records_marc21/records/helpers/details.html mode change 100755 => 100644 invenio_records_marc21/templates/invenio_records_marc21/records/helpers/subjects.html mode change 100755 => 100644 invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html mode change 100755 => 100644 invenio_records_marc21/templates/invenio_records_marc21/results.html mode change 100755 => 100644 invenio_records_marc21/templates/invenio_records_marc21/search.html mode change 100755 => 100644 invenio_records_marc21/ui/records/filters.py mode change 100755 => 100644 invenio_records_marc21/ui/theme/__init__.py mode change 100755 => 100644 invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/components.js mode change 100755 => 100644 invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/index.js mode change 100755 => 100644 invenio_records_marc21/ui/theme/views.py mode change 100755 => 100644 invenio_records_marc21/ui/theme/webpack.py mode change 100755 => 100644 setup.py mode change 100755 => 100644 tests/conftest.py mode change 100755 => 100644 tests/records/systemfields/conftest.py mode change 100755 => 100644 tests/records/systemfields/test_systemfield_access.py mode change 100755 => 100644 tests/resources/serializers/conftest.py mode change 100755 => 100644 tests/resources/serializers/ui/fields/conftest.py mode change 100755 => 100644 tests/services/conftest.py mode change 100755 => 100644 tests/services/schemas/access/conftest.py mode change 100755 => 100644 tests/services/schemas/access/test_record.py mode change 100755 => 100644 tests/services/schemas/conftest.py mode change 100755 => 100644 tests/services/schemas/test_access.py mode change 100755 => 100644 tests/services/test_create_record.py mode change 100755 => 100644 tests/services/test_record_service.py diff --git a/.editorconfig b/.editorconfig index 8710b39f..87f24dce 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,9 +1,12 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. root = true diff --git a/.gitignore b/.gitignore index 9f14cb29..b9ad1bb7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,13 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# +# Copyright (C) 2020 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/invenio_records_marc21/__init__.py b/invenio_records_marc21/__init__.py index e60efda2..8ac31c35 100644 --- a/invenio_records_marc21/__init__.py +++ b/invenio_records_marc21/__init__.py @@ -1,9 +1,12 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """Marc21 datamodel.""" diff --git a/invenio_records_marc21/cli.py b/invenio_records_marc21/cli.py index 0b9e198d..c038af9b 100644 --- a/invenio_records_marc21/cli.py +++ b/invenio_records_marc21/cli.py @@ -1,10 +1,12 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """Command-line tools for demo module.""" diff --git a/invenio_records_marc21/config.py b/invenio_records_marc21/config.py index 66cdad1c..2891154c 100644 --- a/invenio_records_marc21/config.py +++ b/invenio_records_marc21/config.py @@ -8,7 +8,6 @@ # modify it under the terms of the MIT License; see LICENSE file for more # details. - """Default configuration.""" from __future__ import absolute_import, print_function diff --git a/invenio_records_marc21/ext.py b/invenio_records_marc21/ext.py old mode 100755 new mode 100644 index 85f738eb..4558db0e --- a/invenio_records_marc21/ext.py +++ b/invenio_records_marc21/ext.py @@ -1,9 +1,12 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """Flask extension for Invenio-Records-Marc21.""" diff --git a/invenio_records_marc21/proxies.py b/invenio_records_marc21/proxies.py index 0d45337d..180cadc0 100644 --- a/invenio_records_marc21/proxies.py +++ b/invenio_records_marc21/proxies.py @@ -1,9 +1,12 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """Helper proxy to the state object.""" diff --git a/invenio_records_marc21/records/__init__.py b/invenio_records_marc21/records/__init__.py index 8ed9a156..f8220d7f 100644 --- a/invenio_records_marc21/records/__init__.py +++ b/invenio_records_marc21/records/__init__.py @@ -1,10 +1,12 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """Marc21 Records module.""" diff --git a/invenio_records_marc21/records/api.py b/invenio_records_marc21/records/api.py index 385d9e09..b982391b 100644 --- a/invenio_records_marc21/records/api.py +++ b/invenio_records_marc21/records/api.py @@ -4,8 +4,9 @@ # # Copyright (C) 2021 Graz University of Technology. # -# invenio-records-marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """Marc21 Api.""" diff --git a/invenio_records_marc21/records/jsonschemas/__init__.py b/invenio_records_marc21/records/jsonschemas/__init__.py index 25cf8897..10096489 100644 --- a/invenio_records_marc21/records/jsonschemas/__init__.py +++ b/invenio_records_marc21/records/jsonschemas/__init__.py @@ -1,9 +1,12 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """JSON schemas. diff --git a/invenio_records_marc21/records/mappings/__init__.py b/invenio_records_marc21/records/mappings/__init__.py index d89c3892..f227e5c6 100644 --- a/invenio_records_marc21/records/mappings/__init__.py +++ b/invenio_records_marc21/records/mappings/__init__.py @@ -1,9 +1,12 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """Mappings. diff --git a/invenio_records_marc21/records/mappings/v7/__init__.py b/invenio_records_marc21/records/mappings/v7/__init__.py index ef79bca0..3be96c15 100644 --- a/invenio_records_marc21/records/mappings/v7/__init__.py +++ b/invenio_records_marc21/records/mappings/v7/__init__.py @@ -1,9 +1,12 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """Mappings for Elasticsearch 7.x.""" diff --git a/invenio_records_marc21/records/models.py b/invenio_records_marc21/records/models.py index a31a901d..e14ed8fa 100644 --- a/invenio_records_marc21/records/models.py +++ b/invenio_records_marc21/records/models.py @@ -1,9 +1,14 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2020 CERN. +# Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + """Marc21 Record and Draft models.""" diff --git a/invenio_records_marc21/records/systemfields/__init__.py b/invenio_records_marc21/records/systemfields/__init__.py index a5ed4872..57b02a99 100644 --- a/invenio_records_marc21/records/systemfields/__init__.py +++ b/invenio_records_marc21/records/systemfields/__init__.py @@ -1,9 +1,12 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """System fields module.""" diff --git a/invenio_records_marc21/records/systemfields/access/__init__.py b/invenio_records_marc21/records/systemfields/access/__init__.py index 7475caeb..e8adf103 100644 --- a/invenio_records_marc21/records/systemfields/access/__init__.py +++ b/invenio_records_marc21/records/systemfields/access/__init__.py @@ -4,9 +4,9 @@ # # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """Access module.""" diff --git a/invenio_records_marc21/records/systemfields/access/embargo.py b/invenio_records_marc21/records/systemfields/access/embargo.py old mode 100755 new mode 100644 index 0e2422a4..df2d1f74 --- a/invenio_records_marc21/records/systemfields/access/embargo.py +++ b/invenio_records_marc21/records/systemfields/access/embargo.py @@ -5,8 +5,9 @@ # Copyright (C) 2021 TU Wien. # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """Embargo class for the access system field.""" diff --git a/invenio_records_marc21/records/systemfields/access/fields/parent.py b/invenio_records_marc21/records/systemfields/access/fields/parent.py old mode 100755 new mode 100644 index 86e3122a..3b6d5e8d --- a/invenio_records_marc21/records/systemfields/access/fields/parent.py +++ b/invenio_records_marc21/records/systemfields/access/fields/parent.py @@ -5,8 +5,9 @@ # Copyright (C) 2021 TU Wien. # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """Access system field.""" diff --git a/invenio_records_marc21/records/systemfields/access/fields/record.py b/invenio_records_marc21/records/systemfields/access/fields/record.py old mode 100755 new mode 100644 index 00c246e5..5c892b66 --- a/invenio_records_marc21/records/systemfields/access/fields/record.py +++ b/invenio_records_marc21/records/systemfields/access/fields/record.py @@ -5,8 +5,9 @@ # Copyright (C) 2021 TU Wien. # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """Access system field.""" @@ -145,7 +146,8 @@ def obj(self, instance): data = self.get_dictkey(instance) if data: - obj = self._access_obj_class.from_dict(data, has_files=len(instance.files or [])) + obj = self._access_obj_class.from_dict( + data, has_files=len(instance.files or [])) else: obj = self._access_obj_class() diff --git a/invenio_records_marc21/records/systemfields/access/owners.py b/invenio_records_marc21/records/systemfields/access/owners.py old mode 100755 new mode 100644 index a95d2780..6c58adcc --- a/invenio_records_marc21/records/systemfields/access/owners.py +++ b/invenio_records_marc21/records/systemfields/access/owners.py @@ -5,8 +5,9 @@ # Copyright (C) 2021 TU Wien. # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """Owners classes for the access system field.""" diff --git a/invenio_records_marc21/records/systemfields/context.py b/invenio_records_marc21/records/systemfields/context.py index d6464c63..a7d4741b 100644 --- a/invenio_records_marc21/records/systemfields/context.py +++ b/invenio_records_marc21/records/systemfields/context.py @@ -1,9 +1,12 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """System field context for the Marc21 PID field. diff --git a/invenio_records_marc21/records/systemfields/has_draft.py b/invenio_records_marc21/records/systemfields/has_draft.py index 3e7dd33e..004610bb 100644 --- a/invenio_records_marc21/records/systemfields/has_draft.py +++ b/invenio_records_marc21/records/systemfields/has_draft.py @@ -1,10 +1,14 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 CERN. # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + """Record has draft check field. diff --git a/invenio_records_marc21/records/systemfields/providers.py b/invenio_records_marc21/records/systemfields/providers.py index 617bb216..32b2a7ca 100644 --- a/invenio_records_marc21/records/systemfields/providers.py +++ b/invenio_records_marc21/records/systemfields/providers.py @@ -1,9 +1,12 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2020 Graz University of Technology. # -# invenio-records-lom 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """Marc PID providers.""" diff --git a/invenio_records_marc21/records/systemfields/resolver.py b/invenio_records_marc21/records/systemfields/resolver.py index ef84c84f..a73738d5 100644 --- a/invenio_records_marc21/records/systemfields/resolver.py +++ b/invenio_records_marc21/records/systemfields/resolver.py @@ -1,9 +1,12 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2020 Graz University of Technology. # -# invenio-records-lom 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """Marc PID resolver.""" diff --git a/invenio_records_marc21/resources/__init__.py b/invenio_records_marc21/resources/__init__.py old mode 100755 new mode 100644 index c9ac74f2..3308bf7b --- a/invenio_records_marc21/resources/__init__.py +++ b/invenio_records_marc21/resources/__init__.py @@ -1,10 +1,12 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 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 Marc21 module to create REST APIs.""" diff --git a/invenio_records_marc21/resources/config.py b/invenio_records_marc21/resources/config.py old mode 100755 new mode 100644 index 2f2964c9..5bfba994 --- a/invenio_records_marc21/resources/config.py +++ b/invenio_records_marc21/resources/config.py @@ -1,12 +1,15 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """Resources configuration.""" + import marshmallow as ma from flask_resources import JSONDeserializer, RequestBodyParser, ResponseHandler from invenio_drafts_resources.resources import RecordResourceConfig diff --git a/invenio_records_marc21/resources/resources.py b/invenio_records_marc21/resources/resources.py old mode 100755 new mode 100644 index 513da4d8..897ccb27 --- a/invenio_records_marc21/resources/resources.py +++ b/invenio_records_marc21/resources/resources.py @@ -1,13 +1,15 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. # # Copyright (C) 2020-2021 CERN. # Copyright (C) 2020 Northwestern University. # Copyright (C) 2021 TU Wien. # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """Marc21 Record Resource.""" diff --git a/invenio_records_marc21/resources/serializers/errors.py b/invenio_records_marc21/resources/serializers/errors.py index ab89f60d..b29afde7 100644 --- a/invenio_records_marc21/resources/serializers/errors.py +++ b/invenio_records_marc21/resources/serializers/errors.py @@ -8,7 +8,6 @@ # modify it under the terms of the MIT License; see LICENSE file for more # details. - """Errors for serializers.""" diff --git a/invenio_records_marc21/resources/serializers/fields/__init__.py b/invenio_records_marc21/resources/serializers/fields/__init__.py index 1299a084..1b245664 100644 --- a/invenio_records_marc21/resources/serializers/fields/__init__.py +++ b/invenio_records_marc21/resources/serializers/fields/__init__.py @@ -8,7 +8,6 @@ # modify it under the terms of the MIT License; see LICENSE file for more # details. - """Record response fields.""" from .metadata import MetadataSchema diff --git a/invenio_records_marc21/resources/serializers/serializer.py b/invenio_records_marc21/resources/serializers/serializer.py index 1918ae36..e16c8d72 100644 --- a/invenio_records_marc21/resources/serializers/serializer.py +++ b/invenio_records_marc21/resources/serializers/serializer.py @@ -8,7 +8,6 @@ # modify it under the terms of the MIT License; see LICENSE file for more # details. - """Marc21 record response serializers.""" import json diff --git a/invenio_records_marc21/resources/serializers/ui/__init__.py b/invenio_records_marc21/resources/serializers/ui/__init__.py old mode 100755 new mode 100644 index 94fd2585..a1fab37b --- a/invenio_records_marc21/resources/serializers/ui/__init__.py +++ b/invenio_records_marc21/resources/serializers/ui/__init__.py @@ -8,7 +8,6 @@ # modify it under the terms of the MIT License; see LICENSE file for more # details. - """Record response serializers.""" from .schema import Marc21UISchema diff --git a/invenio_records_marc21/resources/serializers/ui/fields/__init__.py b/invenio_records_marc21/resources/serializers/ui/fields/__init__.py old mode 100755 new mode 100644 index ab6d1b77..ac5f5188 --- a/invenio_records_marc21/resources/serializers/ui/fields/__init__.py +++ b/invenio_records_marc21/resources/serializers/ui/fields/__init__.py @@ -8,7 +8,6 @@ # modify it under the terms of the MIT License; see LICENSE file for more # details. - """Record UI response fields.""" diff --git a/invenio_records_marc21/resources/serializers/ui/serializers.py b/invenio_records_marc21/resources/serializers/ui/serializers.py index 02b43242..9f75a5cc 100644 --- a/invenio_records_marc21/resources/serializers/ui/serializers.py +++ b/invenio_records_marc21/resources/serializers/ui/serializers.py @@ -8,7 +8,6 @@ # modify it under the terms of the MIT License; see LICENSE file for more # details. - """Marc21 UI record response serializers.""" diff --git a/invenio_records_marc21/services/__init__.py b/invenio_records_marc21/services/__init__.py index e9ff1322..fdd91dff 100644 --- a/invenio_records_marc21/services/__init__.py +++ b/invenio_records_marc21/services/__init__.py @@ -1,10 +1,12 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """High-level API for wokring with Marc21 records, pids and search.""" diff --git a/invenio_records_marc21/services/components/__init__.py b/invenio_records_marc21/services/components/__init__.py index 1e0a6c13..d3700820 100644 --- a/invenio_records_marc21/services/components/__init__.py +++ b/invenio_records_marc21/services/components/__init__.py @@ -1,9 +1,12 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """Marc21 record components.""" diff --git a/invenio_records_marc21/services/components/access.py b/invenio_records_marc21/services/components/access.py index fa5b7ca7..86d279ba 100644 --- a/invenio_records_marc21/services/components/access.py +++ b/invenio_records_marc21/services/components/access.py @@ -4,8 +4,9 @@ # # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """Marc21 AccessComponent.""" diff --git a/invenio_records_marc21/services/components/metadata.py b/invenio_records_marc21/services/components/metadata.py index 13577f85..c8129642 100644 --- a/invenio_records_marc21/services/components/metadata.py +++ b/invenio_records_marc21/services/components/metadata.py @@ -1,10 +1,12 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """Marc21 record metadata component.""" diff --git a/invenio_records_marc21/services/components/pid.py b/invenio_records_marc21/services/components/pid.py index 5e52418f..c8e6fb64 100644 --- a/invenio_records_marc21/services/components/pid.py +++ b/invenio_records_marc21/services/components/pid.py @@ -1,9 +1,12 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """Marc21 external pid component.""" diff --git a/invenio_records_marc21/services/config.py b/invenio_records_marc21/services/config.py index e5b85a6d..f1fa7496 100644 --- a/invenio_records_marc21/services/config.py +++ b/invenio_records_marc21/services/config.py @@ -1,10 +1,12 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """Marc21 Record Service config.""" diff --git a/invenio_records_marc21/services/errors.py b/invenio_records_marc21/services/errors.py index 6bd57146..da34c1ee 100644 --- a/invenio_records_marc21/services/errors.py +++ b/invenio_records_marc21/services/errors.py @@ -1,4 +1,3 @@ - # -*- coding: utf-8 -*- # # This file is part of Invenio. @@ -9,7 +8,6 @@ # modify it under the terms of the MIT License; see LICENSE file for more # details. - """Services exceptions.""" diff --git a/invenio_records_marc21/services/permissions.py b/invenio_records_marc21/services/permissions.py index f5f01e8b..14d50a1d 100644 --- a/invenio_records_marc21/services/permissions.py +++ b/invenio_records_marc21/services/permissions.py @@ -8,7 +8,6 @@ # modify it under the terms of the MIT License; see LICENSE file for more # details. - """Permissions for Invenio Marc21 Records.""" from invenio_records_permissions.generators import AnyUser diff --git a/invenio_records_marc21/services/record/__init__.py b/invenio_records_marc21/services/record/__init__.py index a7e302ad..d06119ca 100644 --- a/invenio_records_marc21/services/record/__init__.py +++ b/invenio_records_marc21/services/record/__init__.py @@ -1,10 +1,12 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """Marc21 field class.""" diff --git a/invenio_records_marc21/services/record/fields/__init__.py b/invenio_records_marc21/services/record/fields/__init__.py index e046be71..c27c8e60 100644 --- a/invenio_records_marc21/services/record/fields/__init__.py +++ b/invenio_records_marc21/services/record/fields/__init__.py @@ -1,10 +1,12 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """Marc21 field class.""" diff --git a/invenio_records_marc21/services/record/fields/control.py b/invenio_records_marc21/services/record/fields/control.py index 59241d2e..f451a349 100644 --- a/invenio_records_marc21/services/record/fields/control.py +++ b/invenio_records_marc21/services/record/fields/control.py @@ -1,9 +1,12 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """Marc21 control field class.""" diff --git a/invenio_records_marc21/services/record/fields/data.py b/invenio_records_marc21/services/record/fields/data.py index 91103090..e143f531 100644 --- a/invenio_records_marc21/services/record/fields/data.py +++ b/invenio_records_marc21/services/record/fields/data.py @@ -1,9 +1,12 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """Marc21 data field class.""" diff --git a/invenio_records_marc21/services/record/fields/leader.py b/invenio_records_marc21/services/record/fields/leader.py index 68147c03..9f131290 100644 --- a/invenio_records_marc21/services/record/fields/leader.py +++ b/invenio_records_marc21/services/record/fields/leader.py @@ -1,9 +1,12 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """Marc21 leader field class.""" diff --git a/invenio_records_marc21/services/record/fields/sub.py b/invenio_records_marc21/services/record/fields/sub.py index 42bae7f0..70b92dab 100644 --- a/invenio_records_marc21/services/record/fields/sub.py +++ b/invenio_records_marc21/services/record/fields/sub.py @@ -1,9 +1,12 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """Marc21 sub field class.""" diff --git a/invenio_records_marc21/services/record/metadata.py b/invenio_records_marc21/services/record/metadata.py index f96dcc57..4ee4e8f5 100644 --- a/invenio_records_marc21/services/record/metadata.py +++ b/invenio_records_marc21/services/record/metadata.py @@ -1,9 +1,12 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """Marc21 record class.""" diff --git a/invenio_records_marc21/services/schemas/__init__.py b/invenio_records_marc21/services/schemas/__init__.py index 7e3f286f..5515ec3f 100644 --- a/invenio_records_marc21/services/schemas/__init__.py +++ b/invenio_records_marc21/services/schemas/__init__.py @@ -1,9 +1,12 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """Marc21 record schemas.""" diff --git a/invenio_records_marc21/services/schemas/access/__init__.py b/invenio_records_marc21/services/schemas/access/__init__.py index bd6dde3d..fb7746df 100644 --- a/invenio_records_marc21/services/schemas/access/__init__.py +++ b/invenio_records_marc21/services/schemas/access/__init__.py @@ -1,9 +1,12 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """Marc21 parent record schemas.""" diff --git a/invenio_records_marc21/services/schemas/access/embargo.py b/invenio_records_marc21/services/schemas/access/embargo.py index 6769cc43..9f6d95ea 100644 --- a/invenio_records_marc21/services/schemas/access/embargo.py +++ b/invenio_records_marc21/services/schemas/access/embargo.py @@ -1,9 +1,12 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """Marc21 embargo access schemas.""" diff --git a/invenio_records_marc21/services/schemas/access/parent.py b/invenio_records_marc21/services/schemas/access/parent.py index 92c974a3..3e5b1681 100644 --- a/invenio_records_marc21/services/schemas/access/parent.py +++ b/invenio_records_marc21/services/schemas/access/parent.py @@ -4,8 +4,9 @@ # # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """Marc21 record ParentAccessSchema.""" diff --git a/invenio_records_marc21/services/schemas/access/record.py b/invenio_records_marc21/services/schemas/access/record.py index d9ac950f..0a5f1fcb 100644 --- a/invenio_records_marc21/services/schemas/access/record.py +++ b/invenio_records_marc21/services/schemas/access/record.py @@ -4,8 +4,9 @@ # # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """Marc21 services record AccessSchema.""" diff --git a/invenio_records_marc21/services/schemas/files.py b/invenio_records_marc21/services/schemas/files.py index e3ecb9d5..58a2fa95 100644 --- a/invenio_records_marc21/services/schemas/files.py +++ b/invenio_records_marc21/services/schemas/files.py @@ -1,11 +1,15 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2020 CERN. # Copyright (C) 2020 Northwestern University. # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + """Marc21 record files schemas.""" diff --git a/invenio_records_marc21/services/schemas/metadata.py b/invenio_records_marc21/services/schemas/metadata.py index de23b90c..c1290eaf 100644 --- a/invenio_records_marc21/services/schemas/metadata.py +++ b/invenio_records_marc21/services/schemas/metadata.py @@ -1,9 +1,12 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """Marc21 record schemas.""" diff --git a/invenio_records_marc21/services/schemas/pids.py b/invenio_records_marc21/services/schemas/pids.py index c87c1a03..23a19363 100644 --- a/invenio_records_marc21/services/schemas/pids.py +++ b/invenio_records_marc21/services/schemas/pids.py @@ -1,9 +1,12 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """Marc21 record schemas.""" diff --git a/invenio_records_marc21/services/schemas/utils.py b/invenio_records_marc21/services/schemas/utils.py index aa5748c3..671bb128 100644 --- a/invenio_records_marc21/services/schemas/utils.py +++ b/invenio_records_marc21/services/schemas/utils.py @@ -1,9 +1,12 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """Marc21 record schema utilities.""" diff --git a/invenio_records_marc21/services/schemas/versions.py b/invenio_records_marc21/services/schemas/versions.py index cd4c25ae..4df85ba0 100644 --- a/invenio_records_marc21/services/schemas/versions.py +++ b/invenio_records_marc21/services/schemas/versions.py @@ -1,10 +1,14 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 TU Wien. # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + """Marc21 record schema version.""" diff --git a/invenio_records_marc21/services/services.py b/invenio_records_marc21/services/services.py index eed23fbc..97d9f556 100644 --- a/invenio_records_marc21/services/services.py +++ b/invenio_records_marc21/services/services.py @@ -8,7 +8,6 @@ # modify it under the terms of the MIT License; see LICENSE file for more # details. - """Marc21 Record Service.""" import arrow diff --git a/invenio_records_marc21/templates/invenio_records_marc21/base.html b/invenio_records_marc21/templates/invenio_records_marc21/base.html old mode 100755 new mode 100644 index 3ef627d7..caa7cfb6 --- a/invenio_records_marc21/templates/invenio_records_marc21/base.html +++ b/invenio_records_marc21/templates/invenio_records_marc21/base.html @@ -1,4 +1,7 @@ -{# +{# -*- coding: utf-8 -*- + + This file is part of Invenio. + Copyright (C) 2021 Graz University of Technology. Invenio-Records-Marc21 is free software; you can redistribute it and/or diff --git a/invenio_records_marc21/templates/invenio_records_marc21/index.html b/invenio_records_marc21/templates/invenio_records_marc21/index.html old mode 100755 new mode 100644 index 94cf49d4..a786aa2b --- a/invenio_records_marc21/templates/invenio_records_marc21/index.html +++ b/invenio_records_marc21/templates/invenio_records_marc21/index.html @@ -1,4 +1,7 @@ -{# +{# -*- coding: utf-8 -*- + + This file is part of Invenio. + Copyright (C) 2021 Graz University of Technology. Invenio-Records-Marc21 is free software; you can redistribute it and/or diff --git a/invenio_records_marc21/templates/invenio_records_marc21/record.html b/invenio_records_marc21/templates/invenio_records_marc21/record.html old mode 100755 new mode 100644 index 83efa429..826d5d54 --- a/invenio_records_marc21/templates/invenio_records_marc21/record.html +++ b/invenio_records_marc21/templates/invenio_records_marc21/record.html @@ -1,9 +1,11 @@ -{# -*- coding: utf-8 -*- +{# + This file is part of Invenio. Copyright (C) 2021 Graz University of Technology. - Invenio-Records-Marc21 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-Records-Marc21 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 config.INVENIO_MARC21_BASE_TEMPLATE %} diff --git a/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/description.html b/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/description.html old mode 100755 new mode 100644 diff --git a/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/details.html b/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/details.html old mode 100755 new mode 100644 diff --git a/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/subjects.html b/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/subjects.html old mode 100755 new mode 100644 diff --git a/invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html b/invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html old mode 100755 new mode 100644 diff --git a/invenio_records_marc21/templates/invenio_records_marc21/results.html b/invenio_records_marc21/templates/invenio_records_marc21/results.html old mode 100755 new mode 100644 index 06ff4b0e..65beb26a --- a/invenio_records_marc21/templates/invenio_records_marc21/results.html +++ b/invenio_records_marc21/templates/invenio_records_marc21/results.html @@ -1,9 +1,12 @@ {# -*- coding: utf-8 -*- + This file is part of Invenio. + Copyright (C) 2021 Graz University of Technology. - Invenio-Records-Marc21 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-Records-Marc21 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_records_marc21/templates/invenio_records_marc21/search.html b/invenio_records_marc21/templates/invenio_records_marc21/search.html old mode 100755 new mode 100644 diff --git a/invenio_records_marc21/ui/__init__.py b/invenio_records_marc21/ui/__init__.py index e367b23d..ab7a27d2 100644 --- a/invenio_records_marc21/ui/__init__.py +++ b/invenio_records_marc21/ui/__init__.py @@ -1,9 +1,12 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """Records user interface.""" diff --git a/invenio_records_marc21/ui/records/__init__.py b/invenio_records_marc21/ui/records/__init__.py index 7a06dc84..73a667be 100644 --- a/invenio_records_marc21/ui/records/__init__.py +++ b/invenio_records_marc21/ui/records/__init__.py @@ -1,9 +1,12 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """Records user interface.""" diff --git a/invenio_records_marc21/ui/records/decorators.py b/invenio_records_marc21/ui/records/decorators.py index c512dd6a..b35931e7 100644 --- a/invenio_records_marc21/ui/records/decorators.py +++ b/invenio_records_marc21/ui/records/decorators.py @@ -1,12 +1,16 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2019-2021 CERN. # Copyright (C) 2019-2021 Northwestern University. # Copyright (C) 2021 TU Wien. # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + """Routes for record-related pages provided by Invenio-Records-Marc21.""" diff --git a/invenio_records_marc21/ui/records/errors.py b/invenio_records_marc21/ui/records/errors.py index 802d46a5..b4c2be33 100644 --- a/invenio_records_marc21/ui/records/errors.py +++ b/invenio_records_marc21/ui/records/errors.py @@ -1,9 +1,12 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """Records error handlers.""" diff --git a/invenio_records_marc21/ui/records/filters.py b/invenio_records_marc21/ui/records/filters.py old mode 100755 new mode 100644 index 61366b38..445877a0 --- a/invenio_records_marc21/ui/records/filters.py +++ b/invenio_records_marc21/ui/records/filters.py @@ -1,10 +1,12 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """Filters to be used in the Jinja templates.""" diff --git a/invenio_records_marc21/ui/records/records.py b/invenio_records_marc21/ui/records/records.py index cb0768a5..debb9a51 100644 --- a/invenio_records_marc21/ui/records/records.py +++ b/invenio_records_marc21/ui/records/records.py @@ -1,10 +1,12 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """Routes for record-related pages provided by Invenio-App-RDM.""" diff --git a/invenio_records_marc21/ui/theme/__init__.py b/invenio_records_marc21/ui/theme/__init__.py old mode 100755 new mode 100644 index 2ae71329..cb31859b --- a/invenio_records_marc21/ui/theme/__init__.py +++ b/invenio_records_marc21/ui/theme/__init__.py @@ -1,10 +1,12 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """Marc21 Theme Package.""" diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/components.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/components.js old mode 100755 new mode 100644 index 6fc29b58..b79722f5 --- a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/components.js +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/components.js @@ -1,8 +1,10 @@ -// This file is part of Invenio-Records-Marc21. +// This file is part of Invenio. +// // Copyright (C) 2021 Graz University of Technology. // -// Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +// modify it under the terms of the MIT License; see LICENSE file for more +// details. import React, { useState } from "react"; import { diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/index.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/index.js old mode 100755 new mode 100644 diff --git a/invenio_records_marc21/ui/theme/views.py b/invenio_records_marc21/ui/theme/views.py old mode 100755 new mode 100644 index ea1cad45..02490372 --- a/invenio_records_marc21/ui/theme/views.py +++ b/invenio_records_marc21/ui/theme/views.py @@ -1,10 +1,12 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """Routes for general pages provided by Invenio-Records-Marc21.""" diff --git a/invenio_records_marc21/ui/theme/webpack.py b/invenio_records_marc21/ui/theme/webpack.py old mode 100755 new mode 100644 index 9306e7a7..20f57a20 --- a/invenio_records_marc21/ui/theme/webpack.py +++ b/invenio_records_marc21/ui/theme/webpack.py @@ -1,10 +1,12 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """JS/CSS Webpack bundles for theme.""" diff --git a/invenio_records_marc21/version.py b/invenio_records_marc21/version.py index c50edad3..32f92e71 100644 --- a/invenio_records_marc21/version.py +++ b/invenio_records_marc21/version.py @@ -1,9 +1,12 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """Version information for Invenio-Records-Marc21. diff --git a/invenio_records_marc21/views.py b/invenio_records_marc21/views.py index ca7264e1..320da69f 100644 --- a/invenio_records_marc21/views.py +++ b/invenio_records_marc21/views.py @@ -1,9 +1,12 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """Blueprint definitions.""" diff --git a/pytest.ini b/pytest.ini index 352bceeb..902707f4 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # # Invenio-Records-Marc21 is free software; you can redistribute it and/or diff --git a/requirements-devel.txt b/requirements-devel.txt index ea74f3fe..a41c6657 100644 --- a/requirements-devel.txt +++ b/requirements-devel.txt @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # # Invenio-Records-Marc21 is free software; you can redistribute it and/or diff --git a/run-tests.sh b/run-tests.sh index 8f3b397e..9be73f85 100755 --- a/run-tests.sh +++ b/run-tests.sh @@ -1,13 +1,14 @@ #!/usr/bin/env bash # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # # Invenio-Records-Marc21 is free software; you can redistribute it and/or # modify it under the terms of the MIT License; see LICENSE file for more # details. - # Usage: # env DB=postgresql12 SEARCH=elasticsearch7 CACHE=redis MQ=rabbitmq ./run-tests.sh diff --git a/setup.cfg b/setup.cfg index b8e68f73..b33a6b7f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # # Invenio-Records-Marc21 is free software; you can redistribute it and/or diff --git a/setup.py b/setup.py old mode 100755 new mode 100644 index 0da8d5d2..2cee1673 --- a/setup.py +++ b/setup.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # # Invenio-Records-Marc21 is free software; you can redistribute it and/or diff --git a/tests/api/conftest.py b/tests/api/conftest.py index c02d526e..a5d89238 100644 --- a/tests/api/conftest.py +++ b/tests/api/conftest.py @@ -1,9 +1,13 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + """Common pytest fixtures and plugins.""" diff --git a/tests/conftest.py b/tests/conftest.py old mode 100755 new mode 100644 diff --git a/tests/records/conftest.py b/tests/records/conftest.py index 91b57aa8..52e4e322 100644 --- a/tests/records/conftest.py +++ b/tests/records/conftest.py @@ -1,9 +1,12 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """Pytest configuration. diff --git a/tests/records/systemfields/conftest.py b/tests/records/systemfields/conftest.py old mode 100755 new mode 100644 diff --git a/tests/records/systemfields/test_systemfield_access.py b/tests/records/systemfields/test_systemfield_access.py old mode 100755 new mode 100644 diff --git a/tests/records/systemfields/test_systemfield_files.py b/tests/records/systemfields/test_systemfield_files.py index fc9f68a8..f152e28e 100644 --- a/tests/records/systemfields/test_systemfield_files.py +++ b/tests/records/systemfields/test_systemfield_files.py @@ -1,9 +1,13 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + """Files field tests.""" diff --git a/tests/records/test_jsonschema.py b/tests/records/test_jsonschema.py index 55c6c14c..f3e94687 100644 --- a/tests/records/test_jsonschema.py +++ b/tests/records/test_jsonschema.py @@ -1,9 +1,12 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """JSONSchema tests.""" diff --git a/tests/resources/serializers/conftest.py b/tests/resources/serializers/conftest.py old mode 100755 new mode 100644 index 231fa07a..4095b3ba --- a/tests/resources/serializers/conftest.py +++ b/tests/resources/serializers/conftest.py @@ -8,7 +8,6 @@ # modify it under the terms of the MIT License; see LICENSE file for more # details. - """Pytest configuration. See https://pytest-invenio.readthedocs.io/ for documentation on which test diff --git a/tests/resources/serializers/ui/fields/conftest.py b/tests/resources/serializers/ui/fields/conftest.py old mode 100755 new mode 100644 index bfa82f8d..4aca8623 --- a/tests/resources/serializers/ui/fields/conftest.py +++ b/tests/resources/serializers/ui/fields/conftest.py @@ -8,7 +8,6 @@ # modify it under the terms of the MIT License; see LICENSE file for more # details. - """Pytest configuration. See https://pytest-invenio.readthedocs.io/ for documentation on which test diff --git a/tests/services/conftest.py b/tests/services/conftest.py old mode 100755 new mode 100644 index 339dad7d..2d086242 --- a/tests/services/conftest.py +++ b/tests/services/conftest.py @@ -8,7 +8,6 @@ # modify it under the terms of the MIT License; see LICENSE file for more # details. - """Pytest configuration. See https://pytest-invenio.readthedocs.io/ for documentation on which test diff --git a/tests/services/schemas/access/conftest.py b/tests/services/schemas/access/conftest.py old mode 100755 new mode 100644 index bfa82f8d..9415ba44 --- a/tests/services/schemas/access/conftest.py +++ b/tests/services/schemas/access/conftest.py @@ -6,8 +6,7 @@ # # Invenio-Records-Marc21 is free software; you can redistribute it and/or # modify it under the terms of the MIT License; see LICENSE file for more -# details. - +# details """Pytest configuration. diff --git a/tests/services/schemas/access/test_record.py b/tests/services/schemas/access/test_record.py old mode 100755 new mode 100644 index fc3162e6..44c506b0 --- a/tests/services/schemas/access/test_record.py +++ b/tests/services/schemas/access/test_record.py @@ -8,7 +8,6 @@ # modify it under the terms of the MIT License; see LICENSE file for more # details. - """Test record AccessSchema.""" @@ -45,7 +44,8 @@ def test_valid_metadata_protection(value): def test_invalid_metadata_protection(): with pytest.raises(ValidationError) as e: AccessSchema().validate_metadata_protection("invalid") - assert e.value.messages[0] == _("'metadata' must be either 'public', 'embargoed' or 'restricted'") + assert e.value.messages[0] == _( + "'metadata' must be either 'public', 'embargoed' or 'restricted'") assert e.value.field_name == "metadata" @@ -57,5 +57,6 @@ def test_valid_files_protection(value): def test_invalid_files_protection(): with pytest.raises(ValidationError) as e: AccessSchema().validate_files_protection("invalid") - assert e.value.messages[0] == _("'files' must be either 'public', 'embargoed' or 'restricted'") + assert e.value.messages[0] == _( + "'files' must be either 'public', 'embargoed' or 'restricted'") assert e.value.field_name == "files" diff --git a/tests/services/schemas/conftest.py b/tests/services/schemas/conftest.py old mode 100755 new mode 100644 index ce71376b..aa3fbd03 --- a/tests/services/schemas/conftest.py +++ b/tests/services/schemas/conftest.py @@ -8,7 +8,6 @@ # modify it under the terms of the MIT License; see LICENSE file for more # details. - """Pytest configuration. See https://pytest-invenio.readthedocs.io/ for documentation on which test diff --git a/tests/services/schemas/test_access.py b/tests/services/schemas/test_access.py old mode 100755 new mode 100644 index a41da289..10e7e625 --- a/tests/services/schemas/test_access.py +++ b/tests/services/schemas/test_access.py @@ -4,9 +4,9 @@ # # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. """Test record AccessSchema.""" diff --git a/tests/services/schemas/test_metadata.py b/tests/services/schemas/test_metadata.py index 8d5d13fb..2091b83e 100644 --- a/tests/services/schemas/test_metadata.py +++ b/tests/services/schemas/test_metadata.py @@ -8,7 +8,6 @@ # modify it under the terms of the MIT License; see LICENSE file for more # details. - """Test for record MetadataSchema.""" diff --git a/tests/services/schemas/test_utils.py b/tests/services/schemas/test_utils.py index f65fb5d9..51d25b96 100644 --- a/tests/services/schemas/test_utils.py +++ b/tests/services/schemas/test_utils.py @@ -1,9 +1,13 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # -# Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + """Test utilities. diff --git a/tests/services/test_create_record.py b/tests/services/test_create_record.py old mode 100755 new mode 100644 diff --git a/tests/services/test_record_service.py b/tests/services/test_record_service.py old mode 100755 new mode 100644 diff --git a/tests/test_invenio_records_marc21.py b/tests/test_invenio_records_marc21.py index 672d0a75..c7b9cc04 100644 --- a/tests/test_invenio_records_marc21.py +++ b/tests/test_invenio_records_marc21.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- # +# This file is part of Invenio. +# # Copyright (C) 2021 Graz University of Technology. # # Invenio-Records-Marc21 is free software; you can redistribute it and/or From 600b851bd723508407853bcf345132a570e02804 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 11 Oct 2021 09:43:43 +0200 Subject: [PATCH 106/217] feature: load etree metadata loading a metadata xml from etree to internal representation for the marc21 record service --- .../services/record/metadata.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/invenio_records_marc21/services/record/metadata.py b/invenio_records_marc21/services/record/metadata.py index 4ee4e8f5..4887cf39 100644 --- a/invenio_records_marc21/services/record/metadata.py +++ b/invenio_records_marc21/services/record/metadata.py @@ -54,13 +54,21 @@ def xml(self, xml: str): if not isinstance(xml, str): raise TypeError("xml must be from type str") - self._to_xml_tree(xml) + self._to_xml_tree_from_string(xml) self._xml = xml - def _to_xml_tree(self, xml: str): - """Xml to internal representation method.""" + def load(self, xml: etree): + """Load metadata from etree.""" + self._to_xml_tree(xml) + + def _to_xml_tree_from_string(self, xml: str): + """Xml string to internal representation method.""" test = etree.parse(StringIO(xml)) - for element in test.iter(): + self._to_xml_tree(test) + + def _to_xml_tree(self, xml: etree): + """Xml to internal representation method.""" + for element in xml.iter(): if "datafield" in element.tag: self.datafields.append(DataField(**element.attrib)) elif "subfield" in element.tag: From 987a575c173526c2be04a6e7d162170d93af74c5 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 11 Oct 2021 12:29:25 +0200 Subject: [PATCH 107/217] modification: leader field from string loading the leader field from a string --- .../services/record/fields/leader.py | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/invenio_records_marc21/services/record/fields/leader.py b/invenio_records_marc21/services/record/fields/leader.py index 9f131290..4a0d6ac9 100644 --- a/invenio_records_marc21/services/record/fields/leader.py +++ b/invenio_records_marc21/services/record/fields/leader.py @@ -17,8 +17,35 @@ class LeaderField(object): """LeaderField class representing the leaderfield HTML tag in MARC21 XML.""" - def __init__(self, **kwargs): + def __init__(self, data=None, **kwargs): """Default constructor of the class.""" + if data is not None: + self._load_from_str(data) + else: + self._load_from_dict(**kwargs) + + def _load_from_str(self, data: str): + if len(data) != 24: + raise ValueError("Leader must have 24 characters!!") + self.length = data[0:5] + self.status = data[5] + self.type = data[6] + self.level = data[7] + self.control = data[8] + self.charset = data[9] + + self.ind_count = data[10] + self.sub_count = data[11] + self.address = data[12:17] + self.encoding = data[17] + self.description = data[18] + self.multipart_resource_record_level = data[19] + self.length_field_position = data[20] + self.length_starting_character_position_portion = data[21] + self.length_implementation_defined_portion = data[22] + self.undefined = data[23] + + def _load_from_dict(self, **kwargs): self.length = kwargs.get("length", "00000") # 00-04 self.status = kwargs.get("status", "n") # 05 self.type = kwargs.get("type", "a") # 06 From f4d3a6c74ed14e5096bf748bc0987db141aa7ec4 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 11 Oct 2021 12:34:54 +0200 Subject: [PATCH 108/217] modification: init subfield init subfields with datafield together --- invenio_records_marc21/services/record/fields/data.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/invenio_records_marc21/services/record/fields/data.py b/invenio_records_marc21/services/record/fields/data.py index e143f531..f28543b7 100644 --- a/invenio_records_marc21/services/record/fields/data.py +++ b/invenio_records_marc21/services/record/fields/data.py @@ -12,17 +12,25 @@ from os import linesep +from .sub import SubField class DataField(object): """DataField class representing the datafield HTML tag in MARC21 XML.""" - def __init__(self, tag: str = "", ind1: str = " ", ind2: str = " "): + def __init__(self, tag: str = "", ind1: str = " ", ind2: str = " ", subfields=None): """Default constructor of the class.""" self.tag = tag self.ind1 = ind1 self.ind2 = ind2 self.subfields = list() + if subfields: + self.init_subfields(subfields) + + def init_subfields(self, subfields): + """Init containing subfields.""" + for subfield in subfields: + self.subfields.append(SubField(**subfield.attrib, value=subfield.text)) def to_xml_tag(self, tagsep: str = linesep, indent: int = 4) -> str: """Get the Marc21 Datafield XML tag as string.""" From 29dd6d33ce8b2a0cd1520de34e8479c5af855808 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 11 Oct 2021 12:37:34 +0200 Subject: [PATCH 109/217] modification: loading record init leaderfield from a string and init DataField together with subfields nad allow controlfields. --- .../services/record/fields/data.py | 1 + invenio_records_marc21/services/record/metadata.py | 14 ++++++++------ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/invenio_records_marc21/services/record/fields/data.py b/invenio_records_marc21/services/record/fields/data.py index f28543b7..fbbc7bd0 100644 --- a/invenio_records_marc21/services/record/fields/data.py +++ b/invenio_records_marc21/services/record/fields/data.py @@ -12,6 +12,7 @@ from os import linesep + from .sub import SubField diff --git a/invenio_records_marc21/services/record/metadata.py b/invenio_records_marc21/services/record/metadata.py index 4887cf39..bd5d5520 100644 --- a/invenio_records_marc21/services/record/metadata.py +++ b/invenio_records_marc21/services/record/metadata.py @@ -16,7 +16,7 @@ from lxml import etree -from .fields import DataField, LeaderField, SubField +from .fields import ControlField, DataField, LeaderField, SubField class Marc21Metadata(object): @@ -69,12 +69,14 @@ def _to_xml_tree_from_string(self, xml: str): def _to_xml_tree(self, xml: etree): """Xml to internal representation method.""" for element in xml.iter(): - if "datafield" in element.tag: - self.datafields.append(DataField(**element.attrib)) - elif "subfield" in element.tag: - self.datafields[-1].subfields.append( - SubField(**element.attrib, value=element.text) + if "leader" in element.tag: + self.leader = LeaderField(data=element.text) + elif "datafield" in element.tag: + self.datafields.append( + DataField(**element.attrib, subfields=element.getchildren()) ) + elif "controlfield" in element.tag: + self.controlfields.append(ControlField(**element.attrib)) def _to_string(self, tagsep: str = linesep, indent: int = 4) -> str: """Get a pretty-printed XML string of the record.""" From c2586d5d62bf8f381e899008286536d97a5e11a5 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Fri, 15 Oct 2021 10:29:07 +0200 Subject: [PATCH 110/217] feature: template service createing a template service allowing basic interation over the cli creating templates from a file and allow soft/hard delete of templates in db. --- invenio_records_marc21/cli.py | 79 +++++++ .../data/example-templates.json | 70 +++++++ invenio_records_marc21/ext.py | 4 + invenio_records_marc21/system/__init__.py | 19 ++ invenio_records_marc21/system/config.py | 15 ++ invenio_records_marc21/system/models.py | 193 ++++++++++++++++++ invenio_records_marc21/system/service.py | 60 ++++++ setup.py | 1 + 8 files changed, 441 insertions(+) create mode 100644 invenio_records_marc21/data/example-templates.json create mode 100644 invenio_records_marc21/system/__init__.py create mode 100644 invenio_records_marc21/system/config.py create mode 100644 invenio_records_marc21/system/models.py create mode 100644 invenio_records_marc21/system/service.py diff --git a/invenio_records_marc21/cli.py b/invenio_records_marc21/cli.py index c038af9b..19e508c0 100644 --- a/invenio_records_marc21/cli.py +++ b/invenio_records_marc21/cli.py @@ -157,3 +157,82 @@ def demo(number, file, metadata_only): create_fake_record(file) click.secho("Created records!", fg="green") + + +def create_templates(filename): + """Create templates with the service.""" + data_to_use = _load_json(filename) + + service = current_records_marc21.templates_service + templates = [] + for data in data_to_use: + template = service.create(data=data["values"], name=data["name"]) + templates.append(template.to_dict()) + return templates + + +def delete_templates(all, force, name): + """Delete templates with the service.""" + service = current_records_marc21.templates_service + result = service.delete(all=all, force=force, name=name) + return result + + +@marc21.group() +def templates(): + """InvenioMarc21 templates commands.""" + pass + + +@templates.command("create") +@click.option( + "--file", + "-f", + default="data/fake-metadata.xml", + show_default=True, + type=str, + help="Relative path to file", +) +@with_appcontext +@log_exceptions +def create(file): + """Create Templates for Marc21 Deposit app.""" + click.secho("Creating template/s..", fg="blue") + + create_templates(file) + + click.secho("Successfully created Template/s!", fg="green") + + +@templates.command("delete") +@click.option( + "--all", + default=False, + show_default=True, + is_flag=True, + help="Delete all Templates", +) +@click.option( + "--force", + "-f", + default=False, + show_default=True, + is_flag=True, + help="Hard/Soft delete of templates.", +) +@click.option( + "--name", + "-n", + required=False, + type=str, + help="Template name.", +) +@with_appcontext +@log_exceptions +def delete(all, force, name): + """Delete Templates for Marc21 Deposit app.""" + click.secho("Deleting template/s...", fg="blue") + + delete_templates(all, force, name) + + click.secho("Successfully deleted Template!", fg="green") diff --git a/invenio_records_marc21/data/example-templates.json b/invenio_records_marc21/data/example-templates.json new file mode 100644 index 00000000..6bbf4303 --- /dev/null +++ b/invenio_records_marc21/data/example-templates.json @@ -0,0 +1,70 @@ +[ + { + "name": "Test", + "values": { + "metadata": { + "leader": "00000nam a2200000zca4501", + "fields": [ + { + "id": "020", + "ind1": " ", + "ind2": " ", + "subfield": "$$a" + }, + { + "id": "100", + "ind1": "1", + "ind2": " ", + "subfield": "$$u" + }, + { + "id": "245", + "ind1": "0", + "ind2": "0", + "subfield": "$$a $$b $$c " + }, + { + "id": "264", + "ind1": " ", + "ind2": " ", + "subfield": "$$a $$b $$b $$b" + } + ] + } + } + }, + { + "name": "Test 2", + "values": { + "metadata": { + "leader": "00000nam a2200000zca4509", + "fields": [ + { + "id": "020", + "ind1": " ", + "ind2": " ", + "subfield": "$$a" + }, + { + "id": "100", + "ind1": "1", + "ind2": " ", + "subfield": "$$u" + }, + { + "id": "245", + "ind1": "0", + "ind2": "0", + "subfield": "$$a $$b $$c " + }, + { + "id": "264", + "ind1": " ", + "ind2": " ", + "subfield": "$$a $$b $$b $$c" + } + ] + } + } + } +] diff --git a/invenio_records_marc21/ext.py b/invenio_records_marc21/ext.py index 4558db0e..d85f882e 100644 --- a/invenio_records_marc21/ext.py +++ b/invenio_records_marc21/ext.py @@ -33,6 +33,7 @@ Marc21RecordService, Marc21RecordServiceConfig, ) +from .system import Marc21TemplateConfig, Marc21TemplateService def obj_or_import_string(value, default=None): @@ -101,6 +102,9 @@ def init_services(self, app): files_service=FileService(Marc21RecordFilesServiceConfig), draft_files_service=FileService(Marc21DraftFilesServiceConfig), ) + self.templates_service = Marc21TemplateService( + config=Marc21TemplateConfig, + ) def init_resources(self, app): """Initialize resources.""" diff --git a/invenio_records_marc21/system/__init__.py b/invenio_records_marc21/system/__init__.py new file mode 100644 index 00000000..9855f714 --- /dev/null +++ b/invenio_records_marc21/system/__init__.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 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 Marc21 module to create REST APIs.""" + +from .config import Marc21TemplateConfig +from .service import Marc21TemplateService + +__all__ = ( + "Marc21TemplateConfig", + "Marc21TemplateService", +) diff --git a/invenio_records_marc21/system/config.py b/invenio_records_marc21/system/config.py new file mode 100644 index 00000000..fceebaa3 --- /dev/null +++ b/invenio_records_marc21/system/config.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + +"""Resources configuration.""" + + +class Marc21TemplateConfig: + """Marc21 Record resource configuration.""" diff --git a/invenio_records_marc21/system/models.py b/invenio_records_marc21/system/models.py new file mode 100644 index 00000000..27377421 --- /dev/null +++ b/invenio_records_marc21/system/models.py @@ -0,0 +1,193 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it +# under the terms of the MIT License; see LICENSE file for more details. + + +"""Marc21 Record and Draft models.""" + +from copy import deepcopy +from datetime import datetime + +from invenio_db import db +from sqlalchemy.dialects import postgresql +from sqlalchemy.ext.hybrid import hybrid_property +from sqlalchemy.orm.attributes import flag_modified +from sqlalchemy_utils.types import JSONType + + +class Marc21Templates(db.Model): + """Represent a base class for record metadata. + + The RecordMetadata object contains a ``created`` and a ``updated`` + properties that are automatically updated. + """ + + encoder = None + """"Class-level attribute to set a JSON data encoder/decoder. + + This allows customizing you to e.g. convert specific entries to complex + Python objects. For instance you could convert ISO-formatted datetime + objects into Python datetime objects. + """ + + id = db.Column(db.Integer, primary_key=True) + + name = db.Column(db.String, unique=True) + + """Template identifier.""" + values = db.Column( + db.JSON() + .with_variant( + postgresql.JSONB(none_as_null=True), + "postgresql", + ) + .with_variant( + JSONType(), + "sqlite", + ) + .with_variant( + JSONType(), + "mysql", + ), + default=lambda: dict(), + nullable=True, + ) + """Store metadata in JSON format. + + When you create a new ``Record`` the ``json`` field value should never be + ``NULL``. Default value is an empty dict. ``NULL`` value means that the + record metadata has been deleted. + """ + + active = db.Column(db.Boolean, default=True) + """Active Template flag.""" + + created = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) + """Creation timestamp.""" + + @classmethod + def create(cls, name: str, data: dict, id_=None, **kwargs): + """Create a template for Marc21 deposit app. + + :param name: The template name. + :param data: Template values. + :returns: The created Template. + """ + template = cls(id=id_, name=name, data=data) + + db.session.add(template) + db.session.commit() + return template + + @classmethod + def delete(cls, name, force=False): + """Delete a template from table. + + :param name: The template name. + :param force: Hard/Soft delete the template. + :returns: The deleted Template. + """ + template = cls.get_template(name=name) + if force: + db.session.delete(template) + else: + template.is_active = False + db.session.commit() + return template + + @classmethod + def deleteall(cls, force): + """Truncate templates table. + + :param force: Hard/Soft delete all templates. + :returns: A list of :class:`Marc21Templates` instances. + """ + if force: + db.session.query(cls).delete() + else: + templates = cls.get_templates() + for template in templates: + template.is_active = False + db.session.commit() + + @classmethod + def get_template(cls, name): + """Retrieve multiple templates by id. + + :param names: List of template names. + :param with_deleted: If `True` then it includes deleted templates. + :returns: A list of :class:`Marc21Templates` instances. + """ + with db.session.no_autoflush: + query = cls.query.filter(cls.name == str(name)) + return query.first() + + @classmethod + def get_templates(cls, names=None, with_deleted=False): + """Retrieve multiple templates by id. + + :param names: List of template names. + :param with_deleted: If `True` then it includes deleted templates. + :returns: A list of :class:`Marc21Templates` instances. + """ + with db.session.no_autoflush: + query = cls.query + if names: + query = query.filter(cls.name.in_(names)) + if with_deleted: + query = query.filter(cls.is_active != with_deleted) + + return query.all() + + @hybrid_property + def is_active(self): + """Boolean flag to determine if a template is active.""" + return self.active + + @is_active.setter + def is_active(self, value): + """Boolean flag to set template as soft deleted.""" + self.active = value + + @property + def data(self): + """Get data by decoding the JSON. + + This allows a subclass to override + """ + return self.decode(self.values) + + @data.setter + def data(self, value): + """Set data by encoding the JSON. + + This allows a subclass to override + """ + self.values = self.encode(value) + flag_modified(self, "values") + + @classmethod + def encode(cls, value): + """Encode a JSON document.""" + data = deepcopy(value) + return cls.encoder.encode(data) if cls.encoder else data + + @classmethod + def decode(cls, json): + """Decode a JSON document.""" + data = deepcopy(json) + return cls.encoder.decode(data) if cls.encoder else data + + def to_dict(self): + """Dump the Marc21Template to a dictionary.""" + result_dict = { + "id": str(self.id), + "name": str(self.name), + "active": str(self.active), + "values": self.data, + "created_at": self.created, + } + return result_dict diff --git a/invenio_records_marc21/system/service.py b/invenio_records_marc21/system/service.py new file mode 100644 index 00000000..68fc4d48 --- /dev/null +++ b/invenio_records_marc21/system/service.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it +# under the terms of the MIT License; see LICENSE file for more details. + + +"""Marc21 Record Resource.""" + + +from flask_resources import route + +from . import config +from .models import Marc21Templates + + +# +# Records +# +class Marc21TemplateService: + """Marc21 template resource.""" + + config_name = "MARC21_TEMPLATE_CONFIG" + default_config = config.Marc21TemplateConfig + template_cls = Marc21Templates + + def __init__(self, config): + """Constructor for Marc21TemplateService.""" + self.config = config + + def create(self, name, data): + """Create a template for a new record.""" + return self._create(self.template_cls, name, data) + + def _create(self, template_cls, name, data): + template = template_cls.create(name=name, data=data) + return template + + def get_templates(self, names=None, with_deleted=False): + """Get templates for a new record.""" + return self.template_cls.get_templates(names=names, with_deleted=with_deleted) + + def get_template(self, name): + """Get a template for a new record.""" + return self.template_cls.get_template(name=name) + + def delete(self, all, force, name): + """Create a template for a new record.""" + if all: + return self._deleteall(self.template_cls, force=force) + return self._delete(self.template_cls, name=name, force=force) + + def _delete(self, template_cls, name, force): + """Create a template for a new record.""" + return template_cls.delete(name=name, force=force) + + def _deleteall(self, template_cls, force): + """Create a template for a new record.""" + return template_cls.deleteall(force=force) diff --git a/setup.py b/setup.py index 2cee1673..93fe4b43 100644 --- a/setup.py +++ b/setup.py @@ -107,6 +107,7 @@ ], "invenio_db.models": [ "invenio_records_marc21_model = invenio_records_marc21.records.models", + "invenio_records_marc21_template_model = invenio_records_marc21.system.models", ], "invenio_i18n.translations": [ "messages = invenio_records_marc21", From b02c5fa4cdc78d9668998da45a2559294ef8e564 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Fri, 15 Oct 2021 13:21:49 +0200 Subject: [PATCH 111/217] modification: template service removing default value for the file option and rearrange the name parameter as first. --- invenio_records_marc21/cli.py | 10 +++++----- invenio_records_marc21/system/service.py | 20 ++++---------------- 2 files changed, 9 insertions(+), 21 deletions(-) diff --git a/invenio_records_marc21/cli.py b/invenio_records_marc21/cli.py index 19e508c0..317d324a 100644 --- a/invenio_records_marc21/cli.py +++ b/invenio_records_marc21/cli.py @@ -171,10 +171,10 @@ def create_templates(filename): return templates -def delete_templates(all, force, name): +def delete_templates(name, all, force): """Delete templates with the service.""" service = current_records_marc21.templates_service - result = service.delete(all=all, force=force, name=name) + result = service.delete(name=name, all=all, force=force) return result @@ -188,7 +188,7 @@ def templates(): @click.option( "--file", "-f", - default="data/fake-metadata.xml", + required=True, show_default=True, type=str, help="Relative path to file", @@ -229,10 +229,10 @@ def create(file): ) @with_appcontext @log_exceptions -def delete(all, force, name): +def delete(name, all, force): """Delete Templates for Marc21 Deposit app.""" click.secho("Deleting template/s...", fg="blue") - delete_templates(all, force, name) + delete_templates(name, all, force) click.secho("Successfully deleted Template!", fg="green") diff --git a/invenio_records_marc21/system/service.py b/invenio_records_marc21/system/service.py index 68fc4d48..64688542 100644 --- a/invenio_records_marc21/system/service.py +++ b/invenio_records_marc21/system/service.py @@ -31,11 +31,7 @@ def __init__(self, config): def create(self, name, data): """Create a template for a new record.""" - return self._create(self.template_cls, name, data) - - def _create(self, template_cls, name, data): - template = template_cls.create(name=name, data=data) - return template + return self.template_cls.create(name=name, data=data) def get_templates(self, names=None, with_deleted=False): """Get templates for a new record.""" @@ -46,15 +42,7 @@ def get_template(self, name): return self.template_cls.get_template(name=name) def delete(self, all, force, name): - """Create a template for a new record.""" + """Delete a/all marc21 template/s.""" if all: - return self._deleteall(self.template_cls, force=force) - return self._delete(self.template_cls, name=name, force=force) - - def _delete(self, template_cls, name, force): - """Create a template for a new record.""" - return template_cls.delete(name=name, force=force) - - def _deleteall(self, template_cls, force): - """Create a template for a new record.""" - return template_cls.deleteall(force=force) + return self.template_cls.deleteall(force=force) + return self.template_cls.delete(name=name, force=force) From ad2912ef26404b522652f9d6a059e3b645b4c96b Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 18 Oct 2021 13:37:04 +0200 Subject: [PATCH 112/217] modification: create backend links creating invenio api resources links for marc21 records, list files and show drafts and publish drafts --- invenio_records_marc21/config.py | 1 + invenio_records_marc21/resources/config.py | 27 ++++++++++----- invenio_records_marc21/resources/resources.py | 34 +++++++++++++++++-- invenio_records_marc21/services/config.py | 14 ++++---- 4 files changed, 59 insertions(+), 17 deletions(-) diff --git a/invenio_records_marc21/config.py b/invenio_records_marc21/config.py index 2891154c..318768a0 100644 --- a/invenio_records_marc21/config.py +++ b/invenio_records_marc21/config.py @@ -50,6 +50,7 @@ INVENIO_MARC21_UI_THEME_ENDPOINTS = { "index": "/", "record-search": "/search", + "deposit-create": "/uploads", } """Records UI for invenio-records-marc21.""" diff --git a/invenio_records_marc21/resources/config.py b/invenio_records_marc21/resources/config.py index 5bfba994..54971c6e 100644 --- a/invenio_records_marc21/resources/config.py +++ b/invenio_records_marc21/resources/config.py @@ -31,14 +31,14 @@ url_prefix = "/marc21" record_ui_routes = { - "search": f"{url_prefix}/search", - "list": f"{url_prefix}", - "item": f"{url_prefix}/", - "item-versions": f"{url_prefix}//versions", - "item-latest": f"{url_prefix}//versions/latest", - "item-draft": f"{url_prefix}//draft", - "item-publish": f"{url_prefix}//draft/actions/publish", - "item-files-import": f"{url_prefix}//draft/actions/files-import", + "search": "/search", + "list": "", + "item": "/", + "item-versions": "//versions", + "item-latest": "//versions/latest", + "item-draft": "//draft", + "item-publish": "//draft/actions/publish", + "item-files-import": "//draft/actions/files-import", } @@ -64,7 +64,10 @@ class Marc21RecordResourceConfig(RecordResourceConfig): request_args = SearchRequestArgsSchema request_view_args = {"pid_value": ma.fields.Str()} request_headers = {"if_match": ma.fields.Int()} - request_body_parsers = {"application/json": RequestBodyParser(JSONDeserializer())} + request_body_parsers = { + "application/json": RequestBodyParser(JSONDeserializer()), + "application/marcxml": RequestBodyParser(JSONDeserializer()), + } request_view_args = { "pid_value": ma.fields.Str(), @@ -80,6 +83,12 @@ class Marc21RecordFilesResourceConfig(FileResourceConfig): url_prefix = f"{url_prefix}/" links_config = {} + routes = { + "list": "/files", + "item": "/files/", + "item-content": "/files//content", + "item-commit": "/files//commit", + } # diff --git a/invenio_records_marc21/resources/resources.py b/invenio_records_marc21/resources/resources.py index 897ccb27..c5fbce1c 100644 --- a/invenio_records_marc21/resources/resources.py +++ b/invenio_records_marc21/resources/resources.py @@ -36,8 +36,38 @@ def p(self, route): def create_url_rules(self): """Create the URL rules for the record resource.""" routes = self.config.routes - url_rules = super(RecordResource, self).create_url_rules() - return url_rules + + def p(route): + """Prefix a route with the URL prefix.""" + return f"{self.config.url_prefix}{route}" + + rules = [ + route("GET", p(routes["list"]), self.search), + route("POST", p(routes["list"]), self.create), + route("GET", p(routes["item"]), self.read), + route("PUT", p(routes["item"]), self.update), + route("DELETE", p(routes["item"]), self.delete), + route("GET", p(routes["item-versions"]), self.search_versions), + route("POST", p(routes["item-versions"]), self.new_version), + route("GET", p(routes["item-latest"]), self.read_latest), + route("GET", p(routes["item-draft"]), self.read_draft), + route("POST", p(routes["item-draft"]), self.edit), + route("PUT", p(routes["item-draft"]), self.update_draft), + route("DELETE", p(routes["item-draft"]), self.delete_draft), + route("POST", p(routes["item-publish"]), self.publish), + ] + + if self.service.draft_files: + rules.append( + route( + "POST", + p(routes["item-files-import"]), + self.import_files, + apply_decorators=False, + ) + ) + + return rules class Marc21ParentRecordLinksResource(RecordResource): diff --git a/invenio_records_marc21/services/config.py b/invenio_records_marc21/services/config.py index f1fa7496..ba78e9f1 100644 --- a/invenio_records_marc21/services/config.py +++ b/invenio_records_marc21/services/config.py @@ -16,6 +16,7 @@ RecordServiceConfig, SearchDraftsOptions, SearchOptions, + is_draft, is_record, ) from invenio_records_resources.services import ( @@ -106,14 +107,15 @@ class Marc21RecordServiceConfig(RecordServiceConfig): "self_html": ConditionalLink( cond=is_record, if_=RecordLink("{+ui}/marc21/{id}"), - else_=RecordLink("{+ui}/uploads/{id}"), + else_=RecordLink("{+ui}/marc21/upload/{id}"), ), - "files": ConditionalLink( - cond=is_record, - if_=RecordLink("{+api}/marc21/{id}/files"), - else_=RecordLink("{+api}/marc21/{id}/draft/files"), + "latest": RecordLink("{+api}/marc21/{id}/versions/latest"), + "latest_html": RecordLink("{+ui}/marc21/{id}/latest"), + "draft": RecordLink("{+api}/marc21/{id}/draft", when=is_record), + "publish": RecordLink( + "{+api}/marc21/{id}/draft/actions/publish", when=is_draft ), - "access_links": RecordLink("{+api}/marc21/{id}/access/links"), + "versions": RecordLink("{+api}/marc21/{id}/versions"), } From d2fca23ff2ec6da7692f5bae68c2ff340e36118a Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 18 Oct 2021 13:43:14 +0200 Subject: [PATCH 113/217] modification: webpaclk update updating webpack dependencies and create styles for marc21 record forms. --- .../invenio_records_marc21/base.html | 1 + .../less/invenio_records_marc21/deposit.less | 47 +++++++++++++++++++ .../less/invenio_records_marc21/theme.less | 11 +++++ .../invenio_records_marc21/variables.less | 11 +++++ invenio_records_marc21/ui/theme/webpack.py | 21 +++++---- 5 files changed, 82 insertions(+), 9 deletions(-) create mode 100644 invenio_records_marc21/ui/theme/assets/semantic-ui/less/invenio_records_marc21/deposit.less create mode 100644 invenio_records_marc21/ui/theme/assets/semantic-ui/less/invenio_records_marc21/theme.less create mode 100644 invenio_records_marc21/ui/theme/assets/semantic-ui/less/invenio_records_marc21/variables.less diff --git a/invenio_records_marc21/templates/invenio_records_marc21/base.html b/invenio_records_marc21/templates/invenio_records_marc21/base.html index caa7cfb6..f8982adc 100644 --- a/invenio_records_marc21/templates/invenio_records_marc21/base.html +++ b/invenio_records_marc21/templates/invenio_records_marc21/base.html @@ -14,4 +14,5 @@ {%- block css %} {{ super() }} +{{ webpack['invenio-records-marc21-theme.css'] }} {%- endblock css %} diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/less/invenio_records_marc21/deposit.less b/invenio_records_marc21/ui/theme/assets/semantic-ui/less/invenio_records_marc21/deposit.less new file mode 100644 index 00000000..2d912717 --- /dev/null +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/less/invenio_records_marc21/deposit.less @@ -0,0 +1,47 @@ +// This file is part of Invenio. +// +// Copyright (C) 2021 Graz University of Technology. +// +// Invenio-Records-Marc21 is free software; you can redistribute it and/or +// modify it under the terms of the MIT License; see LICENSE file for more +// details. + + +/** Deposit page styles */ +#marc21-deposit-form { + + .metadata { + border: 0px; + margin: 0px; + padding: 0px; + + + } + // Metadata specific styles + .metadata * { + font-size: @depositFontSize; + } + + .fields { + + margin: 0px -0.5rem 0px -0.5rem; + padding: 0px; + padding-bottom: .1rem; + } + + // Sidebar specific styles + .deposit-sidebar { + + .sidebar-buttons { + display: flex; + } + + .save-button { + margin-bottom: 0.5em; + } + + .preview-button { + margin-bottom: 0.5em; + } + } +} \ No newline at end of file diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/less/invenio_records_marc21/theme.less b/invenio_records_marc21/ui/theme/assets/semantic-ui/less/invenio_records_marc21/theme.less new file mode 100644 index 00000000..d22c4238 --- /dev/null +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/less/invenio_records_marc21/theme.less @@ -0,0 +1,11 @@ +// This file is part of Invenio. +// +// Copyright (C) 2021 Graz University of Technology. +// +// Invenio-Records-Marc21 is free software; you can redistribute it and/or +// modify it under the terms of the MIT License; see LICENSE file for more +// details. +/** Marc21 module page styles */ + +@import "variables.less"; +@import "deposit.less"; diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/less/invenio_records_marc21/variables.less b/invenio_records_marc21/ui/theme/assets/semantic-ui/less/invenio_records_marc21/variables.less new file mode 100644 index 00000000..b46880b0 --- /dev/null +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/less/invenio_records_marc21/variables.less @@ -0,0 +1,11 @@ +// This file is part of Invenio. +// +// Copyright (C) 2021 Graz University of Technology. +// +// Invenio-Records-Marc21 is free software; you can redistribute it and/or +// modify it under the terms of the MIT License; see LICENSE file for more +// details. +/** Marc21 module style variables */ + + +@depositFontSize: 14px; diff --git a/invenio_records_marc21/ui/theme/webpack.py b/invenio_records_marc21/ui/theme/webpack.py index 20f57a20..685a6c57 100644 --- a/invenio_records_marc21/ui/theme/webpack.py +++ b/invenio_records_marc21/ui/theme/webpack.py @@ -19,25 +19,28 @@ themes={ "semantic-ui": dict( entry={ + "invenio-records-marc21-theme": "./less/invenio_records_marc21/theme.less", "invenio-records-marc21-search": "./js/invenio_records_marc21/search/index.js", }, dependencies={ "@babel/runtime": "^7.9.0", + "clean-webpack-plugin": "4.0.0", + "react-dropzone": "^11.0.3", + "react-dnd-html5-backend": "^11.1.3", + "prismjs": "1.25.0", + "react-redux": "^7.1.0", "formik": "^2.1.4", - "luxon": "^1.23.0", "path": "^0.12.7", + "axios": "^0.21.1", # load from website data + "luxon": "^1.23.0", "prop-types": "^15.7.2", "react-dnd": "^11.1.3", - "react-dnd-html5-backend": "^11.1.3", - "react-invenio-forms": "^0.6.3", - "react-dropzone": "^11.0.3", - "yup": "^0.27.0", - "@ckeditor/ckeditor5-build-classic": "^16.0.0", - "@ckeditor/ckeditor5-react": "^2.1.0", + "marcjs": "^2.0.1", + "react-invenio-deposit": "^0.16.1", + "react-invenio-forms": "^0.8.7", }, aliases={ - "themes/marc21": "less/invenio_records_marc21/theme", - "@less/invenio_records_marc21": "less/invenio_records_marc21", + "@less/invenio_records_marc21": "./less/invenio_records_marc21", }, ), }, From 0abff9c06a4c8e1dab10a601ad55eee98196db73 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 18 Oct 2021 13:45:09 +0200 Subject: [PATCH 114/217] feature: deposit app init marc21 deposit application creating marc21 records. --- .../deposit/Marc21ApiHandler.js | 128 ++++++++++++++ .../deposit/Marc21Controller.js | 154 ++++++++++++++++ .../deposit/Marc21DepositApp.js | 72 ++++++++ .../deposit/Marc21DepositForm.js | 90 ++++++++++ .../deposit/Marc21FormHandler.js | 60 +++++++ .../deposit/Marc21RecordSerializer.js | 116 +++++++++++++ .../deposit/RecordSchema.js | 144 +++++++++++++++ .../deposit/components/AccordionField.js | 94 ++++++++++ .../deposit/components/LeaderField.js | 44 +++++ .../deposit/components/MetadataField.js | 50 ++++++ .../deposit/components/MetadataFields.js | 56 ++++++ .../deposit/components/PublishButton.js | 107 ++++++++++++ .../deposit/components/SaveButton.js | 62 +++++++ .../deposit/components/TemplateField.js | 100 +++++++++++ .../deposit/components/index.js | 14 ++ .../deposit/fields/Field.js | 39 +++++ .../deposit/fields/MetadataFields.js | 164 ++++++++++++++++++ .../deposit/fields/index.js | 10 ++ .../invenio_records_marc21/deposit/index.js | 28 +++ .../deposit/state/actions/deposit.js | 58 +++++++ .../deposit/state/actions/index.js | 10 ++ .../deposit/state/reducers/deposit.js | 65 +++++++ .../deposit/state/reducers/index.js | 14 ++ .../deposit/state/types/index.js | 23 +++ .../invenio_records_marc21/deposit/store.js | 39 +++++ .../search/components.js | 17 +- .../js/invenio_records_marc21/search/index.js | 8 +- 27 files changed, 1754 insertions(+), 12 deletions(-) create mode 100644 invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21ApiHandler.js create mode 100644 invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21Controller.js create mode 100644 invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21DepositApp.js create mode 100644 invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21DepositForm.js create mode 100644 invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21FormHandler.js create mode 100644 invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21RecordSerializer.js create mode 100644 invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/RecordSchema.js create mode 100644 invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/components/AccordionField.js create mode 100644 invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/components/LeaderField.js create mode 100644 invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/components/MetadataField.js create mode 100644 invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/components/MetadataFields.js create mode 100644 invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/components/PublishButton.js create mode 100644 invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/components/SaveButton.js create mode 100644 invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/components/TemplateField.js create mode 100644 invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/components/index.js create mode 100644 invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/fields/Field.js create mode 100644 invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/fields/MetadataFields.js create mode 100644 invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/fields/index.js create mode 100644 invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/index.js create mode 100644 invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/state/actions/deposit.js create mode 100644 invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/state/actions/index.js create mode 100644 invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/state/reducers/deposit.js create mode 100644 invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/state/reducers/index.js create mode 100644 invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/state/types/index.js create mode 100644 invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/store.js diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21ApiHandler.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21ApiHandler.js new file mode 100644 index 00000000..4935c08c --- /dev/null +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21ApiHandler.js @@ -0,0 +1,128 @@ +// This file is part of Invenio. +// +// Copyright (C) 2021 Graz University of Technology. +// +// Invenio-Records-Marc21 is free software; you can redistribute it and/or +// modify it under the terms of the MIT License; see LICENSE file for more +// details. +import axios from 'axios'; + +const CancelToken = axios.CancelToken; +const apiConfig = { + withCredentials: true, + xsrfCookieName: 'csrftoken', + xsrfHeaderName: 'X-CSRFToken', +}; +const axiosWithconfig = axios.create(apiConfig); + +/** + * API client response. + * + * It's a wrapper/sieve around Axios to contain Axios coupling here. It maps + * good and bad responses to a unified interface. + * + */ +export class Marc21ApiResponse { + constructor(data, errors, code) { + this.data = data; + this.errors = errors; + this.code = code; + } +} + +/** + * API Client for deposits. + * + * It mostly uses the API links passed to it from responses. + * + */ +export class Marc21ApiHandler{ + constructor(createUrl) { + this.createUrl = createUrl; + } + + async createResponse(axios_call) { + try { + let response = await axios_call(); + return new Marc21ApiResponse( + response.data, + response.data.errors, + response.status + ); + } catch (error) { + const err = error.response.data; + return new Marc21ApiResponse( + error.response.data, + error.response.data.errors, + error.response.status + ); + } + } + + /** + * Calls the API to create a new draft. + * + * @param {object} draft - Serialized draft + */ + async create(draft) { + return this.createResponse(() => + axiosWithconfig.post(this.createUrl, draft, { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/vnd.inveniomarc21.v1+marcxml', + }, + }) + ); + } + + /** + * Calls the API to save a pre-existing draft. + * + * @param {object} draft - Serialized draft + */ + async save(draft) { + return this.createResponse(() => + axiosWithconfig.put(draft.links.self, draft, { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/vnd.inveniomarc21.v1+marcxml', + }, + }) + ); + } + + /** + * Publishes the draft by calling its publish link. + * + * @param {object} draft - the payload from create() + */ + async publish(draft) { + return this.createResponse(() => + axiosWithconfig.post( + draft.links.publish, draft, { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/vnd.inveniomarc21.v1+marcxml', + }, + }, + ) + ); + } + + /** + * Deletes the draft by calling DELETE on its self link. + * + * @param {object} draft - the payload from create()/save() + */ + async delete(draft) { + return this.createResponse(() => + axiosWithconfig.delete( + draft.links.self, + {}, + { + headers: { 'Content-Type': 'application/json' }, + } + ) + ); + } +} diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21Controller.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21Controller.js new file mode 100644 index 00000000..76e4f46b --- /dev/null +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21Controller.js @@ -0,0 +1,154 @@ +// This file is part of Invenio. +// +// Copyright (C) 2021 Graz University of Technology. +// +// Invenio-Records-Marc21 is free software; you can redistribute it and/or +// modify it under the terms of the MIT License; see LICENSE file for more +// details. + +import { + + ACTION_CREATE_SUCCEEDED, + ACTION_SAVE_SUCCEEDED, + ACTION_SAVE_FAILED, + ACTION_PUBLISH_FAILED, + ACTION_PUBLISH_SUCCEEDED + } from './state/types'; +import _isEmpty from 'lodash/isEmpty'; +import _set from 'lodash/set'; +import _has from 'lodash/has'; + +export class Marc21Controller { + constructor(apihandler, schema) { + this.apihandler = apihandler; + this.schema = schema; + } + + + + /** + * Creates the current draft (backend) and changes URL to match its edit URL. + * + * @param {object} draft - current draft + * @param {object} store - redux store + */ + async createDraft(draft, { store }) { + const recordSerializer = store.config.recordSerializer; + const payload = recordSerializer.serialize(draft); + const response = await this.apihandler.create(payload); + + if(_has(response, ["data","ui", "metadata"])){ + _set( + response.data, + 'metadata', + response.data.ui.metadata, + ); + } + store.dispatch({ + type: ACTION_CREATE_SUCCEEDED, + payload: { data: recordSerializer.deserialize(response.data) }, + }); + + const draftURL = response.data.links.self_html; + window.history.replaceState(undefined, '', draftURL); + return response; + } + + draftAlreadyCreated(record) { + return record.id ? true : false; + } + + /** + * Saves the current draft (backend) and changes URL to match its edit URL. + * + * @param {object} draft - current draft + * @param {object} formik - the Formik object + * @param {object} store - redux store + */ + async saveDraft(draft, { formik, store }) { + const recordSerializer = store.config.recordSerializer; + // Set defaultPreview for files + draft = _set( + draft, + 'defaultFilePreview', + store.getState().deposit.defaultFilePreview + ); + + let response = {}; + let payload = recordSerializer.serialize(draft); + response = await this.createDraft(draft, { store }); + //response = await this.apihandler.save(payload); + + if(_has(response, ["data","ui", "metadata"])){ + _set( + response.data, + "metadata", + response.data.ui.metadata, + ); + } + + let data = recordSerializer.deserialize(response.data || {}); + let errors = recordSerializer.deserializeErrors(response.errors || []); + + // response 100% successful + if ( 200 <= response.code && response.code < 300 && _isEmpty(errors) ) { + store.dispatch({ + type: ACTION_SAVE_SUCCEEDED, + payload: { data }, + }); + } + // response exceptionally bad + else { + store.dispatch({ + type: ACTION_SAVE_FAILED, + payload: { errors }, + }); + formik.setErrors(errors); + } + + formik.setSubmitting(false); + } + + /** + * Publishes the current draft (backend) and redirects to its view URL. + * + * @param {object} draft - current draft + * @param {object} formik - the Formik object + * @param {object} store - redux store + */ + async publishDraft(draft, { formik, store }) { + const recordSerializer = store.config.recordSerializer; + let response = {}; + + if (!this.draftAlreadyCreated(draft)) { + response = await this.createDraft(draft, { store }); + } + + let payload = recordSerializer.serialize(draft); + response = await this.apihandler.publish(payload); + + let data = recordSerializer.deserialize(response.data || {}); + let errors = recordSerializer.deserializeErrors(response.errors || []); + + // response 100% successful + if (200 <= response.code && response.code < 300 && _isEmpty(errors)) { + store.dispatch({ + type: ACTION_PUBLISH_SUCCEEDED, + payload: { data }, + }); + const recordURL = response.data.links.self_html; + window.location.replace(recordURL); + } + // "succeed or not, there is no partial" + else { + store.dispatch({ + type: ACTION_PUBLISH_FAILED, + payload: { data, errors }, + }); + formik.setErrors(errors); + } + + formik.setSubmitting(false); + } + +} \ No newline at end of file diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21DepositApp.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21DepositApp.js new file mode 100644 index 00000000..3b4ad3ca --- /dev/null +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21DepositApp.js @@ -0,0 +1,72 @@ +// This file is part of Invenio. +// +// Copyright (C) 2021 Graz University of Technology. +// +// Invenio-Records-Marc21 is free software; you can redistribute it and/or +// modify it under the terms of the MIT License; see LICENSE file for more +// details. + +import React, { Component } from "react"; +import { Provider } from 'react-redux'; + +import { configureStore } from "./store"; +import {RecordSchema} from "./RecordSchema"; +import { Marc21Controller } from './Marc21Controller'; +import {Marc21ApiHandler} from './Marc21ApiHandler'; +import {Marc21FormHandler} from './Marc21FormHandler' +import { Marc21RecordSerializer } from './Marc21RecordSerializer'; + + +export class Marc21DepositApp extends Component { + constructor(props) { + super(props); + const fileUploader = props.fileUploader + + const schema = props.schema + ? props.schema + : new RecordSchema(props.config.link, props.config.schema); + + const apihandler = new Marc21ApiHandler(props.config.createUrl); + + const recordSerializer = props.recordSerializer + ? props.recordSerializer + : new Marc21RecordSerializer(); + + const controller = props.controller + ? props.controller + : new Marc21Controller(apihandler, schema); + + this.record_init = recordSerializer.deserialize(props.record) + + const appConfig = { + config: props.config, + record: this.record_init, + files: props.files, + schema: schema, + controller: controller, + apihandler: apihandler, + fileUploader: fileUploader, + permissions: props.permissions, + recordSerializer: recordSerializer, + }; + + const submitFormData = props.submitFormData + ? props.submitFormData + : submitFormData; + + + this.store = configureStore(appConfig); + } + + render() { + return ( + + + {this.props.children} + + + ); + } +} + +Marc21DepositApp.propTypes = {}; diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21DepositForm.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21DepositForm.js new file mode 100644 index 00000000..5b1e8139 --- /dev/null +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21DepositForm.js @@ -0,0 +1,90 @@ +// This file is part of Invenio. +// +// Copyright (C) 2021 Graz University of Technology. +// +// Invenio-Records-Marc21 is free software; you can redistribute it and/or +// modify it under the terms of the MIT License; see LICENSE file for more +// details. + +import _get from "lodash/get"; +import React, { Component, createRef } from "react"; +import {SaveButton, PublishButton} from "./components" +import {Marc21DepositApp} from "./Marc21DepositApp"; +import { AccordionField, MetadataFields } from "./components"; +import { Card, Container, Grid, Ref, Sticky } from "semantic-ui-react"; +import { TemplateField } from "./components/TemplateField"; + + +export class Marc21DepositForm extends Component { + + constructor(props) { + super(props); + this.props = props + this.config = props.config || {}; + this.templates=props.templates || []; + this.files = props.files + this.noFiles = true; + } + + sidebarRef = createRef(); + + accordionStyle = { + header: { className: "segment inverted brand" }, + }; + + render() { + return ( + + + {/* */} + + + + + + + + + {/* Sidebar start */} + + + + +
+ +
+ +
+
+ + {this.templates.length > 0 && + + } + + {/* */} +
+
+
+
+
+
+ ) + } +} diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21FormHandler.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21FormHandler.js new file mode 100644 index 00000000..de4f1cd8 --- /dev/null +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21FormHandler.js @@ -0,0 +1,60 @@ +// This file is part of Invenio. +// +// Copyright (C) 2021 Graz University of Technology. +// +// Invenio-Records-Marc21 is free software; you can redistribute it and/or +// modify it under the terms of the MIT License; see LICENSE file for more +// details. + +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { BaseForm } from "react-invenio-forms"; +import { submitFormData } from './state/actions'; + +class Marc21FormHandlerComponent extends Component { + componentDidMount() { + window.addEventListener('beforeunload', (e) => { + if (this.props.fileUploadOngoing) { + e.returnValue = ''; + return ''; + } + }); + window.addEventListener('unload', async (e) => { + // TODO: cancel all uploads + // Investigate if it's possible to wait for the deletion request to complete + // before unloading the page + }); + } + + render() { + return ( + + {this.props.children} + + ); + } +} + +const mapStateToProps = (state) => { + return { + record: state.deposit.record, + formState: state.deposit.formState, + }; + }; + + const mapDispatchToProps = (dispatch) => ({ + submitFormData: (values, formik) => dispatch(submitFormData(values, formik)), + }); + + +export const Marc21FormHandler = connect( + mapStateToProps, + mapDispatchToProps +)(Marc21FormHandlerComponent); diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21RecordSerializer.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21RecordSerializer.js new file mode 100644 index 00000000..8173654d --- /dev/null +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21RecordSerializer.js @@ -0,0 +1,116 @@ +// This file is part of Invenio. +// +// Copyright (C) 2021 Graz University of Technology. +// +// Invenio-Records-Marc21 is free software; you can redistribute it and/or +// modify it under the terms of the MIT License; see LICENSE file for more +// details. + +import _cloneDeep from 'lodash/cloneDeep'; +import _defaults from 'lodash/defaults'; +import _pick from 'lodash/pick'; +import _set from 'lodash/set'; + +import { + Field, + MetadataFields, + } from './fields'; + + +export class Marc21RecordSerializer { + constructor() { + } + + depositRecordSchema = { + files: new Field({ + fieldpath: "files", + }), + links: new Field({ + fieldpath: "links", + }), + pids: new Field({ + fieldpath: "pids", + deserializedDefault: {}, + serializedDefault: {}, + }), + metadata: new MetadataFields({ + fieldpath: "metadata", + deserializedDefault: {"leader" : "00000nam a2200000zca4500"}, + serializedDefault : "", + }), + + } + + + /** + * Deserialize backend record into format compatible with frontend. + * @method + * @param {object} record - potentially empty object + * @returns {object} frontend compatible record object + */ + deserialize(record) { + + record = _cloneDeep(record); + + let deserializedRecord = record; + deserializedRecord = _pick(deserializedRecord, [ + "access", + "metadata", + "id", + "links", + "files", + "pids", + ]); + for (let key in this.depositRecordSchema) { + deserializedRecord = this.depositRecordSchema[key].deserialize( + deserializedRecord + ); + } + return deserializedRecord; + } + + + /** + * Deserialize backend record errors into format compatible with frontend. + * @method + * @param {array} errors - array of error objects + * @returns {object} - object representing errors + */ + deserializeErrors(errors) { + let deserializedErrors = {}; + for (let e of errors) { + _set(deserializedErrors, e.field, e.messages.join(' ')); + } + + return deserializedErrors; + } + + /** + * Serialize record to send to the backend. + * @method + * @param {object} record - in frontend format + * @returns {object} record - in API format + * + */ + serialize(record) { + record = _cloneDeep(record); + let serializedRecord = record; //this.removeEmptyValues(record); + serializedRecord = _pick(serializedRecord, [ + "access", + "metadata", + "id", + "links", + "files", + "pids", + ]); + for (let key in this.depositRecordSchema) { + serializedRecord = this.depositRecordSchema[key].serialize( + serializedRecord + ); + } + + _defaults(serializedRecord, { metadata: {} }); + + return serializedRecord; + } +} \ No newline at end of file diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/RecordSchema.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/RecordSchema.js new file mode 100644 index 00000000..95b40693 --- /dev/null +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/RecordSchema.js @@ -0,0 +1,144 @@ +// This file is part of Invenio. +// +// Copyright (C) 2021 Graz University of Technology. +// +// Invenio-Records-Marc21 is free software; you can redistribute it and/or +// modify it under the terms of the MIT License; see LICENSE file for more +// details. + +import _get from "lodash/get"; +import _has from "lodash/has"; +import axios from 'axios'; + +export class RecordSchema { + + /** + * Make sure RecordSchema called first time with props.link + */ + + constructor(link, schema) { + this.schema = {} + this.link = link; + this.leader_field = "LDR"; + if (schema !== {}){ + this.setSchema(schema); + this.loaded = true; + } else { + this.loaded = false; + } + } + + isReady(){ + return this.loaded; + } + + setSchema(schema){ + this.schema = schema; + } + + loadSchema() { + axios.get(this.state.link).then( + result => { + this.loaded = true; + this.schema = result.data; + }, + // Note: it's important to handle errors here + // instead of a catch() block so that we don't swallow + // exceptions from actual bugs in components. + error => { + this.loaded = false; + this.schema = {}; + this.error = error; + } + ); + } + + isLeaderField(key){ + let keys = this.getLeaderFieldKeys(); + return keys.includes(key); + } + + isDataField(key){ + let keys = this.getDataFieldKeys(); + return keys.includes(key); + } + + hasLeaderKey(key) { + return _has(this.schema, ["fields", this.leader_field, "positions", key]) + } + + getLeaderFieldKeys() { + + let fields = this.getLeaderFields(); + let keys = [] + for (const [key, value] of Object.entries(fields)) { + keys.push(key); + } + return keys; + } + + getLeaderFields() { + return _get(this.schema, ["fields", this.leader_field, "positions"], {}); + } + + getLeaderField(key) { + const leaderfields = this.getLeaderFields(); + return leaderfields[key]; + } + + + getLeaderFieldOptions(key) { + const leaderfield = this.getLeaderField(key); + const leaderfield_codes = _get(leaderfield, ["codes"], {}); + let codes = this.generateDropdownOptions(leaderfield_codes); + return codes; + } + + getDataFields() { + return _get(this.schema, ["fields"], {}); + } + + + getDataFieldKeys() { + let fields = this.getDataFields(); + let keys = [] + for (const [key, value] of Object.entries(fields)) { + keys.push(key); + } + return keys; + } + + getDataField(key) { + const datafield = this.getDataFields(); + return datafield[key]; + } + + generateDropdownOptions(fields){ + let codes = [] + for (const [key, value] of Object.entries(fields)) { + let code = { key: key, label: key, value: key, text: value["label"]} + codes.push(code); + } + return codes; + } + + /** + * Finds tag and indicator position select options + * + * @param {string} key - tag key name + * @param {string} ind - indicator key name + */ + getDataFieldOptions(key, ind) { + let codes = [] + const datafield = this.getDataField(key); + if (datafield[ind] === null) { + return codes; + } + const datafield_codes = datafield[ind]["codes"]; + codes = this.generateDropdownOptions(datafield_codes); + + + return codes; + } + +} diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/components/AccordionField.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/components/AccordionField.js new file mode 100644 index 00000000..1bc70ff0 --- /dev/null +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/components/AccordionField.js @@ -0,0 +1,94 @@ +// This file is part of Invenio. +// +// Copyright (C) 2021 Graz University of Technology. +// +// Invenio-Records-Marc21 is free software; you can redistribute it and/or +// modify it under the terms of the MIT License; see LICENSE file for more +// details. +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { Field, FastField } from 'formik'; +import { Container, Icon, Segment } from 'semantic-ui-react'; + +export class AccordionField extends Component { + constructor(props) { + super(props); + this.state = { active: props.active || false }; + } + + iconActive = ( + + ); + + iconInactive = ( + + ); + + handleClick = (showContent) => { + this.setState({ active: !showContent }); + }; + + hasError(errors) { + if (this.props.fieldPath in errors) { + return true; + } + return false; + } + + renderAccordion = (props) => { + const { + form: { errors, status }, + } = props; + const { active } = this.state; + const hasError = status ? this.hasError(status) : this.hasError(errors); + + return ( + <> + this.handleClick(active)} + {...(hasError && { ...this.props.ui?.error })} + {...this.props.ui?.header} + > + + {active ? this.iconActive : this.iconInactive} + + + {active && this.props.children} + + + ); + }; + + render() { + const FormikField = this.props.optimized ? FastField : Field; + return ( + + ); + } +} + +AccordionField.propTypes = { + active: PropTypes.bool, + fieldPath: PropTypes.string.isRequired, + label: PropTypes.string, + required: PropTypes.bool, + ui: PropTypes.shape({ + header: PropTypes.object, + content: PropTypes.object, + error: PropTypes.object, + }), + optimized: PropTypes.bool, +}; + +AccordionField.defaultProps = { + active: false, + label: '', + required: false, + ui: { + error: { inverted: true, color: 'red', secondary: true }, + }, + optimized: false, +}; diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/components/LeaderField.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/components/LeaderField.js new file mode 100644 index 00000000..130a2443 --- /dev/null +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/components/LeaderField.js @@ -0,0 +1,44 @@ +// This file is part of Invenio. +// +// Copyright (C) 2021 Graz University of Technology. +// +// Invenio-Records-Marc21 is free software; you can redistribute it and/or +// modify it under the terms of the MIT License; see LICENSE file for more +// details. + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +import { + TextField, +} from 'react-invenio-forms'; + + +export class LeaderField extends Component { + render() { + const { fieldPath} = + this.props; + return ( + <> + + + + + ); + } +} + +LeaderField.propTypes = { + fieldPath: PropTypes.string.isRequired, + helpText: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), +}; + +LeaderField.defaultProps = { + helpText: '', +}; diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/components/MetadataField.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/components/MetadataField.js new file mode 100644 index 00000000..6ab2cd31 --- /dev/null +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/components/MetadataField.js @@ -0,0 +1,50 @@ +// This file is part of Invenio. +// +// Copyright (C) 2021 Graz University of Technology. +// +// Invenio-Records-Marc21 is free software; you can redistribute it and/or +// modify it under the terms of the MIT License; see LICENSE file for more +// details. + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; + +import { + TextField, +} from 'react-invenio-forms'; + +export class MetadataField extends Component { + render() { + const { fieldPath} = + this.props; + return ( + <> + + + + + + + ); + } +} + +MetadataField.propTypes = { + fieldPath: PropTypes.string.isRequired, + width: PropTypes.number, + helpText: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), +}; + +MetadataField.defaultProps = { + helpText: '', +}; diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/components/MetadataFields.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/components/MetadataFields.js new file mode 100644 index 00000000..bf9f63e6 --- /dev/null +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/components/MetadataFields.js @@ -0,0 +1,56 @@ +// This file is part of Invenio. +// +// Copyright (C) 2021 Graz University of Technology. +// +// Invenio-Records-Marc21 is free software; you can redistribute it and/or +// modify it under the terms of the MIT License; see LICENSE file for more +// details. + +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { ArrayField } from 'react-invenio-forms'; +import { Button, Form, Icon } from 'semantic-ui-react'; +import _get from 'lodash/get'; +import { + GroupField, +} from 'react-invenio-forms'; +import { MetadataField, LeaderField } from '.'; +export class MetadataFields extends Component { + render() { + const { fieldPath} = + this.props; + return ( + <> + + + + + {({ array, arrayHelpers, indexPath, key }) => ( + + + + + + + )} + + + ); + } +} + + +MetadataFields.propTypes = { + fieldPath: PropTypes.string, + }; + +MetadataFields.defaultProps = { + fieldPath: 'metadata', + }; + \ No newline at end of file diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/components/PublishButton.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/components/PublishButton.js new file mode 100644 index 00000000..14fe9f11 --- /dev/null +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/components/PublishButton.js @@ -0,0 +1,107 @@ +// This file is part of Invenio. +// +// Copyright (C) 2021 Graz University of Technology. +// +// Invenio-Records-Marc21 is free software; you can redistribute it and/or +// modify it under the terms of the MIT License; see LICENSE file for more +// details. +import _get from 'lodash/get'; +import _isEmpty from 'lodash/isEmpty'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { Icon, Button, Modal } from 'semantic-ui-react'; +import { ActionButton } from 'react-invenio-forms'; + +import { submitAction } from '../state/actions'; +import { FORM_PUBLISHING } from '../state/types'; + + + +export class PublishButtonComponent extends Component { + state = { confirmOpen: false }; + + handleOpen = () => this.setState({ confirmOpen: true }); + + handleClose = () => this.setState({ confirmOpen: false }); + + render() { + const { formState, publishClick, numberOfFiles, errors, ...uiProps } = + this.props; + + const handlePublish = (event, formik) => { + publishClick(event, formik); + this.handleClose(); + }; + + + const isDisabled = (formik) => { + return formik.isSubmitting; + }; + + const action = "publish"; + const capitalizedAction ="Publish"; + return ( + <> + + {(formik) => ( + <> + {formik.isSubmitting && formState === FORM_PUBLISHING ? ( + + ) : ( + + )} + {capitalizedAction} + + )} + + {this.state.confirmOpen && ( + + +

+ {(`Are you sure you want to publish this record?`)} +

+
+ + + + +
+ )} + + ); + } +} + +const mapStateToProps = (state) => ({ + formState: state.deposit.formState, + errors: state.deposit.errors, +}); + +const mapDispatchToProps = (dispatch) => ({ + publishClick: (event, formik) => + dispatch(submitAction(FORM_PUBLISHING, event, formik)), +}); + +export const PublishButton = connect( + mapStateToProps, + mapDispatchToProps +)(PublishButtonComponent); diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/components/SaveButton.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/components/SaveButton.js new file mode 100644 index 00000000..67d703f1 --- /dev/null +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/components/SaveButton.js @@ -0,0 +1,62 @@ +// This file is part of Invenio. +// +// Copyright (C) 2021 Graz University of Technology. +// +// Invenio-Records-Marc21 is free software; you can redistribute it and/or +// modify it under the terms of the MIT License; see LICENSE file for more +// details. + +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { Icon } from 'semantic-ui-react'; +import { ActionButton } from 'react-invenio-forms'; + +import { submitAction } from '../state/actions'; +import { FORM_SAVING } from '../state/types'; + +export class SaveButtonComponent extends Component { + + isDisabled = (formik) => { + return formik.isSubmitting; + }; + + render() { + const { formState, saveClick, ...uiProps } = this.props; + + return ( + + {(formik) => ( +
+ { ( formik.isSubmitting && formState === FORM_SAVING ) ? ( + + ) : ( + + )} + Save draft +
+ )} +
+ ); + } +} + +const mapStateToProps = (state) => ({ + formState: state.deposit.formState, +}); + +const mapDispatchToProps = (dispatch) => ({ + saveClick: (event, formik) => + dispatch(submitAction(FORM_SAVING, event, formik)), +}); + +export const SaveButton = connect( + mapStateToProps, + mapDispatchToProps +)(SaveButtonComponent); diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/components/TemplateField.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/components/TemplateField.js new file mode 100644 index 00000000..74ded26a --- /dev/null +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/components/TemplateField.js @@ -0,0 +1,100 @@ +// This file is part of Invenio. +// +// Copyright (C) 2021 Graz University of Technology. +// +// Invenio-Records-Marc21 is free software; you can redistribute it and/or +// modify it under the terms of the MIT License; see LICENSE file for more +// details. + +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import { FastField } from 'formik'; +import _set from 'lodash/set'; +import { FieldLabel } from 'react-invenio-forms'; +import { Card, Form, Button } from 'semantic-ui-react'; + +class TemplateFieldComponent extends Component { + constructor(props) { + super(props); + this.formik = props.formik; + } + + setTemplate(template) { + this.formik.form.setValues(template); + } + + + render() { + const { + fieldPath, + templates, + label, + labelIcon, + } = this.props; + return ( + + + + + + {templates.map((value, index, array) => { + const arrayPath = fieldPath; + const indexPath = index; + const key = `${arrayPath}.${indexPath}`; + return ( + <> + + + ); + })} + + + + + + ); + } +} + +class FormikTemplateField extends Component { + render() { + + return ( + ( + + )} + /> + ); + } +} + +var TemplateJson = PropTypes.shape({ + active: PropTypes.string, + name: PropTypes.string.isRequired, + values: PropTypes.object.isRequired, + created_at: PropTypes.string, + }) + +FormikTemplateField.propTypes = { + fieldPath: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + labelIcon: PropTypes.string, + templates: PropTypes.arrayOf(TemplateJson.isRequired).isRequired, +}; + +FormikTemplateField.defaultProps = { + fieldPath: 'template', +}; + +const mapStateToProps = (state) => ({ +}); + +export const TemplateField = connect( + mapStateToProps, + null +)(FormikTemplateField); diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/components/index.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/components/index.js new file mode 100644 index 00000000..5cb2d6e9 --- /dev/null +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/components/index.js @@ -0,0 +1,14 @@ +// This file is part of Invenio. +// +// Copyright (C) 2021 Graz University of Technology. +// +// Invenio-Records-Marc21 is free software; you can redistribute it and/or +// modify it under the terms of the MIT License; see LICENSE file for more +// details. + +export { AccordionField } from './AccordionField'; +export { MetadataFields } from './MetadataFields'; +export { MetadataField } from './MetadataField'; +export { LeaderField } from './LeaderField'; +export { SaveButton } from './SaveButton'; +export { PublishButton } from './PublishButton'; \ No newline at end of file diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/fields/Field.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/fields/Field.js new file mode 100644 index 00000000..2e58ac02 --- /dev/null +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/fields/Field.js @@ -0,0 +1,39 @@ +// This file is part of Invenio. +// +// Copyright (C) 2021 Graz University of Technology. +// +// Invenio-Records-Marc21 is free software; you can redistribute it and/or +// modify it under the terms of the MIT License; see LICENSE file for more +// details. + +import _get from 'lodash/get'; +import _set from 'lodash/set'; +import _cloneDeep from 'lodash/cloneDeep'; + +export class Field { + constructor({ + fieldpath, + deserializedDefault = null, + serializedDefault = null, + }) { + this.fieldpath = fieldpath; + this.deserializedDefault = deserializedDefault; + this.serializedDefault = serializedDefault; + } + + deserialize(record) { + let fieldValue = _get(record, this.fieldpath, this.deserializedDefault); + if (fieldValue !== null) { + return _set(_cloneDeep(record), this.fieldpath, fieldValue); + } + return record; + } + + serialize(record) { + let fieldValue = _get(record, this.fieldpath, this.serializedDefault); + if (fieldValue !== null) { + return _set(_cloneDeep(record), this.fieldpath, fieldValue); + } + return record; + } +} \ No newline at end of file diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/fields/MetadataFields.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/fields/MetadataFields.js new file mode 100644 index 00000000..6505841b --- /dev/null +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/fields/MetadataFields.js @@ -0,0 +1,164 @@ +// This file is part of Invenio. +// +// Copyright (C) 2021 Graz University of Technology. +// +// Invenio-Records-Marc21 is free software; you can redistribute it and/or +// modify it under the terms of the MIT License; see LICENSE file for more +// details. + +import _cloneDeep from 'lodash/cloneDeep'; +import _get from 'lodash/get'; +import _has from 'lodash/has'; +import _pick from 'lodash/pick'; +import _set from 'lodash/set'; +import { Field } from './Field'; + +const { Marc, Record } = require('marcjs'); +export class MetadataFields extends Field { + + constructor({ + fieldpath, + deserializedDefault = [], + serializedDefault = [], + }) { + super({fieldpath, deserializedDefault, serializedDefault}) + }; + + + /** + * Compare Metadata field order for sorting in frontend. + * @method + * @param {object} field1 - first field to compare + * @returns {object} field1 - second field to compare + */ + compare(field1, field2) { + if (field1.id < field2.id) { + return -1; + } + if (field1.id > field2.id) { + return 1; + } + return 0; + } + + + /** + * Deserialize backend record into format compatible with frontend. + * @method + * @param {object} record - potentially empty object + * @returns {object} frontend compatible record object + */ + deserialize(record) { + let deserializedRecord = record; + deserializedRecord = _pick(deserializedRecord, [ + 'metadata', + 'id', + 'links', + 'files', + 'pids', + ]); + for (let key in this.depositRecordSchema) { + deserializedRecord = this.depositRecordSchema[key].deserialize( + deserializedRecord, + this.defaultLocale + ); + } + return deserializedRecord; + } + + + static _deserialize_subfields(subfields) { + let field = ""; // subfields.join(" "); + for( let i = 0; i < subfields.length; i++) { + for (const [key, value] of Object.entries(subfields[i])) { + field += " $$" + key + " " + value ; + } + } + return field; + } + + _deserialize_fields(fields) { + let metadata = [] + for (const field of Object.values(fields)) { + const key = Object.keys(field)[0]; + const value = Object.values(field)[0]; + let internal = {id : key, ind1: value["ind1"], ind2: value["ind2"], subfield: MetadataFields._deserialize_subfields(value["subfields"])}; + metadata.push(internal); + } + return metadata; + } + + /** + * Deserialize backend field into format compatible with frontend using + * the given schema. + * @method + * @param {object} element - potentially empty object + * @returns {object} frontend compatible element object + */ + deserialize(record) { + record = _cloneDeep(record) + + let marcxml = _get(record, this.fieldpath) + + let record_dict; + + if (marcxml != null) { + const marcjs = Marc.parse(marcxml, 'marcxml'); + record_dict = marcjs.mij(); + record_dict.fields = this._deserialize_fields(record_dict.fields); + record_dict.fields .sort(this.compare); + } else { + record_dict = this.deserializedDefault + } + + + return _set(record, this.fieldpath, record_dict) + } + + + static _serialize_subfields(subfields) { + let fields = []; + subfields = subfields.trim(); + const subfield_list = subfields.split("$$").filter(String); + + for( let i = 0; i < subfield_list.length; i++) { + const subfield = subfield_list[i].split(" "); + fields = fields.concat([subfield[0], subfield.slice(1).join(" ")]) + } + return fields; + } + + _serialize_fields(marc_record, fields) { + let metadata = [] + for (const field of Object.values(fields)) { + const subfields = MetadataFields._serialize_subfields(field["subfield"]) + let internal = [field["id"], field["ind1"] + field["ind2"]].concat(subfields); + + marc_record.append(internal); + } + return metadata; + } + + /** + * Serialize element to send to the backend. + * @method + * @param {object} element - in frontend format + * @returns {object} element - in API format + * + */ + serialize(record) { + let record_dict = _get(record, this.fieldpath, this.serializedDefault) + let metadata = new Record(); + if(_has(record_dict, ["leader"])){ + metadata.leader = record_dict.leader; + } else { + metadata.leader = ""; + } + if(_has(record_dict, ["fields"])){ + record_dict.fields = this._serialize_fields(metadata, record_dict.fields); + } + const marcxml = {"xml": metadata.as('marcxml')}; + + return _set(_cloneDeep(record),this.fieldpath, marcxml); + } +} diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/fields/index.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/fields/index.js new file mode 100644 index 00000000..85462e9e --- /dev/null +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/fields/index.js @@ -0,0 +1,10 @@ +// This file is part of Invenio. +// +// Copyright (C) 2021 Graz University of Technology. +// +// Invenio-Records-Marc21 is free software; you can redistribute it and/or +// modify it under the terms of the MIT License; see LICENSE file for more +// details. + +export { Field } from './Field'; +export { MetadataFields } from './MetadataFields'; \ No newline at end of file diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/index.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/index.js new file mode 100644 index 00000000..67662bba --- /dev/null +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/index.js @@ -0,0 +1,28 @@ +// This file is part of Invenio. +// +// Copyright (C) 2021 Graz University of Technology. +// +// Invenio-Records-Marc21 is free software; you can redistribute it and/or +// modify it under the terms of the MIT License; see LICENSE file for more +// details. + + +import React from "react"; +import ReactDOM from "react-dom"; +import "semantic-ui-css/semantic.min.css"; + +import { getInputFromDOM } from "react-invenio-deposit"; +import { Marc21DepositForm } from "./Marc21DepositForm"; + + + +ReactDOM.render( + , + document.getElementById("marc21-deposit-form") + ); diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/state/actions/deposit.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/state/actions/deposit.js new file mode 100644 index 00000000..33fa7a52 --- /dev/null +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/state/actions/deposit.js @@ -0,0 +1,58 @@ +// This file is part of Invenio. +// +// Copyright (C) 2021 Graz University of Technology. +// +// Invenio-Records-Marc21 is free software; you can redistribute it and/or +// modify it under the terms of the MIT License; see LICENSE file for more +// details. + +import { + FORM_SAVING, + FORM_PUBLISHING, + FORM_ACTION_EVENT_EMITTED, + } from '../types'; + +export const save = (record, formik) => { + return async (dispatch, getState, config) => { + const controller = config.controller; + controller.saveDraft(record, { + formik, + store: { dispatch, getState, config }, + }); + }; + }; + +export const publish = (record, formik) => { + return (dispatch, getState, config) => { + const controller = config.controller; + return controller.publishDraft(record, { + formik, + store: { dispatch, getState, config }, + }); + }; +}; + + +export const submitAction = (action, event, formik) => { + return async (dispatch, getState, config) => { + dispatch({ + type: FORM_ACTION_EVENT_EMITTED, + payload: action, + }); + formik.handleSubmit(event); // eventually calls submitFormData below + }; +}; + +export const submitFormData = (record, formik) => { + return (dispatch, getState, config) => { + const formState = getState().deposit.formState; + switch (formState) { + case FORM_SAVING: + return dispatch(save(record, formik)); + case FORM_PUBLISHING: + return dispatch(publish(record, formik)); + default: + console.log(`onSubmit triggered with unknown action ${formState}`); + } + }; +}; \ No newline at end of file diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/state/actions/index.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/state/actions/index.js new file mode 100644 index 00000000..e255e7ca --- /dev/null +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/state/actions/index.js @@ -0,0 +1,10 @@ +// This file is part of Invenio. +// +// Copyright (C) 2021 Graz University of Technology. +// +// Invenio-Records-Marc21 is free software; you can redistribute it and/or +// modify it under the terms of the MIT License; see LICENSE file for more +// details. + +export * from './deposit'; + diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/state/reducers/deposit.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/state/reducers/deposit.js new file mode 100644 index 00000000..fe1640b1 --- /dev/null +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/state/reducers/deposit.js @@ -0,0 +1,65 @@ +// This file is part of Invenio. +// +// Copyright (C) 2021 Graz University of Technology. +// +// Invenio-Records-Marc21 is free software; you can redistribute it and/or +// modify it under the terms of the MIT License; see LICENSE file for more +// details. + +import { + FORM_ACTION_EVENT_EMITTED, + ACTION_CREATE_SUCCEEDED, + FORM_SAVE_SUCCEEDED, + FORM_SAVE_FAILED, + FORM_PUBLISH_FAILED, + FORM_PUBLISH_SUCCEEDED, + ACTION_SAVE_SUCCEEDED, + ACTION_SAVE_FAILED, + ACTION_PUBLISH_FAILED, + ACTION_PUBLISH_SUCCEEDED +} from '../types'; + + +export default (state = {}, action) => { + switch (action.type) { + case FORM_ACTION_EVENT_EMITTED: + return { + ...state, + formState: action.payload, + }; + case ACTION_CREATE_SUCCEEDED: + return { + ...state, + record: { ...state.record, ...action.payload.data }, + formState: null, + }; + case ACTION_SAVE_SUCCEEDED: + return { + ...state, + record: { ...state.record, ...action.payload.data }, + errors: {}, + formState: FORM_SAVE_SUCCEEDED, + }; + case ACTION_SAVE_FAILED: + return { + ...state, + errors: { ...action.payload.errors }, + formState: FORM_SAVE_FAILED, + }; + case ACTION_PUBLISH_SUCCEEDED: + return { + ...state, + record: { ...state.record, ...action.payload.data }, + formState: FORM_PUBLISH_SUCCEEDED, + }; + case ACTION_PUBLISH_FAILED: + return { + ...state, + errors: { ...action.payload.errors }, + formState: FORM_PUBLISH_FAILED, + }; + default: + return state; + } + }; + \ No newline at end of file diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/state/reducers/index.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/state/reducers/index.js new file mode 100644 index 00000000..4218a2b7 --- /dev/null +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/state/reducers/index.js @@ -0,0 +1,14 @@ +// This file is part of Invenio. +// +// Copyright (C) 2021 Graz University of Technology. +// +// Invenio-Records-Marc21 is free software; you can redistribute it and/or +// modify it under the terms of the MIT License; see LICENSE file for more +// details. + +import { combineReducers } from 'redux'; + +import depositReducer from './deposit'; +export default combineReducers({ + deposit: depositReducer, +}); diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/state/types/index.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/state/types/index.js new file mode 100644 index 00000000..fd3a2cce --- /dev/null +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/state/types/index.js @@ -0,0 +1,23 @@ +// This file is part of Invenio. +// +// Copyright (C) 2021 Graz University of Technology. +// +// Invenio-Records-Marc21 is free software; you can redistribute it and/or +// modify it under the terms of the MIT License; see LICENSE file for more +// details. + +export const FORM_SAVING = "FORM_SAVING" +export const FORM_ACTION_EVENT_EMITTED = 'FORM_ACTION_EVENT_EMITTED'; +export const FORM_SAVE_SUCCEEDED = "FORM_SAVE_SUCCEEDED"; +export const FORM_SAVE_FAILED = "FORM_SAVE_FAILED"; + +export const FORM_PUBLISHING = "FORM_PUBLISHING"; +export const FORM_PUBLISH_FAILED = 'FORM_PUBLISH_FAILED'; +export const FORM_PUBLISH_SUCCEEDED = 'FORM_PUBLISH_SUCCEEDED'; + +export const ACTION_CREATE_SUCCEEDED = "ACTION_CREATE_SUCCEEDED" +export const ACTION_SAVE_FAILED = 'ACTION_SAVE_FAILED'; +export const ACTION_SAVE_SUCCEEDED = "ACTION_SAVE_SUCCEEDED" + +export const ACTION_PUBLISH_SUCCEEDED = 'ACTION_PUBLISH_SUCCEEDED'; +export const ACTION_PUBLISH_FAILED = 'ACTION_PUBLISH_FAILED'; diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/store.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/store.js new file mode 100644 index 00000000..7c9ec3c2 --- /dev/null +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/store.js @@ -0,0 +1,39 @@ +// This file is part of Invenio. +// +// Copyright (C) 2021 Graz University of Technology. +// +// Invenio-Records-Marc21 is free software; you can redistribute it and/or +// modify it under the terms of the MIT License; see LICENSE file for more +// details. + +import _cloneDeep from 'lodash/cloneDeep'; +import _get from 'lodash/get'; +import { createStore, compose, applyMiddleware } from "redux"; +import thunk from 'redux-thunk'; +import rootReducer from './state/reducers'; + +export const INITIAL_STORE_STATE = { + formState: null, +}; + +export function configureStore(appConfig) { + const { record, files, config, permissions, ...extra } = appConfig; + const initialDepositState = { + record, + config, + permissions, + formState: null, + }; + const preloadedState = { + deposit: initialDepositState, + }; + + const composeEnhancers = + window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; + return createStore( + rootReducer, + preloadedState, + composeEnhancers(applyMiddleware(thunk.withExtraArgument(extra))) + ); + } + \ No newline at end of file diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/components.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/components.js index 7dce9795..a6e3c027 100644 --- a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/components.js +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/components.js @@ -7,6 +7,9 @@ // details. import React, { useState } from "react"; +import ReactDOM from "react-dom"; +import _camelCase from "lodash/camelCase"; +import _truncate from "lodash/truncate"; import { Button, Card, @@ -19,14 +22,10 @@ import { } from "semantic-ui-react"; import { BucketAggregation } from "react-searchkit"; import _get from "lodash/get"; -import _truncate from "lodash/truncate"; +import { loadComponents } from "@js/invenio_theme/templates"; import Overridable from "react-overridable"; import { SearchBar, SearchApp } from "@js/invenio_search_ui/components"; -import _camelCase from "lodash/camelCase"; -import { loadComponents } from "@js/invenio_theme/templates"; -import ReactDOM from "react-dom"; - export const Marc21RecordResultsListItem = ({ result, index }) => { const createdDate = _get( @@ -46,7 +45,7 @@ export const Marc21RecordResultsListItem = ({ result, index }) => { const access_icon = _get(access, "icon", "unlock"); const metadata = _get(result, ["ui", "metadata"], []); - const description = _get(metadata, ["summary", "summary"], "No description"); + const description = _get(metadata, ["summary","summary"], "No description"); const subjects = _get(metadata, "subject_added_entry_topical_term", []); const publication = _get(metadata, ["production_publication_distribution_manufacture_and_copyright_notice"], []); @@ -110,7 +109,7 @@ export const Marc21RecordResultsListItem = ({ result, index }) => { export const Marc21RecordResultsGridItem = ({ result, index }) => { const metadata = _get(result, ["ui", "metadata", "json"], []); - const description = _get(metadata, ["summary", "summary"], "No description"); + const description = _get(metadata, ["summary", "0", "summary"], "No description"); return ( @@ -271,10 +270,10 @@ export const Marc21CountComponent = ({ totalResults }) => { }; -export function createMarc21SearchAppInit( +export function createSearchAppInit( defaultComponents, autoInit = true, - autoInitDataAttr = "invenio-records-marc21-search-config" + autoInitDataAttr = "invenio-search-config" ) { const initSearchApp = (rootElement) => { const config = JSON.parse( diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/index.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/index.js index 34126f9d..f65fdc2b 100644 --- a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/index.js +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/index.js @@ -1,8 +1,10 @@ -// This file is part of Invenio-Records-Marc21. +// This file is part of Invenio. +// // Copyright (C) 2021 Graz University of Technology. // -// Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +// modify it under the terms of the MIT License; see LICENSE file for more +// details. import { createSearchAppInit } from "@js/invenio_search_ui"; From 03d73a6fe79ba045077c3344c8dde6b3dbfc4521 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 18 Oct 2021 13:48:38 +0200 Subject: [PATCH 115/217] feature: init depsit app create deposit app and initialisation with webpack build. --- .../marc21/marc21-structure-v1.0.0.json | 23248 ++++++++++++++++ .../invenio_records_marc21/deposit.html | 36 + invenio_records_marc21/ui/theme/__init__.py | 3 +- invenio_records_marc21/ui/theme/deposit.py | 69 + invenio_records_marc21/ui/theme/views.py | 14 + invenio_records_marc21/ui/theme/webpack.py | 1 + 6 files changed, 23370 insertions(+), 1 deletion(-) create mode 100644 invenio_records_marc21/records/jsonschemas/marc21/marc21-structure-v1.0.0.json create mode 100644 invenio_records_marc21/templates/invenio_records_marc21/deposit.html create mode 100644 invenio_records_marc21/ui/theme/deposit.py diff --git a/invenio_records_marc21/records/jsonschemas/marc21/marc21-structure-v1.0.0.json b/invenio_records_marc21/records/jsonschemas/marc21/marc21-structure-v1.0.0.json new file mode 100644 index 00000000..3c4029d1 --- /dev/null +++ b/invenio_records_marc21/records/jsonschemas/marc21/marc21-structure-v1.0.0.json @@ -0,0 +1,23248 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "id": "local://marc21/marc21-structure-v1.0.0.json", + "title": "Invenio Records Marc21 Structure v1.0.0", + "description": "MARC 21 Format for Bibliographic Data.", + "url": "https://www.loc.gov/marc/bibliographic/", + "additionalProperties": false, + "type": "object", + "fields": { + "LDR": { + "repeatable": false, + "positions": { + "00-04": { + "label": "Record length", + "url": "https://www.loc.gov/marc/bibliographic/bdleader.html" + }, + "05": { + "label": "Record status", + "url": "https://www.loc.gov/marc/bibliographic/bdleader.html", + "codes": { + "a": { + "label": "Increase in encoding level" + }, + "c": { + "label": "Corrected or revised" + }, + "d": { + "label": "Deleted" + }, + "n": { + "label": "New" + }, + "p": { + "label": "Increase in encoding level from prepublication" + } + } + }, + "06": { + "label": "Type of record", + "url": "https://www.loc.gov/marc/bibliographic/bdleader.html", + "codes": { + "a": { + "label": "Language material" + }, + "c": { + "label": "Notated music" + }, + "d": { + "label": "Manuscript notated music" + }, + "e": { + "label": "Cartographic material" + }, + "f": { + "label": "Manuscript cartographic material" + }, + "g": { + "label": "Projected medium" + }, + "i": { + "label": "Nonmusical sound recording" + }, + "j": { + "label": "Musical sound recording" + }, + "k": { + "label": "Two-dimensional nonprojectable graphic" + }, + "m": { + "label": "Computer file" + }, + "o": { + "label": "Kit" + }, + "p": { + "label": "Mixed materials" + }, + "r": { + "label": "Three-dimensional artifact or naturally occurring object" + }, + "t": { + "label": "Manuscript language material" + } + }, + "historical-codes": { + "b": { + "label": "Archival and manuscripts control [OBSOLETE, 1995]" + }, + "h": { + "label": "Microform publications [OBSOLETE, 1972] [USMARC only]" + }, + "n": { + "label": "Special instructional material" + } + } + }, + "07": { + "label": "Bibliographic level", + "url": "https://www.loc.gov/marc/bibliographic/bdleader.html", + "codes": { + "a": { + "label": "Monographic component part" + }, + "b": { + "label": "Serial component part" + }, + "c": { + "label": "Collection" + }, + "d": { + "label": "Subunit" + }, + "i": { + "label": "Integrating resource" + }, + "m": { + "label": "Monograph/Item" + }, + "s": { + "label": "Serial" + } + }, + "historical-codes": { + "p": { + "label": "Pamphlet [OBSOLETE, 1988] [CAN/MARC only]" + } + } + }, + "08": { + "label": "Type of control", + "url": "https://www.loc.gov/marc/bibliographic/bdleader.html", + "codes": { + " ": { + "label": "No specified type" + }, + "a": { + "label": "Archival" + } + } + }, + "09": { + "label": "Character coding scheme", + "url": "https://www.loc.gov/marc/bibliographic/bdleader.html", + "codes": { + " ": { + "label": "MARC-8" + }, + "a": { + "label": "UCS/Unicode" + } + } + }, + "10": { + "label": "Indicator count", + "url": "https://www.loc.gov/marc/bibliographic/bdleader.html" + }, + "11": { + "label": "Subfield code count", + "url": "https://www.loc.gov/marc/bibliographic/bdleader.html" + }, + "12-16": { + "label": "Base address of data", + "url": "https://www.loc.gov/marc/bibliographic/bdleader.html" + }, + "17": { + "label": "Encoding level", + "url": "https://www.loc.gov/marc/bibliographic/bdleader.html", + "codes": { + " ": { + "label": "Full level" + }, + "1": { + "label": "Full level, material not examined" + }, + "2": { + "label": "Less-than-full level, material not examined" + }, + "3": { + "label": "Abbreviated level" + }, + "4": { + "label": "Core level" + }, + "5": { + "label": "Partial (preliminary) level" + }, + "7": { + "label": "Minimal level" + }, + "8": { + "label": "Prepublication level" + }, + "u": { + "label": "Unknown" + }, + "z": { + "label": "Not applicable" + } + }, + "historical-codes": { + "0": { + "label": "Full level with item [OBSOLETE, 1997] [CAN/MARC only]" + }, + "6": { + "label": "Minimal level [OBSOLETE, 1997] [CAN/MARC only]" + } + } + }, + "18": { + "label": "Descriptive cataloging form", + "url": "https://www.loc.gov/marc/bibliographic/bdleader.html", + "codes": { + " ": { + "label": "Non-ISBD" + }, + "a": { + "label": "AACR 2" + }, + "c": { + "label": "ISBD punctuation omitted" + }, + "i": { + "label": "ISBD punctuation included" + }, + "n": { + "label": "Non-ISBD punctuation omitted" + }, + "u": { + "label": "Unknown" + } + }, + "historical-codes": { + "p": { + "label": "Record is in partial ISBD form [OBSOLETE, 1987]" + }, + "r": { + "label": "Record is in provisional form [OBSOLETE, 1981]" + } + } + }, + "19": { + "label": "Multipart resource record level", + "url": "https://www.loc.gov/marc/bibliographic/bdleader.html", + "codes": { + " ": { + "label": "Not specified or not applicable" + }, + "a": { + "label": "Set" + }, + "b": { + "label": "Part with independent title" + }, + "c": { + "label": "Part with dependent title" + } + }, + "historical-codes": { + "r": { + "label": "Linked record requirement [OBSOLETE, 2007]" + }, + "2": { + "label": "Open entry for a collection [OBSOLETE, 1984] [CAN/MARC only]" + } + } + }, + "20": { + "label": "Length of the length-of-field portion", + "url": "https://www.loc.gov/marc/bibliographic/bdleader.html" + }, + "21": { + "label": "Length of the starting-character-position portion", + "url": "https://www.loc.gov/marc/bibliographic/bdleader.html" + }, + "22": { + "label": "Length of the implementation-defined portion", + "url": "https://www.loc.gov/marc/bibliographic/bdleader.html" + } + } + }, + "001": { + "tag": "001", + "label": "Control Number", + "repeatable": false + }, + "003": { + "tag": "003", + "label": "Control Number Identifier", + "repeatable": false + }, + "005": { + "tag": "005", + "label": "Date and Time of Latest Transaction", + "repeatable": false + }, + "006": { + "tag": "006", + "label": "Additional Material Characteristics", + "repeatable": true, + "types": { + "All Materials": { + "positions": { + "00": { + "label": "Form of material", + "url": "https://www.loc.gov/marc/bibliographic/bd006.html", + "codes": { + "a": { + "label": "Language material" + }, + "c": { + "label": "Notated music" + }, + "d": { + "label": "Manuscript notated music" + }, + "e": { + "label": "Cartographic material" + }, + "f": { + "label": "Manuscript cartographic material" + }, + "g": { + "label": "Projected medium" + }, + "i": { + "label": "Nonmusical sound recording" + }, + "j": { + "label": "Musical sound recording" + }, + "k": { + "label": "Two-dimensional nonprojectable graphic" + }, + "m": { + "label": "Computer file" + }, + "o": { + "label": "Kit" + }, + "p": { + "label": "Mixed materials" + }, + "r": { + "label": "Three-dimensional artifact or naturally occurring object" + }, + "s": { + "label": "Serial/Integrating resource" + }, + "t": { + "label": "Manuscript language material" + } + } + } + } + }, + "Books": { + "positions": { + "01-04": { + "label": "Illustrations", + "url": "https://www.loc.gov/marc/bibliographic/bd006.html", + "codes": { + " ": { + "label": "No illustrations" + }, + "a": { + "label": "Illustrations" + }, + "b": { + "label": "Maps" + }, + "c": { + "label": "Portraits" + }, + "d": { + "label": "Charts" + }, + "e": { + "label": "Plans" + }, + "f": { + "label": "Plates" + }, + "g": { + "label": "Music" + }, + "h": { + "label": "Facsimiles" + }, + "i": { + "label": "Coats of arms" + }, + "j": { + "label": "Genealogical tables" + }, + "k": { + "label": "Forms" + }, + "l": { + "label": "Samples" + }, + "m": { + "label": "Phonodisc, phonowire, etc." + }, + "o": { + "label": "Photographs" + }, + "p": { + "label": "Illuminations" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "05": { + "label": "Target audience", + "url": "https://www.loc.gov/marc/bibliographic/bd006.html", + "codes": { + " ": { + "label": "Unknown or not specified" + }, + "a": { + "label": "Preschool" + }, + "b": { + "label": "Primary" + }, + "c": { + "label": "Pre-adolescent" + }, + "d": { + "label": "Adolescent" + }, + "e": { + "label": "Adult" + }, + "f": { + "label": "Specialized" + }, + "g": { + "label": "General" + }, + "j": { + "label": "Juvenile" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "06": { + "label": "Form of item", + "url": "https://www.loc.gov/marc/bibliographic/bd006.html", + "codes": { + " ": { + "label": "None of the following" + }, + "a": { + "label": "Microfilm" + }, + "b": { + "label": "Microfiche" + }, + "c": { + "label": "Microopaque" + }, + "d": { + "label": "Large print" + }, + "f": { + "label": "Braille" + }, + "o": { + "label": "Online" + }, + "q": { + "label": "Direct electronic" + }, + "r": { + "label": "Regular print reproduction" + }, + "s": { + "label": "Electronic" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "07-10": { + "label": "Nature of contents", + "url": "https://www.loc.gov/marc/bibliographic/bd006.html", + "codes": { + " ": { + "label": "No specified nature of contents" + }, + "a": { + "label": "Abstracts/summaries" + }, + "b": { + "label": "Bibliographies" + }, + "c": { + "label": "Catalogs" + }, + "d": { + "label": "Dictionaries" + }, + "e": { + "label": "Encyclopedias" + }, + "f": { + "label": "Handbooks" + }, + "g": { + "label": "Legal articles" + }, + "i": { + "label": "Indexes" + }, + "j": { + "label": "Patent document" + }, + "k": { + "label": "Discographies" + }, + "l": { + "label": "Legislation" + }, + "m": { + "label": "Theses" + }, + "n": { + "label": "Surveys of literature in a subject area" + }, + "o": { + "label": "Reviews" + }, + "p": { + "label": "Programmed texts" + }, + "q": { + "label": "Filmographies" + }, + "r": { + "label": "Directories" + }, + "s": { + "label": "Statistics" + }, + "t": { + "label": "Technical reports" + }, + "u": { + "label": "Standards/specifications" + }, + "v": { + "label": "Legal cases and case notes" + }, + "w": { + "label": "Law reports and digests" + }, + "y": { + "label": "Yearbooks" + }, + "z": { + "label": "Treaties" + }, + "2": { + "label": "Offprints" + }, + "5": { + "label": "Calendars" + }, + "6": { + "label": "Comics/graphic novels" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "11": { + "label": "Government publication", + "url": "https://www.loc.gov/marc/bibliographic/bd006.html", + "codes": { + " ": { + "label": "Not a government publication" + }, + "a": { + "label": "Autonomous or semi-autonomous component" + }, + "c": { + "label": "Multilocal" + }, + "f": { + "label": "Federal/national" + }, + "i": { + "label": "International intergovernmental" + }, + "l": { + "label": "Local" + }, + "m": { + "label": "Multistate" + }, + "o": { + "label": "Government publication-level undetermined" + }, + "s": { + "label": "State, provincial, territorial, dependent, etc." + }, + "u": { + "label": "Unknown if item is government publication" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "12": { + "label": "Conference publication", + "url": "https://www.loc.gov/marc/bibliographic/bd006.html", + "codes": { + "0": { + "label": "Not a conference publication" + }, + "1": { + "label": "Conference publication" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "13": { + "label": "Festschrift", + "url": "https://www.loc.gov/marc/bibliographic/bd006.html", + "codes": { + "0": { + "label": "Not a festschrift" + }, + "1": { + "label": "Festschrift" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "14": { + "label": "Index", + "url": "https://www.loc.gov/marc/bibliographic/bd006.html", + "codes": { + "0": { + "label": "No index" + }, + "1": { + "label": "Index present" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "16": { + "label": "Literary form", + "url": "https://www.loc.gov/marc/bibliographic/bd006.html", + "codes": { + "0": { + "label": "Not fiction (not further specified)" + }, + "1": { + "label": "Fiction (not further specified)" + }, + "d": { + "label": "Dramas" + }, + "e": { + "label": "Essays" + }, + "f": { + "label": "Novels" + }, + "h": { + "label": "Humor, satires, etc." + }, + "i": { + "label": "Letters" + }, + "j": { + "label": "Short stories" + }, + "m": { + "label": "Mixed forms" + }, + "p": { + "label": "Poetry" + }, + "s": { + "label": "Speeches" + }, + "u": { + "label": "Unknown" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "17": { + "label": "Biography", + "url": "https://www.loc.gov/marc/bibliographic/bd006.html", + "codes": { + " ": { + "label": "No biographical material" + }, + "a": { + "label": "Autobiography" + }, + "b": { + "label": "Individual biography" + }, + "c": { + "label": "Collective biography" + }, + "d": { + "label": "Contains biographical information" + }, + "|": { + "label": "No attempt to code" + } + } + } + } + }, + "Continuing Resources": { + "positions": { + "01": { + "label": "Frequency", + "url": "https://www.loc.gov/marc/bibliographic/bd006.html", + "codes": { + " ": { + "label": "No determinable frequency" + }, + "a": { + "label": "Annual" + }, + "b": { + "label": "Bimonthly" + }, + "c": { + "label": "Semiweekly" + }, + "d": { + "label": "Daily" + }, + "e": { + "label": "Biweekly" + }, + "f": { + "label": "Semiannual" + }, + "g": { + "label": "Biennial" + }, + "h": { + "label": "Triennial" + }, + "i": { + "label": "Three times a week" + }, + "j": { + "label": "Three times a month" + }, + "k": { + "label": "Continuously updated" + }, + "m": { + "label": "Monthly" + }, + "q": { + "label": "Quarterly" + }, + "s": { + "label": "Semimonthly" + }, + "t": { + "label": "Three times a year" + }, + "u": { + "label": "Unknown" + }, + "w": { + "label": "Weekly" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "02": { + "label": "Regularity", + "url": "https://www.loc.gov/marc/bibliographic/bd006.html", + "codes": { + "n": { + "label": "Normalized irregular" + }, + "r": { + "label": "Regular" + }, + "u": { + "label": "Unknown" + }, + "x": { + "label": "Completely irregular" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "04": { + "label": "Type of continuing resource", + "url": "https://www.loc.gov/marc/bibliographic/bd006.html", + "codes": { + " ": { + "label": "None of the following" + }, + "d": { + "label": "Updating database" + }, + "l": { + "label": "Updating loose-leaf" + }, + "m": { + "label": "Monographic series" + }, + "n": { + "label": "Newspaper" + }, + "p": { + "label": "Periodical" + }, + "w": { + "label": "Updating Web site" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "05": { + "label": "Form of original item", + "url": "https://www.loc.gov/marc/bibliographic/bd006.html", + "codes": { + " ": { + "label": "None of the following" + }, + "a": { + "label": "Microfilm" + }, + "b": { + "label": "Microfiche" + }, + "c": { + "label": "Microopaque" + }, + "d": { + "label": "Large print" + }, + "e": { + "label": "Newspaper format" + }, + "f": { + "label": "Braille" + }, + "o": { + "label": "Online" + }, + "q": { + "label": "Direct electronic" + }, + "s": { + "label": "Electronic" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "06": { + "label": "Form of item", + "url": "https://www.loc.gov/marc/bibliographic/bd006.html", + "codes": { + " ": { + "label": "None of the following" + }, + "a": { + "label": "Microfilm" + }, + "b": { + "label": "Microfiche" + }, + "c": { + "label": "Microopaque" + }, + "d": { + "label": "Large print" + }, + "f": { + "label": "Braille" + }, + "o": { + "label": "Online" + }, + "q": { + "label": "Direct electronic" + }, + "r": { + "label": "Regular print reproduction" + }, + "s": { + "label": "Electronic" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "07": { + "label": "Nature of entire work", + "url": "https://www.loc.gov/marc/bibliographic/bd006.html", + "codes": { + " ": { + "label": "Not specified" + }, + "a": { + "label": "Abstracts/summaries" + }, + "b": { + "label": "Bibliographies" + }, + "c": { + "label": "Catalogs" + }, + "d": { + "label": "Dictionaries" + }, + "e": { + "label": "Encyclopedias" + }, + "f": { + "label": "Handbooks" + }, + "g": { + "label": "Legal articles" + }, + "h": { + "label": "Biography" + }, + "i": { + "label": "Indexes" + }, + "k": { + "label": "Discographies" + }, + "l": { + "label": "Legislation" + }, + "m": { + "label": "Theses" + }, + "n": { + "label": "Surveys of literature in a subject area" + }, + "o": { + "label": "Reviews" + }, + "p": { + "label": "Programmed texts" + }, + "q": { + "label": "Filmographies" + }, + "r": { + "label": "Directories" + }, + "s": { + "label": "Statistics" + }, + "t": { + "label": "Technical reports" + }, + "u": { + "label": "Standards/specifications" + }, + "v": { + "label": "Legal cases and case notes" + }, + "w": { + "label": "Law reports and digests" + }, + "y": { + "label": "Yearbooks" + }, + "z": { + "label": "Treaties" + }, + "5": { + "label": "Calendars" + }, + "6": { + "label": "Comics/graphic novels" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "08-10": { + "label": "Nature of contents", + "url": "https://www.loc.gov/marc/bibliographic/bd006.html", + "codes": { + " ": { + "label": "Not specified" + }, + "a": { + "label": "Abstracts/summaries" + }, + "b": { + "label": "Bibliographies" + }, + "c": { + "label": "Catalogs" + }, + "d": { + "label": "Dictionaries" + }, + "e": { + "label": "Encyclopedias" + }, + "f": { + "label": "Handbooks" + }, + "g": { + "label": "Legal articles" + }, + "h": { + "label": "Biography" + }, + "i": { + "label": "Indexes" + }, + "k": { + "label": "Discographies" + }, + "l": { + "label": "Legislation" + }, + "m": { + "label": "Theses" + }, + "n": { + "label": "Surveys of literature in a subject area" + }, + "o": { + "label": "Reviews" + }, + "p": { + "label": "Programmed texts" + }, + "q": { + "label": "Filmographies" + }, + "r": { + "label": "Directories" + }, + "s": { + "label": "Statistics" + }, + "t": { + "label": "Technical reports" + }, + "u": { + "label": "Standards/specifications" + }, + "v": { + "label": "Legal cases and case notes" + }, + "w": { + "label": "Law reports and digests" + }, + "y": { + "label": "Yearbooks" + }, + "z": { + "label": "Treaties" + }, + "5": { + "label": "Calendars" + }, + "6": { + "label": "Comics/graphic novels" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "11": { + "label": "Government publication", + "url": "https://www.loc.gov/marc/bibliographic/bd006.html", + "codes": { + " ": { + "label": "Not a government publication" + }, + "a": { + "label": "Autonomous or semi-autonomous component" + }, + "c": { + "label": "Multilocal" + }, + "f": { + "label": "Federal/national" + }, + "i": { + "label": "International intergovernmental" + }, + "l": { + "label": "Local" + }, + "m": { + "label": "Multistate" + }, + "o": { + "label": "Government publication-level undetermined" + }, + "s": { + "label": "State, provincial, territorial, dependent, etc." + }, + "u": { + "label": "Unknown if item is government publication" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "12": { + "label": "Conference publication", + "url": "https://www.loc.gov/marc/bibliographic/bd006.html", + "codes": { + "0": { + "label": "Not a conference publication" + }, + "1": { + "label": "Conference publication" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "16": { + "label": "Original alphabet or script of title", + "url": "https://www.loc.gov/marc/bibliographic/bd006.html", + "codes": { + " ": { + "label": "No alphabet or script given/No key title" + }, + "a": { + "label": "Basic Roman" + }, + "b": { + "label": "Extended Roman" + }, + "c": { + "label": "Cyrillic" + }, + "d": { + "label": "Japanese" + }, + "e": { + "label": "Chinese" + }, + "f": { + "label": "Arabic" + }, + "g": { + "label": "Greek" + }, + "h": { + "label": "Hebrew" + }, + "i": { + "label": "Thai" + }, + "j": { + "label": "Devanagari" + }, + "k": { + "label": "Korean" + }, + "l": { + "label": "Tamil" + }, + "u": { + "label": "Unknown" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "17": { + "label": "Entry convention", + "url": "https://www.loc.gov/marc/bibliographic/bd006.html", + "codes": { + "0": { + "label": "Successive entry" + }, + "1": { + "label": "Latest entry" + }, + "2": { + "label": "Integrated entry" + }, + "|": { + "label": "No attempt to code" + } + } + } + } + }, + "Music": { + "positions": { + "01-02": { + "label": "Form of composition", + "url": "https://www.loc.gov/marc/bibliographic/bd006.html", + "codes": { + "an": { + "label": "Anthems" + }, + "bd": { + "label": "Ballads" + }, + "bg": { + "label": "Bluegrass music" + }, + "bl": { + "label": "Blues" + }, + "bt": { + "label": "Ballets" + }, + "ca": { + "label": "Chaconnes" + }, + "cb": { + "label": "Chants, Other religions" + }, + "cc": { + "label": "Chant, Christian" + }, + "cg": { + "label": "Concerti grossi" + }, + "ch": { + "label": "Chorales" + }, + "cl": { + "label": "Chorale preludes" + }, + "cn": { + "label": "Canons and rounds" + }, + "co": { + "label": "Concertos" + }, + "cp": { + "label": "Chansons, polyphonic" + }, + "cr": { + "label": "Carols" + }, + "cs": { + "label": "Chance compositions" + }, + "ct": { + "label": "Cantatas" + }, + "cy": { + "label": "Country music" + }, + "cz": { + "label": "Canzonas" + }, + "df": { + "label": "Dance forms" + }, + "dv": { + "label": "Divertimentos, serenades, cassations, divertissements, and notturni" + }, + "fg": { + "label": "Fugues" + }, + "fl": { + "label": "Flamenco" + }, + "fm": { + "label": "Folk music" + }, + "ft": { + "label": "Fantasias" + }, + "gm": { + "label": "Gospel music" + }, + "hy": { + "label": "Hymns" + }, + "jz": { + "label": "Jazz" + }, + "mc": { + "label": "Musical revues and comedies" + }, + "md": { + "label": "Madrigals" + }, + "mi": { + "label": "Minuets" + }, + "mo": { + "label": "Motets" + }, + "mp": { + "label": "Motion picture music" + }, + "mr": { + "label": "Marches" + }, + "ms": { + "label": "Masses" + }, + "mu": { + "label": "Multiple forms" + }, + "mz": { + "label": "Mazurkas" + }, + "nc": { + "label": "Nocturnes" + }, + "nn": { + "label": "Not applicable" + }, + "op": { + "label": "Operas" + }, + "or": { + "label": "Oratorios" + }, + "ov": { + "label": "Overtures" + }, + "pg": { + "label": "Program music" + }, + "pm": { + "label": "Passion music" + }, + "po": { + "label": "Polonaises" + }, + "pp": { + "label": "Popular music" + }, + "pr": { + "label": "Preludes" + }, + "ps": { + "label": "Passacaglias" + }, + "pt": { + "label": "Part-songs" + }, + "pv": { + "label": "Pavans" + }, + "rc": { + "label": "Rock music" + }, + "rd": { + "label": "Rondos" + }, + "rg": { + "label": "Ragtime music" + }, + "ri": { + "label": "Ricercars" + }, + "rp": { + "label": "Rhapsodies" + }, + "rq": { + "label": "Requiems" + }, + "sd": { + "label": "Square dance music" + }, + "sg": { + "label": "Songs" + }, + "sn": { + "label": "Sonatas" + }, + "sp": { + "label": "Symphonic poems" + }, + "st": { + "label": "Studies and exercises" + }, + "su": { + "label": "Suites" + }, + "sy": { + "label": "Symphonies" + }, + "tc": { + "label": "Toccatas" + }, + "tl": { + "label": "Teatro lirico" + }, + "ts": { + "label": "Trio-sonatas" + }, + "uu": { + "label": "Unknown" + }, + "vi": { + "label": "Villancicos" + }, + "vr": { + "label": "Variations" + }, + "wz": { + "label": "Waltzes" + }, + "za": { + "label": "Zarzuelas" + }, + "zz": { + "label": "Other" + }, + "||": { + "label": "No attempt to code" + } + } + }, + "03": { + "label": "Format of music", + "url": "https://www.loc.gov/marc/bibliographic/bd006.html", + "codes": { + "a": { + "label": "Full score" + }, + "b": { + "label": "Miniature or study score" + }, + "c": { + "label": "Accompaniment reduced for keyboard" + }, + "d": { + "label": "Voice score with accompaniment omitted" + }, + "e": { + "label": "Condensed score or piano-conductor score" + }, + "g": { + "label": "Close score" + }, + "h": { + "label": "Chorus score" + }, + "i": { + "label": "Condensed score" + }, + "j": { + "label": "Performer-conductor part" + }, + "k": { + "label": "Vocal score" + }, + "l": { + "label": "Score" + }, + "m": { + "label": "Multiple score formats" + }, + "n": { + "label": "Not applicable" + }, + "p": { + "label": "Piano score" + }, + "u": { + "label": "Unknown" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "04": { + "label": "Music parts", + "url": "https://www.loc.gov/marc/bibliographic/bd006.html", + "codes": { + " ": { + "label": "No parts in hand or not specified" + }, + "d": { + "label": "Instrumental and vocal parts" + }, + "e": { + "label": "Instrumental parts" + }, + "f": { + "label": "Vocal parts" + }, + "n": { + "label": "Not applicable" + }, + "u": { + "label": "Unknown" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "05": { + "label": "Target audience", + "url": "https://www.loc.gov/marc/bibliographic/bd006.html", + "codes": { + " ": { + "label": "Unknown or unspecified" + }, + "a": { + "label": "Preschool" + }, + "b": { + "label": "Primary" + }, + "c": { + "label": "Pre-adolescent" + }, + "d": { + "label": "Adolescent" + }, + "e": { + "label": "Adult" + }, + "f": { + "label": "Specialized" + }, + "g": { + "label": "General" + }, + "j": { + "label": "Juvenile" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "06": { + "label": "Form of item", + "url": "https://www.loc.gov/marc/bibliographic/bd006.html", + "codes": { + " ": { + "label": "None of the following" + }, + "a": { + "label": "Microfilm" + }, + "b": { + "label": "Microfiche" + }, + "c": { + "label": "Microopaque" + }, + "d": { + "label": "Large print" + }, + "f": { + "label": "Braille" + }, + "o": { + "label": "Online" + }, + "q": { + "label": "Direct electronic" + }, + "r": { + "label": "Regular print reproduction" + }, + "s": { + "label": "Electronic" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "07-12": { + "label": "Accompanying matter", + "url": "https://www.loc.gov/marc/bibliographic/bd006.html", + "codes": { + " ": { + "label": "No accompanying matter" + }, + "a": { + "label": "Discography" + }, + "b": { + "label": "Bibliography" + }, + "c": { + "label": "Thematic index" + }, + "d": { + "label": "Libretto or text" + }, + "e": { + "label": "Biography of composer or author" + }, + "f": { + "label": "Biography of performer or history of ensemble" + }, + "g": { + "label": "Technical and/or historical information on instruments" + }, + "h": { + "label": "Technical information on music" + }, + "i": { + "label": "Historical information" + }, + "k": { + "label": "Ethnological information" + }, + "r": { + "label": "Instructional materials" + }, + "s": { + "label": "Music" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "13-14": { + "label": "Literary text for sound recordings", + "url": "https://www.loc.gov/marc/bibliographic/bd006.html", + "codes": { + " ": { + "label": "Item is a music sound recording" + }, + "a": { + "label": "Autobiography" + }, + "b": { + "label": "Biography" + }, + "c": { + "label": "Conference proceedings" + }, + "d": { + "label": "Drama" + }, + "e": { + "label": "Essays" + }, + "f": { + "label": "Fiction" + }, + "g": { + "label": "Reporting" + }, + "h": { + "label": "History" + }, + "i": { + "label": "Instruction" + }, + "j": { + "label": "Language instruction" + }, + "k": { + "label": "Comedy" + }, + "l": { + "label": "Lectures, speeches" + }, + "m": { + "label": "Memoirs" + }, + "n": { + "label": "Not applicable" + }, + "o": { + "label": "Folktales" + }, + "p": { + "label": "Poetry" + }, + "r": { + "label": "Rehearsals" + }, + "s": { + "label": "Sounds" + }, + "t": { + "label": "Interviews" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "16": { + "label": "Transposition and arrangement", + "url": "https://www.loc.gov/marc/bibliographic/bd006.html", + "codes": { + " ": { + "label": "Not arrangement or transposition or not specified" + }, + "a": { + "label": "Transposition" + }, + "b": { + "label": "Arrangement" + }, + "c": { + "label": "Both transposed and arranged" + }, + "n": { + "label": "Not applicable" + }, + "u": { + "label": "Unknown" + }, + "|": { + "label": "No attempt to code" + } + } + } + } + }, + "Maps": { + "positions": { + "01-04": { + "label": "Relief", + "url": "https://www.loc.gov/marc/bibliographic/bd006.html", + "codes": { + " ": { + "label": "No relief shown" + }, + "a": { + "label": "Contours" + }, + "b": { + "label": "Shading" + }, + "c": { + "label": "Gradient and bathymetric tints" + }, + "d": { + "label": "Hachures" + }, + "e": { + "label": "Bathymetry/soundings" + }, + "f": { + "label": "Form lines" + }, + "g": { + "label": "Spot heights" + }, + "i": { + "label": "Pictorially" + }, + "j": { + "label": "Land forms" + }, + "k": { + "label": "Bathymetry/isolines" + }, + "m": { + "label": "Rock drawings" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "05-06": { + "label": "Projection", + "url": "https://www.loc.gov/marc/bibliographic/bd006.html", + "codes": { + " ": { + "label": "Projection not specified" + }, + "aa": { + "label": "Aitoff" + }, + "ab": { + "label": "Gnomic" + }, + "ac": { + "label": "Lambert's azimuthal equal area" + }, + "ad": { + "label": "Orthographic" + }, + "ae": { + "label": "Azimuthal equidistant" + }, + "af": { + "label": "Stereographic" + }, + "ag": { + "label": "General vertical near-sided" + }, + "am": { + "label": "Modified stereographic for Alaska" + }, + "an": { + "label": "Chamberlin trimetric" + }, + "ap": { + "label": "Polar stereographic" + }, + "au": { + "label": "Azimuthal, specific type unknown" + }, + "az": { + "label": "Azimuthal, other" + }, + "ba": { + "label": "Gall" + }, + "bb": { + "label": "Goode's homolographic" + }, + "bc": { + "label": "Lambert's cylindrical equal area" + }, + "bd": { + "label": "Mercator" + }, + "be": { + "label": "Miller" + }, + "bf": { + "label": "Mollweide" + }, + "bg": { + "label": "Sinusoidal" + }, + "bh": { + "label": "Transverse Mercator" + }, + "bi": { + "label": "Gauss-Kruger" + }, + "bj": { + "label": "Equirectangular" + }, + "bk": { + "label": "Krovak" + }, + "bl": { + "label": "Cassini-Soldner" + }, + "bo": { + "label": "Oblique Mercator" + }, + "br": { + "label": "Robinson" + }, + "bs": { + "label": "Space oblique Mercator" + }, + "bu": { + "label": "Cylindrical, specific type unknown" + }, + "bz": { + "label": "Cylindrical, other" + }, + "ca": { + "label": "Albers equal area" + }, + "cb": { + "label": "Bonne" + }, + "cc": { + "label": "Lambert's conformal conic" + }, + "ce": { + "label": "Equidistant conic" + }, + "cp": { + "label": "Polyconic" + }, + "cu": { + "label": "Conic, specific type unknown" + }, + "cz": { + "label": "Conic, other" + }, + "da": { + "label": "Armadillo" + }, + "db": { + "label": "Butterfly" + }, + "dc": { + "label": "Eckert" + }, + "dd": { + "label": "Goode's homolosine" + }, + "de": { + "label": "Miller's bipolar oblique conformal conic" + }, + "df": { + "label": "Van Der Grinten" + }, + "dg": { + "label": "Dimaxion" + }, + "dh": { + "label": "Cordiform" + }, + "dl": { + "label": "Lambert conformal" + }, + "zz": { + "label": "Other" + }, + "||": { + "label": "No attempt to code" + } + } + }, + "08": { + "label": "Type of cartographic material", + "url": "https://www.loc.gov/marc/bibliographic/bd006.html", + "codes": { + "a": { + "label": "Single map" + }, + "b": { + "label": "Map series" + }, + "c": { + "label": "Map serial" + }, + "d": { + "label": "Globe" + }, + "e": { + "label": "Atlas" + }, + "f": { + "label": "Separate supplement to another work" + }, + "g": { + "label": "Bound as part of another work" + }, + "u": { + "label": "Unknown" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "11": { + "label": "Government publication", + "url": "https://www.loc.gov/marc/bibliographic/bd006.html", + "codes": { + " ": { + "label": "Not a government publication" + }, + "a": { + "label": "Autonomous or semi-autonomous component" + }, + "c": { + "label": "Multilocal" + }, + "f": { + "label": "Federal/national" + }, + "i": { + "label": "International intergovernmental" + }, + "l": { + "label": "Local" + }, + "m": { + "label": "Multistate" + }, + "o": { + "label": "Government publication-level undetermined" + }, + "s": { + "label": "State, provincial, territorial, dependent, etc." + }, + "u": { + "label": "Unknown if item is government publication" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "12": { + "label": "Form of item", + "url": "https://www.loc.gov/marc/bibliographic/bd006.html", + "codes": { + " ": { + "label": "None of the following" + }, + "a": { + "label": "Microfilm" + }, + "b": { + "label": "Microfiche" + }, + "c": { + "label": "Microopaque" + }, + "d": { + "label": "Large print" + }, + "f": { + "label": "Braille" + }, + "o": { + "label": "Online" + }, + "q": { + "label": "Direct electronic" + }, + "r": { + "label": "Regular print reproduction" + }, + "s": { + "label": "Electronic" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "14": { + "label": "Index", + "url": "https://www.loc.gov/marc/bibliographic/bd006.html", + "codes": { + "0": { + "label": "No index" + }, + "1": { + "label": "Index present" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "16-17": { + "label": "Special format characteristics", + "url": "https://www.loc.gov/marc/bibliographic/bd006.html", + "codes": { + " ": { + "label": "No specified special format characteristics" + }, + "e": { + "label": "Manuscript" + }, + "j": { + "label": "Picture card, post card" + }, + "k": { + "label": "Calendar" + }, + "l": { + "label": "Puzzle" + }, + "n": { + "label": "Game" + }, + "o": { + "label": "Wall map" + }, + "p": { + "label": "Playing cards" + }, + "r": { + "label": "Loose-leaf" + }, + "z": { + "label": "Other" + }, + "||": { + "label": "No attempt to code" + } + } + } + } + }, + "Visual Materials": { + "positions": { + "01-03": { + "label": "Running time for motion pictures and videorecordings", + "url": "https://www.loc.gov/marc/bibliographic/bd006.html", + "codes": { + "000": { + "label": "Running time exceeds three characters" + }, + "001-999": { + "label": "Running time" + }, + "nnn": { + "label": "Not applicable" + }, + "---": { + "label": "Unknown" + }, + "|||": { + "label": "No attempt to code" + } + } + }, + "05": { + "label": "Target audience", + "url": "https://www.loc.gov/marc/bibliographic/bd006.html", + "codes": { + " ": { + "label": "Unknown or not specified" + }, + "a": { + "label": "Preschool" + }, + "b": { + "label": "Primary" + }, + "c": { + "label": "Pre-adolescent" + }, + "d": { + "label": "Adolescent" + }, + "e": { + "label": "Adult" + }, + "f": { + "label": "Specialized" + }, + "g": { + "label": "General" + }, + "j": { + "label": "Juvenile" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "11": { + "label": "Government publication", + "url": "https://www.loc.gov/marc/bibliographic/bd006.html", + "codes": { + " ": { + "label": "Not a government publication" + }, + "a": { + "label": "Autonomous or semi-autonomous component" + }, + "c": { + "label": "Multilocal" + }, + "f": { + "label": "Federal/national" + }, + "i": { + "label": "International intergovernmental" + }, + "l": { + "label": "Local" + }, + "m": { + "label": "Multistate" + }, + "o": { + "label": "Government publication-level undetermined" + }, + "s": { + "label": "State, provincial, territorial, dependent, etc." + }, + "u": { + "label": "Unknown if item is government publication" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "12": { + "label": "Form of item", + "url": "https://www.loc.gov/marc/bibliographic/bd006.html", + "codes": { + " ": { + "label": "None of the following" + }, + "a": { + "label": "Microfilm" + }, + "b": { + "label": "Microfiche" + }, + "c": { + "label": "Microopaque" + }, + "d": { + "label": "Large print" + }, + "f": { + "label": "Braille" + }, + "o": { + "label": "Online" + }, + "q": { + "label": "Direct electronic" + }, + "r": { + "label": "Regular print reproduction" + }, + "s": { + "label": "Electronic" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "16": { + "label": "Type of visual material", + "url": "https://www.loc.gov/marc/bibliographic/bd006.html", + "codes": { + "a": { + "label": "Art original" + }, + "b": { + "label": "Kit" + }, + "c": { + "label": "Art reproduction" + }, + "d": { + "label": "Diorama" + }, + "f": { + "label": "Filmstrip" + }, + "g": { + "label": "Game" + }, + "i": { + "label": "Picture" + }, + "k": { + "label": "Graphic" + }, + "l": { + "label": "Technical drawing" + }, + "m": { + "label": "Motion picture" + }, + "n": { + "label": "Chart" + }, + "o": { + "label": "Flash card" + }, + "p": { + "label": "Microscope slide" + }, + "q": { + "label": "Model" + }, + "r": { + "label": "Realia" + }, + "s": { + "label": "Slide" + }, + "t": { + "label": "Transparency" + }, + "v": { + "label": "Videorecording" + }, + "w": { + "label": "Toy" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "17": { + "label": "Technique", + "url": "https://www.loc.gov/marc/bibliographic/bd006.html", + "codes": { + "a": { + "label": "Animation" + }, + "c": { + "label": "Animation and live action" + }, + "l": { + "label": "Live action" + }, + "n": { + "label": "Not applicable" + }, + "u": { + "label": "Unknown" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + } + } + }, + "Computer Files": { + "positions": { + "05": { + "label": "Target audience", + "url": "https://www.loc.gov/marc/bibliographic/bd006.html", + "codes": { + " ": { + "label": "Unknown or not specified" + }, + "a": { + "label": "Preschool" + }, + "b": { + "label": "Primary" + }, + "c": { + "label": "Pre-adolescent" + }, + "d": { + "label": "Adolescent" + }, + "e": { + "label": "Adult" + }, + "f": { + "label": "Specialized" + }, + "g": { + "label": "General" + }, + "j": { + "label": "Juvenile" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "06": { + "label": "Form of item", + "url": "https://www.loc.gov/marc/bibliographic/bd006.html", + "codes": { + " ": { + "label": "Unknown or not specified" + }, + "o": { + "label": "Online" + }, + "q": { + "label": "Direct electronic" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "09": { + "label": "Type of computer file", + "url": "https://www.loc.gov/marc/bibliographic/bd006.html", + "codes": { + "a": { + "label": "Numeric data" + }, + "b": { + "label": "Computer program" + }, + "c": { + "label": "Representational" + }, + "d": { + "label": "Document" + }, + "e": { + "label": "Bibliographic data" + }, + "f": { + "label": "Font" + }, + "g": { + "label": "Game" + }, + "h": { + "label": "Sound" + }, + "i": { + "label": "Interactive multimedia" + }, + "j": { + "label": "Online system or service" + }, + "m": { + "label": "Combination" + }, + "u": { + "label": "Unknown" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "11": { + "label": "Government publication", + "url": "https://www.loc.gov/marc/bibliographic/bd006.html", + "codes": { + " ": { + "label": "Not a government publication" + }, + "a": { + "label": "Autonomous or semi-autonomous component" + }, + "c": { + "label": "Multilocal" + }, + "f": { + "label": "Federal/national" + }, + "i": { + "label": "International intergovernmental" + }, + "l": { + "label": "Local" + }, + "m": { + "label": "Multistate" + }, + "o": { + "label": "Government publication-level undetermined" + }, + "s": { + "label": "State, provincial, territorial, dependent, etc." + }, + "u": { + "label": "Unknown if item is government publication" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + } + } + }, + "Mixed Materials": { + "positions": { + "06": { + "label": "Form of item", + "url": "https://www.loc.gov/marc/bibliographic/bd006.html", + "codes": { + " ": { + "label": "None of the following" + }, + "a": { + "label": "Microfilm" + }, + "b": { + "label": "Microfiche" + }, + "c": { + "label": "Microopaque" + }, + "d": { + "label": "Large print" + }, + "f": { + "label": "Braille" + }, + "o": { + "label": "Online" + }, + "q": { + "label": "Direct electronic" + }, + "r": { + "label": "Regular print reproduction" + }, + "s": { + "label": "Electronic" + }, + "|": { + "label": "No attempt to code" + } + } + } + } + } + } + }, + "007": { + "tag": "007", + "label": "Physical Description", + "repeatable": true, + "types": { + "Common": { + "positions": { + "00": { + "label": "Category of material", + "url": "https://www.loc.gov/marc/bibliographic/bd007c.html", + "codes": { + "c": { + "label": "Electronic resource" + } + } + } + } + }, + "Map": { + "positions": { + "00": { + "label": "Category of material", + "url": "https://www.loc.gov/marc/bibliographic/bd007a.html", + "codes": { + "a": { + "label": "Map" + } + } + }, + "01": { + "label": "Specific material designation", + "url": "https://www.loc.gov/marc/bibliographic/bd007a.html", + "codes": { + "d": { + "label": "Atlas" + }, + "g": { + "label": "Diagram" + }, + "j": { + "label": "Map" + }, + "k": { + "label": "Profile" + }, + "q": { + "label": "Model" + }, + "r": { + "label": "Remote-sensing image" + }, + "s": { + "label": "Section" + }, + "u": { + "label": "Unspecified" + }, + "y": { + "label": "View" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + }, + "historical-codes": { + "a": { + "label": "Aerial chart" + }, + "b": { + "label": "Aerial remote-sensing image" + }, + "c": { + "label": "Anamorphic map" + }, + "e": { + "label": "Celestial chart" + }, + "f": { + "label": "Chart" + }, + "h": { + "label": "Hydrographic chart" + }, + "i": { + "label": "Imaginative map" + }, + "j": { + "label": "Orthophoto" + }, + "m": { + "label": "Photo mosaic (controlled)" + }, + "n": { + "label": "Photo mosaic (uncontrolled)" + }, + "o": { + "label": "Photomap" + }, + "p": { + "label": "Plan" + }, + "t": { + "label": "Space remote-sensing image" + }, + "v": { + "label": "Terrestrial remote-sensing image" + }, + "w": { + "label": "Topographical drawing" + }, + "x": { + "label": "Topographical print" + } + } + }, + "03": { + "label": "Color", + "url": "https://www.loc.gov/marc/bibliographic/bd007a.html", + "codes": { + "a": { + "label": "One color" + }, + "c": { + "label": "Multicolored" + }, + "|": { + "label": "No attempt to code" + } + }, + "historical-codes": { + "b": { + "label": "Multicolored [OBSOLETE, 1982]" + } + } + }, + "04": { + "label": "Physical medium", + "url": "https://www.loc.gov/marc/bibliographic/bd007a.html", + "codes": { + "a": { + "label": "Paper" + }, + "b": { + "label": "Wood" + }, + "c": { + "label": "Stone" + }, + "d": { + "label": "Metal" + }, + "e": { + "label": "Synthetic" + }, + "f": { + "label": "Skin" + }, + "g": { + "label": "Textiles" + }, + "i": { + "label": "Plastic" + }, + "j": { + "label": "Glass" + }, + "l": { + "label": "Vinyl" + }, + "n": { + "label": "Vellum" + }, + "p": { + "label": "Plaster" + }, + "q": { + "label": "Flexible base photographic, positive" + }, + "r": { + "label": "Flexible base photographic, negative" + }, + "s": { + "label": "Non-flexible base photographic, positive" + }, + "t": { + "label": "Non-flexible base photographic, negative" + }, + "u": { + "label": "Unknown" + }, + "v": { + "label": "Leather" + }, + "w": { + "label": "Parchment" + }, + "y": { + "label": "Other photographic medium" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "05": { + "label": "Type of reproduction", + "url": "https://www.loc.gov/marc/bibliographic/bd007a.html", + "codes": { + "f": { + "label": "Facsimile" + }, + "n": { + "label": "Not applicable" + }, + "u": { + "label": "Unknown" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "06": { + "label": "Production/reproduction details", + "url": "https://www.loc.gov/marc/bibliographic/bd007a.html", + "codes": { + "a": { + "label": "Photocopy, blueline print" + }, + "b": { + "label": "Photocopy" + }, + "c": { + "label": "Pre-production" + }, + "d": { + "label": "Film" + }, + "u": { + "label": "Unknown" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "07": { + "label": "Positive/negative aspect", + "url": "https://www.loc.gov/marc/bibliographic/bd007a.html", + "codes": { + "a": { + "label": "Positive" + }, + "b": { + "label": "Negative" + }, + "m": { + "label": "Mixed polarity" + }, + "n": { + "label": "Not applicable" + }, + "|": { + "label": "No attempt to code" + } + }, + "historical-codes": { + "u": { + "label": "Unknown [OBSOLETE, 1997] [CAN/MARC only]" + } + } + } + } + }, + "Electronic resource": { + "positions": { + "00": { + "label": "Category of material", + "url": "https://www.loc.gov/marc/bibliographic/bd007c.html", + "codes": { + "c": { + "label": "Electronic resource" + } + } + }, + "01": { + "label": "Specific material designation", + "url": "https://www.loc.gov/marc/bibliographic/bd007c.html", + "codes": { + "a": { + "label": "Tape cartridge" + }, + "b": { + "label": "Chip cartridge" + }, + "c": { + "label": "Computer optical disc cartridge" + }, + "d": { + "label": "Computer disc, type unspecified" + }, + "e": { + "label": "Computer disc cartridge, type unspecified" + }, + "f": { + "label": "Tape cassette" + }, + "h": { + "label": "Tape reel" + }, + "j": { + "label": "Magnetic disk" + }, + "k": { + "label": "Computer card" + }, + "m": { + "label": "Magneto-optical disc" + }, + "o": { + "label": "Optical disc" + }, + "r": { + "label": "Remote" + }, + "s": { + "label": "Standalone device" + }, + "u": { + "label": "Unspecified" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "03": { + "label": "Color", + "url": "https://www.loc.gov/marc/bibliographic/bd007c.html", + "codes": { + "a": { + "label": "One color" + }, + "b": { + "label": "Black-and-white" + }, + "c": { + "label": "Multicolored" + }, + "g": { + "label": "Gray scale" + }, + "m": { + "label": "Mixed" + }, + "n": { + "label": "Not applicable" + }, + "u": { + "label": "Unknown" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + }, + "historical-codes": { + "h": { + "label": "Hand coloured [OBSOLETE, 1997] [CAN/MARC only]" + } + } + }, + "04": { + "label": "Dimensions", + "url": "https://www.loc.gov/marc/bibliographic/bd007c.html", + "codes": { + "a": { + "label": "3 1/2 in." + }, + "e": { + "label": "12 in." + }, + "g": { + "label": "4 3/4 in. or 12 cm." + }, + "i": { + "label": "1 1/8 x 2 3/8 in." + }, + "j": { + "label": "3 7/8 x 2 1/2 in." + }, + "n": { + "label": "Not applicable" + }, + "o": { + "label": "5 1/4 in." + }, + "u": { + "label": "Unknown" + }, + "v": { + "label": "8 in." + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "05": { + "label": "Sound", + "url": "https://www.loc.gov/marc/bibliographic/bd007c.html", + "codes": { + " ": { + "label": "No sound (silent)" + }, + "a": { + "label": "Sound" + }, + "u": { + "label": "Unknown" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "06-08": { + "label": "Image bit depth", + "url": "https://www.loc.gov/marc/bibliographic/bd007c.html", + "codes": { + "001-999": { + "label": "Exact bit depth" + }, + "mmm": { + "label": "Multiple" + }, + "nnn": { + "label": "Not applicable" + }, + "---": { + "label": "Unknown" + }, + "|||": { + "label": "No attempt to code" + } + } + }, + "09": { + "label": "File formats", + "url": "https://www.loc.gov/marc/bibliographic/bd007c.html", + "codes": { + "a": { + "label": "One file format" + }, + "m": { + "label": "Multiple file formats" + }, + "u": { + "label": "Unknown" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "10": { + "label": "Quality assurance targets", + "url": "https://www.loc.gov/marc/bibliographic/bd007c.html", + "codes": { + "a": { + "label": "Absent" + }, + "n": { + "label": "Not applicable" + }, + "p": { + "label": "Present" + }, + "u": { + "label": "Unknown" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "11": { + "label": "Antecedent/source", + "url": "https://www.loc.gov/marc/bibliographic/bd007c.html", + "codes": { + "a": { + "label": "File reproduced from original" + }, + "b": { + "label": "File reproduced from microform" + }, + "c": { + "label": "File reproduced from an electronic resource" + }, + "d": { + "label": "File reproduced from an intermediate (not microform)" + }, + "m": { + "label": "Mixed" + }, + "n": { + "label": "Not applicable" + }, + "u": { + "label": "Unknown" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "12": { + "label": "Level of compression", + "url": "https://www.loc.gov/marc/bibliographic/bd007c.html", + "codes": { + "a": { + "label": "Uncompressed" + }, + "b": { + "label": "Lossless" + }, + "d": { + "label": "Lossy" + }, + "m": { + "label": "Mixed" + }, + "u": { + "label": "Unknown" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "13": { + "label": "Reformatting quality", + "url": "https://www.loc.gov/marc/bibliographic/bd007c.html", + "codes": { + "a": { + "label": "Access" + }, + "n": { + "label": "Not applicable" + }, + "p": { + "label": "Preservation" + }, + "r": { + "label": "Replacement" + }, + "u": { + "label": "Unknown" + }, + "|": { + "label": "No attempt to code" + } + } + } + } + }, + "Globe": { + "positions": { + "00": { + "label": "Category of material", + "url": "https://www.loc.gov/marc/bibliographic/bd007d.html", + "codes": { + "d": { + "label": "Globe" + } + } + }, + "01": { + "label": "Specific material designation", + "url": "https://www.loc.gov/marc/bibliographic/bd007d.html", + "codes": { + "a": { + "label": "Celestial globe" + }, + "b": { + "label": "Planetary or lunar globe" + }, + "c": { + "label": "Terrestrial globe" + }, + "e": { + "label": "Earth moon globe" + }, + "u": { + "label": "Unspecified" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + }, + "historical-codes": { + "d": { + "label": "Satellite globe (of our solar system), excluding the earth moon [OBSOLETE, 1997] [CAN/MARC only]" + } + } + }, + "03": { + "label": "Color", + "url": "https://www.loc.gov/marc/bibliographic/bd007d.html", + "codes": { + "a": { + "label": "One color" + }, + "c": { + "label": "Multicolored" + }, + "|": { + "label": "No attempt to code" + } + }, + "historical-codes": { + "b": { + "label": "Multicolored [OBSOLETE, 1982]" + } + } + }, + "04": { + "label": "Physical medium", + "url": "https://www.loc.gov/marc/bibliographic/bd007d.html", + "codes": { + "a": { + "label": "Paper" + }, + "b": { + "label": "Wood" + }, + "c": { + "label": "Stone" + }, + "d": { + "label": "Metal" + }, + "e": { + "label": "Synthetic" + }, + "f": { + "label": "Skin" + }, + "g": { + "label": "Textile" + }, + "i": { + "label": "Plastic" + }, + "l": { + "label": "Vinyl" + }, + "n": { + "label": "Vellum" + }, + "p": { + "label": "Plaster" + }, + "u": { + "label": "Unknown" + }, + "v": { + "label": "Leather" + }, + "w": { + "label": "Parchment" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "05": { + "label": "Type of reproduction", + "url": "https://www.loc.gov/marc/bibliographic/bd007d.html", + "codes": { + "f": { + "label": "Facsimile" + }, + "n": { + "label": "Not applicable" + }, + "u": { + "label": "Unknown" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + } + } + }, + "Tactile material": { + "positions": { + "00": { + "label": "Category of material", + "url": "https://www.loc.gov/marc/bibliographic/bd007f.html", + "codes": { + "f": { + "label": "Tactile material" + } + } + }, + "01": { + "label": "Specific material designation", + "url": "https://www.loc.gov/marc/bibliographic/bd007f.html", + "codes": { + "a": { + "label": "Moon" + }, + "b": { + "label": "Braille" + }, + "c": { + "label": "Combination" + }, + "d": { + "label": "Tactile, with no writing system" + }, + "u": { + "label": "Unspecified" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "03-04": { + "label": "Class of braille writing", + "url": "https://www.loc.gov/marc/bibliographic/bd007f.html", + "codes": { + " ": { + "label": "No specified class of braille writing" + }, + "a": { + "label": "Literary braille" + }, + "b": { + "label": "Format code braille" + }, + "c": { + "label": "Mathematics and scientific braille" + }, + "d": { + "label": "Computer braille" + }, + "e": { + "label": "Music braille" + }, + "m": { + "label": "Multiple braille types" + }, + "n": { + "label": "Not applicable" + }, + "u": { + "label": "Unknown" + }, + "z": { + "label": "Other" + }, + "||": { + "label": "No attempt to code" + } + } + }, + "05": { + "label": "Level of contraction", + "url": "https://www.loc.gov/marc/bibliographic/bd007f.html", + "codes": { + "a": { + "label": "Uncontracted" + }, + "b": { + "label": "Contracted" + }, + "m": { + "label": "Combination" + }, + "n": { + "label": "Not applicable" + }, + "u": { + "label": "Unknown" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "06-08": { + "label": "Braille music format", + "url": "https://www.loc.gov/marc/bibliographic/bd007f.html", + "codes": { + " ": { + "label": "No specified braille music format" + }, + "a": { + "label": "Bar over bar" + }, + "b": { + "label": "Bar by bar" + }, + "c": { + "label": "Line over line" + }, + "d": { + "label": "Paragraph" + }, + "e": { + "label": "Single line" + }, + "f": { + "label": "Section by section" + }, + "g": { + "label": "Line by line" + }, + "h": { + "label": "Open score" + }, + "i": { + "label": "Spanner short form scoring" + }, + "j": { + "label": "Short form scoring" + }, + "k": { + "label": "Outline" + }, + "l": { + "label": "Vertical score" + }, + "n": { + "label": "Not applicable" + }, + "u": { + "label": "Unknown" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "09": { + "label": "Special physical characteristics", + "url": "https://www.loc.gov/marc/bibliographic/bd007f.html", + "codes": { + "a": { + "label": "Print/braille" + }, + "b": { + "label": "Jumbo or enlarged braille" + }, + "n": { + "label": "Not applicable" + }, + "u": { + "label": "Unknown" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + } + } + }, + "Projected graphic": { + "positions": { + "00": { + "label": "Category of material", + "url": "https://www.loc.gov/marc/bibliographic/bd007g.html", + "codes": { + "g": { + "label": "Projected graphic" + } + } + }, + "01": { + "label": "Specific material designation", + "url": "https://www.loc.gov/marc/bibliographic/bd007g.html", + "codes": { + "c": { + "label": "Filmstrip cartridge" + }, + "d": { + "label": "Filmslip" + }, + "f": { + "label": "Filmstrip, type unspecified" + }, + "o": { + "label": "Filmstrip roll" + }, + "s": { + "label": "Slide" + }, + "t": { + "label": "Transparency" + }, + "u": { + "label": "Unspecified" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + }, + "historical-codes": { + " ": { + "label": "Not applicable or no attempt to code [OBSOLETE, 1980]" + }, + "n": { + "label": "Not applicable [OBSOLETE, 1981] [USMARC only]" + } + } + }, + "03": { + "label": "Color", + "url": "https://www.loc.gov/marc/bibliographic/bd007g.html", + "codes": { + "a": { + "label": "One color" + }, + "b": { + "label": "Black-and-white" + }, + "c": { + "label": "Multicolored" + }, + "h": { + "label": "Hand colored" + }, + "m": { + "label": "Mixed" + }, + "n": { + "label": "Not applicable" + }, + "u": { + "label": "Unknown" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "04": { + "label": "Base of emulsion", + "url": "https://www.loc.gov/marc/bibliographic/bd007g.html", + "codes": { + "d": { + "label": "Glass" + }, + "e": { + "label": "Synthetic" + }, + "j": { + "label": "Safety film" + }, + "k": { + "label": "Film base, other than safety film" + }, + "m": { + "label": "Mixed collection" + }, + "o": { + "label": "Paper" + }, + "u": { + "label": "Unknown" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + }, + "historical-codes": { + " ": { + "label": "Not applicable or no attempt to code [OBSOLETE, 1980]" + }, + "n": { + "label": "Not applicable [OBSOLETE, 1981]" + } + } + }, + "05": { + "label": "Sound on medium or separate", + "url": "https://www.loc.gov/marc/bibliographic/bd007g.html", + "codes": { + " ": { + "label": "No sound (silent)" + }, + "a": { + "label": "Sound on medium" + }, + "b": { + "label": "Sound separate from medium" + }, + "u": { + "label": "Unknown" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "06": { + "label": "Medium for sound", + "url": "https://www.loc.gov/marc/bibliographic/bd007g.html", + "codes": { + " ": { + "label": "No sound (silent)" + }, + "a": { + "label": "Optical sound track on motion picture film" + }, + "b": { + "label": "Magnetic sound track on motion picture film" + }, + "c": { + "label": "Magnetic audio tape in cartridge" + }, + "d": { + "label": "Sound disc" + }, + "e": { + "label": "Magnetic audio tape on reel" + }, + "f": { + "label": "Magnetic audio tape in cassette" + }, + "g": { + "label": "Optical and magnetic sound track on motion picture film" + }, + "h": { + "label": "Videotape" + }, + "i": { + "label": "Videodisc" + }, + "u": { + "label": "Unknown" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + }, + "historical-codes": { + "g": { + "label": "Other [OBSOLETE, 1981]" + } + } + }, + "07": { + "label": "Dimensions", + "url": "https://www.loc.gov/marc/bibliographic/bd007g.html", + "codes": { + "a": { + "label": "Standard 8 mm. film width" + }, + "b": { + "label": "Super 8 mm./single 8 mm. film width" + }, + "c": { + "label": "9.5 mm. film width" + }, + "d": { + "label": "16 mm. film width" + }, + "e": { + "label": "28 mm. film width" + }, + "f": { + "label": "35 mm. film width" + }, + "g": { + "label": "70 mm. film width" + }, + "j": { + "label": "2x2 in. or 5x5 cm. slide" + }, + "k": { + "label": "2 1/4 x 2 1/4 in. or 6x6 cm. slide" + }, + "s": { + "label": "4x5 in. or 10x13 cm. transparency" + }, + "t": { + "label": "5x7 in. or 13x18 cm. transparency" + }, + "v": { + "label": "8x10 in. or 21x26 cm. transparency" + }, + "w": { + "label": "9x9 in. or 23x23 cm. transparency" + }, + "x": { + "label": "10x10 in. or 26x26 cm. transparency" + }, + "y": { + "label": "7x7 in. or 18x18 cm. transparency" + }, + "u": { + "label": "Unknown" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + }, + "historical-codes": { + "u": { + "label": "7x7 in. or 18x18 cm. [OBSOLETE, 1980]" + }, + "y": { + "label": "Unknown [OBSOLETE, 1980]" + } + } + }, + "08": { + "label": "Secondary support material", + "url": "https://www.loc.gov/marc/bibliographic/bd007g.html", + "codes": { + " ": { + "label": "No secondary support" + }, + "c": { + "label": "Cardboard" + }, + "d": { + "label": "Glass" + }, + "e": { + "label": "Synthetic" + }, + "h": { + "label": "Metal" + }, + "j": { + "label": "Metal and glass" + }, + "k": { + "label": "Synthetic and glass" + }, + "m": { + "label": "Mixed collection" + }, + "u": { + "label": "Unknown" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + } + } + }, + "Microform": { + "positions": { + "00": { + "label": "Category of material", + "url": "https://www.loc.gov/marc/bibliographic/bd007h.html", + "codes": { + "h": { + "label": "Microform" + } + } + }, + "01": { + "label": "Specific material designation", + "url": "https://www.loc.gov/marc/bibliographic/bd007h.html", + "codes": { + "a": { + "label": "Aperture card" + }, + "b": { + "label": "Microfilm cartridge" + }, + "c": { + "label": "Microfilm cassette" + }, + "d": { + "label": "Microfilm reel" + }, + "e": { + "label": "Microfiche" + }, + "f": { + "label": "Microfiche cassette" + }, + "g": { + "label": "Microopaque" + }, + "h": { + "label": "Microfilm slip" + }, + "j": { + "label": "Microfilm roll" + }, + "u": { + "label": "Unspecified" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "03": { + "label": "Positive/negative aspect", + "url": "https://www.loc.gov/marc/bibliographic/bd007h.html", + "codes": { + "a": { + "label": "Positive" + }, + "b": { + "label": "Negative" + }, + "m": { + "label": "Mixed polarity" + }, + "u": { + "label": "Unknown" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "04": { + "label": "Dimensions", + "url": "https://www.loc.gov/marc/bibliographic/bd007h.html", + "codes": { + "a": { + "label": "8 mm." + }, + "d": { + "label": "16 mm." + }, + "f": { + "label": "35 mm." + }, + "g": { + "label": "70 mm." + }, + "h": { + "label": "105 mm." + }, + "l": { + "label": "3x5 in. or 8x13 cm." + }, + "m": { + "label": "4x6 in. or 11x15 cm." + }, + "o": { + "label": "6x9 in. or 16x23 cm." + }, + "p": { + "label": "3 1/4 x 7 3/8 in. or 9x19 cm." + }, + "u": { + "label": "Unknown" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "05": { + "label": "Reduction ratio range", + "url": "https://www.loc.gov/marc/bibliographic/bd007h.html", + "codes": { + "a": { + "label": "Low reduction ratio" + }, + "b": { + "label": "Normal reduction" + }, + "c": { + "label": "High reduction" + }, + "d": { + "label": "Very high reduction" + }, + "e": { + "label": "Ultra high reduction" + }, + "u": { + "label": "Unknown" + }, + "v": { + "label": "Reduction rate varies" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "06-08": { + "label": "Reduction ratio", + "url": "https://www.loc.gov/marc/bibliographic/bd007h.html" + }, + "09": { + "label": "Color", + "url": "https://www.loc.gov/marc/bibliographic/bd007h.html", + "codes": { + "b": { + "label": "Black-and-white" + }, + "c": { + "label": "Multicolored" + }, + "m": { + "label": "Mixed" + }, + "u": { + "label": "Unknown" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "10": { + "label": "Emulsion on film", + "url": "https://www.loc.gov/marc/bibliographic/bd007h.html", + "codes": { + "a": { + "label": "Silver halide" + }, + "b": { + "label": "Diazo" + }, + "c": { + "label": "Vesicular" + }, + "m": { + "label": "Mixed emulsion" + }, + "n": { + "label": "Not applicable" + }, + "u": { + "label": "Unknown" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "11": { + "label": "Generation", + "url": "https://www.loc.gov/marc/bibliographic/bd007h.html", + "codes": { + "a": { + "label": "First generation (master)" + }, + "b": { + "label": "Printing master" + }, + "c": { + "label": "Service copy" + }, + "m": { + "label": "Mixed generation" + }, + "u": { + "label": "Unknown" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "12": { + "label": "Base of film", + "url": "https://www.loc.gov/marc/bibliographic/bd007h.html", + "codes": { + "a": { + "label": "Safety base, undetermined" + }, + "c": { + "label": "Safety base, acetate undetermined" + }, + "d": { + "label": "Safety base, diacetate" + }, + "i": { + "label": "Nitrate base" + }, + "m": { + "label": "Mixed base (nitrate and safety)" + }, + "n": { + "label": "Not applicable" + }, + "p": { + "label": "Safety base, polyester" + }, + "r": { + "label": "Safety base, mixed" + }, + "t": { + "label": "Safety base, triacetate" + }, + "u": { + "label": "Unknown" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + }, + "historical-codes": { + "b": { + "label": "Not safety base [OBSOLETE, 1991]" + } + } + } + } + }, + "Nonprojected graphic": { + "positions": { + "00": { + "label": "Category of material", + "url": "https://www.loc.gov/marc/bibliographic/bd007k.html", + "codes": { + "k": { + "label": "Nonprojected graphic" + } + } + }, + "01": { + "label": "Specific material designation", + "url": "https://www.loc.gov/marc/bibliographic/bd007k.html", + "codes": { + "a": { + "label": "Activity card" + }, + "c": { + "label": "Collage" + }, + "d": { + "label": "Drawing" + }, + "e": { + "label": "Painting" + }, + "f": { + "label": "Photomechanical print" + }, + "g": { + "label": "Photonegative" + }, + "h": { + "label": "Photoprint" + }, + "i": { + "label": "Picture" + }, + "j": { + "label": "Print" + }, + "k": { + "label": "Poster" + }, + "l": { + "label": "Technical drawing" + }, + "n": { + "label": "Chart" + }, + "o": { + "label": "Flash card" + }, + "p": { + "label": "Postcard" + }, + "q": { + "label": "Icon" + }, + "r": { + "label": "Radiograph" + }, + "s": { + "label": "Study print" + }, + "u": { + "label": "Unspecified" + }, + "v": { + "label": "Photograph, type unspecified" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "03": { + "label": "Color", + "url": "https://www.loc.gov/marc/bibliographic/bd007k.html", + "codes": { + "a": { + "label": "One color" + }, + "b": { + "label": "Black-and-white" + }, + "c": { + "label": "Multicolored" + }, + "h": { + "label": "Hand colored" + }, + "m": { + "label": "Mixed" + }, + "u": { + "label": "Unknown" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "04": { + "label": "Primary support material", + "url": "https://www.loc.gov/marc/bibliographic/bd007k.html", + "codes": { + "a": { + "label": "Canvas" + }, + "b": { + "label": "Bristol board" + }, + "c": { + "label": "Cardboard/illustration board" + }, + "d": { + "label": "Glass" + }, + "e": { + "label": "Synthetic" + }, + "f": { + "label": "Skin" + }, + "g": { + "label": "Textile" + }, + "h": { + "label": "Metal" + }, + "i": { + "label": "Plastic" + }, + "l": { + "label": "Vinyl" + }, + "m": { + "label": "Mixed collection" + }, + "n": { + "label": "Vellum" + }, + "o": { + "label": "Paper" + }, + "p": { + "label": "Plaster" + }, + "q": { + "label": "Hardboard" + }, + "r": { + "label": "Porcelain" + }, + "s": { + "label": "Stone" + }, + "t": { + "label": "Wood" + }, + "u": { + "label": "Unknown" + }, + "v": { + "label": "Leather" + }, + "w": { + "label": "Parchment" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "05": { + "label": "Secondary support material", + "url": "https://www.loc.gov/marc/bibliographic/bd007k.html", + "codes": { + " ": { + "label": "No secondary support" + }, + "a": { + "label": "Canvas" + }, + "b": { + "label": "Bristol board" + }, + "c": { + "label": "Cardboard/illustration board" + }, + "d": { + "label": "Glass" + }, + "e": { + "label": "Synthetic" + }, + "f": { + "label": "Skin" + }, + "g": { + "label": "Textile" + }, + "h": { + "label": "Metal" + }, + "i": { + "label": "Plastic" + }, + "l": { + "label": "Vinyl" + }, + "m": { + "label": "Mixed collection" + }, + "n": { + "label": "Vellum" + }, + "o": { + "label": "Paper" + }, + "p": { + "label": "Plaster" + }, + "q": { + "label": "Hardboard" + }, + "r": { + "label": "Porcelain" + }, + "s": { + "label": "Stone" + }, + "t": { + "label": "Wood" + }, + "u": { + "label": "Unknown" + }, + "v": { + "label": "Leather" + }, + "w": { + "label": "Parchment" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + } + } + }, + "Motion picture": { + "positions": { + "00": { + "label": "Category of material", + "url": "https://www.loc.gov/marc/bibliographic/bd007m.html", + "codes": { + "m": { + "label": "Motion picture" + } + } + }, + "01": { + "label": "Specific material designation", + "url": "https://www.loc.gov/marc/bibliographic/bd007m.html", + "codes": { + "c": { + "label": "Film cartridge" + }, + "f": { + "label": "Film cassette" + }, + "o": { + "label": "Film roll" + }, + "r": { + "label": "Film reel" + }, + "u": { + "label": "Unspecified" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "03": { + "label": "Color", + "url": "https://www.loc.gov/marc/bibliographic/bd007m.html", + "codes": { + "b": { + "label": "Black-and-white" + }, + "c": { + "label": "Multicolored" + }, + "h": { + "label": "Hand colored" + }, + "m": { + "label": "Mixed" + }, + "n": { + "label": "Not applicable" + }, + "u": { + "label": "Unknown" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "04": { + "label": "Motion picture presentation format", + "url": "https://www.loc.gov/marc/bibliographic/bd007m.html", + "codes": { + "a": { + "label": "Standard sound aperture (reduced frame)" + }, + "b": { + "label": "Nonanamorphic (wide-screen)" + }, + "c": { + "label": "3D" + }, + "d": { + "label": "Anamorphic (wide-screen)" + }, + "e": { + "label": "Other wide-screen format" + }, + "f": { + "label": "Standard silent aperture (full frame)" + }, + "u": { + "label": "Unknown" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + }, + "historical-codes": { + "n": { + "label": "Not applicable [OBSOLETE, 1983]" + } + } + }, + "05": { + "label": "Sound on medium or separate", + "url": "https://www.loc.gov/marc/bibliographic/bd007m.html", + "codes": { + " ": { + "label": "No sound (silent)" + }, + "a": { + "label": "Sound on medium" + }, + "b": { + "label": "Sound separate from medium" + }, + "u": { + "label": "Unknown" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "06": { + "label": "Medium for sound", + "url": "https://www.loc.gov/marc/bibliographic/bd007m.html", + "codes": { + " ": { + "label": "No sound (silent)" + }, + "a": { + "label": "Optical sound track on motion picture film" + }, + "b": { + "label": "Magnetic sound track on motion picture film" + }, + "c": { + "label": "Magnetic audio tape in cartridge" + }, + "d": { + "label": "Sound disc" + }, + "e": { + "label": "Magnetic audio tape on reel" + }, + "f": { + "label": "Magnetic audio tape in cassette" + }, + "g": { + "label": "Optical and magnetic sound track on motion picture film" + }, + "h": { + "label": "Videotape" + }, + "i": { + "label": "Videodisc" + }, + "u": { + "label": "Unknown" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "07": { + "label": "Dimensions", + "url": "https://www.loc.gov/marc/bibliographic/bd007m.html", + "codes": { + "a": { + "label": "Standard 8 mm." + }, + "b": { + "label": "Super 8 mm./single 8 mm." + }, + "c": { + "label": "9.5 mm." + }, + "d": { + "label": "16 mm." + }, + "e": { + "label": "28 mm." + }, + "f": { + "label": "35 mm." + }, + "g": { + "label": "70 mm." + }, + "u": { + "label": "Unknown" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "08": { + "label": "Configuration of playback channels", + "url": "https://www.loc.gov/marc/bibliographic/bd007m.html", + "codes": { + "k": { + "label": "Mixed" + }, + "m": { + "label": "Monaural" + }, + "n": { + "label": "Not applicable" + }, + "q": { + "label": "Quadraphonic, multichannel, or surround" + }, + "s": { + "label": "Stereophonic" + }, + "u": { + "label": "Unknown" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "09": { + "label": "Production elements", + "url": "https://www.loc.gov/marc/bibliographic/bd007m.html", + "codes": { + "a": { + "label": "Workprint" + }, + "b": { + "label": "Trims" + }, + "c": { + "label": "Outtakes" + }, + "d": { + "label": "Rushes" + }, + "e": { + "label": "Mixing tracks" + }, + "f": { + "label": "Title bands/inter-title rolls" + }, + "g": { + "label": "Production rolls" + }, + "n": { + "label": "Not applicable" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + }, + "historical-codes": { + "h": { + "label": "Other [OBSOLETE, 1988]" + } + } + }, + "10": { + "label": "Positive/negative aspect", + "url": "https://www.loc.gov/marc/bibliographic/bd007m.html", + "codes": { + "a": { + "label": "Positive" + }, + "b": { + "label": "Negative" + }, + "n": { + "label": "Not applicable" + }, + "u": { + "label": "Unknown" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "11": { + "label": "Generation", + "url": "https://www.loc.gov/marc/bibliographic/bd007m.html", + "codes": { + "d": { + "label": "Duplicate" + }, + "e": { + "label": "Master" + }, + "o": { + "label": "Original" + }, + "r": { + "label": "Reference print/viewing copy" + }, + "u": { + "label": "Unknown" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "12": { + "label": "Base of film", + "url": "https://www.loc.gov/marc/bibliographic/bd007m.html", + "codes": { + "a": { + "label": "Safety base, undetermined" + }, + "c": { + "label": "Safety base, acetate undetermined" + }, + "d": { + "label": "Safety base, diacetate" + }, + "i": { + "label": "Nitrate base" + }, + "m": { + "label": "Mixed base (nitrate and safety)" + }, + "n": { + "label": "Not applicable" + }, + "p": { + "label": "Safety base, polyester" + }, + "r": { + "label": "Safety base, mixed" + }, + "t": { + "label": "Safety base, triacetate" + }, + "u": { + "label": "Unknown" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "13": { + "label": "Refined categories of color", + "url": "https://www.loc.gov/marc/bibliographic/bd007m.html", + "codes": { + "a": { + "label": "3 layer color" + }, + "b": { + "label": "2 color, single strip" + }, + "c": { + "label": "Undetermined 2 color" + }, + "d": { + "label": "Undetermined 3 color" + }, + "e": { + "label": "3 strip color" + }, + "f": { + "label": "2 strip color" + }, + "g": { + "label": "Red strip" + }, + "h": { + "label": "Blue or green strip" + }, + "i": { + "label": "Cyan strip" + }, + "j": { + "label": "Magenta strip" + }, + "k": { + "label": "Yellow strip" + }, + "l": { + "label": "S E N 2" + }, + "m": { + "label": "S E N 3" + }, + "n": { + "label": "Not applicable" + }, + "p": { + "label": "Sepia tone" + }, + "q": { + "label": "Other tone" + }, + "r": { + "label": "Tint" + }, + "s": { + "label": "Tinted and toned" + }, + "t": { + "label": "Stencil color" + }, + "u": { + "label": "Unknown" + }, + "v": { + "label": "Hand colored" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "14": { + "label": "Kind of color stock or print", + "url": "https://www.loc.gov/marc/bibliographic/bd007m.html", + "codes": { + "a": { + "label": "Imbibition dye transfer prints" + }, + "b": { + "label": "Three-layer stock" + }, + "c": { + "label": "Three layer stock, low fade" + }, + "d": { + "label": "Duplitized stock" + }, + "n": { + "label": "Not applicable" + }, + "u": { + "label": "Unknown" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "15": { + "label": "Deterioration stage", + "url": "https://www.loc.gov/marc/bibliographic/bd007m.html", + "codes": { + "a": { + "label": "None apparent" + }, + "b": { + "label": "Nitrate: suspicious odor" + }, + "c": { + "label": "Nitrate: pungent odor" + }, + "d": { + "label": "Nitrate: brownish, discoloration, fading, dusty" + }, + "e": { + "label": "Nitrate: sticky" + }, + "f": { + "label": "Nitrate: frothy, bubbles, blisters" + }, + "g": { + "label": "Nitrate: congealed" + }, + "h": { + "label": "Nitrate: powder" + }, + "k": { + "label": "Non-nitrate: detectable deterioration" + }, + "l": { + "label": "Non-nitrate: advanced deterioration" + }, + "m": { + "label": "Non-nitrate: disaster" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "16": { + "label": "Completeness", + "url": "https://www.loc.gov/marc/bibliographic/bd007m.html", + "codes": { + "c": { + "label": "Complete" + }, + "i": { + "label": "Incomplete" + }, + "n": { + "label": "Not applicable" + }, + "u": { + "label": "Unknown" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "17-22": { + "label": "Film inspection date", + "url": "https://www.loc.gov/marc/bibliographic/bd007m.html" + } + } + }, + "Kit": { + "positions": { + "00": { + "label": "Category of material", + "url": "https://www.loc.gov/marc/bibliographic/bd007o.html", + "codes": { + "o": { + "label": "Kit" + } + } + }, + "01": { + "label": "Specific material designation", + "url": "https://www.loc.gov/marc/bibliographic/bd007o.html", + "codes": { + "u": { + "label": "Unspecified" + }, + "|": { + "label": "No attempt to code" + } + } + } + } + }, + "Notated music": { + "positions": { + "00": { + "label": "Category of material", + "url": "https://www.loc.gov/marc/bibliographic/bd007q.html", + "codes": { + "q": { + "label": "Notated music" + } + } + }, + "01": { + "label": "Specific material designation", + "url": "https://www.loc.gov/marc/bibliographic/bd007q.html", + "codes": { + "u": { + "label": "Unspecified" + }, + "|": { + "label": "No attempt to code" + } + } + } + } + }, + "Remote-sensing image": { + "positions": { + "00": { + "label": "Category of material", + "url": "https://www.loc.gov/marc/bibliographic/bd007r.html", + "codes": { + "r": { + "label": "Remote-sensing image" + } + } + }, + "01": { + "label": "Specific material designation", + "url": "https://www.loc.gov/marc/bibliographic/bd007r.html", + "codes": { + "u": { + "label": "Unspecified" + }, + "|": { + "label": "No attempt to code" + } + }, + "historical-codes": { + " ": { + "label": "No type specified [OBSOLETE, 1998]" + } + } + }, + "03": { + "label": "Altitude of sensor", + "url": "https://www.loc.gov/marc/bibliographic/bd007r.html", + "codes": { + "a": { + "label": "Surface" + }, + "b": { + "label": "Airborne" + }, + "c": { + "label": "Spaceborne" + }, + "n": { + "label": "Not applicable" + }, + "u": { + "label": "Unknown" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "04": { + "label": "Attitude of sensor", + "url": "https://www.loc.gov/marc/bibliographic/bd007r.html", + "codes": { + "a": { + "label": "Low oblique" + }, + "b": { + "label": "High oblique" + }, + "c": { + "label": "Vertical" + }, + "n": { + "label": "Not applicable" + }, + "u": { + "label": "Unknown" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "05": { + "label": "Cloud cover", + "url": "https://www.loc.gov/marc/bibliographic/bd007r.html", + "codes": { + "0": { + "label": "0-9%" + }, + "1": { + "label": "10-19%" + }, + "2": { + "label": "20-29%" + }, + "3": { + "label": "30-39%" + }, + "4": { + "label": "40-49%" + }, + "5": { + "label": "50-59%" + }, + "6": { + "label": "60-69%" + }, + "7": { + "label": "70-79%" + }, + "8": { + "label": "80-89%" + }, + "9": { + "label": "90-100%" + }, + "n": { + "label": "Not applicable" + }, + "u": { + "label": "Unknown" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "06": { + "label": "Platform construction type", + "url": "https://www.loc.gov/marc/bibliographic/bd007r.html", + "codes": { + "a": { + "label": "Balloon" + }, + "b": { + "label": "Aircraft--low altitude" + }, + "c": { + "label": "Aircraft--medium altitude" + }, + "d": { + "label": "Aircraft--high altitude" + }, + "e": { + "label": "Manned spacecraft" + }, + "f": { + "label": "Unmanned spacecraft" + }, + "g": { + "label": "Land-based remote-sensing device" + }, + "h": { + "label": "Water surface-based remote-sensing device" + }, + "i": { + "label": "Submersible remote-sensing device" + }, + "n": { + "label": "Not applicable" + }, + "u": { + "label": "Unknown" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "07": { + "label": "Platform use category", + "url": "https://www.loc.gov/marc/bibliographic/bd007r.html", + "codes": { + "a": { + "label": "Meteorological" + }, + "b": { + "label": "Surface observing" + }, + "c": { + "label": "Space observing" + }, + "m": { + "label": "Mixed uses" + }, + "n": { + "label": "Not applicable" + }, + "u": { + "label": "Unknown" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "08": { + "label": "Sensor type", + "url": "https://www.loc.gov/marc/bibliographic/bd007r.html", + "codes": { + "a": { + "label": "Active" + }, + "b": { + "label": "Passive" + }, + "u": { + "label": "Unknown" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "09-10": { + "label": "Data type", + "url": "https://www.loc.gov/marc/bibliographic/bd007r.html", + "codes": { + "aa": { + "label": "Visible light" + }, + "da": { + "label": "Near infrared" + }, + "db": { + "label": "Middle infrared" + }, + "dc": { + "label": "Far infrared" + }, + "dd": { + "label": "Thermal infrared" + }, + "de": { + "label": "Shortwave infrared (SWIR)" + }, + "df": { + "label": "Reflective infrared" + }, + "dv": { + "label": "Combinations" + }, + "dz": { + "label": "Other infrared data" + }, + "ga": { + "label": "Sidelooking airborne radar (SLAR)" + }, + "gb": { + "label": "Synthetic aperture radar (SAR)-Single frequency" + }, + "gc": { + "label": "SAR-multi-frequency (multichannel)" + }, + "gd": { + "label": "SAR-like polarization" + }, + "ge": { + "label": "SAR-cross polarization" + }, + "gf": { + "label": "Infometric SAR" + }, + "gg": { + "label": "polarmetric SAR" + }, + "gu": { + "label": "Passive microwave mapping" + }, + "gz": { + "label": "Other microwave data" + }, + "ja": { + "label": "Far ultraviolet" + }, + "jb": { + "label": "Middle ultraviolet" + }, + "jc": { + "label": "Near ultraviolet" + }, + "jv": { + "label": "Ultraviolet combinations" + }, + "jz": { + "label": "Other ultraviolet data" + }, + "ma": { + "label": "Multi-spectral, multidata" + }, + "mb": { + "label": "Multi-temporal" + }, + "mm": { + "label": "Combination of various data types" + }, + "nn": { + "label": "Not applicable" + }, + "pa": { + "label": "Sonar--water depth" + }, + "pb": { + "label": "Sonar--bottom topography images, sidescan" + }, + "pc": { + "label": "Sonar--bottom topography, near-surface" + }, + "pd": { + "label": "Sonar--bottom topography, near-bottom" + }, + "pe": { + "label": "Seismic surveys" + }, + "pz": { + "label": "Other acoustical data" + }, + "ra": { + "label": "Gravity anomalies (general)" + }, + "rb": { + "label": "Free-air" + }, + "rc": { + "label": "Bouger" + }, + "rd": { + "label": "Isostatic" + }, + "sa": { + "label": "Magnetic field" + }, + "ta": { + "label": "radiometric surveys" + }, + "uu": { + "label": "Unknown" + }, + "zz": { + "label": "Other" + }, + "||": { + "label": "No attempt to code" + } + } + } + } + }, + "Sound recording": { + "positions": { + "00": { + "label": "Category of material", + "url": "https://www.loc.gov/marc/bibliographic/bd007s.html", + "codes": { + "s": { + "label": "Sound recording" + } + } + }, + "01": { + "label": "Specific material designation", + "url": "https://www.loc.gov/marc/bibliographic/bd007s.html", + "codes": { + "d": { + "label": "Sound disc" + }, + "e": { + "label": "Cylinder" + }, + "g": { + "label": "Sound cartridge" + }, + "i": { + "label": "Sound-track film" + }, + "q": { + "label": "Roll" + }, + "r": { + "label": "Remote" + }, + "s": { + "label": "Sound cassette" + }, + "t": { + "label": "Sound-tape reel" + }, + "u": { + "label": "Unspecified" + }, + "w": { + "label": "Wire recording" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + }, + "historical-codes": { + "c": { + "label": "Cylinder [OBSOLETE]" + }, + "f": { + "label": "Sound-track film [OBSOLETE]" + }, + "r": { + "label": "Roll [OBSOLETE]" + } + } + }, + "03": { + "label": "Speed", + "url": "https://www.loc.gov/marc/bibliographic/bd007s.html", + "codes": { + "a": { + "label": "16 rpm (discs)" + }, + "b": { + "label": "33 1/3 rpm (discs)" + }, + "c": { + "label": "45 rpm (discs)" + }, + "d": { + "label": "78 rpm (discs)" + }, + "e": { + "label": "8 rpm (discs)" + }, + "f": { + "label": "1.4 m. per second (discs)" + }, + "h": { + "label": "120 rpm (cylinders)" + }, + "i": { + "label": "160 rpm (cylinders)" + }, + "k": { + "label": "15/16 ips (tapes)" + }, + "l": { + "label": "1 7/8 ips (tapes)" + }, + "m": { + "label": "3 3/4 ips (tapes)" + }, + "n": { + "label": "Not applicable" + }, + "o": { + "label": "7 1/2 ips (tapes)" + }, + "p": { + "label": "15 ips (tapes)" + }, + "r": { + "label": "30 ips (tape)" + }, + "u": { + "label": "Unknown" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "04": { + "label": "Configuration of playback channels", + "url": "https://www.loc.gov/marc/bibliographic/bd007s.html", + "codes": { + "m": { + "label": "Monaural" + }, + "q": { + "label": "Quadraphonic, multichannel, or surround" + }, + "s": { + "label": "Stereophonic" + }, + "u": { + "label": "Unknown" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + }, + "historical-codes": { + "a": { + "label": "Acoustic [OBSOLETE]" + }, + "f": { + "label": "Monaural (digital) [OBSOLETE]" + }, + "g": { + "label": "Quadraphonic (digital) [OBSOLETE]" + }, + "j": { + "label": "Stereophonic (digital) [OBSOLETE]" + }, + "k": { + "label": "Other (digital) [OBSOLETE]" + }, + "o": { + "label": "Other (electric) [OBSOLETE]" + } + } + }, + "05": { + "label": "Groove width/groove pitch", + "url": "https://www.loc.gov/marc/bibliographic/bd007s.html", + "codes": { + "m": { + "label": "Microgroove/fine" + }, + "n": { + "label": "Not applicable" + }, + "s": { + "label": "Coarse/standard" + }, + "u": { + "label": "Unknown" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "06": { + "label": "Dimensions", + "url": "https://www.loc.gov/marc/bibliographic/bd007s.html", + "codes": { + "a": { + "label": "3 in. diameter" + }, + "b": { + "label": "5 in. diameter" + }, + "c": { + "label": "7 in. diameter" + }, + "d": { + "label": "10 in. diameter" + }, + "e": { + "label": "12 in. diameter" + }, + "f": { + "label": "16 in. diameter" + }, + "g": { + "label": "4 3/4 in. or 12 cm. diameter" + }, + "j": { + "label": "3 7/8 x 2 1/2 in." + }, + "n": { + "label": "Not applicable" + }, + "o": { + "label": "5 1/4 x 3 7/8 in." + }, + "s": { + "label": "2 3/4 x 4 in." + }, + "u": { + "label": "Unknown" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "07": { + "label": "Tape width", + "url": "https://www.loc.gov/marc/bibliographic/bd007s.html", + "codes": { + "l": { + "label": "1/8 in." + }, + "m": { + "label": "1/4 in." + }, + "n": { + "label": "Not applicable" + }, + "o": { + "label": "1/2 in." + }, + "p": { + "label": "1 in." + }, + "u": { + "label": "Unknown" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + }, + "historical-codes": { + "a": { + "label": "1/4 in. [OBSOLETE]" + }, + "b": { + "label": "1/2 in. [OBSOLETE]" + }, + "c": { + "label": "1 in. [OBSOLETE]" + } + } + }, + "08": { + "label": "Tape configuration", + "url": "https://www.loc.gov/marc/bibliographic/bd007s.html", + "codes": { + "a": { + "label": "Full (1) track" + }, + "b": { + "label": "Half (2) track" + }, + "c": { + "label": "Quarter (4) track" + }, + "d": { + "label": "Eight track" + }, + "e": { + "label": "Twelve track" + }, + "f": { + "label": "Sixteen track" + }, + "n": { + "label": "Not applicable" + }, + "u": { + "label": "Unknown" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "09": { + "label": "Kind of disc, cylinder, or tape", + "url": "https://www.loc.gov/marc/bibliographic/bd007s.html", + "codes": { + "a": { + "label": "Master tape" + }, + "b": { + "label": "Tape duplication master" + }, + "d": { + "label": "Disc master (negative)" + }, + "i": { + "label": "Instantaneous (recorded on the spot)" + }, + "m": { + "label": "Mass-produced" + }, + "n": { + "label": "Not applicable" + }, + "r": { + "label": "Mother (positive)" + }, + "s": { + "label": "Stamper (negative)" + }, + "t": { + "label": "Test pressing" + }, + "u": { + "label": "Unknown" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "10": { + "label": "Kind of material", + "url": "https://www.loc.gov/marc/bibliographic/bd007s.html", + "codes": { + "a": { + "label": "Lacquer coating" + }, + "b": { + "label": "Cellulose nitrate" + }, + "c": { + "label": "Acetate tape with ferrous oxide" + }, + "g": { + "label": "Glass with lacquer" + }, + "i": { + "label": "Aluminum with lacquer" + }, + "l": { + "label": "Metal" + }, + "m": { + "label": "Plastic with metal" + }, + "n": { + "label": "Not applicable" + }, + "p": { + "label": "Plastic" + }, + "r": { + "label": "Paper with lacquer or ferrous oxide" + }, + "s": { + "label": "Shellac" + }, + "w": { + "label": "Wax" + }, + "u": { + "label": "Unknown" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "11": { + "label": "Kind of cutting", + "url": "https://www.loc.gov/marc/bibliographic/bd007s.html", + "codes": { + "h": { + "label": "Hill-and-dale cutting" + }, + "l": { + "label": "Lateral or combined cutting" + }, + "n": { + "label": "Not applicable" + }, + "u": { + "label": "Unknown" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "12": { + "label": "Special playback characteristics", + "url": "https://www.loc.gov/marc/bibliographic/bd007s.html", + "codes": { + "a": { + "label": "NAB standard" + }, + "b": { + "label": "CCIR standard" + }, + "c": { + "label": "Dolby-B encoded" + }, + "d": { + "label": "dbx encoded" + }, + "e": { + "label": "Digital recording" + }, + "f": { + "label": "Dolby-A encoded" + }, + "g": { + "label": "Dolby-C encoded" + }, + "h": { + "label": "CX encoded" + }, + "n": { + "label": "Not applicable" + }, + "u": { + "label": "Unknown" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "13": { + "label": "Capture and storage technique", + "url": "https://www.loc.gov/marc/bibliographic/bd007s.html", + "codes": { + "a": { + "label": "Acoustical capture, direct storage" + }, + "b": { + "label": "Direct storage, not acoustical" + }, + "d": { + "label": "Digital storage" + }, + "e": { + "label": "Analog electrical storage" + }, + "u": { + "label": "Unknown" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + } + } + }, + "Text": { + "positions": { + "00": { + "label": "Category of material", + "url": "https://www.loc.gov/marc/bibliographic/bd007t.html", + "codes": { + "t": { + "label": "Text" + } + } + }, + "01": { + "label": "Specific material designation", + "url": "https://www.loc.gov/marc/bibliographic/bd007t.html", + "codes": { + "a": { + "label": "Regular print" + }, + "b": { + "label": "Large print" + }, + "c": { + "label": "Braille" + }, + "d": { + "label": "Loose-leaf" + }, + "u": { + "label": "Unspecified" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + } + } + }, + "Videorecording": { + "positions": { + "00": { + "label": "Category of material", + "url": "https://www.loc.gov/marc/bibliographic/bd007v.html", + "codes": { + "v": { + "label": "Videorecording" + } + } + }, + "01": { + "label": "Specific material designation", + "url": "https://www.loc.gov/marc/bibliographic/bd007v.html", + "codes": { + "c": { + "label": "Videocartridge" + }, + "d": { + "label": "Videodisc" + }, + "f": { + "label": "Videocassette" + }, + "r": { + "label": "Videoreel" + }, + "u": { + "label": "Unspecified" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + }, + "historical-codes": { + " ": { + "label": "Not applicable or no attempt to code [OBSOLETE, 1980]" + }, + "n": { + "label": "Not applicable [OBSOLETE, 1981]" + } + } + }, + "03": { + "label": "Color", + "url": "https://www.loc.gov/marc/bibliographic/bd007v.html", + "codes": { + "a": { + "label": "One color" + }, + "b": { + "label": "Black-and-white" + }, + "c": { + "label": "Multicolored" + }, + "m": { + "label": "Mixed" + }, + "n": { + "label": "Not applicable" + }, + "u": { + "label": "Unknown" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "04": { + "label": "Videorecording format", + "url": "https://www.loc.gov/marc/bibliographic/bd007v.html", + "codes": { + "a": { + "label": "Beta (1/2 in., videocassette)" + }, + "b": { + "label": "VHS (1/2 in., videocassette)" + }, + "c": { + "label": "U-matic (3/4 in., videocasstte)" + }, + "d": { + "label": "EIAJ (1/2 in., reel)" + }, + "e": { + "label": "Type C (1 in., reel)" + }, + "f": { + "label": "Quadruplex (1 in. or 2 in., reel)" + }, + "g": { + "label": "Laserdisc" + }, + "h": { + "label": "CED (Capacitance Electronic Disc) videodisc" + }, + "i": { + "label": "Betacam (1/2 in., videocassette)" + }, + "j": { + "label": "Betacam SP (1/2 in., videocassette)" + }, + "k": { + "label": "Super-VHS (1/2 in., videocassette)" + }, + "m": { + "label": "M-II (1/2 in., videocassette)" + }, + "o": { + "label": "D-2 (3/4 in., videocassette)" + }, + "p": { + "label": "8 mm." + }, + "q": { + "label": "Hi-8 mm." + }, + "s": { + "label": "Blu-ray disc" + }, + "u": { + "label": "Unknown" + }, + "v": { + "label": "DVD" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + }, + "historical-codes": { + " ": { + "label": "Not applicable or no attempt to code [OBSOLETE, 1980]" + }, + "n": { + "label": "Not applicable [OBSOLETE, 1981]" + } + } + }, + "05": { + "label": "Sound on medium or separate", + "url": "https://www.loc.gov/marc/bibliographic/bd007v.html", + "codes": { + " ": { + "label": "No sound (silent)" + }, + "a": { + "label": "Sound on medium" + }, + "b": { + "label": "Sound separate from medium" + }, + "u": { + "label": "Unknown" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "06": { + "label": "Medium for sound", + "url": "https://www.loc.gov/marc/bibliographic/bd007v.html", + "codes": { + " ": { + "label": "No sound (silent)" + }, + "a": { + "label": "Optical sound track on motion picture film" + }, + "b": { + "label": "Magnetic sound track on motion picture film" + }, + "c": { + "label": "Magnetic audio tape in cartridge" + }, + "d": { + "label": "Sound disc" + }, + "e": { + "label": "Magnetic audio tape on reel" + }, + "f": { + "label": "Magnetic audio tape in cassette" + }, + "g": { + "label": "Optical and magnetic sound track on motion picture film" + }, + "h": { + "label": "Videotape" + }, + "i": { + "label": "Videodisc" + }, + "u": { + "label": "Unknown" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + }, + "historical-codes": { + "g": { + "label": "Other [OBSOLETE, 1980]" + } + } + }, + "07": { + "label": "Dimensions", + "url": "https://www.loc.gov/marc/bibliographic/bd007v.html", + "codes": { + "a": { + "label": "8 mm." + }, + "m": { + "label": "1/4 in." + }, + "o": { + "label": "1/2 in." + }, + "p": { + "label": "1 in." + }, + "q": { + "label": "2 in." + }, + "r": { + "label": "3/4 in." + }, + "u": { + "label": "Unknown" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + }, + "historical-codes": { + "n": { + "label": "1/4 in. [OBSOLETE, 1981]" + } + } + }, + "08": { + "label": "Configuration of playback channels", + "url": "https://www.loc.gov/marc/bibliographic/bd007v.html", + "codes": { + "k": { + "label": "Mixed" + }, + "m": { + "label": "Monaural" + }, + "n": { + "label": "Not applicable" + }, + "q": { + "label": "Quadraphonic, multichannel, or surround" + }, + "s": { + "label": "Stereophonic" + }, + "u": { + "label": "Unknown" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + } + } + }, + "Unspecified": { + "positions": { + "00": { + "label": "Category of material", + "url": "https://www.loc.gov/marc/bibliographic/bd007z.html", + "codes": { + "z": { + "label": "Unspecified" + } + } + }, + "01": { + "label": "Specific material designation", + "url": "https://www.loc.gov/marc/bibliographic/bd007z.html", + "codes": { + "m": { + "label": "Multiple physical forms" + }, + "u": { + "label": "Unspecified" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + } + } + } + } + }, + "008": { + "tag": "008", + "label": "General Information", + "repeatable": false, + "types": { + "All Materials": { + "positions": { + "00-05": { + "label": "Date entered on file", + "url": "https://www.loc.gov/marc/bibliographic/bd008a.html" + }, + "06": { + "label": "Type of date/Publication status", + "url": "https://www.loc.gov/marc/bibliographic/bd008a.html", + "codes": { + "b": { + "label": "No dates given; B.C. date involved" + }, + "c": { + "label": "Continuing resource currently published" + }, + "d": { + "label": "Continuing resource ceased publication" + }, + "e": { + "label": "Detailed date" + }, + "i": { + "label": "Inclusive dates of collection" + }, + "k": { + "label": "Range of years of bulk of collection" + }, + "m": { + "label": "Multiple dates" + }, + "n": { + "label": "Dates unknown" + }, + "p": { + "label": "Date of distribution/release/issue and production/recording session when different" + }, + "q": { + "label": "Questionable date" + }, + "r": { + "label": "Reprint/reissue date and original date" + }, + "s": { + "label": "Single known date/probable date" + }, + "t": { + "label": "Publication date and copyright date" + }, + "u": { + "label": "Continuing resource status unknown" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "07-10": { + "label": "Date 1", + "url": "https://www.loc.gov/marc/bibliographic/bd008a.html" + }, + "11-14": { + "label": "Date 2", + "url": "https://www.loc.gov/marc/bibliographic/bd008a.html" + }, + "15-17": { + "label": "Place of publication, production, or execution", + "url": "https://www.loc.gov/marc/bibliographic/bd008a.html" + }, + "35-37": { + "label": "Language", + "url": "https://www.loc.gov/marc/bibliographic/bd008a.html" + }, + "38": { + "label": "Modified record", + "url": "https://www.loc.gov/marc/bibliographic/bd008a.html", + "codes": { + " ": { + "label": "Not modified" + }, + "d": { + "label": "Dashed-on information omitted" + }, + "o": { + "label": "Completely romanized/printed cards romanized" + }, + "r": { + "label": "Completely romanized/printed cards in script" + }, + "s": { + "label": "Shortened" + }, + "x": { + "label": "Missing characters" + }, + "|": { + "label": "No attempt to code" + } + }, + "historical-codes": { + "u": { + "label": "Unknown [OBSOLETE] [CAN/MARC only]" + } + } + }, + "39": { + "label": "Cataloging source", + "url": "https://www.loc.gov/marc/bibliographic/bd008a.html", + "codes": { + " ": { + "label": "National bibliographic agency" + }, + "c": { + "label": "Cooperative cataloging program" + }, + "d": { + "label": "Other" + }, + "u": { + "label": "Unknown" + }, + "|": { + "label": "No attempt to code" + } + }, + "historical-codes": { + "a": { + "label": "National Agricultural Library [OBSOLETE, 1997] [USMARC only]" + }, + "b": { + "label": "National Library of Medicine [OBSOLETE, 1997] [USMARC only]" + }, + "l": { + "label": "Library of Congress cataloguing [OBSOLETE, 1997] [CAN/MARC only]" + }, + "o": { + "label": "Other institution cataloguing [OBSOLETE, 1997] [CAN/MARC only]" + }, + "n": { + "label": "Report to New serials titles [OBSOLETE, 1997] [USMARC only]" + }, + "r": { + "label": "Reporting library [OBSOLETE, 1997] [CAN/MARC only]" + } + } + } + } + }, + "Books": { + "positions": { + "18-21": { + "label": "Illustrations", + "url": "https://www.loc.gov/marc/bibliographic/bd008b.html", + "codes": { + " ": { + "label": "No illustrations" + }, + "a": { + "label": "Illustrations" + }, + "b": { + "label": "Maps" + }, + "c": { + "label": "Portraits" + }, + "d": { + "label": "Charts" + }, + "e": { + "label": "Plans" + }, + "f": { + "label": "Plates" + }, + "g": { + "label": "Music" + }, + "h": { + "label": "Facsimiles" + }, + "i": { + "label": "Coats of arms" + }, + "j": { + "label": "Genealogical tables" + }, + "k": { + "label": "Forms" + }, + "l": { + "label": "Samples" + }, + "m": { + "label": "Phonodisc, phonowire, etc." + }, + "o": { + "label": "Photographs" + }, + "p": { + "label": "Illuminations" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "22": { + "label": "Target audience", + "url": "https://www.loc.gov/marc/bibliographic/bd008b.html", + "codes": { + " ": { + "label": "Unknown or not specified" + }, + "a": { + "label": "Preschool" + }, + "b": { + "label": "Primary" + }, + "c": { + "label": "Pre-adolescent" + }, + "d": { + "label": "Adolescent" + }, + "e": { + "label": "Adult" + }, + "f": { + "label": "Specialized" + }, + "g": { + "label": "General" + }, + "j": { + "label": "Juvenile" + }, + "|": { + "label": "No attempt to code" + } + }, + "historical-codes": { + "u": { + "label": "School material at first level [OBSOLETE]" + }, + "v": { + "label": "School material at second level [OBSOLETE]" + } + } + }, + "23": { + "label": "Form of item", + "url": "https://www.loc.gov/marc/bibliographic/bd008b.html", + "codes": { + " ": { + "label": "None of the following" + }, + "a": { + "label": "Microfilm" + }, + "b": { + "label": "Microfiche" + }, + "c": { + "label": "Microopaque" + }, + "d": { + "label": "Large print" + }, + "f": { + "label": "Braille" + }, + "o": { + "label": "Online" + }, + "q": { + "label": "Direct electronic" + }, + "r": { + "label": "Regular print reproduction" + }, + "s": { + "label": "Electronic" + }, + "|": { + "label": "No attempt to code" + } + }, + "historical-codes": { + "g": { + "label": "Punched paper tape [OBSOLETE, 1987]" + }, + "h": { + "label": "Magnetic tape [OBSOLETE, 1987]" + }, + "i": { + "label": "Multimedia [OBSOLETE, 1987]" + }, + "z": { + "label": "Other form of reproduction [OBSOLETE, 1987]" + } + } + }, + "24-27": { + "label": "Nature of contents", + "url": "https://www.loc.gov/marc/bibliographic/bd008b.html", + "codes": { + " ": { + "label": "No specified nature of contents" + }, + "a": { + "label": "Abstracts/summaries" + }, + "b": { + "label": "Bibliographies" + }, + "c": { + "label": "Catalogs" + }, + "d": { + "label": "Dictionaries" + }, + "e": { + "label": "Encyclopedias" + }, + "f": { + "label": "Handbooks" + }, + "g": { + "label": "Legal articles" + }, + "i": { + "label": "Indexes" + }, + "j": { + "label": "Patent document" + }, + "k": { + "label": "Discographies" + }, + "l": { + "label": "Legislation" + }, + "m": { + "label": "Theses" + }, + "n": { + "label": "Surveys of literature in a subject area" + }, + "o": { + "label": "Reviews" + }, + "p": { + "label": "Programmed texts" + }, + "q": { + "label": "Filmographies" + }, + "r": { + "label": "Directories" + }, + "s": { + "label": "Statistics" + }, + "t": { + "label": "Technical reports" + }, + "u": { + "label": "Standards/specifications" + }, + "v": { + "label": "Legal cases and case notes" + }, + "w": { + "label": "Law reports and digests" + }, + "y": { + "label": "Yearbooks" + }, + "z": { + "label": "Treaties" + }, + "2": { + "label": "Offprints" + }, + "5": { + "label": "Calendars" + }, + "6": { + "label": "Comics/graphic novels" + }, + "|": { + "label": "No attempt to code" + } + }, + "historical-codes": { + "h": { + "label": "Handbooks [OBSOLETE]" + }, + "x": { + "label": "Technical reports [OBSOLETE, 1997]" + }, + "3": { + "label": "Discographies [OBSOLETE, 1997]" + }, + "4": { + "label": "Filmographies [OBSOLETE, 1997]" + } + } + }, + "28": { + "label": "Government publication", + "url": "https://www.loc.gov/marc/bibliographic/bd008b.html", + "codes": { + " ": { + "label": "Not a government publication" + }, + "a": { + "label": "Autonomous or semi-autonomous component" + }, + "c": { + "label": "Multilocal" + }, + "f": { + "label": "Federal/national" + }, + "i": { + "label": "International intergovernmental" + }, + "l": { + "label": "Local" + }, + "m": { + "label": "Multistate" + }, + "o": { + "label": "Government publication-level undetermined" + }, + "s": { + "label": "State, provincial, territorial, dependent, etc." + }, + "u": { + "label": "Unknown if item is government publication" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + }, + "historical-codes": { + "n": { + "label": "Government publication-level undetermined [OBSOLETE]" + } + } + }, + "29": { + "label": "Conference publication", + "url": "https://www.loc.gov/marc/bibliographic/bd008b.html", + "codes": { + "0": { + "label": "Not a conference publication" + }, + "1": { + "label": "Conference publication" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "30": { + "label": "Festschrift", + "url": "https://www.loc.gov/marc/bibliographic/bd008b.html", + "codes": { + "0": { + "label": "Not a festschrift" + }, + "1": { + "label": "Festschrift" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "31": { + "label": "Index", + "url": "https://www.loc.gov/marc/bibliographic/bd008b.html", + "codes": { + "0": { + "label": "No index" + }, + "1": { + "label": "Index present" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "33": { + "label": "Literary form", + "url": "https://www.loc.gov/marc/bibliographic/bd008b.html", + "codes": { + "0": { + "label": "Not fiction (not further specified)" + }, + "1": { + "label": "Fiction (not further specified)" + }, + "d": { + "label": "Dramas" + }, + "e": { + "label": "Essays" + }, + "f": { + "label": "Novels" + }, + "h": { + "label": "Humor, satires, etc." + }, + "i": { + "label": "Letters" + }, + "j": { + "label": "Short stories" + }, + "m": { + "label": "Mixed forms" + }, + "p": { + "label": "Poetry" + }, + "s": { + "label": "Speeches" + }, + "u": { + "label": "Unknown" + }, + "|": { + "label": "No attempt to code" + } + }, + "historical-codes": { + " ": { + "label": "Non-fiction [OBSOLETE, 1997]" + }, + "c": { + "label": "Comic strips [OBSOLETE, 2008]" + } + } + }, + "34": { + "label": "Biography", + "url": "https://www.loc.gov/marc/bibliographic/bd008b.html", + "codes": { + " ": { + "label": "No biographical material" + }, + "a": { + "label": "Autobiography" + }, + "b": { + "label": "Individual biography" + }, + "c": { + "label": "Collective biography" + }, + "d": { + "label": "Contains biographical information" + }, + "|": { + "label": "No attempt to code" + } + } + } + } + }, + "Continuing Resources": { + "positions": { + "18": { + "label": "Frequency", + "url": "https://www.loc.gov/marc/bibliographic/bd008s.html", + "codes": { + " ": { + "label": "No determinable frequency" + }, + "a": { + "label": "Annual" + }, + "b": { + "label": "Bimonthly" + }, + "c": { + "label": "Semiweekly" + }, + "d": { + "label": "Daily" + }, + "e": { + "label": "Biweekly" + }, + "f": { + "label": "Semiannual" + }, + "g": { + "label": "Biennial" + }, + "h": { + "label": "Triennial" + }, + "i": { + "label": "Three times a week" + }, + "j": { + "label": "Three times a month" + }, + "k": { + "label": "Continuously updated" + }, + "m": { + "label": "Monthly" + }, + "q": { + "label": "Quarterly" + }, + "s": { + "label": "Semimonthly" + }, + "t": { + "label": "Three times a year" + }, + "u": { + "label": "Unknown" + }, + "w": { + "label": "Weekly" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "19": { + "label": "Regularity", + "url": "https://www.loc.gov/marc/bibliographic/bd008s.html", + "codes": { + "n": { + "label": "Normalized irregular" + }, + "r": { + "label": "Regular" + }, + "u": { + "label": "Unknown" + }, + "x": { + "label": "Completely irregular" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "21": { + "label": "Type of continuing resource", + "url": "https://www.loc.gov/marc/bibliographic/bd008s.html", + "codes": { + " ": { + "label": "None of the following" + }, + "d": { + "label": "Updating database" + }, + "l": { + "label": "Updating loose-leaf" + }, + "m": { + "label": "Monographic series" + }, + "n": { + "label": "Newspaper" + }, + "p": { + "label": "Periodical" + }, + "w": { + "label": "Updating Web site" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "22": { + "label": "Form of original item", + "url": "https://www.loc.gov/marc/bibliographic/bd008s.html", + "codes": { + " ": { + "label": "None of the following" + }, + "a": { + "label": "Microfilm" + }, + "b": { + "label": "Microfiche" + }, + "c": { + "label": "Microopaque" + }, + "d": { + "label": "Large print" + }, + "e": { + "label": "Newspaper format" + }, + "f": { + "label": "Braille" + }, + "o": { + "label": "Online" + }, + "q": { + "label": "Direct electronic" + }, + "s": { + "label": "Electronic" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "23": { + "label": "Form of item", + "url": "https://www.loc.gov/marc/bibliographic/bd008s.html", + "codes": { + " ": { + "label": "None of the following" + }, + "a": { + "label": "Microfilm" + }, + "b": { + "label": "Microfiche" + }, + "c": { + "label": "Microopaque" + }, + "d": { + "label": "Large print" + }, + "f": { + "label": "Braille" + }, + "o": { + "label": "Online" + }, + "q": { + "label": "Direct electronic" + }, + "r": { + "label": "Regular print reproduction" + }, + "s": { + "label": "Electronic" + }, + "|": { + "label": "No attempt to code" + } + }, + "historical-codes": { + "g": { + "label": "Punched paper tape [OBSOLETE, 1987]" + }, + "h": { + "label": "Magnetic tape [OBSOLETE, 1987]" + }, + "i": { + "label": "Multimedia [OBSOLETE, 1987]" + }, + "z": { + "label": "Other [OBSOLETE, 1987]" + } + } + }, + "24": { + "label": "Nature of entire work", + "url": "https://www.loc.gov/marc/bibliographic/bd008s.html", + "codes": { + " ": { + "label": "Not specified" + }, + "a": { + "label": "Abstracts/summaries" + }, + "b": { + "label": "Bibliographies" + }, + "c": { + "label": "Catalogs" + }, + "d": { + "label": "Dictionaries" + }, + "e": { + "label": "Encyclopedias" + }, + "f": { + "label": "Handbooks" + }, + "g": { + "label": "Legal articles" + }, + "h": { + "label": "Biography" + }, + "i": { + "label": "Indexes" + }, + "k": { + "label": "Discographies" + }, + "l": { + "label": "Legislation" + }, + "m": { + "label": "Theses" + }, + "n": { + "label": "Surveys of literature in a subject area" + }, + "o": { + "label": "Reviews" + }, + "p": { + "label": "Programmed texts" + }, + "q": { + "label": "Filmographies" + }, + "r": { + "label": "Directories" + }, + "s": { + "label": "Statistics" + }, + "t": { + "label": "Technical reports" + }, + "u": { + "label": "Standards/specifications" + }, + "v": { + "label": "Legal cases and case notes" + }, + "w": { + "label": "Law reports and digests" + }, + "y": { + "label": "Yearbooks" + }, + "z": { + "label": "Treaties" + }, + "5": { + "label": "Calendars" + }, + "6": { + "label": "Comics/graphic novels" + }, + "|": { + "label": "No attempt to code" + } + }, + "historical-codes": { + "3": { + "label": "Discographies [OBSOLETE, 1997]" + }, + "4": { + "label": "Filmographies [OBSOLETE, 1997]" + } + } + }, + "25-27": { + "label": "Nature of contents", + "url": "https://www.loc.gov/marc/bibliographic/bd008s.html", + "codes": { + " ": { + "label": "Not specified" + }, + "a": { + "label": "Abstracts/summaries" + }, + "b": { + "label": "Bibliographies" + }, + "c": { + "label": "Catalogs" + }, + "d": { + "label": "Dictionaries" + }, + "e": { + "label": "Encyclopedias" + }, + "f": { + "label": "Handbooks" + }, + "g": { + "label": "Legal articles" + }, + "h": { + "label": "Biography" + }, + "i": { + "label": "Indexes" + }, + "k": { + "label": "Discographies" + }, + "l": { + "label": "Legislation" + }, + "m": { + "label": "Theses" + }, + "n": { + "label": "Surveys of literature in a subject area" + }, + "o": { + "label": "Reviews" + }, + "p": { + "label": "Programmed texts" + }, + "q": { + "label": "Filmographies" + }, + "r": { + "label": "Directories" + }, + "s": { + "label": "Statistics" + }, + "t": { + "label": "Technical reports" + }, + "u": { + "label": "Standards/specifications" + }, + "v": { + "label": "Legal cases and case notes" + }, + "w": { + "label": "Law reports and digests" + }, + "y": { + "label": "Yearbooks" + }, + "z": { + "label": "Treaties" + }, + "5": { + "label": "Calendars" + }, + "6": { + "label": "Comics/graphic novels" + }, + "|": { + "label": "No attempt to code" + } + }, + "historical-codes": { + "3": { + "label": "Discographies [OBSOLETE, 1997]" + }, + "4": { + "label": "Filmographies [OBSOLETE, 1997]" + } + } + }, + "28": { + "label": "Government publication", + "url": "https://www.loc.gov/marc/bibliographic/bd008s.html", + "codes": { + " ": { + "label": "Not a government publication" + }, + "a": { + "label": "Autonomous or semi-autonomous component" + }, + "c": { + "label": "Multilocal" + }, + "f": { + "label": "Federal/national" + }, + "i": { + "label": "International intergovernmental" + }, + "l": { + "label": "Local" + }, + "m": { + "label": "Multistate" + }, + "o": { + "label": "Government publication-level undetermined" + }, + "s": { + "label": "State, provincial, territorial, dependent, etc." + }, + "u": { + "label": "Unknown if item is government publication" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + }, + "historical-codes": { + "n": { + "label": "Government publication-level undetermined [OBSOLETE, 1979]" + } + } + }, + "29": { + "label": "Conference publication", + "url": "https://www.loc.gov/marc/bibliographic/bd008s.html", + "codes": { + "0": { + "label": "Not a conference publication" + }, + "1": { + "label": "Conference publication" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "33": { + "label": "Original alphabet or script of title", + "url": "https://www.loc.gov/marc/bibliographic/bd008s.html", + "codes": { + " ": { + "label": "No alphabet or script given/No key title" + }, + "a": { + "label": "Basic Roman" + }, + "b": { + "label": "Extended Roman" + }, + "c": { + "label": "Cyrillic" + }, + "d": { + "label": "Japanese" + }, + "e": { + "label": "Chinese" + }, + "f": { + "label": "Arabic" + }, + "g": { + "label": "Greek" + }, + "h": { + "label": "Hebrew" + }, + "i": { + "label": "Thai" + }, + "j": { + "label": "Devanagari" + }, + "k": { + "label": "Korean" + }, + "l": { + "label": "Tamil" + }, + "u": { + "label": "Unknown" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "34": { + "label": "Entry convention", + "url": "https://www.loc.gov/marc/bibliographic/bd008s.html", + "codes": { + "0": { + "label": "Successive entry" + }, + "1": { + "label": "Latest entry" + }, + "2": { + "label": "Integrated entry" + }, + "|": { + "label": "No attempt to code" + } + } + } + } + }, + "Music": { + "positions": { + "18-19": { + "label": "Form of composition", + "url": "https://www.loc.gov/marc/bibliographic/bd008m.html", + "codes": { + "an": { + "label": "Anthems" + }, + "bd": { + "label": "Ballads" + }, + "bg": { + "label": "Bluegrass music" + }, + "bl": { + "label": "Blues" + }, + "bt": { + "label": "Ballets" + }, + "ca": { + "label": "Chaconnes" + }, + "cb": { + "label": "Chants, Other religions" + }, + "cc": { + "label": "Chant, Christian" + }, + "cg": { + "label": "Concerti grossi" + }, + "ch": { + "label": "Chorales" + }, + "cl": { + "label": "Chorale preludes" + }, + "cn": { + "label": "Canons and rounds" + }, + "co": { + "label": "Concertos" + }, + "cp": { + "label": "Chansons, polyphonic" + }, + "cr": { + "label": "Carols" + }, + "cs": { + "label": "Chance compositions" + }, + "ct": { + "label": "Cantatas" + }, + "cy": { + "label": "Country music" + }, + "cz": { + "label": "Canzonas" + }, + "df": { + "label": "Dance forms" + }, + "dv": { + "label": "Divertimentos, serenades, cassations, divertissements, and notturni" + }, + "fg": { + "label": "Fugues" + }, + "fl": { + "label": "Flamenco" + }, + "fm": { + "label": "Folk music" + }, + "ft": { + "label": "Fantasias" + }, + "gm": { + "label": "Gospel music" + }, + "hy": { + "label": "Hymns" + }, + "jz": { + "label": "Jazz" + }, + "mc": { + "label": "Musical revues and comedies" + }, + "md": { + "label": "Madrigals" + }, + "mi": { + "label": "Minuets" + }, + "mo": { + "label": "Motets" + }, + "mp": { + "label": "Motion picture music" + }, + "mr": { + "label": "Marches" + }, + "ms": { + "label": "Masses" + }, + "mu": { + "label": "Multiple forms" + }, + "mz": { + "label": "Mazurkas" + }, + "nc": { + "label": "Nocturnes" + }, + "nn": { + "label": "Not applicable" + }, + "op": { + "label": "Operas" + }, + "or": { + "label": "Oratorios" + }, + "ov": { + "label": "Overtures" + }, + "pg": { + "label": "Program music" + }, + "pm": { + "label": "Passion music" + }, + "po": { + "label": "Polonaises" + }, + "pp": { + "label": "Popular music" + }, + "pr": { + "label": "Preludes" + }, + "ps": { + "label": "Passacaglias" + }, + "pt": { + "label": "Part-songs" + }, + "pv": { + "label": "Pavans" + }, + "rc": { + "label": "Rock music" + }, + "rd": { + "label": "Rondos" + }, + "rg": { + "label": "Ragtime music" + }, + "ri": { + "label": "Ricercars" + }, + "rp": { + "label": "Rhapsodies" + }, + "rq": { + "label": "Requiems" + }, + "sd": { + "label": "Square dance music" + }, + "sg": { + "label": "Songs" + }, + "sn": { + "label": "Sonatas" + }, + "sp": { + "label": "Symphonic poems" + }, + "st": { + "label": "Studies and exercises" + }, + "su": { + "label": "Suites" + }, + "sy": { + "label": "Symphonies" + }, + "tc": { + "label": "Toccatas" + }, + "tl": { + "label": "Teatro lirico" + }, + "ts": { + "label": "Trio-sonatas" + }, + "uu": { + "label": "Unknown" + }, + "vi": { + "label": "Villancicos" + }, + "vr": { + "label": "Variations" + }, + "wz": { + "label": "Waltzes" + }, + "za": { + "label": "Zarzuelas" + }, + "zz": { + "label": "Other" + }, + "||": { + "label": "No attempt to code" + } + } + }, + "20": { + "label": "Format of music", + "url": "https://www.loc.gov/marc/bibliographic/bd008m.html", + "codes": { + "a": { + "label": "Full score" + }, + "b": { + "label": "Miniature or study score" + }, + "c": { + "label": "Accompaniment reduced for keyboard" + }, + "d": { + "label": "Voice score with accompaniment omitted" + }, + "e": { + "label": "Condensed score or piano-conductor score" + }, + "g": { + "label": "Close score" + }, + "h": { + "label": "Chorus score" + }, + "i": { + "label": "Condensed score" + }, + "j": { + "label": "Performer-conductor part" + }, + "k": { + "label": "Vocal score" + }, + "l": { + "label": "Score" + }, + "m": { + "label": "Multiple score formats" + }, + "n": { + "label": "Not applicable" + }, + "p": { + "label": "Piano score" + }, + "u": { + "label": "Unknown" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "21": { + "label": "Music parts", + "url": "https://www.loc.gov/marc/bibliographic/bd008m.html", + "codes": { + " ": { + "label": "No parts in hand or not specified" + }, + "d": { + "label": "Instrumental and vocal parts" + }, + "e": { + "label": "Instrumental parts" + }, + "f": { + "label": "Vocal parts" + }, + "n": { + "label": "Not applicable" + }, + "u": { + "label": "Unknown" + }, + "|": { + "label": "No attempt to code" + } + }, + "historical-codes": { + "a": { + "label": "Parts exist" + } + } + }, + "22": { + "label": "Target audience", + "url": "https://www.loc.gov/marc/bibliographic/bd008m.html", + "codes": { + " ": { + "label": "Unknown or unspecified" + }, + "a": { + "label": "Preschool" + }, + "b": { + "label": "Primary" + }, + "c": { + "label": "Pre-adolescent" + }, + "d": { + "label": "Adolescent" + }, + "e": { + "label": "Adult" + }, + "f": { + "label": "Specialized" + }, + "g": { + "label": "General" + }, + "j": { + "label": "Juvenile" + }, + "|": { + "label": "No attempt to code" + } + }, + "historical-codes": { + "u": { + "label": "School material at first level [OBSOLETE] [CAN/MARC only]" + }, + "v": { + "label": "School material at second level [OBSOLETE] [CAN/MARC only]" + } + } + }, + "23": { + "label": "Form of item", + "url": "https://www.loc.gov/marc/bibliographic/bd008m.html", + "codes": { + " ": { + "label": "None of the following" + }, + "a": { + "label": "Microfilm" + }, + "b": { + "label": "Microfiche" + }, + "c": { + "label": "Microopaque" + }, + "d": { + "label": "Large print" + }, + "f": { + "label": "Braille" + }, + "o": { + "label": "Online" + }, + "q": { + "label": "Direct electronic" + }, + "r": { + "label": "Regular print reproduction" + }, + "s": { + "label": "Electronic" + }, + "|": { + "label": "No attempt to code" + } + }, + "historical-codes": { + "g": { + "label": "Punched paper tape [OBSOLETE, 1987]" + }, + "h": { + "label": "Magnetic tape [OBSOLETE, 1987]" + }, + "i": { + "label": "Multimedia [OBSOLETE, 1987]" + }, + "x": { + "label": "Other form of reproduction [OBSOLETE] [USMARC only]" + }, + "z": { + "label": "Other form of reproduction [OBSOLETE]" + } + } + }, + "24-29": { + "label": "Accompanying matter", + "url": "https://www.loc.gov/marc/bibliographic/bd008m.html", + "codes": { + " ": { + "label": "No accompanying matter" + }, + "a": { + "label": "Discography" + }, + "b": { + "label": "Bibliography" + }, + "c": { + "label": "Thematic index" + }, + "d": { + "label": "Libretto or text" + }, + "e": { + "label": "Biography of composer or author" + }, + "f": { + "label": "Biography of performer or history of ensemble" + }, + "g": { + "label": "Technical and/or historical information on instruments" + }, + "h": { + "label": "Technical information on music" + }, + "i": { + "label": "Historical information" + }, + "k": { + "label": "Ethnological information" + }, + "r": { + "label": "Instructional materials" + }, + "s": { + "label": "Music" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + }, + "historical-codes": { + "g": { + "label": "Punched paper tape [OBSOLETE, 1987]" + }, + "n": { + "label": "Not applicable [OBSOLETE, 1980]" + }, + "j": { + "label": "Historical information other than music [OBSOLETE, 1980]" + }, + "l": { + "label": "Biography of arranger or transcriber [OBSOLETE, 1997]" + } + } + }, + "30-31": { + "label": "Literary text for sound recordings", + "url": "https://www.loc.gov/marc/bibliographic/bd008m.html", + "codes": { + " ": { + "label": "Item is a music sound recording" + }, + "a": { + "label": "Autobiography" + }, + "b": { + "label": "Biography" + }, + "c": { + "label": "Conference proceedings" + }, + "d": { + "label": "Drama" + }, + "e": { + "label": "Essays" + }, + "f": { + "label": "Fiction" + }, + "g": { + "label": "Reporting" + }, + "h": { + "label": "History" + }, + "i": { + "label": "Instruction" + }, + "j": { + "label": "Language instruction" + }, + "k": { + "label": "Comedy" + }, + "l": { + "label": "Lectures, speeches" + }, + "m": { + "label": "Memoirs" + }, + "n": { + "label": "Not applicable" + }, + "o": { + "label": "Folktales" + }, + "p": { + "label": "Poetry" + }, + "r": { + "label": "Rehearsals" + }, + "s": { + "label": "Sounds" + }, + "t": { + "label": "Interviews" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "33": { + "label": "Transposition and arrangement", + "url": "https://www.loc.gov/marc/bibliographic/bd008m.html", + "codes": { + " ": { + "label": "Not arrangement or transposition or not specified" + }, + "a": { + "label": "Transposition" + }, + "b": { + "label": "Arrangement" + }, + "c": { + "label": "Both transposed and arranged" + }, + "n": { + "label": "Not applicable" + }, + "u": { + "label": "Unknown" + }, + "|": { + "label": "No attempt to code" + } + } + } + } + }, + "Maps": { + "positions": { + "18-21": { + "label": "Relief", + "url": "https://www.loc.gov/marc/bibliographic/bd008p.html", + "codes": { + " ": { + "label": "No relief shown" + }, + "a": { + "label": "Contours" + }, + "b": { + "label": "Shading" + }, + "c": { + "label": "Gradient and bathymetric tints" + }, + "d": { + "label": "Hachures" + }, + "e": { + "label": "Bathymetry/soundings" + }, + "f": { + "label": "Form lines" + }, + "g": { + "label": "Spot heights" + }, + "i": { + "label": "Pictorially" + }, + "j": { + "label": "Land forms" + }, + "k": { + "label": "Bathymetry/isolines" + }, + "m": { + "label": "Rock drawings" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + }, + "historical-codes": { + "h": { + "label": "Color [OBSOLETE, 1980]" + } + } + }, + "22-23": { + "label": "Projection", + "url": "https://www.loc.gov/marc/bibliographic/bd008p.html", + "codes": { + " ": { + "label": "Projection not specified" + }, + "aa": { + "label": "Aitoff" + }, + "ab": { + "label": "Gnomic" + }, + "ac": { + "label": "Lambert's azimuthal equal area" + }, + "ad": { + "label": "Orthographic" + }, + "ae": { + "label": "Azimuthal equidistant" + }, + "af": { + "label": "Stereographic" + }, + "ag": { + "label": "General vertical near-sided" + }, + "am": { + "label": "Modified stereographic for Alaska" + }, + "an": { + "label": "Chamberlin trimetric" + }, + "ap": { + "label": "Polar stereographic" + }, + "au": { + "label": "Azimuthal, specific type unknown" + }, + "az": { + "label": "Azimuthal, other" + }, + "ba": { + "label": "Gall" + }, + "bb": { + "label": "Goode's homolographic" + }, + "bc": { + "label": "Lambert's cylindrical equal area" + }, + "bd": { + "label": "Mercator" + }, + "be": { + "label": "Miller" + }, + "bf": { + "label": "Mollweide" + }, + "bg": { + "label": "Sinusoidal" + }, + "bh": { + "label": "Transverse Mercator" + }, + "bi": { + "label": "Gauss-Kruger" + }, + "bj": { + "label": "Equirectangular" + }, + "bk": { + "label": "Krovak" + }, + "bl": { + "label": "Cassini-Soldner" + }, + "bo": { + "label": "Oblique Mercator" + }, + "br": { + "label": "Robinson" + }, + "bs": { + "label": "Space oblique Mercator" + }, + "bu": { + "label": "Cylindrical, specific type unknown" + }, + "bz": { + "label": "Cylindrical, other" + }, + "ca": { + "label": "Albers equal area" + }, + "cb": { + "label": "Bonne" + }, + "cc": { + "label": "Lambert's conformal conic" + }, + "ce": { + "label": "Equidistant conic" + }, + "cp": { + "label": "Polyconic" + }, + "cu": { + "label": "Conic, specific type unknown" + }, + "cz": { + "label": "Conic, other" + }, + "da": { + "label": "Armadillo" + }, + "db": { + "label": "Butterfly" + }, + "dc": { + "label": "Eckert" + }, + "dd": { + "label": "Goode's homolosine" + }, + "de": { + "label": "Miller's bipolar oblique conformal conic" + }, + "df": { + "label": "Van Der Grinten" + }, + "dg": { + "label": "Dimaxion" + }, + "dh": { + "label": "Cordiform" + }, + "dl": { + "label": "Lambert conformal" + }, + "zz": { + "label": "Other" + }, + "||": { + "label": "No attempt to code" + } + } + }, + "25": { + "label": "Type of cartographic material", + "url": "https://www.loc.gov/marc/bibliographic/bd008p.html", + "codes": { + "a": { + "label": "Single map" + }, + "b": { + "label": "Map series" + }, + "c": { + "label": "Map serial" + }, + "d": { + "label": "Globe" + }, + "e": { + "label": "Atlas" + }, + "f": { + "label": "Separate supplement to another work" + }, + "g": { + "label": "Bound as part of another work" + }, + "u": { + "label": "Unknown" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "28": { + "label": "Government publication", + "url": "https://www.loc.gov/marc/bibliographic/bd008p.html", + "codes": { + " ": { + "label": "Not a government publication" + }, + "a": { + "label": "Autonomous or semi-autonomous component" + }, + "c": { + "label": "Multilocal" + }, + "f": { + "label": "Federal/national" + }, + "i": { + "label": "International intergovernmental" + }, + "l": { + "label": "Local" + }, + "m": { + "label": "Multistate" + }, + "o": { + "label": "Government publication-level undetermined" + }, + "s": { + "label": "State, provincial, territorial, dependent, etc." + }, + "u": { + "label": "Unknown if item is government publication" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "29": { + "label": "Form of item", + "url": "https://www.loc.gov/marc/bibliographic/bd008p.html", + "codes": { + " ": { + "label": "None of the following" + }, + "a": { + "label": "Microfilm" + }, + "b": { + "label": "Microfiche" + }, + "c": { + "label": "Microopaque" + }, + "d": { + "label": "Large print" + }, + "f": { + "label": "Braille" + }, + "o": { + "label": "Online" + }, + "q": { + "label": "Direct electronic" + }, + "r": { + "label": "Regular print reproduction" + }, + "s": { + "label": "Electronic" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "31": { + "label": "Index", + "url": "https://www.loc.gov/marc/bibliographic/bd008p.html", + "codes": { + "0": { + "label": "No index" + }, + "1": { + "label": "Index present" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "33-34": { + "label": "Special format characteristics", + "url": "https://www.loc.gov/marc/bibliographic/bd008p.html", + "codes": { + " ": { + "label": "No specified special format characteristics" + }, + "e": { + "label": "Manuscript" + }, + "j": { + "label": "Picture card, post card" + }, + "k": { + "label": "Calendar" + }, + "l": { + "label": "Puzzle" + }, + "n": { + "label": "Game" + }, + "o": { + "label": "Wall map" + }, + "p": { + "label": "Playing cards" + }, + "r": { + "label": "Loose-leaf" + }, + "z": { + "label": "Other" + }, + "||": { + "label": "No attempt to code" + } + }, + "historical-codes": { + "a": { + "label": "Photocopy, blue line print [OBSOLETE, 1982]" + }, + "b": { + "label": "Photocopy [OBSOLETE, 1982]" + }, + "c": { + "label": "Negative photocopy [OBSOLETE, 1982]" + }, + "d": { + "label": "Film negative [OBSOLETE, 1982]" + }, + "f": { + "label": "Facsimile [OBSOLETE, 1982]" + }, + "g": { + "label": "Relief model [OBSOLETE, 1982]" + }, + "h": { + "label": "Rare [OBSOLETE, 1982]" + }, + "m": { + "label": "Braille [OBSOLETE, 1998]" + }, + "q": { + "label": "Large print [OBSOLETE, 1998]" + } + } + } + } + }, + "Visual Materials": { + "positions": { + "18-20": { + "label": "Running time for motion pictures and videorecordings", + "url": "https://www.loc.gov/marc/bibliographic/bd008v.html", + "codes": { + "000": { + "label": "Running time exceeds three characters" + }, + "001-999": { + "label": "Running time" + }, + "nnn": { + "label": "Not applicable" + }, + "---": { + "label": "Unknown" + }, + "|||": { + "label": "No attempt to code" + } + } + }, + "22": { + "label": "Target audience", + "url": "https://www.loc.gov/marc/bibliographic/bd008v.html", + "codes": { + " ": { + "label": "Unknown or not specified" + }, + "a": { + "label": "Preschool" + }, + "b": { + "label": "Primary" + }, + "c": { + "label": "Pre-adolescent" + }, + "d": { + "label": "Adolescent" + }, + "e": { + "label": "Adult" + }, + "f": { + "label": "Specialized" + }, + "g": { + "label": "General" + }, + "j": { + "label": "Juvenile" + }, + "|": { + "label": "No attempt to code" + } + }, + "historical-codes": { + "f": { + "label": "General [OBSOLETE]" + }, + "g": { + "label": "Specialized [OBSOLETE]" + }, + "h": { + "label": "Secondary (grades 10-12) [OBSOLETE] [CAN/MARC only]" + }, + "k": { + "label": "Preschool and Kindergarten [OBSOLETE] [CAN/MARC only]" + }, + "m": { + "label": "Primary (grades 4-6) [OBSOLETE] [CAN/MARC only]" + }, + "p": { + "label": "Special education - general [OBSOLETE] [CAN/MARC only]" + }, + "q": { + "label": "Physically handicapped [OBSOLETE] [CAN/MARC only]" + }, + "r": { + "label": "Mentally retarded [OBSOLETE] [CAN/MARC only]" + }, + "s": { + "label": "Simplified works for adults [OBSOLETE] [CAN/MARC only]" + }, + "t": { + "label": "Gifted [OBSOLETE] [CAN/MARC only]" + } + } + }, + "28": { + "label": "Government publication", + "url": "https://www.loc.gov/marc/bibliographic/bd008v.html", + "codes": { + " ": { + "label": "Not a government publication" + }, + "a": { + "label": "Autonomous or semi-autonomous component" + }, + "c": { + "label": "Multilocal" + }, + "f": { + "label": "Federal/national" + }, + "i": { + "label": "International intergovernmental" + }, + "l": { + "label": "Local" + }, + "m": { + "label": "Multistate" + }, + "o": { + "label": "Government publication-level undetermined" + }, + "s": { + "label": "State, provincial, territorial, dependent, etc." + }, + "u": { + "label": "Unknown if item is government publication" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + }, + "historical-codes": { + "n": { + "label": "Government publication-level undetermined [OBSOLETE, 1979]" + } + } + }, + "29": { + "label": "Form of item", + "url": "https://www.loc.gov/marc/bibliographic/bd008v.html", + "codes": { + " ": { + "label": "None of the following" + }, + "a": { + "label": "Microfilm" + }, + "b": { + "label": "Microfiche" + }, + "c": { + "label": "Microopaque" + }, + "d": { + "label": "Large print" + }, + "f": { + "label": "Braille" + }, + "o": { + "label": "Online" + }, + "q": { + "label": "Direct electronic" + }, + "r": { + "label": "Regular print reproduction" + }, + "s": { + "label": "Electronic" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "33": { + "label": "Type of visual material", + "url": "https://www.loc.gov/marc/bibliographic/bd008v.html", + "codes": { + "a": { + "label": "Art original" + }, + "b": { + "label": "Kit" + }, + "c": { + "label": "Art reproduction" + }, + "d": { + "label": "Diorama" + }, + "f": { + "label": "Filmstrip" + }, + "g": { + "label": "Game" + }, + "i": { + "label": "Picture" + }, + "k": { + "label": "Graphic" + }, + "l": { + "label": "Technical drawing" + }, + "m": { + "label": "Motion picture" + }, + "n": { + "label": "Chart" + }, + "o": { + "label": "Flash card" + }, + "p": { + "label": "Microscope slide" + }, + "q": { + "label": "Model" + }, + "r": { + "label": "Realia" + }, + "s": { + "label": "Slide" + }, + "t": { + "label": "Transparency" + }, + "v": { + "label": "Videorecording" + }, + "w": { + "label": "Toy" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + }, + "historical-codes": { + "e": { + "label": "Electronic videorecording [OBSOLETE, 1975]" + } + } + }, + "34": { + "label": "Technique", + "url": "https://www.loc.gov/marc/bibliographic/bd008v.html", + "codes": { + "a": { + "label": "Animation" + }, + "c": { + "label": "Animation and live action" + }, + "l": { + "label": "Live action" + }, + "n": { + "label": "Not applicable" + }, + "u": { + "label": "Unknown" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + }, + "historical-codes": { + " ": { + "label": "Not applicable [OBSOLETE, 1980]" + } + } + } + } + }, + "Computer Files": { + "positions": { + "22": { + "label": "Target audience", + "url": "https://www.loc.gov/marc/bibliographic/bd008c.html", + "codes": { + " ": { + "label": "Unknown or not specified" + }, + "a": { + "label": "Preschool" + }, + "b": { + "label": "Primary" + }, + "c": { + "label": "Pre-adolescent" + }, + "d": { + "label": "Adolescent" + }, + "e": { + "label": "Adult" + }, + "f": { + "label": "Specialized" + }, + "g": { + "label": "General" + }, + "j": { + "label": "Juvenile" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "23": { + "label": "Form of item", + "url": "https://www.loc.gov/marc/bibliographic/bd008c.html", + "codes": { + " ": { + "label": "Unknown or not specified" + }, + "o": { + "label": "Online" + }, + "q": { + "label": "Direct electronic" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "26": { + "label": "Type of computer file", + "url": "https://www.loc.gov/marc/bibliographic/bd008c.html", + "codes": { + "a": { + "label": "Numeric data" + }, + "b": { + "label": "Computer program" + }, + "c": { + "label": "Representational" + }, + "d": { + "label": "Document" + }, + "e": { + "label": "Bibliographic data" + }, + "f": { + "label": "Font" + }, + "g": { + "label": "Game" + }, + "h": { + "label": "Sound" + }, + "i": { + "label": "Interactive multimedia" + }, + "j": { + "label": "Online system or service" + }, + "m": { + "label": "Combination" + }, + "u": { + "label": "Unknown" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + }, + "28": { + "label": "Government publication", + "url": "https://www.loc.gov/marc/bibliographic/bd008c.html", + "codes": { + " ": { + "label": "Not a government publication" + }, + "a": { + "label": "Autonomous or semi-autonomous component" + }, + "c": { + "label": "Multilocal" + }, + "f": { + "label": "Federal/national" + }, + "i": { + "label": "International intergovernmental" + }, + "l": { + "label": "Local" + }, + "m": { + "label": "Multistate" + }, + "o": { + "label": "Government publication-level undetermined" + }, + "s": { + "label": "State, provincial, territorial, dependent, etc." + }, + "u": { + "label": "Unknown if item is government publication" + }, + "z": { + "label": "Other" + }, + "|": { + "label": "No attempt to code" + } + } + } + } + }, + "Mixed Materials": { + "positions": { + "23": { + "label": "Form of item", + "url": "https://www.loc.gov/marc/bibliographic/bd008x.html", + "codes": { + " ": { + "label": "None of the following" + }, + "a": { + "label": "Microfilm" + }, + "b": { + "label": "Microfiche" + }, + "c": { + "label": "Microopaque" + }, + "d": { + "label": "Large print" + }, + "f": { + "label": "Braille" + }, + "o": { + "label": "Online" + }, + "q": { + "label": "Direct electronic" + }, + "r": { + "label": "Regular print reproduction" + }, + "s": { + "label": "Electronic" + }, + "|": { + "label": "No attempt to code" + } + }, + "historical-codes": { + "g": { + "label": "Punched paper tape [OBSOLETE, 1987]" + }, + "h": { + "label": "Magnetic tape [OBSOLETE, 1987]" + }, + "i": { + "label": "Multimedia [OBSOLETE, 1987]" + }, + "j": { + "label": "Handwritten transcript [OBSOLETE, 1987]" + }, + "p": { + "label": "Photocopy [OBSOLETE, 1987]" + }, + "t": { + "label": "Typewritten transcript [OBSOLETE, 1987]" + }, + "z": { + "label": "Other form of reproduction [OBSOLETE, 1987]" + } + } + } + } + } + } + }, + "010": { + "tag": "010", + "label": "Library of Congress Control Number", + "url": "https://www.loc.gov/marc/bibliographic/bd010.html", + "repeatable": false, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "LC control number", + "repeatable": false + }, + "b": { + "label": "NUCMC control number", + "repeatable": true + }, + "z": { + "label": "Canceled/invalid LC control number", + "repeatable": true + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "013": { + "tag": "013", + "label": "Patent Control Information", + "url": "https://www.loc.gov/marc/bibliographic/bd013.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Number", + "repeatable": false + }, + "b": { + "label": "Country", + "repeatable": false, + "codelist": { + "name": "MARC Code List for Countries", + "url": "http://www.loc.gov/marc/countries/countries_code.html" + } + }, + "c": { + "label": "Type of number", + "repeatable": false + }, + "d": { + "label": "Date", + "repeatable": true + }, + "e": { + "label": "Status", + "repeatable": true + }, + "f": { + "label": "Party to document", + "repeatable": true, + "codelist": { + "name": "MARC Organization Codes", + "url": "http://www.loc.gov/marc/organizations/orgshome.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "015": { + "tag": "015", + "label": "National Bibliography Number", + "url": "https://www.loc.gov/marc/bibliographic/bd015.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "National bibliography number", + "repeatable": true + }, + "q": { + "label": "Qualifying information", + "repeatable": true + }, + "z": { + "label": "Canceled/invalid national bibliography number", + "repeatable": true + }, + "2": { + "label": "Source", + "repeatable": false, + "codelist": { + "name": "National Bibliography Number Source Codes", + "url": "http://www.loc.gov/standards/sourcelist/national-bibliography.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "016": { + "tag": "016", + "label": "National Bibliographic Agency Control Number", + "url": "https://www.loc.gov/marc/bibliographic/bd016.html", + "repeatable": true, + "indicator1": { + "label": "National bibliographic agency", + "codes": { + " ": { + "label": "Library and Archives Canada" + }, + "7": { + "label": "Source specified in subfield $2" + } + } + }, + "indicator2": null, + "subfields": { + "a": { + "label": "Record control number", + "repeatable": false + }, + "z": { + "label": "Canceled/invalid control number", + "repeatable": true + }, + "2": { + "label": "Source", + "repeatable": false, + "codelist": { + "name": "MARC Organization Codes", + "url": "http://www.loc.gov/marc/organizations/orgshome.html" + } + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "017": { + "tag": "017", + "label": "Copyright or Legal Deposit Number", + "url": "https://www.loc.gov/marc/bibliographic/bd017.html", + "repeatable": true, + "indicator1": null, + "indicator2": { + "label": "Display constant controller", + "codes": { + " ": { + "label": "Copyright or legal deposit number" + }, + "8": { + "label": "No display constant generated" + } + } + }, + "subfields": { + "a": { + "label": "Copyright or legal deposit number", + "repeatable": true + }, + "b": { + "label": "Assigning agency", + "repeatable": false + }, + "d": { + "label": "Date", + "repeatable": false + }, + "i": { + "label": "Display text", + "repeatable": false + }, + "z": { + "label": "Canceled/invalid copyright or legal deposit number", + "repeatable": true + }, + "2": { + "label": "Source", + "repeatable": false, + "codelist": { + "name": "Copyright and Legal Deposit Number Source Codes", + "url": "http://www.loc.gov/standards/sourcelist/copyright-legal-deposit.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "018": { + "tag": "018", + "label": "Copyright Article-Fee Code", + "url": "https://www.loc.gov/marc/bibliographic/bd018.html", + "repeatable": false, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Copyright article-fee code", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "020": { + "tag": "020", + "label": "International Standard Book Number", + "url": "https://www.loc.gov/marc/bibliographic/bd020.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "International Standard Book Number", + "repeatable": false + }, + "c": { + "label": "Terms of availability", + "repeatable": false + }, + "q": { + "label": "Qualifying information", + "repeatable": true + }, + "z": { + "label": "Canceled/invalid ISBN", + "repeatable": true + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + }, + "historical-subfields": { + "b": { + "label": "Binding information (BK, MP, MU) [OBSOLETE]" + } + } + }, + "022": { + "tag": "022", + "label": "International Standard Serial Number", + "url": "https://www.loc.gov/marc/bibliographic/bd022.html", + "repeatable": true, + "indicator1": { + "label": "Level of international interest", + "codes": { + " ": { + "label": "No level specified" + }, + "0": { + "label": "Continuing resource of international interest" + }, + "1": { + "label": "Continuing resource not of international interest" + } + } + }, + "indicator2": null, + "subfields": { + "a": { + "label": "International Standard Serial Number", + "repeatable": false + }, + "l": { + "label": "ISSN-L", + "repeatable": false + }, + "m": { + "label": "Canceled ISSN-L", + "repeatable": true + }, + "y": { + "label": "Incorrect ISSN", + "repeatable": true + }, + "z": { + "label": "Canceled ISSN", + "repeatable": true + }, + "2": { + "label": "Source", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + }, + "historical-subfields": { + "b": { + "label": "Form of issue [OBSOLETE] [CAN/MARC only]" + }, + "c": { + "label": "Price [OBSOLETE] [CAN/MARC only]" + } + } + }, + "024": { + "tag": "024", + "label": "Other Standard Identifier", + "url": "https://www.loc.gov/marc/bibliographic/bd024.html", + "repeatable": true, + "indicator1": { + "label": "Type of standard number or code", + "codes": { + "0": { + "label": "International Standard Recording Code" + }, + "1": { + "label": "Universal Product Code" + }, + "2": { + "label": "International Standard Music Number" + }, + "3": { + "label": "International Article Number" + }, + "4": { + "label": "Serial Item and Contribution Identifier" + }, + "7": { + "label": "Source specified in subfield $2" + }, + "8": { + "label": "Unspecified type of standard number or code" + } + } + }, + "indicator2": { + "label": "Difference indicator", + "codes": { + " ": { + "label": "No information provided" + }, + "0": { + "label": "No difference" + }, + "1": { + "label": "Difference" + } + } + }, + "subfields": { + "a": { + "label": "Standard number or code", + "repeatable": false + }, + "c": { + "label": "Terms of availability", + "repeatable": false + }, + "d": { + "label": "Additional codes following the standard number or code", + "repeatable": false + }, + "q": { + "label": "Qualifying information", + "repeatable": true + }, + "z": { + "label": "Canceled/invalid standard number or code", + "repeatable": true + }, + "2": { + "label": "Source of number or code", + "repeatable": false, + "codelist": { + "name": "Standard Identifier Source Codes", + "url": "https://www.loc.gov/standards/sourcelist/standard-identifier.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + }, + "historical-subfields": { + "b": { + "label": "Additional codes following the standard number [OBSOLETE]" + } + } + }, + "025": { + "tag": "025", + "label": "Overseas Acquisition Number", + "url": "https://www.loc.gov/marc/bibliographic/bd025.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Overseas acquisition number", + "repeatable": true + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "026": { + "tag": "026", + "label": "Fingerprint Identifier", + "url": "https://www.loc.gov/marc/bibliographic/bd026.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "First and second groups of characters", + "repeatable": false + }, + "b": { + "label": "Third and fourth groups of characters", + "repeatable": false + }, + "c": { + "label": "Date", + "repeatable": false + }, + "d": { + "label": "Number of volume or part", + "repeatable": true + }, + "e": { + "label": "Unparsed fingerprint", + "repeatable": false + }, + "2": { + "label": "Source", + "repeatable": false, + "codelist": { + "name": "Fingerprint Scheme Source Codes", + "url": "http://www.loc.gov/standards/sourcelist/fingerprint.html" + } + }, + "5": { + "label": "Institution to which field applies", + "repeatable": true, + "codelist": { + "name": "MARC Organization Codes", + "url": "http://www.loc.gov/marc/organizations/orgshome.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "027": { + "tag": "027", + "label": "Standard Technical Report Number", + "url": "https://www.loc.gov/marc/bibliographic/bd027.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Standard technical report number", + "repeatable": false + }, + "q": { + "label": "Qualifying information", + "repeatable": true + }, + "z": { + "label": "Canceled/invalid number", + "repeatable": true + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "028": { + "tag": "028", + "label": "Publisher or Distributor Number", + "url": "https://www.loc.gov/marc/bibliographic/bd028.html", + "repeatable": true, + "indicator1": { + "label": "Type of number", + "codes": { + "0": { + "label": "Issue number" + }, + "1": { + "label": "Matrix number" + }, + "2": { + "label": "Plate number" + }, + "3": { + "label": "Other music publisher number" + }, + "4": { + "label": "Video recording publisher number" + }, + "5": { + "label": "Other publisher number" + }, + "6": { + "label": "Distributor number" + } + } + }, + "indicator2": { + "label": "Note/added entry controller", + "codes": { + "0": { + "label": "No note, no added entry" + }, + "1": { + "label": "Note, added entry" + }, + "2": { + "label": "Note, no added entry" + }, + "3": { + "label": "No note, added entry" + } + } + }, + "subfields": { + "a": { + "label": "Publisher or distributor number", + "repeatable": false + }, + "b": { + "label": "Source", + "repeatable": false + }, + "q": { + "label": "Qualifying information", + "repeatable": true + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "030": { + "tag": "030", + "label": "CODEN Designation", + "url": "https://www.loc.gov/marc/bibliographic/bd030.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Publisher or distributor number", + "repeatable": false + }, + "z": { + "label": "Canceled/invalid CODEN", + "repeatable": true + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "031": { + "tag": "031", + "label": "Musical Incipits Information", + "url": "https://www.loc.gov/marc/bibliographic/bd031.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Number of work", + "repeatable": false + }, + "b": { + "label": "Number of movement", + "repeatable": false + }, + "c": { + "label": "Number of excerpt", + "repeatable": false + }, + "d": { + "label": "Caption or heading", + "repeatable": true + }, + "e": { + "label": "Role", + "repeatable": false + }, + "g": { + "label": "Clef", + "repeatable": false + }, + "m": { + "label": "Voice/instrument", + "repeatable": false + }, + "n": { + "label": "Key signature", + "repeatable": false + }, + "o": { + "label": "Time signature", + "repeatable": false + }, + "p": { + "label": "Musical notation", + "repeatable": false + }, + "q": { + "label": "General note", + "repeatable": true + }, + "r": { + "label": "Key or mode", + "repeatable": false + }, + "s": { + "label": "Coded validity note", + "repeatable": true + }, + "t": { + "label": "Text incipit", + "repeatable": true + }, + "u": { + "label": "Uniform Resource Identifier", + "repeatable": true + }, + "y": { + "label": "Link text", + "repeatable": true + }, + "z": { + "label": "Public note", + "repeatable": true + }, + "2": { + "label": "System code", + "repeatable": false, + "codelist": { + "name": "Musical Incipit Scheme Source Codes", + "url": "https://www.loc.gov/standards/sourcelist/musical-incipit.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "032": { + "tag": "032", + "label": "Postal Registration Number", + "url": "https://www.loc.gov/marc/bibliographic/bd032.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Postal registration number", + "repeatable": false + }, + "b": { + "label": "Source agency assigning number", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "033": { + "tag": "033", + "label": "Date/Time and Place of an Event", + "url": "https://www.loc.gov/marc/bibliographic/bd033.html", + "repeatable": true, + "indicator1": { + "label": "Type of date in subfield $a", + "codes": { + " ": { + "label": "No date information" + }, + "0": { + "label": "Single date" + }, + "1": { + "label": "Multiple single dates" + }, + "2": { + "label": "Range of dates" + } + } + }, + "indicator2": { + "label": "Type of event", + "codes": { + " ": { + "label": "No information provided" + }, + "0": { + "label": "Capture" + }, + "1": { + "label": "Broadcast" + }, + "2": { + "label": "Finding" + } + } + }, + "subfields": { + "a": { + "label": "Formatted date/time", + "repeatable": true + }, + "b": { + "label": "Geographic classification area code", + "repeatable": true + }, + "c": { + "label": "Geographic classification subarea code", + "repeatable": true + }, + "p": { + "label": "Place of event", + "repeatable": true + }, + "0": { + "label": "Authority record control number", + "repeatable": true + }, + "2": { + "label": "Source of term", + "repeatable": true, + "codelist": { + "name": "Subject Heading and Term Source Codes", + "url": "http://www.loc.gov/standards/sourcelist/subject.html" + } + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "034": { + "tag": "034", + "label": "Coded Cartographic Mathematical Data", + "url": "https://www.loc.gov/marc/bibliographic/bd034.html", + "repeatable": true, + "indicator1": { + "label": "Type of scale", + "codes": { + "0": { + "label": "Scale indeterminable/No scale recorded" + }, + "1": { + "label": "Single scale" + }, + "3": { + "label": "Range of scales" + } + }, + "historical-codes": { + "2": { + "label": "Two or more scales (BK, MP, SE) [OBSOLETE]" + } + } + }, + "indicator2": { + "label": "Type of ring", + "codes": { + " ": { + "label": "Not applicable" + }, + "0": { + "label": "Outer ring" + }, + "1": { + "label": "Exclusion ring" + } + } + }, + "subfields": { + "a": { + "label": "Category of scale", + "repeatable": false + }, + "b": { + "label": "Constant ratio linear horizontal scale", + "repeatable": true + }, + "c": { + "label": "Constant ratio linear vertical scale", + "repeatable": true + }, + "d": { + "label": "Coordinates - westernmost longitude", + "repeatable": false + }, + "e": { + "label": "Coordinates - easternmost longitude", + "repeatable": false + }, + "f": { + "label": "Coordinates - northernmost latitude", + "repeatable": false + }, + "g": { + "label": "Coordinates - southernmost latitude", + "repeatable": false + }, + "h": { + "label": "Angular scale", + "repeatable": true + }, + "j": { + "label": "Declination - northern limit", + "repeatable": false + }, + "k": { + "label": "Declination - southern limit", + "repeatable": false + }, + "m": { + "label": "Right ascension - eastern limit", + "repeatable": false + }, + "n": { + "label": "Right ascension - western limit", + "repeatable": false + }, + "p": { + "label": "Equinox", + "repeatable": false + }, + "r": { + "label": "Distance from earth", + "repeatable": false + }, + "s": { + "label": "G-ring latitude", + "repeatable": true + }, + "t": { + "label": "G-ring longitude", + "repeatable": true + }, + "x": { + "label": "Beginning date", + "repeatable": false + }, + "y": { + "label": "Ending date", + "repeatable": false + }, + "z": { + "label": "Name of extraterrestrial body", + "repeatable": false + }, + "0": { + "label": "Authority record control number or standard number", + "repeatable": true + }, + "2": { + "label": "Source", + "repeatable": false, + "codelist": { + "name": "Cartographic Data Source Codes", + "url": "https://loc.gov/standards/sourcelist/cartographic-data.html" + } + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "035": { + "tag": "035", + "label": "System Control Number", + "url": "https://www.loc.gov/marc/bibliographic/bd035.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "System control number", + "repeatable": false + }, + "z": { + "label": "Canceled/invalid control number", + "repeatable": true + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "036": { + "tag": "036", + "label": "Original Study Number for Computer Data Files", + "url": "https://www.loc.gov/marc/bibliographic/bd036.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Original study number", + "repeatable": false + }, + "b": { + "label": "Source agency assigning number", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "037": { + "tag": "037", + "label": "Source of Acquisition", + "url": "https://www.loc.gov/marc/bibliographic/bd037.html", + "repeatable": true, + "indicator1": { + "label": "Source of acquisition sequence", + "codes": { + " ": { + "label": "Not applicable/No information provided/Earliest" + }, + "2": { + "label": "Intervening" + }, + "3": { + "label": "Current/Latest" + } + } + }, + "indicator2": null, + "subfields": { + "a": { + "label": "Stock number", + "repeatable": false + }, + "b": { + "label": "Source of stock number/acquisition", + "repeatable": false + }, + "c": { + "label": "Terms of availability", + "repeatable": true + }, + "f": { + "label": "Form of issue", + "repeatable": true + }, + "g": { + "label": "Additional format characteristics", + "repeatable": true + }, + "n": { + "label": "Note", + "repeatable": true + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "5": { + "label": "Institution to which field applies", + "repeatable": true + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "038": { + "tag": "038", + "label": "Record Content Licensor", + "url": "https://www.loc.gov/marc/bibliographic/bd038.html", + "repeatable": false, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Record content licensor", + "repeatable": false, + "codelist": { + "name": "MARC Organization Codes", + "url": "http://www.loc.gov/marc/organizations/orgshome.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "040": { + "tag": "040", + "label": "Cataloging Source", + "url": "https://www.loc.gov/marc/bibliographic/bd040.html", + "repeatable": false, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Original cataloging agency", + "repeatable": false, + "codelist": { + "name": "MARC Organization Codes", + "url": "http://www.loc.gov/marc/organizations/orgshome.html" + } + }, + "b": { + "label": "Language of cataloging", + "repeatable": false, + "codelist": { + "name": "MARC Code List for Languages", + "url": "http://www.loc.gov/marc/languages/language_code.html" + } + }, + "c": { + "label": "Transcribing agency", + "repeatable": false, + "codelist": { + "name": "MARC Organization Codes", + "url": "http://www.loc.gov/marc/organizations/orgshome.html" + } + }, + "d": { + "label": "Modifying agency", + "repeatable": true, + "codelist": { + "name": "MARC Organization Codes", + "url": "http://www.loc.gov/marc/organizations/orgshome.html" + } + }, + "e": { + "label": "Description conventions", + "repeatable": true, + "codelist": { + "name": "Description Convention Source Codes", + "url": "https://www.loc.gov/standards/sourcelist/descriptive-conventions.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "041": { + "tag": "041", + "label": "Language Code", + "url": "https://www.loc.gov/marc/bibliographic/bd041.html", + "repeatable": true, + "indicator1": { + "label": "Translation indication", + "codes": { + " ": { + "label": "No information provided" + }, + "0": { + "label": "Item not a translation/does not include a translation" + }, + "1": { + "label": "Item is or includes a translation" + } + } + }, + "indicator2": { + "label": "Source of code", + "codes": { + " ": { + "label": "MARC language code" + }, + "7": { + "label": "Source specified in subfield $2" + } + } + }, + "subfields": { + "a": { + "label": "Language code of text/sound track or separate title", + "repeatable": true, + "codelist": { + "name": "MARC Code List for Languages", + "url": "http://www.loc.gov/marc/languages/language_code.html" + } + }, + "b": { + "label": "Language code of summary or abstract", + "repeatable": true, + "codelist": { + "name": "MARC Code List for Languages", + "url": "http://www.loc.gov/marc/languages/language_code.html" + } + }, + "d": { + "label": "Language code of sung or spoken text", + "repeatable": true, + "codelist": { + "name": "MARC Code List for Languages", + "url": "http://www.loc.gov/marc/languages/language_code.html" + } + }, + "e": { + "label": "Language code of librettos", + "repeatable": true, + "codelist": { + "name": "MARC Code List for Languages", + "url": "http://www.loc.gov/marc/languages/language_code.html" + } + }, + "f": { + "label": "Language code of table of contents", + "repeatable": true, + "codelist": { + "name": "MARC Code List for Languages", + "url": "http://www.loc.gov/marc/languages/language_code.html" + } + }, + "g": { + "label": "Language code of accompanying material other than librettos", + "repeatable": true, + "codelist": { + "name": "MARC Code List for Languages", + "url": "http://www.loc.gov/marc/languages/language_code.html" + } + }, + "h": { + "label": "Language code of original", + "repeatable": true, + "codelist": { + "name": "MARC Code List for Languages", + "url": "http://www.loc.gov/marc/languages/language_code.html" + } + }, + "j": { + "label": "Language code of subtitles or captions", + "repeatable": true, + "codelist": { + "name": "MARC Code List for Languages", + "url": "http://www.loc.gov/marc/languages/language_code.html" + } + }, + "k": { + "label": "Language code of intermediate translations", + "repeatable": true, + "codelist": { + "name": "MARC Code List for Languages", + "url": "http://www.loc.gov/marc/languages/language_code.html" + } + }, + "m": { + "label": "Language code of original accompanying materials other than librettos", + "repeatable": true, + "codelist": { + "name": "MARC Code List for Languages", + "url": "http://www.loc.gov/marc/languages/language_code.html" + } + }, + "n": { + "label": "Language code of original libretto", + "repeatable": true, + "codelist": { + "name": "MARC Code List for Languages", + "url": "http://www.loc.gov/marc/languages/language_code.html" + } + }, + "2": { + "label": "Source of code", + "repeatable": false, + "codelist": { + "name": "Language Code and Term Source Codes", + "url": "http://www.loc.gov/standards/sourcelist/language.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + }, + "historical-subfields": { + "c": { + "label": "Languages of separate titles (VM) [OBSOLETE, 1972], Languages of available translation (SE) [OBSOLETE, 1977]" + } + } + }, + "042": { + "tag": "042", + "label": "Authentication Code", + "url": "https://www.loc.gov/marc/bibliographic/bd042.html", + "repeatable": false, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Authentication code", + "repeatable": true, + "codelist": { + "name": "MARC Authentication Action Code List", + "url": "http://www.loc.gov/standards/valuelist/marcauthen.html" + } + } + } + }, + "043": { + "tag": "043", + "label": "Geographic Area Code", + "url": "https://www.loc.gov/marc/bibliographic/bd043.html", + "repeatable": false, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Geographic area code", + "repeatable": true, + "codelist": { + "name": "MARC Code List for Geographic Areas", + "url": "http://www.loc.gov/marc/geoareas/" + } + }, + "b": { + "label": "Local GAC code", + "repeatable": true + }, + "c": { + "label": "ISO code", + "repeatable": true + }, + "0": { + "label": "Authority record control number or standard number", + "repeatable": true + }, + "2": { + "label": "Source of local code", + "repeatable": true, + "codelist": { + "name": "Geographic Area Code and Term Source Codes", + "url": "http://www.loc.gov/standards/sourcelist/geographic-area.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "044": { + "tag": "044", + "label": "Country of Publishing/Producing Entity Code", + "url": "https://www.loc.gov/marc/bibliographic/bd044.html", + "repeatable": false, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "MARC country code", + "repeatable": true, + "codelist": { + "name": "MARC Code List for Countries", + "url": "http://www.loc.gov/marc/countries/countries_code.html" + } + }, + "b": { + "label": "Local subentity code", + "repeatable": true + }, + "c": { + "label": "ISO country code", + "repeatable": true + }, + "2": { + "label": "Source of local subentity code", + "repeatable": true, + "codelist": { + "name": "Country Code and Term Source Codes", + "url": "http://www.loc.gov/standards/sourcelist/country.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "045": { + "tag": "045", + "label": "Time Period of Content", + "url": "https://www.loc.gov/marc/bibliographic/bd045.html", + "repeatable": false, + "indicator1": { + "label": "Type of time period in subfield $b or $c", + "codes": { + " ": { + "label": "Subfield $b or $c not present" + }, + "0": { + "label": "Single date/time" + }, + "1": { + "label": "Multiple single dates/times" + }, + "2": { + "label": "Range of dates/times" + } + } + }, + "indicator2": null, + "subfields": { + "a": { + "label": "Time period code", + "repeatable": true + }, + "b": { + "label": "Formatted 9999 B.C. through C.E. time period", + "repeatable": true + }, + "c": { + "label": "Formatted pre-9999 B.C. time period", + "repeatable": true + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "046": { + "tag": "046", + "label": "Special Coded Dates", + "url": "https://www.loc.gov/marc/bibliographic/bd046.html", + "repeatable": false, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Type of date code", + "repeatable": false + }, + "b": { + "label": "Date 1, B.C.E. date", + "repeatable": false + }, + "c": { + "label": "Date 1, C.E. date", + "repeatable": false + }, + "d": { + "label": "Date 2, B.C.E. date", + "repeatable": false + }, + "e": { + "label": "Date 2, C.E. date", + "repeatable": false + }, + "j": { + "label": "Date resource modified", + "repeatable": false + }, + "k": { + "label": "Beginning or single date created", + "repeatable": false + }, + "l": { + "label": "Ending date created", + "repeatable": false + }, + "m": { + "label": "Beginning of date valid", + "repeatable": false + }, + "n": { + "label": "End of date valid", + "repeatable": false + }, + "o": { + "label": "Single or starting date for aggregated content", + "repeatable": false + }, + "p": { + "label": "Ending date for aggregated content", + "repeatable": false + }, + "2": { + "label": "Source of date", + "repeatable": false, + "codelist": { + "name": "Date and Time Scheme Source Codes", + "url": "http://www.loc.gov/standards/sourcelist/date-time.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "047": { + "tag": "047", + "label": "Form of Musical Composition Code", + "url": "https://www.loc.gov/marc/bibliographic/bd047.html", + "repeatable": true, + "indicator1": null, + "indicator2": { + "label": "Source of code", + "codes": { + " ": { + "label": "MARC musical composition code" + }, + "7": { + "label": "Source specified in subfield $2" + } + } + }, + "subfields": { + "a": { + "label": "Form of musical composition code", + "repeatable": true + }, + "2": { + "label": "Source of code", + "repeatable": false, + "codelist": { + "name": "Musical Composition Form Code Source Codes", + "url": "https://www.loc.gov/standards/sourcelist/musical-composition.html" + } + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "048": { + "tag": "048", + "label": "Number of Musical Instruments or Voices Codes", + "url": "https://www.loc.gov/marc/bibliographic/bd048.html", + "repeatable": true, + "indicator1": null, + "indicator2": { + "label": "Source of code", + "codes": { + " ": { + "label": "MARC code" + }, + "7": { + "label": "Source specified in subfield $2" + } + } + }, + "subfields": { + "a": { + "label": "Performer or ensemble", + "repeatable": true, + "codelist": { + "name": "Instruments or Voices Codes", + "url": "http://www.loc.gov/marc/bibliographic/bd048.html" + } + }, + "b": { + "label": "Soloist", + "repeatable": true, + "codelist": { + "name": "Instruments or Voices Codes", + "url": "http://www.loc.gov/marc/bibliographic/bd048.html" + } + }, + "2": { + "label": "Source of code", + "repeatable": false, + "codelist": { + "name": "Musical Instrumentation and Voice Code Source Codes", + "url": "http://www.loc.gov/standards/sourcelist/musical-instrumentation.html" + } + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "050": { + "tag": "050", + "label": "Library of Congress Call Number", + "url": "https://www.loc.gov/marc/bibliographic/bd050.html", + "repeatable": true, + "indicator1": { + "label": "Existence in LC collection", + "codes": { + " ": { + "label": "No information provided" + }, + "0": { + "label": "Item is in LC" + }, + "1": { + "label": "Item is not in LC" + } + } + }, + "indicator2": { + "label": "Source of call number", + "codes": { + "0": { + "label": "Assigned by LC" + }, + "4": { + "label": "Assigned by agency other than LC" + } + }, + "historical-codes": { + " ": { + "label": "No information provided" + }, + "0": { + "label": "No series involved" + }, + "1": { + "label": "Main series" + }, + "2": { + "label": "Subseries" + }, + "3": { + "label": "Sub-subseries" + } + } + }, + "subfields": { + "a": { + "label": "Classification number", + "repeatable": true + }, + "b": { + "label": "Item number", + "repeatable": false + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + }, + "historical-subfields": { + "d": { + "label": "Supplementary class number (MU) [OBSOLETE, 1981]" + } + } + }, + "051": { + "tag": "051", + "label": "Library of Congress Copy, Issue, Offprint Statement", + "url": "https://www.loc.gov/marc/bibliographic/bd051.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Classification number", + "repeatable": false + }, + "b": { + "label": "Item number", + "repeatable": false + }, + "c": { + "label": "Copy information", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "052": { + "tag": "052", + "label": "Geographic Classification", + "url": "https://www.loc.gov/marc/bibliographic/bd052.html", + "repeatable": true, + "indicator1": { + "label": "Code source", + "codes": { + " ": { + "label": "Library of Congress Classification" + }, + "1": { + "label": "U.S. Dept. of Defense Classification" + }, + "7": { + "label": "Source specified in subfield $2" + } + }, + "historical-codes": { + "0": { + "label": "U.S. Dept. of Defense Classification [OBSOLETE, 2002]" + } + } + }, + "indicator2": null, + "subfields": { + "a": { + "label": "Geographic classification area code", + "repeatable": false + }, + "b": { + "label": "Geographic classification subarea code", + "repeatable": true + }, + "d": { + "label": "Populated place name", + "repeatable": true + }, + "2": { + "label": "Code source", + "repeatable": false, + "codelist": { + "name": "Classification Scheme Source Codes", + "url": "http://www.loc.gov/standards/sourcelist/classification.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + }, + "historical-subfields": { + "c": { + "label": "Subject (MP) [OBSOLETE]" + } + } + }, + "055": { + "tag": "055", + "label": "Classification Numbers Assigned in Canada", + "url": "https://www.loc.gov/marc/bibliographic/bd055.html", + "repeatable": true, + "indicator1": { + "label": "Existence in LAC collection", + "codes": { + " ": { + "label": "Information not provided" + }, + "0": { + "label": "Work held by LAC" + }, + "1": { + "label": "Work not held by LAC" + } + } + }, + "indicator2": { + "label": "Type, completeness, source of class/call number", + "codes": { + "0": { + "label": "LC-based call number assigned by LAC" + }, + "1": { + "label": "Complete LC class number assigned by LAC" + }, + "2": { + "label": "Incomplete LC class number assigned by LAC" + }, + "3": { + "label": "LC-based call number assigned by the contributing library" + }, + "4": { + "label": "Complete LC class number assigned by the contributing library" + }, + "5": { + "label": "Incomplete LC class number assigned by the contributing library" + }, + "6": { + "label": "Other call number assigned by LAC" + }, + "7": { + "label": "Other class number assigned by LAC" + }, + "8": { + "label": "Other call number assigned by the contributing library" + }, + "9": { + "label": "Other class number assigned by the contributing library" + } + } + }, + "subfields": { + "a": { + "label": "Classification number", + "repeatable": false + }, + "b": { + "label": "Item number", + "repeatable": false + }, + "0": { + "label": "Authority record control number or standard number", + "repeatable": true + }, + "2": { + "label": "Source of call/class number", + "repeatable": false, + "codelist": { + "name": "Classification Scheme Source Codes", + "url": "http://www.loc.gov/standards/sourcelist/classification.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "060": { + "tag": "060", + "label": "National Library of Medicine Call Number", + "url": "https://www.loc.gov/marc/bibliographic/bd060.html", + "repeatable": true, + "indicator1": { + "label": "Existence in NLM collection", + "codes": { + " ": { + "label": "No information provided" + }, + "0": { + "label": "Item is in NLM" + }, + "1": { + "label": "Item is not in NLM" + } + } + }, + "indicator2": { + "label": "Source of call number", + "codes": { + "0": { + "label": "Assigned by NLM" + }, + "4": { + "label": "Assigned by agency other than NLM" + } + }, + "historical-codes": { + "0": { + "label": "No series involved" + }, + "1": { + "label": "Main series" + }, + "2": { + "label": "Subseries" + }, + "3": { + "label": "Sub-subseries" + }, + " ": { + "label": "No information provided [OBSOLETE]" + } + } + }, + "subfields": { + "a": { + "label": "Classification number", + "repeatable": true + }, + "b": { + "label": "Item number", + "repeatable": false + }, + "0": { + "label": "Authority record control number or standard number", + "repeatable": true + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "061": { + "tag": "061", + "label": "National Library of Medicine Copy Statement", + "url": "https://www.loc.gov/marc/bibliographic/bd061.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Classification number", + "repeatable": true + }, + "b": { + "label": "Item number", + "repeatable": false + }, + "c": { + "label": "Copy information", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "066": { + "tag": "066", + "label": "Character Sets Present", + "url": "https://www.loc.gov/marc/bibliographic/bd066.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Primary G0 character set", + "repeatable": false + }, + "b": { + "label": "Primary G1 character set", + "repeatable": false + }, + "c": { + "label": "Alternate G0 or G1 character set", + "repeatable": true + } + } + }, + "070": { + "tag": "070", + "label": "National Agricultural Library Call Number", + "url": "https://www.loc.gov/marc/bibliographic/bd070.html", + "repeatable": true, + "indicator1": { + "label": " collection", + "codes": { + "0": { + "label": "Item is in NAL" + }, + "1": { + "label": "Item is not in NAL" + } + } + }, + "indicator2": null, + "subfields": { + "a": { + "label": "Classification number", + "repeatable": true + }, + "b": { + "label": "Item number", + "repeatable": false + }, + "0": { + "label": "Authority record control number or standard number", + "repeatable": true + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "071": { + "tag": "071", + "label": "National Agricultural Library Copy Statement", + "url": "https://www.loc.gov/marc/bibliographic/bd071.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Classification number", + "repeatable": true + }, + "b": { + "label": "Item number", + "repeatable": false + }, + "c": { + "label": "Copy information", + "repeatable": true + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "072": { + "tag": "072", + "label": "Subject Category Code", + "url": "https://www.loc.gov/marc/bibliographic/bd072.html", + "repeatable": true, + "indicator1": null, + "indicator2": { + "label": "Code source", + "codes": { + "0": { + "label": "NAL subject category code list" + }, + "7": { + "label": "Source specified in subfield $2" + } + }, + "historical-codes": { + " ": { + "label": "undefined" + } + } + }, + "subfields": { + "a": { + "label": "Subject category code", + "repeatable": false + }, + "x": { + "label": "Subject category code subdivision", + "repeatable": true + }, + "2": { + "label": "Source", + "repeatable": false, + "codelist": { + "name": "Subject Category Code Source Codes", + "url": "http://www.loc.gov/standards/sourcelist/subject-category.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "074": { + "tag": "074", + "label": "GPO Item Number", + "url": "https://www.loc.gov/marc/bibliographic/bd074.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "GPO item number", + "repeatable": false + }, + "z": { + "label": "Canceled/invalid GPO item number", + "repeatable": true + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "080": { + "tag": "080", + "label": "Universal Decimal Classification Number", + "url": "https://www.loc.gov/marc/bibliographic/bd080.html", + "repeatable": true, + "indicator1": { + "label": "Type of edition", + "codes": { + " ": { + "label": "No information provided" + }, + "0": { + "label": "Full" + }, + "1": { + "label": "Abridged" + } + } + }, + "indicator2": null, + "subfields": { + "a": { + "label": "Universal Decimal Classification number", + "repeatable": false + }, + "b": { + "label": "Item number", + "repeatable": false + }, + "x": { + "label": "Common auxiliary subdivision", + "repeatable": true + }, + "2": { + "label": "Edition identifier", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "082": { + "tag": "082", + "label": "Dewey Decimal Classification Number", + "url": "https://www.loc.gov/marc/bibliographic/bd082.html", + "repeatable": true, + "indicator1": { + "label": "Type of edition", + "codes": { + "0": { + "label": "Full edition" + }, + "1": { + "label": "Abridged edition" + }, + "7": { + "label": "Other edition specified in subfield $2" + } + }, + "historical-codes": { + " ": { + "label": "No edition information recorded (BK, MU, VM, SE) [OBSOLETE]" + }, + "2": { + "label": "Abridged NST version (BK, MU, VM, SE) [OBSOLETE]" + } + } + }, + "indicator2": { + "label": "Source of classification number", + "codes": { + " ": { + "label": "No information provided" + }, + "0": { + "label": "Assigned by LC" + }, + "4": { + "label": "Assigned by agency other than LC" + } + }, + "historical-codes": { + " ": { + "label": "No information provided [OBSOLETE] [USMARC only, BK, CF, MU, VM, SE]" + } + } + }, + "subfields": { + "a": { + "label": "Classification number", + "repeatable": true + }, + "b": { + "label": "Item number", + "repeatable": false + }, + "m": { + "label": "Standard or optional designation", + "repeatable": false + }, + "q": { + "label": "Assigning agency", + "repeatable": false, + "codelist": { + "name": "MARC Organization Codes", + "url": "http://www.loc.gov/marc/organizations/orgshome.html" + } + }, + "2": { + "label": "Edition number", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + }, + "historical-subfields": { + "b": { + "label": "DDC number-abridged NST version (SE) [OBSOLETE]" + } + } + }, + "083": { + "tag": "083", + "label": "Additional Dewey Decimal Classification Number", + "url": "https://www.loc.gov/marc/bibliographic/bd083.html", + "repeatable": true, + "indicator1": { + "label": "Type of edition", + "codes": { + "0": { + "label": "Full edition" + }, + "1": { + "label": "Abridged edition" + }, + "7": { + "label": "Other edition specified in subfield $2" + } + } + }, + "indicator2": null, + "subfields": { + "a": { + "label": "Classification number", + "repeatable": true + }, + "c": { + "label": "Classification number--Ending number of span", + "repeatable": true + }, + "m": { + "label": "Standard or optional designation", + "repeatable": false + }, + "q": { + "label": "Assigning agency", + "repeatable": false, + "codelist": { + "name": "MARC Organization Codes", + "url": "http://www.loc.gov/marc/organizations/orgshome.html" + } + }, + "y": { + "label": "Table sequence number for internal subarrangement or add table", + "repeatable": true + }, + "z": { + "label": "Table identification", + "repeatable": true + }, + "2": { + "label": "Edition number", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "084": { + "tag": "084", + "label": "Other Classificaton Number", + "url": "https://www.loc.gov/marc/bibliographic/bd084.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Classification number", + "repeatable": true + }, + "b": { + "label": "Item number", + "repeatable": false + }, + "q": { + "label": "Assigning agency", + "repeatable": false, + "codelist": { + "name": "MARC Organization Codes", + "url": "http://www.loc.gov/marc/organizations/orgshome.html" + } + }, + "2": { + "label": "Number source", + "repeatable": false, + "codelist": { + "name": "Classification Scheme Source Codes", + "url": "http://www.loc.gov/standards/sourcelist/classification.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "085": { + "tag": "085", + "label": "Synthesized Classification Number Components", + "url": "https://www.loc.gov/marc/bibliographic/bd085.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Number where instructions are found-single number or beginning number of span", + "repeatable": true + }, + "b": { + "label": "Base number", + "repeatable": true + }, + "c": { + "label": "Classification number-ending number of span", + "repeatable": true + }, + "f": { + "label": "Facet designator", + "repeatable": true + }, + "r": { + "label": "Root number", + "repeatable": true + }, + "s": { + "label": "Digits added from classification number in schedule or external table", + "repeatable": true + }, + "t": { + "label": "Digits added from internal subarrangement or add table", + "repeatable": true + }, + "u": { + "label": "Number being analyzed", + "repeatable": true + }, + "v": { + "label": "Number in internal subarrangement or add table where instructions are found", + "repeatable": true + }, + "w": { + "label": "Table identification-Internal subarrangement or add table", + "repeatable": true + }, + "y": { + "label": "Table sequence number for internal subarrangement or add table", + "repeatable": true + }, + "z": { + "label": "Table identification", + "repeatable": true + }, + "0": { + "label": "Authority record control number or standard number", + "repeatable": true + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "086": { + "tag": "086", + "label": "Government Document Classification Number", + "url": "https://www.loc.gov/marc/bibliographic/bd086.html", + "repeatable": true, + "indicator1": { + "label": "Number source", + "codes": { + " ": { + "label": "Source specified in subfield $2" + }, + "0": { + "label": "Superintendent of Documents Classification System" + }, + "1": { + "label": "Government of Canada Publications: Outline of Classification" + } + }, + "historical-codes": { + "0": { + "label": "United States" + }, + "1": { + "label": "Canada" + }, + "2-9": { + "label": "Reserved" + } + } + }, + "indicator2": null, + "subfields": { + "a": { + "label": "Classification number", + "repeatable": false + }, + "z": { + "label": "Canceled/invalid classification number", + "repeatable": true + }, + "2": { + "label": "Number source", + "repeatable": false, + "codelist": { + "name": "Classification Scheme Source Codes", + "url": "http://www.loc.gov/standards/sourcelist/classification.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "088": { + "tag": "088", + "label": "Report Number", + "url": "https://www.loc.gov/marc/bibliographic/bd088.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Report number", + "repeatable": false + }, + "z": { + "label": "Canceled/invalid report number", + "repeatable": true + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "100": { + "tag": "100", + "label": "Main Entry - Personal Name", + "url": "https://www.loc.gov/marc/bibliographic/bd100.html", + "repeatable": false, + "indicator1": { + "label": "Type of personal name entry element", + "codes": { + "0": { + "label": "Forename" + }, + "1": { + "label": "Surname" + }, + "3": { + "label": "Family name" + } + } + }, + "indicator2": null, + "subfields": { + "a": { + "label": "Personal name", + "repeatable": false + }, + "b": { + "label": "Numeration", + "repeatable": false + }, + "c": { + "label": "Titles and words associated with a name", + "repeatable": true + }, + "d": { + "label": "Dates associated with a name", + "repeatable": false + }, + "e": { + "label": "Relator term", + "repeatable": true + }, + "f": { + "label": "Date of a work", + "repeatable": false + }, + "g": { + "label": "Miscellaneous information", + "repeatable": true + }, + "j": { + "label": "Attribution qualifier", + "repeatable": true + }, + "k": { + "label": "Form subheading", + "repeatable": true + }, + "l": { + "label": "Language of a work", + "repeatable": false + }, + "n": { + "label": "Number of part/section of a work", + "repeatable": true + }, + "p": { + "label": "Name of part/section of a work", + "repeatable": true + }, + "q": { + "label": "Fuller form of name", + "repeatable": false + }, + "t": { + "label": "Title of a work", + "repeatable": false + }, + "u": { + "label": "Affiliation", + "repeatable": false + }, + "0": { + "label": "Authority record control number or standard number", + "repeatable": true + }, + "4": { + "label": "Relator code", + "repeatable": true, + "codelist": { + "name": "MARC Code List for Relators", + "url": "https://www.loc.gov/marc/relators/relaterm.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "110": { + "tag": "110", + "label": "Main Entry - Corporate Name", + "url": "https://www.loc.gov/marc/bibliographic/bd110.html", + "repeatable": false, + "indicator1": { + "label": "Type of corporate name entry element", + "codes": { + "0": { + "label": "Inverted name" + }, + "1": { + "label": "Jurisdiction name" + }, + "2": { + "label": "Name in direct order" + } + } + }, + "indicator2": null, + "subfields": { + "a": { + "label": "Corporate name or jurisdiction name as entry element", + "repeatable": false + }, + "b": { + "label": "Subordinate unit", + "repeatable": true + }, + "c": { + "label": "Location of meeting", + "repeatable": true + }, + "d": { + "label": "Date of meeting or treaty signing", + "repeatable": true + }, + "e": { + "label": "Relator term", + "repeatable": true + }, + "f": { + "label": "Date of a work", + "repeatable": false + }, + "g": { + "label": "Miscellaneous information", + "repeatable": true + }, + "k": { + "label": "Form subheading", + "repeatable": true + }, + "l": { + "label": "Language of a work", + "repeatable": false + }, + "n": { + "label": "Number of part/section/meeting", + "repeatable": true + }, + "p": { + "label": "Name of part/section of a work", + "repeatable": true + }, + "t": { + "label": "Title of a work", + "repeatable": false + }, + "u": { + "label": "Affiliation", + "repeatable": false + }, + "0": { + "label": "Authority record control number or standard number", + "repeatable": true + }, + "4": { + "label": "Relator code", + "repeatable": true, + "codelist": { + "name": "MARC Code List for Relators", + "url": "https://www.loc.gov/marc/relators/relaterm.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "111": { + "tag": "111", + "label": "Main Entry - Meeting Name", + "url": "https://www.loc.gov/marc/bibliographic/bd111.html", + "repeatable": false, + "indicator1": { + "label": "Type of meeting name entry element", + "codes": { + "0": { + "label": "Inverted name" + }, + "1": { + "label": "Jurisdiction name" + }, + "2": { + "label": "Name in direct order" + } + } + }, + "indicator2": null, + "subfields": { + "a": { + "label": "Meeting name or jurisdiction name as entry element", + "repeatable": false + }, + "c": { + "label": "Location of meeting", + "repeatable": true + }, + "d": { + "label": "Date of meeting", + "repeatable": false + }, + "e": { + "label": "Subordinate unit", + "repeatable": true + }, + "f": { + "label": "Date of a work", + "repeatable": false + }, + "g": { + "label": "Miscellaneous information", + "repeatable": true + }, + "j": { + "label": "Relator term", + "repeatable": true + }, + "k": { + "label": "Form subheading", + "repeatable": true + }, + "l": { + "label": "Language of a work", + "repeatable": false + }, + "n": { + "label": "Number of part/section/meeting", + "repeatable": true + }, + "p": { + "label": "Name of part/section of a work", + "repeatable": true + }, + "q": { + "label": "Name of meeting following jurisdiction name entry element", + "repeatable": false + }, + "t": { + "label": "Title of a work", + "repeatable": false + }, + "u": { + "label": "Affiliation", + "repeatable": false + }, + "0": { + "label": "Authority record control number or standard number", + "repeatable": true + }, + "4": { + "label": "Relationship", + "repeatable": true, + "codelist": { + "name": "MARC Code List for Relators", + "url": "https://www.loc.gov/marc/relators/relaterm.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "130": { + "tag": "130", + "label": "Main Entry - Uniform Title", + "url": "https://www.loc.gov/marc/bibliographic/bd130.html", + "repeatable": false, + "indicator1": { + "label": "Nonfiling characters", + "codes": { + "0-9": { + "label": "Number of nonfiling characters" + } + } + }, + "indicator2": null, + "subfields": { + "a": { + "label": "Uniform title", + "repeatable": false + }, + "d": { + "label": "Date of treaty signing", + "repeatable": true + }, + "f": { + "label": "Date of a work", + "repeatable": false + }, + "g": { + "label": "Miscellaneous information", + "repeatable": true + }, + "h": { + "label": "Medium", + "repeatable": false + }, + "k": { + "label": "Form subheading", + "repeatable": true + }, + "l": { + "label": "Language of a work", + "repeatable": false + }, + "m": { + "label": "Medium of performance for music", + "repeatable": true + }, + "n": { + "label": "Number of part/section of a work", + "repeatable": true + }, + "o": { + "label": "Arranged statement for music", + "repeatable": false + }, + "p": { + "label": "Name of part/section of a work", + "repeatable": true + }, + "r": { + "label": "Key for music", + "repeatable": false + }, + "s": { + "label": "Version", + "repeatable": false + }, + "t": { + "label": "Title of a work", + "repeatable": false + }, + "0": { + "label": "Authority record control number or standard number", + "repeatable": true + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "210": { + "tag": "210", + "label": "Abbreviated Title", + "url": "https://www.loc.gov/marc/bibliographic/bd210.html", + "repeatable": true, + "indicator1": { + "label": "Title added entry", + "codes": { + "0": { + "label": "No added entry" + }, + "1": { + "label": "Added entry" + } + } + }, + "indicator2": { + "label": "Type", + "codes": { + " ": { + "label": "Abbreviated key title" + }, + "0": { + "label": "Other abbreviated title" + } + } + }, + "subfields": { + "a": { + "label": "Abbreviated title", + "repeatable": false + }, + "b": { + "label": "Qualifying information", + "repeatable": false + }, + "2": { + "label": "Source", + "repeatable": true, + "codelist": { + "name": "Abbreviated Title Source Codes", + "url": "http://www.loc.gov/standards/sourcelist/abbreviated-title.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "222": { + "tag": "222", + "label": "Key Title", + "url": "https://www.loc.gov/marc/bibliographic/bd222.html", + "repeatable": true, + "indicator1": null, + "indicator2": { + "label": "Nonfiling characters", + "codes": { + "0": { + "label": "No nonfiling characters" + }, + "1-9": { + "label": "Number of nonfiling characters" + } + }, + "historical-codes": { + "0": { + "label": "Key title is same as field 245 / No key title added entry; title proper same" + }, + "1": { + "label": "Key title is not the same as field 245 / Key title added entry; title proper different" + }, + "2": { + "label": "Key title added entry; title proper same" + }, + "3": { + "label": "No key title added entry; title proper different" + } + } + }, + "subfields": { + "a": { + "label": "Key title", + "repeatable": false + }, + "b": { + "label": "Qualifying information", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "240": { + "tag": "240", + "label": "Uniform Title", + "url": "https://www.loc.gov/marc/bibliographic/bd240.html", + "repeatable": false, + "indicator1": { + "label": "Uniform title printed or displayed", + "codes": { + "0": { + "label": "Not printed or displayed" + }, + "1": { + "label": "Printed or displayed" + } + }, + "historical-codes": { + "2": { + "label": "Not printed on card, title added entry (MU) [OBSOLETE, 1993]" + }, + "3": { + "label": "Printed on card, title added entry (MU) [OBSOLETE, 1993]" + } + } + }, + "indicator2": { + "label": "Nonfiling characters", + "codes": { + "0-9": { + "label": "Number of nonfiling characters" + } + } + }, + "subfields": { + "a": { + "label": "Uniform title", + "repeatable": false + }, + "d": { + "label": "Date of treaty signing", + "repeatable": true + }, + "f": { + "label": "Date of a work", + "repeatable": false + }, + "g": { + "label": "Miscellaneous information", + "repeatable": true + }, + "h": { + "label": "Medium", + "repeatable": false + }, + "k": { + "label": "Form subheading", + "repeatable": true + }, + "l": { + "label": "Language of a work", + "repeatable": false + }, + "m": { + "label": "Medium of performance for music", + "repeatable": true + }, + "n": { + "label": "Number of part/section of a work", + "repeatable": true + }, + "o": { + "label": "Arranged statement for music", + "repeatable": false + }, + "p": { + "label": "Name of part/section of a work", + "repeatable": true + }, + "r": { + "label": "Key for music", + "repeatable": false + }, + "s": { + "label": "Version", + "repeatable": false + }, + "0": { + "label": "Authority record control number or standard number", + "repeatable": true + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "242": { + "tag": "242", + "label": "Translation of Title by Cataloging Agency", + "url": "https://www.loc.gov/marc/bibliographic/bd242.html", + "repeatable": true, + "indicator1": { + "label": "Title added entry", + "codes": { + "0": { + "label": "No added entry" + }, + "1": { + "label": "Added entry" + } + } + }, + "indicator2": { + "label": "Nonfiling characters", + "codes": { + "0": { + "label": "No nonfiling characters" + }, + "1-9": { + "label": "Number of nonfiling characters" + } + } + }, + "subfields": { + "a": { + "label": "Title", + "repeatable": false + }, + "b": { + "label": "Remainder of title", + "repeatable": false + }, + "c": { + "label": "Statement of responsibility, etc.", + "repeatable": false + }, + "h": { + "label": "Medium", + "repeatable": false + }, + "n": { + "label": "Number of part/section of a work", + "repeatable": true + }, + "p": { + "label": "Name of part/section of a work", + "repeatable": true + }, + "y": { + "label": "Language code of translated title", + "repeatable": false, + "codelist": { + "name": "MARC Code List for Languages", + "url": "http://www.loc.gov/marc/languages/language_code.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + }, + "historical-subfields": { + "d": { + "label": "Designation of section (BK, AM, MP, MU, VM, SE) [OBSOLETE, 1979]" + }, + "e": { + "label": "Name of part/section (BK, AM, MP, MU, VM, SE) [OBSOLETE, 1979]" + } + } + }, + "243": { + "tag": "243", + "label": "Collective Uniform Title", + "url": "https://www.loc.gov/marc/bibliographic/bd243.html", + "repeatable": false, + "indicator1": { + "label": "Uniform title printed or displayed", + "codes": { + "0": { + "label": "Not printed or displayed" + }, + "1": { + "label": "Printed or displayed" + } + }, + "historical-codes": { + "2": { + "label": "Not printed on card, title added entry (MU) [USMARC only] [OBSOLETE, 1993] / Selections (extracts) [CAN/MARC only] [OBSOLETE]" + }, + "3": { + "label": "Printed on card, title added entry (MU) [USMARC only] [OBSOLETE, 1993] / Other collective titles [CAN/MARC only] [OBSOLETE]" + } + } + }, + "indicator2": { + "label": "Nonfiling characters", + "codes": { + "0-9": { + "label": "Number of nonfiling characters" + } + } + }, + "subfields": { + "a": { + "label": "Uniform title", + "repeatable": false + }, + "d": { + "label": "Date of treaty signing", + "repeatable": true + }, + "f": { + "label": "Date of a work", + "repeatable": false + }, + "g": { + "label": "Miscellaneous information", + "repeatable": true + }, + "h": { + "label": "Medium", + "repeatable": false + }, + "k": { + "label": "Form subheading", + "repeatable": true + }, + "l": { + "label": "Language of a work", + "repeatable": false + }, + "m": { + "label": "Medium of performance for music", + "repeatable": true + }, + "n": { + "label": "Number of part/section of a work", + "repeatable": true + }, + "o": { + "label": "Arranged statement for music", + "repeatable": false + }, + "p": { + "label": "Name of part/section of a work", + "repeatable": true + }, + "r": { + "label": "Key for music", + "repeatable": false + }, + "s": { + "label": "Version", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "245": { + "tag": "245", + "label": "Title Statement", + "url": "https://www.loc.gov/marc/bibliographic/bd245.html", + "repeatable": false, + "indicator1": { + "label": "Title added entry", + "codes": { + "0": { + "label": "No added entry" + }, + "1": { + "label": "Added entry" + } + } + }, + "indicator2": { + "label": "Nonfiling characters", + "codes": { + "0": { + "label": "No nonfiling characters" + }, + "1-9": { + "label": "Number of nonfiling characters" + } + } + }, + "subfields": { + "a": { + "label": "Title", + "repeatable": false + }, + "b": { + "label": "Remainder of title", + "repeatable": false + }, + "c": { + "label": "Statement of responsibility, etc.", + "repeatable": false + }, + "f": { + "label": "Inclusive dates", + "repeatable": false + }, + "g": { + "label": "Bulk dates", + "repeatable": false + }, + "h": { + "label": "Medium", + "repeatable": false + }, + "k": { + "label": "Form", + "repeatable": true + }, + "n": { + "label": "Number of part/section of a work", + "repeatable": true + }, + "p": { + "label": "Name of part/section of a work", + "repeatable": true + }, + "s": { + "label": "Version", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + }, + "historical-subfields": { + "d": { + "label": "Designation of section/part/series (SE) [OBSOLETE, 1979]" + }, + "e": { + "label": "Name of part/section/series (SE) [OBSOLETE, 1979]" + } + } + }, + "246": { + "tag": "246", + "label": "Varying Form of Title", + "url": "https://www.loc.gov/marc/bibliographic/bd246.html", + "repeatable": true, + "indicator1": { + "label": "Note/added entry controller", + "codes": { + "0": { + "label": "Note, no added entry" + }, + "1": { + "label": "Note, added entry" + }, + "2": { + "label": "No note, no added entry" + }, + "3": { + "label": "No note, added entry" + } + } + }, + "indicator2": { + "label": "Type of title", + "codes": { + " ": { + "label": "No type specified" + }, + "0": { + "label": "Portion of title" + }, + "1": { + "label": "Parallel title" + }, + "2": { + "label": "Distinctive title" + }, + "3": { + "label": "Other title" + }, + "4": { + "label": "Cover title" + }, + "5": { + "label": "Added title page title" + }, + "6": { + "label": "Caption title" + }, + "7": { + "label": "Running title" + }, + "8": { + "label": "Spine title" + } + } + }, + "subfields": { + "a": { + "label": "Title proper/short title", + "repeatable": false + }, + "b": { + "label": "Remainder of title", + "repeatable": false + }, + "f": { + "label": "Date or sequential designation", + "repeatable": false + }, + "g": { + "label": "Miscellaneous information", + "repeatable": true + }, + "h": { + "label": "Medium", + "repeatable": false + }, + "i": { + "label": "Display text", + "repeatable": false + }, + "n": { + "label": "Number of part/section of a work", + "repeatable": true + }, + "p": { + "label": "Name of part/section of a work", + "repeatable": true + }, + "5": { + "label": "Institution to which field applies", + "repeatable": false, + "codelist": { + "name": "MARC Organization Codes", + "url": "http://www.loc.gov/marc/organizations/orgshome.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + }, + "historical-subfields": { + "c": { + "label": "Remainder of title page transcription [OBSOLETE, 1991] [CAN/MARC only]" + }, + "d": { + "label": "Designation of section/part/series (SE) [OBSOLETE, 1979]" + }, + "e": { + "label": "Name of section/part/series (SE) [OBSOLETE, 1979]" + } + } + }, + "247": { + "tag": "247", + "label": "Former Title", + "url": "https://www.loc.gov/marc/bibliographic/bd247.html", + "repeatable": true, + "indicator1": { + "label": "Title added entry", + "codes": { + "0": { + "label": "No added entry" + }, + "1": { + "label": "Added entry" + } + } + }, + "indicator2": { + "label": "Note controller", + "codes": { + "0": { + "label": "Display note" + }, + "1": { + "label": "Do not display note" + } + } + }, + "subfields": { + "a": { + "label": "Title", + "repeatable": false + }, + "b": { + "label": "Remainder of title", + "repeatable": false + }, + "f": { + "label": "Date or sequential designation", + "repeatable": false + }, + "g": { + "label": "Miscellaneous information", + "repeatable": true + }, + "h": { + "label": "Medium", + "repeatable": false + }, + "n": { + "label": "Number of part/section of a work", + "repeatable": true + }, + "p": { + "label": "Name of part/section of a work", + "repeatable": true + }, + "x": { + "label": "International Standard Serial Number", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + }, + "historical-subfields": { + "d": { + "label": "Designation of section/part/series (SE) [OBSOLETE, 1979]" + }, + "e": { + "label": "Name of section/part/series (SE) [OBSOLETE, 1979]" + }, + "c": { + "label": "Remainder of Title Page transcription [OBSOLETE] [CAN/MARC only]" + } + } + }, + "250": { + "tag": "250", + "label": "Edition Statement", + "url": "https://www.loc.gov/marc/bibliographic/bd250.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Edition statement", + "repeatable": false + }, + "b": { + "label": "Remainder of edition statement", + "repeatable": false + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "254": { + "tag": "254", + "label": "Musical Presentation Statement", + "url": "https://www.loc.gov/marc/bibliographic/bd254.html", + "repeatable": false, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Musical presentation statement", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "255": { + "tag": "255", + "label": "Cartographic Mathematical Data", + "url": "https://www.loc.gov/marc/bibliographic/bd255.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Statement of scale", + "repeatable": false + }, + "b": { + "label": "Statement of projection", + "repeatable": false + }, + "c": { + "label": "Statement of coordinates", + "repeatable": false + }, + "d": { + "label": "Statement of zone", + "repeatable": false + }, + "e": { + "label": "Statement of equinox", + "repeatable": false + }, + "f": { + "label": "Outer G-ring coordinate pairs", + "repeatable": false + }, + "g": { + "label": "Exclusion G-ring coordinate pairs", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "256": { + "tag": "256", + "label": "Computer File Characteristics", + "url": "https://www.loc.gov/marc/bibliographic/bd256.html", + "repeatable": false, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Computer file characteristics", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "257": { + "tag": "257", + "label": "Country of Producing Entity", + "url": "https://www.loc.gov/marc/bibliographic/bd257.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Country of producing entity", + "repeatable": true + }, + "0": { + "label": "Authority record control number or standard number", + "repeatable": true + }, + "2": { + "label": "Source", + "repeatable": false, + "codelist": { + "name": "Subject Heading and Term Source Codes", + "url": "http://www.loc.gov/standards/sourcelist/subject.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "258": { + "tag": "258", + "label": "Philatelic Issue Data", + "url": "https://www.loc.gov/marc/bibliographic/bd258.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Issuing jurisdiction", + "repeatable": false + }, + "b": { + "label": "Denomination", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "260": { + "tag": "260", + "label": "Publication, Distribution, etc. (Imprint)", + "url": "https://www.loc.gov/marc/bibliographic/bd260.html", + "repeatable": true, + "indicator1": { + "label": "Sequence of publishing statements", + "codes": { + " ": { + "label": "Not applicable/No information provided/Earliest available publisher" + }, + "2": { + "label": "Intervening publisher" + }, + "3": { + "label": "Current/latest publisher" + } + }, + "historical-codes": { + "0": { + "label": "Publisher, distributor, etc. is present" + }, + "1": { + "label": "Publisher, distributor, etc. not present" + } + } + }, + "indicator2": null, + "subfields": { + "a": { + "label": "Place of publication, distribution, etc.", + "repeatable": true + }, + "b": { + "label": "Name of publisher, distributor, etc.", + "repeatable": true + }, + "c": { + "label": "Date of publication, distribution, etc.", + "repeatable": true + }, + "e": { + "label": "Place of manufacture", + "repeatable": true + }, + "f": { + "label": "Manufacturer", + "repeatable": true + }, + "g": { + "label": "Date of manufacture", + "repeatable": true + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + }, + "historical-subfields": { + "d": { + "label": "Plate or publisher's number for music (Pre-AACR2) [OBSOLETE, 1981] [CAN/MARC only] / Plate or publisher's number for music (Pre-AACR2) [OBSOLETE, 1999] [USMARC only]" + }, + "k": { + "label": "Identification/manufacturer number [OBSOLETE, 1988] [CAN/MARC only]" + }, + "l": { + "label": "Matrix and/or take number [OBSOLETE, 1988] [CAN/MARC only]" + } + } + }, + "263": { + "tag": "263", + "label": "Projected Publication Date", + "url": "https://www.loc.gov/marc/bibliographic/bd263.html", + "repeatable": false, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Projected publication date", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "264": { + "tag": "264", + "label": "Production, Publication, Distribution, Manufacture, and Copyright Notice", + "url": "https://www.loc.gov/marc/bibliographic/bd264.html", + "repeatable": true, + "indicator1": { + "label": "Sequence of statements", + "codes": { + " ": { + "label": "Not applicable/No information provided/Earliest" + }, + "2": { + "label": "Intervening" + }, + "3": { + "label": "Current/Latest" + } + } + }, + "indicator2": { + "label": "Function of entity", + "codes": { + "0": { + "label": "Production" + }, + "1": { + "label": "Publication" + }, + "2": { + "label": "Distribution" + }, + "3": { + "label": "Manufacture" + }, + "4": { + "label": "Copyright notice date" + } + } + }, + "subfields": { + "a": { + "label": "Place of production, publication, distribution, manufacture", + "repeatable": true + }, + "b": { + "label": "Name of producer, publisher, distributor, manufacturer", + "repeatable": true + }, + "c": { + "label": "Date of production, publication, distribution, manufacture, or copyright notice", + "repeatable": true + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "270": { + "tag": "270", + "label": "Address", + "url": "https://www.loc.gov/marc/bibliographic/bd270.html", + "repeatable": true, + "indicator1": { + "label": "Level", + "codes": { + " ": { + "label": "No level specified" + }, + "1": { + "label": "Primary" + }, + "2": { + "label": "Secondary" + } + } + }, + "indicator2": { + "label": "Type of address", + "codes": { + " ": { + "label": "No type specified" + }, + "0": { + "label": "Mailing" + }, + "7": { + "label": "Type specified in subfield $i" + } + } + }, + "subfields": { + "a": { + "label": "Address", + "repeatable": true + }, + "b": { + "label": "City", + "repeatable": false + }, + "c": { + "label": "State or province", + "repeatable": false + }, + "d": { + "label": "Country", + "repeatable": false + }, + "e": { + "label": "Postal code", + "repeatable": false + }, + "f": { + "label": "Terms preceding attention name", + "repeatable": false + }, + "g": { + "label": "Attention name", + "repeatable": false + }, + "h": { + "label": "Attention position", + "repeatable": false + }, + "i": { + "label": "Type of address", + "repeatable": false + }, + "j": { + "label": "Specialized telephone number", + "repeatable": true + }, + "k": { + "label": "Telephone number", + "repeatable": true + }, + "l": { + "label": "Fax number", + "repeatable": true + }, + "m": { + "label": "Electronic mail address", + "repeatable": true + }, + "n": { + "label": "TDD or TTY number", + "repeatable": true + }, + "p": { + "label": "Contact person", + "repeatable": true + }, + "q": { + "label": "Title of contact person", + "repeatable": true + }, + "r": { + "label": "Hours", + "repeatable": true + }, + "z": { + "label": "Public note", + "repeatable": true + }, + "4": { + "label": "Relationship", + "repeatable": true, + "codelist": { + "name": "MARC Code List for Relators", + "url": "https://www.loc.gov/marc/relators/relaterm.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "300": { + "tag": "300", + "label": "Physical Description", + "url": "https://www.loc.gov/marc/bibliographic/bd300.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Extent", + "repeatable": true + }, + "b": { + "label": "Other physical details", + "repeatable": false + }, + "c": { + "label": "Dimensions", + "repeatable": true + }, + "e": { + "label": "Accompanying material", + "repeatable": false + }, + "f": { + "label": "Type of unit", + "repeatable": true + }, + "g": { + "label": "Size of unit", + "repeatable": true + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + }, + "historical-subfields": { + "d": { + "label": "Accompanying material [OBSOLETE, 1997] [CAN/MARC only]" + }, + "m": { + "label": "Identification/manufacturer number [pre-AACR2 records only] [OBSOLETE, 1988] [CAN/MARC only]" + }, + "n": { + "label": "Matrix and/or take number [Sound recordings, pre-AACR2 records only] [OBSOLETE, 1988] [CAN/MARC only]" + } + } + }, + "306": { + "tag": "306", + "label": "Playing Time", + "url": "https://www.loc.gov/marc/bibliographic/bd306.html", + "repeatable": false, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Playing time", + "repeatable": true + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "307": { + "tag": "307", + "label": "Hours, etc.", + "url": "https://www.loc.gov/marc/bibliographic/bd307.html", + "repeatable": true, + "indicator1": { + "label": "Display constant controller", + "codes": { + " ": { + "label": "Hours" + }, + "8": { + "label": "No display constant generated" + } + } + }, + "indicator2": null, + "subfields": { + "a": { + "label": "Hours", + "repeatable": false + }, + "b": { + "label": "Additional information", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "310": { + "tag": "310", + "label": "Current Publication Frequency", + "url": "https://www.loc.gov/marc/bibliographic/bd310.html", + "repeatable": false, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Current publication frequency", + "repeatable": false + }, + "b": { + "label": "Date of current publication frequency", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "321": { + "tag": "321", + "label": "Former Publication Frequency", + "url": "https://www.loc.gov/marc/bibliographic/bd321.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Former publication frequency", + "repeatable": false + }, + "b": { + "label": "Dates of former publication frequency", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "336": { + "tag": "336", + "label": "Content Type", + "url": "https://www.loc.gov/marc/bibliographic/bd336.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Content type term", + "repeatable": true + }, + "b": { + "label": "Content type code", + "repeatable": true + }, + "0": { + "label": "Authority record control number or standard number", + "repeatable": true + }, + "2": { + "label": "Source", + "repeatable": false, + "codelist": { + "name": "Genre/Form Code and Term Source Codes", + "url": "http://www.loc.gov/standards/sourcelist/genre-form.html" + } + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "337": { + "tag": "337", + "label": "Media Type", + "url": "https://www.loc.gov/marc/bibliographic/bd337.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Media type term", + "repeatable": true + }, + "b": { + "label": "Media type code", + "repeatable": true + }, + "0": { + "label": "Authority record control number or standard number", + "repeatable": true + }, + "2": { + "label": "Source", + "repeatable": false, + "codelist": { + "name": "Genre/Form Code and Term Source Codes", + "url": "http://www.loc.gov/standards/sourcelist/genre-form.html" + } + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "338": { + "tag": "338", + "label": "Carrier Type", + "url": "https://www.loc.gov/marc/bibliographic/bd338.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Carrier type term", + "repeatable": true + }, + "b": { + "label": "Carrier type code", + "repeatable": true + }, + "0": { + "label": "Authority record control number or standard number", + "repeatable": true + }, + "2": { + "label": "Source", + "repeatable": false, + "codelist": { + "name": "Genre/Form Code and Term Source Codes", + "url": "http://www.loc.gov/standards/sourcelist/genre-form.html" + } + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "340": { + "tag": "340", + "label": "Physical Medium", + "url": "https://www.loc.gov/marc/bibliographic/bd340.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Material base and configuration", + "repeatable": true + }, + "b": { + "label": "Dimensions", + "repeatable": true + }, + "c": { + "label": "Materials applied to surface", + "repeatable": true + }, + "d": { + "label": "Information recording technique", + "repeatable": true + }, + "e": { + "label": "Support", + "repeatable": true + }, + "f": { + "label": "Production rate/ratio", + "repeatable": true + }, + "g": { + "label": "Color content", + "repeatable": true + }, + "h": { + "label": "Location within medium", + "repeatable": true + }, + "i": { + "label": "Technical specifications of medium", + "repeatable": true + }, + "j": { + "label": "Generation", + "repeatable": true + }, + "k": { + "label": "Layout", + "repeatable": true + }, + "m": { + "label": "Book format", + "repeatable": true + }, + "n": { + "label": "Font size", + "repeatable": true + }, + "o": { + "label": "Polarity", + "repeatable": true + }, + "0": { + "label": "Authority record control number or standard number", + "repeatable": true + }, + "2": { + "label": "Source", + "repeatable": false + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "342": { + "tag": "342", + "label": "Geospatial Reference Data", + "url": "https://www.loc.gov/marc/bibliographic/bd342.html", + "repeatable": true, + "indicator1": { + "label": "Geospatial reference dimension", + "codes": { + "0": { + "label": "Horizontal coordinate system" + }, + "1": { + "label": "Vertical coordinate system" + } + } + }, + "indicator2": { + "label": "Geospatial reference method", + "codes": { + "0": { + "label": "Geographic" + }, + "1": { + "label": "Map projection" + }, + "2": { + "label": "Grid coordinate system" + }, + "3": { + "label": "Local planar" + }, + "4": { + "label": "Local" + }, + "5": { + "label": "Geodetic model" + }, + "6": { + "label": "Altitude" + }, + "7": { + "label": "Method specified in $2" + }, + "8": { + "label": "Depth" + } + } + }, + "subfields": { + "a": { + "label": "Name", + "repeatable": false + }, + "b": { + "label": "Coordinate units or distance units", + "repeatable": false + }, + "c": { + "label": "Latitude resolution", + "repeatable": false + }, + "d": { + "label": "Longitude resolution", + "repeatable": false + }, + "e": { + "label": "Standard parallel or oblique line latitude", + "repeatable": true + }, + "f": { + "label": "Oblique line longitude", + "repeatable": true + }, + "g": { + "label": "Longitude of central meridian or projection center", + "repeatable": false + }, + "h": { + "label": "Latitude of projection center or projection origin", + "repeatable": false + }, + "i": { + "label": "False easting", + "repeatable": false + }, + "j": { + "label": "False northing", + "repeatable": false + }, + "k": { + "label": "Scale factor", + "repeatable": false + }, + "l": { + "label": "Height of perspective point above surface", + "repeatable": false + }, + "m": { + "label": "Azimuthal angle", + "repeatable": false + }, + "n": { + "label": "Azimuth measure point longitude or straight vertical longitude from pole", + "repeatable": false + }, + "o": { + "label": "Landsat number and path number", + "repeatable": false + }, + "p": { + "label": "Zone identifier", + "repeatable": false + }, + "q": { + "label": "Ellipsoid name", + "repeatable": false + }, + "r": { + "label": "Semi-major axis", + "repeatable": false + }, + "s": { + "label": "Denominator of flattening ratio", + "repeatable": false + }, + "t": { + "label": "Vertical resolution", + "repeatable": false + }, + "u": { + "label": "Vertical encoding method", + "repeatable": false + }, + "v": { + "label": "Local planar, local, or other projection or grid description", + "repeatable": false + }, + "w": { + "label": "Local planar or local georeference information", + "repeatable": false + }, + "2": { + "label": "Reference method used", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "343": { + "tag": "343", + "label": "Planar Coordinate Data", + "url": "https://www.loc.gov/marc/bibliographic/bd343.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Planar coordinate encoding method", + "repeatable": false + }, + "b": { + "label": "Planar distance units", + "repeatable": false + }, + "c": { + "label": "Abscissa resolution", + "repeatable": false + }, + "d": { + "label": "Ordinate resolution", + "repeatable": false + }, + "e": { + "label": "Distance resolution", + "repeatable": false + }, + "f": { + "label": "Bearing resolution", + "repeatable": false + }, + "g": { + "label": "Bearing units", + "repeatable": false + }, + "h": { + "label": "Bearing reference direction", + "repeatable": false + }, + "i": { + "label": "Bearing reference meridian", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "344": { + "tag": "344", + "label": "Sound Characteristics", + "url": "https://www.loc.gov/marc/bibliographic/bd344.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Type of recording", + "repeatable": true + }, + "b": { + "label": "Recording medium", + "repeatable": true + }, + "c": { + "label": "Playing speed", + "repeatable": true + }, + "d": { + "label": "Groove characteristic", + "repeatable": true + }, + "e": { + "label": "Track configuration", + "repeatable": true + }, + "f": { + "label": "Tape configuration", + "repeatable": true + }, + "g": { + "label": "Configuration of playback channels", + "repeatable": true + }, + "h": { + "label": "Special playback characteristics", + "repeatable": true + }, + "0": { + "label": "Authority record control number or standard number", + "repeatable": true + }, + "2": { + "label": "Source", + "repeatable": false + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "345": { + "tag": "345", + "label": "Projection Characteristics of Moving Image", + "url": "https://www.loc.gov/marc/bibliographic/bd345.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Presentation format", + "repeatable": true + }, + "b": { + "label": "Projection speed", + "repeatable": true + }, + "0": { + "label": "Authority record control number or standard number", + "repeatable": true + }, + "2": { + "label": "Source", + "repeatable": false + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "346": { + "tag": "346", + "label": "Video Characteristics", + "url": "https://www.loc.gov/marc/bibliographic/bd346.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Video format", + "repeatable": true + }, + "b": { + "label": "Broadcast standard", + "repeatable": true + }, + "0": { + "label": "Authority record control number or standard number", + "repeatable": true + }, + "2": { + "label": "Source", + "repeatable": false + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "347": { + "tag": "347", + "label": "Digital File Characteristics", + "url": "https://www.loc.gov/marc/bibliographic/bd347.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "File type", + "repeatable": true + }, + "b": { + "label": "Encoding format", + "repeatable": true + }, + "c": { + "label": "File size", + "repeatable": true + }, + "d": { + "label": "Resolution", + "repeatable": true + }, + "e": { + "label": "Regional encoding", + "repeatable": true + }, + "f": { + "label": "Encoded bitrate", + "repeatable": true + }, + "0": { + "label": "Authority record control number or standard number", + "repeatable": true + }, + "2": { + "label": "Source", + "repeatable": false + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "348": { + "tag": "348", + "label": "Format of Notated Music", + "url": "https://www.loc.gov/marc/bibliographic/bd348.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Format of notated music term", + "repeatable": true + }, + "b": { + "label": "Format of notated music code", + "repeatable": true + }, + "0": { + "label": "Authority record control number or standard number", + "repeatable": true + }, + "2": { + "label": "Source", + "repeatable": false + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "351": { + "tag": "351", + "label": "Organization and Arrangement of Materials", + "url": "https://www.loc.gov/marc/bibliographic/bd351.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Organization", + "repeatable": true + }, + "b": { + "label": "Arrangement", + "repeatable": true + }, + "c": { + "label": "Hierarchical level", + "repeatable": false + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "352": { + "tag": "352", + "label": "Digital Graphic Representation", + "url": "https://www.loc.gov/marc/bibliographic/bd352.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Direct reference method", + "repeatable": false + }, + "b": { + "label": "Object type", + "repeatable": true + }, + "c": { + "label": "Object count", + "repeatable": true + }, + "d": { + "label": "Row count", + "repeatable": false + }, + "e": { + "label": "Column count", + "repeatable": false + }, + "f": { + "label": "Vertical count", + "repeatable": false + }, + "g": { + "label": "VPF topology level", + "repeatable": false + }, + "i": { + "label": "Indirect reference description", + "repeatable": false + }, + "q": { + "label": "Format of the digital image", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "355": { + "tag": "355", + "label": "Security Classification Control", + "url": "https://www.loc.gov/marc/bibliographic/bd355.html", + "repeatable": true, + "indicator1": { + "label": "Controlled element", + "codes": { + "0": { + "label": "Document" + }, + "1": { + "label": "Title" + }, + "2": { + "label": "Abstract" + }, + "3": { + "label": "Contents note" + }, + "4": { + "label": "Author" + }, + "5": { + "label": "Record" + }, + "8": { + "label": "None of the above" + } + } + }, + "indicator2": null, + "subfields": { + "a": { + "label": "Security classification", + "repeatable": false + }, + "b": { + "label": "Handling instructions", + "repeatable": true + }, + "c": { + "label": "External dissemination information", + "repeatable": true + }, + "d": { + "label": "Downgrading or declassification event", + "repeatable": false + }, + "e": { + "label": "Classification system", + "repeatable": false + }, + "f": { + "label": "Country of origin code", + "repeatable": false, + "codelist": { + "name": "MARC Code List for Countries", + "url": "http://www.loc.gov/marc/countries/countries_code.html" + } + }, + "g": { + "label": "Downgrading date", + "repeatable": false + }, + "h": { + "label": "Declassification date", + "repeatable": false + }, + "j": { + "label": "Authorization", + "repeatable": true, + "codelist": { + "name": "MARC Organization Codes", + "url": "http://www.loc.gov/marc/organizations/orgshome.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "357": { + "tag": "357", + "label": "Originator Dissemination Control", + "url": "https://www.loc.gov/marc/bibliographic/bd357.html", + "repeatable": false, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Originator control term", + "repeatable": false + }, + "b": { + "label": "Originating agency", + "repeatable": true + }, + "c": { + "label": "Authorized recipients of material", + "repeatable": true + }, + "g": { + "label": "Other restrictions", + "repeatable": true + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "362": { + "tag": "362", + "label": "Dates of Publication and/or Sequential Designation", + "url": "https://www.loc.gov/marc/bibliographic/bd362.html", + "repeatable": true, + "indicator1": { + "label": "Format of date", + "codes": { + "0": { + "label": "Formatted style" + }, + "1": { + "label": "Unformatted note" + } + } + }, + "indicator2": null, + "subfields": { + "a": { + "label": "Dates of publication and/or sequential designation", + "repeatable": false + }, + "z": { + "label": "Source of information", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "363": { + "tag": "363", + "label": "Normalized Date and Sequential Designation", + "url": "https://www.loc.gov/marc/bibliographic/bd363.html", + "repeatable": true, + "indicator1": { + "label": "Start/End designator", + "codes": { + " ": { + "label": "No information provided" + }, + "0": { + "label": "Starting information" + }, + "1": { + "label": "Ending information" + } + } + }, + "indicator2": { + "label": "State of issuance", + "codes": { + " ": { + "label": "Not specified" + }, + "0": { + "label": "Closed" + }, + "1": { + "label": "Open" + } + } + }, + "subfields": { + "a": { + "label": "First level of enumeration", + "repeatable": false + }, + "b": { + "label": "Second level of enumeration", + "repeatable": false + }, + "c": { + "label": "Third level of enumeration", + "repeatable": false + }, + "d": { + "label": "Fourth level of enumeration", + "repeatable": false + }, + "e": { + "label": "Fifth level of enumeration", + "repeatable": false + }, + "f": { + "label": "Sixth level of enumeration", + "repeatable": false + }, + "g": { + "label": "Alternative numbering scheme, first level of enumeration", + "repeatable": false + }, + "h": { + "label": "Alternative numbering scheme, second level of enumeration", + "repeatable": false + }, + "i": { + "label": "First level of chronology", + "repeatable": false + }, + "j": { + "label": "Second level of chronology", + "repeatable": false + }, + "k": { + "label": "Third level of chronology", + "repeatable": false + }, + "l": { + "label": "Fourth level of chronology", + "repeatable": false + }, + "m": { + "label": "Alternative numbering scheme, chronology", + "repeatable": false + }, + "u": { + "label": "First level textual designation", + "repeatable": false + }, + "v": { + "label": "First level of chronology, issuance", + "repeatable": false + }, + "x": { + "label": "Nonpublic note", + "repeatable": true + }, + "z": { + "label": "Public note", + "repeatable": true + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "365": { + "tag": "365", + "label": "Trade Price", + "url": "https://www.loc.gov/marc/bibliographic/bd365.html", + "repeatable": true, + "indicator1": { + "label": "Start/End designator", + "codes": { + " ": { + "label": "No information provided" + }, + "0": { + "label": "Starting information" + }, + "1": { + "label": "Ending information" + } + } + }, + "indicator2": { + "label": "State of issuance", + "codes": { + " ": { + "label": "Not specified" + }, + "0": { + "label": "Closed" + }, + "1": { + "label": "Open" + } + } + }, + "subfields": { + "a": { + "label": "Price type code", + "repeatable": false + }, + "b": { + "label": "Price amount", + "repeatable": false + }, + "c": { + "label": "Currency code", + "repeatable": false + }, + "d": { + "label": "Unit of pricing", + "repeatable": false + }, + "e": { + "label": "Price note", + "repeatable": false + }, + "f": { + "label": "Price effective from", + "repeatable": false + }, + "g": { + "label": "Price effective until", + "repeatable": false + }, + "h": { + "label": "Tax rate 1", + "repeatable": false + }, + "i": { + "label": "Tax rate 2", + "repeatable": false + }, + "j": { + "label": "ISO country code", + "repeatable": false + }, + "k": { + "label": "MARC country code", + "repeatable": false, + "codelist": { + "name": "MARC Code List for Countries", + "url": "http://www.loc.gov/marc/countries/countries_code.html" + } + }, + "m": { + "label": "Identification of pricing entity", + "repeatable": false + }, + "2": { + "label": "Source of price type code", + "repeatable": false, + "codelist": { + "name": "Price Type Code Source Codes", + "url": "http://www.loc.gov/standards/sourcelist/price-type.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "366": { + "tag": "366", + "label": "Trade Availability Information", + "url": "https://www.loc.gov/marc/bibliographic/bd366.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Publishers' compressed title identification", + "repeatable": false + }, + "b": { + "label": "Detailed date of publication", + "repeatable": false + }, + "c": { + "label": "Availability status code", + "repeatable": false + }, + "d": { + "label": "Expected next availability date", + "repeatable": false + }, + "e": { + "label": "Note", + "repeatable": false + }, + "f": { + "label": "Publisher's discount category", + "repeatable": false + }, + "g": { + "label": "Date made out of print", + "repeatable": false + }, + "j": { + "label": "ISO country code", + "repeatable": false + }, + "k": { + "label": "MARC country code", + "repeatable": false, + "codelist": { + "name": "MARC Code List for Countries", + "url": "http://www.loc.gov/marc/countries/countries_code.html" + } + }, + "m": { + "label": "Identification of agency", + "repeatable": false + }, + "2": { + "label": "Source of availability status code", + "repeatable": false, + "codelist": { + "name": "Price Type Code Source Codes", + "url": "http://www.loc.gov/standards/sourcelist/price-type.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "370": { + "tag": "370", + "label": "Associated Place", + "url": "https://www.loc.gov/marc/bibliographic/bd370.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "c": { + "label": "Associated country", + "repeatable": true + }, + "f": { + "label": "Other associated place", + "repeatable": true + }, + "g": { + "label": "Place of origin of work or expression", + "repeatable": true + }, + "i": { + "label": "Relationship information", + "repeatable": true + }, + "s": { + "label": "Start period", + "repeatable": false + }, + "t": { + "label": "End period", + "repeatable": false + }, + "u": { + "label": "Uniform Resource Identifier", + "repeatable": true + }, + "v": { + "label": "Source of information", + "repeatable": true + }, + "0": { + "label": "Authority record control number or standard number", + "repeatable": true + }, + "2": { + "label": "Source of term", + "repeatable": false, + "codelist": { + "name": "Subject Heading and Term Source Codes", + "url": "http://www.loc.gov/standards/sourcelist/subject.html" + } + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "4": { + "label": "Relationship", + "repeatable": true + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "377": { + "tag": "377", + "label": "Associated Language", + "url": "https://www.loc.gov/marc/bibliographic/bd377.html", + "repeatable": true, + "indicator1": null, + "indicator2": { + "label": "Source of code", + "codes": { + " ": { + "label": "MARC language code" + }, + "7": { + "label": "Source specified in $2" + } + } + }, + "subfields": { + "a": { + "label": "Language code", + "repeatable": true + }, + "l": { + "label": "Language term", + "repeatable": true + }, + "0": { + "label": "Authority record control number or standard number", + "repeatable": true + }, + "2": { + "label": "Source", + "repeatable": false, + "codelist": { + "name": "Language Code and Term Source Codes", + "url": "http://www.loc.gov/standards/sourcelist/language.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "380": { + "tag": "380", + "label": "Form of Work", + "url": "https://www.loc.gov/marc/bibliographic/bd380.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Form of work", + "repeatable": true + }, + "0": { + "label": "Record control number", + "repeatable": true + }, + "2": { + "label": "Source of term", + "repeatable": false, + "codelist": { + "name": "Subject Heading and Term Source Codes", + "url": "http://www.loc.gov/standards/sourcelist/subject.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "381": { + "tag": "381", + "label": "Other Distinguishing Characteristics of Work or Expression", + "url": "https://www.loc.gov/marc/bibliographic/bd381.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Other distinguishing characteristic", + "repeatable": true + }, + "u": { + "label": "Uniform Resource Identifier", + "repeatable": true + }, + "v": { + "label": "Source of information", + "repeatable": true + }, + "0": { + "label": "Record control number", + "repeatable": true + }, + "2": { + "label": "Source of term", + "repeatable": false, + "codelist": { + "name": "Subject Heading and Term Source Codes", + "url": "http://www.loc.gov/standards/sourcelist/subject.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "382": { + "tag": "382", + "label": "Medium of Performance", + "url": "https://www.loc.gov/marc/bibliographic/bd382.html", + "repeatable": true, + "indicator1": { + "label": "Display constant controller", + "codes": { + " ": { + "label": "No information provided" + }, + "0": { + "label": "Medium of performance" + }, + "1": { + "label": "Partial medium of performance" + } + } + }, + "indicator2": { + "label": "Access control", + "codes": { + " ": { + "label": "No information provided" + }, + "0": { + "label": "Not intended for access" + }, + "1": { + "label": "Intended for access" + } + } + }, + "subfields": { + "a": { + "label": "Medium of performance", + "repeatable": true + }, + "b": { + "label": "Soloist", + "repeatable": true + }, + "d": { + "label": "Doubling instrument", + "repeatable": true + }, + "e": { + "label": "Number of ensembles of the same type", + "repeatable": true + }, + "n": { + "label": "Number of performers of the same medium", + "repeatable": true + }, + "p": { + "label": "Alternative medium of performance", + "repeatable": true + }, + "r": { + "label": "Total number of individuals performing alongside ensembles", + "repeatable": false + }, + "s": { + "label": "Total number of performers", + "repeatable": false + }, + "t": { + "label": "Total number of ensembles", + "repeatable": false + }, + "v": { + "label": "Note", + "repeatable": true + }, + "0": { + "label": "Authority record control number or standard number", + "repeatable": true + }, + "2": { + "label": "Source of term", + "repeatable": false, + "codelist": { + "name": "Musical Instrumentation and Voice Code Source Codes", + "url": "http://www.loc.gov/standards/sourcelist/musical-instrumentation.html" + } + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "383": { + "tag": "383", + "label": "Numeric Designation of Musical Work", + "url": "https://www.loc.gov/marc/bibliographic/bd383.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Serial number", + "repeatable": true + }, + "b": { + "label": "Opus number", + "repeatable": true + }, + "c": { + "label": "Thematic index number", + "repeatable": true + }, + "d": { + "label": "Thematic index code", + "repeatable": false + }, + "e": { + "label": "Publisher associated with opus number", + "repeatable": false + }, + "2": { + "label": "Source", + "repeatable": false, + "codelist": { + "name": "Thematic Index Code Source Codes", + "url": "http://www.loc.gov/standards/sourcelist/thematic-index.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "384": { + "tag": "384", + "label": "Key", + "url": "https://www.loc.gov/marc/bibliographic/bd384.html", + "repeatable": true, + "indicator1": { + "label": "Key type", + "codes": { + " ": { + "label": "Relationship to original unknown" + }, + "0": { + "label": "Original key" + }, + "1": { + "label": "Transposed key" + } + } + }, + "indicator2": null, + "subfields": { + "a": { + "label": "Key", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "385": { + "tag": "385", + "label": "Audience Characteristics", + "url": "https://www.loc.gov/marc/bibliographic/bd385.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Audience term", + "repeatable": true + }, + "b": { + "label": "Audience code", + "repeatable": true + }, + "m": { + "label": "Demographic group term", + "repeatable": false + }, + "n": { + "label": "Demographic group code", + "repeatable": false + }, + "0": { + "label": "Authority record control number or standard number", + "repeatable": true + }, + "2": { + "label": "Source", + "repeatable": false, + "codelist": { + "name": "Thematic Index Code Source Codes", + "url": "http://www.loc.gov/standards/sourcelist/thematic-index.html" + } + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "386": { + "tag": "386", + "label": "Creator/Contributor Characteristics", + "url": "https://www.loc.gov/marc/bibliographic/bd386.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Creator/contributor term", + "repeatable": true + }, + "b": { + "label": "Creator/contributor code", + "repeatable": true + }, + "i": { + "label": "Relationship information", + "repeatable": true + }, + "m": { + "label": "Demographic group term", + "repeatable": false + }, + "n": { + "label": "Demographic group code", + "repeatable": false + }, + "0": { + "label": "Authority record control number or standard number", + "repeatable": true + }, + "2": { + "label": "Source", + "repeatable": false, + "codelist": { + "name": "Subject Heading and Term Source Codes", + "url": "http://www.loc.gov/standards/sourcelist/subject.html" + } + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "4": { + "label": "Relationship", + "repeatable": true + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "388": { + "tag": "388", + "label": "Time Period of Creation", + "url": "https://www.loc.gov/marc/bibliographic/bd388.html", + "repeatable": true, + "indicator1": { + "label": "Type of time period", + "codes": { + " ": { + "label": "No information provided" + }, + "1": { + "label": "Creation of work" + }, + "2": { + "label": "Creation of aggregate work" + } + } + }, + "indicator2": null, + "subfields": { + "a": { + "label": "Time period of creation term", + "repeatable": true + }, + "0": { + "label": "Authority record control number or standard number", + "repeatable": true + }, + "2": { + "label": "Source", + "repeatable": false + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "400": { + "tag": "400", + "label": "Series Statement/Added Entry-Personal Name", + "url": "https://www.loc.gov/marc/bibliographic/bd400.html", + "repeatable": true, + "indicator1": { + "label": "Type of personal name entry element", + "codes": { + "0": { + "label": "Forename" + }, + "1": { + "label": "Surname" + }, + "3": { + "label": "Family name" + } + } + }, + "indicator2": { + "label": "Pronoun represents main entry", + "codes": { + "0": { + "label": "Main entry not represented by pronoun" + }, + "1": { + "label": "Main entry represented by pronoun" + } + } + }, + "subfields": { + "a": { + "label": "Personal name", + "repeatable": false + }, + "b": { + "label": "Numeration", + "repeatable": false + }, + "c": { + "label": "Titles and other words associated with a name", + "repeatable": true + }, + "d": { + "label": "Dates associated with a name", + "repeatable": false + }, + "e": { + "label": "Relator term", + "repeatable": true + }, + "f": { + "label": "Date of a work", + "repeatable": false + }, + "g": { + "label": "Miscellaneous information", + "repeatable": false + }, + "k": { + "label": "Form subheading", + "repeatable": true + }, + "l": { + "label": "Language of a work", + "repeatable": false + }, + "n": { + "label": "Number of part/section of a work", + "repeatable": true + }, + "p": { + "label": "Name of part/section of a work", + "repeatable": true + }, + "t": { + "label": "Title of a work", + "repeatable": false + }, + "u": { + "label": "Affiliation", + "repeatable": false + }, + "v": { + "label": "Volume/sequential designation", + "repeatable": false + }, + "x": { + "label": "International Standard Serial Number", + "repeatable": false + }, + "4": { + "label": "Relator code", + "repeatable": true, + "codelist": { + "name": "MARC Code List for Relators", + "url": "https://www.loc.gov/marc/relators/relaterm.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "410": { + "tag": "410", + "label": "Series Statement/Added Entry-Corporate Name", + "url": "https://www.loc.gov/marc/bibliographic/bd410.html", + "repeatable": true, + "indicator1": { + "label": "Type of corporate name entry element", + "codes": { + "0": { + "label": "Inverted name" + }, + "1": { + "label": "Jurisdiction name" + }, + "2": { + "label": "Name in direct order" + } + } + }, + "indicator2": { + "label": "Pronoun represents main entry", + "codes": { + "0": { + "label": "Main entry not represented by pronoun" + }, + "1": { + "label": "Main entry represented by pronoun" + } + } + }, + "subfields": { + "a": { + "label": "Corporate name or jurisdiction name as entry element", + "repeatable": false + }, + "b": { + "label": "Subordinate unit", + "repeatable": true + }, + "c": { + "label": "Location of meeting", + "repeatable": false + }, + "d": { + "label": "Date of meeting or treaty signing", + "repeatable": true + }, + "e": { + "label": "Relator term", + "repeatable": true + }, + "f": { + "label": "Date of a work", + "repeatable": false + }, + "g": { + "label": "Miscellaneous information", + "repeatable": false + }, + "k": { + "label": "Form subheading", + "repeatable": true + }, + "l": { + "label": "Language of a work", + "repeatable": false + }, + "n": { + "label": "Number of part/section/meeting", + "repeatable": true + }, + "p": { + "label": "Name of part/section of a work", + "repeatable": true + }, + "t": { + "label": "Title of a work", + "repeatable": false + }, + "u": { + "label": "Affiliation", + "repeatable": false + }, + "v": { + "label": "Volume/sequential designation", + "repeatable": false + }, + "x": { + "label": "International Standard Serial Number", + "repeatable": false + }, + "4": { + "label": "Relator code", + "repeatable": true, + "codelist": { + "name": "MARC Code List for Relators", + "url": "https://www.loc.gov/marc/relators/relaterm.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "411": { + "tag": "411", + "label": "Series Statement/Added Entry Meeting Name", + "url": "https://www.loc.gov/marc/bibliographic/bd411.html", + "repeatable": true, + "indicator1": { + "label": "Type of meeting name entry element", + "codes": { + "0": { + "label": "Inverted name" + }, + "1": { + "label": "Jurisdiction name" + }, + "2": { + "label": "Name in direct order" + } + } + }, + "indicator2": { + "label": "Pronoun represents main entry", + "codes": { + "0": { + "label": "Main entry not represented by pronoun" + }, + "9": { + "label": "Main entry represented by pronoun" + } + } + }, + "subfields": { + "a": { + "label": "Meeting name or jurisdiction name as entry element", + "repeatable": false + }, + "c": { + "label": "Location of meeting", + "repeatable": false + }, + "d": { + "label": "Date of meeting", + "repeatable": false + }, + "e": { + "label": "Subordinate unit", + "repeatable": true + }, + "f": { + "label": "Date of a work", + "repeatable": false + }, + "g": { + "label": "Miscellaneous information", + "repeatable": false + }, + "k": { + "label": "Form subheading", + "repeatable": true + }, + "l": { + "label": "Language of a work", + "repeatable": false + }, + "n": { + "label": "Number of part/section/meeting", + "repeatable": true + }, + "p": { + "label": "Name of part/section of a work", + "repeatable": true + }, + "q": { + "label": "Name of meeting following jurisdiction name entry element", + "repeatable": false + }, + "t": { + "label": "Title of a work", + "repeatable": false + }, + "u": { + "label": "Affiliation", + "repeatable": false + }, + "v": { + "label": "Volume/sequential designation", + "repeatable": false + }, + "x": { + "label": "International Standard Serial Number", + "repeatable": false + }, + "4": { + "label": "Relator code", + "repeatable": true, + "codelist": { + "name": "MARC Code List for Relators", + "url": "https://www.loc.gov/marc/relators/relaterm.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "440": { + "tag": "440", + "label": "Series Statement/Added Entry-Title", + "url": "https://www.loc.gov/marc/bibliographic/bd440.html", + "repeatable": true, + "indicator1": null, + "indicator2": { + "label": "Nonfiling characters", + "codes": { + "0": { + "label": "No nonfiling characters" + }, + "1-9": { + "label": "Number of nonfiling characters" + } + } + }, + "subfields": { + "a": { + "label": "Title", + "repeatable": false + }, + "n": { + "label": "Number of part/section of a work", + "repeatable": true + }, + "p": { + "label": "Name of part/section of a work", + "repeatable": true + }, + "v": { + "label": "Volume/sequential designation", + "repeatable": false + }, + "w": { + "label": "Bibliographic record control number", + "repeatable": true + }, + "x": { + "label": "International Standard Serial Number", + "repeatable": false + }, + "0": { + "label": "Authority record control number", + "repeatable": true + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + }, + "historical-subfields": { + "h": { + "label": "General material designation [OBSOLETE, 1997] [CAN/MARC only]" + } + } + }, + "490": { + "tag": "490", + "label": "Series Statement", + "url": "https://www.loc.gov/marc/bibliographic/bd490.html", + "repeatable": true, + "indicator1": { + "label": "Series tracing policy", + "codes": { + "0": { + "label": "Series not traced" + }, + "1": { + "label": "Series traced" + } + } + }, + "indicator2": null, + "subfields": { + "a": { + "label": "Series statement", + "repeatable": true + }, + "l": { + "label": "Library of Congress call number", + "repeatable": false + }, + "v": { + "label": "Volume/sequential designation", + "repeatable": true + }, + "x": { + "label": "International Standard Serial Number", + "repeatable": true + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "500": { + "tag": "500", + "label": "General Note", + "url": "https://www.loc.gov/marc/bibliographic/bd500.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "General note", + "repeatable": false + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "5": { + "label": "Institution to which field applies", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + }, + "historical-subfields": { + "l": { + "label": "Library of Congress call number (SE) [OBSOLETE, 1990]" + }, + "x": { + "label": "International Standard Serial Number (SE) [OBSOLETE, 1990]" + }, + "z": { + "label": "Source of note information (AM, SE) [OBSOLETE, 1990]" + } + } + }, + "501": { + "tag": "501", + "label": "With Note", + "url": "https://www.loc.gov/marc/bibliographic/bd501.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "With note", + "repeatable": false + }, + "5": { + "label": "Institution to which field applies", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "502": { + "tag": "502", + "label": "Dissertation Note", + "url": "https://www.loc.gov/marc/bibliographic/bd502.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Dissertation note", + "repeatable": false + }, + "b": { + "label": "Degree type", + "repeatable": false + }, + "c": { + "label": "Name of granting institution", + "repeatable": false + }, + "d": { + "label": "Year degree granted", + "repeatable": false + }, + "g": { + "label": "Miscellaneous information", + "repeatable": true + }, + "o": { + "label": "Dissertation identifier", + "repeatable": true + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "504": { + "tag": "504", + "label": "Bibliography, etc. Note", + "url": "https://www.loc.gov/marc/bibliographic/bd504.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Bibliography, etc. note", + "repeatable": false + }, + "b": { + "label": "Number of references", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "505": { + "tag": "505", + "label": "Formatted Contents Note", + "url": "https://www.loc.gov/marc/bibliographic/bd505.html", + "repeatable": true, + "indicator1": { + "label": "Display constant controller", + "codes": { + "0": { + "label": "Contents" + }, + "1": { + "label": "Incomplete contents" + }, + "2": { + "label": "Partial contents" + }, + "8": { + "label": "No display constant generated" + } + } + }, + "indicator2": { + "label": "Level of content designation", + "codes": { + " ": { + "label": "Basic" + }, + "0": { + "label": "Enhanced" + } + } + }, + "subfields": { + "a": { + "label": "Formatted contents note", + "repeatable": false + }, + "g": { + "label": "Miscellaneous information", + "repeatable": true + }, + "r": { + "label": "Statement of responsibility", + "repeatable": true + }, + "t": { + "label": "Title", + "repeatable": true + }, + "u": { + "label": "Uniform Resource Identifier", + "repeatable": true + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "506": { + "tag": "506", + "label": "Restrictions on Access Note", + "url": "https://www.loc.gov/marc/bibliographic/bd506.html", + "repeatable": true, + "indicator1": { + "label": "Restriction", + "codes": { + " ": { + "label": "No information provided" + }, + "0": { + "label": "No restrictions" + }, + "1": { + "label": "Restrictions apply" + } + } + }, + "indicator2": null, + "subfields": { + "a": { + "label": "Terms governing access", + "repeatable": false + }, + "b": { + "label": "Jurisdiction", + "repeatable": true + }, + "c": { + "label": "Physical access provisions", + "repeatable": true + }, + "d": { + "label": "Authorized users", + "repeatable": true + }, + "e": { + "label": "Authorization", + "repeatable": true + }, + "f": { + "label": "Standardized terminology for access restriction", + "repeatable": true + }, + "u": { + "label": "Uniform Resource Identifier", + "repeatable": true + }, + "2": { + "label": "Source of term", + "repeatable": false, + "codelist": { + "name": "Access Restriction Term Source Codes", + "url": "https://www.loc.gov/standards/sourcelist/access-restriction.html" + } + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "5": { + "label": "Institution to which field applies", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "507": { + "tag": "507", + "label": "Scale Note for Graphic Material", + "url": "https://www.loc.gov/marc/bibliographic/bd507.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Representative fraction of scale note", + "repeatable": false + }, + "b": { + "label": "Remainder of scale note", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "508": { + "tag": "508", + "label": "Creation/Production Credits Note", + "url": "https://www.loc.gov/marc/bibliographic/bd508.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Creation/production credits note", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "510": { + "tag": "510", + "label": "Citation/References Note", + "url": "https://www.loc.gov/marc/bibliographic/bd510.html", + "repeatable": true, + "indicator1": { + "label": "Coverage/location in source", + "codes": { + "0": { + "label": "Coverage unknown" + }, + "1": { + "label": "Coverage complete" + }, + "2": { + "label": "Coverage is selective" + }, + "3": { + "label": "Location in source not given" + }, + "4": { + "label": "Location in source given" + } + } + }, + "indicator2": null, + "subfields": { + "a": { + "label": "Name of source", + "repeatable": false + }, + "b": { + "label": "Coverage of source", + "repeatable": false + }, + "c": { + "label": "Location within source", + "repeatable": false + }, + "u": { + "label": "Uniform Resource Identifier", + "repeatable": true + }, + "x": { + "label": "International Standard Serial Number", + "repeatable": false + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "511": { + "tag": "511", + "label": "Participant or Performer Note", + "url": "https://www.loc.gov/marc/bibliographic/bd511.html", + "repeatable": true, + "indicator1": { + "label": "Display constant controller", + "codes": { + "0": { + "label": "No display constant generated" + }, + "1": { + "label": "Cast" + } + }, + "historical-codes": { + "2": { + "label": "Presenter (VM, MU) [OBSOLETE, 1993]" + }, + "3": { + "label": "Narrator (VM, MU) [OBSOLETE, 1993]" + } + } + }, + "indicator2": null, + "subfields": { + "a": { + "label": "Participant or performer note", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "513": { + "tag": "513", + "label": "Type of Report and Period Covered Note", + "url": "https://www.loc.gov/marc/bibliographic/bd513.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Type of report", + "repeatable": false + }, + "b": { + "label": "Period covered", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "514": { + "tag": "514", + "label": "Data Quality Note", + "url": "https://www.loc.gov/marc/bibliographic/bd514.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Attribute accuracy report", + "repeatable": false + }, + "b": { + "label": "Attribute accuracy value", + "repeatable": true + }, + "c": { + "label": "Attribute accuracy explanation", + "repeatable": true + }, + "d": { + "label": "Logical consistency report", + "repeatable": false + }, + "e": { + "label": "Completeness report", + "repeatable": false + }, + "f": { + "label": "Horizontal position accuracy report", + "repeatable": false + }, + "g": { + "label": "Horizontal position accuracy value", + "repeatable": true + }, + "h": { + "label": "Horizontal position accuracy explanation", + "repeatable": true + }, + "i": { + "label": "Vertical positional accuracy report", + "repeatable": false + }, + "j": { + "label": "Vertical positional accuracy value", + "repeatable": true + }, + "k": { + "label": "Vertical positional accuracy explanation", + "repeatable": true + }, + "m": { + "label": "Cloud cover", + "repeatable": false + }, + "u": { + "label": "Uniform Resource Identifier", + "repeatable": true + }, + "z": { + "label": "Display note", + "repeatable": true + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "515": { + "tag": "515", + "label": "Numbering Peculiarities Note", + "url": "https://www.loc.gov/marc/bibliographic/bd515.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Numbering peculiarities note", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + }, + "historical-subfields": { + "z": { + "label": "Source of note information (SE) [OBSOLETE, 1990]" + } + } + }, + "516": { + "tag": "516", + "label": "Type of Computer File or Data Note", + "url": "https://www.loc.gov/marc/bibliographic/bd516.html", + "repeatable": true, + "indicator1": { + "label": "Display constant controller", + "codes": { + " ": { + "label": "Type of file" + }, + "8": { + "label": "No display constant generated" + } + } + }, + "indicator2": null, + "subfields": { + "a": { + "label": "Type of computer file or data note", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "518": { + "tag": "518", + "label": "Date/Time and Place of an Event Note", + "url": "https://www.loc.gov/marc/bibliographic/bd518.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Date/time and place of an event note", + "repeatable": false + }, + "d": { + "label": "Date of event", + "repeatable": true + }, + "o": { + "label": "Other event information", + "repeatable": true + }, + "p": { + "label": "Place of event", + "repeatable": true + }, + "0": { + "label": "Record control number", + "repeatable": true + }, + "2": { + "label": "Source of term", + "repeatable": true, + "codelist": { + "name": "Subject Heading and Term Source Codes", + "url": "http://www.loc.gov/standards/sourcelist/subject.html" + } + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "520": { + "tag": "520", + "label": "Summary, etc.", + "url": "https://www.loc.gov/marc/bibliographic/bd520.html", + "repeatable": true, + "indicator1": { + "label": "Display constant controller", + "codes": { + " ": { + "label": "Summary" + }, + "0": { + "label": "Subject" + }, + "1": { + "label": "Review" + }, + "2": { + "label": "Scope and content" + }, + "3": { + "label": "Abstract" + }, + "4": { + "label": "Content advice" + }, + "8": { + "label": "No display constant generated" + } + } + }, + "indicator2": null, + "subfields": { + "a": { + "label": "Summary, etc.", + "repeatable": false + }, + "b": { + "label": "Expansion of summary note", + "repeatable": false + }, + "c": { + "label": "Assigning source", + "repeatable": false + }, + "u": { + "label": "Uniform Resource Identifier", + "repeatable": true + }, + "2": { + "label": "Source", + "repeatable": false, + "codelist": { + "name": "Content Advice Classification Source Codes", + "url": "https://www.loc.gov/standards/sourcelist/content-advice.html" + } + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + }, + "historical-subfields": { + "z": { + "label": "Source of note information (BK, AM, CF, SE) [OBSOLETE, 1990]" + } + } + }, + "521": { + "tag": "521", + "label": "Target Audience Note", + "url": "https://www.loc.gov/marc/bibliographic/bd521.html", + "repeatable": true, + "indicator1": { + "label": "Display constant controller", + "codes": { + " ": { + "label": "Audience" + }, + "0": { + "label": "Reading grade level" + }, + "1": { + "label": "Interest age level" + }, + "2": { + "label": "Interest grade level" + }, + "3": { + "label": "Special audience characteristics" + }, + "4": { + "label": "Motivation/interest level" + }, + "8": { + "label": "No display constant generated" + } + } + }, + "indicator2": null, + "subfields": { + "a": { + "label": "Target audience note", + "repeatable": true + }, + "b": { + "label": "Source", + "repeatable": false + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "522": { + "tag": "522", + "label": "Geographic Coverage Note", + "url": "https://www.loc.gov/marc/bibliographic/bd522.html", + "repeatable": true, + "indicator1": { + "label": "Display constant controller", + "codes": { + " ": { + "label": "Geographic coverage" + }, + "8": { + "label": "No display constant generated" + } + } + }, + "indicator2": null, + "subfields": { + "a": { + "label": "Geographic coverage note", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "524": { + "tag": "524", + "label": "Preferred Citation of Described Materials Note", + "url": "https://www.loc.gov/marc/bibliographic/bd524.html", + "repeatable": true, + "indicator1": { + "label": "Display constant controller", + "codes": { + " ": { + "label": "Cite as" + }, + "8": { + "label": "No display constant generated" + } + } + }, + "indicator2": null, + "subfields": { + "a": { + "label": "Preferred citation of described materials note", + "repeatable": false + }, + "2": { + "label": "Source of schema used", + "repeatable": false, + "codelist": { + "name": "Citation Scheme Source Codes", + "url": "https://www.loc.gov/standards/sourcelist/citation.html" + } + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "525": { + "tag": "525", + "label": "Supplement Note", + "url": "https://www.loc.gov/marc/bibliographic/bd525.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Supplement note", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + }, + "historical-subfields": { + "z": { + "label": "Source of note information (SE) [OBSOLETE, 1990]" + } + } + }, + "526": { + "tag": "526", + "label": "Study Program Information Note", + "url": "https://www.loc.gov/marc/bibliographic/bd526.html", + "repeatable": true, + "indicator1": { + "label": "Display constant controller", + "codes": { + "0": { + "label": "Reading program" + }, + "8": { + "label": "No display constant generated" + } + } + }, + "indicator2": null, + "subfields": { + "a": { + "label": "Program name", + "repeatable": false + }, + "b": { + "label": "Interest level", + "repeatable": false + }, + "c": { + "label": "Reading level", + "repeatable": false + }, + "d": { + "label": "Title point value", + "repeatable": false + }, + "i": { + "label": "Display text", + "repeatable": false + }, + "x": { + "label": "Nonpublic note", + "repeatable": true + }, + "z": { + "label": "Public note", + "repeatable": true + }, + "5": { + "label": "Institution to which field applies", + "repeatable": false, + "codelist": { + "name": "MARC Organization Codes", + "url": "http://www.loc.gov/marc/organizations/orgshome.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "530": { + "tag": "530", + "label": "Additional Physical Form available Note", + "url": "https://www.loc.gov/marc/bibliographic/bd530.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Additional physical form available note", + "repeatable": false + }, + "b": { + "label": "Availability source", + "repeatable": false + }, + "c": { + "label": "Availability conditions", + "repeatable": false + }, + "d": { + "label": "Order number", + "repeatable": false + }, + "u": { + "label": "Uniform Resource Identifier", + "repeatable": true + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + }, + "historical-subfields": { + "z": { + "label": "Source of note information (AM, CF, VM, SE) [OBSOLETE, 1990]" + } + } + }, + "533": { + "tag": "533", + "label": "Reproduction Note", + "url": "https://www.loc.gov/marc/bibliographic/bd533.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Type of reproduction", + "repeatable": false + }, + "b": { + "label": "Place of reproduction", + "repeatable": true + }, + "c": { + "label": "Agency responsible for reproduction", + "repeatable": true + }, + "d": { + "label": "Date of reproduction", + "repeatable": false + }, + "e": { + "label": "Physical description of reproduction", + "repeatable": false + }, + "f": { + "label": "Series statement of reproduction", + "repeatable": true + }, + "m": { + "label": "Dates and/or sequential designation of issues reproduced", + "repeatable": true + }, + "n": { + "label": "Note about reproduction", + "repeatable": true + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "5": { + "label": "Institution to which field applies", + "repeatable": false + }, + "7": { + "label": "Fixed-length data elements of reproduction", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "534": { + "tag": "534", + "label": "Original Version Note", + "url": "https://www.loc.gov/marc/bibliographic/bd534.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Main entry of original", + "repeatable": false + }, + "b": { + "label": "Edition statement of original", + "repeatable": false + }, + "c": { + "label": "Publication, distribution, etc. of original", + "repeatable": false + }, + "e": { + "label": "Physical description, etc. of original", + "repeatable": false + }, + "f": { + "label": "Series statement of original", + "repeatable": true + }, + "k": { + "label": "Key title of original", + "repeatable": true + }, + "l": { + "label": "Location of original", + "repeatable": false + }, + "m": { + "label": "Material specific details", + "repeatable": false + }, + "n": { + "label": "Note about original", + "repeatable": true + }, + "o": { + "label": "Other resource identifier", + "repeatable": true + }, + "p": { + "label": "Introductory phrase", + "repeatable": false + }, + "t": { + "label": "Title statement of original", + "repeatable": false + }, + "x": { + "label": "International Standard Serial Number", + "repeatable": true + }, + "z": { + "label": "International Standard Book Number", + "repeatable": true + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "535": { + "tag": "535", + "label": "Location of Originals/Duplicates Note", + "url": "https://www.loc.gov/marc/bibliographic/bd535.html", + "repeatable": true, + "indicator1": { + "label": "Custodial role", + "codes": { + "1": { + "label": "Holder of originals" + }, + "2": { + "label": "Holder of duplicates" + } + }, + "historical-codes": { + "0": { + "label": "Repository (AM) [OBSOLETE, 1984]" + }, + "3": { + "label": "Holder of oral tapes (AM) [OBSOLETE, 1984]" + } + } + }, + "indicator2": null, + "subfields": { + "a": { + "label": "Custodian", + "repeatable": false + }, + "b": { + "label": "Postal address", + "repeatable": true + }, + "c": { + "label": "Country", + "repeatable": true + }, + "d": { + "label": "Telecommunications address", + "repeatable": true + }, + "g": { + "label": "Repository location code", + "repeatable": false, + "codelist": { + "name": "MARC Code List for Countries", + "url": "http://www.loc.gov/marc/countries/countries_code.html" + } + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "536": { + "tag": "536", + "label": "Funding Information Note", + "url": "https://www.loc.gov/marc/bibliographic/bd536.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Text of note", + "repeatable": false + }, + "b": { + "label": "Contract number", + "repeatable": true + }, + "c": { + "label": "Grant number", + "repeatable": true + }, + "d": { + "label": "Undifferentiated number", + "repeatable": true + }, + "e": { + "label": "Program element number", + "repeatable": true + }, + "f": { + "label": "Project number", + "repeatable": true + }, + "g": { + "label": "Task number", + "repeatable": true + }, + "h": { + "label": "Work unit number", + "repeatable": true + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "538": { + "tag": "538", + "label": "System Details Note", + "url": "https://www.loc.gov/marc/bibliographic/bd538.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "System details note", + "repeatable": false + }, + "i": { + "label": "Display text", + "repeatable": false + }, + "u": { + "label": "Uniform Resource Identifier", + "repeatable": true + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "5": { + "label": "Institution to which field applies", + "repeatable": true + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "540": { + "tag": "540", + "label": "Terms Governing Use and Reproduction Note", + "url": "https://www.loc.gov/marc/bibliographic/bd540.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Terms governing use and reproduction", + "repeatable": false + }, + "b": { + "label": "Jurisdiction", + "repeatable": false + }, + "c": { + "label": "Authorization", + "repeatable": false + }, + "d": { + "label": "Authorized users", + "repeatable": false + }, + "u": { + "label": "Uniform Resource Identifier", + "repeatable": true + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "5": { + "label": "Institution to which field applies", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "541": { + "tag": "541", + "label": "Immediate Source of Acquisition Note", + "url": "https://www.loc.gov/marc/bibliographic/bd541.html", + "repeatable": true, + "indicator1": { + "label": "Privacy", + "codes": { + " ": { + "label": "No information provided" + }, + "0": { + "label": "Private" + }, + "1": { + "label": "Not private" + } + } + }, + "indicator2": null, + "subfields": { + "a": { + "label": "Source of acquisition", + "repeatable": false + }, + "b": { + "label": "Address", + "repeatable": false + }, + "c": { + "label": "Method of acquisition", + "repeatable": false + }, + "d": { + "label": "Date of acquisition", + "repeatable": false + }, + "e": { + "label": "Accession number", + "repeatable": false + }, + "f": { + "label": "Owner", + "repeatable": false + }, + "h": { + "label": "Purchase price", + "repeatable": false + }, + "n": { + "label": "Extent", + "repeatable": true + }, + "o": { + "label": "Type of unit", + "repeatable": true + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "5": { + "label": "Institution to which field applies", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "542": { + "tag": "542", + "label": "Information Relating to Copyright Status", + "url": "https://www.loc.gov/marc/bibliographic/bd542.html", + "repeatable": true, + "indicator1": { + "label": "Privacy", + "codes": { + " ": { + "label": "No information provided" + }, + "0": { + "label": "Private" + }, + "1": { + "label": "Not private" + } + } + }, + "indicator2": null, + "subfields": { + "a": { + "label": "Personal creator", + "repeatable": false + }, + "b": { + "label": "Personal creator death date", + "repeatable": false + }, + "c": { + "label": "Corporate creator", + "repeatable": false + }, + "d": { + "label": "Copyright holder", + "repeatable": true + }, + "e": { + "label": "Copyright holder contact information", + "repeatable": true + }, + "f": { + "label": "Copyright statement", + "repeatable": true + }, + "g": { + "label": "Copyright date", + "repeatable": false + }, + "h": { + "label": "Copyright renewal date", + "repeatable": true + }, + "i": { + "label": "Publication date", + "repeatable": false + }, + "j": { + "label": "Creation date", + "repeatable": false + }, + "k": { + "label": "Publisher", + "repeatable": true + }, + "l": { + "label": "Copyright status", + "repeatable": false + }, + "m": { + "label": "Publication status", + "repeatable": false + }, + "n": { + "label": "Note", + "repeatable": true + }, + "o": { + "label": "Research date", + "repeatable": false + }, + "p": { + "label": "Country of publication or creation", + "repeatable": true + }, + "q": { + "label": "Supplying agency", + "repeatable": false + }, + "r": { + "label": "Jurisdiction of copyright assessment", + "repeatable": false + }, + "s": { + "label": "Source of information", + "repeatable": false + }, + "u": { + "label": "Uniform Resource Identifier", + "repeatable": true + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "544": { + "tag": "544", + "label": "Location of Other Archival Materials Note", + "url": "https://www.loc.gov/marc/bibliographic/bd544.html", + "repeatable": true, + "indicator1": { + "label": "Relationship", + "codes": { + " ": { + "label": "No information provided" + }, + "0": { + "label": "Associated materials" + }, + "1": { + "label": "Related materials" + } + } + }, + "indicator2": null, + "subfields": { + "a": { + "label": "Custodian", + "repeatable": true + }, + "b": { + "label": "Address", + "repeatable": true + }, + "c": { + "label": "Country", + "repeatable": true + }, + "d": { + "label": "Title", + "repeatable": true + }, + "e": { + "label": "Provenance", + "repeatable": true + }, + "n": { + "label": "Note", + "repeatable": true + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "545": { + "tag": "545", + "label": "Biographical or Historical Data", + "url": "https://www.loc.gov/marc/bibliographic/bd545.html", + "repeatable": true, + "indicator1": { + "label": "Type of data", + "codes": { + " ": { + "label": "No information provided" + }, + "0": { + "label": "Biographical sketch" + }, + "1": { + "label": "Administrative history" + } + } + }, + "indicator2": null, + "subfields": { + "a": { + "label": "Biographical or historical data", + "repeatable": false + }, + "b": { + "label": "Expansion", + "repeatable": false + }, + "u": { + "label": "Uniform Resource Identifier", + "repeatable": true + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "546": { + "tag": "546", + "label": "Language Note", + "url": "https://www.loc.gov/marc/bibliographic/bd546.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Language note", + "repeatable": false + }, + "b": { + "label": "Information code or alphabet", + "repeatable": true + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + }, + "historical-subfields": { + "z": { + "label": "Source of note information (SE) [OBSOLETE, 1990]" + } + } + }, + "547": { + "tag": "547", + "label": "Former Title Complexity Note", + "url": "https://www.loc.gov/marc/bibliographic/bd547.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Former title complexity note", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + }, + "historical-subfields": { + "z": { + "label": "Source of note information (SE) [OBSOLETE, 1990]" + } + } + }, + "550": { + "tag": "550", + "label": "Issuing Body Note", + "url": "https://www.loc.gov/marc/bibliographic/bd550.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Issuing body note", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + }, + "historical-subfields": { + "z": { + "label": "Source of note information (SE) [OBSOLETE, 1990]" + } + } + }, + "552": { + "tag": "552", + "label": "Entity and Attribute Information Note", + "url": "https://www.loc.gov/marc/bibliographic/bd552.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Entity type label", + "repeatable": false + }, + "b": { + "label": "Entity type definition and source", + "repeatable": false + }, + "c": { + "label": "Attribute label", + "repeatable": false + }, + "d": { + "label": "Attribute definition and source", + "repeatable": false + }, + "e": { + "label": "Enumerated domain value", + "repeatable": true + }, + "f": { + "label": "Enumerated domain value definition and source", + "repeatable": true + }, + "g": { + "label": "Range domain minimum and maximum", + "repeatable": false + }, + "h": { + "label": "Codeset name and source", + "repeatable": false + }, + "i": { + "label": "Unrepresentable domain", + "repeatable": false + }, + "j": { + "label": "Attribute units of measurement and resolution", + "repeatable": false + }, + "k": { + "label": "Beginning and ending date of attribute values", + "repeatable": false + }, + "l": { + "label": "Attribute value accuracy", + "repeatable": false + }, + "m": { + "label": "Attribute value accuracy explanation", + "repeatable": false + }, + "n": { + "label": "Attribute measurement frequency", + "repeatable": false + }, + "o": { + "label": "Entity and attribute overview", + "repeatable": true + }, + "p": { + "label": "Entity and attribute detail citation", + "repeatable": true + }, + "u": { + "label": "Uniform Resource Identifier", + "repeatable": true + }, + "z": { + "label": "Display note", + "repeatable": true + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "555": { + "tag": "555", + "label": "Cumulative Index/Finding Aids Note", + "url": "https://www.loc.gov/marc/bibliographic/bd555.html", + "repeatable": true, + "indicator1": { + "label": "Display constant controller", + "codes": { + " ": { + "label": "Indexes" + }, + "0": { + "label": "Finding aids" + }, + "8": { + "label": "No display constant generated" + } + } + }, + "indicator2": null, + "subfields": { + "a": { + "label": "Cumulative index/finding aids note", + "repeatable": false + }, + "b": { + "label": "Availability source", + "repeatable": true + }, + "c": { + "label": "Degree of control", + "repeatable": false + }, + "d": { + "label": "Bibliographic reference", + "repeatable": false + }, + "u": { + "label": "Uniform Resource Identifier", + "repeatable": true + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "556": { + "tag": "556", + "label": "Information About Documentation Note", + "url": "https://www.loc.gov/marc/bibliographic/bd556.html", + "repeatable": true, + "indicator1": { + "label": "Display constant controller", + "codes": { + " ": { + "label": "Documentation" + }, + "8": { + "label": "No display constant generated" + } + } + }, + "indicator2": null, + "subfields": { + "a": { + "label": "Information about documentation note", + "repeatable": false + }, + "z": { + "label": "International Standard Book Number", + "repeatable": true + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "561": { + "tag": "561", + "label": "Ownership and Custodial History", + "url": "https://www.loc.gov/marc/bibliographic/bd561.html", + "repeatable": true, + "indicator1": { + "label": "Privacy", + "codes": { + " ": { + "label": "No information provided" + }, + "0": { + "label": "Private" + }, + "1": { + "label": "Not private" + } + } + }, + "indicator2": null, + "subfields": { + "a": { + "label": "History", + "repeatable": false + }, + "u": { + "label": "Uniform Resource Identifier", + "repeatable": true + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "5": { + "label": "Institution to which field applies", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + }, + "historical-subfields": { + "b": { + "label": "Time of collation (NR) [OBSOLETE, 1997]" + } + } + }, + "562": { + "tag": "562", + "label": "Copy and Version Identification Note", + "url": "https://www.loc.gov/marc/bibliographic/bd562.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Identifying markings", + "repeatable": true + }, + "b": { + "label": "Copy identification", + "repeatable": true + }, + "c": { + "label": "Version identification", + "repeatable": true + }, + "d": { + "label": "Presentation format", + "repeatable": true + }, + "e": { + "label": "Number of copies", + "repeatable": true + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "5": { + "label": "Institution to which field applies", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "563": { + "tag": "563", + "label": "Binding Information", + "url": "https://www.loc.gov/marc/bibliographic/bd563.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Binding note", + "repeatable": false + }, + "u": { + "label": "Uniform Resource Identifier", + "repeatable": true + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "5": { + "label": "Institution to which field applies", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "565": { + "tag": "565", + "label": "Case File Characteristics Note", + "url": "https://www.loc.gov/marc/bibliographic/bd565.html", + "repeatable": true, + "indicator1": { + "label": "Display constant controller", + "codes": { + " ": { + "label": "File size" + }, + "0": { + "label": "Case file characteristics" + }, + "8": { + "label": "No display constant generated" + } + } + }, + "indicator2": null, + "subfields": { + "a": { + "label": "Number of cases/variables", + "repeatable": false + }, + "b": { + "label": "Name of variable", + "repeatable": true + }, + "c": { + "label": "Unit of analysis", + "repeatable": true + }, + "d": { + "label": "Universe of data", + "repeatable": true + }, + "e": { + "label": "Filing scheme or code", + "repeatable": true + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "567": { + "tag": "567", + "label": "Methodology Note", + "url": "https://www.loc.gov/marc/bibliographic/bd567.html", + "repeatable": true, + "indicator1": { + "label": "Display constant controller", + "codes": { + " ": { + "label": "Methodology" + }, + "8": { + "label": "No display constant generated" + } + } + }, + "indicator2": null, + "subfields": { + "a": { + "label": "Methodology note", + "repeatable": false + }, + "b": { + "label": "Controlled term", + "repeatable": true + }, + "0": { + "label": "Authority record control number or standard number", + "repeatable": true + }, + "2": { + "label": "Source of term", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "580": { + "tag": "580", + "label": "Linking Entry Complexity Note", + "url": "https://www.loc.gov/marc/bibliographic/bd580.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Linking entry complexity note", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + }, + "historical-subfields": { + "z": { + "label": "Source of note information [OBSOLETE, 1990]" + } + } + }, + "581": { + "tag": "581", + "label": "Publications About Described Materials Note", + "url": "https://www.loc.gov/marc/bibliographic/bd581.html", + "repeatable": true, + "indicator1": { + "label": "Display constant controller", + "codes": { + " ": { + "label": "Publications" + }, + "8": { + "label": "No display constant generated" + } + } + }, + "indicator2": null, + "subfields": { + "a": { + "label": "Publications about described materials note", + "repeatable": false + }, + "z": { + "label": "International Standard Book Number", + "repeatable": true + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "583": { + "tag": "583", + "label": "Action Note", + "url": "https://www.loc.gov/marc/bibliographic/bd583.html", + "repeatable": true, + "indicator1": { + "label": "Privacy", + "codes": { + " ": { + "label": "No information provided" + }, + "0": { + "label": "Private" + }, + "1": { + "label": "Not private" + } + } + }, + "indicator2": null, + "subfields": { + "a": { + "label": "Action", + "repeatable": false + }, + "b": { + "label": "Action identification", + "repeatable": true + }, + "c": { + "label": "Time/date of action", + "repeatable": true + }, + "d": { + "label": "Action interval", + "repeatable": true + }, + "e": { + "label": "Contingency for action", + "repeatable": true + }, + "f": { + "label": "Authorization", + "repeatable": true + }, + "h": { + "label": "Jurisdiction", + "repeatable": true + }, + "i": { + "label": "Method of action", + "repeatable": true + }, + "j": { + "label": "Site of action", + "repeatable": true + }, + "k": { + "label": "Action agent", + "repeatable": true + }, + "l": { + "label": "Status", + "repeatable": true + }, + "n": { + "label": "Extent", + "repeatable": true + }, + "o": { + "label": "Type of unit", + "repeatable": true + }, + "u": { + "label": "Uniform Resource Identifier", + "repeatable": true + }, + "x": { + "label": "Nonpublic note", + "repeatable": true + }, + "z": { + "label": "Public note", + "repeatable": true + }, + "2": { + "label": "Source of term", + "repeatable": false, + "codelist": { + "name": "Resource Action Term Source Codes", + "url": "https://www.loc.gov/standards/sourcelist/resource-action.html" + } + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "5": { + "label": "Institution to which field applies", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "584": { + "tag": "584", + "label": "Accumulation and Frequency of Use Note", + "url": "https://www.loc.gov/marc/bibliographic/bd584.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Accumulation", + "repeatable": true + }, + "b": { + "label": "Frequency of use", + "repeatable": true + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "5": { + "label": "Institution to which field applies", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "585": { + "tag": "585", + "label": "Exhibitions Note", + "url": "https://www.loc.gov/marc/bibliographic/bd585.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Exhibitions note", + "repeatable": false + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "5": { + "label": "Institution to which field applies", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "586": { + "tag": "586", + "label": "Awards Note", + "url": "https://www.loc.gov/marc/bibliographic/bd586.html", + "repeatable": true, + "indicator1": { + "label": "Display constant controller", + "codes": { + " ": { + "label": "Awards" + }, + "8": { + "label": "No display constant generated" + } + } + }, + "indicator2": null, + "subfields": { + "a": { + "label": "Awards note", + "repeatable": false + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "588": { + "tag": "588", + "label": "Source of Description Note", + "url": "https://www.loc.gov/marc/bibliographic/bd588.html", + "repeatable": true, + "indicator1": { + "label": "Display constant controller", + "codes": { + " ": { + "label": "No information provided" + }, + "0": { + "label": "Source of description" + }, + "1": { + "label": "Latest issue consulted" + } + } + }, + "indicator2": null, + "subfields": { + "a": { + "label": "Source of description note", + "repeatable": false + }, + "5": { + "label": "Institution to which field applies", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "600": { + "tag": "600", + "label": "Subject Added Entry - Personal Name", + "url": "https://www.loc.gov/marc/bibliographic/bd600.html", + "repeatable": true, + "indicator1": { + "label": "Type of personal name entry element", + "codes": { + "0": { + "label": "Forename" + }, + "1": { + "label": "Surname" + }, + "3": { + "label": "Family name" + } + }, + "historical-codes": { + "2": { + "label": "Multiple surname [OBSOLETE, 1996]" + } + } + }, + "indicator2": { + "label": "Thesaurus", + "codes": { + "0": { + "label": "Library of Congress Subject Headings" + }, + "1": { + "label": "LC subject headings for children's literature" + }, + "2": { + "label": "Medical Subject Headings" + }, + "3": { + "label": "National Agricultural Library subject authority file" + }, + "4": { + "label": "Source not specified" + }, + "5": { + "label": "Canadian Subject Headings" + }, + "6": { + "label": "Répertoire de vedettes-matière" + }, + "7": { + "label": "Source specified in subfield $2" + } + } + }, + "subfields": { + "a": { + "label": "Personal name", + "repeatable": false + }, + "b": { + "label": "Numeration", + "repeatable": false + }, + "c": { + "label": "Titles and other words associated with a name", + "repeatable": true + }, + "d": { + "label": "Dates associated with a name", + "repeatable": false + }, + "e": { + "label": "Relator term", + "repeatable": true + }, + "f": { + "label": "Date of a work", + "repeatable": false + }, + "g": { + "label": "Miscellaneous information", + "repeatable": true + }, + "h": { + "label": "Medium", + "repeatable": false + }, + "j": { + "label": "Attribution qualifier", + "repeatable": true + }, + "k": { + "label": "Form subheading", + "repeatable": true + }, + "l": { + "label": "Language of a work", + "repeatable": false + }, + "m": { + "label": "Medium of performance for music", + "repeatable": true + }, + "n": { + "label": "Number of part/section of a work", + "repeatable": true + }, + "o": { + "label": "Arranged statement for music", + "repeatable": false + }, + "p": { + "label": "Name of part/section of a work", + "repeatable": true + }, + "q": { + "label": "Fuller form of name", + "repeatable": false + }, + "r": { + "label": "Key for music", + "repeatable": false + }, + "s": { + "label": "Version", + "repeatable": false + }, + "t": { + "label": "Title of a work", + "repeatable": false + }, + "u": { + "label": "Affiliation", + "repeatable": false + }, + "v": { + "label": "Form subdivision", + "repeatable": true + }, + "x": { + "label": "General subdivision", + "repeatable": true + }, + "y": { + "label": "Chronological subdivision", + "repeatable": true + }, + "z": { + "label": "Geographic subdivision", + "repeatable": true + }, + "0": { + "label": "Authority record control number or standard number", + "repeatable": true + }, + "2": { + "label": "Source of heading or term", + "repeatable": false, + "codelist": { + "name": "Subject Heading and Term Source Codes", + "url": "http://www.loc.gov/standards/sourcelist/subject.html" + } + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "4": { + "label": "Relationship", + "repeatable": true, + "codelist": { + "name": "MARC Code List for Relators", + "url": "https://www.loc.gov/marc/relators/relaterm.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "610": { + "tag": "610", + "label": "Subject Added Entry - Corporate Name", + "url": "https://www.loc.gov/marc/bibliographic/bd610.html", + "repeatable": true, + "indicator1": { + "label": "Type of corporate name entry element", + "codes": { + "0": { + "label": "Inverted name" + }, + "1": { + "label": "Jurisdiction name" + }, + "2": { + "label": "Name in direct order" + } + } + }, + "indicator2": { + "label": "Thesaurus", + "codes": { + "0": { + "label": "Library of Congress Subject Headings" + }, + "1": { + "label": "LC subject headings for children's literature" + }, + "2": { + "label": "Medical Subject Headings" + }, + "3": { + "label": "National Agricultural Library subject authority file" + }, + "4": { + "label": "Source not specified" + }, + "5": { + "label": "Canadian Subject Headings" + }, + "6": { + "label": "Répertoire de vedettes-matière" + }, + "7": { + "label": "Source specified in subfield $2" + } + } + }, + "subfields": { + "a": { + "label": "Corporate name or jurisdiction name as entry element", + "repeatable": false + }, + "b": { + "label": "Subordinate unit", + "repeatable": true + }, + "c": { + "label": "Location of meeting", + "repeatable": true + }, + "d": { + "label": "Date of meeting or treaty signing", + "repeatable": true + }, + "e": { + "label": "Relator term", + "repeatable": true + }, + "f": { + "label": "Date of a work", + "repeatable": false + }, + "g": { + "label": "Miscellaneous information", + "repeatable": true + }, + "h": { + "label": "Medium", + "repeatable": false + }, + "k": { + "label": "Form subheading", + "repeatable": true + }, + "l": { + "label": "Language of a work", + "repeatable": false + }, + "m": { + "label": "Medium of performance for music", + "repeatable": true + }, + "n": { + "label": "Number of part/section/meeting", + "repeatable": true + }, + "o": { + "label": "Arranged statement for music", + "repeatable": false + }, + "p": { + "label": "Name of part/section of a work", + "repeatable": true + }, + "r": { + "label": "Key for music", + "repeatable": false + }, + "s": { + "label": "Version", + "repeatable": false + }, + "t": { + "label": "Title of a work", + "repeatable": false + }, + "u": { + "label": "Affiliation", + "repeatable": false + }, + "v": { + "label": "Form subdivision", + "repeatable": true + }, + "x": { + "label": "General subdivision", + "repeatable": true + }, + "y": { + "label": "Chronological subdivision", + "repeatable": true + }, + "z": { + "label": "Geographic subdivision", + "repeatable": true + }, + "0": { + "label": "Authority record control number or standard number", + "repeatable": true + }, + "2": { + "label": "Source of heading or term", + "repeatable": false + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "4": { + "label": "Relationship", + "repeatable": true, + "codelist": { + "name": "MARC Code List for Relators", + "url": "https://www.loc.gov/marc/relators/relaterm.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "611": { + "tag": "611", + "label": "Subject Added Entry - Meeting Name", + "url": "https://www.loc.gov/marc/bibliographic/bd611.html", + "repeatable": true, + "indicator1": { + "label": "Type of meeting name entry element", + "codes": { + "0": { + "label": "Inverted name" + }, + "1": { + "label": "Jurisdiction name" + }, + "2": { + "label": "Name in direct order" + } + } + }, + "indicator2": { + "label": "Thesaurus", + "codes": { + "0": { + "label": "Library of Congress Subject Headings" + }, + "1": { + "label": "LC subject headings for children's literature" + }, + "2": { + "label": "Medical Subject Headings" + }, + "3": { + "label": "National Agricultural Library subject authority file" + }, + "4": { + "label": "Source not specified" + }, + "5": { + "label": "Canadian Subject Headings" + }, + "6": { + "label": "Répertoire de vedettes-matière" + }, + "7": { + "label": "Source specified in subfield $2" + } + } + }, + "subfields": { + "a": { + "label": "Meeting name or jurisdiction name as entry element", + "repeatable": false + }, + "c": { + "label": "Location of meeting", + "repeatable": true + }, + "d": { + "label": "Date of meeting", + "repeatable": false + }, + "e": { + "label": "Subordinate unit", + "repeatable": true + }, + "f": { + "label": "Date of a work", + "repeatable": false + }, + "g": { + "label": "Miscellaneous information", + "repeatable": true + }, + "h": { + "label": "Medium", + "repeatable": false + }, + "j": { + "label": "Relator term", + "repeatable": true + }, + "k": { + "label": "Form subheading", + "repeatable": true + }, + "l": { + "label": "Language of a work", + "repeatable": false + }, + "n": { + "label": "Number of part/section/meeting", + "repeatable": true + }, + "p": { + "label": "Name of part/section of a work", + "repeatable": true + }, + "q": { + "label": "Name of meeting following jurisdiction name entry element", + "repeatable": false + }, + "s": { + "label": "Version", + "repeatable": false + }, + "t": { + "label": "Title of a work", + "repeatable": false + }, + "u": { + "label": "Affiliation", + "repeatable": false + }, + "v": { + "label": "Form subdivision", + "repeatable": true + }, + "x": { + "label": "General subdivision", + "repeatable": true + }, + "y": { + "label": "Chronological subdivision", + "repeatable": true + }, + "z": { + "label": "Geographic subdivision", + "repeatable": true + }, + "0": { + "label": "Authority record control number or standard number", + "repeatable": true + }, + "2": { + "label": "Source of heading or term", + "repeatable": false, + "codelist": { + "name": "Subject Heading and Term Source Codes", + "url": "http://www.loc.gov/standards/sourcelist/subject.html" + } + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "4": { + "label": "Relationship", + "repeatable": true, + "codelist": { + "name": "MARC Code List for Relators", + "url": "https://www.loc.gov/marc/relators/relaterm.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "630": { + "tag": "630", + "label": "Subject Added Entry - Uniform Title", + "url": "https://www.loc.gov/marc/bibliographic/bd630.html", + "repeatable": true, + "indicator1": { + "label": "Nonfiling characters", + "codes": { + "0-9": { + "label": "Number of nonfiling characters" + } + } + }, + "indicator2": { + "label": "Thesaurus", + "codes": { + "0": { + "label": "Library of Congress Subject Headings" + }, + "1": { + "label": "LC subject headings for children's literature" + }, + "2": { + "label": "Medical Subject Headings" + }, + "3": { + "label": "National Agricultural Library subject authority file" + }, + "4": { + "label": "Source not specified" + }, + "5": { + "label": "Canadian Subject Headings" + }, + "6": { + "label": "Répertoire de vedettes-matière" + }, + "7": { + "label": "Source specified in subfield $2" + } + } + }, + "subfields": { + "a": { + "label": "Uniform title", + "repeatable": false + }, + "d": { + "label": "Date of treaty signing", + "repeatable": true + }, + "e": { + "label": "Relator term", + "repeatable": true + }, + "f": { + "label": "Date of a work", + "repeatable": false + }, + "g": { + "label": "Miscellaneous information", + "repeatable": true + }, + "h": { + "label": "Medium", + "repeatable": false + }, + "k": { + "label": "Form subheading", + "repeatable": true + }, + "l": { + "label": "Language of a work", + "repeatable": false + }, + "m": { + "label": "Medium of performance for music", + "repeatable": true + }, + "n": { + "label": "Number of part/section of a work", + "repeatable": true + }, + "o": { + "label": "Arranged statement for music", + "repeatable": false + }, + "p": { + "label": "Name of part/section of a work", + "repeatable": true + }, + "r": { + "label": "Key for music", + "repeatable": false + }, + "s": { + "label": "Version", + "repeatable": false + }, + "t": { + "label": "Title of a work", + "repeatable": false + }, + "v": { + "label": "Form subdivision", + "repeatable": true + }, + "x": { + "label": "General subdivision", + "repeatable": true + }, + "y": { + "label": "Chronological subdivision", + "repeatable": true + }, + "z": { + "label": "Geographic subdivision", + "repeatable": true + }, + "0": { + "label": "Authority record control number or standard number", + "repeatable": true + }, + "2": { + "label": "Source of heading or term", + "repeatable": false, + "codelist": { + "name": "Subject Heading and Term Source Codes", + "url": "http://www.loc.gov/standards/sourcelist/subject.html" + } + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "4": { + "label": "Relationship", + "repeatable": true, + "codelist": { + "name": "MARC Code List for Relators", + "url": "https://www.loc.gov/marc/relators/relaterm.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "647": { + "tag": "647", + "label": "Subject Added Entry - Named Event", + "url": "https://www.loc.gov/marc/bibliographic/bd647.html", + "repeatable": true, + "indicator1": null, + "indicator2": { + "label": "Thesaurus", + "codes": { + "0": { + "label": "Library of Congress Subject Headings" + }, + "1": { + "label": "LC subject headings for children's literature" + }, + "2": { + "label": "Medical Subject Headings" + }, + "3": { + "label": "National Agricultural Library subject authority file" + }, + "4": { + "label": "Source not specified" + }, + "5": { + "label": "Canadian Subject Headings" + }, + "6": { + "label": "Répertoire de vedettes-matière" + }, + "7": { + "label": "Source specified in subfield $2" + } + } + }, + "subfields": { + "a": { + "label": "Named event", + "repeatable": false + }, + "c": { + "label": "Location of named event", + "repeatable": true + }, + "d": { + "label": "Date of named event", + "repeatable": false + }, + "g": { + "label": "Miscellaneous information", + "repeatable": true + }, + "v": { + "label": "Form subdivision", + "repeatable": true + }, + "x": { + "label": "General subdivision", + "repeatable": true + }, + "y": { + "label": "Chronological subdivision", + "repeatable": true + }, + "z": { + "label": "Geographic subdivision", + "repeatable": true + }, + "0": { + "label": "Authority record control number or standard number", + "repeatable": true + }, + "2": { + "label": "Source of heading or term", + "repeatable": false + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "648": { + "tag": "648", + "label": "Subject Added Entry - Chronological Term", + "url": "https://www.loc.gov/marc/bibliographic/bd648.html", + "repeatable": true, + "indicator1": null, + "indicator2": { + "label": "Thesaurus", + "codes": { + "0": { + "label": "Library of Congress Subject Headings" + }, + "1": { + "label": "LC subject headings for children's literature" + }, + "2": { + "label": "Medical Subject Headings" + }, + "3": { + "label": "National Agricultural Library subject authority file" + }, + "4": { + "label": "Source not specified" + }, + "5": { + "label": "Canadian Subject Headings" + }, + "6": { + "label": "Répertoire de vedettes-matière" + }, + "7": { + "label": "Source specified in subfield $2" + } + } + }, + "subfields": { + "a": { + "label": "Chronological term", + "repeatable": false + }, + "v": { + "label": "Form subdivision", + "repeatable": true + }, + "x": { + "label": "General subdivision", + "repeatable": true + }, + "y": { + "label": "Chronological subdivision", + "repeatable": true + }, + "z": { + "label": "Geographic subdivision", + "repeatable": true + }, + "0": { + "label": "Authority record control number or standard number", + "repeatable": true + }, + "2": { + "label": "Source of heading or term", + "repeatable": false, + "codelist": { + "name": "Subject Heading and Term Source Codes", + "url": "http://www.loc.gov/standards/sourcelist/subject.html" + } + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "650": { + "tag": "650", + "label": "Subject Added Entry - Topical Term", + "url": "https://www.loc.gov/marc/bibliographic/bd650.html", + "repeatable": true, + "indicator1": { + "label": "Level of subject", + "codes": { + " ": { + "label": "No information provided" + }, + "0": { + "label": "No level specified" + }, + "1": { + "label": "Primary" + }, + "2": { + "label": "Secondary" + } + } + }, + "indicator2": { + "label": "Thesaurus", + "codes": { + "0": { + "label": "Library of Congress Subject Headings" + }, + "1": { + "label": "LC subject headings for children's literature" + }, + "2": { + "label": "Medical Subject Headings" + }, + "3": { + "label": "National Agricultural Library subject authority file" + }, + "4": { + "label": "Source not specified" + }, + "5": { + "label": "Canadian Subject Headings" + }, + "6": { + "label": "Répertoire de vedettes-matière" + }, + "7": { + "label": "Source specified in subfield $2" + } + } + }, + "subfields": { + "a": { + "label": "Topical term or geographic name entry element", + "repeatable": false + }, + "b": { + "label": "Topical term following geographic name entry element", + "repeatable": false + }, + "c": { + "label": "Location of event", + "repeatable": false + }, + "d": { + "label": "Active dates", + "repeatable": false + }, + "e": { + "label": "Relator term", + "repeatable": true + }, + "g": { + "label": "Miscellaneous information", + "repeatable": true + }, + "4": { + "label": "Relationship", + "repeatable": true, + "codelist": { + "name": "MARC Code List for Relators", + "url": "https://www.loc.gov/marc/relators/relaterm.html" + } + }, + "v": { + "label": "Form subdivision", + "repeatable": true + }, + "x": { + "label": "General subdivision", + "repeatable": true + }, + "y": { + "label": "Chronological subdivision", + "repeatable": true + }, + "z": { + "label": "Geographic subdivision", + "repeatable": true + }, + "0": { + "label": "Authority record control number or standard number", + "repeatable": true + }, + "2": { + "label": "Source of heading or term", + "repeatable": false, + "codelist": { + "name": "Subject Heading and Term Source Codes", + "url": "http://www.loc.gov/standards/sourcelist/subject.html" + } + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + }, + "historical-subfields": { + "b": { + "label": "Topical term following geographic name as entry element [OBSOLETE, 1981]" + } + } + }, + "651": { + "tag": "651", + "label": "Subject Added Entry - Geographic Name", + "url": "https://www.loc.gov/marc/bibliographic/bd651.html", + "repeatable": true, + "indicator1": null, + "indicator2": { + "label": "Thesaurus", + "codes": { + "0": { + "label": "Library of Congress Subject Headings" + }, + "1": { + "label": "LC subject headings for children's literature" + }, + "2": { + "label": "Medical Subject Headings" + }, + "3": { + "label": "National Agricultural Library subject authority file" + }, + "4": { + "label": "Source not specified" + }, + "5": { + "label": "Canadian Subject Headings" + }, + "6": { + "label": "Répertoire de vedettes-matière" + }, + "7": { + "label": "Source specified in subfield $2" + } + } + }, + "subfields": { + "a": { + "label": "Geographic name", + "repeatable": false + }, + "e": { + "label": "Relator term", + "repeatable": true + }, + "g": { + "label": "Miscellaneous information", + "repeatable": true + }, + "4": { + "label": "Relationship", + "repeatable": true, + "codelist": { + "name": "MARC Code List for Relators", + "url": "https://www.loc.gov/marc/relators/relaterm.html" + } + }, + "v": { + "label": "Form subdivision", + "repeatable": true + }, + "x": { + "label": "General subdivision", + "repeatable": true + }, + "y": { + "label": "Chronological subdivision", + "repeatable": true + }, + "z": { + "label": "Geographic subdivision", + "repeatable": true + }, + "0": { + "label": "Authority record control number or standard number", + "repeatable": true + }, + "2": { + "label": "Source of heading or term", + "repeatable": false, + "codelist": { + "name": "Subject Heading and Term Source Codes", + "url": "http://www.loc.gov/standards/sourcelist/subject.html" + } + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + }, + "historical-subfields": { + "b": { + "label": "Geographic name following place entry element [OBSOLETE, 1981]" + } + } + }, + "653": { + "tag": "653", + "label": "Index Term - Uncontrolled", + "url": "https://www.loc.gov/marc/bibliographic/bd653.html", + "repeatable": true, + "indicator1": { + "label": "Level of index term", + "codes": { + " ": { + "label": "No information provided" + }, + "0": { + "label": "No level specified" + }, + "1": { + "label": "Primary" + }, + "2": { + "label": "Secondary" + } + } + }, + "indicator2": { + "label": "Type of term or name", + "codes": { + " ": { + "label": "No information provided" + }, + "0": { + "label": "Topical term" + }, + "1": { + "label": "Personal name" + }, + "2": { + "label": "Corporate name" + }, + "3": { + "label": "Meeting name" + }, + "4": { + "label": "Chronological term" + }, + "5": { + "label": "Geographic name" + }, + "6": { + "label": "Genre/form term" + } + } + }, + "subfields": { + "a": { + "label": "Uncontrolled term", + "repeatable": true + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "654": { + "tag": "654", + "label": "Subject Added Entry - Faceted Topical Terms", + "url": "https://www.loc.gov/marc/bibliographic/bd654.html", + "repeatable": true, + "indicator1": { + "label": "Level of subject", + "codes": { + " ": { + "label": "No information provided" + }, + "0": { + "label": "No level specified" + }, + "1": { + "label": "Primary" + }, + "2": { + "label": "Secondary" + } + } + }, + "indicator2": null, + "subfields": { + "a": { + "label": "Focus term", + "repeatable": true + }, + "b": { + "label": "Non-focus term", + "repeatable": true + }, + "c": { + "label": "Facet/hierarchy designation", + "repeatable": true + }, + "e": { + "label": "Relator term", + "repeatable": true + }, + "v": { + "label": "Form subdivision", + "repeatable": true + }, + "y": { + "label": "Chronological subdivision", + "repeatable": true + }, + "z": { + "label": "Geographic subdivision", + "repeatable": true + }, + "0": { + "label": "Authority record control number or standard number", + "repeatable": true + }, + "2": { + "label": "Source of heading or term", + "repeatable": false, + "codelist": { + "name": "Subject Heading and Term Source Codes", + "url": "http://www.loc.gov/standards/sourcelist/subject.html" + } + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "4": { + "label": "Relationship", + "repeatable": true, + "codelist": { + "name": "MARC Code List for Relators", + "url": "https://www.loc.gov/marc/relators/relaterm.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "655": { + "tag": "655", + "label": "Index Term - Genre/Form", + "url": "https://www.loc.gov/marc/bibliographic/bd655.html", + "repeatable": true, + "indicator1": { + "label": "Type of heading", + "codes": { + " ": { + "label": "Basic" + }, + "0": { + "label": "Faceted" + } + } + }, + "indicator2": { + "label": "Thesaurus", + "codes": { + "0": { + "label": "Library of Congress Subject Headings" + }, + "1": { + "label": "LC subject headings for children's literature" + }, + "2": { + "label": "Medical Subject Headings" + }, + "3": { + "label": "National Agricultural Library subject authority file" + }, + "4": { + "label": "Source not specified" + }, + "5": { + "label": "Canadian Subject Headings" + }, + "6": { + "label": "Répertoire de vedettes-matière" + }, + "7": { + "label": "Source specified in subfield $2" + } + } + }, + "subfields": { + "a": { + "label": "Genre/form data or focus term", + "repeatable": false + }, + "b": { + "label": "Non-focus term", + "repeatable": true + }, + "c": { + "label": "Facet/hierarchy designation", + "repeatable": true + }, + "v": { + "label": "Form subdivision", + "repeatable": true + }, + "x": { + "label": "General subdivision", + "repeatable": true + }, + "y": { + "label": "Chronological subdivision", + "repeatable": true + }, + "z": { + "label": "Geographic subdivision", + "repeatable": true + }, + "0": { + "label": "Authority record control number", + "repeatable": true + }, + "2": { + "label": "Source of term", + "repeatable": false, + "codelist": { + "name": "Genre/Form Code and Term Source Codes", + "url": "http://www.loc.gov/standards/sourcelist/genre-form.html" + } + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "5": { + "label": "Institution to which field applies", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "656": { + "tag": "656", + "label": "Index Term - Occupation", + "url": "https://www.loc.gov/marc/bibliographic/bd656.html", + "repeatable": true, + "indicator1": null, + "indicator2": { + "label": "Source of term", + "codes": { + "7": { + "label": "Source specified in subfield $2" + } + } + }, + "subfields": { + "a": { + "label": "Occupation", + "repeatable": false + }, + "k": { + "label": "Form", + "repeatable": false + }, + "v": { + "label": "Form subdivision", + "repeatable": true + }, + "x": { + "label": "General subdivision", + "repeatable": true + }, + "y": { + "label": "Chronological subdivision", + "repeatable": true + }, + "z": { + "label": "Geographic subdivision", + "repeatable": true + }, + "0": { + "label": "Authority record control number", + "repeatable": true + }, + "2": { + "label": "Source of term", + "repeatable": false, + "codelist": { + "name": "Occupation Term Source Codes", + "url": "http://www.loc.gov/standards/sourcelist/occupation.html" + } + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "657": { + "tag": "657", + "label": "Index Term - Function", + "url": "https://www.loc.gov/marc/bibliographic/bd657.html", + "repeatable": true, + "indicator1": null, + "indicator2": { + "label": "Source of term", + "codes": { + "7": { + "label": "Source specified in subfield $2" + } + } + }, + "subfields": { + "a": { + "label": "Function", + "repeatable": false + }, + "v": { + "label": "Form subdivision", + "repeatable": true + }, + "x": { + "label": "General subdivision", + "repeatable": true + }, + "y": { + "label": "Chronological subdivision", + "repeatable": true + }, + "z": { + "label": "Geographic subdivision", + "repeatable": true + }, + "0": { + "label": "Authority record control number", + "repeatable": true + }, + "2": { + "label": "Source of term", + "repeatable": false, + "codelist": { + "name": "Function Term Source Codes", + "url": "http://www.loc.gov/standards/sourcelist/function.html" + } + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "658": { + "tag": "658", + "label": "Index Term - Curriculum Objective", + "url": "https://www.loc.gov/marc/bibliographic/bd658.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Main curriculum objective", + "repeatable": false + }, + "b": { + "label": "Subordinate curriculum objective", + "repeatable": true + }, + "c": { + "label": "Curriculum code", + "repeatable": false + }, + "d": { + "label": "Correlation factor", + "repeatable": false + }, + "2": { + "label": "Source of term or code", + "repeatable": false, + "codelist": { + "name": "Curriculum Objective Term and Code Source Codes", + "url": "http://www.loc.gov/standards/sourcelist/curriculum-objective.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "662": { + "tag": "662", + "label": "Subject Added Entry - Hierarchical Place Name", + "url": "https://www.loc.gov/marc/bibliographic/bd662.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Country or larger entity", + "repeatable": true + }, + "b": { + "label": "First-order political jurisdiction", + "repeatable": false + }, + "c": { + "label": "Intermediate political jurisdiction", + "repeatable": true + }, + "d": { + "label": "City", + "repeatable": false + }, + "e": { + "label": "Relator term", + "repeatable": true + }, + "f": { + "label": "City subsection", + "repeatable": true + }, + "g": { + "label": "Other nonjurisdictional geographic region and feature", + "repeatable": true + }, + "h": { + "label": "Extraterrestrial area", + "repeatable": true + }, + "0": { + "label": "Authority record control number or standard number", + "repeatable": true + }, + "2": { + "label": "Source of heading or term", + "repeatable": false, + "codelist": { + "name": "Subject Heading and Term Source Codes", + "url": "http://www.loc.gov/standards/sourcelist/subject.html" + } + }, + "4": { + "label": "Relationship", + "repeatable": true, + "codelist": { + "name": "MARC Code List for Relators", + "url": "https://www.loc.gov/marc/relators/relaterm.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "700": { + "tag": "700", + "label": "Added Entry - Personal Name", + "url": "https://www.loc.gov/marc/bibliographic/bd700.html", + "repeatable": true, + "indicator1": { + "label": "Type of personal name entry element", + "codes": { + "0": { + "label": "Forename" + }, + "1": { + "label": "Surname" + }, + "3": { + "label": "Family name" + } + } + }, + "indicator2": { + "label": "Type of added entry", + "codes": { + " ": { + "label": "No information provided" + }, + "2": { + "label": "Analytical entry" + } + } + }, + "subfields": { + "a": { + "label": "Personal name", + "repeatable": false + }, + "b": { + "label": "Numeration", + "repeatable": false + }, + "c": { + "label": "Titles and other words associated with a name", + "repeatable": true + }, + "d": { + "label": "Dates associated with a name", + "repeatable": false + }, + "e": { + "label": "Relator term", + "repeatable": true + }, + "f": { + "label": "Date of a work", + "repeatable": false + }, + "g": { + "label": "Miscellaneous information", + "repeatable": true + }, + "h": { + "label": "Medium", + "repeatable": false + }, + "i": { + "label": "Relationship information", + "repeatable": true + }, + "j": { + "label": "Attribution qualifier", + "repeatable": true + }, + "k": { + "label": "Form subheading", + "repeatable": true + }, + "l": { + "label": "Language of a work", + "repeatable": false + }, + "m": { + "label": "Medium of performance for music", + "repeatable": true + }, + "n": { + "label": "Number of part/section of a work", + "repeatable": true + }, + "o": { + "label": "Arranged statement for music", + "repeatable": false + }, + "p": { + "label": "Name of part/section of a work", + "repeatable": true + }, + "q": { + "label": "Fuller form of name", + "repeatable": false + }, + "r": { + "label": "Key for music", + "repeatable": false + }, + "s": { + "label": "Version", + "repeatable": false + }, + "t": { + "label": "Title of a work", + "repeatable": false + }, + "u": { + "label": "Affiliation", + "repeatable": false + }, + "x": { + "label": "International Standard Serial Number", + "repeatable": false + }, + "0": { + "label": "Authority record control number or standard number", + "repeatable": true + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "4": { + "label": "Relationship", + "repeatable": true, + "codelist": { + "name": "MARC Code List for Relators", + "url": "https://www.loc.gov/marc/relators/relaterm.html" + } + }, + "5": { + "label": "Institution to which field applies", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "710": { + "tag": "710", + "label": "Added Entry - Corporate Name", + "url": "https://www.loc.gov/marc/bibliographic/bd710.html", + "repeatable": true, + "indicator1": { + "label": "Type of corporate name entry element", + "codes": { + "0": { + "label": "Inverted name" + }, + "1": { + "label": "Jurisdiction name" + }, + "2": { + "label": "Name in direct order" + } + } + }, + "indicator2": { + "label": "Type of added entry", + "codes": { + " ": { + "label": "No information provided" + }, + "2": { + "label": "Analytical entry" + } + } + }, + "subfields": { + "a": { + "label": "Corporate name or jurisdiction name as entry element", + "repeatable": false + }, + "b": { + "label": "Subordinate unit", + "repeatable": true + }, + "c": { + "label": "Location of meeting", + "repeatable": true + }, + "d": { + "label": "Date of meeting or treaty signing", + "repeatable": true + }, + "e": { + "label": "Relator term", + "repeatable": true + }, + "f": { + "label": "Date of a work", + "repeatable": false + }, + "g": { + "label": "Miscellaneous information", + "repeatable": true + }, + "h": { + "label": "Medium", + "repeatable": false + }, + "i": { + "label": "Relationship information", + "repeatable": true + }, + "k": { + "label": "Form subheading", + "repeatable": true + }, + "l": { + "label": "Language of a work", + "repeatable": false + }, + "m": { + "label": "Medium of performance for music", + "repeatable": true + }, + "n": { + "label": "Number of part/section/meeting", + "repeatable": true + }, + "o": { + "label": "Arranged statement for music", + "repeatable": false + }, + "p": { + "label": "Name of part/section of a work", + "repeatable": true + }, + "r": { + "label": "Key for music", + "repeatable": false + }, + "s": { + "label": "Version", + "repeatable": false + }, + "t": { + "label": "Title of a work", + "repeatable": false + }, + "u": { + "label": "Affiliation", + "repeatable": false + }, + "x": { + "label": "International Standard Serial Number", + "repeatable": false + }, + "0": { + "label": "Authority record control number or standard number", + "repeatable": true + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "4": { + "label": "Relationship", + "repeatable": true, + "codelist": { + "name": "MARC Code List for Relators", + "url": "https://www.loc.gov/marc/relators/relaterm.html" + } + }, + "5": { + "label": "Institution to which field applies", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "711": { + "tag": "711", + "label": "Added Entry - Meeting Name", + "url": "https://www.loc.gov/marc/bibliographic/bd711.html", + "repeatable": true, + "indicator1": { + "label": "Type of meeting name entry element", + "codes": { + "0": { + "label": "Inverted name" + }, + "1": { + "label": "Jurisdiction name" + }, + "2": { + "label": "Name in direct order" + } + } + }, + "indicator2": { + "label": "Type of added entry", + "codes": { + " ": { + "label": "No information provided" + }, + "2": { + "label": "Analytical entry" + } + } + }, + "subfields": { + "a": { + "label": "Meeting name or jurisdiction name as entry element", + "repeatable": false + }, + "c": { + "label": "Location of meeting", + "repeatable": true + }, + "d": { + "label": "Date of meeting", + "repeatable": false + }, + "e": { + "label": "Subordinate unit", + "repeatable": true + }, + "f": { + "label": "Date of a work", + "repeatable": false + }, + "g": { + "label": "Miscellaneous information", + "repeatable": true + }, + "h": { + "label": "Medium", + "repeatable": false + }, + "i": { + "label": "Relationship information", + "repeatable": true + }, + "j": { + "label": "Relator term", + "repeatable": true + }, + "k": { + "label": "Form subheading", + "repeatable": true + }, + "l": { + "label": "Language of a work", + "repeatable": false + }, + "n": { + "label": "Number of part/section/meeting", + "repeatable": true + }, + "p": { + "label": "Name of part/section of a work", + "repeatable": true + }, + "q": { + "label": "Name of meeting following jurisdiction name entry element", + "repeatable": false + }, + "s": { + "label": "Version", + "repeatable": false + }, + "t": { + "label": "Title of a work", + "repeatable": false + }, + "u": { + "label": "Affiliation", + "repeatable": false + }, + "x": { + "label": "International Standard Serial Number", + "repeatable": false + }, + "0": { + "label": "Authority record control number or standard number", + "repeatable": true + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "4": { + "label": "Relationship", + "repeatable": true, + "codelist": { + "name": "MARC Code List for Relators", + "url": "https://www.loc.gov/marc/relators/relaterm.html" + } + }, + "5": { + "label": "Institution to which field applies", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "720": { + "tag": "720", + "label": "Added Entry - Uncontrolled Name", + "url": "https://www.loc.gov/marc/bibliographic/bd720.html", + "repeatable": true, + "indicator1": { + "label": "Type of name", + "codes": { + " ": { + "label": "Not specified" + }, + "1": { + "label": "Personal" + }, + "2": { + "label": "Other" + } + } + }, + "indicator2": null, + "subfields": { + "a": { + "label": "Name", + "repeatable": false + }, + "e": { + "label": "Relator term", + "repeatable": true + }, + "4": { + "label": "Relationship", + "repeatable": true, + "codelist": { + "name": "MARC Code List for Relators", + "url": "https://www.loc.gov/marc/relators/relaterm.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "730": { + "tag": "730", + "label": "Added Entry - Uniform Title", + "url": "https://www.loc.gov/marc/bibliographic/bd730.html", + "repeatable": true, + "indicator1": { + "label": "Nonfiling characters", + "codes": { + "0-9": { + "label": "Number of nonfiling characters" + } + } + }, + "indicator2": { + "label": "Type of added entry", + "codes": { + " ": { + "label": "No information provided" + }, + "2": { + "label": "Analytical entry" + } + } + }, + "subfields": { + "a": { + "label": "Uniform title", + "repeatable": false + }, + "d": { + "label": "Date of treaty signing", + "repeatable": true + }, + "f": { + "label": "Date of a work", + "repeatable": false + }, + "g": { + "label": "Miscellaneous information", + "repeatable": true + }, + "h": { + "label": "Medium", + "repeatable": false + }, + "i": { + "label": "Relationship information", + "repeatable": true + }, + "k": { + "label": "Form subheading", + "repeatable": true + }, + "l": { + "label": "Language of a work", + "repeatable": false + }, + "m": { + "label": "Medium of performance for music", + "repeatable": true + }, + "n": { + "label": "Number of part/section of a work", + "repeatable": true + }, + "o": { + "label": "Arranged statement for music", + "repeatable": false + }, + "p": { + "label": "Name of part/section of a work", + "repeatable": true + }, + "r": { + "label": "Key for music", + "repeatable": false + }, + "s": { + "label": "Version", + "repeatable": false + }, + "t": { + "label": "Title of a work", + "repeatable": false + }, + "x": { + "label": "International Standard Serial Number", + "repeatable": false + }, + "0": { + "label": "Authority record control number or standard number", + "repeatable": true + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "5": { + "label": "Institution to which field applies", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "740": { + "tag": "740", + "label": "Added Entry - Uncontrolled Related/Analytical Title", + "url": "https://www.loc.gov/marc/bibliographic/bd740.html", + "repeatable": true, + "indicator1": { + "label": "Nonfiling characters", + "codes": { + "0": { + "label": "No nonfiling characters" + }, + "1-9": { + "label": "Number of nonfiling characters" + } + }, + "historical-codes": { + " ": { + "label": "Nonfiling characters not specified [OBSOLETE, 1980]" + } + } + }, + "indicator2": { + "label": "Type of added entry", + "codes": { + " ": { + "label": "No information provided" + }, + "2": { + "label": "Analytical entry" + } + }, + "historical-codes": { + "0": { + "label": "Alternative entry (BK, AM, CF, MP, MU) [OBSOLETE, 1993]" + }, + "1": { + "label": "Secondary entry (BK, AM, CF, MP, MU) / Printed on card (VM) [OBSOLETE, 1993]" + }, + "3": { + "label": "Not printed on card [OBSOLETE, 1993]" + } + } + }, + "subfields": { + "a": { + "label": "Uncontrolled related/analytical title", + "repeatable": false + }, + "h": { + "label": "Medium", + "repeatable": false + }, + "n": { + "label": "Number of part/section of a work", + "repeatable": true + }, + "p": { + "label": "Name of part/section of a work", + "repeatable": true + }, + "5": { + "label": "Institution to which field applies", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "751": { + "tag": "751", + "label": "Added Entry - Geographic Name", + "url": "https://www.loc.gov/marc/bibliographic/bd751.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Geographic name", + "repeatable": false + }, + "e": { + "label": "Relator term", + "repeatable": true + }, + "0": { + "label": "Authority record control number or standard number", + "repeatable": true + }, + "2": { + "label": "Source of heading or term", + "repeatable": false, + "codelist": { + "name": "Name and Title Authority Source Codes", + "url": "https://www.loc.gov/standards/sourcelist/name-title.html" + } + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "4": { + "label": "Relationship", + "repeatable": true, + "codelist": { + "name": "MARC Code List for Relators", + "url": "https://www.loc.gov/marc/relators/relaterm.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "752": { + "tag": "752", + "label": "Added Entry - Hierarchical Place Name", + "url": "https://www.loc.gov/marc/bibliographic/bd752.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Country or larger entity", + "repeatable": true + }, + "b": { + "label": "First-order political jurisdiction", + "repeatable": false + }, + "c": { + "label": "Intermediate political jurisdiction", + "repeatable": true + }, + "d": { + "label": "City", + "repeatable": false + }, + "e": { + "label": "Relator term", + "repeatable": true + }, + "f": { + "label": "City subsection", + "repeatable": true + }, + "g": { + "label": "Other nonjurisdictional geographic region and feature", + "repeatable": true + }, + "h": { + "label": "Extraterrestrial area", + "repeatable": true + }, + "0": { + "label": "Authority record control number or standard number", + "repeatable": true + }, + "2": { + "label": "Source of heading or term", + "repeatable": false, + "codelist": { + "name": "Name and Title Authority Source Codes", + "url": "https://www.loc.gov/standards/sourcelist/name-title.html" + } + }, + "4": { + "label": "Relationship", + "repeatable": true, + "codelist": { + "name": "MARC Code List for Relators", + "url": "https://www.loc.gov/marc/relators/relaterm.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "753": { + "tag": "753", + "label": "System Details Access to Computer Files", + "url": "https://www.loc.gov/marc/bibliographic/bd753.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Make and model of machine", + "repeatable": false + }, + "b": { + "label": "Programming language", + "repeatable": false + }, + "c": { + "label": "Operating system", + "repeatable": false + }, + "0": { + "label": "Authority record control number or standard number", + "repeatable": true + }, + "2": { + "label": "Source of term", + "repeatable": false, + "codelist": { + "name": "Subject Heading and Term Source Codes", + "url": "http://www.loc.gov/standards/sourcelist/subject.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "754": { + "tag": "754", + "label": "Added Entry - Taxonomic Identification", + "url": "https://www.loc.gov/marc/bibliographic/bd754.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Taxonomic name", + "repeatable": true + }, + "c": { + "label": "Taxonomic category", + "repeatable": true + }, + "d": { + "label": "Common or alternative name", + "repeatable": true + }, + "x": { + "label": "Non-public note", + "repeatable": true + }, + "z": { + "label": "Public note", + "repeatable": true + }, + "0": { + "label": "Authority record control number", + "repeatable": true + }, + "2": { + "label": "Source of taxonomic identification", + "repeatable": false, + "codelist": { + "name": "Taxonomic Classification Source Codes", + "url": "https://www.loc.gov/standards/sourcelist/taxonomic.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "760": { + "tag": "760", + "label": "Main Series Entry", + "url": "https://www.loc.gov/marc/bibliographic/bd760.html", + "repeatable": true, + "indicator1": { + "label": "Note controller", + "codes": { + "0": { + "label": "Display note" + }, + "1": { + "label": "Do not display note" + } + } + }, + "indicator2": { + "label": "Display constant controller", + "codes": { + " ": { + "label": "Main series" + }, + "8": { + "label": "No display constant generated" + } + } + }, + "subfields": { + "a": { + "label": "Main entry heading", + "repeatable": false + }, + "b": { + "label": "Edition", + "repeatable": false + }, + "c": { + "label": "Qualifying information", + "repeatable": false + }, + "d": { + "label": "Place, publisher, and date of publication", + "repeatable": false + }, + "g": { + "label": "Related parts", + "repeatable": true + }, + "h": { + "label": "Physical description", + "repeatable": false + }, + "i": { + "label": "Relationship information", + "repeatable": true + }, + "m": { + "label": "Material-specific details", + "repeatable": false + }, + "n": { + "label": "Note", + "repeatable": true + }, + "o": { + "label": "Other item identifier", + "repeatable": true + }, + "s": { + "label": "Uniform title", + "repeatable": false + }, + "t": { + "label": "Title", + "repeatable": false + }, + "w": { + "label": "Record control number", + "repeatable": true + }, + "x": { + "label": "International Standard Serial Number", + "repeatable": false + }, + "y": { + "label": "CODEN designation", + "repeatable": false + }, + "4": { + "label": "Relationship", + "repeatable": true, + "codelist": { + "name": "MARC Code List for Relators", + "url": "https://www.loc.gov/marc/relators/relaterm.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "7": { + "label": "Control subfield", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "762": { + "tag": "762", + "label": "Subseries Entry", + "url": "https://www.loc.gov/marc/bibliographic/bd762.html", + "repeatable": true, + "indicator1": { + "label": "Note controller", + "codes": { + "0": { + "label": "Display note" + }, + "1": { + "label": "Do not display note" + } + } + }, + "indicator2": { + "label": "Display constant controller", + "codes": { + " ": { + "label": "Has subseries" + }, + "8": { + "label": "No display constant generated" + } + } + }, + "subfields": { + "a": { + "label": "Main entry heading", + "repeatable": false + }, + "b": { + "label": "Edition", + "repeatable": false + }, + "c": { + "label": "Qualifying information", + "repeatable": false + }, + "d": { + "label": "Place, publisher, and date of publication", + "repeatable": false + }, + "g": { + "label": "Related parts", + "repeatable": true + }, + "h": { + "label": "Physical description", + "repeatable": false + }, + "i": { + "label": "Relationship information", + "repeatable": true + }, + "m": { + "label": "Material-specific details", + "repeatable": false + }, + "n": { + "label": "Note", + "repeatable": true + }, + "o": { + "label": "Other item identifier", + "repeatable": true + }, + "s": { + "label": "Uniform title", + "repeatable": false + }, + "t": { + "label": "Title", + "repeatable": false + }, + "w": { + "label": "Record control number", + "repeatable": true + }, + "x": { + "label": "International Standard Serial Number", + "repeatable": false + }, + "y": { + "label": "CODEN designation", + "repeatable": false + }, + "4": { + "label": "Relationship", + "repeatable": true, + "codelist": { + "name": "MARC Code List for Relators", + "url": "https://www.loc.gov/marc/relators/relaterm.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "7": { + "label": "Control subfield", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "765": { + "tag": "765", + "label": "Original Language Entry", + "url": "https://www.loc.gov/marc/bibliographic/bd765.html", + "repeatable": true, + "indicator1": { + "label": "Note controller", + "codes": { + "0": { + "label": "Display note" + }, + "1": { + "label": "Do not display note" + } + } + }, + "indicator2": { + "label": "Display constant controller", + "codes": { + " ": { + "label": "Translation of" + }, + "8": { + "label": "No display constant generated" + } + } + }, + "subfields": { + "a": { + "label": "Main entry heading", + "repeatable": false + }, + "b": { + "label": "Edition", + "repeatable": false + }, + "c": { + "label": "Qualifying information", + "repeatable": false + }, + "d": { + "label": "Place, publisher, and date of publication", + "repeatable": false + }, + "g": { + "label": "Related parts", + "repeatable": true + }, + "h": { + "label": "Physical description", + "repeatable": false + }, + "i": { + "label": "Relationship information", + "repeatable": true + }, + "k": { + "label": "Series data for related item", + "repeatable": true + }, + "m": { + "label": "Material-specific details", + "repeatable": false + }, + "n": { + "label": "Note", + "repeatable": true + }, + "o": { + "label": "Other item identifier", + "repeatable": true + }, + "r": { + "label": "Report number", + "repeatable": true + }, + "s": { + "label": "Uniform title", + "repeatable": false + }, + "t": { + "label": "Title", + "repeatable": false + }, + "u": { + "label": "Standard Technical Report Number", + "repeatable": false + }, + "w": { + "label": "Record control number", + "repeatable": true + }, + "x": { + "label": "International Standard Serial Number", + "repeatable": false + }, + "y": { + "label": "CODEN designation", + "repeatable": false + }, + "z": { + "label": "International Standard Book Number", + "repeatable": true + }, + "4": { + "label": "Relationship", + "repeatable": true, + "codelist": { + "name": "MARC Code List for Relators", + "url": "https://www.loc.gov/marc/relators/relaterm.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "7": { + "label": "Control subfield", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "767": { + "tag": "767", + "label": "Translation Entry", + "url": "https://www.loc.gov/marc/bibliographic/bd767.html", + "repeatable": true, + "indicator1": { + "label": "Note controller", + "codes": { + "0": { + "label": "Display note" + }, + "1": { + "label": "Do not display note" + } + } + }, + "indicator2": { + "label": "Display constant controller", + "codes": { + " ": { + "label": "Translated as" + }, + "8": { + "label": "No display constant generated" + } + } + }, + "subfields": { + "a": { + "label": "Main entry heading", + "repeatable": false + }, + "b": { + "label": "Edition", + "repeatable": false + }, + "c": { + "label": "Qualifying information", + "repeatable": false + }, + "d": { + "label": "Place, publisher, and date of publication", + "repeatable": false + }, + "g": { + "label": "Related parts", + "repeatable": true + }, + "h": { + "label": "Physical description", + "repeatable": false + }, + "i": { + "label": "Relationship information", + "repeatable": true + }, + "k": { + "label": "Series data for related item", + "repeatable": true + }, + "m": { + "label": "Material-specific details", + "repeatable": false + }, + "n": { + "label": "Note", + "repeatable": true + }, + "o": { + "label": "Other item identifier", + "repeatable": true + }, + "r": { + "label": "Report number", + "repeatable": true + }, + "s": { + "label": "Uniform title", + "repeatable": false + }, + "t": { + "label": "Title", + "repeatable": false + }, + "u": { + "label": "Standard Technical Report Number", + "repeatable": false + }, + "w": { + "label": "Record control number", + "repeatable": true + }, + "x": { + "label": "International Standard Serial Number", + "repeatable": false + }, + "y": { + "label": "CODEN designation", + "repeatable": false + }, + "z": { + "label": "International Standard Book Number", + "repeatable": true + }, + "4": { + "label": "Relationship", + "repeatable": true, + "codelist": { + "name": "MARC Code List for Relators", + "url": "https://www.loc.gov/marc/relators/relaterm.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "7": { + "label": "Control subfield", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "770": { + "tag": "770", + "label": "Supplement/Special Issue Entry", + "url": "https://www.loc.gov/marc/bibliographic/bd770.html", + "repeatable": true, + "indicator1": { + "label": "Note controller", + "codes": { + "0": { + "label": "Display note" + }, + "1": { + "label": "Do not display note" + } + } + }, + "indicator2": { + "label": "Display constant controller", + "codes": { + " ": { + "label": "Has supplement" + }, + "8": { + "label": "No display constant generated" + } + } + }, + "subfields": { + "a": { + "label": "Main entry heading", + "repeatable": false + }, + "b": { + "label": "Edition", + "repeatable": false + }, + "c": { + "label": "Qualifying information", + "repeatable": false + }, + "d": { + "label": "Place, publisher, and date of publication", + "repeatable": false + }, + "g": { + "label": "Related parts", + "repeatable": true + }, + "h": { + "label": "Physical description", + "repeatable": false + }, + "i": { + "label": "Relationship information", + "repeatable": true + }, + "k": { + "label": "Series data for related item", + "repeatable": true + }, + "m": { + "label": "Material-specific details", + "repeatable": false + }, + "n": { + "label": "Note", + "repeatable": true + }, + "o": { + "label": "Other item identifier", + "repeatable": true + }, + "r": { + "label": "Report number", + "repeatable": true + }, + "s": { + "label": "Uniform title", + "repeatable": false + }, + "t": { + "label": "Title", + "repeatable": false + }, + "u": { + "label": "Standard Technical Report Number", + "repeatable": false + }, + "w": { + "label": "Record control number", + "repeatable": true + }, + "x": { + "label": "International Standard Serial Number", + "repeatable": false + }, + "y": { + "label": "CODEN designation", + "repeatable": false + }, + "z": { + "label": "International Standard Book Number", + "repeatable": true + }, + "4": { + "label": "Relationship", + "repeatable": true, + "codelist": { + "name": "MARC Code List for Relators", + "url": "https://www.loc.gov/marc/relators/relaterm.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "7": { + "label": "Control subfield", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "772": { + "tag": "772", + "label": "Supplement Parent Entry", + "url": "https://www.loc.gov/marc/bibliographic/bd772.html", + "repeatable": true, + "indicator1": { + "label": "Note controller", + "codes": { + "0": { + "label": "Display note" + }, + "1": { + "label": "Do not display note" + } + } + }, + "indicator2": { + "label": "Display constant controller", + "codes": { + " ": { + "label": "Supplement to" + }, + "0": { + "label": "Parent" + }, + "8": { + "label": "No display constant generated" + } + }, + "historical-codes": { + "1": { + "label": "Special issue [OBSOLETE] [CAN/MARC only]" + } + } + }, + "subfields": { + "a": { + "label": "Main entry heading", + "repeatable": false + }, + "b": { + "label": "Edition", + "repeatable": false + }, + "c": { + "label": "Qualifying information", + "repeatable": false + }, + "d": { + "label": "Place, publisher, and date of publication", + "repeatable": false + }, + "g": { + "label": "Related parts", + "repeatable": true + }, + "h": { + "label": "Physical description", + "repeatable": false + }, + "i": { + "label": "Relationship information", + "repeatable": true + }, + "k": { + "label": "Series data for related item", + "repeatable": true + }, + "m": { + "label": "Material-specific details", + "repeatable": false + }, + "n": { + "label": "Note", + "repeatable": true + }, + "o": { + "label": "Other item identifier", + "repeatable": true + }, + "r": { + "label": "Report number", + "repeatable": true + }, + "s": { + "label": "Uniform title", + "repeatable": false + }, + "t": { + "label": "Title", + "repeatable": false + }, + "u": { + "label": "Standard Technical Report Number", + "repeatable": false + }, + "w": { + "label": "Record control number", + "repeatable": true + }, + "x": { + "label": "International Standard Serial Number", + "repeatable": false + }, + "y": { + "label": "CODEN designation", + "repeatable": false + }, + "z": { + "label": "International Standard Book Number", + "repeatable": true + }, + "4": { + "label": "Relationship", + "repeatable": true, + "codelist": { + "name": "MARC Code List for Relators", + "url": "https://www.loc.gov/marc/relators/relaterm.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "7": { + "label": "Control subfield", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "773": { + "tag": "773", + "label": "Host Item Entry", + "url": "https://www.loc.gov/marc/bibliographic/bd773.html", + "repeatable": true, + "indicator1": { + "label": "Note controller", + "codes": { + "0": { + "label": "Display note" + }, + "1": { + "label": "Do not display note" + } + } + }, + "indicator2": { + "label": "Display constant controller", + "codes": { + " ": { + "label": "In" + }, + "8": { + "label": "No display constant generated" + } + } + }, + "subfields": { + "a": { + "label": "Main entry heading", + "repeatable": false + }, + "b": { + "label": "Edition", + "repeatable": false + }, + "d": { + "label": "Place, publisher, and date of publication", + "repeatable": false + }, + "g": { + "label": "Related parts", + "repeatable": true + }, + "h": { + "label": "Physical description", + "repeatable": false + }, + "i": { + "label": "Relationship information", + "repeatable": true + }, + "k": { + "label": "Series data for related item", + "repeatable": true + }, + "m": { + "label": "Material-specific details", + "repeatable": false + }, + "n": { + "label": "Note", + "repeatable": true + }, + "o": { + "label": "Other item identifier", + "repeatable": true + }, + "p": { + "label": "Abbreviated title", + "repeatable": false + }, + "q": { + "label": "Enumeration and first page", + "repeatable": false + }, + "r": { + "label": "Report number", + "repeatable": true + }, + "s": { + "label": "Uniform title", + "repeatable": false + }, + "t": { + "label": "Title", + "repeatable": false + }, + "u": { + "label": "Standard Technical Report Number", + "repeatable": false + }, + "w": { + "label": "Record control number", + "repeatable": true + }, + "x": { + "label": "International Standard Serial Number", + "repeatable": false + }, + "y": { + "label": "CODEN designation", + "repeatable": false + }, + "z": { + "label": "International Standard Book Number", + "repeatable": true + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "4": { + "label": "Relationship code", + "repeatable": true, + "codelist": { + "name": "MARC Code List for Relators", + "url": "https://www.loc.gov/marc/relators/relaterm.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "7": { + "label": "Control subfield", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "774": { + "tag": "774", + "label": "Constituent Unit Entry", + "url": "https://www.loc.gov/marc/bibliographic/bd774.html", + "repeatable": true, + "indicator1": { + "label": "Note controller", + "codes": { + "0": { + "label": "Display note" + }, + "1": { + "label": "Do not display note" + } + } + }, + "indicator2": { + "label": "Display constant controller", + "codes": { + " ": { + "label": "Constituent unit" + }, + "8": { + "label": "No display constant generated" + } + }, + "historical-codes": { + "0": { + "label": "Includes [OBSOLETE] [CAN/MARC only]" + } + } + }, + "subfields": { + "a": { + "label": "Main entry heading", + "repeatable": false + }, + "b": { + "label": "Edition", + "repeatable": false + }, + "c": { + "label": "Qualifying information", + "repeatable": false + }, + "d": { + "label": "Place, publisher, and date of publication", + "repeatable": false + }, + "g": { + "label": "Related parts", + "repeatable": true + }, + "h": { + "label": "Physical description", + "repeatable": false + }, + "i": { + "label": "Relationship information", + "repeatable": true + }, + "k": { + "label": "Series data for related item", + "repeatable": true + }, + "m": { + "label": "Material-specific details", + "repeatable": false + }, + "n": { + "label": "Note", + "repeatable": true + }, + "o": { + "label": "Other item identifier", + "repeatable": true + }, + "r": { + "label": "Report number", + "repeatable": true + }, + "s": { + "label": "Uniform title", + "repeatable": false + }, + "t": { + "label": "Title", + "repeatable": false + }, + "u": { + "label": "Standard Technical Report Number", + "repeatable": false + }, + "w": { + "label": "Record control number", + "repeatable": true + }, + "x": { + "label": "International Standard Serial Number", + "repeatable": false + }, + "y": { + "label": "CODEN designation", + "repeatable": false + }, + "z": { + "label": "International Standard Book Number", + "repeatable": true + }, + "4": { + "label": "Relationship", + "repeatable": true, + "codelist": { + "name": "MARC Code List for Relators", + "url": "https://www.loc.gov/marc/relators/relaterm.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "7": { + "label": "Control subfield", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "775": { + "tag": "775", + "label": "Other Edition Entry", + "url": "https://www.loc.gov/marc/bibliographic/bd775.html", + "repeatable": true, + "indicator1": { + "label": "Note controller", + "codes": { + "0": { + "label": "Display note" + }, + "1": { + "label": "Do not display note" + } + } + }, + "indicator2": { + "label": "Display constant controller", + "codes": { + " ": { + "label": "Other edition available" + }, + "8": { + "label": "No display constant generated" + } + }, + "historical-codes": { + "0": { + "label": "Entry not the same" + }, + "1": { + "label": "Entry is the same as title" + }, + "2": { + "label": "Entry is the same as main entry and title" + } + } + }, + "subfields": { + "a": { + "label": "Main entry heading", + "repeatable": false + }, + "b": { + "label": "Edition", + "repeatable": false + }, + "c": { + "label": "Qualifying information", + "repeatable": false + }, + "d": { + "label": "Place, publisher, and date of publication", + "repeatable": false + }, + "e": { + "label": "Language code", + "repeatable": false + }, + "f": { + "label": "Country code", + "repeatable": false + }, + "g": { + "label": "Related parts", + "repeatable": true + }, + "h": { + "label": "Physical description", + "repeatable": false + }, + "i": { + "label": "Relationship information", + "repeatable": true + }, + "k": { + "label": "Series data for related item", + "repeatable": true + }, + "m": { + "label": "Material-specific details", + "repeatable": false + }, + "n": { + "label": "Note", + "repeatable": true + }, + "o": { + "label": "Other item identifier", + "repeatable": true + }, + "r": { + "label": "Report number", + "repeatable": true + }, + "s": { + "label": "Uniform title", + "repeatable": false + }, + "t": { + "label": "Title", + "repeatable": false + }, + "u": { + "label": "Standard Technical Report Number", + "repeatable": false + }, + "w": { + "label": "Record control number", + "repeatable": true + }, + "x": { + "label": "International Standard Serial Number", + "repeatable": false + }, + "y": { + "label": "CODEN designation", + "repeatable": false + }, + "z": { + "label": "International Standard Book Number", + "repeatable": true + }, + "4": { + "label": "Relationship", + "repeatable": true, + "codelist": { + "name": "MARC Code List for Relators", + "url": "https://www.loc.gov/marc/relators/relaterm.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "7": { + "label": "Control subfield", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "776": { + "tag": "776", + "label": "Additional Physical Form Entry", + "url": "https://www.loc.gov/marc/bibliographic/bd776.html", + "repeatable": true, + "indicator1": { + "label": "Note controller", + "codes": { + "0": { + "label": "Display note" + }, + "1": { + "label": "Do not display note" + } + } + }, + "indicator2": { + "label": "Display constant controller", + "codes": { + " ": { + "label": "Available in another form" + }, + "8": { + "label": "No display constant generated" + } + } + }, + "subfields": { + "a": { + "label": "Main entry heading", + "repeatable": false + }, + "b": { + "label": "Edition", + "repeatable": false + }, + "c": { + "label": "Qualifying information", + "repeatable": false + }, + "d": { + "label": "Place, publisher, and date of publication", + "repeatable": false + }, + "g": { + "label": "Related parts", + "repeatable": true + }, + "h": { + "label": "Physical description", + "repeatable": false + }, + "i": { + "label": "Relationship information", + "repeatable": true + }, + "k": { + "label": "Series data for related item", + "repeatable": true + }, + "m": { + "label": "Material-specific details", + "repeatable": false + }, + "n": { + "label": "Note", + "repeatable": true + }, + "o": { + "label": "Other item identifier", + "repeatable": true + }, + "r": { + "label": "Report number", + "repeatable": true + }, + "s": { + "label": "Uniform title", + "repeatable": false + }, + "t": { + "label": "Title", + "repeatable": false + }, + "u": { + "label": "Standard Technical Report Number", + "repeatable": false + }, + "w": { + "label": "Record control number", + "repeatable": true + }, + "x": { + "label": "International Standard Serial Number", + "repeatable": false + }, + "y": { + "label": "CODEN designation", + "repeatable": false + }, + "z": { + "label": "International Standard Book Number", + "repeatable": true + }, + "4": { + "label": "Relationship", + "repeatable": true, + "codelist": { + "name": "MARC Code List for Relators", + "url": "https://www.loc.gov/marc/relators/relaterm.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "7": { + "label": "Control subfield", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "777": { + "tag": "777", + "label": "Issued With Entry", + "url": "https://www.loc.gov/marc/bibliographic/bd777.html", + "repeatable": true, + "indicator1": { + "label": "Note controller", + "codes": { + "0": { + "label": "Display note" + }, + "1": { + "label": "Do not display note" + } + } + }, + "indicator2": { + "label": "Display constant controller", + "codes": { + " ": { + "label": "Issued with" + }, + "8": { + "label": "No display constant generated" + } + }, + "historical-codes": { + "0": { + "label": "Issued with [OBSOLETE] [CAN/MARC only]" + }, + "1": { + "label": "With [OBSOLETE] [CAN/MARC only]" + }, + "2": { + "label": "Bound with [OBSOLETE] [CAN/MARC only]" + } + } + }, + "subfields": { + "a": { + "label": "Main entry heading", + "repeatable": false + }, + "b": { + "label": "Edition", + "repeatable": false + }, + "c": { + "label": "Qualifying information", + "repeatable": false + }, + "d": { + "label": "Place, publisher, and date of publication", + "repeatable": false + }, + "g": { + "label": "Related parts", + "repeatable": true + }, + "h": { + "label": "Physical description", + "repeatable": false + }, + "i": { + "label": "Relationship information", + "repeatable": true + }, + "k": { + "label": "Series data for related item", + "repeatable": true + }, + "m": { + "label": "Material-specific details", + "repeatable": false + }, + "n": { + "label": "Note", + "repeatable": true + }, + "o": { + "label": "Other item identifier", + "repeatable": true + }, + "s": { + "label": "Uniform title", + "repeatable": false + }, + "t": { + "label": "Title", + "repeatable": false + }, + "w": { + "label": "Record control number", + "repeatable": true + }, + "x": { + "label": "International Standard Serial Number", + "repeatable": false + }, + "y": { + "label": "CODEN designation", + "repeatable": false + }, + "4": { + "label": "Relationship", + "repeatable": true, + "codelist": { + "name": "MARC Code List for Relators", + "url": "https://www.loc.gov/marc/relators/relaterm.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "7": { + "label": "Control subfield", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "780": { + "tag": "780", + "label": "Preceding Entry", + "url": "https://www.loc.gov/marc/bibliographic/bd780.html", + "repeatable": true, + "indicator1": { + "label": "Note controller", + "codes": { + "0": { + "label": "Display note" + }, + "1": { + "label": "Do not display note" + } + } + }, + "indicator2": { + "label": "Type of relationship", + "codes": { + "0": { + "label": "Continues" + }, + "1": { + "label": "Continues in part" + }, + "2": { + "label": "Supersedes" + }, + "3": { + "label": "Supersedes in part" + }, + "4": { + "label": "Formed by the union of ... and ..." + }, + "5": { + "label": "Absorbed" + }, + "6": { + "label": "Absorbed in part" + }, + "7": { + "label": "Separated from" + } + } + }, + "subfields": { + "a": { + "label": "Main entry heading", + "repeatable": false + }, + "b": { + "label": "Edition", + "repeatable": false + }, + "c": { + "label": "Qualifying information", + "repeatable": false + }, + "d": { + "label": "Place, publisher, and date of publication", + "repeatable": false + }, + "g": { + "label": "Related parts", + "repeatable": true + }, + "h": { + "label": "Physical description", + "repeatable": false + }, + "i": { + "label": "Relationship information", + "repeatable": true + }, + "k": { + "label": "Series data for related item", + "repeatable": true + }, + "m": { + "label": "Material-specific details", + "repeatable": false + }, + "n": { + "label": "Note", + "repeatable": true + }, + "o": { + "label": "Other item identifier", + "repeatable": true + }, + "r": { + "label": "Report number", + "repeatable": true + }, + "s": { + "label": "Uniform title", + "repeatable": false + }, + "t": { + "label": "Title", + "repeatable": false + }, + "u": { + "label": "Standard Technical Report Number", + "repeatable": false + }, + "w": { + "label": "Record control number", + "repeatable": true + }, + "x": { + "label": "International Standard Serial Number", + "repeatable": false + }, + "y": { + "label": "CODEN designation", + "repeatable": false + }, + "z": { + "label": "International Standard Book Number", + "repeatable": true + }, + "4": { + "label": "Relationship", + "repeatable": true, + "codelist": { + "name": "MARC Code List for Relators", + "url": "https://www.loc.gov/marc/relators/relaterm.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "7": { + "label": "Control subfield", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "785": { + "tag": "785", + "label": "Succeeding Entry", + "url": "https://www.loc.gov/marc/bibliographic/bd785.html", + "repeatable": true, + "indicator1": { + "label": "Note controller", + "codes": { + "0": { + "label": "Display note" + }, + "1": { + "label": "Do not display note" + } + } + }, + "indicator2": { + "label": "Type of relationship", + "codes": { + "0": { + "label": "Continued by" + }, + "1": { + "label": "Continued in part by" + }, + "2": { + "label": "Superseded by" + }, + "3": { + "label": "Superseded in part by" + }, + "4": { + "label": "Absorbed by" + }, + "5": { + "label": "Absorbed in part by" + }, + "6": { + "label": "Split into ... and ..." + }, + "7": { + "label": "Merged with ... to form ..." + }, + "8": { + "label": "Changed back to" + } + } + }, + "subfields": { + "a": { + "label": "Main entry heading", + "repeatable": false + }, + "b": { + "label": "Edition", + "repeatable": false + }, + "c": { + "label": "Qualifying information", + "repeatable": false + }, + "d": { + "label": "Place, publisher, and date of publication", + "repeatable": false + }, + "g": { + "label": "Related parts", + "repeatable": true + }, + "h": { + "label": "Physical description", + "repeatable": false + }, + "i": { + "label": "Relationship information", + "repeatable": true + }, + "k": { + "label": "Series data for related item", + "repeatable": true + }, + "m": { + "label": "Material-specific details", + "repeatable": false + }, + "n": { + "label": "Note", + "repeatable": true + }, + "o": { + "label": "Other item identifier", + "repeatable": true + }, + "r": { + "label": "Report number", + "repeatable": true + }, + "s": { + "label": "Uniform title", + "repeatable": false + }, + "t": { + "label": "Title", + "repeatable": false + }, + "u": { + "label": "Standard Technical Report Number", + "repeatable": false + }, + "w": { + "label": "Record control number", + "repeatable": true + }, + "x": { + "label": "International Standard Serial Number", + "repeatable": false + }, + "y": { + "label": "CODEN designation", + "repeatable": false + }, + "z": { + "label": "International Standard Book Number", + "repeatable": true + }, + "4": { + "label": "Relationship", + "repeatable": true, + "codelist": { + "name": "MARC Code List for Relators", + "url": "https://www.loc.gov/marc/relators/relaterm.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "7": { + "label": "Control subfield", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "786": { + "tag": "786", + "label": "Data Source Entry", + "url": "https://www.loc.gov/marc/bibliographic/bd786.html", + "repeatable": true, + "indicator1": { + "label": "Note controller", + "codes": { + "0": { + "label": "Display note" + }, + "1": { + "label": "Do not display note" + } + } + }, + "indicator2": { + "label": "Display constant controller", + "codes": { + " ": { + "label": "Data source" + }, + "8": { + "label": "No display constant generated" + } + } + }, + "subfields": { + "a": { + "label": "Main entry heading", + "repeatable": false + }, + "b": { + "label": "Edition", + "repeatable": false + }, + "c": { + "label": "Qualifying information", + "repeatable": false + }, + "d": { + "label": "Place, publisher, and date of publication", + "repeatable": false + }, + "g": { + "label": "Related parts", + "repeatable": true + }, + "h": { + "label": "Physical description", + "repeatable": false + }, + "i": { + "label": "Relationship information", + "repeatable": true + }, + "j": { + "label": "Period of content", + "repeatable": false + }, + "k": { + "label": "Series data for related item", + "repeatable": true + }, + "m": { + "label": "Material-specific details", + "repeatable": false + }, + "n": { + "label": "Note", + "repeatable": true + }, + "o": { + "label": "Other item identifier", + "repeatable": true + }, + "p": { + "label": "Abbreviated title", + "repeatable": false + }, + "r": { + "label": "Report number", + "repeatable": true + }, + "s": { + "label": "Uniform title", + "repeatable": false + }, + "t": { + "label": "Title", + "repeatable": false + }, + "u": { + "label": "Standard Technical Report Number", + "repeatable": false + }, + "v": { + "label": "Source Contribution", + "repeatable": false + }, + "w": { + "label": "Record control number", + "repeatable": true + }, + "x": { + "label": "International Standard Serial Number", + "repeatable": false + }, + "y": { + "label": "CODEN designation", + "repeatable": false + }, + "z": { + "label": "International Standard Book Number", + "repeatable": true + }, + "4": { + "label": "Relationship", + "repeatable": true, + "codelist": { + "name": "MARC Code List for Relators", + "url": "https://www.loc.gov/marc/relators/relaterm.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "7": { + "label": "Control subfield", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "787": { + "tag": "787", + "label": "Other Relationship Entry", + "url": "https://www.loc.gov/marc/bibliographic/bd787.html", + "repeatable": true, + "indicator1": { + "label": "Note controller", + "codes": { + "0": { + "label": "Display note" + }, + "1": { + "label": "Do not display note" + } + } + }, + "indicator2": { + "label": "Display constant controller", + "codes": { + " ": { + "label": "Related item" + }, + "8": { + "label": "No display constant generated" + } + } + }, + "subfields": { + "a": { + "label": "Main entry heading", + "repeatable": false + }, + "b": { + "label": "Edition", + "repeatable": false + }, + "c": { + "label": "Qualifying information", + "repeatable": false + }, + "d": { + "label": "Place, publisher, and date of publication", + "repeatable": false + }, + "g": { + "label": "Related parts", + "repeatable": true + }, + "h": { + "label": "Physical description", + "repeatable": false + }, + "i": { + "label": "Relationship information", + "repeatable": true + }, + "k": { + "label": "Series data for related item", + "repeatable": true + }, + "m": { + "label": "Material-specific details", + "repeatable": false + }, + "n": { + "label": "Note", + "repeatable": true + }, + "o": { + "label": "Other item identifier", + "repeatable": true + }, + "r": { + "label": "Report number", + "repeatable": true + }, + "s": { + "label": "Uniform title", + "repeatable": false + }, + "t": { + "label": "Title", + "repeatable": false + }, + "u": { + "label": "Standard Technical Report Number", + "repeatable": false + }, + "w": { + "label": "Record control number", + "repeatable": true + }, + "x": { + "label": "International Standard Serial Number", + "repeatable": false + }, + "y": { + "label": "CODEN designation", + "repeatable": false + }, + "z": { + "label": "International Standard Book Number", + "repeatable": true + }, + "4": { + "label": "Relationship", + "repeatable": true, + "codelist": { + "name": "MARC Code List for Relators", + "url": "https://www.loc.gov/marc/relators/relaterm.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "7": { + "label": "Control subfield", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "800": { + "tag": "800", + "label": "Series Added Entry - Personal Name", + "url": "https://www.loc.gov/marc/bibliographic/bd800.html", + "repeatable": true, + "indicator1": { + "label": "Type of personal name entry element", + "codes": { + "0": { + "label": "Forename" + }, + "1": { + "label": "Surname" + }, + "3": { + "label": "Family name" + } + } + }, + "indicator2": null, + "subfields": { + "a": { + "label": "Personal name", + "repeatable": false + }, + "b": { + "label": "Numeration", + "repeatable": false + }, + "c": { + "label": "Titles and other words associated with a name", + "repeatable": true + }, + "d": { + "label": "Dates associated with a name", + "repeatable": false + }, + "e": { + "label": "Relator term", + "repeatable": true + }, + "f": { + "label": "Date of a work", + "repeatable": false + }, + "g": { + "label": "Miscellaneous information", + "repeatable": true + }, + "h": { + "label": "Medium", + "repeatable": false + }, + "j": { + "label": "Attribution qualifier", + "repeatable": true + }, + "k": { + "label": "Form subheading", + "repeatable": true + }, + "l": { + "label": "Language of a work", + "repeatable": false + }, + "m": { + "label": "Medium of performance for music", + "repeatable": true + }, + "n": { + "label": "Number of part/section of a work", + "repeatable": true + }, + "o": { + "label": "Arranged statement for music", + "repeatable": false + }, + "p": { + "label": "Name of part/section of a work", + "repeatable": true + }, + "q": { + "label": "Fuller form of name", + "repeatable": false + }, + "r": { + "label": "Key for music", + "repeatable": false + }, + "s": { + "label": "Version", + "repeatable": false + }, + "t": { + "label": "Title of a work", + "repeatable": false + }, + "u": { + "label": "Affiliation", + "repeatable": false + }, + "v": { + "label": "Volume/sequential designation", + "repeatable": false + }, + "w": { + "label": "Bibliographic record control number", + "repeatable": true + }, + "x": { + "label": "International Standard Serial Number", + "repeatable": false + }, + "0": { + "label": "Authority record control number or standard number", + "repeatable": true + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "4": { + "label": "Relationship", + "repeatable": true, + "codelist": { + "name": "MARC Code List for Relators", + "url": "https://www.loc.gov/marc/relators/relaterm.html" + } + }, + "5": { + "label": "Institution to which field applies", + "repeatable": true + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "7": { + "label": "Control subfield", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "810": { + "tag": "810", + "label": "Series Added Entry - Corporate Name", + "url": "https://www.loc.gov/marc/bibliographic/bd810.html", + "repeatable": true, + "indicator1": { + "label": "Type of corporate name entry element", + "codes": { + "0": { + "label": "Inverted name" + }, + "1": { + "label": "Jurisdiction name" + }, + "2": { + "label": "Name in direct order" + } + } + }, + "indicator2": null, + "subfields": { + "a": { + "label": "Corporate name or jurisdiction name as entry element", + "repeatable": false + }, + "b": { + "label": "Subordinate unit", + "repeatable": true + }, + "c": { + "label": "Location of meeting", + "repeatable": true + }, + "d": { + "label": "Date of meeting or treaty signing", + "repeatable": true + }, + "e": { + "label": "Relator term", + "repeatable": true + }, + "f": { + "label": "Date of a work", + "repeatable": false + }, + "g": { + "label": "Miscellaneous information", + "repeatable": true + }, + "h": { + "label": "Medium", + "repeatable": false + }, + "k": { + "label": "Form subheading", + "repeatable": true + }, + "l": { + "label": "Language of a work", + "repeatable": false + }, + "m": { + "label": "Medium of performance for music", + "repeatable": true + }, + "n": { + "label": "Number of part/section/meeting", + "repeatable": true + }, + "o": { + "label": "Arranged statement for music", + "repeatable": false + }, + "p": { + "label": "Name of part/section of a work", + "repeatable": true + }, + "r": { + "label": "Key for music", + "repeatable": false + }, + "s": { + "label": "Version", + "repeatable": false + }, + "t": { + "label": "Title of a work", + "repeatable": false + }, + "u": { + "label": "Affiliation", + "repeatable": false + }, + "v": { + "label": "Volume/sequential designation", + "repeatable": false + }, + "w": { + "label": "Bibliographic record control number", + "repeatable": true + }, + "x": { + "label": "International Standard Serial Number", + "repeatable": false + }, + "0": { + "label": "Authority record control number or standard number", + "repeatable": true + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "4": { + "label": "Relationship", + "repeatable": true, + "codelist": { + "name": "MARC Code List for Relators", + "url": "https://www.loc.gov/marc/relators/relaterm.html" + } + }, + "5": { + "label": "Institution to which field applies", + "repeatable": true + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "7": { + "label": "Control subfield", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "811": { + "tag": "811", + "label": "Series Added Entry - Meeting Name", + "url": "https://www.loc.gov/marc/bibliographic/bd811.html", + "repeatable": true, + "indicator1": { + "label": "Type of meeting name entry element", + "codes": { + "0": { + "label": "Inverted name" + }, + "1": { + "label": "Jurisdiction name" + }, + "2": { + "label": "Name in direct order" + } + } + }, + "indicator2": null, + "subfields": { + "a": { + "label": "Meeting name or jurisdiction name as entry element", + "repeatable": false + }, + "c": { + "label": "Location of meeting", + "repeatable": true + }, + "d": { + "label": "Date of meeting", + "repeatable": false + }, + "e": { + "label": "Subordinate unit", + "repeatable": true + }, + "f": { + "label": "Date of a work", + "repeatable": false + }, + "g": { + "label": "Miscellaneous information", + "repeatable": true + }, + "h": { + "label": "Medium", + "repeatable": false + }, + "j": { + "label": "Relator term", + "repeatable": true + }, + "k": { + "label": "Form subheading", + "repeatable": true + }, + "l": { + "label": "Language of a work", + "repeatable": false + }, + "n": { + "label": "Number of part/section/meeting", + "repeatable": true + }, + "p": { + "label": "Name of part/section of a work", + "repeatable": true + }, + "q": { + "label": "Name of meeting following jurisdiction name entry element", + "repeatable": false + }, + "s": { + "label": "Version", + "repeatable": false + }, + "t": { + "label": "Title of a work", + "repeatable": false + }, + "u": { + "label": "Affiliation", + "repeatable": false + }, + "v": { + "label": "Volume/sequential designation", + "repeatable": false + }, + "w": { + "label": "Bibliographic record control number", + "repeatable": true + }, + "x": { + "label": "International Standard Serial Number", + "repeatable": false + }, + "0": { + "label": "Authority record control number or standard number", + "repeatable": true + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "4": { + "label": "Relationship", + "repeatable": true, + "codelist": { + "name": "MARC Code List for Relators", + "url": "https://www.loc.gov/marc/relators/relaterm.html" + } + }, + "5": { + "label": "Institution to which field applies", + "repeatable": true + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "7": { + "label": "Control subfield", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "830": { + "tag": "830", + "label": "Series Added Entry - Uniform Title", + "url": "https://www.loc.gov/marc/bibliographic/bd830.html", + "repeatable": true, + "indicator1": null, + "indicator2": { + "label": "Nonfiling characters", + "codes": { + "0": { + "label": "No nonfiling characters" + }, + "1-9": { + "label": "Number of nonfiling characters" + } + } + }, + "subfields": { + "a": { + "label": "Uniform title", + "repeatable": false + }, + "d": { + "label": "Date of treaty signing", + "repeatable": true + }, + "f": { + "label": "Date of a work", + "repeatable": false + }, + "g": { + "label": "Miscellaneous information", + "repeatable": true + }, + "h": { + "label": "Medium", + "repeatable": false + }, + "k": { + "label": "Form subheading", + "repeatable": true + }, + "l": { + "label": "Language of a work", + "repeatable": false + }, + "m": { + "label": "Medium of performance for music", + "repeatable": true + }, + "n": { + "label": "Number of part/section of a work", + "repeatable": true + }, + "o": { + "label": "Arranged statement for music", + "repeatable": false + }, + "p": { + "label": "Name of part/section of a work", + "repeatable": true + }, + "r": { + "label": "Key for music", + "repeatable": false + }, + "s": { + "label": "Version", + "repeatable": false + }, + "t": { + "label": "Title of a work", + "repeatable": false + }, + "v": { + "label": "Volume/sequential designation", + "repeatable": false + }, + "w": { + "label": "Bibliographic record control number", + "repeatable": true + }, + "x": { + "label": "International Standard Serial Number", + "repeatable": false + }, + "0": { + "label": "Authority record control number or standard number", + "repeatable": true + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "5": { + "label": "Institution to which field applies", + "repeatable": true + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "7": { + "label": "Control subfield", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "850": { + "tag": "850", + "label": "Holding Institution", + "url": "https://www.loc.gov/marc/bibliographic/bd850.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Holding institution", + "repeatable": true, + "codelist": { + "name": "MARC Organization Codes", + "url": "http://www.loc.gov/marc/organizations/orgshome.html" + } + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + }, + "historical-subfields": { + "b": { + "label": "Holdings (MU, VM, SE) [OBSOLETE, 1990]" + }, + "d": { + "label": "Inclusive dates (MU, VM, SE) [OBSOLETE, 1990]" + }, + "e": { + "label": "Retention statement (CF, MU, VM, SE) [OBSOLETE, 1990]" + } + } + }, + "852": { + "tag": "852", + "label": "Location", + "url": "https://www.loc.gov/marc/bibliographic/bd852.html", + "repeatable": true, + "indicator1": { + "label": "Shelving scheme", + "codes": { + " ": { + "label": "No information provided" + }, + "0": { + "label": "Library of Congress classification" + }, + "1": { + "label": "Dewey Decimal classification" + }, + "2": { + "label": "National Library of Medicine classification" + }, + "3": { + "label": "Superintendent of Documents classification" + }, + "4": { + "label": "Shelving control number" + }, + "5": { + "label": "Title" + }, + "6": { + "label": "Shelved separately" + }, + "7": { + "label": "Source specified in subfield $2" + }, + "8": { + "label": "Other scheme" + } + } + }, + "indicator2": { + "label": "Shelving order", + "codes": { + " ": { + "label": "No information provided" + }, + "0": { + "label": "Not enumeration" + }, + "1": { + "label": "Primary enumeration" + }, + "2": { + "label": "Alternative enumeration" + } + } + }, + "subfields": { + "a": { + "label": "Location", + "repeatable": false, + "codelist": { + "name": "MARC Organization Codes", + "url": "http://www.loc.gov/marc/organizations/orgshome.html" + } + }, + "b": { + "label": "Sublocation or collection", + "repeatable": true + }, + "c": { + "label": "Shelving location", + "repeatable": true + }, + "d": { + "label": "Former shelving location", + "repeatable": true + }, + "e": { + "label": "Address", + "repeatable": true + }, + "f": { + "label": "Coded location qualifier", + "repeatable": true + }, + "g": { + "label": "Non-coded location qualifier", + "repeatable": true + }, + "h": { + "label": "Classification part", + "repeatable": false + }, + "i": { + "label": "Item part", + "repeatable": true + }, + "j": { + "label": "Shelving control number", + "repeatable": false + }, + "k": { + "label": "Call number prefix", + "repeatable": true + }, + "l": { + "label": "Shelving form of title", + "repeatable": false + }, + "m": { + "label": "Call number suffix", + "repeatable": true + }, + "n": { + "label": "Country code", + "repeatable": false, + "codelist": { + "name": "MARC Code List for Countries", + "url": "http://www.loc.gov/marc/countries/countries_code.html" + } + }, + "p": { + "label": "Piece designation", + "repeatable": false + }, + "q": { + "label": "Piece physical condition", + "repeatable": false + }, + "s": { + "label": "Copyright article-fee code", + "repeatable": true + }, + "t": { + "label": "Copy number", + "repeatable": false + }, + "u": { + "label": "Uniform Resource Identifier", + "repeatable": true + }, + "x": { + "label": "Nonpublic note", + "repeatable": true + }, + "z": { + "label": "Public note", + "repeatable": true + }, + "2": { + "label": "Source of classification or shelving scheme", + "repeatable": false, + "codelist": { + "name": "Classification Scheme Source Codes", + "url": "http://www.loc.gov/standards/sourcelist/classification.html" + } + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "856": { + "tag": "856", + "label": "Electronic Location and Access", + "url": "https://www.loc.gov/marc/bibliographic/bd856.html", + "repeatable": true, + "indicator1": { + "label": "Access method", + "codes": { + " ": { + "label": "No information provided" + }, + "0": { + "label": "Email" + }, + "1": { + "label": "FTP" + }, + "2": { + "label": "Remote login (Telnet)" + }, + "3": { + "label": "Dial-up" + }, + "4": { + "label": "HTTP" + }, + "7": { + "label": "Method specified in subfield $2" + } + } + }, + "indicator2": { + "label": "Relationship", + "codes": { + " ": { + "label": "No information provided" + }, + "0": { + "label": "Resource" + }, + "1": { + "label": "Version of resource" + }, + "2": { + "label": "Related resource" + }, + "8": { + "label": "No display constant generated" + } + } + }, + "subfields": { + "a": { + "label": "Host name", + "repeatable": true + }, + "b": { + "label": "Access number", + "repeatable": true + }, + "c": { + "label": "Compression information", + "repeatable": true + }, + "d": { + "label": "Path", + "repeatable": true + }, + "f": { + "label": "Electronic name", + "repeatable": true + }, + "h": { + "label": "Processor of request", + "repeatable": false + }, + "i": { + "label": "Instruction", + "repeatable": true + }, + "j": { + "label": "Bits per second", + "repeatable": false + }, + "k": { + "label": "Password", + "repeatable": false + }, + "l": { + "label": "Logon", + "repeatable": false + }, + "m": { + "label": "Contact for access assistance", + "repeatable": true + }, + "n": { + "label": "Name of location of host", + "repeatable": false + }, + "o": { + "label": "Operating system", + "repeatable": false + }, + "p": { + "label": "Port", + "repeatable": false + }, + "q": { + "label": "Electronic format type", + "repeatable": false + }, + "r": { + "label": "Settings", + "repeatable": false + }, + "s": { + "label": "File size", + "repeatable": true + }, + "t": { + "label": "Terminal emulation", + "repeatable": true + }, + "u": { + "label": "Uniform Resource Identifier", + "repeatable": true + }, + "v": { + "label": "Hours access method available", + "repeatable": true + }, + "w": { + "label": "Record control number", + "repeatable": true, + "codelist": { + "name": "MARC Organization Codes", + "url": "http://www.loc.gov/marc/organizations/orgshome.html" + } + }, + "x": { + "label": "Nonpublic note", + "repeatable": true + }, + "y": { + "label": "Link text", + "repeatable": true + }, + "z": { + "label": "Public note", + "repeatable": true + }, + "2": { + "label": "Access method", + "repeatable": false, + "codelist": { + "name": "Electronic Access Methods Code List", + "url": "http://www.loc.gov/standards/valuelist/electronaccess.html" + } + }, + "3": { + "label": "Materials specified", + "repeatable": false + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + }, + "historical-subfields": { + "g": { + "label": "Uniform Resource Name [OBSOLETE, 2000]" + } + } + }, + "880": { + "tag": "880", + "label": "Alternate Graphic Representation", + "url": "https://www.loc.gov/marc/bibliographic/bd880.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "6": { + "label": "Linkage", + "repeatable": false + }, + "a": { + "label": "Same as associated field", + "repeatable": true + }, + "b": { + "label": "Same as associated field", + "repeatable": true + }, + "c": { + "label": "Same as associated field", + "repeatable": true + }, + "d": { + "label": "Same as associated field", + "repeatable": true + }, + "e": { + "label": "Same as associated field", + "repeatable": true + }, + "f": { + "label": "Same as associated field", + "repeatable": true + }, + "g": { + "label": "Same as associated field", + "repeatable": true + }, + "h": { + "label": "Same as associated field", + "repeatable": true + }, + "i": { + "label": "Same as associated field", + "repeatable": true + }, + "j": { + "label": "Same as associated field", + "repeatable": true + }, + "k": { + "label": "Same as associated field", + "repeatable": true + }, + "l": { + "label": "Same as associated field", + "repeatable": true + }, + "m": { + "label": "Same as associated field", + "repeatable": true + }, + "n": { + "label": "Same as associated field", + "repeatable": true + }, + "o": { + "label": "Same as associated field", + "repeatable": true + }, + "p": { + "label": "Same as associated field", + "repeatable": true + }, + "q": { + "label": "Same as associated field", + "repeatable": true + }, + "r": { + "label": "Same as associated field", + "repeatable": true + }, + "s": { + "label": "Same as associated field", + "repeatable": true + }, + "t": { + "label": "Same as associated field", + "repeatable": true + }, + "u": { + "label": "Same as associated field", + "repeatable": true + }, + "v": { + "label": "Same as associated field", + "repeatable": true + }, + "w": { + "label": "Same as associated field", + "repeatable": true + }, + "x": { + "label": "Same as associated field", + "repeatable": true + }, + "y": { + "label": "Same as associated field", + "repeatable": true + }, + "z": { + "label": "Same as associated field", + "repeatable": true + }, + "0": { + "label": "Same as associated field", + "repeatable": true + }, + "1": { + "label": "Same as associated field", + "repeatable": true + }, + "2": { + "label": "Same as associated field", + "repeatable": true + }, + "3": { + "label": "Same as associated field", + "repeatable": true + }, + "4": { + "label": "Same as associated field", + "repeatable": true + }, + "5": { + "label": "Same as associated field", + "repeatable": true + }, + "7": { + "label": "Same as associated field", + "repeatable": true + }, + "8": { + "label": "Same as associated field", + "repeatable": true + }, + "9": { + "label": "Same as associated field", + "repeatable": true + } + } + }, + "882": { + "tag": "882", + "label": "Replacement Record Information", + "url": "https://www.loc.gov/marc/bibliographic/bd882.html", + "repeatable": false, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Replacement title", + "repeatable": true + }, + "i": { + "label": "Explanatory text", + "repeatable": true + }, + "w": { + "label": "Replacement bibliographic record control number", + "repeatable": true, + "codelist": { + "name": "MARC Organization Codes", + "url": "http://www.loc.gov/marc/organizations/orgshome.html" + } + }, + "6": { + "label": "Linkage", + "repeatable": false + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "883": { + "tag": "883", + "label": "Machine-generated Metadata Provenance", + "url": "https://www.loc.gov/marc/bibliographic/bd883.html", + "repeatable": true, + "indicator1": { + "label": "Method of machine assignment", + "codes": { + " ": { + "label": "No information provided/not applicable" + }, + "0": { + "label": "Fully machine-generated" + }, + "1": { + "label": "Partially machine-generated" + } + } + }, + "indicator2": null, + "subfields": { + "a": { + "label": "Generation process", + "repeatable": false + }, + "c": { + "label": "Confidence value", + "repeatable": false + }, + "d": { + "label": "Generation date", + "repeatable": false + }, + "q": { + "label": "Generation agency", + "repeatable": false, + "codelist": { + "name": "MARC Organization Codes", + "url": "http://www.loc.gov/marc/organizations/orgshome.html" + } + }, + "x": { + "label": "Validity end date", + "repeatable": false + }, + "u": { + "label": "Uniform Resource Identifier", + "repeatable": false + }, + "w": { + "label": "Bibliographic record control number", + "repeatable": true + }, + "0": { + "label": "Authority record control number or standard number", + "repeatable": true + }, + "8": { + "label": "Field link and sequence number", + "repeatable": true + } + } + }, + "884": { + "tag": "884", + "label": "Description Conversion Information", + "url": "https://www.loc.gov/marc/bibliographic/bd884.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Conversion process", + "repeatable": false + }, + "g": { + "label": "Conversion date", + "repeatable": false + }, + "k": { + "label": "Identifier of source metadata", + "repeatable": false + }, + "q": { + "label": "Conversion agency", + "repeatable": false, + "codelist": { + "name": "MARC Organization Codes", + "url": "http://www.loc.gov/marc/organizations/orgshome.html" + } + }, + "u": { + "label": "Uniform Resource Identifier", + "repeatable": true + } + } + }, + "885": { + "tag": "885", + "label": "Matching Information", + "url": "https://www.loc.gov/marc/bibliographic/bd885.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Matching information", + "repeatable": false + }, + "b": { + "label": "Status of matching and its checking", + "repeatable": false + }, + "c": { + "label": "Confidence value", + "repeatable": false + }, + "d": { + "label": "Generation date", + "repeatable": false + }, + "w": { + "label": "Record control number", + "repeatable": true + }, + "x": { + "label": "Nonpublic note", + "repeatable": true + }, + "z": { + "label": "Public note", + "repeatable": true + }, + "0": { + "label": "Authority record control number or standard number", + "repeatable": true + }, + "2": { + "label": "Source", + "repeatable": false, + "codelist": { + "name": "MARC Organization Codes", + "url": "http://www.loc.gov/marc/organizations/orgshome.html" + } + }, + "5": { + "label": "Institution to which field applies", + "repeatable": false + } + } + }, + "886": { + "tag": "886", + "label": "Foreign MARC Information Field", + "url": "https://www.loc.gov/marc/bibliographic/bd886.html", + "repeatable": true, + "indicator1": { + "label": "Type of field", + "codes": { + "0": { + "label": "Leader" + }, + "1": { + "label": "Variable control fields (002-009)" + }, + "2": { + "label": "Variable data fields (010-999)" + } + } + }, + "indicator2": null, + "subfields": { + "a": { + "label": "Tag of the foreign MARC field", + "repeatable": false + }, + "b": { + "label": "Content of the foreign MARC field", + "repeatable": false + }, + "2": { + "label": "Source of data", + "repeatable": false, + "codelist": { + "name": "Format Source Codes", + "url": "http://www.loc.gov/standards/sourcelist/format.html" + } + } + }, + "historical-subfields": { + "c": { + "label": "Content of the foreign MARC control fields 002-009 [OBSOLETE, 1997] [CAN/MARC only]" + }, + "d": { + "label": "Content designators and data elements of the foreign MARC variable fields 010-999 [OBSOLETE, 1997] [CAN/MARC only]" + } + } + }, + "887": { + "tag": "887", + "label": "Non-MARC Information Field", + "url": "https://www.loc.gov/marc/bibliographic/bd887.html", + "repeatable": true, + "indicator1": null, + "indicator2": null, + "subfields": { + "a": { + "label": "Content of non-MARC field", + "repeatable": false + }, + "2": { + "label": "Source of data", + "repeatable": false, + "codelist": { + "name": "Format Source Codes", + "url": "http://www.loc.gov/standards/sourcelist/format.html" + } + } + } + } + } +} diff --git a/invenio_records_marc21/templates/invenio_records_marc21/deposit.html b/invenio_records_marc21/templates/invenio_records_marc21/deposit.html new file mode 100644 index 00000000..3771e48c --- /dev/null +++ b/invenio_records_marc21/templates/invenio_records_marc21/deposit.html @@ -0,0 +1,36 @@ +{# + Copyright (C) 2021 Graz University of Technology + + invenio-records-marc21 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 config.INVENIO_MARC21_BASE_TEMPLATE %} + + +{%- set title = _("New upload") %} + + +{%- block page_body %} + {%- if record %} + + {%- endif %} + {%- if files %} + + {%- endif %} + {%- if forms_config %} + + {%- endif %} + {%- if templates %} + +{%- endif %} + {%- if permissions %} + + {%- endif %} +
+{%- endblock page_body %} + +{%- block javascript %} + {{ super() }} + {{ webpack["invenio-records-marc21-deposit.js"] }} +{%- endblock %} \ No newline at end of file diff --git a/invenio_records_marc21/ui/theme/__init__.py b/invenio_records_marc21/ui/theme/__init__.py index cb31859b..e5fda20f 100644 --- a/invenio_records_marc21/ui/theme/__init__.py +++ b/invenio_records_marc21/ui/theme/__init__.py @@ -13,7 +13,7 @@ from flask_babelex import lazy_gettext as _ from flask_menu import current_menu -from .views import index, search +from .views import deposit_create, index, search # @@ -25,5 +25,6 @@ def init_theme_views(blueprint, app): blueprint.add_url_rule(routes["index"], view_func=index) blueprint.add_url_rule(routes["record-search"], view_func=search) + blueprint.add_url_rule(routes["deposit-create"], view_func=deposit_create) return blueprint diff --git a/invenio_records_marc21/ui/theme/deposit.py b/invenio_records_marc21/ui/theme/deposit.py new file mode 100644 index 00000000..d94cb5d3 --- /dev/null +++ b/invenio_records_marc21/ui/theme/deposit.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 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 os.path import dirname, join + +from flask import current_app +from marshmallow import Schema, fields +from marshmallow.schema import SchemaMeta +from marshmallow_utils.fields import NestedAttribute + +from invenio_records_marc21.services.schemas import Marc21RecordSchema + +from ...proxies import current_records_marc21 + + +def dump_empty(schema_or_field): + """Return a full marc21 record dict with empty values.""" + if isinstance(schema_or_field, (Schema,)): + schema = schema_or_field + return {k: dump_empty(v) for (k, v) in schema.fields.items()} + if isinstance(schema_or_field, SchemaMeta): + schema = schema_or_field() + return {k: dump_empty(v) for (k, v) in schema.fields.items()} + if isinstance(schema_or_field, fields.List): + field = schema_or_field + return [dump_empty(field.inner)] + if isinstance(schema_or_field, NestedAttribute): + field = schema_or_field + return dump_empty(field.nested) + + return None + + +def empty_record(): + """Create an empty record.""" + + record = dump_empty(Marc21RecordSchema) + + record["metadata"] = "" + record["is_published"] = False + record["files"] = {"enabled": True} + return record + + +def deposit_templates(): + templates = current_records_marc21.templates_service.get_templates() + + if templates: + return [template.to_dict() for template in templates] + return [] + + +def deposit_config(): + """Create an deposit configuration.""" + jsonschema = current_app.extensions["invenio-jsonschemas"] + schema = {} + if jsonschema: + schema = jsonschema.get_schema(path="marc21/marc21-structure-v1.0.0.json") + config = { + "error": "", + "loaded": False, + "schema": schema, + "createUrl": ("/api/marc21"), + } + return config diff --git a/invenio_records_marc21/ui/theme/views.py b/invenio_records_marc21/ui/theme/views.py index 02490372..42f09d8a 100644 --- a/invenio_records_marc21/ui/theme/views.py +++ b/invenio_records_marc21/ui/theme/views.py @@ -11,6 +11,9 @@ """Routes for general pages provided by Invenio-Records-Marc21.""" from flask import render_template +from flask_login import login_required + +from .deposit import deposit_config, deposit_templates, empty_record def index(): @@ -21,3 +24,14 @@ def index(): def search(): """Search help guide.""" return render_template("invenio_records_marc21/search.html") + + +@login_required +def deposit_create(): + """Create a new deposit page.""" + return render_template( + "invenio_records_marc21/deposit.html", + record=empty_record(), + templates=deposit_templates(), + forms_config=deposit_config(), + ) diff --git a/invenio_records_marc21/ui/theme/webpack.py b/invenio_records_marc21/ui/theme/webpack.py index 685a6c57..ad009284 100644 --- a/invenio_records_marc21/ui/theme/webpack.py +++ b/invenio_records_marc21/ui/theme/webpack.py @@ -20,6 +20,7 @@ "semantic-ui": dict( entry={ "invenio-records-marc21-theme": "./less/invenio_records_marc21/theme.less", + "invenio-records-marc21-deposit": "./js/invenio_records_marc21/deposit/index.js", "invenio-records-marc21-search": "./js/invenio_records_marc21/search/index.js", }, dependencies={ From 078a4e5e4009af9e6b9258e515247162400a34d1 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 18 Oct 2021 13:56:30 +0200 Subject: [PATCH 116/217] bugfix: add missing manifest entry --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index 981cdfc4..a3df44dd 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -40,6 +40,7 @@ recursive-include invenio_records_marc21 *.py recursive-include invenio_records_marc21 *.csv recursive-include invenio_records_marc21 *.js recursive-include invenio_records_marc21 *.xsd +recursive-include invenio_records_marc21 *.less recursive-include tests *.py recursive-include tests *.json recursive-include tests *.xml \ No newline at end of file From 38119653b2e070dfa4a9ae0f5678263ea5b661e4 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 18 Oct 2021 14:06:45 +0200 Subject: [PATCH 117/217] bugfix: pycodestyle --- invenio_records_marc21/ui/theme/deposit.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/invenio_records_marc21/ui/theme/deposit.py b/invenio_records_marc21/ui/theme/deposit.py index d94cb5d3..60e7ec96 100644 --- a/invenio_records_marc21/ui/theme/deposit.py +++ b/invenio_records_marc21/ui/theme/deposit.py @@ -5,6 +5,8 @@ # Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it # under the terms of the MIT License; see LICENSE file for more details. +"""Marc21 records deposit backend.""" + from os.path import dirname, join from flask import current_app @@ -37,7 +39,6 @@ def dump_empty(schema_or_field): def empty_record(): """Create an empty record.""" - record = dump_empty(Marc21RecordSchema) record["metadata"] = "" @@ -47,6 +48,7 @@ def empty_record(): def deposit_templates(): + """Retrieve from DB the tamplates for marc21 deposit form.""" templates = current_records_marc21.templates_service.get_templates() if templates: From 7fb2689b8ab8f8db579fa8375e587fcdaaed0ac4 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Tue, 19 Oct 2021 14:52:54 +0200 Subject: [PATCH 118/217] modification: update record acces schema using invenio_rdm_records as system field provider for the service and serializers --- invenio_records_marc21/cli.py | 10 +- invenio_records_marc21/records/api.py | 11 +- .../jsonschemas/marc21/marc21-v1.0.0.json | 21 +- .../jsonschemas/marc21/parent-v1.0.0.json | 95 ++++++--- .../marc21records/drafts/marc21-v1.0.0.json | 32 ++- .../marc21records/marc21/marc21-v1.0.0.json | 32 ++- .../records/systemfields/__init__.py | 2 - .../records/systemfields/access/__init__.py | 34 ---- .../records/systemfields/access/embargo.py | 125 ------------ .../systemfields/access/fields/__init__.py | 21 -- .../systemfields/access/fields/parent.py | 145 -------------- .../systemfields/access/fields/record.py | 189 ------------------ .../records/systemfields/access/owners.py | 127 ------------ .../records/systemfields/access/protection.py | 75 ------- .../records/systemfields/access/status.py | 27 --- .../resources/serializers/schema.py | 2 +- .../serializers/ui/fields/__init__.py | 19 -- .../resources/serializers/ui/fields/access.py | 86 -------- .../resources/serializers/ui/schema.py | 5 +- .../services/components/access.py | 9 +- invenio_records_marc21/services/config.py | 6 +- .../services/record/metadata.py | 4 +- .../services/schemas/__init__.py | 8 +- .../services/schemas/access/__init__.py | 21 -- .../services/schemas/access/embargo.py | 53 ----- .../services/schemas/access/parent.py | 29 --- .../services/schemas/access/record.py | 65 ------ invenio_records_marc21/services/services.py | 11 +- .../invenio_records_marc21/record.html | 6 +- .../search/components.js | 11 +- .../js/invenio_records_marc21/search/index.js | 8 +- setup.py | 5 +- 32 files changed, 179 insertions(+), 1115 deletions(-) delete mode 100644 invenio_records_marc21/records/systemfields/access/__init__.py delete mode 100644 invenio_records_marc21/records/systemfields/access/embargo.py delete mode 100644 invenio_records_marc21/records/systemfields/access/fields/__init__.py delete mode 100644 invenio_records_marc21/records/systemfields/access/fields/parent.py delete mode 100644 invenio_records_marc21/records/systemfields/access/fields/record.py delete mode 100644 invenio_records_marc21/records/systemfields/access/owners.py delete mode 100644 invenio_records_marc21/records/systemfields/access/protection.py delete mode 100644 invenio_records_marc21/records/systemfields/access/status.py delete mode 100644 invenio_records_marc21/resources/serializers/ui/fields/__init__.py delete mode 100644 invenio_records_marc21/resources/serializers/ui/fields/access.py delete mode 100644 invenio_records_marc21/services/schemas/access/__init__.py delete mode 100644 invenio_records_marc21/services/schemas/access/embargo.py delete mode 100644 invenio_records_marc21/services/schemas/access/parent.py delete mode 100644 invenio_records_marc21/services/schemas/access/record.py diff --git a/invenio_records_marc21/cli.py b/invenio_records_marc21/cli.py index 317d324a..1810be02 100644 --- a/invenio_records_marc21/cli.py +++ b/invenio_records_marc21/cli.py @@ -20,10 +20,12 @@ from flask.cli import with_appcontext from flask_principal import Identity from invenio_access import any_user +from invenio_rdm_records.records.systemfields.access.field.record import ( + AccessStatusEnum, +) from .errors import log_exceptions from .proxies import current_records_marc21 -from .records.systemfields.access import AccessStatusEnum from .services.record import Marc21Metadata @@ -67,7 +69,7 @@ def create_fake_metadata(filename): data_acces = { "owned_by": [{"user": system_identity().id}], "files": "public", - "metadata": metadata_access, + "record": metadata_access, } if metadata_access == AccessStatusEnum.EMBARGOED.value: embargo = { @@ -97,8 +99,8 @@ def create_fake_record(filename): metadata_access = fake_access_right() data_acces = { "owned_by": [{"user": system_identity().id}], - "files": AccessStatusEnum.PUBLIC.value, - "metadata": metadata_access, + "files": AccessStatusEnum.OPEN.value, + "record": metadata_access, } if metadata_access == AccessStatusEnum.EMBARGOED.value: data_acces.update({"embargo_date": fake_feature_date()}) diff --git a/invenio_records_marc21/records/api.py b/invenio_records_marc21/records/api.py index b982391b..ad07e144 100644 --- a/invenio_records_marc21/records/api.py +++ b/invenio_records_marc21/records/api.py @@ -15,6 +15,11 @@ from invenio_drafts_resources.records import Draft, Record from invenio_drafts_resources.records.api import ParentRecord as BaseParentRecord from invenio_drafts_resources.records.systemfields import ParentField +from invenio_rdm_records.records.systemfields import ( + HasDraftCheckField, + ParentRecordAccessField, + RecordAccessField, +) from invenio_records.systemfields import ConstantField, ModelField from invenio_records_resources.records.api import FileRecord as BaseFileRecord from invenio_records_resources.records.systemfields import ( @@ -26,13 +31,11 @@ from . import models from .systemfields import ( - HasDraftField, MarcDraftProvider, MarcPIDFieldContext, MarcRecordProvider, MarcResolver, ) -from .systemfields.access import ParentRecordAccessField, RecordAccessField # @@ -90,7 +93,7 @@ class Marc21Draft(Draft): delete=False, ) access = RecordAccessField() - has_draft = HasDraftField() + has_draft = HasDraftCheckField() bucket_id = ModelField(dump=False) @@ -133,7 +136,7 @@ class Marc21Record(Record): ) access = RecordAccessField() - has_draft = HasDraftField(Marc21Draft) + has_draft = HasDraftCheckField(Marc21Draft) bucket_id = ModelField(dump=False) diff --git a/invenio_records_marc21/records/jsonschemas/marc21/marc21-v1.0.0.json b/invenio_records_marc21/records/jsonschemas/marc21/marc21-v1.0.0.json index 65217e7f..10e508ff 100644 --- a/invenio_records_marc21/records/jsonschemas/marc21/marc21-v1.0.0.json +++ b/invenio_records_marc21/records/jsonschemas/marc21/marc21-v1.0.0.json @@ -32,12 +32,11 @@ "description": "Record access control and ownership.", "additionalProperties": false, "properties": { - "metadata": { - "description": "Metadata visibility (public or restricted)", + "record": { + "description": "Record visibility (public or restricted)", "type": "string", "enum": [ "public", - "embargoed", "restricted" ] }, @@ -46,25 +45,16 @@ "type": "string", "enum": [ "public", - "embargoed", "restricted" ] }, - "owned_by": { - "description": "List of user IDs that are owners of the record.", - "type": "array", - "minItems": 1, - "uniqueItems": true, - "items": { - "$ref": "local://marc21/definitions-v1.0.0.json#/agent" - } - }, "embargo": { - "description": "Embargo date of record (ISO8601 formatted date time in UTC). At this time both metadata and files will be made public.", + "description": "Description of the embargo on the record.", "type": "object", "additionalProperties": false, "properties": { "active": { + "description": "Whether or not the embargo is (still) active.", "type": [ "boolean", "null" @@ -75,7 +65,8 @@ "type": [ "string", "null" - ] + ], + "format": "date" }, "reason": { "description": "The reason why the record is under embargo.", diff --git a/invenio_records_marc21/records/jsonschemas/marc21/parent-v1.0.0.json b/invenio_records_marc21/records/jsonschemas/marc21/parent-v1.0.0.json index 6de7a86f..399f21ac 100644 --- a/invenio_records_marc21/records/jsonschemas/marc21/parent-v1.0.0.json +++ b/invenio_records_marc21/records/jsonschemas/marc21/parent-v1.0.0.json @@ -1,35 +1,80 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", - "id": "local://marc21/parent-v1.0.0.json", - "title": "Invenio Parent Record Marc21 v1.0.0", - "type": "object", - "additionalProperties": false, - "properties": { - "$schema": { - "description": "JSONSchema declaration.", - "type": "string" + "$schema": "http://json-schema.org/draft-07/schema#", + "id": "local://marc21/parent-v1.0.0.json", + "title": "Invenio Parent Record Marc21 v1.0.0", + "type": "object", + "additionalProperties": false, + "properties": { + "$schema": { + "description": "JSONSchema declaration.", + "type": "string" + }, + "id": { + "description": "Persistent record identifier (alphanumeric).", + "type": "string" + }, + "pid": { + "$ref": "local://marc21/definitions-v1.0.0.json#/internal-pid" + }, + "access": { + "type": "object", + "description": "Access control and ownership for all versions of a record.", + "additionalProperties": false, + "properties": { + "owned_by": { + "description": "List of owners of the child records.", + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "local://marc21/definitions-v1.0.0.json#/agent" + } }, - "id": { - "description": "Persistent record identifier (alphanumeric).", - "type": "string" - }, - "pid": { - "$ref": "local://marc21/definitions-v1.0.0.json#/internal-pid" + "grants": { + "description": "Access grants for the child records.", + "type": "array", + "items": { + "type": "object", + "required": [ + "subject", + "id", + "level" + ], + "additionalProperties": false, + "properties": { + "subject": { + "type": "string", + "enum": [ + "user", + "role", + "sysrole" + ] + }, + "id": { + "type": "string" + }, + "level": { + "type": "string" + } + } + } }, - "access": { + "links": { + "description": "Secret links for the child records.", + "type": "array", + "items": { "type": "object", - "description": "Record access control and ownership.", + "required": [ + "id" + ], "additionalProperties": false, "properties": { - "owned_by": { - "description": "List of user IDs that are owners of the record.", - "type": "array", - "uniqueItems": true, - "items": { - "$ref": "local://marc21/definitions-v1.0.0.json#/agent" - } - } + "id": { + "type": "string" + } } + } } + } } + } } diff --git a/invenio_records_marc21/records/mappings/v7/marc21records/drafts/marc21-v1.0.0.json b/invenio_records_marc21/records/mappings/v7/marc21records/drafts/marc21-v1.0.0.json index 575588f3..228321e6 100644 --- a/invenio_records_marc21/records/mappings/v7/marc21records/drafts/marc21-v1.0.0.json +++ b/invenio_records_marc21/records/mappings/v7/marc21records/drafts/marc21-v1.0.0.json @@ -8,7 +8,7 @@ }, "access": { "properties": { - "metadata": { + "record": { "type": "keyword" }, "files": { @@ -27,13 +27,6 @@ } } }, - "owned_by": { - "properties": { - "user": { - "type": "keyword" - } - } - }, "status": { "type": "keyword" } @@ -52,6 +45,29 @@ "type": "keyword" } } + }, + "grants": { + "properties": { + "subject": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "level": { + "type": "keyword" + } + } + }, + "grant_tokens": { + "type": "keyword" + }, + "links": { + "properties": { + "id": { + "type": "keyword" + } + } } } } diff --git a/invenio_records_marc21/records/mappings/v7/marc21records/marc21/marc21-v1.0.0.json b/invenio_records_marc21/records/mappings/v7/marc21records/marc21/marc21-v1.0.0.json index 575588f3..228321e6 100644 --- a/invenio_records_marc21/records/mappings/v7/marc21records/marc21/marc21-v1.0.0.json +++ b/invenio_records_marc21/records/mappings/v7/marc21records/marc21/marc21-v1.0.0.json @@ -8,7 +8,7 @@ }, "access": { "properties": { - "metadata": { + "record": { "type": "keyword" }, "files": { @@ -27,13 +27,6 @@ } } }, - "owned_by": { - "properties": { - "user": { - "type": "keyword" - } - } - }, "status": { "type": "keyword" } @@ -52,6 +45,29 @@ "type": "keyword" } } + }, + "grants": { + "properties": { + "subject": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "level": { + "type": "keyword" + } + } + }, + "grant_tokens": { + "type": "keyword" + }, + "links": { + "properties": { + "id": { + "type": "keyword" + } + } } } } diff --git a/invenio_records_marc21/records/systemfields/__init__.py b/invenio_records_marc21/records/systemfields/__init__.py index 57b02a99..647c4325 100644 --- a/invenio_records_marc21/records/systemfields/__init__.py +++ b/invenio_records_marc21/records/systemfields/__init__.py @@ -12,12 +12,10 @@ """System fields module.""" from .context import MarcPIDFieldContext -from .has_draft import HasDraftField from .providers import MarcDraftProvider, MarcRecordProvider from .resolver import MarcResolver __all__ = ( - "HasDraftField", "MarcPIDFieldContext", "MarcDraftProvider", "MarcRecordProvider", diff --git a/invenio_records_marc21/records/systemfields/access/__init__.py b/invenio_records_marc21/records/systemfields/access/__init__.py deleted file mode 100644 index e8adf103..00000000 --- a/invenio_records_marc21/records/systemfields/access/__init__.py +++ /dev/null @@ -1,34 +0,0 @@ -# -*- coding: utf-8 -*- -# -# This file is part of Invenio. -# -# Copyright (C) 2021 Graz University of Technology. -# -# Invenio-Records-Marc21 is free software; you can redistribute it and/or -# modify it under the terms of the MIT License; see LICENSE file for more -# details. - -"""Access module.""" - -from .embargo import Embargo -from .fields import ( - ParentRecordAccess, - ParentRecordAccessField, - RecordAccess, - RecordAccessField, -) -from .owners import Owner, Owners -from .protection import Protection -from .status import AccessStatusEnum - -__all__ = ( - "AccessStatusEnum", - "Embargo", - "ParentRecordAccessField", - "ParentRecordAccess", - "RecordAccessField", - "RecordAccess", - "Owner", - "Owners", - "Protection", -) diff --git a/invenio_records_marc21/records/systemfields/access/embargo.py b/invenio_records_marc21/records/systemfields/access/embargo.py deleted file mode 100644 index df2d1f74..00000000 --- a/invenio_records_marc21/records/systemfields/access/embargo.py +++ /dev/null @@ -1,125 +0,0 @@ -# -*- coding: utf-8 -*- -# -# This file is part of Invenio. -# -# Copyright (C) 2021 TU Wien. -# Copyright (C) 2021 Graz University of Technology. -# -# Invenio-Records-Marc21 is free software; you can redistribute it and/or -# modify it under the terms of the MIT License; see LICENSE file for more -# details. - -"""Embargo class for the access system field.""" - -import arrow - - -class Embargo: - """Embargo class for the access system field.""" - - def __init__(self, until=None, reason=None, active=None): - """Create a new Embargo.""" - self.until = until - if isinstance(until, str): - self.until = arrow.get(until).datetime - - self.reason = reason - self._active = active - - @property - def active(self): - """Whether or not the Embargo is (still) active.""" - if self._active is not None: - return self._active - - elif self.until is None: - return False - - return arrow.utcnow().datetime < self.until - - @active.setter - def active(self, value): - """Set the Embargo's (boolean) active state.""" - self._active = bool(value) - - def lift(self): - """Update the embargo active status if it has expired. - - Returns ``True`` if the embargo was actually lifted (i.e. it was - expired but still marked as active). - """ - if self.until is not None: - if arrow.utcnow().datetime > self.until: - was_active = bool(self._active) - self.active = False - return was_active - - return False - - def clear(self): - """Clear any information if the embargo was ever active.""" - self.until = None - self.reason = None - self._active = None - - def dump(self): - """Dump the embargo as dictionary.""" - until_str = None - if self.until is not None: - until_str = self.until.strftime("%Y-%m-%d") - - return { - "active": self.active, - "until": until_str, - "reason": self.reason, - } - - def __repr__(self): - """Return repr(self).""" - if self == Embargo(): - return "" - - until_str = self.until or "n/a" - - return "<{} (active: {}, until: {}, reason: {})>".format( - type(self).__name__, self.active, until_str, self.reason - ) - - def __eq__(self, other): - """Return self == other.""" - if type(self) != type(other): - return False - - return ( - self.reason == other.reason and self.until == other.until and self.active == other.active - ) - - def __ne__(self, other): - """Return self != other.""" - return not (self == other) - - def __bool__(self): - """Return bool(self).""" - return self.active - - @classmethod - def from_dict(cls, dict_, ignore_active_value=False): - """Parse the Embargo from the given dictionary.""" - if not dict_: - return cls() - - until = dict_.get("until") - if until: - until = arrow.get(until).datetime - - reason = dict_.get("reason") - active = dict_.get("active") - if ignore_active_value: - # with ignore_active_value, the 'active' value is re-calculated - # instead of parsed - active = None - - if not until and not reason and not active: - return cls() - - return cls(until=until, reason=reason, active=active) diff --git a/invenio_records_marc21/records/systemfields/access/fields/__init__.py b/invenio_records_marc21/records/systemfields/access/fields/__init__.py deleted file mode 100644 index 04b87d78..00000000 --- a/invenio_records_marc21/records/systemfields/access/fields/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- -# -# This file is part of Invenio. -# -# Copyright (C) 2021 Graz University of Technology. -# -# Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it -# under the terms of the MIT License; see LICENSE file for more details. - - -"""System access fields module.""" - -from .parent import ParentRecordAccess, ParentRecordAccessField -from .record import RecordAccess, RecordAccessField - -__all__ = ( - "ParentRecordAccessField", - "ParentRecordAccess", - "RecordAccessField", - "RecordAccess", -) diff --git a/invenio_records_marc21/records/systemfields/access/fields/parent.py b/invenio_records_marc21/records/systemfields/access/fields/parent.py deleted file mode 100644 index 3b6d5e8d..00000000 --- a/invenio_records_marc21/records/systemfields/access/fields/parent.py +++ /dev/null @@ -1,145 +0,0 @@ -# -*- coding: utf-8 -*- -# -# This file is part of Invenio. -# -# Copyright (C) 2021 TU Wien. -# Copyright (C) 2021 Graz University of Technology. -# -# Invenio-Records-Marc21 is free software; you can redistribute it and/or -# modify it under the terms of the MIT License; see LICENSE file for more -# details. - -"""Access system field.""" - -from invenio_records.systemfields import SystemField - -from ..owners import Owners - - -class ParentRecordAccess: - """Access management for all versions of a record.""" - - owners_cls = Owners - - def __init__( - self, - owned_by=None, - owners_cls=None, - ): - """Create a new Access object for a record. - - If ``owned_by`` are not specified, - a new instance of ``owners_cls`` - will be used, respectively. - :param owned_by: The set of record owners - """ - owners_cls = owners_cls or ParentRecordAccess.owners_cls - self.owned_by = owned_by if owned_by else owners_cls() - self.errors = [] - - @property - def owners(self): - """An alias for the owned_by property.""" - return self.owned_by - - def dump(self): - """Dump the field values as dictionary.""" - access = { - "owned_by": self.owned_by.dump(), - } - - return access - - def refresh_from_dict(self, access_dict): - """Re-initialize the Access object with the data in the access_dict.""" - new_access = self.from_dict(access_dict) - self.errors = new_access.errors - self.owned_by = new_access.owned_by - - @classmethod - def from_dict( - cls, - access_dict, - owners_cls=None, - ): - """Create a new Access object from the specified 'access' property. - - The new ``ParentRecordAccess`` object will be populated with new - instances from the configured classes. - If ``access_dict`` is empty, the ``ParentRecordAccess`` object will - be populated with new instances of ``owners_cls``. - """ - owners_cls = owners_cls or cls.owners_cls - errors = [] - - owners = owners_cls() - - if access_dict: - for owner_dict in access_dict.get("owned_by", []): - try: - owners.add(owners.owner_cls(owner_dict)) - except Exception as e: - errors.append(e) - - access = cls( - owned_by=owners, - ) - access.errors = errors - return access - - def __repr__(self): - """Return repr(self).""" - return ("<{} (owners: {})>").format( - type(self).__name__, - len(self.owners or []), - ) - - -class ParentRecordAccessField(SystemField): - """System field for managing record access.""" - - def __init__(self, key="access", access_obj_class=ParentRecordAccess): - """Create a new ParentRecordAccessField instance.""" - self._access_obj_class = access_obj_class - super().__init__(key=key) - - def obj(self, instance): - """Get the access object.""" - data = self.get_dictkey(instance) - obj = self._get_cache(instance) - if obj is not None and data is None: - return obj - - if data: - obj = self._access_obj_class.from_dict(data) - else: - obj = self._access_obj_class() - - self._set_cache(instance, obj) - return obj - - def set_obj(self, record, obj): - """Set the access object.""" - if isinstance(obj, dict): - obj = self._access_obj_class.from_dict(obj) - - assert isinstance(obj, self._access_obj_class) - - self._set_cache(record, obj) - - def __get__(self, record, owner=None): - """Get the record's access object.""" - if record is None: - return self - - return self.obj(record) - - def __set__(self, record, obj): - """Set the records access object.""" - self.set_obj(record, obj) - - def pre_commit(self, record): - """Dump the configured values before the record is committed.""" - obj = self.obj(record) - if obj is not None: - record["access"] = obj.dump() diff --git a/invenio_records_marc21/records/systemfields/access/fields/record.py b/invenio_records_marc21/records/systemfields/access/fields/record.py deleted file mode 100644 index 5c892b66..00000000 --- a/invenio_records_marc21/records/systemfields/access/fields/record.py +++ /dev/null @@ -1,189 +0,0 @@ -# -*- coding: utf-8 -*- -# -# This file is part of Invenio. -# -# Copyright (C) 2021 TU Wien. -# Copyright (C) 2021 Graz University of Technology. -# -# Invenio-Records-Marc21 is free software; you can redistribute it and/or -# modify it under the terms of the MIT License; see LICENSE file for more -# details. - -"""Access system field.""" - -from enum import Enum - -from invenio_records.systemfields import SystemField - -from ..embargo import Embargo -from ..protection import Protection -from ..status import AccessStatusEnum - - -class RecordAccess: - """Access management per record.""" - - protection_cls = Protection - embargo_cls = Embargo - - def __init__( - self, - protection=None, - embargo=None, - protection_cls=None, - embargo_cls=None, - has_files=None, - ): - """Create a new RecordAccess object for a record. - - If ``protection`` or ``embargo`` are not specified, - a new instance of ``protection_cls`` or ``embargo_cls`` - will be used, respectively. - :param protection: The record and file protection levels - :param embargo: The embargo on the record (None means no embargo) - """ - protection_cls = protection_cls or RecordAccess.protection_cls - embargo_cls = embargo_cls or RecordAccess.embargo_cls - - public = protection_cls("public", "public") - self.protection = protection if protection is not None else public - self.embargo = embargo if embargo is not None else embargo_cls() - self.has_files = has_files - self.errors = [] - - @property - def status(self): - """Record's access status.""" - status = AccessStatusEnum.RESTRICTED - - if self.embargo.active: - status = AccessStatusEnum.EMBARGOED - elif self.protection.metadata == self.protection.files == "public": - status = AccessStatusEnum.PUBLIC - - return status - - def dump(self): - """Dump the field values as dictionary.""" - access = { - "metadata": self.protection.metadata, - "files": self.protection.files, - "embargo": self.embargo.dump(), - } - - return access - - def refresh_from_dict(self, access_dict): - """Re-initialize the Access object with the data in the access_dict.""" - new_access = self.from_dict(access_dict) - self.protection = new_access.protection - self.embargo = new_access.embargo - self.errors = new_access.errors - - @classmethod - def from_dict( - cls, access_dict, protection_cls=None, embargo_cls=None, has_files=None - ): - """Create a new Access object from the specified 'access' property. - - The new ``RecordAccess`` object will be populated with new instances - from the configured classes. - If ``access_dict`` is empty, the ``Access`` object will be populated - with new instances of ``protection_cls`` and ``embargo_cls``. - """ - protection_cls = protection_cls or cls.protection_cls - embargo_cls = embargo_cls or cls.embargo_cls - errors = [] - - protection = protection_cls() - embargo = embargo_cls() - - if access_dict: - try: - protection = protection_cls( - access_dict["metadata"], access_dict["files"] - ) - except Exception as e: - errors.append(e) - - embargo_dict = access_dict.get("embargo") - if embargo_dict is not None: - embargo = embargo_cls.from_dict(embargo_dict) - - access = cls( - protection=protection, - embargo=embargo, - has_files=has_files, - ) - access.errors = errors - - return access - - def __repr__(self): - """Return repr(self).""" - protection_str = "{}/{}".format(self.protection.metadata, self.protection.files) - - return ("<{} (protection: {}, {})>").format( - type(self).__name__, - protection_str, - self.embargo, - ) - - -class RecordAccessField(SystemField): - """System field for managing record access.""" - - def __init__(self, key="access", access_obj_class=RecordAccess): - """Create a new RecordAccessField instance.""" - self._access_obj_class = access_obj_class - super().__init__(key=key) - - def obj(self, instance): - """Get the access object.""" - obj = self._get_cache(instance) - if obj is not None: - return obj - - data = self.get_dictkey(instance) - if data: - obj = self._access_obj_class.from_dict( - data, has_files=len(instance.files or [])) - else: - obj = self._access_obj_class() - - self._set_cache(instance, obj) - return obj - - def set_obj(self, record, obj): - """Set the access object.""" - if isinstance(obj, dict): - obj = self._access_obj_class.from_dict(obj) - - assert isinstance(obj, self._access_obj_class) - self._set_cache(record, obj) - - def __get__(self, record, owner=None): - """Get the record's access object.""" - if record is None: - return self - return self.obj(record) - - def __set__(self, metadata, obj): - """Set the records access object.""" - self.set_obj(metadata, obj) - - def pre_commit(self, record): - """Dump the configured values before the record is committed.""" - obj = self.obj(record) - if obj is not None: - record["access"] = obj.dump() - - def post_dump(self, record, data, dumper=None): - """Called before a record is dumped.""" - if data.get("access") and isinstance(data.get("access"), dict): - data["access"]["status"] = record.access.status.value - - def pre_load(self, data, loader=None): - """Called before a record is dumped.""" - if data.get("access") and isinstance(data.get("access"), dict): - data["access"].pop("status", None) diff --git a/invenio_records_marc21/records/systemfields/access/owners.py b/invenio_records_marc21/records/systemfields/access/owners.py deleted file mode 100644 index 6c58adcc..00000000 --- a/invenio_records_marc21/records/systemfields/access/owners.py +++ /dev/null @@ -1,127 +0,0 @@ -# -*- coding: utf-8 -*- -# -# This file is part of Invenio. -# -# Copyright (C) 2021 TU Wien. -# Copyright (C) 2021 Graz University of Technology. -# -# Invenio-Records-Marc21 is free software; you can redistribute it and/or -# modify it under the terms of the MIT License; see LICENSE file for more -# details. - -"""Owners classes for the access system field.""" - -from invenio_accounts.models import User - - -class Owner: - """An abstraction between owner entities and specifications as dicts.""" - - def __init__(self, owner): - """Create an owner from either a dictionary or a User.""" - self._entity = None - self.owner_type = None - self.owner_id = None - - if isinstance(owner, dict): - if "user" in owner: - self.owner_type = "user" - self.owner_id = owner["user"] - - else: - raise ValueError("unknown owner type: {}".format(owner)) - - elif isinstance(owner, User): - self._entity = owner - self.owner_id = owner.id - self.owner_type = "user" - - else: - raise TypeError("invalid owner type: {}".format(type(owner))) - - def dump(self): - """Dump the owner to a dictionary.""" - return {self.owner_type: self.owner_id} - - def resolve(self, raise_exc=False): - """Resolve the owner entity (e.g. User) via a database query.""" - if self._entity is None: - if self.owner_type == "user": - self._entity = User.query.get(self.owner_id) - - else: - raise ValueError("unknown owner type: {}".format(self.owner_type)) - - if self._entity is None and raise_exc: - raise LookupError("could not find owner: {}".format(self.dump())) - - return self._entity - - def __hash__(self): - """Return hash(self).""" - return hash(self.owner_type) + hash(self.owner_id) - - def __eq__(self, other): - """Return self == other.""" - if type(self) != type(other): - return False - - return self.owner_type == other.owner_type and self.owner_id == other.owner_id - - def __ne__(self, other): - """Return self != other.""" - return not self == other - - def __str__(self): - """Return str(self).""" - return str(self.resolve()) - - def __repr__(self): - """Return repr(self).""" - return repr(self.resolve()) - - -class Owners(list): - """A list of owners for a record.""" - - owner_cls = Owner - - def __init__(self, owners=None, owner_cls=None): - """Create a new list of owners.""" - self.owner_cls = owner_cls or Owners.owner_cls - for owner in owners or []: - self.add(owner) - - def add(self, owner): - """Alias for self.append(owner).""" - self.append(owner) - - def append(self, owner): - """Add the specified owner to the list of owners. - - :param owner: The record's owner (either a dict, User or Owner). - """ - if not isinstance(owner, self.owner_cls): - owner = self.owner_cls(owner) - - if owner not in self: - super().append(owner) - - def extend(self, owners): - """Add all new items from the specified owners to this list.""" - for owner in owners: - self.add(owner) - - def remove(self, owner): - """Remove the specified owner from the list of owners. - - :param owner: The record's owner (either a dict, User or Owner). - """ - if not isinstance(owner, self.owner_cls): - owner = self.owner_cls(owner) - - super().remove(owner) - - def dump(self): - """Dump the owners as a list of owner dictionaries.""" - return [owner.dump() for owner in self] diff --git a/invenio_records_marc21/records/systemfields/access/protection.py b/invenio_records_marc21/records/systemfields/access/protection.py deleted file mode 100644 index 61db7d2e..00000000 --- a/invenio_records_marc21/records/systemfields/access/protection.py +++ /dev/null @@ -1,75 +0,0 @@ -# -*- coding: utf-8 -*- -# -# This file is part of Invenio. -# -# Copyright (C) 2021 TU Wien. -# Copyright (C) 2021 Graz University of Technology. -# -# Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it -# under the terms of the MIT License; see LICENSE file for more details. - -"""Protection class for the access system field.""" - - -class Protection: - """Protection class for the access system field.""" - - def __init__(self, metadata="public", files="public"): - """Create a new protection levels instance.""" - self.set(metadata=metadata, files=files) - - def _validate_protection_level(self, level): - return level in ("public", "embargoed", "restricted") - - @property - def metadata(self): - """Get the record's overall protection level.""" - return self._metadata - - @metadata.setter - def metadata(self, value): - """Set the record's overall protection level.""" - if not self._validate_protection_level(value): - raise ValueError("unknown metadata protection level: {}".format(value)) - - if value == "restricted": - self._files = "restricted" - - self._metadata = value - - @property - def files(self): - """Get the record's files protection level.""" - return self._files - - @files.setter - def files(self, value): - """Set the record's files protection level.""" - if not self._validate_protection_level(value): - raise ValueError("unknown files protection level: {}".format(value)) - - if self.metadata == "restricted": - self._files = "restricted" - else: - self._files = value - - def set(self, metadata, files=None): - """Set the protection level for record and files.""" - self.metadata = metadata - if files is not None: - self.files = files - - def __get__(self): - """Get the protection level of the record and its files.""" - return { - "metadata": self.metadata, - "files": self.files, - } - - def __repr__(self): - """Return repr(self).""" - return "<{} (metadata: {}, files: {})>".format( - type(self).__name__, - self.metadata, - self.files, - ) diff --git a/invenio_records_marc21/records/systemfields/access/status.py b/invenio_records_marc21/records/systemfields/access/status.py deleted file mode 100644 index 9016c4a1..00000000 --- a/invenio_records_marc21/records/systemfields/access/status.py +++ /dev/null @@ -1,27 +0,0 @@ -# -*- coding: utf-8 -*- -# -# This file is part of Invenio. -# -# Copyright (C) 2021 Graz University of Technology. -# -# Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it -# under the terms of the MIT License; see LICENSE file for more details. - -"""Marc21 access status.""" - -from enum import Enum - - -class AccessStatusEnum(Enum): - """Enum defining access statuses.""" - - PUBLIC = "public" - - EMBARGOED = "embargoed" - - RESTRICTED = "restricted" - - @staticmethod - def list(): - """List all access statuses.""" - return list(map(lambda c: c.value, AccessStatusEnum)) diff --git a/invenio_records_marc21/resources/serializers/schema.py b/invenio_records_marc21/resources/serializers/schema.py index 8b5d5859..cd829cce 100644 --- a/invenio_records_marc21/resources/serializers/schema.py +++ b/invenio_records_marc21/resources/serializers/schema.py @@ -28,7 +28,7 @@ class Meta: """Meta class to accept unknwon fields.""" additional = ( - "access", + "access_status", "created", "updated", "links", diff --git a/invenio_records_marc21/resources/serializers/ui/fields/__init__.py b/invenio_records_marc21/resources/serializers/ui/fields/__init__.py deleted file mode 100644 index ac5f5188..00000000 --- a/invenio_records_marc21/resources/serializers/ui/fields/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -# -*- coding: utf-8 -*- -# -# This file is part of Invenio. -# -# Copyright (C) 2021 Graz University of Technology. -# -# Invenio-Records-Marc21 is free software; you can redistribute it and/or -# modify it under the terms of the MIT License; see LICENSE file for more -# details. - -"""Record UI response fields.""" - - -from .access import AccessField, UIAccessSchema - -__all__ = ( - "AccessField", - "UIAccessSchema", -) diff --git a/invenio_records_marc21/resources/serializers/ui/fields/access.py b/invenio_records_marc21/resources/serializers/ui/fields/access.py deleted file mode 100644 index 8836823c..00000000 --- a/invenio_records_marc21/resources/serializers/ui/fields/access.py +++ /dev/null @@ -1,86 +0,0 @@ -# -*- coding: utf-8 -*- -# -# This file is part of Invenio. -# -# Copyright (C) 2020 CERN. -# Copyright (C) 2020 Northwestern University. -# Copyright (C) 2021 Graz University of Technology. -# -# Invenio-Records-Marc21 is free software; you can redistribute it and/or -# modify it under the terms of the MIT License; see LICENSE file for more -# details. - -"""Access field for UI.""" - - -from babel_edtf import format_edtf -from flask_babelex import gettext as _ -from marshmallow import fields - -from .....records.systemfields.access import AccessStatusEnum - - -class UIAccessSchema: - """Access status properties to display in the UI.""" - - def __init__(self, access): - """Build access status object.""" - self.access = access - self.access_status = AccessStatusEnum(access.get("status")) - - @property - def id(self): - """Access status id.""" - return self.access_status.value - - @property - def title(self): - """Access status title.""" - return { - AccessStatusEnum.PUBLIC: _("Public"), - AccessStatusEnum.EMBARGOED: _("Embargoed"), - AccessStatusEnum.RESTRICTED: _("Restricted"), - }.get(self.access_status) - - @property - def icon(self): - """Access status icon.""" - return { - AccessStatusEnum.PUBLIC: "unlock", - AccessStatusEnum.EMBARGOED: "outline clock", - AccessStatusEnum.RESTRICTED: "ban", - }.get(self.access_status) - - @property - def embargo_date(self): - """Embargo date.""" - until = self.access.get("embargo").get("until") - if until: - return format_edtf(until, format="long") - return until - - @property - def message_class(self): - """UI message class name.""" - return { - AccessStatusEnum.PUBLIC: "teal", - AccessStatusEnum.EMBARGOED: "warning", - AccessStatusEnum.RESTRICTED: "negative", - }.get(self.access_status) - - -class AccessField(fields.Field): - """Record access status.""" - - def _serialize(self, value, attr, obj, **kwargs): - """Serialise access status.""" - record_access_dict = obj.get("access") - if record_access_dict: - record_access_ui = UIAccessSchema(record_access_dict) - return { - "id": record_access_ui.id, - "title": record_access_ui.title, - "icon": record_access_ui.icon, - "embargo_date": record_access_ui.embargo_date, - "message_class": record_access_ui.message_class, - } diff --git a/invenio_records_marc21/resources/serializers/ui/schema.py b/invenio_records_marc21/resources/serializers/ui/schema.py index 7209e0e3..488085a6 100644 --- a/invenio_records_marc21/resources/serializers/ui/schema.py +++ b/invenio_records_marc21/resources/serializers/ui/schema.py @@ -13,11 +13,11 @@ from functools import partial from flask_babelex import get_locale +from invenio_rdm_records.resources.serializers.ui.fields import AccessStatusField from marshmallow_utils.fields import FormatDate as BaseFormatDatetime from marshmallow_utils.fields import SanitizedUnicode from ..schema import Marc21Schema -from .fields import AccessField FormatDatetime = partial(BaseFormatDatetime, locale=get_locale) @@ -26,7 +26,8 @@ class Marc21UISchema(Marc21Schema): """Schema for dumping extra information for the UI.""" id = SanitizedUnicode(data_key="id", attribute="id") - access = AccessField(attribute="access") + + access_status = AccessStatusField(attribute="access") created = FormatDatetime(attribute="created", format="long") diff --git a/invenio_records_marc21/services/components/access.py b/invenio_records_marc21/services/components/access.py index 86d279ba..4316bd54 100644 --- a/invenio_records_marc21/services/components/access.py +++ b/invenio_records_marc21/services/components/access.py @@ -12,10 +12,11 @@ from enum import Enum +from invenio_rdm_records.records.systemfields.access.field.record import ( + AccessStatusEnum, +) from invenio_records_resources.services.records.components import ServiceComponent -from ...records.systemfields.access import AccessStatusEnum - class AccessComponent(ServiceComponent): """Service component for access integration.""" @@ -25,8 +26,8 @@ def _default_access(self, identity, data, record, **kwargs): default_access = { "access": { "owned_by": [{"user": identity.id}], - "metadata": AccessStatusEnum.PUBLIC.value, - "files": AccessStatusEnum.PUBLIC.value, + "record": AccessStatusEnum.OPEN.value, + "files": AccessStatusEnum.OPEN.value, }, } diff --git a/invenio_records_marc21/services/config.py b/invenio_records_marc21/services/config.py index f1fa7496..d1bc36f7 100644 --- a/invenio_records_marc21/services/config.py +++ b/invenio_records_marc21/services/config.py @@ -18,6 +18,9 @@ SearchOptions, is_record, ) +from invenio_rdm_records.records.systemfields.access.field.record import ( + AccessStatusEnum, +) from invenio_records_resources.services import ( ConditionalLink, FileServiceConfig, @@ -29,7 +32,6 @@ from invenio_records_resources.services.records.links import RecordLink from ..records import Marc21Draft, Marc21Parent, Marc21Record -from ..records.systemfields.access import AccessStatusEnum from .components import AccessComponent, MetadataComponent, PIDComponent from .permissions import Marc21RecordPermissionPolicy from .schemas import Marc21ParentSchema, Marc21RecordSchema @@ -38,7 +40,7 @@ field="access.metadata", label=_("Access status"), value_labels={ - AccessStatusEnum.PUBLIC.value: _("Public"), + AccessStatusEnum.OPEN.value: _("Public"), AccessStatusEnum.EMBARGOED.value: _("Embargoed"), AccessStatusEnum.RESTRICTED.value: _("Restricted"), }, diff --git a/invenio_records_marc21/services/record/metadata.py b/invenio_records_marc21/services/record/metadata.py index bd5d5520..b881ded1 100644 --- a/invenio_records_marc21/services/record/metadata.py +++ b/invenio_records_marc21/services/record/metadata.py @@ -95,7 +95,9 @@ def contains(self, ref_df: DataField, ref_sf: SubField) -> bool: """Return True if record contains reference datafield, which contains reference subfield.""" for df in self.datafields: if ( - df.tag == ref_df.tag and df.ind1 == ref_df.ind1 and df.ind2 == ref_df.ind2 + df.tag == ref_df.tag + and df.ind1 == ref_df.ind1 + and df.ind2 == ref_df.ind2 ): for sf in df.subfields: if sf.code == ref_sf.code and sf.value == ref_sf.value: diff --git a/invenio_records_marc21/services/schemas/__init__.py b/invenio_records_marc21/services/schemas/__init__.py index 50817c11..29cd4812 100644 --- a/invenio_records_marc21/services/schemas/__init__.py +++ b/invenio_records_marc21/services/schemas/__init__.py @@ -9,14 +9,14 @@ # details. """Marc21 record schemas.""" - from invenio_drafts_resources.services.records.schema import ParentSchema +from invenio_rdm_records.services.schemas.access import AccessSchema +from invenio_rdm_records.services.schemas.parent.access import ParentAccessSchema from invenio_records_resources.services.records.schema import BaseRecordSchema from marshmallow.decorators import post_dump from marshmallow.fields import Boolean, Integer, List, Nested, Str from marshmallow_utils.fields import NestedAttribute -from .access import AccessSchema, ParentAccessSchema from .files import FilesSchema from .metadata import MetadataField from .pids import PIDSchema @@ -26,6 +26,10 @@ class Marc21ParentSchema(ParentSchema): """Record schema.""" + field_dump_permissions = { + "access": "manage", + } + access = Nested(ParentAccessSchema) diff --git a/invenio_records_marc21/services/schemas/access/__init__.py b/invenio_records_marc21/services/schemas/access/__init__.py deleted file mode 100644 index fb7746df..00000000 --- a/invenio_records_marc21/services/schemas/access/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- -# -# This file is part of Invenio. -# -# Copyright (C) 2021 Graz University of Technology. -# -# Invenio-Records-Marc21 is free software; you can redistribute it and/or -# modify it under the terms of the MIT License; see LICENSE file for more -# details. - -"""Marc21 parent record schemas.""" - -from .embargo import EmbargoSchema -from .parent import ParentAccessSchema -from .record import AccessSchema - -__all__ = ( - "ParentAccessSchema", - "AccessSchema", - "EmbargoSchema", -) diff --git a/invenio_records_marc21/services/schemas/access/embargo.py b/invenio_records_marc21/services/schemas/access/embargo.py deleted file mode 100644 index 9f6d95ea..00000000 --- a/invenio_records_marc21/services/schemas/access/embargo.py +++ /dev/null @@ -1,53 +0,0 @@ -# -*- coding: utf-8 -*- -# -# This file is part of Invenio. -# -# Copyright (C) 2021 Graz University of Technology. -# -# Invenio-Records-Marc21 is free software; you can redistribute it and/or -# modify it under the terms of the MIT License; see LICENSE file for more -# details. - -"""Marc21 embargo access schemas.""" - -import arrow -from flask_babelex import lazy_gettext as _ -from marshmallow import Schema, ValidationError, validates_schema -from marshmallow.fields import Bool -from marshmallow_utils.fields import ISODateString, SanitizedUnicode - - -class EmbargoSchema(Schema): - """Schema for an embargo on the record.""" - - active = Bool(allow_none=True, missing=None) - until = ISODateString(allow_none=True, missing=None) - reason = SanitizedUnicode(allow_none=True, missing=None) - - @validates_schema - def validate_embargo(self, data, **kwargs): - """Validate that the properties are consistent with each other.""" - if data.get("until") is not None: - until_date = arrow.get(data.get("until")) - else: - until_date = None - - if data.get("active", False): - if until_date is None or until_date < arrow.utcnow(): - raise ValidationError( - _( - "Embargo end date must be set to a future date " - "if active is True." - ), - field_name="until", - ) - - elif data.get("active", None) is not None: - if until_date is not None and until_date > arrow.utcnow(): - raise ValidationError( - _( - "Embargo end date must be unset or in the past " - "if active is False." - ), - field_name="until", - ) diff --git a/invenio_records_marc21/services/schemas/access/parent.py b/invenio_records_marc21/services/schemas/access/parent.py deleted file mode 100644 index 3e5b1681..00000000 --- a/invenio_records_marc21/services/schemas/access/parent.py +++ /dev/null @@ -1,29 +0,0 @@ -# -*- coding: utf-8 -*- -# -# This file is part of Invenio. -# -# Copyright (C) 2021 Graz University of Technology. -# -# Invenio-Records-Marc21 is free software; you can redistribute it and/or -# modify it under the terms of the MIT License; see LICENSE file for more -# details. - -"""Marc21 record ParentAccessSchema.""" - -from flask_babelex import lazy_gettext as _ -from marshmallow import Schema, ValidationError, fields, validates_schema -from marshmallow.fields import Integer, List - -from ....records.systemfields.access import AccessStatusEnum - - -class Agent(Schema): - """An agent schema.""" - - user = Integer(required=True) - - -class ParentAccessSchema(Schema): - """Access schema.""" - - owned_by = List(fields.Nested(Agent)) diff --git a/invenio_records_marc21/services/schemas/access/record.py b/invenio_records_marc21/services/schemas/access/record.py deleted file mode 100644 index 0a5f1fcb..00000000 --- a/invenio_records_marc21/services/schemas/access/record.py +++ /dev/null @@ -1,65 +0,0 @@ -# -*- coding: utf-8 -*- -# -# This file is part of Invenio. -# -# Copyright (C) 2021 Graz University of Technology. -# -# Invenio-Records-Marc21 is free software; you can redistribute it and/or -# modify it under the terms of the MIT License; see LICENSE file for more -# details. - -"""Marc21 services record AccessSchema.""" - -import arrow -from flask_babelex import lazy_gettext as _ -from marshmallow import Schema, ValidationError, validates, validates_schema -from marshmallow.fields import Integer, List, Nested -from marshmallow_utils.fields import NestedAttribute, SanitizedUnicode - -from ....records.systemfields.access import AccessStatusEnum -from .embargo import EmbargoSchema - - -class Agent(Schema): - """An agent schema.""" - - user = Integer(required=True) - - -class AccessSchema(Schema): - """Access schema.""" - - metadata = SanitizedUnicode(required=True) - files = SanitizedUnicode(required=True) - embargo = NestedAttribute(EmbargoSchema) - status = SanitizedUnicode(dump_only=False) - owned_by = List(Nested(Agent)) - - def validate_protection_value(self, value, field_name): - """Check that the protection value is valid.""" - if value not in AccessStatusEnum.list(): - raise ValidationError( - _("'{}' must be either '{}', '{}' or '{}'").format( - field_name, - *AccessStatusEnum.list(), - ), - field_name, - ) - - def get_attribute(self, obj, key, default): - """Override from where we get attributes when serializing.""" - if key in ["metadata", "files"]: - return getattr(obj.protection, key, default) - elif key == "status": - return obj.status.value - return getattr(obj, key, default) - - @validates("metadata") - def validate_metadata_protection(self, value): - """Validate the metadata protection value.""" - self.validate_protection_value(value, "metadata") - - @validates("files") - def validate_files_protection(self, value): - """Validate the files protection value.""" - self.validate_protection_value(value, "files") diff --git a/invenio_records_marc21/services/services.py b/invenio_records_marc21/services/services.py index 97d9f556..33f76d1c 100644 --- a/invenio_records_marc21/services/services.py +++ b/invenio_records_marc21/services/services.py @@ -10,13 +10,14 @@ """Marc21 Record Service.""" -import arrow from invenio_db import db from invenio_drafts_resources.services.records import RecordService +from invenio_rdm_records.records.systemfields.access.field.record import ( + AccessStatusEnum, +) from invenio_records_resources.services.files.service import FileService from invenio_records_resources.services.records.results import RecordItem -from ..records.systemfields.access import AccessStatusEnum from .config import ( Marc21DraftFilesServiceConfig, Marc21RecordFilesServiceConfig, @@ -46,8 +47,8 @@ def _create_data(self, identity, data, metadata, access=None): default_access = { "access": { "owned_by": [{"user": identity.id}], - "metadata": AccessStatusEnum.PUBLIC.value, - "files": AccessStatusEnum.PUBLIC.value, + "metadata": AccessStatusEnum.OPEN.value, + "files": AccessStatusEnum.OPEN.value, }, } if access is not None: @@ -91,7 +92,7 @@ def update_draft( def _lift_embargo_from(self, record): """Lifts embargo from record or draft.""" - if not record.access.embargo.lift(): + if not record.access.lift_embargo(): raise EmbargoNotLiftedError(record["id"]) record.access.protection.metadata = "public" record.access.protection.files = "public" diff --git a/invenio_records_marc21/templates/invenio_records_marc21/record.html b/invenio_records_marc21/templates/invenio_records_marc21/record.html index b43315b8..e8aee0cf 100644 --- a/invenio_records_marc21/templates/invenio_records_marc21/record.html +++ b/invenio_records_marc21/templates/invenio_records_marc21/record.html @@ -38,11 +38,11 @@
-
+
- {{ record.ui.access.title }} + {{ record.ui.access_status.title_l10n }}
-
{{ record.ui.access.description }}

+
{{ record.ui.access_status.description_l10n }}

{% if record.access.embargo.reason %}

{{_("Reason")}}: {{record.access.embargo.reason}}

{% endif%} diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/components.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/components.js index 7dce9795..0f7d2d93 100644 --- a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/components.js +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/components.js @@ -7,6 +7,9 @@ // details. import React, { useState } from "react"; +import ReactDOM from "react-dom"; +import _camelCase from "lodash/camelCase"; +import _truncate from "lodash/truncate"; import { Button, Card, @@ -19,14 +22,10 @@ import { } from "semantic-ui-react"; import { BucketAggregation } from "react-searchkit"; import _get from "lodash/get"; -import _truncate from "lodash/truncate"; +import { loadComponents } from "@js/invenio_theme/templates"; import Overridable from "react-overridable"; import { SearchBar, SearchApp } from "@js/invenio_search_ui/components"; -import _camelCase from "lodash/camelCase"; -import { loadComponents } from "@js/invenio_theme/templates"; -import ReactDOM from "react-dom"; - export const Marc21RecordResultsListItem = ({ result, index }) => { const createdDate = _get( @@ -40,7 +39,7 @@ export const Marc21RecordResultsListItem = ({ result, index }) => { "ui.updated", "No update date found." ); - const access = _get(result, ["ui", "access"], []); + const access = _get(result, ["ui", "access_status"], []); const access_id = _get(access, "id", "public"); const access_status = _get(access, "title", "Public"); const access_icon = _get(access, "icon", "unlock"); diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/index.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/index.js index 34126f9d..f65fdc2b 100644 --- a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/index.js +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/index.js @@ -1,8 +1,10 @@ -// This file is part of Invenio-Records-Marc21. +// This file is part of Invenio. +// // Copyright (C) 2021 Graz University of Technology. // -// Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or +// modify it under the terms of the MIT License; see LICENSE file for more +// details. import { createSearchAppInit } from "@js/invenio_search_ui"; diff --git a/setup.py b/setup.py index 93fe4b43..2023af2d 100644 --- a/setup.py +++ b/setup.py @@ -53,14 +53,11 @@ ] install_requires = [ - "invenio-i18n>=1.3.0", "arrow>=1.0.0", "dojson>=1.4.0", "lxml>=4.6.2", "invenio-celery>=1.2.0,<2.0.0", - "invenio-records-rest>=1.5.0,<2.0.0", - "invenio-drafts-resources>=0.13.0,<0.14.0", - "invenio-vocabularies>=0.8.0,<0.9.0", + "invenio-rdm-records>=0.32.2,<1.0.0", ] packages = find_packages() From e469d05aede5a5d7d91e80ad87e74ba5990c2852 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Wed, 20 Oct 2021 09:11:55 +0200 Subject: [PATCH 119/217] modification: update testset updating test set to new access systemfield and remove double tests --- tests/conftest.py | 26 ++- .../systemfields/test_systemfield_access.py | 102 +-------- tests/records/test-record.json | 7 +- .../resources/serializers/test_serializer.py | 16 +- .../components/test_component_access.py | 10 +- tests/services/schemas/access/test_record.py | 24 +- tests/services/schemas/test_access.py | 208 ------------------ tests/services/test_create_record.py | 24 +- tests/services/test_record_service.py | 6 +- tests/test_invenio_records_cli.py | 6 +- 10 files changed, 67 insertions(+), 362 deletions(-) delete mode 100644 tests/services/schemas/test_access.py diff --git a/tests/conftest.py b/tests/conftest.py index 0788da83..103258cf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -31,16 +31,20 @@ def embargoed_record(): embargoed_record = { "metadata": {"json": "test"}, "access": { + "record": "restricted", "files": "restricted", "status": "embargoed", "embargo": { "active": True, - "until": (arrow.utcnow().datetime + timedelta(days=2)).strftime( + "until": (arrow.utcnow().datetime + timedelta(days=-365)).strftime( "%Y-%m-%d" ), "reason": None, }, }, + "files": { + "enabled": False, # Most tests don't care about files + }, } return embargoed_record @@ -53,12 +57,30 @@ def marc21_record(): "access": { "files": "public", "status": "public", - "metadata": "public", + "record": "public", "embargo": { "active": False, "reason": None, }, }, + "files": { + "enabled": True, + "total_size": 1114324524355, + "count": 1, + "bucket": "81983514-22e5-473a-b521-24254bd5e049", + "default_preview": "big-dataset.zip", + "order": ["big-dataset.zip"], + "entries": { + "big-dataset.zip": { + "checksum": "md5:234245234213421342", + "mimetype": "application/zip", + "size": 1114324524355, + "key": "big-dataset.zip", + "file_id": "445aaacd-9de1-41ab-af52-25ab6cb93df7", + } + }, + "meta": {"big-dataset.zip": {"description": "File containing the data."}}, + }, } return marc21_record diff --git a/tests/records/systemfields/test_systemfield_access.py b/tests/records/systemfields/test_systemfield_access.py index bb157fb4..9b538fd0 100644 --- a/tests/records/systemfields/test_systemfield_access.py +++ b/tests/records/systemfields/test_systemfield_access.py @@ -14,33 +14,7 @@ from datetime import timedelta import arrow -import pytest - -from invenio_records_marc21.records import Marc21Record -from invenio_records_marc21.records.systemfields.access import ( - Embargo, - Protection, - RecordAccess, -) - -# -# Protection -# - - -def test_protection_valid(): - p = Protection("public", "public") - assert p.metadata == "public" - assert p.files == "public" - p.set("restricted", files="restricted") - assert p.metadata == "restricted" - assert p.files == "restricted" - - -def test_protection_invalid_values(): - with pytest.raises(ValueError): - Protection("invalid", "values") - +from invenio_rdm_records.records.systemfields.access.field.record import Embargo # # Embargo @@ -86,77 +60,7 @@ def test_embargo_lift(): new_embargo = Embargo.from_dict(embargo_dict1) old_embargo = Embargo.from_dict(embargo_dict2) - assert old_embargo.lift() + assert old_embargo._lift() assert not old_embargo.active - assert not new_embargo.lift() + assert not new_embargo._lift() assert new_embargo.active - - -# -# Record Access System Field -# - - -def test_access_field_on_record(appaccess, marc21_record, parent): - future_date = arrow.utcnow().datetime + timedelta(days=+30) - marc21_record["access"]["embargo"] = { - "until": future_date.strftime("%Y-%m-%d"), - "active": True, - "reason": "nothing in particular", - } - rec = Marc21Record.create(marc21_record, parent=parent) - - assert isinstance(rec.access, RecordAccess) - assert isinstance(rec.access.protection, Protection) - assert rec.access.protection.metadata == marc21_record["access"]["metadata"] - assert rec.access.protection.files == marc21_record["access"]["files"] - assert isinstance(rec.access.embargo, Embargo) - - -def test_access_field_update_embargo(appaccess, marc21_record, parent): - future_date = arrow.utcnow().datetime + timedelta(days=+30) - marc21_record["access"]["embargo"] = { - "until": future_date.strftime("%Y-%m-%d"), - "active": True, - "reason": "Because I can", - } - rec = Marc21Record.create(marc21_record.copy(), parent=parent) - - assert rec.access.embargo - assert rec.access.embargo.active - assert rec.access.embargo.reason == "Because I can" - - rec.access.embargo.active = False - rec.access.embargo.reason = "can't remember" - rec.commit() - - assert not rec["access"]["embargo"]["active"] - assert rec["access"]["embargo"]["reason"] == "can't remember" - - -def test_access_field_clear_embargo(appaccess, marc21_record, parent): - future_date = arrow.utcnow().datetime + timedelta(days=+30) - marc21_record["access"]["embargo"] = { - "until": future_date.strftime("%Y-%m-%d"), - "active": True, - "reason": "nothing in particular", - } - rec = Marc21Record.create(marc21_record, parent=parent) - - rec.access.embargo.clear() - assert not rec.access.embargo - - -def test_access_field_update_protection(appaccess, marc21_record, parent): - marc21_record["access"]["metadata"] = "restricted" - marc21_record["access"]["files"] = "restricted" - - rec = Marc21Record.create(marc21_record, parent=parent) - assert rec.access.protection.metadata == "restricted" - assert rec.access.protection.files == "restricted" - - rec.access.protection.set("public", "public") - rec.commit() - - assert rec["access"]["metadata"] == "public" - assert rec["access"]["files"] == "public" diff --git a/tests/records/test-record.json b/tests/records/test-record.json index 12d5c6d5..35c21bf7 100644 --- a/tests/records/test-record.json +++ b/tests/records/test-record.json @@ -1,11 +1,6 @@ { "access": { - "owned_by": [ - { - "user": 1 - } - ], - "metadata": "public", + "record": "public", "files": "public" }, "metadata": { diff --git a/tests/resources/serializers/test_serializer.py b/tests/resources/serializers/test_serializer.py index f865b6ee..b08b7bdf 100644 --- a/tests/resources/serializers/test_serializer.py +++ b/tests/resources/serializers/test_serializer.py @@ -33,8 +33,8 @@ def test_marcxml_serializer_init(): def test_marcxml_serializer_serialize_object(full_record): marc = Marc21XMLSerializer() - test_keys = ["metadata", "access", "id", "files"] - exept_keys = ["pid"] + test_keys = ["metadata", "id", "files"] + exept_keys = ["pid", "access"] obj = marc.serialize_object(full_record) assert isinstance(obj, str) _test_key_in_serialized_obj(obj, test_keys, exept_keys) @@ -43,8 +43,8 @@ def test_marcxml_serializer_serialize_object(full_record): def test_marcxml_serializer_serialize_object_list(list_records): - test_keys = ["metadata", "access", "id", "files"] - exept_keys = ["pid"] + test_keys = ["metadata", "id", "files"] + exept_keys = ["pid", "access"] marc = Marc21XMLSerializer() obj_list = marc.serialize_object_list(list_records) assert isinstance(obj_list, str) @@ -62,8 +62,8 @@ def test_json_serializer_init(): def test_json_serializer_serialize_object(full_record): - test_keys = ["metadata", "access", "id", "files"] - exept_keys = ["pid"] + test_keys = ["metadata", "id", "files"] + exept_keys = ["pid", "access"] marc = Marc21JSONSerializer() obj = marc.serialize_object(full_record) assert isinstance(obj, str) @@ -71,8 +71,8 @@ def test_json_serializer_serialize_object(full_record): def test_json_serializer_serialize_object_list(list_records): - test_keys = ["metadata", "access", "id", "files"] - exept_keys = ["pid"] + test_keys = ["metadata", "id", "files"] + exept_keys = ["pid", "access"] marc = Marc21JSONSerializer() obj_list = marc.serialize_object_list(list_records) assert isinstance(obj_list, str) diff --git a/tests/services/components/test_component_access.py b/tests/services/components/test_component_access.py index 140016e5..a4ee4818 100644 --- a/tests/services/components/test_component_access.py +++ b/tests/services/components/test_component_access.py @@ -29,7 +29,7 @@ def test_component_access_default_access(parent, identity_simple): component.create(identity=identity_simple, data=marc21_record, record=record) prot = record.access.protection - assert prot.metadata == "public" + assert prot.record == "public" assert prot.files == "public" assert len(record.parent.access.owners) > 0 @@ -41,7 +41,7 @@ def test_component_access_update_access_via_json( ): next_year = arrow.utcnow().datetime + timedelta(days=+365) restricted_access = { - "metadata": "restricted", + "record": "restricted", "files": "restricted", "embargo": { "until": next_year.strftime("%Y-%m-%d"), @@ -58,12 +58,12 @@ def test_component_access_update_access_via_json( prot = record.access.protection assert record.access.embargo is not None assert "embargo" in record["access"] - assert prot.metadata == record["access"]["metadata"] == "restricted" + assert prot.record == record["access"]["record"] == "restricted" assert prot.files == record["access"]["files"] == "restricted" new_data = marc21_record.copy() new_data["access"] = { - "metadata": "public", + "record": "public", "files": "public", } component.create(identity_simple, new_data, record) @@ -71,5 +71,5 @@ def test_component_access_update_access_via_json( prot = record.access.protection assert not record.access.embargo assert "embargo" not in record["access"] - assert prot.metadata == record["access"]["metadata"] == "public" + assert prot.record == record["access"]["record"] == "public" assert prot.files == record["access"]["files"] == "public" diff --git a/tests/services/schemas/access/test_record.py b/tests/services/schemas/access/test_record.py index 44c506b0..00c41465 100644 --- a/tests/services/schemas/access/test_record.py +++ b/tests/services/schemas/access/test_record.py @@ -16,16 +16,14 @@ import arrow import pytest from flask_babelex import lazy_gettext as _ +from invenio_rdm_records.services.schemas.access import AccessSchema from marshmallow import ValidationError from marshmallow.exceptions import ValidationError -from invenio_records_marc21.services.schemas.access import AccessSchema - def test_valid_full(): valid_full = { - "metadata": "embargoed", - "owned_by": [{"user": 1}], + "record": "restricted", "embargo": { "until": (arrow.utcnow().datetime + timedelta(days=2)).strftime("%Y-%m-%d"), "active": True, @@ -36,20 +34,19 @@ def test_valid_full(): assert valid_full == AccessSchema().load(valid_full) -@pytest.mark.parametrize("value", ["public", "embargoed", "restricted"]) +@pytest.mark.parametrize("value", ["public", "restricted"]) def test_valid_metadata_protection(value): - assert AccessSchema().validate_metadata_protection(value) is None + assert AccessSchema().validate_record_protection(value) is None def test_invalid_metadata_protection(): with pytest.raises(ValidationError) as e: - AccessSchema().validate_metadata_protection("invalid") - assert e.value.messages[0] == _( - "'metadata' must be either 'public', 'embargoed' or 'restricted'") - assert e.value.field_name == "metadata" + AccessSchema().validate_record_protection("invalid") + assert e.value.messages[0] == _("'record' must be either 'public' or 'restricted'") + assert e.value.field_name == "record" -@pytest.mark.parametrize("value", ["public", "embargoed", "restricted"]) +@pytest.mark.parametrize("value", ["public", "restricted"]) def test_valid_files_protection(value): assert AccessSchema().validate_files_protection(value) is None @@ -57,6 +54,5 @@ def test_valid_files_protection(value): def test_invalid_files_protection(): with pytest.raises(ValidationError) as e: AccessSchema().validate_files_protection("invalid") - assert e.value.messages[0] == _( - "'files' must be either 'public', 'embargoed' or 'restricted'") - assert e.value.field_name == "files" + assert e.value.messages[0] == _("'files' must be either 'public' or 'restricted'") + assert e.value.field_name == "record" diff --git a/tests/services/schemas/test_access.py b/tests/services/schemas/test_access.py deleted file mode 100644 index 10e7e625..00000000 --- a/tests/services/schemas/test_access.py +++ /dev/null @@ -1,208 +0,0 @@ -# -*- coding: utf-8 -*- -# -# This file is part of Invenio. -# -# Copyright (C) 2021 Graz University of Technology. -# -# Invenio-Records-Marc21 is free software; you can redistribute it and/or -# modify it under the terms of the MIT License; see LICENSE file for more -# details. - -"""Test record AccessSchema.""" - - -from datetime import timedelta - -import arrow -import pytest -from flask_babelex import lazy_gettext as _ -from marshmallow import ValidationError -from marshmallow.exceptions import ValidationError - -from invenio_records_marc21.services.schemas.access import AccessSchema, EmbargoSchema - - -def _assert_raises_messages(lambda_expression, expected_messages): - with pytest.raises(ValidationError) as e: - lambda_expression() - - messages = e.value.normalized_messages() - assert expected_messages == messages - - -def test_valid_full(): - valid_full = { - "metadata": "embargoed", - "owned_by": [{"user": 1}], - "embargo": { - "until": (arrow.utcnow().datetime + timedelta(days=2)).strftime("%Y-%m-%d"), - "active": True, - "reason": "Because I can!", - }, - "files": "public", - } - assert valid_full == AccessSchema().load(valid_full) - - -def test_invalid_access_right(): - invalid_access_right = { - "files": "public", - "owned_by": [{"user": 1}], - "metadata": "invalid value", - } - - _assert_raises_messages( - lambda: AccessSchema().load(invalid_access_right), - { - "metadata": [ - _("'metadata' must be either 'public', 'embargoed' or 'restricted'") - ] - }, - ) - - -@pytest.mark.parametrize( - "invalid_access,missing_attr", - [ - ( - { - "metadata": "public", - "files": "public", - "owned_by": [1], - }, - "owned_by", - ), - ( - {"metadata": "public", "owned_by": [{"user": 1}]}, - "files", - ), - ( - {"owned_by": [{"user": 1}], "files": "public"}, - "metadata", - ), - ], -) -def test_invalid(invalid_access, missing_attr): - - with pytest.raises(ValidationError) as e: - AccessSchema().load(invalid_access) - - error_fields = e.value.messages.keys() - assert len(error_fields) == 1 - assert missing_attr in error_fields - - -def test_embargo_load_no_until_is_valid(): - expected = {"active": False, "until": None, "reason": None} - - valid_no_until = { - "active": False, - } - assert expected == EmbargoSchema().load(valid_no_until) - - valid_no_until = { - "active": False, - "until": None, - } - assert expected == EmbargoSchema().load(valid_no_until) - - -def test_embargo_dump_no_until_is_valid(): - valid_no_until = { - "active": False, - } - assert valid_no_until == EmbargoSchema().dump(valid_no_until) - - expected = { - "active": False, - } - valid_no_until = { - "active": False, - "until": None, - } - assert expected == EmbargoSchema().dump(valid_no_until) - - -@pytest.mark.parametrize( - "invalid_access,invalid_attr", - [ - ( - { - "files": "restricted", - "embargo": { - "active": True, - "until": (arrow.utcnow().datetime + timedelta(days=2)).strftime("%Y-%m-%d"), - "reason": "Because I can!", - }, - }, - "metadata", - ), - ( - { - "metadata": "public", - "embargo": { - "active": True, - "until": (arrow.utcnow().datetime + timedelta(days=2)).strftime("%Y-%m-%d"), - "reason": "Because I can!", - }, - }, - "files", - ), - ( - { - "metadata": "public", - "files": "restricted", - "embargo": { - "active": False, - "until": (arrow.utcnow().datetime + timedelta(days=2)).strftime("%Y-%m-%d"), - "reason": "Because I can!", - }, - }, - "embargo", - ), - ( - { - "metadata": "public", - "files": "restricted", - "embargo": { - "active": True, - "until": (arrow.utcnow().datetime + timedelta(days=-20)).strftime("%Y-%m-%d"), - "reason": "Because I can!", - }, - }, - "embargo", - ), - ( - { - "metadata": "invalid", - "files": "restricted", - "embargo": { - "active": False, - "until": (arrow.utcnow().datetime + timedelta(days=-20)).strftime("%Y-%m-%d"), - "reason": "Because I can!", - }, - }, - "metadata", - ), - ( - { - "metadata": "public", - "files": "invalid", - "embargo": { - "active": False, - "until": (arrow.utcnow().datetime + timedelta(days=-20)).strftime("%Y-%m-%d"), - "reason": "Because I can!", - }, - }, - "files", - ), - ], -) -def test_invalid(invalid_access, invalid_attr): - - with pytest.raises(ValidationError) as e: - AccessSchema().load(invalid_access) - - error_fields = e.value.messages.keys() - assert len(error_fields) == 1 - assert invalid_attr in error_fields diff --git a/tests/services/test_create_record.py b/tests/services/test_create_record.py index 1b8f8c7d..27a62de7 100644 --- a/tests/services/test_create_record.py +++ b/tests/services/test_create_record.py @@ -78,28 +78,29 @@ def empty_data(): [ { "input": { - "metadata": "public", + "record": "public", }, "expect": { "access": { - "metadata": "public", + "record": "public", "files": "public", "embargo": { "active": False, "reason": None, }, - "status": "public", + "status": "metadata-only", }, }, }, { "input": { - "metadata": "restricted", + "record": "restricted", + "files": "restricted", }, "expect": { "access": { "files": "restricted", - "metadata": "restricted", + "record": "restricted", "embargo": { "active": False, "reason": None, @@ -110,7 +111,7 @@ def empty_data(): }, { "input": { - "metadata": "embargoed", + "record": "restricted", "embargo": { "until": (arrow.utcnow().datetime + timedelta(days=2)).strftime( "%Y-%m-%d" @@ -122,7 +123,7 @@ def empty_data(): "expect": { "access": { "files": "public", - "metadata": "embargoed", + "record": "public", "embargo": { "until": (arrow.utcnow().datetime + timedelta(days=2)).strftime( "%Y-%m-%d" @@ -148,12 +149,3 @@ def test_create_with_access(running_app, empty_data, access): record.data, access["expect"], ) - _assert_fields( - ["access"], - record.data["parent"], - { - "access": { - "owned_by": [{"user": 1}], - } - }, - ) diff --git a/tests/services/test_record_service.py b/tests/services/test_record_service.py index 6fcb23ee..47fd3a51 100644 --- a/tests/services/test_record_service.py +++ b/tests/services/test_record_service.py @@ -206,10 +206,11 @@ def test_embargo_lift_without_draft(mock_arrow, running_app, marc21_record): assert not record_lifted.access.embargo.active assert record_lifted.access.protection.files == "public" - assert record_lifted.access.protection.metadata == "public" - assert record_lifted.access.status.value == "public" + assert record_lifted.access.protection.record == "public" + assert record_lifted.access.status.value == "metadata-only" +@pytest.mark.skip("Cannot be tested yet!") @mock.patch("arrow.utcnow") def test_embargo_lift_with_draft(mock_arrow, running_app, marc21_record): identity_simple = running_app.identity_simple @@ -242,6 +243,7 @@ def test_embargo_lift_with_draft(mock_arrow, running_app, marc21_record): assert draft_lifted.access.protection.metadata == "public" +@pytest.mark.skip("Cannot be tested yet!") @mock.patch("arrow.utcnow") def test_embargo_lift_with_updated_draft(mock_arrow, running_app, marc21_record): identity_simple = running_app.identity_simple diff --git a/tests/test_invenio_records_cli.py b/tests/test_invenio_records_cli.py index 300107c8..988ef23f 100644 --- a/tests/test_invenio_records_cli.py +++ b/tests/test_invenio_records_cli.py @@ -14,6 +14,9 @@ from datetime import timedelta import arrow +from invenio_rdm_records.records.systemfields.access.field.record import ( + AccessStatusEnum, +) from invenio_records_marc21.cli import ( create_fake_metadata, @@ -23,7 +26,6 @@ marc21, system_identity, ) -from invenio_records_marc21.records.systemfields.access import AccessStatusEnum def test_system_identity(): @@ -36,7 +38,7 @@ def test_system_identity(): def test_fake_access_right(): """Test random access right for demo.""" access = fake_access_right() - assert access in AccessStatusEnum.list() + assert access in list(map(lambda c: c.value, AccessStatusEnum)) def test_fake_feature_date(): From 7e1f3c37b12e6f09c00ec2445abf5dae9794e2a9 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Wed, 20 Oct 2021 10:24:59 +0200 Subject: [PATCH 120/217] build: reducing ci build time --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 2023af2d..3bc8e8f8 100644 --- a/setup.py +++ b/setup.py @@ -57,6 +57,7 @@ "dojson>=1.4.0", "lxml>=4.6.2", "invenio-celery>=1.2.0,<2.0.0", + "invenio-drafts-resources>=0.14.1", "invenio-rdm-records>=0.32.2,<1.0.0", ] From 3e6ef463ce43ccd2e4de9f6e2c57e730c1cb7fdd Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Wed, 20 Oct 2021 10:34:38 +0200 Subject: [PATCH 121/217] codestyle: format code --- invenio_records_marc21/services/record/metadata.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/invenio_records_marc21/services/record/metadata.py b/invenio_records_marc21/services/record/metadata.py index b881ded1..360dd380 100644 --- a/invenio_records_marc21/services/record/metadata.py +++ b/invenio_records_marc21/services/record/metadata.py @@ -95,10 +95,8 @@ def contains(self, ref_df: DataField, ref_sf: SubField) -> bool: """Return True if record contains reference datafield, which contains reference subfield.""" for df in self.datafields: if ( - df.tag == ref_df.tag - and df.ind1 == ref_df.ind1 - and df.ind2 == ref_df.ind2 - ): + df.tag == ref_df.tag and df.ind1 == ref_df.ind1 + ) and df.ind2 == ref_df.ind2: for sf in df.subfields: if sf.code == ref_sf.code and sf.value == ref_sf.value: return True From e81aed2ca6027ba51e26c1a6fe1f2350793d2103 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Thu, 21 Oct 2021 07:44:11 +0200 Subject: [PATCH 122/217] codestyle: prettify jsavascript applying prettier formater --- .../deposit/Marc21ApiHandler.js | 32 +++--- .../deposit/Marc21Controller.js | 62 ++++------ .../deposit/Marc21DepositApp.js | 24 ++-- .../deposit/Marc21DepositForm.js | 101 ++++++++--------- .../deposit/Marc21FormHandler.js | 31 +++-- ...{RecordSchema.js => Marc21RecordSchema.js} | 64 +++++------ .../deposit/Marc21RecordSerializer.js | 77 ++++++------- .../deposit/components/AccordionField.js | 16 +-- .../deposit/components/LeaderField.js | 28 ++--- .../deposit/components/MetadataField.js | 32 ++---- .../deposit/components/MetadataFields.js | 55 ++++----- .../deposit/components/PublishButton.js | 25 ++-- .../deposit/components/SaveButton.js | 15 ++- .../deposit/components/TemplateField.js | 73 ++++++------ .../deposit/components/index.js | 12 +- .../deposit/fields/Field.js | 8 +- .../deposit/fields/MetadataFields.js | 107 +++++++++--------- .../deposit/fields/index.js | 4 +- .../invenio_records_marc21/deposit/index.js | 21 ++-- .../deposit/state/actions/deposit.js | 43 ++++--- .../deposit/state/actions/index.js | 3 +- .../deposit/state/reducers/deposit.js | 88 +++++++------- .../deposit/state/reducers/index.js | 4 +- .../deposit/state/types/index.js | 18 +-- .../invenio_records_marc21/deposit/store.js | 47 ++++---- .../search/components.js | 44 +++---- .../js/invenio_records_marc21/search/index.js | 2 - 27 files changed, 477 insertions(+), 559 deletions(-) rename invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/{RecordSchema.js => Marc21RecordSchema.js} (82%) diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21ApiHandler.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21ApiHandler.js index 4935c08c..6ae1ff4e 100644 --- a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21ApiHandler.js +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21ApiHandler.js @@ -5,13 +5,13 @@ // Invenio-Records-Marc21 is free software; you can redistribute it and/or // modify it under the terms of the MIT License; see LICENSE file for more // details. -import axios from 'axios'; +import axios from "axios"; const CancelToken = axios.CancelToken; const apiConfig = { withCredentials: true, - xsrfCookieName: 'csrftoken', - xsrfHeaderName: 'X-CSRFToken', + xsrfCookieName: "csrftoken", + xsrfHeaderName: "X-CSRFToken", }; const axiosWithconfig = axios.create(apiConfig); @@ -36,7 +36,7 @@ export class Marc21ApiResponse { * It mostly uses the API links passed to it from responses. * */ -export class Marc21ApiHandler{ +export class Marc21ApiHandler { constructor(createUrl) { this.createUrl = createUrl; } @@ -64,12 +64,12 @@ export class Marc21ApiHandler{ * * @param {object} draft - Serialized draft */ - async create(draft) { + async create(draft) { return this.createResponse(() => axiosWithconfig.post(this.createUrl, draft, { headers: { - 'Content-Type': 'application/json', - Accept: 'application/vnd.inveniomarc21.v1+marcxml', + "Content-Type": "application/json", + Accept: "application/vnd.inveniomarc21.v1+marcxml", }, }) ); @@ -84,8 +84,8 @@ export class Marc21ApiHandler{ return this.createResponse(() => axiosWithconfig.put(draft.links.self, draft, { headers: { - 'Content-Type': 'application/json', - Accept: 'application/vnd.inveniomarc21.v1+marcxml', + "Content-Type": "application/json", + Accept: "application/vnd.inveniomarc21.v1+marcxml", }, }) ); @@ -98,14 +98,12 @@ export class Marc21ApiHandler{ */ async publish(draft) { return this.createResponse(() => - axiosWithconfig.post( - draft.links.publish, draft, { - headers: { - 'Content-Type': 'application/json', - Accept: 'application/vnd.inveniomarc21.v1+marcxml', + axiosWithconfig.post(draft.links.publish, draft, { + headers: { + "Content-Type": "application/json", + Accept: "application/vnd.inveniomarc21.v1+marcxml", }, - }, - ) + }) ); } @@ -120,7 +118,7 @@ export class Marc21ApiHandler{ draft.links.self, {}, { - headers: { 'Content-Type': 'application/json' }, + headers: { "Content-Type": "application/json" }, } ) ); diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21Controller.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21Controller.js index 76e4f46b..9bd2dc7c 100644 --- a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21Controller.js +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21Controller.js @@ -7,25 +7,22 @@ // details. import { - - ACTION_CREATE_SUCCEEDED, - ACTION_SAVE_SUCCEEDED, - ACTION_SAVE_FAILED, - ACTION_PUBLISH_FAILED, - ACTION_PUBLISH_SUCCEEDED - } from './state/types'; -import _isEmpty from 'lodash/isEmpty'; -import _set from 'lodash/set'; -import _has from 'lodash/has'; + ACTION_CREATE_SUCCEEDED, + ACTION_SAVE_SUCCEEDED, + ACTION_SAVE_FAILED, + ACTION_PUBLISH_FAILED, + ACTION_PUBLISH_SUCCEEDED, +} from "./state/types"; +import _isEmpty from "lodash/isEmpty"; +import _set from "lodash/set"; +import _has from "lodash/has"; export class Marc21Controller { - constructor(apihandler, schema) { - this.apihandler = apihandler; - this.schema = schema; - } - + constructor(apihandler, schema) { + this.apihandler = apihandler; + this.schema = schema; + } - /** * Creates the current draft (backend) and changes URL to match its edit URL. * @@ -36,21 +33,17 @@ export class Marc21Controller { const recordSerializer = store.config.recordSerializer; const payload = recordSerializer.serialize(draft); const response = await this.apihandler.create(payload); - - if(_has(response, ["data","ui", "metadata"])){ - _set( - response.data, - 'metadata', - response.data.ui.metadata, - ); + + if (_has(response, ["data", "ui", "metadata"])) { + _set(response.data, "metadata", response.data.ui.metadata); } store.dispatch({ type: ACTION_CREATE_SUCCEEDED, payload: { data: recordSerializer.deserialize(response.data) }, }); - + const draftURL = response.data.links.self_html; - window.history.replaceState(undefined, '', draftURL); + window.history.replaceState(undefined, "", draftURL); return response; } @@ -70,7 +63,7 @@ export class Marc21Controller { // Set defaultPreview for files draft = _set( draft, - 'defaultFilePreview', + "defaultFilePreview", store.getState().deposit.defaultFilePreview ); @@ -79,19 +72,15 @@ export class Marc21Controller { response = await this.createDraft(draft, { store }); //response = await this.apihandler.save(payload); - if(_has(response, ["data","ui", "metadata"])){ - _set( - response.data, - "metadata", - response.data.ui.metadata, - ); + if (_has(response, ["data", "ui", "metadata"])) { + _set(response.data, "metadata", response.data.ui.metadata); } let data = recordSerializer.deserialize(response.data || {}); let errors = recordSerializer.deserializeErrors(response.errors || []); // response 100% successful - if ( 200 <= response.code && response.code < 300 && _isEmpty(errors) ) { + if (200 <= response.code && response.code < 300 && _isEmpty(errors)) { store.dispatch({ type: ACTION_SAVE_SUCCEEDED, payload: { data }, @@ -116,7 +105,7 @@ export class Marc21Controller { * @param {object} formik - the Formik object * @param {object} store - redux store */ - async publishDraft(draft, { formik, store }) { + async publishDraft(draft, { formik, store }) { const recordSerializer = store.config.recordSerializer; let response = {}; @@ -126,7 +115,7 @@ export class Marc21Controller { let payload = recordSerializer.serialize(draft); response = await this.apihandler.publish(payload); - + let data = recordSerializer.deserialize(response.data || {}); let errors = recordSerializer.deserializeErrors(response.errors || []); @@ -150,5 +139,4 @@ export class Marc21Controller { formik.setSubmitting(false); } - -} \ No newline at end of file +} diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21DepositApp.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21DepositApp.js index 3b4ad3ca..00036874 100644 --- a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21DepositApp.js +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21DepositApp.js @@ -7,24 +7,23 @@ // details. import React, { Component } from "react"; -import { Provider } from 'react-redux'; +import { Provider } from "react-redux"; import { configureStore } from "./store"; -import {RecordSchema} from "./RecordSchema"; -import { Marc21Controller } from './Marc21Controller'; -import {Marc21ApiHandler} from './Marc21ApiHandler'; -import {Marc21FormHandler} from './Marc21FormHandler' -import { Marc21RecordSerializer } from './Marc21RecordSerializer'; - +import { Marc21RecordSchema } from "./Marc21RecordSchema"; +import { Marc21Controller } from "./Marc21Controller"; +import { Marc21ApiHandler } from "./Marc21ApiHandler"; +import { Marc21FormHandler } from "./Marc21FormHandler"; +import { Marc21RecordSerializer } from "./Marc21RecordSerializer"; export class Marc21DepositApp extends Component { constructor(props) { super(props); - const fileUploader = props.fileUploader + const fileUploader = props.fileUploader; const schema = props.schema ? props.schema - : new RecordSchema(props.config.link, props.config.schema); + : new Marc21RecordSchema(props.config.link, props.config.schema); const apihandler = new Marc21ApiHandler(props.config.createUrl); @@ -36,7 +35,7 @@ export class Marc21DepositApp extends Component { ? props.controller : new Marc21Controller(apihandler, schema); - this.record_init = recordSerializer.deserialize(props.record) + this.record_init = recordSerializer.deserialize(props.record); const appConfig = { config: props.config, @@ -51,9 +50,8 @@ export class Marc21DepositApp extends Component { }; const submitFormData = props.submitFormData - ? props.submitFormData - : submitFormData; - + ? props.submitFormData + : submitFormData; this.store = configureStore(appConfig); } diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21DepositForm.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21DepositForm.js index 5b1e8139..6978e375 100644 --- a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21DepositForm.js +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21DepositForm.js @@ -7,24 +7,22 @@ // details. import _get from "lodash/get"; -import React, { Component, createRef } from "react"; -import {SaveButton, PublishButton} from "./components" -import {Marc21DepositApp} from "./Marc21DepositApp"; +import React, { Component, createRef } from "react"; +import { SaveButton, PublishButton } from "./components"; +import { Marc21DepositApp } from "./Marc21DepositApp"; import { AccordionField, MetadataFields } from "./components"; import { Card, Container, Grid, Ref, Sticky } from "semantic-ui-react"; import { TemplateField } from "./components/TemplateField"; - export class Marc21DepositForm extends Component { - - constructor(props) { + constructor(props) { super(props); - this.props = props + this.props = props; this.config = props.config || {}; - this.templates=props.templates || []; - this.files = props.files + this.templates = props.templates || []; + this.files = props.files; this.noFiles = true; - } + } sidebarRef = createRef(); @@ -42,49 +40,48 @@ export class Marc21DepositForm extends Component { templates={this.templates} > - {/* */} - - - - - - - - - {/* Sidebar start */} - - - - -
- -
- -
-
- - {this.templates.length > 0 && - - } - - {/* */} + + + + + + + + {/* Sidebar start */} + + + + +
+ +
+ +
+
+ + {this.templates.length > 0 && ( + + )} + + {/* */} -
-
-
-
-
+ + + + + - ) - } + ); + } } diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21FormHandler.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21FormHandler.js index de4f1cd8..566c65a5 100644 --- a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21FormHandler.js +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21FormHandler.js @@ -6,20 +6,20 @@ // modify it under the terms of the MIT License; see LICENSE file for more // details. -import React, { Component } from 'react'; -import { connect } from 'react-redux'; +import React, { Component } from "react"; +import { connect } from "react-redux"; import { BaseForm } from "react-invenio-forms"; -import { submitFormData } from './state/actions'; +import { submitFormData } from "./state/actions"; class Marc21FormHandlerComponent extends Component { componentDidMount() { - window.addEventListener('beforeunload', (e) => { + window.addEventListener("beforeunload", (e) => { if (this.props.fileUploadOngoing) { - e.returnValue = ''; - return ''; + e.returnValue = ""; + return ""; } }); - window.addEventListener('unload', async (e) => { + window.addEventListener("unload", async (e) => { // TODO: cancel all uploads // Investigate if it's possible to wait for the deletion request to complete // before unloading the page @@ -43,16 +43,15 @@ class Marc21FormHandlerComponent extends Component { } const mapStateToProps = (state) => { - return { - record: state.deposit.record, - formState: state.deposit.formState, - }; + return { + record: state.deposit.record, + formState: state.deposit.formState, }; - - const mapDispatchToProps = (dispatch) => ({ - submitFormData: (values, formik) => dispatch(submitFormData(values, formik)), - }); - +}; + +const mapDispatchToProps = (dispatch) => ({ + submitFormData: (values, formik) => dispatch(submitFormData(values, formik)), +}); export const Marc21FormHandler = connect( mapStateToProps, diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/RecordSchema.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21RecordSchema.js similarity index 82% rename from invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/RecordSchema.js rename to invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21RecordSchema.js index 95b40693..cfbba50e 100644 --- a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/RecordSchema.js +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21RecordSchema.js @@ -8,69 +8,67 @@ import _get from "lodash/get"; import _has from "lodash/has"; -import axios from 'axios'; - -export class RecordSchema { +import axios from "axios"; +export class Marc21RecordSchema { /** * Make sure RecordSchema called first time with props.link */ - constructor(link, schema) { - this.schema = {} - this.link = link; - this.leader_field = "LDR"; - if (schema !== {}){ - this.setSchema(schema); - this.loaded = true; - } else { - this.loaded = false; - } - } + constructor(link, schema) { + this.schema = {}; + this.link = link; + this.leader_field = "LDR"; + if (schema !== {}) { + this.setSchema(schema); + this.loaded = true; + } else { + this.loaded = false; + } + } - isReady(){ + isReady() { return this.loaded; } - setSchema(schema){ + setSchema(schema) { this.schema = schema; } loadSchema() { axios.get(this.state.link).then( - result => { - this.loaded = true; + (result) => { + this.loaded = true; this.schema = result.data; }, // Note: it's important to handle errors here // instead of a catch() block so that we don't swallow // exceptions from actual bugs in components. - error => { - this.loaded = false; + (error) => { + this.loaded = false; this.schema = {}; this.error = error; } ); } - isLeaderField(key){ + isLeaderField(key) { let keys = this.getLeaderFieldKeys(); return keys.includes(key); } - isDataField(key){ + isDataField(key) { let keys = this.getDataFieldKeys(); return keys.includes(key); } hasLeaderKey(key) { - return _has(this.schema, ["fields", this.leader_field, "positions", key]) + return _has(this.schema, ["fields", this.leader_field, "positions", key]); } getLeaderFieldKeys() { - let fields = this.getLeaderFields(); - let keys = [] + let keys = []; for (const [key, value] of Object.entries(fields)) { keys.push(key); } @@ -86,7 +84,6 @@ export class RecordSchema { return leaderfields[key]; } - getLeaderFieldOptions(key) { const leaderfield = this.getLeaderField(key); const leaderfield_codes = _get(leaderfield, ["codes"], {}); @@ -98,10 +95,9 @@ export class RecordSchema { return _get(this.schema, ["fields"], {}); } - getDataFieldKeys() { let fields = this.getDataFields(); - let keys = [] + let keys = []; for (const [key, value] of Object.entries(fields)) { keys.push(key); } @@ -113,10 +109,10 @@ export class RecordSchema { return datafield[key]; } - generateDropdownOptions(fields){ - let codes = [] + generateDropdownOptions(fields) { + let codes = []; for (const [key, value] of Object.entries(fields)) { - let code = { key: key, label: key, value: key, text: value["label"]} + let code = { key: key, label: key, value: key, text: value["label"] }; codes.push(code); } return codes; @@ -129,16 +125,14 @@ export class RecordSchema { * @param {string} ind - indicator key name */ getDataFieldOptions(key, ind) { - let codes = [] + let codes = []; const datafield = this.getDataField(key); if (datafield[ind] === null) { return codes; } const datafield_codes = datafield[ind]["codes"]; codes = this.generateDropdownOptions(datafield_codes); - - + return codes; } - } diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21RecordSerializer.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21RecordSerializer.js index 8173654d..1cb840a3 100644 --- a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21RecordSerializer.js +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21RecordSerializer.js @@ -6,50 +6,42 @@ // modify it under the terms of the MIT License; see LICENSE file for more // details. -import _cloneDeep from 'lodash/cloneDeep'; -import _defaults from 'lodash/defaults'; -import _pick from 'lodash/pick'; -import _set from 'lodash/set'; - -import { - Field, - MetadataFields, - } from './fields'; +import _cloneDeep from "lodash/cloneDeep"; +import _defaults from "lodash/defaults"; +import _pick from "lodash/pick"; +import _set from "lodash/set"; +import { Field, MetadataFields } from "./fields"; export class Marc21RecordSerializer { - constructor() { - } - - depositRecordSchema = { - files: new Field({ - fieldpath: "files", - }), - links: new Field({ - fieldpath: "links", - }), - pids: new Field({ - fieldpath: "pids", - deserializedDefault: {}, - serializedDefault: {}, - }), - metadata: new MetadataFields({ - fieldpath: "metadata", - deserializedDefault: {"leader" : "00000nam a2200000zca4500"}, - serializedDefault : "", - }), - - } + constructor() {} + + depositRecordSchema = { + files: new Field({ + fieldpath: "files", + }), + links: new Field({ + fieldpath: "links", + }), + pids: new Field({ + fieldpath: "pids", + deserializedDefault: {}, + serializedDefault: {}, + }), + metadata: new MetadataFields({ + fieldpath: "metadata", + deserializedDefault: { leader: "00000nam a2200000zca4500" }, + serializedDefault: "", + }), + }; - - /** + /** * Deserialize backend record into format compatible with frontend. * @method * @param {object} record - potentially empty object * @returns {object} frontend compatible record object */ deserialize(record) { - record = _cloneDeep(record); let deserializedRecord = record; @@ -62,14 +54,12 @@ export class Marc21RecordSerializer { "pids", ]); for (let key in this.depositRecordSchema) { - deserializedRecord = this.depositRecordSchema[key].deserialize( - deserializedRecord - ); + deserializedRecord = + this.depositRecordSchema[key].deserialize(deserializedRecord); } return deserializedRecord; } - /** * Deserialize backend record errors into format compatible with frontend. * @method @@ -79,7 +69,7 @@ export class Marc21RecordSerializer { deserializeErrors(errors) { let deserializedErrors = {}; for (let e of errors) { - _set(deserializedErrors, e.field, e.messages.join(' ')); + _set(deserializedErrors, e.field, e.messages.join(" ")); } return deserializedErrors; @@ -104,13 +94,12 @@ export class Marc21RecordSerializer { "pids", ]); for (let key in this.depositRecordSchema) { - serializedRecord = this.depositRecordSchema[key].serialize( - serializedRecord - ); - } + serializedRecord = + this.depositRecordSchema[key].serialize(serializedRecord); + } _defaults(serializedRecord, { metadata: {} }); return serializedRecord; } -} \ No newline at end of file +} diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/components/AccordionField.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/components/AccordionField.js index 1bc70ff0..87a16433 100644 --- a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/components/AccordionField.js +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/components/AccordionField.js @@ -5,10 +5,10 @@ // Invenio-Records-Marc21 is free software; you can redistribute it and/or // modify it under the terms of the MIT License; see LICENSE file for more // details. -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { Field, FastField } from 'formik'; -import { Container, Icon, Segment } from 'semantic-ui-react'; +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import { Field, FastField } from "formik"; +import { Container, Icon, Segment } from "semantic-ui-react"; export class AccordionField extends Component { constructor(props) { @@ -17,11 +17,11 @@ export class AccordionField extends Component { } iconActive = ( - + ); iconInactive = ( - + ); handleClick = (showContent) => { @@ -85,10 +85,10 @@ AccordionField.propTypes = { AccordionField.defaultProps = { active: false, - label: '', + label: "", required: false, ui: { - error: { inverted: true, color: 'red', secondary: true }, + error: { inverted: true, color: "red", secondary: true }, }, optimized: false, }; diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/components/LeaderField.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/components/LeaderField.js index 130a2443..008a7b2a 100644 --- a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/components/LeaderField.js +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/components/LeaderField.js @@ -6,30 +6,24 @@ // modify it under the terms of the MIT License; see LICENSE file for more // details. -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; - -import { - TextField, -} from 'react-invenio-forms'; +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import { TextField } from "react-invenio-forms"; export class LeaderField extends Component { render() { - const { fieldPath} = - this.props; + const { fieldPath } = this.props; return ( <> - + - - ); } } @@ -40,5 +34,5 @@ LeaderField.propTypes = { }; LeaderField.defaultProps = { - helpText: '', + helpText: "", }; diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/components/MetadataField.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/components/MetadataField.js index 6ab2cd31..a9ee9fdf 100644 --- a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/components/MetadataField.js +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/components/MetadataField.js @@ -6,35 +6,21 @@ // modify it under the terms of the MIT License; see LICENSE file for more // details. -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; +import React, { Component } from "react"; +import PropTypes from "prop-types"; -import { - TextField, -} from 'react-invenio-forms'; +import { TextField } from "react-invenio-forms"; export class MetadataField extends Component { render() { - const { fieldPath} = - this.props; + const { fieldPath } = this.props; return ( <> - - - - + + + + - ); } } @@ -46,5 +32,5 @@ MetadataField.propTypes = { }; MetadataField.defaultProps = { - helpText: '', + helpText: "", }; diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/components/MetadataFields.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/components/MetadataFields.js index bf9f63e6..c941a3db 100644 --- a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/components/MetadataFields.js +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/components/MetadataFields.js @@ -6,37 +6,34 @@ // modify it under the terms of the MIT License; see LICENSE file for more // details. -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { ArrayField } from 'react-invenio-forms'; -import { Button, Form, Icon } from 'semantic-ui-react'; -import _get from 'lodash/get'; -import { - GroupField, -} from 'react-invenio-forms'; -import { MetadataField, LeaderField } from '.'; +import React, { Component } from "react"; +import PropTypes from "prop-types"; +import { ArrayField } from "react-invenio-forms"; +import { Button, Form, Icon } from "semantic-ui-react"; +import _get from "lodash/get"; +import { GroupField } from "react-invenio-forms"; +import { MetadataField, LeaderField } from "."; export class MetadataFields extends Component { render() { - const { fieldPath} = - this.props; + const { fieldPath } = this.props; return ( <> - - - - + + + {({ array, arrayHelpers, indexPath, key }) => ( - - - - + + + + )} @@ -45,12 +42,10 @@ export class MetadataFields extends Component { } } - MetadataFields.propTypes = { - fieldPath: PropTypes.string, - }; - + fieldPath: PropTypes.string, +}; + MetadataFields.defaultProps = { - fieldPath: 'metadata', - }; - \ No newline at end of file + fieldPath: "metadata", +}; diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/components/PublishButton.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/components/PublishButton.js index 14fe9f11..50a29434 100644 --- a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/components/PublishButton.js +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/components/PublishButton.js @@ -5,17 +5,15 @@ // Invenio-Records-Marc21 is free software; you can redistribute it and/or // modify it under the terms of the MIT License; see LICENSE file for more // details. -import _get from 'lodash/get'; -import _isEmpty from 'lodash/isEmpty'; -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { Icon, Button, Modal } from 'semantic-ui-react'; -import { ActionButton } from 'react-invenio-forms'; - -import { submitAction } from '../state/actions'; -import { FORM_PUBLISHING } from '../state/types'; - +import _get from "lodash/get"; +import _isEmpty from "lodash/isEmpty"; +import React, { Component } from "react"; +import { connect } from "react-redux"; +import { Icon, Button, Modal } from "semantic-ui-react"; +import { ActionButton } from "react-invenio-forms"; +import { submitAction } from "../state/actions"; +import { FORM_PUBLISHING } from "../state/types"; export class PublishButtonComponent extends Component { state = { confirmOpen: false }; @@ -32,14 +30,13 @@ export class PublishButtonComponent extends Component { publishClick(event, formik); this.handleClose(); }; - const isDisabled = (formik) => { return formik.isSubmitting; }; const action = "publish"; - const capitalizedAction ="Publish"; + const capitalizedAction = "Publish"; return ( <> -

- {(`Are you sure you want to publish this record?`)} -

+

{`Are you sure you want to publish this record?`}

- - ); - })} - - + const arrayPath = fieldPath; + const indexPath = index; + const key = `${arrayPath}.${indexPath}`; + return ( + <> + + + ); + })} @@ -61,7 +62,6 @@ class TemplateFieldComponent extends Component { class FormikTemplateField extends Component { render() { - return ( ({ -}); +const mapStateToProps = (state) => ({}); export const TemplateField = connect( mapStateToProps, diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/components/index.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/components/index.js index 5cb2d6e9..34aefa82 100644 --- a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/components/index.js +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/components/index.js @@ -6,9 +6,9 @@ // modify it under the terms of the MIT License; see LICENSE file for more // details. -export { AccordionField } from './AccordionField'; -export { MetadataFields } from './MetadataFields'; -export { MetadataField } from './MetadataField'; -export { LeaderField } from './LeaderField'; -export { SaveButton } from './SaveButton'; -export { PublishButton } from './PublishButton'; \ No newline at end of file +export { AccordionField } from "./AccordionField"; +export { MetadataFields } from "./MetadataFields"; +export { MetadataField } from "./MetadataField"; +export { LeaderField } from "./LeaderField"; +export { SaveButton } from "./SaveButton"; +export { PublishButton } from "./PublishButton"; diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/fields/Field.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/fields/Field.js index 2e58ac02..dcc888b6 100644 --- a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/fields/Field.js +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/fields/Field.js @@ -6,9 +6,9 @@ // modify it under the terms of the MIT License; see LICENSE file for more // details. -import _get from 'lodash/get'; -import _set from 'lodash/set'; -import _cloneDeep from 'lodash/cloneDeep'; +import _get from "lodash/get"; +import _set from "lodash/set"; +import _cloneDeep from "lodash/cloneDeep"; export class Field { constructor({ @@ -36,4 +36,4 @@ export class Field { } return record; } -} \ No newline at end of file +} diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/fields/MetadataFields.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/fields/MetadataFields.js index 6505841b..387cb8f2 100644 --- a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/fields/MetadataFields.js +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/fields/MetadataFields.js @@ -6,26 +6,20 @@ // modify it under the terms of the MIT License; see LICENSE file for more // details. -import _cloneDeep from 'lodash/cloneDeep'; -import _get from 'lodash/get'; -import _has from 'lodash/has'; -import _pick from 'lodash/pick'; -import _set from 'lodash/set'; -import { Field } from './Field'; - -const { Marc, Record } = require('marcjs'); +import _cloneDeep from "lodash/cloneDeep"; +import _get from "lodash/get"; +import _has from "lodash/has"; +import _pick from "lodash/pick"; +import _set from "lodash/set"; +import { Field } from "./Field"; + +const { Marc, Record } = require("marcjs"); export class MetadataFields extends Field { + constructor({ fieldpath, deserializedDefault = [], serializedDefault = [] }) { + super({ fieldpath, deserializedDefault, serializedDefault }); + } - constructor({ - fieldpath, - deserializedDefault = [], - serializedDefault = [], - }) { - super({fieldpath, deserializedDefault, serializedDefault}) - }; - - - /** + /** * Compare Metadata field order for sorting in frontend. * @method * @param {object} field1 - first field to compare @@ -40,9 +34,8 @@ export class MetadataFields extends Field { } return 0; } - - /** + /** * Deserialize backend record into format compatible with frontend. * @method * @param {object} record - potentially empty object @@ -51,11 +44,11 @@ export class MetadataFields extends Field { deserialize(record) { let deserializedRecord = record; deserializedRecord = _pick(deserializedRecord, [ - 'metadata', - 'id', - 'links', - 'files', - 'pids', + "metadata", + "id", + "links", + "files", + "pids", ]); for (let key in this.depositRecordSchema) { deserializedRecord = this.depositRecordSchema[key].deserialize( @@ -66,23 +59,27 @@ export class MetadataFields extends Field { return deserializedRecord; } - static _deserialize_subfields(subfields) { let field = ""; // subfields.join(" "); - for( let i = 0; i < subfields.length; i++) { + for (let i = 0; i < subfields.length; i++) { for (const [key, value] of Object.entries(subfields[i])) { - field += " $$" + key + " " + value ; + field += " $$" + key + " " + value; } } return field; } _deserialize_fields(fields) { - let metadata = [] - for (const field of Object.values(fields)) { - const key = Object.keys(field)[0]; + let metadata = []; + for (const field of Object.values(fields)) { + const key = Object.keys(field)[0]; const value = Object.values(field)[0]; - let internal = {id : key, ind1: value["ind1"], ind2: value["ind2"], subfield: MetadataFields._deserialize_subfields(value["subfields"])}; + let internal = { + id: key, + ind1: value["ind1"], + ind2: value["ind2"], + subfield: MetadataFields._deserialize_subfields(value["subfields"]), + }; metadata.push(internal); } return metadata; @@ -90,49 +87,49 @@ export class MetadataFields extends Field { /** * Deserialize backend field into format compatible with frontend using - * the given schema. + * the given schema. * @method * @param {object} element - potentially empty object * @returns {object} frontend compatible element object */ deserialize(record) { - record = _cloneDeep(record) + record = _cloneDeep(record); - let marcxml = _get(record, this.fieldpath) + let marcxml = _get(record, this.fieldpath); let record_dict; if (marcxml != null) { - const marcjs = Marc.parse(marcxml, 'marcxml'); + const marcjs = Marc.parse(marcxml, "marcxml"); record_dict = marcjs.mij(); record_dict.fields = this._deserialize_fields(record_dict.fields); - record_dict.fields .sort(this.compare); + record_dict.fields.sort(this.compare); } else { - record_dict = this.deserializedDefault + record_dict = this.deserializedDefault; } - - return _set(record, this.fieldpath, record_dict) + return _set(record, this.fieldpath, record_dict); } - static _serialize_subfields(subfields) { - let fields = []; + let fields = []; subfields = subfields.trim(); const subfield_list = subfields.split("$$").filter(String); - - for( let i = 0; i < subfield_list.length; i++) { + + for (let i = 0; i < subfield_list.length; i++) { const subfield = subfield_list[i].split(" "); - fields = fields.concat([subfield[0], subfield.slice(1).join(" ")]) + fields = fields.concat([subfield[0], subfield.slice(1).join(" ")]); } return fields; } _serialize_fields(marc_record, fields) { - let metadata = [] - for (const field of Object.values(fields)) { - const subfields = MetadataFields._serialize_subfields(field["subfield"]) - let internal = [field["id"], field["ind1"] + field["ind2"]].concat(subfields); + let metadata = []; + for (const field of Object.values(fields)) { + const subfields = MetadataFields._serialize_subfields(field["subfield"]); + let internal = [field["id"], field["ind1"] + field["ind2"]].concat( + subfields + ); marc_record.append(internal); } @@ -147,18 +144,18 @@ export class MetadataFields extends Field { * */ serialize(record) { - let record_dict = _get(record, this.fieldpath, this.serializedDefault) + let record_dict = _get(record, this.fieldpath, this.serializedDefault); let metadata = new Record(); - if(_has(record_dict, ["leader"])){ + if (_has(record_dict, ["leader"])) { metadata.leader = record_dict.leader; } else { metadata.leader = ""; } - if(_has(record_dict, ["fields"])){ + if (_has(record_dict, ["fields"])) { record_dict.fields = this._serialize_fields(metadata, record_dict.fields); } - const marcxml = {"xml": metadata.as('marcxml')}; - - return _set(_cloneDeep(record),this.fieldpath, marcxml); + const marcxml = { xml: metadata.as("marcxml") }; + + return _set(_cloneDeep(record), this.fieldpath, marcxml); } } diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/fields/index.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/fields/index.js index 85462e9e..385edc39 100644 --- a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/fields/index.js +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/fields/index.js @@ -6,5 +6,5 @@ // modify it under the terms of the MIT License; see LICENSE file for more // details. -export { Field } from './Field'; -export { MetadataFields } from './MetadataFields'; \ No newline at end of file +export { Field } from "./Field"; +export { MetadataFields } from "./MetadataFields"; diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/index.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/index.js index 67662bba..b43b690f 100644 --- a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/index.js +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/index.js @@ -6,7 +6,6 @@ // modify it under the terms of the MIT License; see LICENSE file for more // details. - import React from "react"; import ReactDOM from "react-dom"; import "semantic-ui-css/semantic.min.css"; @@ -14,15 +13,13 @@ import "semantic-ui-css/semantic.min.css"; import { getInputFromDOM } from "react-invenio-deposit"; import { Marc21DepositForm } from "./Marc21DepositForm"; - - ReactDOM.render( - , - document.getElementById("marc21-deposit-form") - ); + , + document.getElementById("marc21-deposit-form") +); diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/state/actions/deposit.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/state/actions/deposit.js index 33fa7a52..51376020 100644 --- a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/state/actions/deposit.js +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/state/actions/deposit.js @@ -7,21 +7,21 @@ // details. import { - FORM_SAVING, - FORM_PUBLISHING, - FORM_ACTION_EVENT_EMITTED, - } from '../types'; - + FORM_SAVING, + FORM_PUBLISHING, + FORM_ACTION_EVENT_EMITTED, +} from "../types"; + export const save = (record, formik) => { - return async (dispatch, getState, config) => { - const controller = config.controller; - controller.saveDraft(record, { - formik, - store: { dispatch, getState, config }, - }); - }; + return async (dispatch, getState, config) => { + const controller = config.controller; + controller.saveDraft(record, { + formik, + store: { dispatch, getState, config }, + }); }; - +}; + export const publish = (record, formik) => { return (dispatch, getState, config) => { const controller = config.controller; @@ -32,15 +32,14 @@ export const publish = (record, formik) => { }; }; - export const submitAction = (action, event, formik) => { - return async (dispatch, getState, config) => { - dispatch({ - type: FORM_ACTION_EVENT_EMITTED, - payload: action, - }); - formik.handleSubmit(event); // eventually calls submitFormData below - }; + return async (dispatch, getState, config) => { + dispatch({ + type: FORM_ACTION_EVENT_EMITTED, + payload: action, + }); + formik.handleSubmit(event); // eventually calls submitFormData below + }; }; export const submitFormData = (record, formik) => { @@ -55,4 +54,4 @@ export const submitFormData = (record, formik) => { console.log(`onSubmit triggered with unknown action ${formState}`); } }; -}; \ No newline at end of file +}; diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/state/actions/index.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/state/actions/index.js index e255e7ca..f1e89924 100644 --- a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/state/actions/index.js +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/state/actions/index.js @@ -6,5 +6,4 @@ // modify it under the terms of the MIT License; see LICENSE file for more // details. -export * from './deposit'; - +export * from "./deposit"; diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/state/reducers/deposit.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/state/reducers/deposit.js index fe1640b1..467f94c2 100644 --- a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/state/reducers/deposit.js +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/state/reducers/deposit.js @@ -16,50 +16,48 @@ import { ACTION_SAVE_SUCCEEDED, ACTION_SAVE_FAILED, ACTION_PUBLISH_FAILED, - ACTION_PUBLISH_SUCCEEDED -} from '../types'; - + ACTION_PUBLISH_SUCCEEDED, +} from "../types"; export default (state = {}, action) => { - switch (action.type) { - case FORM_ACTION_EVENT_EMITTED: - return { - ...state, - formState: action.payload, - }; - case ACTION_CREATE_SUCCEEDED: - return { - ...state, - record: { ...state.record, ...action.payload.data }, - formState: null, - }; - case ACTION_SAVE_SUCCEEDED: - return { - ...state, - record: { ...state.record, ...action.payload.data }, - errors: {}, - formState: FORM_SAVE_SUCCEEDED, - }; - case ACTION_SAVE_FAILED: - return { - ...state, - errors: { ...action.payload.errors }, - formState: FORM_SAVE_FAILED, - }; - case ACTION_PUBLISH_SUCCEEDED: - return { - ...state, - record: { ...state.record, ...action.payload.data }, - formState: FORM_PUBLISH_SUCCEEDED, - }; - case ACTION_PUBLISH_FAILED: - return { - ...state, - errors: { ...action.payload.errors }, - formState: FORM_PUBLISH_FAILED, - }; - default: - return state; - } - }; - \ No newline at end of file + switch (action.type) { + case FORM_ACTION_EVENT_EMITTED: + return { + ...state, + formState: action.payload, + }; + case ACTION_CREATE_SUCCEEDED: + return { + ...state, + record: { ...state.record, ...action.payload.data }, + formState: null, + }; + case ACTION_SAVE_SUCCEEDED: + return { + ...state, + record: { ...state.record, ...action.payload.data }, + errors: {}, + formState: FORM_SAVE_SUCCEEDED, + }; + case ACTION_SAVE_FAILED: + return { + ...state, + errors: { ...action.payload.errors }, + formState: FORM_SAVE_FAILED, + }; + case ACTION_PUBLISH_SUCCEEDED: + return { + ...state, + record: { ...state.record, ...action.payload.data }, + formState: FORM_PUBLISH_SUCCEEDED, + }; + case ACTION_PUBLISH_FAILED: + return { + ...state, + errors: { ...action.payload.errors }, + formState: FORM_PUBLISH_FAILED, + }; + default: + return state; + } +}; diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/state/reducers/index.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/state/reducers/index.js index 4218a2b7..6b4065fe 100644 --- a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/state/reducers/index.js +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/state/reducers/index.js @@ -6,9 +6,9 @@ // modify it under the terms of the MIT License; see LICENSE file for more // details. -import { combineReducers } from 'redux'; +import { combineReducers } from "redux"; -import depositReducer from './deposit'; +import depositReducer from "./deposit"; export default combineReducers({ deposit: depositReducer, }); diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/state/types/index.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/state/types/index.js index fd3a2cce..9b1cdf6a 100644 --- a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/state/types/index.js +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/state/types/index.js @@ -6,18 +6,18 @@ // modify it under the terms of the MIT License; see LICENSE file for more // details. -export const FORM_SAVING = "FORM_SAVING" -export const FORM_ACTION_EVENT_EMITTED = 'FORM_ACTION_EVENT_EMITTED'; +export const FORM_SAVING = "FORM_SAVING"; +export const FORM_ACTION_EVENT_EMITTED = "FORM_ACTION_EVENT_EMITTED"; export const FORM_SAVE_SUCCEEDED = "FORM_SAVE_SUCCEEDED"; export const FORM_SAVE_FAILED = "FORM_SAVE_FAILED"; export const FORM_PUBLISHING = "FORM_PUBLISHING"; -export const FORM_PUBLISH_FAILED = 'FORM_PUBLISH_FAILED'; -export const FORM_PUBLISH_SUCCEEDED = 'FORM_PUBLISH_SUCCEEDED'; +export const FORM_PUBLISH_FAILED = "FORM_PUBLISH_FAILED"; +export const FORM_PUBLISH_SUCCEEDED = "FORM_PUBLISH_SUCCEEDED"; -export const ACTION_CREATE_SUCCEEDED = "ACTION_CREATE_SUCCEEDED" -export const ACTION_SAVE_FAILED = 'ACTION_SAVE_FAILED'; -export const ACTION_SAVE_SUCCEEDED = "ACTION_SAVE_SUCCEEDED" +export const ACTION_CREATE_SUCCEEDED = "ACTION_CREATE_SUCCEEDED"; +export const ACTION_SAVE_FAILED = "ACTION_SAVE_FAILED"; +export const ACTION_SAVE_SUCCEEDED = "ACTION_SAVE_SUCCEEDED"; -export const ACTION_PUBLISH_SUCCEEDED = 'ACTION_PUBLISH_SUCCEEDED'; -export const ACTION_PUBLISH_FAILED = 'ACTION_PUBLISH_FAILED'; +export const ACTION_PUBLISH_SUCCEEDED = "ACTION_PUBLISH_SUCCEEDED"; +export const ACTION_PUBLISH_FAILED = "ACTION_PUBLISH_FAILED"; diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/store.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/store.js index 7c9ec3c2..8b18f868 100644 --- a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/store.js +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/store.js @@ -6,34 +6,33 @@ // modify it under the terms of the MIT License; see LICENSE file for more // details. -import _cloneDeep from 'lodash/cloneDeep'; -import _get from 'lodash/get'; +import _cloneDeep from "lodash/cloneDeep"; +import _get from "lodash/get"; import { createStore, compose, applyMiddleware } from "redux"; -import thunk from 'redux-thunk'; -import rootReducer from './state/reducers'; +import thunk from "redux-thunk"; +import rootReducer from "./state/reducers"; export const INITIAL_STORE_STATE = { formState: null, }; export function configureStore(appConfig) { - const { record, files, config, permissions, ...extra } = appConfig; - const initialDepositState = { - record, - config, - permissions, - formState: null, - }; - const preloadedState = { - deposit: initialDepositState, - }; - - const composeEnhancers = - window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; - return createStore( - rootReducer, - preloadedState, - composeEnhancers(applyMiddleware(thunk.withExtraArgument(extra))) - ); - } - \ No newline at end of file + const { record, files, config, permissions, ...extra } = appConfig; + const initialDepositState = { + record, + config, + permissions, + formState: null, + }; + const preloadedState = { + deposit: initialDepositState, + }; + + const composeEnhancers = + window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; + return createStore( + rootReducer, + preloadedState, + composeEnhancers(applyMiddleware(thunk.withExtraArgument(extra))) + ); +} diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/components.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/components.js index a6e3c027..ec89fd30 100644 --- a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/components.js +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/search/components.js @@ -27,29 +27,28 @@ import Overridable from "react-overridable"; import { SearchBar, SearchApp } from "@js/invenio_search_ui/components"; export const Marc21RecordResultsListItem = ({ result, index }) => { + const createdDate = _get(result, "ui.created", "No creation date found."); - const createdDate = _get( - result, - "ui.created", - "No creation date found." - ); - - const publicationDate = _get( - result, - "ui.updated", - "No update date found." - ); + const publicationDate = _get(result, "ui.updated", "No update date found."); const access = _get(result, ["ui", "access"], []); const access_id = _get(access, "id", "public"); const access_status = _get(access, "title", "Public"); const access_icon = _get(access, "icon", "unlock"); const metadata = _get(result, ["ui", "metadata"], []); - const description = _get(metadata, ["summary","summary"], "No description"); + const description = _get(metadata, ["summary", "summary"], "No description"); const subjects = _get(metadata, "subject_added_entry_topical_term", []); - - const publication = _get(metadata, ["production_publication_distribution_manufacture_and_copyright_notice"], []); - const creators = _get(publication, "name_of_producer_publisher_distributor_manufacturer", []); + + const publication = _get( + metadata, + ["production_publication_distribution_manufacture_and_copyright_notice"], + [] + ); + const creators = _get( + publication, + "name_of_producer_publisher_distributor_manufacturer", + [] + ); const title = _get(metadata, ["title_statement", "title"], "No title"); const version = _get(result, "revision_id", null); @@ -65,10 +64,8 @@ export const Marc21RecordResultsListItem = ({ result, index }) => { {publicationDate} {version ? `(${version})` : null}
{%- endblock page_body %} diff --git a/invenio_records_marc21/ui/records/errors.py b/invenio_records_marc21/ui/records/errors.py index b4c2be33..50ddc51a 100644 --- a/invenio_records_marc21/ui/records/errors.py +++ b/invenio_records_marc21/ui/records/errors.py @@ -10,7 +10,7 @@ """Records error handlers.""" -from flask import current_app, render_template, request +from flask import current_app, render_template from flask_login import current_user diff --git a/invenio_records_marc21/ui/theme/__init__.py b/invenio_records_marc21/ui/theme/__init__.py index cb31859b..97e908a9 100644 --- a/invenio_records_marc21/ui/theme/__init__.py +++ b/invenio_records_marc21/ui/theme/__init__.py @@ -10,9 +10,6 @@ """Marc21 Theme Package.""" -from flask_babelex import lazy_gettext as _ -from flask_menu import current_menu - from .views import index, search From ed572c9005c2237b9b8ca8e0755249fa85a98254 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Thu, 28 Oct 2021 08:35:09 +0200 Subject: [PATCH 130/217] feature: add filepreviewer adding file previewer to the landing page for marc21 records --- invenio_records_marc21/config.py | 2 + .../services/schemas/files.py | 3 +- .../records/helpers/files.html | 43 ++++++- .../records/macros/files.html | 121 ++++++++++++++++++ invenio_records_marc21/ui/records/__init__.py | 13 +- .../ui/records/decorators.py | 84 +++++++++++- invenio_records_marc21/ui/records/records.py | 105 ++++++++++++++- setup.py | 1 + 8 files changed, 363 insertions(+), 9 deletions(-) create mode 100644 invenio_records_marc21/templates/invenio_records_marc21/records/macros/files.html diff --git a/invenio_records_marc21/config.py b/invenio_records_marc21/config.py index 2891154c..c22834c2 100644 --- a/invenio_records_marc21/config.py +++ b/invenio_records_marc21/config.py @@ -44,6 +44,8 @@ INVENIO_MARC21_UI_ENDPOINTS = { "record-detail": "/", "record-export": "//export/", + "record_file_preview": "marc21//preview/", + "record_file_download": "marc21//files/", } """Marc21 Record ui endpoints.""" diff --git a/invenio_records_marc21/services/schemas/files.py b/invenio_records_marc21/services/schemas/files.py index 97f564fd..82abef5a 100644 --- a/invenio_records_marc21/services/schemas/files.py +++ b/invenio_records_marc21/services/schemas/files.py @@ -15,6 +15,7 @@ from marshmallow import Schema, fields from marshmallow_utils.fields import SanitizedUnicode +from marshmallow_utils.permissions import FieldPermissionsMixin class FileSchema(Schema): @@ -30,7 +31,7 @@ class FileSchema(Schema): storage_class = SanitizedUnicode() -class FilesSchema(Schema): +class FilesSchema(Schema, FieldPermissionsMixin): """Files metadata schema.""" field_dump_permissions = { diff --git a/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/files.html b/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/files.html index 2663bd57..ae56591e 100644 --- a/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/files.html +++ b/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/files.html @@ -4,6 +4,47 @@ Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it under the terms of the MIT License; see LICENSE file for more details. + + For the original code see the NOTE below. +#} +{# + NOTE: + copy pasted code from invenio_app_rdm/records/detail.html + it was necessary to copy paste it because it was necessary to copy paste the macros to modify those. + To use then the modified macros the following line is necessary and therefore the copy paste was + necessary. Further info see macros. #} +{% from "invenio_records_marc21/records/macros/files.html" import file_list_box, preview_file_box %} -
File previewer for marc21 not yet implemented!
+{% block record_files %} +{%- if record.files.enabled -%} + {%- if permissions.can_read_files -%} + {# record has files AND user can see files #} + {%- set files = files|order_entries %} + {%- if files|has_previewable_files -%} + {%-set preview_file = files|select_preview_file(default_preview=record.files.default_preview) %} + {{ preview_file_box(preview_file, pid, is_preview, record) }} + {%- endif -%} + {{ file_list_box(files, pid, is_preview, record) }} + {% else %} + {# record has files BUT user cannot see files #} +
+
+
+ {{ _("Files") }} + +
+
+
+ {{ record.ui.access_status.title_l10n }} +

{{ record.ui.access_status.description_l10n }}

+ {% if record.access.embargo.reason %} +

{{_("Reason")}}: {{record.access.embargo.reason}}

+ {% endif%} +
+
+
+
+ {%- endif %} +{%- endif %} +{% endblock record_files %} diff --git a/invenio_records_marc21/templates/invenio_records_marc21/records/macros/files.html b/invenio_records_marc21/templates/invenio_records_marc21/records/macros/files.html new file mode 100644 index 00000000..1fbfdc55 --- /dev/null +++ b/invenio_records_marc21/templates/invenio_records_marc21/records/macros/files.html @@ -0,0 +1,121 @@ +{# -*- coding: utf-8 -*- + + Copyright (C) 2021 Graz University of Technology. + + Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it + under the terms of the MIT License; see LICENSE file for more details. + + For the original code see the NOTE below. +#} +{# + NOTE: + copy pasted code from invenio_app_rdm/records/macros/files.html + it was necessary to copy paste it to override the endpoint from invenio_app_rdm_records to invenio_records_marc21 +#} +{%- macro preview_file(preview_endpoint, pid_value, filename, is_preview, id='preview-iframe', width='100%', height='400' ) %} + {% if is_preview %} + {%- set preview_url = url_for(preview_endpoint, pid_value=pid_value, filename=filename, preview=1) -%} + {% else %} + {%- set preview_url = url_for(preview_endpoint, pid_value=pid_value, filename=filename) -%} + {% endif %} + +{%- endmacro %} + + +{% macro preview_file_box(file, pid, is_preview, record) %} +
+
+
+ {{file.key}} + +
+
+ {{ preview_file('invenio_records_marc21.record_file_preview', pid_value=pid, filename=file.key, is_preview=is_preview) }} +
+
+
+{%- endmacro %} + + +{%- macro file_list(files, pid, is_preview, with_preview=true, download_endpoint='invenio_records_marc21.record_file_download', preview_endpoint='invenio_records_marc21.record_file_preview') %} + + + + + + + + + + {% for file in files %} + {% if is_preview %} + {%- set file_url_download = url_for(download_endpoint, pid_value=pid, filename=file.key, download=1, preview=1) %} + {%- set file_url_preview = url_for(preview_endpoint, pid_value=pid, filename=file.key, preview=1) %} + {% else %} + {%- set file_url_download = url_for(download_endpoint, pid_value=pid, filename=file.key, download=1) %} + {%- set file_url_preview = url_for(preview_endpoint, pid_value=pid, filename=file.key) %} + {% endif %} + {%- set file_type = file.key.split('.')[-1] %} + + + + + + {% endfor %} + +
{{_('Name')}}{{_('Size')}}
+ {{ file.key }} +
+ {{ file.checksum }} +
+ +
+
+
{{ file.size|filesizeformat }} + + {% if with_preview and file_type|lower is previewable %} + + {{_("Preview")}} + + {% endif %} + + + {{_('Download')}} + + +
+{%- endmacro %} + + +{% macro file_list_box(files, pid, is_preview, record) %} +
+
+
+ {{ _("Files") }} + {% if files %} ({{files|sum(attribute='size')|filesizeformat}}){% endif %} + +
+
+ {% if record.access.files == 'restricted' %} +
+ {{ record.ui.access_status.title_l10n }} +

{{ record.ui.access_status.description_l10n }}

+ {% if record.access.embargo.reason %} +

{{_("Reason")}}: {{record.access.embargo.reason}}

+ {% endif%} +
+ {% endif %} +
+ {{ file_list(files, pid, is_preview) }} +
+
+
+
+{%- endmacro %} diff --git a/invenio_records_marc21/ui/records/__init__.py b/invenio_records_marc21/ui/records/__init__.py index 73a667be..8b0649d2 100644 --- a/invenio_records_marc21/ui/records/__init__.py +++ b/invenio_records_marc21/ui/records/__init__.py @@ -19,7 +19,12 @@ record_tombstone_error, ) from .filters import pid_url, sanitize_title -from .records import record_detail, record_export +from .records import ( + record_detail, + record_export, + record_file_download, + record_file_preview, +) # @@ -40,6 +45,12 @@ def init_records_views(blueprint, app): view_func=record_export, ) + blueprint.add_url_rule(routes["record_file_preview"], view_func=record_file_preview) + + blueprint.add_url_rule( + routes["record_file_download"], view_func=record_file_download + ) + # Register error handlers blueprint.register_error_handler(PIDDeletedError, record_tombstone_error) blueprint.register_error_handler(PIDDoesNotExistError, not_found_error) diff --git a/invenio_records_marc21/ui/records/decorators.py b/invenio_records_marc21/ui/records/decorators.py index b35931e7..a82cee88 100644 --- a/invenio_records_marc21/ui/records/decorators.py +++ b/invenio_records_marc21/ui/records/decorators.py @@ -13,10 +13,9 @@ """Routes for record-related pages provided by Invenio-Records-Marc21.""" - from functools import wraps -from flask import g +from flask import g, request from invenio_records_resources.services.errors import PermissionDeniedError from sqlalchemy.orm.exc import NoResultFound @@ -73,6 +72,21 @@ def get_record(): return view +def pass_is_preview(f): + """Decorate a view to check if it's a preview.""" + + @wraps(f) + def view(**kwargs): + preview = request.args.get("preview") + is_preview = False + if preview == "1": + is_preview = True + kwargs["is_preview"] = is_preview + return f(**kwargs) + + return view + + def pass_record_files(f): """Decorate a view to pass a record's files using the files service.""" @@ -105,6 +119,72 @@ def list_record_files(): return view +# NOTE: +# copy pasted code from invenio_app_rdm/records_ui/views/decorators.py to use +# the local file_service service +def pass_file_item(f): + """Decorate a view to pass a file item using the files service.""" + + @wraps(f) + def view(**kwargs): + pid_value = kwargs.get("pid_value") + file_key = kwargs.get("filename") + is_preview = kwargs.get("is_preview") + + def get_record_file_content(): + """Retrieve record file content.""" + return files_service().get_file_content( + id_=pid_value, file_key=file_key, identity=g.identity + ) + + if is_preview: + try: + item = draft_files_service().get_file_content( + id_=pid_value, file_key=file_key, identity=g.identity + ) + except NoResultFound: + item = get_record_file_content() + else: + item = get_record_file_content() + kwargs["file_item"] = item + return f(**kwargs) + + return view + + +# NOTE: +# copy pasted code from invenio_app_rdm/records_ui/views/decorators.py to use +# the local file_service service +def pass_file_metadata(f): + """Decorate a view to pass a file's metadata using the files service.""" + + @wraps(f) + def view(**kwargs): + pid_value = kwargs.get("pid_value") + file_key = kwargs.get("filename") + is_preview = kwargs.get("is_preview") + + def get_record_file_content(): + """Retrieve record file metadata.""" + return files_service().read_file_metadata( + id_=pid_value, file_key=file_key, identity=g.identity + ) + + if is_preview: + try: + files = draft_files_service().read_file_metadata( + id_=pid_value, file_key=file_key, identity=g.identity + ) + except NoResultFound: + files = get_record_file_content() + else: + files = get_record_file_content() + kwargs["file_metadata"] = files + return f(**kwargs) + + return view + + def user_permissions(actions=[]): """Decorate a view to pass user's permissions for the provided actions. diff --git a/invenio_records_marc21/ui/records/records.py b/invenio_records_marc21/ui/records/records.py index debb9a51..40490b69 100644 --- a/invenio_records_marc21/ui/records/records.py +++ b/invenio_records_marc21/ui/records/records.py @@ -10,11 +10,58 @@ """Routes for record-related pages provided by Invenio-App-RDM.""" -from flask import abort, current_app, render_template +from os.path import splitext + +from flask import abort, current_app, render_template, request, url_for from invenio_base.utils import obj_or_import_string +from invenio_previewer.extensions import default +from invenio_previewer.proxies import current_previewer from ...resources.serializers.ui import Marc21UIJSONSerializer -from .decorators import pass_record_files, pass_record_or_draft, user_permissions +from .decorators import ( + pass_file_item, + pass_file_metadata, + pass_is_preview, + pass_record_files, + pass_record_or_draft, +) + + +class PreviewFile: + """Preview file implementation for InvenioRDM. + + This class was apparently created because of subtle differences with + `invenio_previewer.api.PreviewFile`. + """ + + def __init__(self, file_item, record_pid_value, url=None): + """Create a new PreviewFile.""" + self.file = file_item + self.data = file_item.data + self.size = self.data["size"] + self.filename = self.data["key"] + self.bucket = self.data["bucket_id"] + self.uri = url or url_for( + "invenio_app_rdm_records.record_file_download", + pid_value=record_pid_value, + filename=self.filename, + ) + + def is_local(self): + """Check if file is local.""" + return True + + def has_extensions(self, *exts): + """Check if file has one of the extensions. + + Each `exts` has the format `.{file type}` e.g. `.txt` . + """ + file_ext = splitext(self.data["key"])[1].lower() + return file_ext in exts + + def open(self): + """Open the file.""" + return self.file._file.file.storage().open() # @@ -22,7 +69,7 @@ # @pass_record_or_draft @pass_record_files -def record_detail(record=None, files=None, pid_value=None, permissions=None): +def record_detail(record=None, files=None, pid_value=None, is_preview=False): """Record detail page (aka landing page).""" files_dict = None if files is None else files.to_dict() return render_template( @@ -30,7 +77,11 @@ def record_detail(record=None, files=None, pid_value=None, permissions=None): record=Marc21UIJSONSerializer().dump_one(record.to_dict()), pid=pid_value, files=files_dict, - permissions=permissions, + permissions=record.has_permissions_to( + ["edit", "new_version", "manage", "update_draft", "read_files"] + ), + is_preview=is_preview, + is_draft=record._record.is_draft, ) @@ -62,3 +113,49 @@ def record_export(record=None, export_format=None, pid_value=None, permissions=N record=Marc21UIJSONSerializer().dump_one(record.to_dict()), permissions=permissions, ) + + +# NOTE: +# copy pasted code from invenio_app_rdm/records_ui/views/records.py with +# minor change invenio_app_rdm_records -> invenio_records_marc21 +@pass_is_preview +@pass_record_or_draft +@pass_file_metadata +def record_file_preview( + record=None, + pid_value=None, + pid_type="recid", + file_metadata=None, + is_preview=False, + **kwargs +): + """Render a preview of the specified file.""" + + file_previewer = file_metadata.data.get("previewer") + + url = url_for( + "invenio_records_marc21.record_file_download", + pid_value=pid_value, + filename=file_metadata.data["key"], + preview=1 if is_preview else 0, + ) + # Find a suitable previewer + fileobj = PreviewFile(file_metadata, pid_value, url) + for plugin in current_previewer.iter_previewers( + previewers=[file_previewer] if file_previewer else None + ): + if plugin.can_preview(fileobj): + return plugin.preview(fileobj) + + return default.preview(fileobj) + + +# NOTE: +# copy pasted code from invenio_app_rdm/records_ui/views/records.py with +# minor change invenio_app_rdm_records -> invenio_records_marc21 +@pass_is_preview +@pass_file_item +def record_file_download(file_item=None, pid_value=None, is_preview=False, **kwargs): + """Download a file from a record.""" + download = bool(request.args.get("download")) + return file_item.send_file(as_attachment=download) diff --git a/setup.py b/setup.py index d4b9d1fe..0b05b08c 100644 --- a/setup.py +++ b/setup.py @@ -22,6 +22,7 @@ "invenio-indexer>=1.2.0", "pytest-invenio>=1.4.0,<2.0.0", "mock>=4.0.3", + "invenio-previewer>=1.3.4", ] # Should follow inveniosoftware/invenio versions From 621fc886d0dd93ac5c7f24f7e0a28f2ff7d9cebf Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Thu, 28 Oct 2021 08:41:08 +0200 Subject: [PATCH 131/217] modification: enable files support in services adding possibility to enable files when create a marc21 record --- invenio_records_marc21/services/services.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/invenio_records_marc21/services/services.py b/invenio_records_marc21/services/services.py index ca9bb3d9..823b345f 100644 --- a/invenio_records_marc21/services/services.py +++ b/invenio_records_marc21/services/services.py @@ -33,17 +33,18 @@ class Marc21RecordService(RecordService): config_name = "MARC21_RECORDS_SERVICE_CONFIG" default_config = Marc21RecordServiceConfig - def _create_data(self, identity, data, metadata, access=None): + def _create_data(self, identity, data, metadata, files=False, access=None): """Create a data json. :param identity: Identity of user creating the record. :param Metadata metadata: Input data according to the metadata schema. + :param bool files: enabledisable file support. :param dict access: provide access additional information :return data: marc21 record data """ if data is None: data = {"metadata": {"xml": metadata.xml, "json": metadata.json}} - data.setdefault("files", {"enabled": False}) + data["files"] = {"enabled": files} if "access" not in data: default_access = { "access": { @@ -58,17 +59,17 @@ def _create_data(self, identity, data, metadata, access=None): return data def create( - self, identity, data=None, metadata=Marc21Metadata(), access=None + self, identity, data=None, metadata=Marc21Metadata(), files=False, access=None ) -> RecordItem: """Create a draft record. :param identity: Identity of user creating the record. :param dict data: Input data according to the data schema. :param Marc21Metadata metadata: Input data according to the metadata schema. - :param links_config: Links configuration. + :param bool files: enable/disable file support for the record. :param dict access: provide access additional information """ - data = self._create_data(identity, data, metadata, access) + data = self._create_data(identity, data, metadata, files, access) return super().create(identity, data) def update_draft( From feaeab346b20a73223276f4322c8a3866abf13f9 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Thu, 28 Oct 2021 08:50:33 +0200 Subject: [PATCH 132/217] modification: record schema adding check field is_published to record and the pids dict field --- invenio_records_marc21/records/api.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/invenio_records_marc21/records/api.py b/invenio_records_marc21/records/api.py index 5b50b6cc..05f74941 100644 --- a/invenio_records_marc21/records/api.py +++ b/invenio_records_marc21/records/api.py @@ -15,17 +15,19 @@ from invenio_drafts_resources.records import Draft, Record from invenio_drafts_resources.records.api import ParentRecord as BaseParentRecord from invenio_drafts_resources.records.systemfields import ParentField +from invenio_pidstore.models import PIDStatus from invenio_rdm_records.records.systemfields import ( HasDraftCheckField, ParentRecordAccessField, RecordAccessField, ) -from invenio_records.systemfields import ConstantField, ModelField +from invenio_records.systemfields import ConstantField, DictField, ModelField from invenio_records_resources.records.api import FileRecord as BaseFileRecord from invenio_records_resources.records.systemfields import ( FilesField, IndexField, PIDField, + PIDStatusCheckField, ) from werkzeug.local import LocalProxy @@ -145,5 +147,6 @@ class Marc21Record(Record): bucket = ModelField(dump=False) + is_published = PIDStatusCheckField(status=PIDStatus.REGISTERED, dump=True) -RecordFile.record_cls = Marc21Record + pids = DictField("pids") From 77e3549420b28634a0168edd7b320fc6cffb14eb Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Thu, 28 Oct 2021 08:51:51 +0200 Subject: [PATCH 133/217] modification: FieldsPermissionMixin adding fieldPermissionMixin schema to marc21 records --- invenio_records_marc21/services/schemas/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/invenio_records_marc21/services/schemas/__init__.py b/invenio_records_marc21/services/schemas/__init__.py index aea54abf..ccbaa5d1 100644 --- a/invenio_records_marc21/services/schemas/__init__.py +++ b/invenio_records_marc21/services/schemas/__init__.py @@ -9,7 +9,6 @@ # details. """Marc21 record schemas.""" - from invenio_drafts_resources.services.records.schema import ParentSchema from invenio_rdm_records.services.schemas.access import AccessSchema from invenio_rdm_records.services.schemas.parent.access import ParentAccessSchema @@ -17,6 +16,7 @@ from marshmallow.decorators import post_dump from marshmallow.fields import Boolean, Integer, List, Nested, Str from marshmallow_utils.fields import NestedAttribute +from marshmallow_utils.permissions import FieldPermissionsMixin from .files import FilesSchema from .metadata import MetadataField @@ -34,7 +34,7 @@ class Marc21ParentSchema(ParentSchema): access = Nested(ParentAccessSchema) -class Marc21RecordSchema(BaseRecordSchema): +class Marc21RecordSchema(BaseRecordSchema, FieldPermissionsMixin): """Record schema.""" field_load_permissions = { From 40305c1b7fec8ac1c65e2e2d0c9b5b1f0593fcab Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Thu, 28 Oct 2021 08:56:29 +0200 Subject: [PATCH 134/217] modification: codestyle update --- invenio_records_marc21/ui/records/records.py | 1 - 1 file changed, 1 deletion(-) diff --git a/invenio_records_marc21/ui/records/records.py b/invenio_records_marc21/ui/records/records.py index 40490b69..1794b652 100644 --- a/invenio_records_marc21/ui/records/records.py +++ b/invenio_records_marc21/ui/records/records.py @@ -130,7 +130,6 @@ def record_file_preview( **kwargs ): """Render a preview of the specified file.""" - file_previewer = file_metadata.data.get("previewer") url = url_for( From de485e6c0ce3949c65b9f7b462f39f46d2b91fac Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Thu, 28 Oct 2021 08:58:23 +0200 Subject: [PATCH 135/217] tests: record protection updating record access protection schema to new access schema --- tests/services/test_record_service.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/tests/services/test_record_service.py b/tests/services/test_record_service.py index b6f754d7..f2fa06cd 100644 --- a/tests/services/test_record_service.py +++ b/tests/services/test_record_service.py @@ -210,7 +210,6 @@ def test_embargo_lift_without_draft(mock_arrow, running_app, marc21_record): assert record_lifted.access.status.value == "metadata-only" -@pytest.mark.skip("Cannot be tested yet!") @mock.patch("arrow.utcnow") def test_embargo_lift_with_draft(mock_arrow, running_app, marc21_record): identity_simple = running_app.identity_simple @@ -236,14 +235,13 @@ def test_embargo_lift_with_draft(mock_arrow, running_app, marc21_record): assert record_lifted.access.embargo.active is False assert record_lifted.access.protection.files == "public" - assert record_lifted.access.protection.metadata == "public" + assert record_lifted.access.protection.record == "public" assert draft_lifted.access.embargo.active is False assert draft_lifted.access.protection.files == "public" - assert draft_lifted.access.protection.metadata == "public" + assert draft_lifted.access.protection.record == "public" -@pytest.mark.skip("Cannot be tested yet!") @mock.patch("arrow.utcnow") def test_embargo_lift_with_updated_draft(mock_arrow, running_app, marc21_record): identity_simple = running_app.identity_simple @@ -278,11 +276,11 @@ def test_embargo_lift_with_updated_draft(mock_arrow, running_app, marc21_record) assert record_lifted.access.embargo.active is False assert record_lifted.access.protection.files == "public" - assert record_lifted.access.protection.metadata == "public" + assert record_lifted.access.protection.record == "public" assert draft_lifted.access.embargo.active is False assert draft_lifted.access.protection.files == "restricted" - assert draft_lifted.access.protection.metadata == "public" + assert draft_lifted.access.protection.record == "public" def test_embargo_lift_with_error(running_app, marc21_record): From 9feaa52561b82da6a0cfbe1c2f2b353f9abdc6af Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Thu, 4 Nov 2021 10:20:50 +0100 Subject: [PATCH 136/217] docs: update invenio-records-marc21 documentation --- INSTALL.rst | 18 ++++ README.rst | 27 ++++++ docs/api.rst | 6 +- docs/conf.py | 15 ++- docs/usage.rst | 1 + invenio_records_marc21/__init__.py | 102 +++++++++++++++++++- invenio_records_marc21/records/api.py | 4 +- invenio_records_marc21/services/services.py | 47 +++++---- 8 files changed, 197 insertions(+), 23 deletions(-) diff --git a/INSTALL.rst b/INSTALL.rst index 7c40eee6..8817eada 100644 --- a/INSTALL.rst +++ b/INSTALL.rst @@ -6,3 +6,21 @@ Invenio-Records-Marc21 is on PyPI so all you need is: .. code-block:: console $ pip install invenio-records-marc21 + + +Choose a version of elasticsearch and a DB, if you want to test the module. +The installation [options] supported for the Marc21 module: + + ``docs`` + for documentation building dependencies; + ``postgresql`` + to use PostgreSQL database backend; + ``tests`` + for test dependencies. + +An example installation: + +.. code-block:: console + + pip install -e .[all,elasticsearch7,postgresql] + diff --git a/README.rst b/README.rst index 7a6ca61a..7d03bc9d 100644 --- a/README.rst +++ b/README.rst @@ -25,3 +25,30 @@ Marc21 datamodel Further documentation is available on https://invenio-records-marc21.readthedocs.io/ + +Development +=========== + +Install +------- + +Choose a version of elasticsearch and a DB, then run: + +.. code-block:: console + + pipenv run pip install -e .[all] + pipenv run pip install invenio-search[elasticsearch7] + pipenv run pip install invenio-db[postgresql,versioning] + + +Service +========= + +** Create Marc21 Record** + +Tests +========= + +.. code-block:: console + + pipenv run ./run-tests.sh \ No newline at end of file diff --git a/docs/api.rst b/docs/api.rst index 906e08f0..ed8da803 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -8,10 +8,14 @@ API Docs ======== - .. automodule:: invenio_records_marc21.ext :members: +Service API +----------- +.. autoclass:: invenio_records_marc21.services::Marc21RecordService + :members: + Views ----- diff --git a/docs/conf.py b/docs/conf.py index 8161dd0b..1417ea47 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -332,8 +332,19 @@ # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { "python": ("https://docs.python.org/", None), - # TODO: Configure external documentation references, eg: - # 'Flask-Admin': ('https://flask-admin.readthedocs.io/en/latest/', None), + # "invenio-celery": ("invenio-celery.readthedocs.io/en/latest/", None), + "flask_principal": ( + "https://flask-principal.readthedocs.io/en/latest/", + None, + ), + "invenio_records_resources": ( + "https://invenio-records-resources.readthedocs.io/en/latest/", + None, + ), + "invenio_rdm_records": ( + "https://invenio-rdm-records.readthedocs.io/en/latest/", + None, + ), } # Autodoc configuraton. diff --git a/docs/usage.rst b/docs/usage.rst index 1c6bbb9b..5ab72d44 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -10,3 +10,4 @@ Usage ===== .. automodule:: invenio_records_marc21 + diff --git a/invenio_records_marc21/__init__.py b/invenio_records_marc21/__init__.py index 8ac31c35..29feda39 100644 --- a/invenio_records_marc21/__init__.py +++ b/invenio_records_marc21/__init__.py @@ -8,7 +8,107 @@ # modify it under the terms of the MIT License; see LICENSE file for more # details. -"""Marc21 datamodel.""" +"""Invenio-Records-Marc21 datamodel. + +This guide will show you how to get started with +Invenio-Records-Marc21. It assumes that you already have knowledge of +Flask applications and Invenio modules. + +It will then explain key topics and concepts of this module. + +Getting started +--------------- + +You will learn how to create a new Marc21 records, upload a File and accosiate with the record +using the programmatic APIs of Invenio-Records-Marc21. + + +This is the initial configuration needed to have things running: + +:py:data:`invenio_records_marc21.config` + + +Service operations +------------------ + +Creation +~~~~~~~~ + + +Let's **create** a very simple record: + +>>> from flask import Flask, g +>>> app = Flask(__name__) + +>>> from invenio_records_marc21 import InvenioRecordsMARC21 +>>> ext = InvenioRecordsMARC21(app) + +>>> from invenio_records_marc21.proxies import current_records_marc21 +>>> service = current_records_marc21.record_resource +>>> draft = service.create(identity=g.identity,data={"metadata": {"title": "The title of the record"}}) + + +A new row has been added to the database, in the table ``marc21_drafts_metadata``: +this corresponds to the record metadata, first version (version 1). + + +Publish +~~~~~~~~ + + +Let's **publish** our very simple record: + + +>>> record = service.publish(identity=g.identity,id_=draft.id) + +A new row has been added to the database, in the table ``marc21_records_metadata``: +this corresponds to the record metadata, second version (version 2). The created +Draft from before has been deleted. + + +Service Advanced +---------------- + +The Invenio-Records-Marc21 service provides advanced Record creation functionality. + +>>> def create(self, identity, data=None, metadata=Marc21Metadata(), files=False, access=None) -> RecordItem: + + :data: Input data according to the data schema. + :metadata: Input data according to the metadata schema. + :files: enable/disable file support for the record. + :access: provide access additional information + +The api does not only takes a json like object e.q. `data`. The Records can be created with only the metadata. + +Marc21Metadata +~~~~~~~~~~~~~~ + +>>> from invenio_records_marc21.services.record import Marc21Metadata +>>> metadata = Marc21Metadata() +>>> metadata.emplace_field(tag="245", ind1="1", ind2="0", value="nulla sunt laborum") + +or set a whole Marc21 xml string + +>>> metadata.xml = "00000nam a2200000zca4500" + +Access +~~~~~~ + +The access dict structure required for Invenio-Records-Marc21 records: + +>>> access = { +... "record": "open/restricted", +... "files": "open/restricted", +... "embargo": { +... "active": True/False, +... "until": "YYYY-MM-DD", +... "reason": "Reason", +... } +... } + +See :doc:`api` for extensive API documentation. + +""" from __future__ import absolute_import, print_function diff --git a/invenio_records_marc21/records/api.py b/invenio_records_marc21/records/api.py index 05f74941..abc74b48 100644 --- a/invenio_records_marc21/records/api.py +++ b/invenio_records_marc21/records/api.py @@ -29,7 +29,6 @@ PIDField, PIDStatusCheckField, ) -from werkzeug.local import LocalProxy from . import models from .systemfields import ( @@ -150,3 +149,6 @@ class Marc21Record(Record): is_published = PIDStatusCheckField(status=PIDStatus.REGISTERED, dump=True) pids = DictField("pids") + + +RecordFile.record_cls = Marc21Record diff --git a/invenio_records_marc21/services/services.py b/invenio_records_marc21/services/services.py index 823b345f..28c8b73f 100644 --- a/invenio_records_marc21/services/services.py +++ b/invenio_records_marc21/services/services.py @@ -18,29 +18,28 @@ from invenio_records_resources.services.files.service import FileService from invenio_records_resources.services.records.results import RecordItem -from .config import ( - Marc21DraftFilesServiceConfig, - Marc21RecordFilesServiceConfig, - Marc21RecordServiceConfig, -) +from .config import Marc21DraftFilesServiceConfig, Marc21RecordFilesServiceConfig from .errors import EmbargoNotLiftedError from .record import Marc21Metadata class Marc21RecordService(RecordService): - """Marc21 record service.""" - - config_name = "MARC21_RECORDS_SERVICE_CONFIG" - default_config = Marc21RecordServiceConfig + """Marc21 record service class.""" def _create_data(self, identity, data, metadata, files=False, access=None): """Create a data json. :param identity: Identity of user creating the record. - :param Metadata metadata: Input data according to the metadata schema. - :param bool files: enabledisable file support. + :type identity: `flask_principal.identity` + :param data: Input data according to the data schema. + :type data: dict + :param metadata: Input data according to the metadata schema. + :type metadata: `services.record.Marc21Metadata` + :param files: enable/disable file support for the record. + :type files: bool :param dict access: provide access additional information - :return data: marc21 record data + :return: marc21 record dict + :rtype: dict """ if data is None: data = {"metadata": {"xml": metadata.xml, "json": metadata.json}} @@ -64,10 +63,16 @@ def create( """Create a draft record. :param identity: Identity of user creating the record. - :param dict data: Input data according to the data schema. - :param Marc21Metadata metadata: Input data according to the metadata schema. - :param bool files: enable/disable file support for the record. + :type identity: `flask_principal.identity` + :param data: Input data according to the data schema. + :type data: dict + :param metadata: Input data according to the metadata schema. + :type metadata: `services.record.Marc21Metadata` + :param files: enable/disable file support for the record. + :type files: bool :param dict access: provide access additional information + :return: marc21 record item + :rtype: `invenio_records_resources.services.records.results.RecordItem` """ data = self._create_data(identity, data, metadata, files, access) return super().create(identity, data) @@ -84,10 +89,16 @@ def update_draft( """Update a draft record. :param identity: Identity of user creating the record. - :param dict data: Input data according to the data schema. - :param Marc21Metadata metadata: Input data according to the metadata schema. - :param links_config: Links configuration. + :type identity: `flask_principal.identity` + :param data: Input data according to the data schema. + :type data: dict + :param metadata: Input data according to the metadata schema. + :type metadata: `services.record.Marc21Metadata` + :param files: enable/disable file support for the record. + :type files: bool :param dict access: provide access additional information + :return: marc21 record item + :rtype: `invenio_records_resources.services.records.results.RecordItem` """ data = self._create_data(identity, data, metadata, access) return super().update_draft(id_, identity, data, revision_id) From 98c9434e2260b22f44495729ac951c00d757f38d Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Thu, 4 Nov 2021 10:27:56 +0100 Subject: [PATCH 137/217] bugfix: initialize Marc21Record to RecordFile --- invenio_records_marc21/records/api.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/invenio_records_marc21/records/api.py b/invenio_records_marc21/records/api.py index 05f74941..4878f6cd 100644 --- a/invenio_records_marc21/records/api.py +++ b/invenio_records_marc21/records/api.py @@ -150,3 +150,6 @@ class Marc21Record(Record): is_published = PIDStatusCheckField(status=PIDStatus.REGISTERED, dump=True) pids = DictField("pids") + + +RecordFile.record_cls = Marc21Record From 10e6e75cf6db6634cf2a49c4eebabcb48759c2a5 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Thu, 4 Nov 2021 12:52:36 +0100 Subject: [PATCH 138/217] bugfix: typehints breaks autodocs --- docs/conf.py | 1 + setup.py | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index 1417ea47..63d72259 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -25,6 +25,7 @@ # ones. extensions = [ "sphinx.ext.autodoc", + "sphinx_autodoc_typehints", "sphinx.ext.coverage", "sphinx.ext.doctest", "sphinx.ext.intersphinx", diff --git a/setup.py b/setup.py index 0b05b08c..be331763 100644 --- a/setup.py +++ b/setup.py @@ -32,6 +32,7 @@ extras_require = { "docs": [ "Sphinx>=3,<4", + "sphinx-autodoc-typehints>=1.10.3", ], "elasticsearch7": [ "invenio-search[elasticsearch7]{}".format(invenio_search_version), From 4bfdd66ba03142db23789b71ed87c052bf65b854 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Thu, 4 Nov 2021 15:16:01 +0100 Subject: [PATCH 139/217] docs: modify marc21 service documentation --- INSTALL.rst | 2 + invenio_records_marc21/__init__.py | 100 +++++++++++++++++++++++++---- 2 files changed, 88 insertions(+), 14 deletions(-) diff --git a/INSTALL.rst b/INSTALL.rst index 8817eada..c8a54494 100644 --- a/INSTALL.rst +++ b/INSTALL.rst @@ -13,6 +13,8 @@ The installation [options] supported for the Marc21 module: ``docs`` for documentation building dependencies; + ``elasticsearch7`` + to use elasticsearch version 7 backend; ``postgresql`` to use PostgreSQL database backend; ``tests`` diff --git a/invenio_records_marc21/__init__.py b/invenio_records_marc21/__init__.py index 29feda39..a7004c47 100644 --- a/invenio_records_marc21/__init__.py +++ b/invenio_records_marc21/__init__.py @@ -31,21 +31,86 @@ Service operations ------------------ -Creation -~~~~~~~~ - -Let's **create** a very simple record: +Initialization +~~~~~~~~~~~~~~ ->>> from flask import Flask, g ->>> app = Flask(__name__) +>>> import os +>>> db_url = os.environ.get('SQLALCHEMY_DATABASE_URI', 'sqlite://') +>>> from flask import Flask +>>> app = Flask('myapp') +>>> app.config.update({ +... "JSONSCHEMAS_HOST": "not-used", +... "RECORDS_REFRESOLVER_CLS": "invenio_records.resolver.InvenioRefResolver", +... "RECORDS_REFRESOLVER_STORE": "invenio_jsonschemas.proxies.current_refresolver_store", +... "SQLALCHEMY_DATABASE_URI": db_url, +... "SQLALCHEMY_TRACK_MODIFICATIONS": False, +... }) + +Initialize Invenio-Records-Marc21 dependencies and Invenio-Records-Marc21 itself: + +>>> from invenio_db import InvenioDB +>>> ext_db = InvenioDB(app) +>>> from invenio_records import InvenioRecords +>>> ext_records = InvenioRecords(app) +>>> from invenio_access import InvenioAccess +>>> ext_access = InvenioAccess(app) +>>> from invenio_files_rest import InvenioFilesREST +>>> ext_rdm = InvenioFilesREST(app) +>>> from invenio_jsonschemas import InvenioJSONSchemas +>>> ext_json = InvenioJSONSchemas(app) +>>> from invenio_search import InvenioSearch +>>> ext_search = InvenioSearch(app) +>>> from invenio_rdm_records import InvenioRDMRecords +>>> ext_rdm = InvenioRDMRecords(app) >>> from invenio_records_marc21 import InvenioRecordsMARC21 >>> ext = InvenioRecordsMARC21(app) +The following examples needs to run in a Flask application context, so +let's push one: + +>>> app.app_context().push() + + +Also, for the examples to work we need to create the database and tables (note, +in this example we use an in-memory SQLite database by default): + +>>> from invenio_db import db +>>> from sqlalchemy_utils.functions import create_database, database_exists, drop_database +>>> if database_exists(str(db.engine.url)): +... drop_database(str(db.engine.url)) +>>> create_database(str(db.engine.url)) +>>> db.create_all() + + +The Invenio-Records-Marc21 module needs also a location for Files upload, if the +record contains Files. + +>>> import tempfile +>>> from invenio_files_rest.models import Location +>>> location_obj = Location( +... name="marc21-file-location", uri=tempfile.mkdtemp(), default=True +... ) + +>>> from invenio_db import db +>>> db.session.add(location_obj) +>>> db.session.commit() + +Creation +~~~~~~~~ + + +Let's **create** a very simple record: + >>> from invenio_records_marc21.proxies import current_records_marc21 ->>> service = current_records_marc21.record_resource ->>> draft = service.create(identity=g.identity,data={"metadata": {"title": "The title of the record"}}) +>>> service = current_records_marc21.records_service + +>>> from flask_principal import Identity +>>> from invenio_access import any_user +>>> identity = Identity(1) +>>> identity.provides.add(any_user) +>>> draft = service.create(identity=identity, data={"metadata": {"title": "The title of the record"}}) A new row has been added to the database, in the table ``marc21_drafts_metadata``: @@ -58,8 +123,11 @@ Let's **publish** our very simple record: - ->>> record = service.publish(identity=g.identity,id_=draft.id) +>>> from flask_principal import Identity +>>> from invenio_access import any_user +>>> identity = Identity(1) +>>> identity.provides.add(any_user) +>>> record = service.publish(identity=identity, id_=draft.id) A new row has been added to the database, in the table ``marc21_records_metadata``: this corresponds to the record metadata, second version (version 2). The created @@ -71,12 +139,16 @@ The Invenio-Records-Marc21 service provides advanced Record creation functionality. ->>> def create(self, identity, data=None, metadata=Marc21Metadata(), files=False, access=None) -> RecordItem: +.. code-block:: + + def create(self, identity, data=None, metadata=Marc21Metadata(), files=False, access=None): + +The Create Method of the Marc21 service takes additional parameters: :data: Input data according to the data schema. - :metadata: Input data according to the metadata schema. + :metadata: Input data according to the metadata schema and provided in the module `invenio_records_marc21.services.record.Marc21Metadata`. :files: enable/disable file support for the record. - :access: provide access additional information + :access: provide additional access information. The api does not only takes a json like object e.q. `data`. The Records can be created with only the metadata. @@ -100,7 +172,7 @@ ... "record": "open/restricted", ... "files": "open/restricted", ... "embargo": { -... "active": True/False, +... "active": False/True, ... "until": "YYYY-MM-DD", ... "reason": "Reason", ... } From 7a51b80486bbab51d46218260cb464b1dc2ac655 Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Fri, 5 Nov 2021 10:25:43 +0100 Subject: [PATCH 140/217] docs: modify documentation --- invenio_records_marc21/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/invenio_records_marc21/__init__.py b/invenio_records_marc21/__init__.py index a7004c47..82f4edf0 100644 --- a/invenio_records_marc21/__init__.py +++ b/invenio_records_marc21/__init__.py @@ -19,7 +19,7 @@ Getting started --------------- -You will learn how to create a new Marc21 records, upload a File and accosiate with the record +You will learn how to create a new Marc21 records, upload a file and accosiate with the record using the programmatic APIs of Invenio-Records-Marc21. @@ -110,7 +110,7 @@ >>> from invenio_access import any_user >>> identity = Identity(1) >>> identity.provides.add(any_user) ->>> draft = service.create(identity=identity, data={"metadata": {"title": "The title of the record"}}) +>>> draft = service.create(identity=identity, data={"metadata": {"title_statement": {"title": "The title of the record"}}}) A new row has been added to the database, in the table ``marc21_drafts_metadata``: @@ -178,6 +178,8 @@ def create(self, identity, data=None, metadata=Marc21Metadata(), files=False, ac ... } ... } + + See :doc:`api` for extensive API documentation. """ From e40f9d8326d92abac6217d83c73a1f645240b8ea Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Mon, 8 Nov 2021 12:31:42 +0100 Subject: [PATCH 141/217] feature: summary section --- invenio_records_marc21/__init__.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/invenio_records_marc21/__init__.py b/invenio_records_marc21/__init__.py index 82f4edf0..efcd9ed0 100644 --- a/invenio_records_marc21/__init__.py +++ b/invenio_records_marc21/__init__.py @@ -178,8 +178,30 @@ def create(self, identity, data=None, metadata=Marc21Metadata(), files=False, ac ... } ... } +Now let us put this all together +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +>>> from invenio_records_marc21.proxies import current_records_marc21 +>>> service = current_records_marc21.records_service + +>>> access = { +... "record": "open", +... "files": "open", +... "embargo": { +... "active": False, +... } +... } +>>> from invenio_records_marc21.services.record import Marc21Metadata +>>> from flask_principal import Identity +>>> from invenio_access import any_user +>>> identity = Identity(1) +>>> identity.provides.add(any_user) +>>> metadata = Marc21Metadata() +>>> metadata.xml = "00000nam a2200000zca4500" +>>> draft = service.create(identity=identity, metadata=metadata, access=access) +>>> record = service.publish(identity=identity, id_=draft.id) + See :doc:`api` for extensive API documentation. """ From 55dab233a9483da582db385ed586108b2594b563 Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Tue, 9 Nov 2021 08:09:52 +0100 Subject: [PATCH 142/217] modification: landing page details adding more details to the record landing page --- .../records/helpers/details.html | 9 +- .../records/macros/detail.html | 84 +++++++++++++++---- 2 files changed, 76 insertions(+), 17 deletions(-) diff --git a/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/details.html b/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/details.html index 081d5d16..02be9a84 100644 --- a/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/details.html +++ b/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/details.html @@ -6,14 +6,17 @@ under the terms of the MIT License; see LICENSE file for more details. #} -{%- from "invenio_records_marc21/records/macros/detail.html" import show_subtitles, show_detail, show_publisher %} +{%- from "invenio_records_marc21/records/macros/detail.html" import show_subtitles, show_detail, show_publisher, show_dissertation, show_location_and_access, show_index_term_genre %} {% set details = marcrecord %}

{{ _('Details')}}


-
+
{{ show_detail(_('Subtitle'), show_subtitles(details.title_statement)) if details.title_statement }} {{ show_detail(_('Publisher'), show_publisher(details.production_publication_distribution_manufacture_and_copyright_notice)) if details.production_publication_distribution_manufacture_and_copyright_notice }} {{ show_detail(_('Publication date'), record.ui.publication_date_l10n) if record.ui.publication_date_l10n }} -
+ {{ show_detail(_('Dissertation'), show_dissertation(details.dissertation_note)) if details.dissertation_note }} + {{ show_detail(_('Location and Access'), show_location_and_access(details.electronic_location_and_access)) if details.electronic_location_and_access }} + {{ show_detail(_('Metadata'), show_index_term_genre(details.index_term_genre_form)) if details.index_term_genre_form }} + diff --git a/invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html b/invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html index 4d92b486..4f638e64 100644 --- a/invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html +++ b/invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html @@ -20,7 +20,8 @@ {%- endmacro %} -{% macro show_detail(title, value) %} + +{% macro show_detail(title, value, type="str") %}
@@ -29,8 +30,11 @@
{% if value is iterable and (value is not string and value is not mapping) %} {%- set value = value|join(', ') %} - {% endif %} + {% elif type == "link" %} + {{ value }} + {%else %} {{ value }} + {% endif %}
@@ -40,13 +44,13 @@ {% for key, value in titles.items() %} {% if key != "title" and key != "nonfiling_characters" %} {% if key == "statement_of_responsibility" %} - {% set key = "Responsible" %} + {% set key = _("Responsible") %} {% elif key == "title_added_entry"%} - {% set key = "Added Title" %} + {% set key = _("Added Title") %} {% elif key == "title_added_entry"%} - {% set key = "Added Title" %} + {% set key = _("Added Title") %} {% elif key == "remainder_of_title"%} - {% set key = "Subtitle" %} + {% set key = _("Subtitle") %} {% endif %} {{ show_detail(key, value) }} {% endif %} @@ -58,27 +62,79 @@ {% macro show_publisher(publishers) %} {% for key, value in publishers.items() %} {% if key == "sequence_of_statements" %} - {% set key = "Sequence" %} + {% set key = _("Sequence") %} {% elif key == "name_of_producer_publisher_distributor_manufacturer"%} - {% set key = "Publisher" %} + {% set key = _("Publisher") %} {% elif key == "place_of_production_publication_distribution_manufacture"%} - {% set key = "Place of publication" %} - {% elif key == "date_of_production_publication_distribution_manufacture_or_copyright_notice"%} - {% set key = "Publication Date" %} + {% set key = _("Place of publication") %} + {% elif key == "date_of_production_publication_distribution_manufacture_or_copyright_notice"%} + {% set key = _("Publication Date") %} + {% elif key == "function_of_entity"%} + {% set key = _("Function") %} + {% endif %} + {{ show_detail(key, value) }} + {% endfor %} +{%- endmacro %} + + +{% macro show_dissertation(dissertation) %} + {% for key, value in dissertation.items() %} + {% if key == "degree_type" %} + {% set key = "Dissertation Type" %} + {% elif key == "name_of_granting_institution"%} + {% set key = "Institution" %} + {% elif key == "year_degree_granted"%} + {% set key = "Year of degree" %} + {% endif %} + {{ show_detail(key, value) }} + {% endfor %} +{%- endmacro %} + + + +{% macro show_location_and_access(location_and_access) %} + {% for key, value in location_and_access.items() %} + {%set type = "str" %} + {% if key == "uniform_resource_identifier" %} + {% set key = _("Link") %} + {% set type = "link" %} + {% elif key == "nonpublic_note"%} + {% set key = _("Note") %} + {% elif key == "materials_specified"%} + {% set key = _("Materials") %} + {% elif key == "access_method"%} + {% set key = _("Access Method") %} + {% endif %} + {{ show_detail(key, value, type) }} + {% endfor %} +{%- endmacro %} + + +{% macro show_index_term_genre(index_term_genre) %} + {% for key, value in index_term_genre.items() %} + {% if key == "authority_record_control_number" %} + {% set key = _("Control number") %} + {% elif key == "genre_form_data_or_focus_term"%} + {% set key = _("Type") %} + {% elif key == "type_of_heading"%} + {% set key = _("Heading") %} + {% elif key == "source_of_term"%} + {% set key = _("Source") %} {% endif %} {{ show_detail(key, value) }} {% endfor %} {%- endmacro %} + {% macro show_pages(pages) %} {% for key, value in publishers.items() %} {% if key == "sequence_of_statements" %} - {% set key = "Sequence" %} + {% set key = _("Sequence") %} {% elif key == "name_of_producer_publisher_distributor_manufacturer"%} - {% set key = "Publisher" %} + {% set key = _("Publisher") %} {% elif key == "place_of_production_publication_distribution_manufacture"%} - {% set key = "Place of publication" %} + {% set key = _("Place of publication") %} {% endif %} {{ show_detail(key, value) }} {% endfor %} From 2ef8d56a32e71839556548c9a584ee128c6bb9da Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Tue, 9 Nov 2021 08:11:21 +0100 Subject: [PATCH 143/217] modification: landing page title adding publisher and location to the record title --- .../invenio_records_marc21/record.html | 6 ++++-- .../records/helpers/title.html | 20 +++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 invenio_records_marc21/templates/invenio_records_marc21/records/helpers/title.html diff --git a/invenio_records_marc21/templates/invenio_records_marc21/record.html b/invenio_records_marc21/templates/invenio_records_marc21/record.html index c3e205d6..638b842d 100644 --- a/invenio_records_marc21/templates/invenio_records_marc21/record.html +++ b/invenio_records_marc21/templates/invenio_records_marc21/record.html @@ -19,7 +19,7 @@ {%- block page_body %} -
+
@@ -56,7 +56,8 @@ {%- endblock record_header -%} {%- block record_title -%} -

{{ marcrecord.get('title_statement').get('title') | sanitize_title() }}

+
+ {%- include "invenio_records_marc21/records/helpers/title.html" %} {%- endblock record_title -%} {%- block record_content -%} @@ -70,6 +71,7 @@

{{ marcrecord.get('title_statement').get('title') | sanitize_title() }}

{%- block record_files -%}
+
{%- include "invenio_records_marc21/records/helpers/files.html" %} {%- endblock record_files -%} diff --git a/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/title.html b/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/title.html new file mode 100644 index 00000000..385511a9 --- /dev/null +++ b/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/title.html @@ -0,0 +1,20 @@ +{# -*- coding: utf-8 -*- + + Copyright (C) 2021 Graz University of Technology. + + Invenio-Records-Marc21 is free software; you can redistribute it and/or modify it + under the terms of the MIT License; see LICENSE file for more details. +#} + + +{% set title = marcrecord.get('title_statement') %} +{% set publisher = marcrecord.get('production_publication_distribution_manufacture_and_copyright_notice') %} +{% if title %} +
+

{{title.get('title') | sanitize_title() }}

+

{{ title.get('statement_of_responsibility')}}

+{% if publisher.get('place_of_production_publication_distribution_manufacture') %} +

{{ publisher.get('place_of_production_publication_distribution_manufacture') + " " + publisher.get('date_of_production_publication_distribution_manufacture_or_copyright_notice') }}

+{% endif %} +
+{% endif %} From b7d3d310d94d07d5f4c2f8860ec2cfda994ac9ab Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Tue, 9 Nov 2021 08:12:23 +0100 Subject: [PATCH 144/217] modification: update translation strings --- .../translations/de/LC_MESSAGES/messages.po | 205 +++++++++++++++--- .../translations/en/LC_MESSAGES/messages.po | 165 +++++++++++++- .../translations/messages.pot | 163 +++++++++++++- 3 files changed, 482 insertions(+), 51 deletions(-) diff --git a/invenio_records_marc21/translations/de/LC_MESSAGES/messages.po b/invenio_records_marc21/translations/de/LC_MESSAGES/messages.po index b8bbae6d..8820c0aa 100644 --- a/invenio_records_marc21/translations/de/LC_MESSAGES/messages.po +++ b/invenio_records_marc21/translations/de/LC_MESSAGES/messages.po @@ -8,8 +8,8 @@ msgid "" msgstr "" "Project-Id-Version: invenio-records-marc21 0.2.1\n" "Report-Msgid-Bugs-To: info@tugraz.at\n" -"POT-Creation-Date: 2021-04-26 12:03+0200\n" -"PO-Revision-Date: 2021-04-26 12:03+0200\n" +"POT-Creation-Date: 2021-11-09 08:02+0100\n" +"PO-Revision-Date: 2021-11-09 08:02+0100\n" "Last-Translator: FULL NAME \n" "Language: de\n" "Language-Team: de \n" @@ -19,18 +19,66 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.9.0\n" -#: invenio_records_marc21/services/schemas/access.py:55 -msgid "Embargo date must be in the future." -msgstr "Das Ende des Embargos muss in der Zukunft liegen." +#: invenio_records_marc21/errors.py:40 +msgid "" +"The table you are looking for does not exist.\n" +"Maybe you missed to re-setup the services after the marc21 module was " +"installed.\n" +"Try to do a invenio-cli services setup -f" +msgstr "" -#: invenio_records_marc21/templates/invenio_records_marc21/index.html:17 -#, python-format -msgid "Welcome to %(module_name)s" -msgstr "Willkommen zu %(module_name)s" +#: invenio_records_marc21/errors.py:45 +msgid "" +"Connection to Services refused.\n" +"Maybe you missed to start the services.\n" +"Try to do a invenio-cli services setup -f" +msgstr "" + +#: invenio_records_marc21/services/config.py:42 +msgid "Access status" +msgstr "" + +#: invenio_records_marc21/services/config.py:44 +msgid "Public" +msgstr "" + +#: invenio_records_marc21/services/config.py:45 +msgid "Embargoed" +msgstr "" + +#: invenio_records_marc21/services/config.py:46 +msgid "Restricted" +msgstr "" + +#: invenio_records_marc21/services/config.py:52 +msgid "State" +msgstr "" + +#: invenio_records_marc21/services/config.py:53 +msgid "Published" +msgstr "" + +#: invenio_records_marc21/services/config.py:53 +msgid "Unpublished" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/deposit.html:11 +msgid "New upload" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/index.html:25 +msgid "Type and press enter to search" +msgstr "" -#: invenio_records_marc21/templates/invenio_records_marc21/record.html:32 +#: invenio_records_marc21/templates/invenio_records_marc21/record.html:34 msgid "Creation date" -msgstr "Erstellungsdatum" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/record.html:48 +#: invenio_records_marc21/templates/invenio_records_marc21/records/helpers/files.html:42 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/files.html:111 +msgid "Reason" +msgstr "" #: invenio_records_marc21/templates/invenio_records_marc21/tombstone.html:11 msgid "Tombstone" @@ -38,7 +86,7 @@ msgstr "" #: invenio_records_marc21/templates/invenio_records_marc21/tombstone.html:14 msgid "Gone" -msgstr "Gelöscht" +msgstr "" #: invenio_records_marc21/templates/invenio_records_marc21/tombstone.html:16 #, python-format @@ -48,62 +96,155 @@ msgid "" "The\n" " metadata of the record is kept for archival purposes.\n" " " -msgstr "\n" -" Der angeforderte Datensatz, wurde gelöscht von %(sitename)s. " -"Die\n" -" Metadaten wurden archiviert.\n" -" " +msgstr "" #: invenio_records_marc21/templates/invenio_records_marc21/records/export.html:27 msgid "Back to details" -msgstr "Zurück zu den Details" +msgstr "" #: invenio_records_marc21/templates/invenio_records_marc21/records/details/subjects.html:12 #: invenio_records_marc21/templates/invenio_records_marc21/records/helpers/subjects.html:12 msgid "Subjects" -msgstr "Themen" +msgstr "" #: invenio_records_marc21/templates/invenio_records_marc21/records/helpers/authors.html:13 msgid "Rights Holder(s)" -msgstr "Rechteinhaber" +msgstr "" #: invenio_records_marc21/templates/invenio_records_marc21/records/helpers/description.html:15 msgid "Desciption" -msgstr "Beschreibung" +msgstr "" #: invenio_records_marc21/templates/invenio_records_marc21/records/helpers/details.html:12 msgid "Details" -msgstr "Details" +msgstr "" #: invenio_records_marc21/templates/invenio_records_marc21/records/helpers/details.html:15 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:53 msgid "Subtitle" -msgstr "Untertitel" +msgstr "" #: invenio_records_marc21/templates/invenio_records_marc21/records/helpers/details.html:16 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:67 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:135 msgid "Publisher" -msgstr "Herausgeber" +msgstr "" #: invenio_records_marc21/templates/invenio_records_marc21/records/helpers/details.html:17 msgid "Publication date" -msgstr "Erscheinungsdatum" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/helpers/details.html:18 +msgid "Dissertation" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/helpers/details.html:19 +msgid "Location and Access" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/helpers/details.html:20 +msgid "Metadata" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/helpers/files.html:34 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/files.html:101 +msgid "Files" +msgstr "" #: invenio_records_marc21/templates/invenio_records_marc21/records/helpers/footer.html:14 msgid "Upload infromation" -msgstr "Upload infromation" +msgstr "" #: invenio_records_marc21/templates/invenio_records_marc21/records/helpers/footer.html:18 msgid "Created:" -msgstr "Erstellt:" +msgstr "" #: invenio_records_marc21/templates/invenio_records_marc21/records/helpers/footer.html:18 msgid "Modified:" -msgstr "Bearbeitet:" +msgstr "" #: invenio_records_marc21/templates/invenio_records_marc21/records/helpers/side_bar.html:10 msgid "Export" -msgstr "Export" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:47 +msgid "Responsible" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:49 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:51 +msgid "Added Title" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:65 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:133 +msgid "Sequence" +msgstr "" -#: invenio_records_marc21/vocabularies/vocabulary.py:106 -msgid "Invalid value. Choose one of {choices}." -msgstr "Wählen Sie bitte eines aus {choice}" +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:69 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:137 +msgid "Place of publication" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:71 +msgid "Publication Date" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:73 +msgid "Function" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:99 +msgid "Link" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:102 +msgid "Note" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:104 +msgid "Materials" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:106 +msgid "Access Method" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:116 +msgid "Control number" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:118 +msgid "Type" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:120 +msgid "Heading" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:122 +msgid "Source" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/files.html:51 +msgid "Name" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/files.html:52 +msgid "Size" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/files.html:71 +msgid "" +"This is the file fingerprint (checksum), which can be used to verify the " +"file integrity." +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/files.html:81 +msgid "Preview" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/files.html:86 +msgid "Download" +msgstr "" diff --git a/invenio_records_marc21/translations/en/LC_MESSAGES/messages.po b/invenio_records_marc21/translations/en/LC_MESSAGES/messages.po index 12a2e8f0..5cedad66 100644 --- a/invenio_records_marc21/translations/en/LC_MESSAGES/messages.po +++ b/invenio_records_marc21/translations/en/LC_MESSAGES/messages.po @@ -8,8 +8,8 @@ msgid "" msgstr "" "Project-Id-Version: invenio-records-marc21 0.2.1\n" "Report-Msgid-Bugs-To: info@tugraz.at\n" -"POT-Creation-Date: 2021-04-26 12:03+0200\n" -"PO-Revision-Date: 2021-04-26 12:03+0200\n" +"POT-Creation-Date: 2021-11-09 08:02+0100\n" +"PO-Revision-Date: 2021-11-09 08:02+0100\n" "Last-Translator: FULL NAME \n" "Language: en\n" "Language-Team: en \n" @@ -19,19 +19,67 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.9.0\n" -#: invenio_records_marc21/services/schemas/access.py:55 -msgid "Embargo date must be in the future." +#: invenio_records_marc21/errors.py:40 +msgid "" +"The table you are looking for does not exist.\n" +"Maybe you missed to re-setup the services after the marc21 module was " +"installed.\n" +"Try to do a invenio-cli services setup -f" msgstr "" -#: invenio_records_marc21/templates/invenio_records_marc21/index.html:17 -#, python-format -msgid "Welcome to %(module_name)s" +#: invenio_records_marc21/errors.py:45 +msgid "" +"Connection to Services refused.\n" +"Maybe you missed to start the services.\n" +"Try to do a invenio-cli services setup -f" +msgstr "" + +#: invenio_records_marc21/services/config.py:42 +msgid "Access status" +msgstr "" + +#: invenio_records_marc21/services/config.py:44 +msgid "Public" +msgstr "" + +#: invenio_records_marc21/services/config.py:45 +msgid "Embargoed" +msgstr "" + +#: invenio_records_marc21/services/config.py:46 +msgid "Restricted" +msgstr "" + +#: invenio_records_marc21/services/config.py:52 +msgid "State" +msgstr "" + +#: invenio_records_marc21/services/config.py:53 +msgid "Published" +msgstr "" + +#: invenio_records_marc21/services/config.py:53 +msgid "Unpublished" msgstr "" -#: invenio_records_marc21/templates/invenio_records_marc21/record.html:32 +#: invenio_records_marc21/templates/invenio_records_marc21/deposit.html:11 +msgid "New upload" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/index.html:25 +msgid "Type and press enter to search" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/record.html:34 msgid "Creation date" msgstr "" +#: invenio_records_marc21/templates/invenio_records_marc21/record.html:48 +#: invenio_records_marc21/templates/invenio_records_marc21/records/helpers/files.html:42 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/files.html:111 +msgid "Reason" +msgstr "" + #: invenio_records_marc21/templates/invenio_records_marc21/tombstone.html:11 msgid "Tombstone" msgstr "" @@ -72,10 +120,13 @@ msgid "Details" msgstr "" #: invenio_records_marc21/templates/invenio_records_marc21/records/helpers/details.html:15 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:53 msgid "Subtitle" msgstr "" #: invenio_records_marc21/templates/invenio_records_marc21/records/helpers/details.html:16 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:67 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:135 msgid "Publisher" msgstr "" @@ -83,6 +134,23 @@ msgstr "" msgid "Publication date" msgstr "" +#: invenio_records_marc21/templates/invenio_records_marc21/records/helpers/details.html:18 +msgid "Dissertation" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/helpers/details.html:19 +msgid "Location and Access" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/helpers/details.html:20 +msgid "Metadata" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/helpers/files.html:34 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/files.html:101 +msgid "Files" +msgstr "" + #: invenio_records_marc21/templates/invenio_records_marc21/records/helpers/footer.html:14 msgid "Upload infromation" msgstr "" @@ -99,7 +167,84 @@ msgstr "" msgid "Export" msgstr "" -#: invenio_records_marc21/vocabularies/vocabulary.py:106 -msgid "Invalid value. Choose one of {choices}." +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:47 +msgid "Responsible" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:49 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:51 +msgid "Added Title" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:65 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:133 +msgid "Sequence" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:69 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:137 +msgid "Place of publication" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:71 +msgid "Publication Date" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:73 +msgid "Function" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:99 +msgid "Link" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:102 +msgid "Note" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:104 +msgid "Materials" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:106 +msgid "Access Method" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:116 +msgid "Control number" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:118 +msgid "Type" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:120 +msgid "Heading" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:122 +msgid "Source" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/files.html:51 +msgid "Name" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/files.html:52 +msgid "Size" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/files.html:71 +msgid "" +"This is the file fingerprint (checksum), which can be used to verify the " +"file integrity." +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/files.html:81 +msgid "Preview" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/files.html:86 +msgid "Download" msgstr "" diff --git a/invenio_records_marc21/translations/messages.pot b/invenio_records_marc21/translations/messages.pot index 76f39c03..3cc6d15c 100644 --- a/invenio_records_marc21/translations/messages.pot +++ b/invenio_records_marc21/translations/messages.pot @@ -9,7 +9,7 @@ msgid "" msgstr "" "Project-Id-Version: invenio-records-marc21 0.2.1\n" "Report-Msgid-Bugs-To: info@tugraz.at\n" -"POT-Creation-Date: 2021-04-26 12:03+0200\n" +"POT-Creation-Date: 2021-11-09 08:02+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -18,19 +18,67 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.9.0\n" -#: invenio_records_marc21/services/schemas/access.py:55 -msgid "Embargo date must be in the future." +#: invenio_records_marc21/errors.py:40 +msgid "" +"The table you are looking for does not exist.\n" +"Maybe you missed to re-setup the services after the marc21 module was " +"installed.\n" +"Try to do a invenio-cli services setup -f" msgstr "" -#: invenio_records_marc21/templates/invenio_records_marc21/index.html:17 -#, python-format -msgid "Welcome to %(module_name)s" +#: invenio_records_marc21/errors.py:45 +msgid "" +"Connection to Services refused.\n" +"Maybe you missed to start the services.\n" +"Try to do a invenio-cli services setup -f" +msgstr "" + +#: invenio_records_marc21/services/config.py:42 +msgid "Access status" +msgstr "" + +#: invenio_records_marc21/services/config.py:44 +msgid "Public" +msgstr "" + +#: invenio_records_marc21/services/config.py:45 +msgid "Embargoed" +msgstr "" + +#: invenio_records_marc21/services/config.py:46 +msgid "Restricted" +msgstr "" + +#: invenio_records_marc21/services/config.py:52 +msgid "State" +msgstr "" + +#: invenio_records_marc21/services/config.py:53 +msgid "Published" +msgstr "" + +#: invenio_records_marc21/services/config.py:53 +msgid "Unpublished" msgstr "" -#: invenio_records_marc21/templates/invenio_records_marc21/record.html:32 +#: invenio_records_marc21/templates/invenio_records_marc21/deposit.html:11 +msgid "New upload" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/index.html:25 +msgid "Type and press enter to search" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/record.html:34 msgid "Creation date" msgstr "" +#: invenio_records_marc21/templates/invenio_records_marc21/record.html:48 +#: invenio_records_marc21/templates/invenio_records_marc21/records/helpers/files.html:42 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/files.html:111 +msgid "Reason" +msgstr "" + #: invenio_records_marc21/templates/invenio_records_marc21/tombstone.html:11 msgid "Tombstone" msgstr "" @@ -71,10 +119,13 @@ msgid "Details" msgstr "" #: invenio_records_marc21/templates/invenio_records_marc21/records/helpers/details.html:15 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:53 msgid "Subtitle" msgstr "" #: invenio_records_marc21/templates/invenio_records_marc21/records/helpers/details.html:16 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:67 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:135 msgid "Publisher" msgstr "" @@ -82,6 +133,23 @@ msgstr "" msgid "Publication date" msgstr "" +#: invenio_records_marc21/templates/invenio_records_marc21/records/helpers/details.html:18 +msgid "Dissertation" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/helpers/details.html:19 +msgid "Location and Access" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/helpers/details.html:20 +msgid "Metadata" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/helpers/files.html:34 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/files.html:101 +msgid "Files" +msgstr "" + #: invenio_records_marc21/templates/invenio_records_marc21/records/helpers/footer.html:14 msgid "Upload infromation" msgstr "" @@ -98,7 +166,84 @@ msgstr "" msgid "Export" msgstr "" -#: invenio_records_marc21/vocabularies/vocabulary.py:106 -msgid "Invalid value. Choose one of {choices}." +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:47 +msgid "Responsible" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:49 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:51 +msgid "Added Title" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:65 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:133 +msgid "Sequence" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:69 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:137 +msgid "Place of publication" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:71 +msgid "Publication Date" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:73 +msgid "Function" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:99 +msgid "Link" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:102 +msgid "Note" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:104 +msgid "Materials" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:106 +msgid "Access Method" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:116 +msgid "Control number" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:118 +msgid "Type" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:120 +msgid "Heading" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:122 +msgid "Source" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/files.html:51 +msgid "Name" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/files.html:52 +msgid "Size" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/files.html:71 +msgid "" +"This is the file fingerprint (checksum), which can be used to verify the " +"file integrity." +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/files.html:81 +msgid "Preview" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/files.html:86 +msgid "Download" msgstr "" From 9d6250071e836db6b7781e8d34d02d7ef4eada68 Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Tue, 9 Nov 2021 08:15:34 +0100 Subject: [PATCH 145/217] modification: stylesheet adding records landing page stylesheet for details and title section. --- .../invenio_records_marc21/landing-page.less | 36 +++++++++++++++++++ .../less/invenio_records_marc21/theme.less | 1 + 2 files changed, 37 insertions(+) create mode 100644 invenio_records_marc21/ui/theme/assets/semantic-ui/less/invenio_records_marc21/landing-page.less diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/less/invenio_records_marc21/landing-page.less b/invenio_records_marc21/ui/theme/assets/semantic-ui/less/invenio_records_marc21/landing-page.less new file mode 100644 index 00000000..fb9e053d --- /dev/null +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/less/invenio_records_marc21/landing-page.less @@ -0,0 +1,36 @@ +// This file is part of Invenio. +// +// Copyright (C) 2021 Graz University of Technology. +// +// Invenio-Records-Marc21 is free software; you can redistribute it and/or +// modify it under the terms of the MIT License; see LICENSE file for more +// details. + + +/** Landing Page page styles */ +#marc21-landing-page-record-title { + p { + margin: 0em 0em .1em; + } + + h1 { + margin: 0em 0em .1em; + } +} + +#marc21-landing-page-details { + + .ui.grid + .grid { + margin-top: 0rem; + } + + .row { + margin: 0.1rem 0.3rem 0.1rem; + padding: .1rem .1rem .1rem; // top left/right bottom + } + .column { + margin: 0rem; + padding: .4rem .9rem .4rem; // top left/right bottom + } + +} \ No newline at end of file diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/less/invenio_records_marc21/theme.less b/invenio_records_marc21/ui/theme/assets/semantic-ui/less/invenio_records_marc21/theme.less index d22c4238..1042b2ef 100644 --- a/invenio_records_marc21/ui/theme/assets/semantic-ui/less/invenio_records_marc21/theme.less +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/less/invenio_records_marc21/theme.less @@ -9,3 +9,4 @@ @import "variables.less"; @import "deposit.less"; +@import "landing-page.less"; \ No newline at end of file From aa17b14a2697bc35c6d2628698b577c27b9e5820 Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Tue, 9 Nov 2021 08:16:21 +0100 Subject: [PATCH 146/217] build: remove unnaccessary dependencies --- setup.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/setup.py b/setup.py index be331763..2fedb614 100644 --- a/setup.py +++ b/setup.py @@ -18,9 +18,8 @@ history = open("CHANGES.rst").read() tests_require = [ - "invenio-app>=1.3.0,<2.0.0", - "invenio-indexer>=1.2.0", "pytest-invenio>=1.4.0,<2.0.0", + "invenio-app>=1.3.0,<2.0.0", "mock>=4.0.3", "invenio-previewer>=1.3.4", ] @@ -58,8 +57,6 @@ "arrow>=1.0.0", "dojson>=1.4.0", "lxml>=4.6.2", - "invenio-celery>=1.2.0,<2.0.0", - "invenio-drafts-resources>=0.14.1", "invenio-rdm-records>=0.32.2,<1.0.0", ] From 1f0d7cc0690472ad9e0970efb84c6e8ae4af4476 Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Tue, 9 Nov 2021 10:51:25 +0100 Subject: [PATCH 147/217] modification: remove unwanted details --- .../records/helpers/details.html | 4 +- .../records/macros/detail.html | 76 +++++++++---------- 2 files changed, 37 insertions(+), 43 deletions(-) diff --git a/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/details.html b/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/details.html index 02be9a84..ae6582e2 100644 --- a/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/details.html +++ b/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/details.html @@ -6,13 +6,13 @@ under the terms of the MIT License; see LICENSE file for more details. #} -{%- from "invenio_records_marc21/records/macros/detail.html" import show_subtitles, show_detail, show_publisher, show_dissertation, show_location_and_access, show_index_term_genre %} +{%- from "invenio_records_marc21/records/macros/detail.html" import show_personal_name, show_detail, show_publisher, show_dissertation, show_location_and_access, show_index_term_genre %} {% set details = marcrecord %}

{{ _('Details')}}


- {{ show_detail(_('Subtitle'), show_subtitles(details.title_statement)) if details.title_statement }} + {{ show_detail(_('Author'), show_personal_name(details.main_entry_personal_name)) if details.main_entry_personal_name }} {{ show_detail(_('Publisher'), show_publisher(details.production_publication_distribution_manufacture_and_copyright_notice)) if details.production_publication_distribution_manufacture_and_copyright_notice }} {{ show_detail(_('Publication date'), record.ui.publication_date_l10n) if record.ui.publication_date_l10n }} {{ show_detail(_('Dissertation'), show_dissertation(details.dissertation_note)) if details.dissertation_note }} diff --git a/invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html b/invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html index 4f638e64..c05755cb 100644 --- a/invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html +++ b/invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html @@ -40,17 +40,13 @@
{%- endmacro %} -{% macro show_subtitles(titles) %} + +{% macro show_personal_name(titles) %} {% for key, value in titles.items() %} - {% if key != "title" and key != "nonfiling_characters" %} - {% if key == "statement_of_responsibility" %} - {% set key = _("Responsible") %} - {% elif key == "title_added_entry"%} - {% set key = _("Added Title") %} - {% elif key == "title_added_entry"%} - {% set key = _("Added Title") %} - {% elif key == "remainder_of_title"%} - {% set key = _("Subtitle") %} + {% if key != "type_of_personal_name_entry_element" %} + {% if key == "personal_name" %} + {% set key = _("Name") %} + {% set value = titles | personal_name %} {% endif %} {{ show_detail(key, value) }} {% endif %} @@ -61,18 +57,18 @@ {% macro show_publisher(publishers) %} {% for key, value in publishers.items() %} - {% if key == "sequence_of_statements" %} - {% set key = _("Sequence") %} - {% elif key == "name_of_producer_publisher_distributor_manufacturer"%} - {% set key = _("Publisher") %} - {% elif key == "place_of_production_publication_distribution_manufacture"%} - {% set key = _("Place of publication") %} - {% elif key == "date_of_production_publication_distribution_manufacture_or_copyright_notice"%} - {% set key = _("Publication Date") %} - {% elif key == "function_of_entity"%} - {% set key = _("Function") %} + {% if key != "sequence_of_statements" %} + {% if key == "name_of_producer_publisher_distributor_manufacturer"%} + {% set key = _("Publisher") %} + {% elif key == "place_of_production_publication_distribution_manufacture"%} + {% set key = _("Place of publication") %} + {% elif key == "date_of_production_publication_distribution_manufacture_or_copyright_notice"%} + {% set key = _("Publication Date") %} + {% elif key == "function_of_entity"%} + {% set key = _("Function") %} + {% endif %} + {{ show_detail(key, value) }} {% endif %} - {{ show_detail(key, value) }} {% endfor %} {%- endmacro %} @@ -94,34 +90,32 @@ {% macro show_location_and_access(location_and_access) %} {% for key, value in location_and_access.items() %} - {%set type = "str" %} - {% if key == "uniform_resource_identifier" %} - {% set key = _("Link") %} - {% set type = "link" %} - {% elif key == "nonpublic_note"%} - {% set key = _("Note") %} - {% elif key == "materials_specified"%} - {% set key = _("Materials") %} - {% elif key == "access_method"%} - {% set key = _("Access Method") %} + {% if key != "access_method" %} + {%set type = "str" %} + {% if key == "uniform_resource_identifier" %} + {% set key = _("Link") %} + {% set type = "link" %} + {% elif key == "nonpublic_note"%} + {% set key = _("Note") %} + {% elif key == "materials_specified"%} + {% set key = _("Materials") %} + {% endif %} + {{ show_detail(key, value, type) }} {% endif %} - {{ show_detail(key, value, type) }} {% endfor %} {%- endmacro %} {% macro show_index_term_genre(index_term_genre) %} {% for key, value in index_term_genre.items() %} - {% if key == "authority_record_control_number" %} - {% set key = _("Control number") %} - {% elif key == "genre_form_data_or_focus_term"%} - {% set key = _("Type") %} - {% elif key == "type_of_heading"%} - {% set key = _("Heading") %} - {% elif key == "source_of_term"%} - {% set key = _("Source") %} + {% if key != "source_of_term" and key != "type_of_heading" %} + {% if key == "authority_record_control_number" %} + {% set key = _("Control number") %} + {% elif key == "genre_form_data_or_focus_term"%} + {% set key = _("Type") %} + {% endif %} + {{ show_detail(key, value) }} {% endif %} - {{ show_detail(key, value) }} {% endfor %} {%- endmacro %} From 9d0e6109fdf1fcbcdf3865d0e3f515258d7478c7 Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Tue, 9 Nov 2021 10:53:46 +0100 Subject: [PATCH 148/217] feature: personal name filter --- invenio_records_marc21/ui/records/__init__.py | 3 ++- invenio_records_marc21/ui/records/filters.py | 18 +++++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/invenio_records_marc21/ui/records/__init__.py b/invenio_records_marc21/ui/records/__init__.py index 8b0649d2..06f9941e 100644 --- a/invenio_records_marc21/ui/records/__init__.py +++ b/invenio_records_marc21/ui/records/__init__.py @@ -18,7 +18,7 @@ record_permission_denied_error, record_tombstone_error, ) -from .filters import pid_url, sanitize_title +from .filters import pid_url, sanitize_title, personal_name from .records import ( record_detail, record_export, @@ -61,4 +61,5 @@ def init_records_views(blueprint, app): blueprint.add_app_template_filter(pid_url) blueprint.add_app_template_filter(sanitize_title) + blueprint.add_app_template_filter(personal_name) return blueprint diff --git a/invenio_records_marc21/ui/records/filters.py b/invenio_records_marc21/ui/records/filters.py index 445877a0..08ebb0c5 100644 --- a/invenio_records_marc21/ui/records/filters.py +++ b/invenio_records_marc21/ui/records/filters.py @@ -11,13 +11,21 @@ """Filters to be used in the Jinja templates.""" import re - +from flask_babelex import gettext as _ import idutils from dojson.contrib.marc21.utils import create_record from dojson.contrib.to_marc21 import to_marc21 from flask import current_app +PERSONAL_CODES = { + "aut": _("Author") +} + +def get_personal_code(code): + return PERSONAL_CODES.get(code, code) + + def pid_url(identifier, scheme=None, url_scheme="https"): """Convert persistent identifier into a link.""" if scheme is None: @@ -49,3 +57,11 @@ def json_to_marc21(json): def sanitize_title(title): """Sanitize record title.""" return re.sub("[<>]", "", title) + +def personal_name(titles): + """Personal Name for the Frontend.""" + name = titles.get("personal_name") + code = get_personal_code(titles.get("relator_code")) + + return f"{name}[{code}]" + From 23a6f9c0af686bbf698188f05a63bb82be4a69ae Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Tue, 9 Nov 2021 10:59:52 +0100 Subject: [PATCH 149/217] modification: update translations --- .../translations/de/LC_MESSAGES/messages.po | 61 ++++++------------- .../translations/en/LC_MESSAGES/messages.po | 61 ++++++------------- .../translations/messages.pot | 59 ++++++------------ 3 files changed, 59 insertions(+), 122 deletions(-) diff --git a/invenio_records_marc21/translations/de/LC_MESSAGES/messages.po b/invenio_records_marc21/translations/de/LC_MESSAGES/messages.po index 8820c0aa..47e5983e 100644 --- a/invenio_records_marc21/translations/de/LC_MESSAGES/messages.po +++ b/invenio_records_marc21/translations/de/LC_MESSAGES/messages.po @@ -8,8 +8,8 @@ msgid "" msgstr "" "Project-Id-Version: invenio-records-marc21 0.2.1\n" "Report-Msgid-Bugs-To: info@tugraz.at\n" -"POT-Creation-Date: 2021-11-09 08:02+0100\n" -"PO-Revision-Date: 2021-11-09 08:02+0100\n" +"POT-Creation-Date: 2021-11-09 10:57+0100\n" +"PO-Revision-Date: 2021-11-09 10:58+0100\n" "Last-Translator: FULL NAME \n" "Language: de\n" "Language-Team: de \n" @@ -120,13 +120,13 @@ msgid "Details" msgstr "" #: invenio_records_marc21/templates/invenio_records_marc21/records/helpers/details.html:15 -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:53 -msgid "Subtitle" +#: invenio_records_marc21/ui/records/filters.py:22 +msgid "Author" msgstr "" #: invenio_records_marc21/templates/invenio_records_marc21/records/helpers/details.html:16 -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:67 -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:135 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:62 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:129 msgid "Publisher" msgstr "" @@ -167,67 +167,46 @@ msgstr "" msgid "Export" msgstr "" -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:47 -msgid "Responsible" -msgstr "" - -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:49 -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:51 -msgid "Added Title" -msgstr "" - -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:65 -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:133 -msgid "Sequence" +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:48 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/files.html:51 +msgid "Name" msgstr "" -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:69 -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:137 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:64 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:131 msgid "Place of publication" msgstr "" -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:71 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:66 msgid "Publication Date" msgstr "" -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:73 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:68 msgid "Function" msgstr "" -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:99 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:96 msgid "Link" msgstr "" -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:102 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:99 msgid "Note" msgstr "" -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:104 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:101 msgid "Materials" msgstr "" -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:106 -msgid "Access Method" -msgstr "" - -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:116 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:113 msgid "Control number" msgstr "" -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:118 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:115 msgid "Type" msgstr "" -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:120 -msgid "Heading" -msgstr "" - -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:122 -msgid "Source" -msgstr "" - -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/files.html:51 -msgid "Name" +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:127 +msgid "Sequence" msgstr "" #: invenio_records_marc21/templates/invenio_records_marc21/records/macros/files.html:52 diff --git a/invenio_records_marc21/translations/en/LC_MESSAGES/messages.po b/invenio_records_marc21/translations/en/LC_MESSAGES/messages.po index 5cedad66..42756993 100644 --- a/invenio_records_marc21/translations/en/LC_MESSAGES/messages.po +++ b/invenio_records_marc21/translations/en/LC_MESSAGES/messages.po @@ -8,8 +8,8 @@ msgid "" msgstr "" "Project-Id-Version: invenio-records-marc21 0.2.1\n" "Report-Msgid-Bugs-To: info@tugraz.at\n" -"POT-Creation-Date: 2021-11-09 08:02+0100\n" -"PO-Revision-Date: 2021-11-09 08:02+0100\n" +"POT-Creation-Date: 2021-11-09 10:57+0100\n" +"PO-Revision-Date: 2021-11-09 10:58+0100\n" "Last-Translator: FULL NAME \n" "Language: en\n" "Language-Team: en \n" @@ -120,13 +120,13 @@ msgid "Details" msgstr "" #: invenio_records_marc21/templates/invenio_records_marc21/records/helpers/details.html:15 -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:53 -msgid "Subtitle" +#: invenio_records_marc21/ui/records/filters.py:22 +msgid "Author" msgstr "" #: invenio_records_marc21/templates/invenio_records_marc21/records/helpers/details.html:16 -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:67 -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:135 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:62 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:129 msgid "Publisher" msgstr "" @@ -167,67 +167,46 @@ msgstr "" msgid "Export" msgstr "" -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:47 -msgid "Responsible" -msgstr "" - -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:49 -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:51 -msgid "Added Title" -msgstr "" - -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:65 -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:133 -msgid "Sequence" +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:48 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/files.html:51 +msgid "Name" msgstr "" -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:69 -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:137 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:64 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:131 msgid "Place of publication" msgstr "" -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:71 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:66 msgid "Publication Date" msgstr "" -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:73 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:68 msgid "Function" msgstr "" -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:99 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:96 msgid "Link" msgstr "" -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:102 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:99 msgid "Note" msgstr "" -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:104 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:101 msgid "Materials" msgstr "" -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:106 -msgid "Access Method" -msgstr "" - -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:116 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:113 msgid "Control number" msgstr "" -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:118 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:115 msgid "Type" msgstr "" -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:120 -msgid "Heading" -msgstr "" - -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:122 -msgid "Source" -msgstr "" - -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/files.html:51 -msgid "Name" +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:127 +msgid "Sequence" msgstr "" #: invenio_records_marc21/templates/invenio_records_marc21/records/macros/files.html:52 diff --git a/invenio_records_marc21/translations/messages.pot b/invenio_records_marc21/translations/messages.pot index 3cc6d15c..75fbad5f 100644 --- a/invenio_records_marc21/translations/messages.pot +++ b/invenio_records_marc21/translations/messages.pot @@ -9,7 +9,7 @@ msgid "" msgstr "" "Project-Id-Version: invenio-records-marc21 0.2.1\n" "Report-Msgid-Bugs-To: info@tugraz.at\n" -"POT-Creation-Date: 2021-11-09 08:02+0100\n" +"POT-Creation-Date: 2021-11-09 10:58+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -119,13 +119,13 @@ msgid "Details" msgstr "" #: invenio_records_marc21/templates/invenio_records_marc21/records/helpers/details.html:15 -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:53 -msgid "Subtitle" +#: invenio_records_marc21/ui/records/filters.py:22 +msgid "Author" msgstr "" #: invenio_records_marc21/templates/invenio_records_marc21/records/helpers/details.html:16 -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:67 -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:135 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:62 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:129 msgid "Publisher" msgstr "" @@ -166,67 +166,46 @@ msgstr "" msgid "Export" msgstr "" -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:47 -msgid "Responsible" -msgstr "" - -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:49 -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:51 -msgid "Added Title" -msgstr "" - -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:65 -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:133 -msgid "Sequence" +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:48 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/files.html:51 +msgid "Name" msgstr "" -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:69 -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:137 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:64 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:131 msgid "Place of publication" msgstr "" -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:71 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:66 msgid "Publication Date" msgstr "" -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:73 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:68 msgid "Function" msgstr "" -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:99 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:96 msgid "Link" msgstr "" -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:102 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:99 msgid "Note" msgstr "" -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:104 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:101 msgid "Materials" msgstr "" -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:106 -msgid "Access Method" -msgstr "" - -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:116 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:113 msgid "Control number" msgstr "" -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:118 +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:115 msgid "Type" msgstr "" -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:120 -msgid "Heading" -msgstr "" - -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:122 -msgid "Source" -msgstr "" - -#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/files.html:51 -msgid "Name" +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:127 +msgid "Sequence" msgstr "" #: invenio_records_marc21/templates/invenio_records_marc21/records/macros/files.html:52 From 6c0546fa2d94654eaccfe1a5b5436e23ad5365f1 Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Tue, 9 Nov 2021 12:07:03 +0100 Subject: [PATCH 150/217] modification: codestyle --- invenio_records_marc21/ui/records/__init__.py | 2 +- invenio_records_marc21/ui/records/filters.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/invenio_records_marc21/ui/records/__init__.py b/invenio_records_marc21/ui/records/__init__.py index 06f9941e..bd782a0d 100644 --- a/invenio_records_marc21/ui/records/__init__.py +++ b/invenio_records_marc21/ui/records/__init__.py @@ -18,7 +18,7 @@ record_permission_denied_error, record_tombstone_error, ) -from .filters import pid_url, sanitize_title, personal_name +from .filters import personal_name, pid_url, sanitize_title from .records import ( record_detail, record_export, diff --git a/invenio_records_marc21/ui/records/filters.py b/invenio_records_marc21/ui/records/filters.py index 08ebb0c5..686b15f5 100644 --- a/invenio_records_marc21/ui/records/filters.py +++ b/invenio_records_marc21/ui/records/filters.py @@ -58,10 +58,10 @@ def sanitize_title(title): """Sanitize record title.""" return re.sub("[<>]", "", title) + def personal_name(titles): """Personal Name for the Frontend.""" name = titles.get("personal_name") code = get_personal_code(titles.get("relator_code")) - return f"{name}[{code}]" - + return f"{name}[{code}]" From 2d60a1cae865ba0f8bc8378f850d76054cc0ff41 Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Tue, 9 Nov 2021 12:12:13 +0100 Subject: [PATCH 151/217] modification: move string wrapper to own file moving the function get_personal_code function to a file. --- invenio_records_marc21/ui/records/filters.py | 10 ++----- invenio_records_marc21/ui/records/wrappers.py | 28 +++++++++++++++++++ 2 files changed, 30 insertions(+), 8 deletions(-) create mode 100644 invenio_records_marc21/ui/records/wrappers.py diff --git a/invenio_records_marc21/ui/records/filters.py b/invenio_records_marc21/ui/records/filters.py index 686b15f5..fc33695a 100644 --- a/invenio_records_marc21/ui/records/filters.py +++ b/invenio_records_marc21/ui/records/filters.py @@ -11,19 +11,13 @@ """Filters to be used in the Jinja templates.""" import re -from flask_babelex import gettext as _ + import idutils from dojson.contrib.marc21.utils import create_record from dojson.contrib.to_marc21 import to_marc21 from flask import current_app - -PERSONAL_CODES = { - "aut": _("Author") -} - -def get_personal_code(code): - return PERSONAL_CODES.get(code, code) +from .wrappers import get_personal_code def pid_url(identifier, scheme=None, url_scheme="https"): diff --git a/invenio_records_marc21/ui/records/wrappers.py b/invenio_records_marc21/ui/records/wrappers.py new file mode 100644 index 00000000..d4ccbde4 --- /dev/null +++ b/invenio_records_marc21/ui/records/wrappers.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + +"""Records string wrapper.""" + +from flask_babelex import gettext as _ + +PERSONAL_CODES = { + "aut": _("Author"), + "aud": _("Author of dialog"), + "act": _("Actor"), + "arc": _("Architect"), + "bkp": _("Book producer"), + "cre": _("Creator"), + "cur": _("Curator"), +} + + +def get_personal_code(code): + """Getter function for personal codes.""" + return PERSONAL_CODES.get(code, code) From b624cb9cba657d3ad687892f448fbd3ddb23575b Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Tue, 9 Nov 2021 12:17:02 +0100 Subject: [PATCH 152/217] modification: update translation strings --- .../translations/de/LC_MESSAGES/messages.po | 32 ++++++++++++++++--- .../translations/en/LC_MESSAGES/messages.po | 32 ++++++++++++++++--- .../translations/messages.pot | 30 +++++++++++++++-- 3 files changed, 83 insertions(+), 11 deletions(-) diff --git a/invenio_records_marc21/translations/de/LC_MESSAGES/messages.po b/invenio_records_marc21/translations/de/LC_MESSAGES/messages.po index 47e5983e..54283e86 100644 --- a/invenio_records_marc21/translations/de/LC_MESSAGES/messages.po +++ b/invenio_records_marc21/translations/de/LC_MESSAGES/messages.po @@ -8,8 +8,8 @@ msgid "" msgstr "" "Project-Id-Version: invenio-records-marc21 0.2.1\n" "Report-Msgid-Bugs-To: info@tugraz.at\n" -"POT-Creation-Date: 2021-11-09 10:57+0100\n" -"PO-Revision-Date: 2021-11-09 10:58+0100\n" +"POT-Creation-Date: 2021-11-09 12:13+0100\n" +"PO-Revision-Date: 2021-11-09 12:13+0100\n" "Last-Translator: FULL NAME \n" "Language: de\n" "Language-Team: de \n" @@ -17,7 +17,7 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.9.0\n" +"Generated-By: Babel 2.9.1\n" #: invenio_records_marc21/errors.py:40 msgid "" @@ -120,7 +120,7 @@ msgid "Details" msgstr "" #: invenio_records_marc21/templates/invenio_records_marc21/records/helpers/details.html:15 -#: invenio_records_marc21/ui/records/filters.py:22 +#: invenio_records_marc21/ui/records/wrappers.py:16 msgid "Author" msgstr "" @@ -227,3 +227,27 @@ msgstr "" msgid "Download" msgstr "" +#: invenio_records_marc21/ui/records/wrappers.py:17 +msgid "Author of dialog" +msgstr "" + +#: invenio_records_marc21/ui/records/wrappers.py:18 +msgid "Actor" +msgstr "" + +#: invenio_records_marc21/ui/records/wrappers.py:19 +msgid "Architect" +msgstr "" + +#: invenio_records_marc21/ui/records/wrappers.py:20 +msgid "Book producer" +msgstr "" + +#: invenio_records_marc21/ui/records/wrappers.py:21 +msgid "Creator" +msgstr "" + +#: invenio_records_marc21/ui/records/wrappers.py:22 +msgid "Curator" +msgstr "" + diff --git a/invenio_records_marc21/translations/en/LC_MESSAGES/messages.po b/invenio_records_marc21/translations/en/LC_MESSAGES/messages.po index 42756993..8dde70c0 100644 --- a/invenio_records_marc21/translations/en/LC_MESSAGES/messages.po +++ b/invenio_records_marc21/translations/en/LC_MESSAGES/messages.po @@ -8,8 +8,8 @@ msgid "" msgstr "" "Project-Id-Version: invenio-records-marc21 0.2.1\n" "Report-Msgid-Bugs-To: info@tugraz.at\n" -"POT-Creation-Date: 2021-11-09 10:57+0100\n" -"PO-Revision-Date: 2021-11-09 10:58+0100\n" +"POT-Creation-Date: 2021-11-09 12:13+0100\n" +"PO-Revision-Date: 2021-11-09 12:13+0100\n" "Last-Translator: FULL NAME \n" "Language: en\n" "Language-Team: en \n" @@ -17,7 +17,7 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.9.0\n" +"Generated-By: Babel 2.9.1\n" #: invenio_records_marc21/errors.py:40 msgid "" @@ -120,7 +120,7 @@ msgid "Details" msgstr "" #: invenio_records_marc21/templates/invenio_records_marc21/records/helpers/details.html:15 -#: invenio_records_marc21/ui/records/filters.py:22 +#: invenio_records_marc21/ui/records/wrappers.py:16 msgid "Author" msgstr "" @@ -227,3 +227,27 @@ msgstr "" msgid "Download" msgstr "" +#: invenio_records_marc21/ui/records/wrappers.py:17 +msgid "Author of dialog" +msgstr "" + +#: invenio_records_marc21/ui/records/wrappers.py:18 +msgid "Actor" +msgstr "" + +#: invenio_records_marc21/ui/records/wrappers.py:19 +msgid "Architect" +msgstr "" + +#: invenio_records_marc21/ui/records/wrappers.py:20 +msgid "Book producer" +msgstr "" + +#: invenio_records_marc21/ui/records/wrappers.py:21 +msgid "Creator" +msgstr "" + +#: invenio_records_marc21/ui/records/wrappers.py:22 +msgid "Curator" +msgstr "" + diff --git a/invenio_records_marc21/translations/messages.pot b/invenio_records_marc21/translations/messages.pot index 75fbad5f..8b7e7177 100644 --- a/invenio_records_marc21/translations/messages.pot +++ b/invenio_records_marc21/translations/messages.pot @@ -9,14 +9,14 @@ msgid "" msgstr "" "Project-Id-Version: invenio-records-marc21 0.2.1\n" "Report-Msgid-Bugs-To: info@tugraz.at\n" -"POT-Creation-Date: 2021-11-09 10:58+0100\n" +"POT-Creation-Date: 2021-11-09 12:13+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.9.0\n" +"Generated-By: Babel 2.9.1\n" #: invenio_records_marc21/errors.py:40 msgid "" @@ -119,7 +119,7 @@ msgid "Details" msgstr "" #: invenio_records_marc21/templates/invenio_records_marc21/records/helpers/details.html:15 -#: invenio_records_marc21/ui/records/filters.py:22 +#: invenio_records_marc21/ui/records/wrappers.py:16 msgid "Author" msgstr "" @@ -226,3 +226,27 @@ msgstr "" msgid "Download" msgstr "" +#: invenio_records_marc21/ui/records/wrappers.py:17 +msgid "Author of dialog" +msgstr "" + +#: invenio_records_marc21/ui/records/wrappers.py:18 +msgid "Actor" +msgstr "" + +#: invenio_records_marc21/ui/records/wrappers.py:19 +msgid "Architect" +msgstr "" + +#: invenio_records_marc21/ui/records/wrappers.py:20 +msgid "Book producer" +msgstr "" + +#: invenio_records_marc21/ui/records/wrappers.py:21 +msgid "Creator" +msgstr "" + +#: invenio_records_marc21/ui/records/wrappers.py:22 +msgid "Curator" +msgstr "" + From ad70b2da2f9cd2d464a75b9a9a41d74443899a54 Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Tue, 9 Nov 2021 12:33:38 +0100 Subject: [PATCH 153/217] modification: landing page removing relator_code from detail section and improve authors style --- .../templates/invenio_records_marc21/records/macros/detail.html | 2 +- invenio_records_marc21/ui/records/filters.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html b/invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html index c05755cb..5887c4f4 100644 --- a/invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html +++ b/invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html @@ -43,7 +43,7 @@ {% macro show_personal_name(titles) %} {% for key, value in titles.items() %} - {% if key != "type_of_personal_name_entry_element" %} + {% if key != "type_of_personal_name_entry_element" and key != "relator_code" %} {% if key == "personal_name" %} {% set key = _("Name") %} {% set value = titles | personal_name %} diff --git a/invenio_records_marc21/ui/records/filters.py b/invenio_records_marc21/ui/records/filters.py index fc33695a..a7284b54 100644 --- a/invenio_records_marc21/ui/records/filters.py +++ b/invenio_records_marc21/ui/records/filters.py @@ -58,4 +58,4 @@ def personal_name(titles): name = titles.get("personal_name") code = get_personal_code(titles.get("relator_code")) - return f"{name}[{code}]" + return f"{name} [{code}]" From d904e5263410853a811cade34051b343fa92a379 Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Tue, 9 Nov 2021 12:48:36 +0100 Subject: [PATCH 154/217] modification: record author showing the personal_name field as author of the record --- .../invenio_records_marc21/records/helpers/title.html | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/title.html b/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/title.html index 385511a9..a405d8ab 100644 --- a/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/title.html +++ b/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/title.html @@ -12,7 +12,10 @@ {% if title %}

{{title.get('title') | sanitize_title() }}

-

{{ title.get('statement_of_responsibility')}}

+{% set personal = marcrecord.get('main_entry_personal_name') %} +{% if personal %} +

{{ personal.get('personal_name')}}

+{% endif %} {% if publisher.get('place_of_production_publication_distribution_manufacture') %}

{{ publisher.get('place_of_production_publication_distribution_manufacture') + " " + publisher.get('date_of_production_publication_distribution_manufacture_or_copyright_notice') }}

{% endif %} From bee0487bb430c05648e19a7203fd11f02eb6cb61 Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Tue, 9 Nov 2021 13:05:43 +0100 Subject: [PATCH 155/217] modification: change html code --- .../invenio_records_marc21/records/helpers/details.html | 4 ++-- .../invenio_records_marc21/records/helpers/title.html | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/details.html b/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/details.html index ae6582e2..8cfd5659 100644 --- a/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/details.html +++ b/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/details.html @@ -11,12 +11,12 @@ {% set details = marcrecord %}

{{ _('Details')}}


-
+
{{ show_detail(_('Author'), show_personal_name(details.main_entry_personal_name)) if details.main_entry_personal_name }} {{ show_detail(_('Publisher'), show_publisher(details.production_publication_distribution_manufacture_and_copyright_notice)) if details.production_publication_distribution_manufacture_and_copyright_notice }} {{ show_detail(_('Publication date'), record.ui.publication_date_l10n) if record.ui.publication_date_l10n }} {{ show_detail(_('Dissertation'), show_dissertation(details.dissertation_note)) if details.dissertation_note }} {{ show_detail(_('Location and Access'), show_location_and_access(details.electronic_location_and_access)) if details.electronic_location_and_access }} {{ show_detail(_('Metadata'), show_index_term_genre(details.index_term_genre_form)) if details.index_term_genre_form }} -
+
diff --git a/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/title.html b/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/title.html index a405d8ab..624ad509 100644 --- a/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/title.html +++ b/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/title.html @@ -10,7 +10,7 @@ {% set title = marcrecord.get('title_statement') %} {% set publisher = marcrecord.get('production_publication_distribution_manufacture_and_copyright_notice') %} {% if title %} -
+

{{title.get('title') | sanitize_title() }}

{% set personal = marcrecord.get('main_entry_personal_name') %} {% if personal %} @@ -19,5 +19,5 @@

{{title.get('title') | sanitize_title() }}

{% if publisher.get('place_of_production_publication_distribution_manufacture') %}

{{ publisher.get('place_of_production_publication_distribution_manufacture') + " " + publisher.get('date_of_production_publication_distribution_manufacture_or_copyright_notice') }}

{% endif %} -
+
{% endif %} From 9faae1bd5ec6261063336e9da16706e108436537 Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Tue, 9 Nov 2021 13:27:50 +0100 Subject: [PATCH 156/217] codestyle: prettier for less files --- .../less/invenio_records_marc21/deposit.less | 12 +++--------- .../less/invenio_records_marc21/landing-page.less | 13 +++++-------- .../less/invenio_records_marc21/theme.less | 2 +- .../less/invenio_records_marc21/variables.less | 1 - 4 files changed, 9 insertions(+), 19 deletions(-) diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/less/invenio_records_marc21/deposit.less b/invenio_records_marc21/ui/theme/assets/semantic-ui/less/invenio_records_marc21/deposit.less index 2d912717..829dd605 100644 --- a/invenio_records_marc21/ui/theme/assets/semantic-ui/less/invenio_records_marc21/deposit.less +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/less/invenio_records_marc21/deposit.less @@ -6,16 +6,12 @@ // modify it under the terms of the MIT License; see LICENSE file for more // details. - /** Deposit page styles */ #marc21-deposit-form { - .metadata { border: 0px; margin: 0px; padding: 0px; - - } // Metadata specific styles .metadata * { @@ -23,15 +19,13 @@ } .fields { - margin: 0px -0.5rem 0px -0.5rem; padding: 0px; - padding-bottom: .1rem; + padding-bottom: 0.1rem; } // Sidebar specific styles .deposit-sidebar { - .sidebar-buttons { display: flex; } @@ -39,9 +33,9 @@ .save-button { margin-bottom: 0.5em; } - + .preview-button { margin-bottom: 0.5em; } } -} \ No newline at end of file +} diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/less/invenio_records_marc21/landing-page.less b/invenio_records_marc21/ui/theme/assets/semantic-ui/less/invenio_records_marc21/landing-page.less index fb9e053d..0d5c3cb1 100644 --- a/invenio_records_marc21/ui/theme/assets/semantic-ui/less/invenio_records_marc21/landing-page.less +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/less/invenio_records_marc21/landing-page.less @@ -6,31 +6,28 @@ // modify it under the terms of the MIT License; see LICENSE file for more // details. - /** Landing Page page styles */ #marc21-landing-page-record-title { p { - margin: 0em 0em .1em; + margin: 0em 0em 0.1em; } h1 { - margin: 0em 0em .1em; + margin: 0em 0em 0.1em; } } #marc21-landing-page-details { - .ui.grid + .grid { margin-top: 0rem; } .row { margin: 0.1rem 0.3rem 0.1rem; - padding: .1rem .1rem .1rem; // top left/right bottom + padding: 0.1rem 0.1rem 0.1rem; // top left/right bottom } .column { margin: 0rem; - padding: .4rem .9rem .4rem; // top left/right bottom + padding: 0.4rem 0.9rem 0.4rem; // top left/right bottom } - -} \ No newline at end of file +} diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/less/invenio_records_marc21/theme.less b/invenio_records_marc21/ui/theme/assets/semantic-ui/less/invenio_records_marc21/theme.less index 1042b2ef..35bbad9e 100644 --- a/invenio_records_marc21/ui/theme/assets/semantic-ui/less/invenio_records_marc21/theme.less +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/less/invenio_records_marc21/theme.less @@ -9,4 +9,4 @@ @import "variables.less"; @import "deposit.less"; -@import "landing-page.less"; \ No newline at end of file +@import "landing-page.less"; diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/less/invenio_records_marc21/variables.less b/invenio_records_marc21/ui/theme/assets/semantic-ui/less/invenio_records_marc21/variables.less index b46880b0..015c1ec9 100644 --- a/invenio_records_marc21/ui/theme/assets/semantic-ui/less/invenio_records_marc21/variables.less +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/less/invenio_records_marc21/variables.less @@ -7,5 +7,4 @@ // details. /** Marc21 module style variables */ - @depositFontSize: 14px; From ccdfe8a853b21b0c3d9ad39022da46a77e1a66ef Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Thu, 11 Nov 2021 11:21:44 +0100 Subject: [PATCH 157/217] modification: translations marking dissertation detail strings for translation and reinit the ger translation strings --- .../records/macros/detail.html | 6 +-- .../translations/de/LC_MESSAGES/messages.po | 48 ++++++++++++------- .../translations/en/LC_MESSAGES/messages.po | 17 +++++-- .../translations/messages.pot | 14 +++++- 4 files changed, 60 insertions(+), 25 deletions(-) diff --git a/invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html b/invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html index 5887c4f4..938c546f 100644 --- a/invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html +++ b/invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html @@ -76,11 +76,11 @@ {% macro show_dissertation(dissertation) %} {% for key, value in dissertation.items() %} {% if key == "degree_type" %} - {% set key = "Dissertation Type" %} + {% set key = _("Dissertation Type") %} {% elif key == "name_of_granting_institution"%} - {% set key = "Institution" %} + {% set key = _("Institution") %} {% elif key == "year_degree_granted"%} - {% set key = "Year of degree" %} + {% set key = _("Year of degree") %} {% endif %} {{ show_detail(key, value) }} {% endfor %} diff --git a/invenio_records_marc21/translations/de/LC_MESSAGES/messages.po b/invenio_records_marc21/translations/de/LC_MESSAGES/messages.po index 54283e86..9cd8218d 100644 --- a/invenio_records_marc21/translations/de/LC_MESSAGES/messages.po +++ b/invenio_records_marc21/translations/de/LC_MESSAGES/messages.po @@ -64,7 +64,7 @@ msgstr "" #: invenio_records_marc21/templates/invenio_records_marc21/deposit.html:11 msgid "New upload" -msgstr "" +msgstr "Neuer upload" #: invenio_records_marc21/templates/invenio_records_marc21/index.html:25 msgid "Type and press enter to search" @@ -72,7 +72,7 @@ msgstr "" #: invenio_records_marc21/templates/invenio_records_marc21/record.html:34 msgid "Creation date" -msgstr "" +msgstr "Erstellungsdatum" #: invenio_records_marc21/templates/invenio_records_marc21/record.html:48 #: invenio_records_marc21/templates/invenio_records_marc21/records/helpers/files.html:42 @@ -86,7 +86,7 @@ msgstr "" #: invenio_records_marc21/templates/invenio_records_marc21/tombstone.html:14 msgid "Gone" -msgstr "" +msgstr "Gelöscht" #: invenio_records_marc21/templates/invenio_records_marc21/tombstone.html:16 #, python-format @@ -100,24 +100,24 @@ msgstr "" #: invenio_records_marc21/templates/invenio_records_marc21/records/export.html:27 msgid "Back to details" -msgstr "" +msgstr "Zurück zu den Details" #: invenio_records_marc21/templates/invenio_records_marc21/records/details/subjects.html:12 #: invenio_records_marc21/templates/invenio_records_marc21/records/helpers/subjects.html:12 msgid "Subjects" -msgstr "" +msgstr "Themen" #: invenio_records_marc21/templates/invenio_records_marc21/records/helpers/authors.html:13 msgid "Rights Holder(s)" -msgstr "" +msgstr "Rechteinhaber" #: invenio_records_marc21/templates/invenio_records_marc21/records/helpers/description.html:15 msgid "Desciption" -msgstr "" +msgstr "Beschreibung" -#: invenio_records_marc21/templates/invenio_records_marc21/records/helpers/details.html:12 +#: invenio_records_marSsc21/templates/invenio_records_marc21/records/helpers/details.html:12 msgid "Details" -msgstr "" +msgstr "Details" #: invenio_records_marc21/templates/invenio_records_marc21/records/helpers/details.html:15 #: invenio_records_marc21/ui/records/wrappers.py:16 @@ -128,11 +128,11 @@ msgstr "" #: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:62 #: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:129 msgid "Publisher" -msgstr "" +msgstr "Herausgeber" #: invenio_records_marc21/templates/invenio_records_marc21/records/helpers/details.html:17 msgid "Publication date" -msgstr "" +msgstr "Erscheinungsdatum" #: invenio_records_marc21/templates/invenio_records_marc21/records/helpers/details.html:18 msgid "Dissertation" @@ -149,28 +149,28 @@ msgstr "" #: invenio_records_marc21/templates/invenio_records_marc21/records/helpers/files.html:34 #: invenio_records_marc21/templates/invenio_records_marc21/records/macros/files.html:101 msgid "Files" -msgstr "" +msgstr "Dateien" #: invenio_records_marc21/templates/invenio_records_marc21/records/helpers/footer.html:14 msgid "Upload infromation" -msgstr "" +msgstr "Upload infromation" #: invenio_records_marc21/templates/invenio_records_marc21/records/helpers/footer.html:18 msgid "Created:" -msgstr "" +msgstr "Erstellt:" #: invenio_records_marc21/templates/invenio_records_marc21/records/helpers/footer.html:18 msgid "Modified:" -msgstr "" +msgstr "Bearbeitet:" #: invenio_records_marc21/templates/invenio_records_marc21/records/helpers/side_bar.html:10 msgid "Export" -msgstr "" +msgstr "Export" #: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:48 #: invenio_records_marc21/templates/invenio_records_marc21/records/macros/files.html:51 msgid "Name" -msgstr "" +msgstr "Name" #: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:64 #: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:131 @@ -185,9 +185,21 @@ msgstr "" msgid "Function" msgstr "" +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:79 +msgid "Dissertation Type" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:81 +msgid "Institution" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:83 +msgid "Year of degree" +msgstr "" + #: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:96 msgid "Link" -msgstr "" +msgstr "Link" #: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:99 msgid "Note" diff --git a/invenio_records_marc21/translations/en/LC_MESSAGES/messages.po b/invenio_records_marc21/translations/en/LC_MESSAGES/messages.po index 8dde70c0..dbafd680 100644 --- a/invenio_records_marc21/translations/en/LC_MESSAGES/messages.po +++ b/invenio_records_marc21/translations/en/LC_MESSAGES/messages.po @@ -8,8 +8,8 @@ msgid "" msgstr "" "Project-Id-Version: invenio-records-marc21 0.2.1\n" "Report-Msgid-Bugs-To: info@tugraz.at\n" -"POT-Creation-Date: 2021-11-09 12:13+0100\n" -"PO-Revision-Date: 2021-11-09 12:13+0100\n" +"POT-Creation-Date: 2021-11-11 11:13+0100\n" +"PO-Revision-Date: 2021-11-11 11:14+0100\n" "Last-Translator: FULL NAME \n" "Language: en\n" "Language-Team: en \n" @@ -185,6 +185,18 @@ msgstr "" msgid "Function" msgstr "" +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:79 +msgid "Dissertation Type" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:81 +msgid "Institution" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:83 +msgid "Year of degree" +msgstr "" + #: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:96 msgid "Link" msgstr "" @@ -250,4 +262,3 @@ msgstr "" #: invenio_records_marc21/ui/records/wrappers.py:22 msgid "Curator" msgstr "" - diff --git a/invenio_records_marc21/translations/messages.pot b/invenio_records_marc21/translations/messages.pot index 8b7e7177..a80770bc 100644 --- a/invenio_records_marc21/translations/messages.pot +++ b/invenio_records_marc21/translations/messages.pot @@ -9,7 +9,7 @@ msgid "" msgstr "" "Project-Id-Version: invenio-records-marc21 0.2.1\n" "Report-Msgid-Bugs-To: info@tugraz.at\n" -"POT-Creation-Date: 2021-11-09 12:13+0100\n" +"POT-Creation-Date: 2021-11-11 11:13+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -184,6 +184,18 @@ msgstr "" msgid "Function" msgstr "" +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:79 +msgid "Dissertation Type" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:81 +msgid "Institution" +msgstr "" + +#: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:83 +msgid "Year of degree" +msgstr "" + #: invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html:96 msgid "Link" msgstr "" From e0b2edd76fe7566d58d5573855b84038447e58a1 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Thu, 11 Nov 2021 11:22:13 +0100 Subject: [PATCH 158/217] codestyle:change variable name --- .../invenio_records_marc21/records/macros/detail.html | 6 +++--- invenio_records_marc21/ui/records/filters.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html b/invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html index 938c546f..55fada4c 100644 --- a/invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html +++ b/invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html @@ -41,12 +41,12 @@ {%- endmacro %} -{% macro show_personal_name(titles) %} - {% for key, value in titles.items() %} +{% macro show_personal_name(personal) %} + {% for key, value in personal.items() %} {% if key != "type_of_personal_name_entry_element" and key != "relator_code" %} {% if key == "personal_name" %} {% set key = _("Name") %} - {% set value = titles | personal_name %} + {% set value = personal | personal_name %} {% endif %} {{ show_detail(key, value) }} {% endif %} diff --git a/invenio_records_marc21/ui/records/filters.py b/invenio_records_marc21/ui/records/filters.py index a7284b54..7cabcd7d 100644 --- a/invenio_records_marc21/ui/records/filters.py +++ b/invenio_records_marc21/ui/records/filters.py @@ -53,9 +53,9 @@ def sanitize_title(title): return re.sub("[<>]", "", title) -def personal_name(titles): +def personal_name(personal): """Personal Name for the Frontend.""" - name = titles.get("personal_name") - code = get_personal_code(titles.get("relator_code")) + name = personal.get("personal_name") + code = get_personal_code(personal.get("relator_code")) return f"{name} [{code}]" From 91016c03eda11d192b38aa0fbbbefe2903be4fa7 Mon Sep 17 00:00:00 2001 From: Christoph Ladurner Date: Thu, 11 Nov 2021 14:45:15 +0100 Subject: [PATCH 159/217] bugfix: the shown version number on the landing page was wrong --- invenio_records_marc21/resources/serializers/ui/schema.py | 5 ++++- .../templates/invenio_records_marc21/record.html | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/invenio_records_marc21/resources/serializers/ui/schema.py b/invenio_records_marc21/resources/serializers/ui/schema.py index 488085a6..15d67047 100644 --- a/invenio_records_marc21/resources/serializers/ui/schema.py +++ b/invenio_records_marc21/resources/serializers/ui/schema.py @@ -14,8 +14,9 @@ from flask_babelex import get_locale from invenio_rdm_records.resources.serializers.ui.fields import AccessStatusField +from invenio_rdm_records.resources.serializers.ui.schema import record_version from marshmallow_utils.fields import FormatDate as BaseFormatDatetime -from marshmallow_utils.fields import SanitizedUnicode +from marshmallow_utils.fields import Function, SanitizedUnicode from ..schema import Marc21Schema @@ -32,3 +33,5 @@ class Marc21UISchema(Marc21Schema): created = FormatDatetime(attribute="created", format="long") updated = FormatDatetime(attribute="updated", format="long") + + version = Function(record_version) diff --git a/invenio_records_marc21/templates/invenio_records_marc21/record.html b/invenio_records_marc21/templates/invenio_records_marc21/record.html index c3e205d6..5e9a6d3d 100644 --- a/invenio_records_marc21/templates/invenio_records_marc21/record.html +++ b/invenio_records_marc21/templates/invenio_records_marc21/record.html @@ -33,7 +33,7 @@
{{ record.ui.created }} {%- if record.revision_id %} - | Version {{ record.revision_id }} + | Version {{ record.ui.version }} {% endif %}
From 39774937e1824e8bff4cbbde4569d5445ac67e41 Mon Sep 17 00:00:00 2001 From: Christoph Ladurner Date: Thu, 11 Nov 2021 14:55:26 +0100 Subject: [PATCH 160/217] add functionality to set a pre-calculated marcid With that hack it is possible to create a record with a pre-calculated marcid. This approach is necessary because there exists for now some DOI's with marcid (with no corresponding record in the repository) where the records should have then also the same marcid when they will be created then in the repository. To set the pre-calculated marcid it has to be assigned to MarcDraftProvider.predefined_pid_value before the creation of the record with the corresponding service --- .../records/systemfields/providers.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/invenio_records_marc21/records/systemfields/providers.py b/invenio_records_marc21/records/systemfields/providers.py index 32b2a7ca..9dc342bb 100644 --- a/invenio_records_marc21/records/systemfields/providers.py +++ b/invenio_records_marc21/records/systemfields/providers.py @@ -32,3 +32,28 @@ class MarcDraftProvider(MarcRecordProvider): """ default_status_with_obj = PIDStatus.NEW + + predefined_pid_value = "" + + @classmethod + def create(cls, object_type=None, object_uuid=None, options=None, **kwargs): + """Intermediate step to create the marcid. + + With this intermediate step it is possible to set pre-calculated marcid's. + """ + if ( + "record" in kwargs and "$schema" in kwargs["record"] + ) or cls.predefined_pid_value == "": + return super(MarcDraftProvider, cls).create( + object_type=object_type, object_uuid=object_uuid, **kwargs + ) + else: + kwargs["pid_value"] = cls.predefined_pid_value + kwargs.setdefault("status", cls.default_status) + + if object_type and object_uuid: + kwargs["status"] = cls.default_status_with_obj + + return super(RecordIdProviderV2, cls).create( + object_type=object_type, object_uuid=object_uuid, **kwargs + ) From 4aceff369588e9fce543264b1ac942e0c20818d7 Mon Sep 17 00:00:00 2001 From: Christoph Ladurner Date: Thu, 11 Nov 2021 19:43:17 +0100 Subject: [PATCH 161/217] fix tests --- tests/resources/serializers/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/resources/serializers/conftest.py b/tests/resources/serializers/conftest.py index 4095b3ba..aa270457 100644 --- a/tests/resources/serializers/conftest.py +++ b/tests/resources/serializers/conftest.py @@ -169,6 +169,7 @@ def full_record(marc21_record, marc21_metadata): "status": "restricted", } marc21_record["metadata"] = marc21_metadata + marc21_record["versions"] = {"index": 1, "is_latest": True, "is_latest_draft": True} return marc21_record From d8e501528ec305c5317c997f83e167aa11530221 Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Mon, 15 Nov 2021 08:16:58 +0100 Subject: [PATCH 162/217] feature: marc21 records version creating a react component to show a records versions and allow editing a marc21 record --- .../landing_page/EditButton.js | 45 ++++++ .../landing_page/Marc21RecordManagement.js | 50 ++++++ .../landing_page/Marc21RecordVersionsList.js | 144 ++++++++++++++++++ .../landing_page/index.js | 58 +++++++ 4 files changed, 297 insertions(+) create mode 100644 invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/landing_page/EditButton.js create mode 100644 invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/landing_page/Marc21RecordManagement.js create mode 100644 invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/landing_page/Marc21RecordVersionsList.js create mode 100644 invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/landing_page/index.js diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/landing_page/EditButton.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/landing_page/EditButton.js new file mode 100644 index 00000000..11832694 --- /dev/null +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/landing_page/EditButton.js @@ -0,0 +1,45 @@ +// This file is part of Invenio. +// +// Copyright (C) 2021 Northwestern University. +// Copyright (C) 2021 Graz University of Technology. +// +// Invenio-Records-Marc21 is free software; you can redistribute it and/or +// modify it under the terms of the MIT License; see LICENSE file for more +// details. + +import React, { useState } from "react"; +import { Icon, Button } from "semantic-ui-react"; +import axios from "axios"; + +const apiConfig = { + withCredentials: true, + xsrfCookieName: "csrftoken", + xsrfHeaderName: "X-CSRFToken", +}; + +export const axiosWithconfig = axios.create(apiConfig); + +export const EditButton = (props) => { + const [loading, setLoading] = useState(false); + const recid = props.recid; + const handleError = props.onError; + const handleClick = () => { + setLoading(true); + axiosWithconfig + .post(`/api/marc21/${recid}/draft`) + .then((response) => { + window.location = `/marc21/uploads/${recid}`; + }) + .catch((error) => { + setLoading(false); + handleError(error.response.data.message); + }); + }; + + return ( + + ); +}; diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/landing_page/Marc21RecordManagement.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/landing_page/Marc21RecordManagement.js new file mode 100644 index 00000000..270d1718 --- /dev/null +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/landing_page/Marc21RecordManagement.js @@ -0,0 +1,50 @@ +// This file is part of Invenio. +// +// Copyright (C) 2020-2021 CERN. +// Copyright (C) 2020-2021 Northwestern University. +// Copyright (C) 2021 Graz University of Technology. +// +// Invenio-Records-Marc21 is free software; you can redistribute it and/or +// modify it under the terms of the MIT License; see LICENSE file for more +// details. + +import React, { useState } from "react"; +import { Grid, Icon } from "semantic-ui-react"; + +import { EditButton } from "./EditButton"; +import { NewVersionButton } from "react-invenio-deposit"; + +export const RecordManagement = (props) => { + const record = props.record; + const recid = record.id; + const permissions = props.permissions; + const [error, setError] = useState(""); + const handleError = (errorMessage) => { + console.log(errorMessage); + setError(errorMessage); + }; + + return ( + + + + + {"Manage"} + + + + + + {error && ( + +

{error}

+
+ )} +
+
+ ); +}; diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/landing_page/Marc21RecordVersionsList.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/landing_page/Marc21RecordVersionsList.js new file mode 100644 index 00000000..98fc48c6 --- /dev/null +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/landing_page/Marc21RecordVersionsList.js @@ -0,0 +1,144 @@ +// This file is part of Invenio. +// +// Copyright (C) 2020-2021 CERN. +// Copyright (C) 2020-2021 Northwestern University. +// Copyright (C) 2021 Graz University of Technology. +// +// Invenio-Records-Marc21 is free software; you can redistribute it and/or +// modify it under the terms of the MIT License; see LICENSE file for more +// details. + +import axios from "axios"; +import _get from "lodash/get"; +import React, { useEffect, useState } from "react"; +import { Divider, Grid, Icon, Message, Placeholder } from "semantic-ui-react"; + +const deserializeRecord = (record) => ({ + id: record.id, + parent_id: record.parent.id, + publication_date: record.ui.publication_date_l10n_medium, + version: record.ui.version, + links: record.links, + pids: record.pids, +}); + +const NUMBER_OF_VERSIONS = 5; + +const RecordVersionItem = ({ item, activeVersion }) => { + const doi = _get(item.pids, "pk", ""); + return ( + <> + + + + {item.publication_date} + + + {"Version"} {item.version} + + {
} + {doi && ( + + {doi} + + )} +
+
+ + + ); +}; + +const PlaceholderLoader = ({ size = NUMBER_OF_VERSIONS }) => { + const PlaceholderItem = () => ( + + + + + ); + let numberOfHeader = []; + for (let i = 0; i < size; i++) { + numberOfHeader.push(); + } + + return {numberOfHeader}; +}; + +const PreviewMessage = () => { + return ( + + + + + + {"Preview"} + +

{"Only published versions are displayed."}

+
+
+
+ ); +}; + +export const RecordVersionsList = (props) => { + const record = deserializeRecord(props.record); + const { isPreview } = props; + const recid = record.id; + const [loading, setLoading] = useState(true); + const [currentRecordInResults, setCurrentRecordInResults] = useState(false); + const [recordVersions, setRecordVersions] = useState({}); + + useEffect(() => { + async function fetchVersions() { + const result = await axios( + `${record.links.versions}?size=${NUMBER_OF_VERSIONS}&sort=version&allversions=true`, + { + headers: { + Accept: "application/vnd.inveniomarc21.v1+json", + }, + withCredentials: true, + } + ); + let { hits, total } = result.data.hits; + hits = hits.map(deserializeRecord); + setCurrentRecordInResults(hits.some((record) => record.id === recid)); + setRecordVersions({ hits, total }); + setLoading(false); + } + fetchVersions(); + }, []); + + return loading ? ( + <>{isPreview ? : } + ) : ( + + {isPreview ? : null} + {recordVersions.hits.map((item) => ( + + ))} + {!currentRecordInResults && ( + <> + ... + + + )} + + + {`View all ` + recordVersions.total + ` versions`} + + + + ); +}; diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/landing_page/index.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/landing_page/index.js new file mode 100644 index 00000000..65c805ac --- /dev/null +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/landing_page/index.js @@ -0,0 +1,58 @@ +// This file is part of Invenio. +// +// Copyright (C) 2021 Graz University of Technology. +// +// Invenio-Records-Marc21 is free software; you can redistribute it and/or +// modify it under the terms of the MIT License; see LICENSE file for more +// details. + +import $ from "jquery"; +import React from "react"; +import ReactDOM from "react-dom"; + +import { RecordManagement } from "./Marc21RecordManagement"; +import { RecordVersionsList } from "./Marc21RecordVersionsList"; + +const recordManagementAppDiv = document.getElementById("recordManagement"); +const recordVersionsAppDiv = document.getElementById("recordVersions"); + +if (recordManagementAppDiv) { + ReactDOM.render( + , + recordManagementAppDiv + ); +} + +if (recordVersionsAppDiv) { + ReactDOM.render( + , + recordVersionsAppDiv + ); +} + +$(".ui.accordion").accordion({ + selector: { + trigger: ".title .dropdown", + }, +}); + +$(".ui.tooltip-popup").popup(); + +$(".preview-link").on("click", function (event) { + $("#preview").find(".title").html(event.target.dataset.fileKey); +}); + +$("#jump-btn").on("click", function (event) { + document.documentElement.scrollTop = 0; +}); + +// func to toggle the icon class +$(".panel-heading").click(function () { + $("i", this).toggleClass("down right"); +}); From a5fde41210c94c76febaa0e8c95b5d383736b0d3 Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Mon, 15 Nov 2021 08:17:42 +0100 Subject: [PATCH 163/217] modification: build assets marc21 landingpage --- invenio_records_marc21/ui/theme/webpack.py | 1 + 1 file changed, 1 insertion(+) diff --git a/invenio_records_marc21/ui/theme/webpack.py b/invenio_records_marc21/ui/theme/webpack.py index ad009284..76d37966 100644 --- a/invenio_records_marc21/ui/theme/webpack.py +++ b/invenio_records_marc21/ui/theme/webpack.py @@ -22,6 +22,7 @@ "invenio-records-marc21-theme": "./less/invenio_records_marc21/theme.less", "invenio-records-marc21-deposit": "./js/invenio_records_marc21/deposit/index.js", "invenio-records-marc21-search": "./js/invenio_records_marc21/search/index.js", + "invenio-records-marc21-landing-page": "./js/invenio_records_marc21/landing_page/index.js", }, dependencies={ "@babel/runtime": "^7.9.0", From 42ee8d10478711b037a3436e8723e07a8def4dfd Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Mon, 15 Nov 2021 08:18:42 +0100 Subject: [PATCH 164/217] modification: sidebar landing page adding the version creat component to the landing page side bar --- .../invenio_records_marc21/records/helpers/side_bar.html | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/side_bar.html b/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/side_bar.html index 536872c7..2ed735f2 100644 --- a/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/side_bar.html +++ b/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/side_bar.html @@ -18,4 +18,12 @@

{{ _('Export')}}

+
+
{{ _('Versions')}}
+
+
+
+
+
+
From bcb5fb4d24a6622ac8d2eb0c4611d9f2123379ec Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Mon, 15 Nov 2021 08:21:02 +0100 Subject: [PATCH 165/217] modification: add landing page react component --- .../templates/invenio_records_marc21/record.html | 1 + 1 file changed, 1 insertion(+) diff --git a/invenio_records_marc21/templates/invenio_records_marc21/record.html b/invenio_records_marc21/templates/invenio_records_marc21/record.html index 638b842d..44323cbb 100644 --- a/invenio_records_marc21/templates/invenio_records_marc21/record.html +++ b/invenio_records_marc21/templates/invenio_records_marc21/record.html @@ -97,4 +97,5 @@ {%- block javascript %} {{ super() }} +{{ webpack["invenio-records-marc21-landing-page.js"] }} {%- endblock javascript %} From 257023f96ef055d8682b81748b7803874f3d3a87 Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Tue, 16 Nov 2021 12:51:13 +0100 Subject: [PATCH 166/217] bugfix: access protection to record --- invenio_records_marc21/services/services.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invenio_records_marc21/services/services.py b/invenio_records_marc21/services/services.py index 28c8b73f..09e9a37f 100644 --- a/invenio_records_marc21/services/services.py +++ b/invenio_records_marc21/services/services.py @@ -107,7 +107,7 @@ def _lift_embargo_from(self, record): """Lifts embargo from record or draft.""" if not record.access.lift_embargo(): raise EmbargoNotLiftedError(record["id"]) - record.access.protection.metadata = "public" + record.access.protection.record = "public" record.access.protection.files = "public" def _is_draft_access_field_modified(self, draft, record): From 795825632e5dc1fc46dc7ce42abd765b46eec811 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 22 Nov 2021 09:59:39 +0100 Subject: [PATCH 167/217] modification(ui): change sidebar swap sidbar export and versions list --- .../records/helpers/side_bar.html | 48 ++++++++++--------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/side_bar.html b/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/side_bar.html index 2ed735f2..31268597 100644 --- a/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/side_bar.html +++ b/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/side_bar.html @@ -1,29 +1,33 @@ -{# -*- coding: utf-8 -*- - +{# -*- coding: utf-8 -*- + Copyright (C) 2021 Graz University of Technology. - Invenio-Records-Marc21 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-Records-Marc21 is free software; you can redistribute it and/or modify + it under the terms of the MIT License; see LICENSE file for more details. #}
-
-

{{ _('Export')}}

-
-
- {%- for fmt, val in config.get("INVENIO_MARC21_RECORD_EXPORTERS", {}).items() -%} - {%- set name = val.get("name", fmt) -%} - {%- set export_url = url_for('invenio_records_marc21.record_export', pid_value=record.id, export_format=fmt) -%} - {{ name }}
- {%- endfor -%} +
+
+
{{ _('Versions')}}
+
+
+
+
+
+
+

{{ _('Export')}}

+
+
+ {%- for fmt, val in config.get("INVENIO_MARC21_RECORD_EXPORTERS",{}).items() -%} + {%- set name = val.get("name", fmt) -%} + {%- set export_url = url_for('invenio_records_marc21.record_export', pid_value=record.id, export_format=fmt) -%} + {{ name }}
+ {%- endfor -%} +
-
-
-
{{ _('Versions')}}
-
-
-
-
-
-
From 6f61bd815f120bbe2fec8f570665c6bb11466037 Mon Sep 17 00:00:00 2001 From: Christoph Ladurner Date: Mon, 22 Nov 2021 10:54:57 +0100 Subject: [PATCH 168/217] bugfix: not updated the check, after updating the output key --- .../templates/invenio_records_marc21/record.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invenio_records_marc21/templates/invenio_records_marc21/record.html b/invenio_records_marc21/templates/invenio_records_marc21/record.html index 5e9a6d3d..6c160dae 100644 --- a/invenio_records_marc21/templates/invenio_records_marc21/record.html +++ b/invenio_records_marc21/templates/invenio_records_marc21/record.html @@ -32,7 +32,7 @@
{{ record.ui.created }} - {%- if record.revision_id %} + {%- if record.ui.version %} | Version {{ record.ui.version }} {% endif %}
From e986da822ab55392f277f53dc8651bcf058a32f5 Mon Sep 17 00:00:00 2001 From: Christoph Ladurner Date: Mon, 22 Nov 2021 10:55:55 +0100 Subject: [PATCH 169/217] improvement: not use records_version from invenio-rdm-records because there exists no version field in metadata as it does in rdm-records. But there may be the possibility that we will use the marc21 version field 251 in the future. --- invenio_records_marc21/resources/serializers/ui/schema.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/invenio_records_marc21/resources/serializers/ui/schema.py b/invenio_records_marc21/resources/serializers/ui/schema.py index 15d67047..4e14f464 100644 --- a/invenio_records_marc21/resources/serializers/ui/schema.py +++ b/invenio_records_marc21/resources/serializers/ui/schema.py @@ -14,7 +14,6 @@ from flask_babelex import get_locale from invenio_rdm_records.resources.serializers.ui.fields import AccessStatusField -from invenio_rdm_records.resources.serializers.ui.schema import record_version from marshmallow_utils.fields import FormatDate as BaseFormatDatetime from marshmallow_utils.fields import Function, SanitizedUnicode @@ -23,6 +22,13 @@ FormatDatetime = partial(BaseFormatDatetime, locale=get_locale) +def record_version(obj): + """Return record's version.""" + # TODO default should be used the field 251, but that is not yet + # implemented in dojson + return f"v{obj['versions']['index']}" + + class Marc21UISchema(Marc21Schema): """Schema for dumping extra information for the UI.""" From 3d987c8a3d4a5cc1e13a6f733eab7e8fd0ac8970 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 22 Nov 2021 12:47:56 +0100 Subject: [PATCH 170/217] bugfix(ui): record exporter page adding the pass_is_preview decorator for the export page and load the versionlist of a record in the sidebar --- .../templates/invenio_records_marc21/record.html | 2 +- .../invenio_records_marc21/records/export.html | 14 +++++++++----- invenio_records_marc21/ui/records/records.py | 13 +++++++++++-- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/invenio_records_marc21/templates/invenio_records_marc21/record.html b/invenio_records_marc21/templates/invenio_records_marc21/record.html index 44323cbb..dd4a7299 100644 --- a/invenio_records_marc21/templates/invenio_records_marc21/record.html +++ b/invenio_records_marc21/templates/invenio_records_marc21/record.html @@ -71,7 +71,7 @@ {%- block record_files -%}
-
+
{%- include "invenio_records_marc21/records/helpers/files.html" %} {%- endblock record_files -%} diff --git a/invenio_records_marc21/templates/invenio_records_marc21/records/export.html b/invenio_records_marc21/templates/invenio_records_marc21/records/export.html index 2863f5ed..ca2ee19b 100644 --- a/invenio_records_marc21/templates/invenio_records_marc21/records/export.html +++ b/invenio_records_marc21/templates/invenio_records_marc21/records/export.html @@ -13,14 +13,14 @@
- + {% block record_header %}

{{ export_format }} Export

-
+
@@ -32,7 +32,6 @@

{{ export_format }} Export

{% endblock record_header %} {% block record_content %} -
           {{- exported_record -}}
@@ -43,8 +42,8 @@ 

{{ export_format }} Export

{% block record_files %}{% endblock %} {% block record_details %}{% endblock %} {% block record_footer %} - - + + {% endblock %}
{%- endblock page_body %} + +{%- block javascript %} +{{ super() }} +{{ webpack["invenio-records-marc21-landing-page.js"] }} +{%- endblock javascript %} diff --git a/invenio_records_marc21/ui/records/records.py b/invenio_records_marc21/ui/records/records.py index 1794b652..4fd17a5f 100644 --- a/invenio_records_marc21/ui/records/records.py +++ b/invenio_records_marc21/ui/records/records.py @@ -85,8 +85,15 @@ def record_detail(record=None, files=None, pid_value=None, is_preview=False): ) +@pass_is_preview @pass_record_or_draft -def record_export(record=None, export_format=None, pid_value=None, permissions=None): +def record_export( + record=None, + export_format=None, + pid_value=None, + permissions=None, + is_preview=False, +): """Export marc21 record page view.""" exporter = current_app.config.get("INVENIO_MARC21_RECORD_EXPORTERS", {}).get( export_format @@ -111,7 +118,9 @@ def record_export(record=None, export_format=None, pid_value=None, permissions=N export_format=exporter.get("name", export_format), exported_record=exported_record, record=Marc21UIJSONSerializer().dump_one(record.to_dict()), - permissions=permissions, + permissions=record.has_permissions_to(["update_draft"]), + is_preview=is_preview, + is_draft=record._record.is_draft, ) From 5b263359345e93821a129cb0b3d31994ac03e0d4 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 22 Nov 2021 14:03:14 +0100 Subject: [PATCH 171/217] feature: deposit edit view creating a deposit edit endpoint for marc21 records. --- invenio_records_marc21/config.py | 1 + invenio_records_marc21/ui/theme/__init__.py | 3 +- invenio_records_marc21/ui/theme/decorators.py | 78 +++++++++++++++++++ invenio_records_marc21/ui/theme/views.py | 20 +++++ 4 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 invenio_records_marc21/ui/theme/decorators.py diff --git a/invenio_records_marc21/config.py b/invenio_records_marc21/config.py index ace89213..ab562ac5 100644 --- a/invenio_records_marc21/config.py +++ b/invenio_records_marc21/config.py @@ -53,6 +53,7 @@ "index": "/", "record-search": "/search", "deposit-create": "/uploads", + "deposit-edit": "/uploads/", } """Records UI for invenio-records-marc21.""" diff --git a/invenio_records_marc21/ui/theme/__init__.py b/invenio_records_marc21/ui/theme/__init__.py index feebd38c..0e329952 100644 --- a/invenio_records_marc21/ui/theme/__init__.py +++ b/invenio_records_marc21/ui/theme/__init__.py @@ -14,7 +14,7 @@ from flask_babelex import lazy_gettext as _ from flask_menu import current_menu -from .views import deposit_create, index, search +from .views import deposit_create, deposit_edit, index, search # @@ -27,5 +27,6 @@ def init_theme_views(blueprint, app): blueprint.add_url_rule(routes["index"], view_func=index) blueprint.add_url_rule(routes["record-search"], view_func=search) blueprint.add_url_rule(routes["deposit-create"], view_func=deposit_create) + blueprint.add_url_rule(routes["deposit-edit"], view_func=deposit_edit) return blueprint diff --git a/invenio_records_marc21/ui/theme/decorators.py b/invenio_records_marc21/ui/theme/decorators.py new file mode 100644 index 00000000..8aeb7053 --- /dev/null +++ b/invenio_records_marc21/ui/theme/decorators.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + + +"""Routes for record-related pages provided by Invenio-Records-Marc21.""" + +from functools import wraps + +from flask import g +from invenio_records_resources.services.errors import PermissionDeniedError + +from ...proxies import current_records_marc21 + + +def links_config(): + """Get the record links config.""" + return current_records_marc21.record_resource.config.links_config + + +def draft_links_config(): + """Get the drafts links config.""" + return current_records_marc21.record_resource.config.draft_links_config + + +def files_service(): + """Get the record files service.""" + return current_records_marc21.records_service.files + + +def draft_files_service(): + """Get the record files service.""" + return current_records_marc21.records_service.draft_files + + +def service(): + """Get the record service.""" + return current_records_marc21.records_service + + +def pass_draft(f): + """Decorator to retrieve the draft using the record service.""" + + @wraps(f) + def view(**kwargs): + pid_value = kwargs.get("pid_value") + draft = service().read_draft(id_=pid_value, identity=g.identity) + kwargs["draft"] = draft + return f(**kwargs) + + return view + + +def pass_draft_files(f): + """Decorate a view to pass a draft's files using the files service.""" + + @wraps(f) + def view(**kwargs): + try: + pid_value = kwargs.get("pid_value") + files = draft_files_service().list_files(id_=pid_value, identity=g.identity) + kwargs["draft_files"] = files + + except PermissionDeniedError: + # this is handled here because we don't want a 404 on the landing + # page when a user is allowed to read the metadata but not the + # files + kwargs["draft_files"] = None + + return f(**kwargs) + + return view diff --git a/invenio_records_marc21/ui/theme/views.py b/invenio_records_marc21/ui/theme/views.py index 42f09d8a..0921f934 100644 --- a/invenio_records_marc21/ui/theme/views.py +++ b/invenio_records_marc21/ui/theme/views.py @@ -13,6 +13,9 @@ from flask import render_template from flask_login import login_required +from invenio_records_marc21.resources.serializers.ui import Marc21UIXMLSerializer + +from .decorators import pass_draft, pass_draft_files from .deposit import deposit_config, deposit_templates, empty_record @@ -35,3 +38,20 @@ def deposit_create(): templates=deposit_templates(), forms_config=deposit_config(), ) + + +@login_required +@pass_draft +@pass_draft_files +def deposit_edit(draft=None, draft_files=None, pid_value=None): + """Edit an existing deposit.""" + serializer = Marc21UIXMLSerializer() + record = serializer.dump_one(draft.to_dict()) + + return render_template( + "invenio_records_marc21/deposit.html", + forms_config=deposit_config(apiUrl=f"/api/marc21/{pid_value}/draft"), + record=record, + files=draft_files.to_dict(), + permissions=draft.has_permissions_to(["new_version"]), + ) From 5d3f109f7c3d7726747f39da4947e6f7a951ddf6 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 22 Nov 2021 14:07:20 +0100 Subject: [PATCH 172/217] bugfix: typo record api links deposit edit endpoint in the marc21 api correcting a typo error --- invenio_records_marc21/services/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invenio_records_marc21/services/config.py b/invenio_records_marc21/services/config.py index 1f685ac2..b07442ca 100644 --- a/invenio_records_marc21/services/config.py +++ b/invenio_records_marc21/services/config.py @@ -110,7 +110,7 @@ class Marc21RecordServiceConfig(RecordServiceConfig): "self_html": ConditionalLink( cond=is_record, if_=RecordLink("{+ui}/marc21/{id}"), - else_=RecordLink("{+ui}/marc21/upload/{id}"), + else_=RecordLink("{+ui}/marc21/uploads/{id}"), ), "latest": RecordLink("{+api}/marc21/{id}/versions/latest"), "latest_html": RecordLink("{+ui}/marc21/{id}/latest"), From 294d68911e1738d3cb10a6cc4191293aa179eee1 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 22 Nov 2021 14:09:04 +0100 Subject: [PATCH 173/217] modification: deserialize default metadata preventing marcjs from failing if no leader is present in the metadata --- .../invenio_records_marc21/deposit/Marc21RecordSerializer.js | 3 ++- invenio_records_marc21/ui/theme/deposit.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21RecordSerializer.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21RecordSerializer.js index 1cb840a3..3b0c224b 100644 --- a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21RecordSerializer.js +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21RecordSerializer.js @@ -30,7 +30,8 @@ export class Marc21RecordSerializer { }), metadata: new MetadataFields({ fieldpath: "metadata", - deserializedDefault: { leader: "00000nam a2200000zca4500" }, + deserializedDefault: + " 00000nam a2200000zca4500", serializedDefault: "", }), }; diff --git a/invenio_records_marc21/ui/theme/deposit.py b/invenio_records_marc21/ui/theme/deposit.py index 60e7ec96..27922692 100644 --- a/invenio_records_marc21/ui/theme/deposit.py +++ b/invenio_records_marc21/ui/theme/deposit.py @@ -41,7 +41,7 @@ def empty_record(): """Create an empty record.""" record = dump_empty(Marc21RecordSchema) - record["metadata"] = "" + record["metadata"] = " 00000nam a2200000zca4500" record["is_published"] = False record["files"] = {"enabled": True} return record From f9a9a8b251eeed92f14227d42fcb3010e60f37e8 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 22 Nov 2021 14:13:40 +0100 Subject: [PATCH 174/217] modification: deposit_config default parameter allowing deposit_config method override default values for new records configuration values and edit record configuration --- invenio_records_marc21/ui/theme/deposit.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/invenio_records_marc21/ui/theme/deposit.py b/invenio_records_marc21/ui/theme/deposit.py index 27922692..5198bc58 100644 --- a/invenio_records_marc21/ui/theme/deposit.py +++ b/invenio_records_marc21/ui/theme/deposit.py @@ -56,16 +56,14 @@ def deposit_templates(): return [] -def deposit_config(): +def deposit_config(**kwargs): """Create an deposit configuration.""" jsonschema = current_app.extensions["invenio-jsonschemas"] schema = {} if jsonschema: schema = jsonschema.get_schema(path="marc21/marc21-structure-v1.0.0.json") - config = { - "error": "", - "loaded": False, - "schema": schema, - "createUrl": ("/api/marc21"), - } + config = {**kwargs} + config.setdefault("error", "") + config.setdefault("schema", schema) + config.setdefault("createUrl", "/api/marc21") return config From 1d52ad224dabdfea36c91601203dabca84eec14e Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 22 Nov 2021 14:14:57 +0100 Subject: [PATCH 175/217] modification: title deposit page changing the title of the deposit page according to the actual deposit state of a record --- .../templates/invenio_records_marc21/deposit.html | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/invenio_records_marc21/templates/invenio_records_marc21/deposit.html b/invenio_records_marc21/templates/invenio_records_marc21/deposit.html index 3771e48c..14caa863 100644 --- a/invenio_records_marc21/templates/invenio_records_marc21/deposit.html +++ b/invenio_records_marc21/templates/invenio_records_marc21/deposit.html @@ -8,7 +8,13 @@ {%- extends config.INVENIO_MARC21_BASE_TEMPLATE %} -{%- set title = _("New upload") %} +{%- if not record.is_published and record.versions.index and record.versions.index > 1%} + {%- set title = _("New version") %} +{%- elif not record.is_published %} + {%- set title = _("New upload") %} +{% else %} + {%- set title = _("Edit upload") %} +{%- endif %} {%- block page_body %} From 48ccd5aca225d1ee434e7d6a77194d61ef3dae94 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 22 Nov 2021 14:42:14 +0100 Subject: [PATCH 176/217] modification: dump marc21 metadata as string decoding the metdata from binary array to string , because the frontend has trouble to convert the json. --- invenio_records_marc21/resources/serializers/fields/metadata.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invenio_records_marc21/resources/serializers/fields/metadata.py b/invenio_records_marc21/resources/serializers/fields/metadata.py index 3385f50c..7d648bfd 100644 --- a/invenio_records_marc21/resources/serializers/fields/metadata.py +++ b/invenio_records_marc21/resources/serializers/fields/metadata.py @@ -47,7 +47,7 @@ def convert_xml(self, data, **kwargs): marcxml = self.context.get("marcxml", False) if data and marcxml: try: - data = dumps(to_marc21.do(data)) # .decode("UTF-8") + data = dumps(to_marc21.do(data)).decode("UTF-8") except Exception as e: raise Marc21XMLConvertError(e) From 2f473b12538ce6e95cd5886cd035fe38e82da335 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 22 Nov 2021 14:11:02 +0100 Subject: [PATCH 177/217] feature: add AccessRightField adding AccessRightField to the marc21 deposit form --- .../js/invenio_records_marc21/deposit/Marc21DepositForm.js | 6 ++---- .../deposit/Marc21RecordSerializer.js | 7 +++++++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21DepositForm.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21DepositForm.js index 6978e375..31d85b41 100644 --- a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21DepositForm.js +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21DepositForm.js @@ -13,7 +13,7 @@ import { Marc21DepositApp } from "./Marc21DepositApp"; import { AccordionField, MetadataFields } from "./components"; import { Card, Container, Grid, Ref, Sticky } from "semantic-ui-react"; import { TemplateField } from "./components/TemplateField"; - +import { AccessRightField } from "react-invenio-deposit"; export class Marc21DepositForm extends Component { constructor(props) { super(props); @@ -73,9 +73,7 @@ export class Marc21DepositForm extends Component { /> )} - {/* */} + diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21RecordSerializer.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21RecordSerializer.js index 1cb840a3..27d60f8f 100644 --- a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21RecordSerializer.js +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21RecordSerializer.js @@ -17,6 +17,13 @@ export class Marc21RecordSerializer { constructor() {} depositRecordSchema = { + access: new Field({ + fieldpath: "access", + deserializedDefault: { + record: "public", + files: "public", + }, + }), files: new Field({ fieldpath: "files", }), From 9ddad52562ec366ab9abc9e5e6b7661a8289cd01 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 22 Nov 2021 14:30:25 +0100 Subject: [PATCH 178/217] build(ui): dependency update --- invenio_records_marc21/ui/theme/webpack.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/invenio_records_marc21/ui/theme/webpack.py b/invenio_records_marc21/ui/theme/webpack.py index 76d37966..6c356c19 100644 --- a/invenio_records_marc21/ui/theme/webpack.py +++ b/invenio_records_marc21/ui/theme/webpack.py @@ -40,6 +40,9 @@ "marcjs": "^2.0.1", "react-invenio-deposit": "^0.16.1", "react-invenio-forms": "^0.8.7", + "i18next": "^20.3.1", + "react-i18next": "^11.11.3", + "i18next-browser-languagedetector": "^6.1.1", }, aliases={ "@less/invenio_records_marc21": "./less/invenio_records_marc21", From c5d1d92e5c236fa69fe69f2fe1e24c3764e1277d Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Tue, 23 Nov 2021 07:38:06 +0100 Subject: [PATCH 179/217] modification(tests): serializer xml dumping the metadata as string --- tests/resources/serializers/ui/fields/test_ui_metadata.py | 6 +++--- tests/resources/serializers/ui/test_serializers.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/resources/serializers/ui/fields/test_ui_metadata.py b/tests/resources/serializers/ui/fields/test_ui_metadata.py index 460f5e42..a5bd45da 100644 --- a/tests/resources/serializers/ui/fields/test_ui_metadata.py +++ b/tests/resources/serializers/ui/fields/test_ui_metadata.py @@ -58,9 +58,9 @@ def test_ui_metadata_convert_xml(marc21_metadata): test = deepcopy(marc21_metadata) data = metadata.dump({"metadata": test}) - assert isinstance(data["metadata"], bytes) + assert isinstance(data["metadata"], str) - expect_str = dumps(to_marc21.do(marc21_metadata)) + expect_str = dumps(to_marc21.do(marc21_metadata)).decode("UTF-8") assert expect_str == data["metadata"] @@ -85,7 +85,7 @@ def test_ui_metadata_xml_schema(marc21_metadata): test = deepcopy(marc21_metadata) data = metadata.dump({"metadata": test}) - s = "".join(data["metadata"].decode("UTF-8").split("\n")[1:-1]) + s = "".join(data["metadata"].split("\n")[1:-1]) test = marc21.do(create_record(s)) _test_without_order( test, diff --git a/tests/resources/serializers/ui/test_serializers.py b/tests/resources/serializers/ui/test_serializers.py index a3df1714..58eb6054 100644 --- a/tests/resources/serializers/ui/test_serializers.py +++ b/tests/resources/serializers/ui/test_serializers.py @@ -33,7 +33,7 @@ def test_ui_marcxml_serializer_dump_one(full_record): assert marc._object_key in obj obj_ui = obj[marc._object_key] assert "metadata" in obj_ui - assert isinstance(obj_ui["metadata"], bytes) + assert isinstance(obj_ui["metadata"], str) def test_ui_marcxml_serializer_dump_many(list_records): @@ -47,7 +47,7 @@ def test_ui_marcxml_serializer_dump_many(list_records): obj_ui = obj[marc._object_key] assert "metadata" in obj_ui - assert isinstance(obj_ui["metadata"], bytes) + assert isinstance(obj_ui["metadata"], str) def test_ui_json_serializer_init(): From 8912da400df0a3ae0afc1a53bc08b123d6c59b69 Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Tue, 23 Nov 2021 07:52:03 +0100 Subject: [PATCH 180/217] bugfix(ui): record publisher preventing jinia2 execption if publisher not set --- .../templates/invenio_records_marc21/records/helpers/title.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/title.html b/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/title.html index 624ad509..4d360924 100644 --- a/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/title.html +++ b/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/title.html @@ -16,7 +16,7 @@

{{title.get('title') | sanitize_title() }}

{% if personal %}

{{ personal.get('personal_name')}}

{% endif %} -{% if publisher.get('place_of_production_publication_distribution_manufacture') %} +{% if publisher %}

{{ publisher.get('place_of_production_publication_distribution_manufacture') + " " + publisher.get('date_of_production_publication_distribution_manufacture_or_copyright_notice') }}

{% endif %}
From bae9de72498ddf389c0d7520f47bec72a0eba3d3 Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Tue, 23 Nov 2021 09:05:08 +0100 Subject: [PATCH 181/217] modification(resources): serializer xml metadata dumping the metadata of a marc21 record as a xml string --- .../resources/serializers/ui/serializers.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/invenio_records_marc21/resources/serializers/ui/serializers.py b/invenio_records_marc21/resources/serializers/ui/serializers.py index 9f75a5cc..4eea036e 100644 --- a/invenio_records_marc21/resources/serializers/ui/serializers.py +++ b/invenio_records_marc21/resources/serializers/ui/serializers.py @@ -50,3 +50,19 @@ class Marc21UIXMLSerializer(Marc21UIBASESerializer): "remove_order": False, "marcxml": True, } + + def __init__(self, object_key="ui", **options): + """Marc21 UI XML Constructor. + + :param object_key: str key dump ui specific information + """ + super().__init__(object_key=object_key, **options) + + def dump_one(self, obj): + """Dump the object into a JSON string.""" + obj[self._object_key] = self._schema_cls(context=self.ctx).dump(deepcopy(obj)) + + # For edit a marc21 record in the deposit react app we need + # the metadata field also as a marcxml string + obj["metadata"] = obj[self._object_key]["metadata"] + return obj From 4a9055de104f9bfbef59c392277a45066c397237 Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Tue, 23 Nov 2021 14:39:30 +0100 Subject: [PATCH 182/217] tests: resource serializer copy records and check if metadata is of instance string --- tests/resources/serializers/conftest.py | 4 +++- tests/resources/serializers/ui/test_serializers.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/resources/serializers/conftest.py b/tests/resources/serializers/conftest.py index 4095b3ba..0fcda188 100644 --- a/tests/resources/serializers/conftest.py +++ b/tests/resources/serializers/conftest.py @@ -15,6 +15,8 @@ """ +from copy import deepcopy + import pytest @@ -177,6 +179,6 @@ def full_record(marc21_record, marc21_metadata): def list_records(full_record): """Fixture list of records.""" list_records = { - "hits": {"hits": [full_record, full_record]}, + "hits": {"hits": [deepcopy(full_record), deepcopy(full_record)]}, } return list_records diff --git a/tests/resources/serializers/ui/test_serializers.py b/tests/resources/serializers/ui/test_serializers.py index 58eb6054..87cf5580 100644 --- a/tests/resources/serializers/ui/test_serializers.py +++ b/tests/resources/serializers/ui/test_serializers.py @@ -27,7 +27,7 @@ def test_ui_marcxml_serializer_init(): def test_ui_marcxml_serializer_dump_one(full_record): marc = Marc21UIXMLSerializer() obj = marc.dump_one(full_record) - assert isinstance(obj["metadata"], dict) + assert isinstance(obj["metadata"], str) assert full_record["metadata"] == obj["metadata"] assert marc._object_key in obj From 02a405ef8f26137afc69283f912332ce240eae8f Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Tue, 23 Nov 2021 14:42:15 +0100 Subject: [PATCH 183/217] modification: deepcopy metadata make a deepcopy of the metadata object from _object_key metadata section into the metadata section to encapsulate the both objects --- .../resources/serializers/ui/serializers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/invenio_records_marc21/resources/serializers/ui/serializers.py b/invenio_records_marc21/resources/serializers/ui/serializers.py index 4eea036e..604cf3f2 100644 --- a/invenio_records_marc21/resources/serializers/ui/serializers.py +++ b/invenio_records_marc21/resources/serializers/ui/serializers.py @@ -62,7 +62,7 @@ def dump_one(self, obj): """Dump the object into a JSON string.""" obj[self._object_key] = self._schema_cls(context=self.ctx).dump(deepcopy(obj)) - # For edit a marc21 record in the deposit react app we need + # For edit a marc21 record in the deposit react app we need # the metadata field also as a marcxml string - obj["metadata"] = obj[self._object_key]["metadata"] + obj["metadata"] = deepcopy(obj[self._object_key]["metadata"]) return obj From d81c7ad8d7c3a8a81315462fd874c8da3619d825 Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Tue, 23 Nov 2021 14:49:11 +0100 Subject: [PATCH 184/217] bugfix(ui): dump empty subfields deserialize marc21 empty datafield prevent the react app from crashing if the subfield list ist empty --- .../invenio_records_marc21/deposit/fields/MetadataFields.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/fields/MetadataFields.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/fields/MetadataFields.js index 387cb8f2..19f8f943 100644 --- a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/fields/MetadataFields.js +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/fields/MetadataFields.js @@ -61,6 +61,10 @@ export class MetadataFields extends Field { static _deserialize_subfields(subfields) { let field = ""; // subfields.join(" "); + // Empty datafield leave subfield in UI empty + if (subfields == null) { + return field; + } for (let i = 0; i < subfields.length; i++) { for (const [key, value] of Object.entries(subfields[i])) { field += " $$" + key + " " + value; From 24c7c4abb4ab8ab0dcea990cb984edcf1b024579 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Thu, 25 Nov 2021 07:50:36 +0100 Subject: [PATCH 185/217] modifcation: save draft saving changes into the backend if a draft exists save the changes otherwise create a new draft --- .../deposit/Marc21Controller.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21Controller.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21Controller.js index 9bd2dc7c..bbd25f35 100644 --- a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21Controller.js +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/Marc21Controller.js @@ -68,9 +68,13 @@ export class Marc21Controller { ); let response = {}; - let payload = recordSerializer.serialize(draft); - response = await this.createDraft(draft, { store }); - //response = await this.apihandler.save(payload); + //let payload = recordSerializer.serialize(draft); + if (!this.draftAlreadyCreated(draft)) { + response = await this.createDraft(draft, { store }); + } else { + let payload = recordSerializer.serialize(draft); + response = await this.apihandler.save(payload); + } if (_has(response, ["data", "ui", "metadata"])) { _set(response.data, "metadata", response.data.ui.metadata); @@ -111,6 +115,9 @@ export class Marc21Controller { if (!this.draftAlreadyCreated(draft)) { response = await this.createDraft(draft, { store }); + } else { + let payload = recordSerializer.serialize(draft); + response = await this.apihandler.save(payload); } let payload = recordSerializer.serialize(draft); From ad1d71bcb75325b74e7402029c8388139bb68eac Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Thu, 25 Nov 2021 09:49:59 +0100 Subject: [PATCH 186/217] modification: empty subfields iterateing over subfields in a save way --- .../deposit/fields/MetadataFields.js | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/fields/MetadataFields.js b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/fields/MetadataFields.js index 19f8f943..be2caa0f 100644 --- a/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/fields/MetadataFields.js +++ b/invenio_records_marc21/ui/theme/assets/semantic-ui/js/invenio_records_marc21/deposit/fields/MetadataFields.js @@ -60,16 +60,12 @@ export class MetadataFields extends Field { } static _deserialize_subfields(subfields) { - let field = ""; // subfields.join(" "); - // Empty datafield leave subfield in UI empty - if (subfields == null) { - return field; - } - for (let i = 0; i < subfields.length; i++) { - for (const [key, value] of Object.entries(subfields[i])) { + let field = ""; + subfields.forEach((subfield) => { + for (const [key, value] of Object.entries(subfield)) { field += " $$" + key + " " + value; } - } + }); return field; } From f6e6231234ce7872ee81584a0255dc24e2c4cc0d Mon Sep 17 00:00:00 2001 From: Christoph Ladurner Date: Wed, 24 Nov 2021 09:53:36 +0100 Subject: [PATCH 187/217] bugfix: not present variable cause page rendering fault play safe, and show a value only if it is set --- .../records/helpers/title.html | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/title.html b/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/title.html index 624ad509..2bf46c70 100644 --- a/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/title.html +++ b/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/title.html @@ -9,15 +9,22 @@ {% set title = marcrecord.get('title_statement') %} {% set publisher = marcrecord.get('production_publication_distribution_manufacture_and_copyright_notice') %} + {% if title %} -
-

{{title.get('title') | sanitize_title() }}

+ {% set personal = marcrecord.get('main_entry_personal_name') %} -{% if personal %} -

{{ personal.get('personal_name')}}

-{% endif %} -{% if publisher.get('place_of_production_publication_distribution_manufacture') %} -

{{ publisher.get('place_of_production_publication_distribution_manufacture') + " " + publisher.get('date_of_production_publication_distribution_manufacture_or_copyright_notice') }}

-{% endif %} +{% set location = publisher.get('place_of_production_publication_distribution_manufacture') %} +{% set date = publisher.get('date_of_production_publication_distribution_manufacture_or_copyright_notice') %} + +
+

{{title.get('title') | sanitize_title() }}

+ {% if personal %} +

{{ personal.get('personal_name')}}

+ {% endif %} + + {% if publisher %} +

{{location if location}} {{date if date}}

+ {% endif %} +
{% endif %} From 8f2ee8a57c1310d57f9c07f7b5904f613e50d8a0 Mon Sep 17 00:00:00 2001 From: Christoph Ladurner Date: Fri, 26 Nov 2021 14:18:49 +0100 Subject: [PATCH 188/217] bugfix: title could be not a string, be careful --- invenio_records_marc21/ui/records/filters.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/invenio_records_marc21/ui/records/filters.py b/invenio_records_marc21/ui/records/filters.py index 7cabcd7d..18d5e2a2 100644 --- a/invenio_records_marc21/ui/records/filters.py +++ b/invenio_records_marc21/ui/records/filters.py @@ -50,7 +50,10 @@ def json_to_marc21(json): def sanitize_title(title): """Sanitize record title.""" - return re.sub("[<>]", "", title) + if isinstance(title, str): + return re.sub("[<>]", "", title) + else: + return "" def personal_name(personal): From a93a9bc5325879138a512fbfc80e5048d800a24b Mon Sep 17 00:00:00 2001 From: Christoph Ladurner Date: Fri, 26 Nov 2021 14:20:03 +0100 Subject: [PATCH 189/217] bugfix: get from variable after check variable not none --- .../invenio_records_marc21/records/helpers/title.html | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/title.html b/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/title.html index 2bf46c70..5587badf 100644 --- a/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/title.html +++ b/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/title.html @@ -13,8 +13,6 @@ {% if title %} {% set personal = marcrecord.get('main_entry_personal_name') %} -{% set location = publisher.get('place_of_production_publication_distribution_manufacture') %} -{% set date = publisher.get('date_of_production_publication_distribution_manufacture_or_copyright_notice') %}

{{title.get('title') | sanitize_title() }}

@@ -23,6 +21,9 @@

{{title.get('title') | sanitize_title() }}

{% endif %} {% if publisher %} + {% set location = publisher.get('place_of_production_publication_distribution_manufacture') %} + {% set date = publisher.get('date_of_production_publication_distribution_manufacture_or_copyright_notice') %} +

{{location if location}} {{date if date}}

{% endif %} From dfdb871e3a8b48c4c67c3d5b30d5ce46ffad9546 Mon Sep 17 00:00:00 2001 From: Christoph Ladurner Date: Fri, 26 Nov 2021 15:55:43 +0100 Subject: [PATCH 190/217] rewrite: changed the details part of the landing page to one column align the look and feel with the ub.tugraz.at library search details --- .../records/helpers/details.html | 18 +-- .../records/macros/detail.html | 122 ++++++------------ invenio_records_marc21/ui/records/__init__.py | 3 +- invenio_records_marc21/ui/records/filters.py | 8 ++ 4 files changed, 55 insertions(+), 96 deletions(-) diff --git a/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/details.html b/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/details.html index 8cfd5659..0001d887 100644 --- a/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/details.html +++ b/invenio_records_marc21/templates/invenio_records_marc21/records/helpers/details.html @@ -6,17 +6,17 @@ under the terms of the MIT License; see LICENSE file for more details. #} -{%- from "invenio_records_marc21/records/macros/detail.html" import show_personal_name, show_detail, show_publisher, show_dissertation, show_location_and_access, show_index_term_genre %} +{%- from "invenio_records_marc21/records/macros/detail.html" import show_personal_name, show_thesis_information, show_title, show_physical_description, show_description, show_creation_date, show_language %} {% set details = marcrecord %}

{{ _('Details')}}


-
- {{ show_detail(_('Author'), show_personal_name(details.main_entry_personal_name)) if details.main_entry_personal_name }} - {{ show_detail(_('Publisher'), show_publisher(details.production_publication_distribution_manufacture_and_copyright_notice)) if details.production_publication_distribution_manufacture_and_copyright_notice }} - {{ show_detail(_('Publication date'), record.ui.publication_date_l10n) if record.ui.publication_date_l10n }} - {{ show_detail(_('Dissertation'), show_dissertation(details.dissertation_note)) if details.dissertation_note }} - {{ show_detail(_('Location and Access'), show_location_and_access(details.electronic_location_and_access)) if details.electronic_location_and_access }} - {{ show_detail(_('Metadata'), show_index_term_genre(details.index_term_genre_form)) if details.index_term_genre_form }} +
+ {{ show_personal_name(details) }} + {{ show_title(details) }} + {{ show_physical_description(details) }} + {{ show_thesis_information(details) }} + {{ show_description(details) }} + {{ show_creation_date(details) }} + {{ show_language(details) }}
- diff --git a/invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html b/invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html index 55fada4c..20443c89 100644 --- a/invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html +++ b/invenio_records_marc21/templates/invenio_records_marc21/records/macros/detail.html @@ -19,13 +19,11 @@
{%- endmacro %} - - {% macro show_detail(title, value, type="str") %}
-

{{ _(title) }}

+

{{ _(title) if title }}

{% if value is iterable and (value is not string and value is not mapping) %} @@ -40,96 +38,48 @@
{%- endmacro %} - -{% macro show_personal_name(personal) %} - {% for key, value in personal.items() %} - {% if key != "type_of_personal_name_entry_element" and key != "relator_code" %} - {% if key == "personal_name" %} - {% set key = _("Name") %} - {% set value = personal | personal_name %} - {% endif %} - {{ show_detail(key, value) }} - {% endif %} - {% endfor %} +{% macro show_personal_name(details) %} + {% set key = _("Name") %} + {% set value = details.main_entry_personal_name | personal_name %} + {{ show_detail(key, value) }} {%- endmacro %} - - -{% macro show_publisher(publishers) %} - {% for key, value in publishers.items() %} - {% if key != "sequence_of_statements" %} - {% if key == "name_of_producer_publisher_distributor_manufacturer"%} - {% set key = _("Publisher") %} - {% elif key == "place_of_production_publication_distribution_manufacture"%} - {% set key = _("Place of publication") %} - {% elif key == "date_of_production_publication_distribution_manufacture_or_copyright_notice"%} - {% set key = _("Publication Date") %} - {% elif key == "function_of_entity"%} - {% set key = _("Function") %} - {% endif %} - {{ show_detail(key, value) }} - {% endif %} - {% endfor %} +{% macro show_description(details) %} + {% set key = _("Description") %} + {% set value = details.general_note.general_note %} + {{ show_detail(key, value) }} {%- endmacro %} - -{% macro show_dissertation(dissertation) %} - {% for key, value in dissertation.items() %} - {% if key == "degree_type" %} - {% set key = _("Dissertation Type") %} - {% elif key == "name_of_granting_institution"%} - {% set key = _("Institution") %} - {% elif key == "year_degree_granted"%} - {% set key = _("Year of degree") %} - {% endif %} - {{ show_detail(key, value) }} - {% endfor %} +{% macro show_creation_date(details) %} + {% set key = _("Creation Date") %} + {% set value = details.production_publication_distribution_manufacture_and_copyright_notice.date_of_production_publication_distribution_manufacture_or_copyright_notice %} + {{ show_detail(key, value) }} {%- endmacro %} - - -{% macro show_location_and_access(location_and_access) %} - {% for key, value in location_and_access.items() %} - {% if key != "access_method" %} - {%set type = "str" %} - {% if key == "uniform_resource_identifier" %} - {% set key = _("Link") %} - {% set type = "link" %} - {% elif key == "nonpublic_note"%} - {% set key = _("Note") %} - {% elif key == "materials_specified"%} - {% set key = _("Materials") %} - {% endif %} - {{ show_detail(key, value, type) }} - {% endif %} - {% endfor %} +{% macro show_language(details) %} + {% set key = _("Language") %} + {% set value = details.language_code.language_code_of_text_sound_track_or_separate_title %} + {{ show_detail(key, value) }} {%- endmacro %} - -{% macro show_index_term_genre(index_term_genre) %} - {% for key, value in index_term_genre.items() %} - {% if key != "source_of_term" and key != "type_of_heading" %} - {% if key == "authority_record_control_number" %} - {% set key = _("Control number") %} - {% elif key == "genre_form_data_or_focus_term"%} - {% set key = _("Type") %} - {% endif %} - {{ show_detail(key, value) }} - {% endif %} - {% endfor %} +{% macro show_thesis_information(details) %} + {% set key = _("Thesis Information") %} + {% set value = details.dissertation_note.dissertation_note %} + {{ show_detail(key, value) }} {%- endmacro %} - - -{% macro show_pages(pages) %} - {% for key, value in publishers.items() %} - {% if key == "sequence_of_statements" %} - {% set key = _("Sequence") %} - {% elif key == "name_of_producer_publisher_distributor_manufacturer"%} - {% set key = _("Publisher") %} - {% elif key == "place_of_production_publication_distribution_manufacture"%} - {% set key = _("Place of publication") %} - {% endif %} - {{ show_detail(key, value) }} - {% endfor %} -{%- endmacro %} \ No newline at end of file +{% macro show_title(details) %} + {% set key = _("Title") %} + {% set value = details.title_statement.title %} + {{ show_detail(key, value) }} + + {% set key = _("Stmt. of Responsibility") %} + {% set value = details.title_statement.statement_of_responsibility %} + {{ show_detail(key, value) }} +{% endmacro %} + +{% macro show_physical_description(details) %} + {% set key = _("Type/Extent/Format") %} + {% set value = details.physical_description | physical_description %} + {{ show_detail(key, value) }} +{% endmacro %} diff --git a/invenio_records_marc21/ui/records/__init__.py b/invenio_records_marc21/ui/records/__init__.py index bd782a0d..7bb03d29 100644 --- a/invenio_records_marc21/ui/records/__init__.py +++ b/invenio_records_marc21/ui/records/__init__.py @@ -18,7 +18,7 @@ record_permission_denied_error, record_tombstone_error, ) -from .filters import personal_name, pid_url, sanitize_title +from .filters import personal_name, physical_description, pid_url, sanitize_title from .records import ( record_detail, record_export, @@ -62,4 +62,5 @@ def init_records_views(blueprint, app): blueprint.add_app_template_filter(pid_url) blueprint.add_app_template_filter(sanitize_title) blueprint.add_app_template_filter(personal_name) + blueprint.add_app_template_filter(physical_description) return blueprint diff --git a/invenio_records_marc21/ui/records/filters.py b/invenio_records_marc21/ui/records/filters.py index 18d5e2a2..c4cea2e5 100644 --- a/invenio_records_marc21/ui/records/filters.py +++ b/invenio_records_marc21/ui/records/filters.py @@ -62,3 +62,11 @@ def personal_name(personal): code = get_personal_code(personal.get("relator_code")) return f"{name} [{code}]" + + +def physical_description(physical_description): + """ "Phsyical description for frontent.""" + extent = physical_description.get("extent", "") + other_physical_details = physical_description.get("other_physical_details", "") + + return f"{extent}, {other_physical_details}" From a60b09d36f0474f9f763b882364bb64c8d25d22b Mon Sep 17 00:00:00 2001 From: Christoph Ladurner Date: Tue, 7 Dec 2021 11:17:04 +0100 Subject: [PATCH 191/217] bugfix: docstring was wrong --- invenio_records_marc21/ui/records/filters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invenio_records_marc21/ui/records/filters.py b/invenio_records_marc21/ui/records/filters.py index c4cea2e5..b1061c64 100644 --- a/invenio_records_marc21/ui/records/filters.py +++ b/invenio_records_marc21/ui/records/filters.py @@ -65,7 +65,7 @@ def personal_name(personal): def physical_description(physical_description): - """ "Phsyical description for frontent.""" + """Physical description for frontend.""" extent = physical_description.get("extent", "") other_physical_details = physical_description.get("other_physical_details", "") From 611bba76145b7d3153919d67c4a79f371f0a20a8 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Tue, 7 Dec 2021 13:28:53 +0100 Subject: [PATCH 192/217] modification: convert marc21 xml string converting the marc21 xml string to the internal representation in the create method of the marc21 service module --- invenio_records_marc21/services/record/metadata.py | 7 +++++++ invenio_records_marc21/services/services.py | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/invenio_records_marc21/services/record/metadata.py b/invenio_records_marc21/services/record/metadata.py index 360dd380..34380ab5 100644 --- a/invenio_records_marc21/services/record/metadata.py +++ b/invenio_records_marc21/services/record/metadata.py @@ -26,6 +26,7 @@ def __init__(self, leader: LeaderField = LeaderField()): """Default constructor of the class.""" self._xml = "" self._json = {} + self._etree = None self.leader = leader self.controlfields = list() self.datafields = list() @@ -35,6 +36,11 @@ def json(self): """Metadata json getter method.""" return self._json + @property + def etree(self): + """Metadata json getter method.""" + return self._etree + @json.setter def json(self, json: dict): """Metadata json setter method.""" @@ -59,6 +65,7 @@ def xml(self, xml: str): def load(self, xml: etree): """Load metadata from etree.""" + self._etree = xml self._to_xml_tree(xml) def _to_xml_tree_from_string(self, xml: str): diff --git a/invenio_records_marc21/services/services.py b/invenio_records_marc21/services/services.py index 09e9a37f..08f16112 100644 --- a/invenio_records_marc21/services/services.py +++ b/invenio_records_marc21/services/services.py @@ -42,7 +42,8 @@ def _create_data(self, identity, data, metadata, files=False, access=None): :rtype: dict """ if data is None: - data = {"metadata": {"xml": metadata.xml, "json": metadata.json}} + record = create_record(metadata.etree) + data = {"metadata": marc21.do(record)} data["files"] = {"enabled": files} if "access" not in data: default_access = { From 4ba068cb7042192cf636ec496e8b02fa5cfbbc83 Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Tue, 7 Dec 2021 13:31:06 +0100 Subject: [PATCH 193/217] bugfix: default access updating the default access to "record" --- invenio_records_marc21/services/services.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/invenio_records_marc21/services/services.py b/invenio_records_marc21/services/services.py index 08f16112..5b8e0f97 100644 --- a/invenio_records_marc21/services/services.py +++ b/invenio_records_marc21/services/services.py @@ -10,6 +10,8 @@ """Marc21 Record Service.""" +from dojson.contrib.marc21 import marc21 +from dojson.contrib.marc21.utils import create_record from invenio_db import db from invenio_drafts_resources.services.records import RecordService from invenio_rdm_records.records.systemfields.access.field.record import ( @@ -49,7 +51,7 @@ def _create_data(self, identity, data, metadata, files=False, access=None): default_access = { "access": { "owned_by": [{"user": identity.id}], - "metadata": AccessStatusEnum.OPEN.value, + "record": AccessStatusEnum.OPEN.value, "files": AccessStatusEnum.OPEN.value, }, } From d29951eae1fda4debd10fb84078a9ada9bf41c37 Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Tue, 14 Dec 2021 08:39:13 +0100 Subject: [PATCH 194/217] modification: store etree from string for backward compatibility storing the lxml etree if the xml property is set --- invenio_records_marc21/services/record/metadata.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/invenio_records_marc21/services/record/metadata.py b/invenio_records_marc21/services/record/metadata.py index 34380ab5..21132d44 100644 --- a/invenio_records_marc21/services/record/metadata.py +++ b/invenio_records_marc21/services/record/metadata.py @@ -60,7 +60,8 @@ def xml(self, xml: str): if not isinstance(xml, str): raise TypeError("xml must be from type str") - self._to_xml_tree_from_string(xml) + etree = self._to_xml_tree_from_string(xml) + self._etree = etree self._xml = xml def load(self, xml: etree): @@ -70,8 +71,9 @@ def load(self, xml: etree): def _to_xml_tree_from_string(self, xml: str): """Xml string to internal representation method.""" - test = etree.parse(StringIO(xml)) - self._to_xml_tree(test) + tree = etree.parse(StringIO(xml)) + self._to_xml_tree(tree) + return tree def _to_xml_tree(self, xml: etree): """Xml to internal representation method.""" From 08546c5349d16bd92de70cecf8806d0bdf89dca3 Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Tue, 14 Dec 2021 08:49:06 +0100 Subject: [PATCH 195/217] modification: delete conversion in schema deleting the conversion from marcxml string into internal representation in the metadata schema --- invenio_records_marc21/services/schemas/metadata.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/invenio_records_marc21/services/schemas/metadata.py b/invenio_records_marc21/services/schemas/metadata.py index a3cd9b4e..f233fe0c 100644 --- a/invenio_records_marc21/services/schemas/metadata.py +++ b/invenio_records_marc21/services/schemas/metadata.py @@ -13,8 +13,6 @@ import typing -from dojson.contrib.marc21 import marc21 -from dojson.contrib.marc21.utils import create_record from marshmallow.fields import Field @@ -43,8 +41,6 @@ def _deserialize( .. versionchanged:: 3.0.0 Added ``**kwargs`` to signature. """ - if "xml" in value: - value = marc21.do(create_record(value["xml"])) return value def _validate(self, value): From 6afd8cca65d3625d4305f8f8bf1ffb107271af75 Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Tue, 14 Dec 2021 08:52:01 +0100 Subject: [PATCH 196/217] modification(tests): update conversion testset updating the testset for the new conversion done in the record service instead in the metadata schema class --- tests/services/conftest.py | 20 +++++++- tests/services/schemas/conftest.py | 27 ----------- tests/services/schemas/test_metadata.py | 61 ------------------------- tests/services/test_create_record.py | 2 +- tests/services/test_record_service.py | 21 +++++++-- 5 files changed, 37 insertions(+), 94 deletions(-) delete mode 100644 tests/services/schemas/test_metadata.py diff --git a/tests/services/conftest.py b/tests/services/conftest.py index 2d086242..2eedbf2d 100644 --- a/tests/services/conftest.py +++ b/tests/services/conftest.py @@ -65,7 +65,7 @@ def service(appctx): def metadata(): """Input data (as coming from the view layer).""" metadata = Marc21Metadata() - metadata.emplace_field(tag="245", ind1="1", ind2="0", value="laborum sunt ut nulla") + metadata.xml = "laborum sunt ut nulla" return metadata @@ -73,7 +73,7 @@ def metadata(): def metadata2(): """Input data (as coming from the view layer).""" metadata = Marc21Metadata() - metadata.emplace_field(tag="245", ind1="1", ind2="0", value="nulla sunt laborum") + metadata.xml = "nulla sunt laborum" return metadata @@ -85,3 +85,19 @@ def embargoedrecord(embargoed_record): draft = service.create(identity_simple, embargoed_record) record = service.publish(id_=draft.id, identity=identity_simple) return record + + +@pytest.fixture() +def full_metadata(): + """Metadata full record marc21 xml.""" + metadata = Marc21Metadata() + metadata.xml = "00000nam a2200000zca4500AC08088803" + return metadata + + +@pytest.fixture() +def min_metadata(): + """Metadata empty record marc21 xml.""" + metadata = Marc21Metadata() + metadata.xml = "" + return metadata diff --git a/tests/services/schemas/conftest.py b/tests/services/schemas/conftest.py index aa3fbd03..724a9d11 100644 --- a/tests/services/schemas/conftest.py +++ b/tests/services/schemas/conftest.py @@ -15,30 +15,3 @@ """ import pytest - - -@pytest.fixture() -def full_metadata(): - """Metadata full record marc21 xml.""" - metadata = { - "xml": "990079940640203331 AT-OBV20170703041800.0cr 100504|1932AC08088803AC08088803" - } - return metadata - - -@pytest.fixture() -def min_metadata(): - """Metadata empty record marc21 xml.""" - metadata = { - "xml": "" - } - return metadata - - -@pytest.fixture() -def min_json_metadata(): - """Metadata empty record marc21 json.""" - metadata = { - "json": {} - } - return metadata diff --git a/tests/services/schemas/test_metadata.py b/tests/services/schemas/test_metadata.py deleted file mode 100644 index f4a35c22..00000000 --- a/tests/services/schemas/test_metadata.py +++ /dev/null @@ -1,61 +0,0 @@ -# -*- coding: utf-8 -*- -# -# This file is part of Invenio. -# -# Copyright (C) 2021 Graz University of Technology. -# -# Invenio-Records-Marc21 is free software; you can redistribute it and/or -# modify it under the terms of the MIT License; see LICENSE file for more -# details. - -"""Test for record MetadataField.""" - - -from dojson.contrib.marc21 import marc21 -from dojson.contrib.marc21.utils import create_record -from marshmallow import Schema - -from invenio_records_marc21.services.schemas.metadata import MetadataField - - -class Marc21TestSchema(Schema): - metadata = MetadataField(attribute="metadata") - - -def _test_metadata(test, expected): - assert test.keys() == expected.keys() - for key in test.keys(): - assert test[key] == expected[key] - - -def test_full_metadata_xml_schema(app, full_metadata): - """Test metadata schema.""" - - metadata = Marc21TestSchema() - data = metadata.load({"metadata": full_metadata}) - _test_metadata( - data["metadata"], - marc21.do(create_record(full_metadata["xml"])), - ) - assert "xml" not in data - - -def test_minimal_metadata_xml_schema(app, min_metadata): - metadata = Marc21TestSchema() - data = metadata.load({"metadata": min_metadata}) - _test_metadata( - data["metadata"], - marc21.do(create_record(min_metadata["xml"])), - ) - assert "xml" not in data - - -def test_minimal_metadata_json_schema(app, min_json_metadata): - metadata = Marc21TestSchema() - data = metadata.load({"metadata": min_json_metadata}) - assert data["metadata"] == min_json_metadata - _test_metadata( - data["metadata"], - min_json_metadata, - ) - assert "json" not in data diff --git a/tests/services/test_create_record.py b/tests/services/test_create_record.py index 27a62de7..8000b5f6 100644 --- a/tests/services/test_create_record.py +++ b/tests/services/test_create_record.py @@ -34,7 +34,7 @@ def _assert_fields(fields, values, expected): @pytest.fixture() def marc21(): """marc21 record.""" - return {"metadata": {"xml": ""}} + return {"metadata": {}} def test_create_with_service(running_app, marc21): diff --git a/tests/services/test_record_service.py b/tests/services/test_record_service.py index f2fa06cd..7a01542c 100644 --- a/tests/services/test_record_service.py +++ b/tests/services/test_record_service.py @@ -16,6 +16,8 @@ import arrow import pytest from dateutil import tz +from dojson.contrib.marc21 import marc21 +from dojson.contrib.marc21.utils import create_record from dojson.contrib.to_marc21 import to_marc21 from invenio_pidstore.errors import PIDDoesNotExistError from invenio_pidstore.models import PIDStatus @@ -24,9 +26,22 @@ from invenio_records_marc21.proxies import current_records_marc21 from invenio_records_marc21.services.errors import EmbargoNotLiftedError -# -# Operations tests -# + +def _test_metadata(test, expected): + assert test.keys() == expected.keys() + for key in test.keys(): + assert test[key] == expected[key] + + +def test_full_metadata_xml_schema(running_app, full_metadata): + """Test metadata schema.""" + service = running_app.service + data = service.create(running_app.identity_simple, metadata=full_metadata) + + _test_metadata( + data["metadata"], + marc21.do(create_record(full_metadata.xml)), + ) def test_create_draft(running_app, metadata): From ea55239a254a2ea4ef618db0c7697bdfb28ff12f Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Tue, 14 Dec 2021 11:15:03 +0100 Subject: [PATCH 197/217] modification: docstyle --- invenio_records_marc21/services/record/metadata.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/invenio_records_marc21/services/record/metadata.py b/invenio_records_marc21/services/record/metadata.py index 21132d44..40c44666 100644 --- a/invenio_records_marc21/services/record/metadata.py +++ b/invenio_records_marc21/services/record/metadata.py @@ -38,7 +38,7 @@ def json(self): @property def etree(self): - """Metadata json getter method.""" + """Metadata etree getter method.""" return self._etree @json.setter From d71e6cfb1a59cbc6d3d4168e5a5875c89876f4aa Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 20 Dec 2021 10:53:09 +0100 Subject: [PATCH 198/217] build: dependency update installing invenio-rdm-records version less than 0.34.0 in order to avoid features in development --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 2fedb614..908a9b55 100644 --- a/setup.py +++ b/setup.py @@ -57,7 +57,7 @@ "arrow>=1.0.0", "dojson>=1.4.0", "lxml>=4.6.2", - "invenio-rdm-records>=0.32.2,<1.0.0", + "invenio-rdm-records>=0.32.2,<0.34.0", ] packages = find_packages() From f09bca3302a95e22efbda5791c0b98e1db3d890d Mon Sep 17 00:00:00 2001 From: "Gualdi, Philipp" Date: Mon, 20 Dec 2021 13:38:07 +0100 Subject: [PATCH 199/217] modification: codestyle --- invenio_records_marc21/services/record/metadata.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/invenio_records_marc21/services/record/metadata.py b/invenio_records_marc21/services/record/metadata.py index 40c44666..0745973b 100644 --- a/invenio_records_marc21/services/record/metadata.py +++ b/invenio_records_marc21/services/record/metadata.py @@ -60,8 +60,8 @@ def xml(self, xml: str): if not isinstance(xml, str): raise TypeError("xml must be from type str") - etree = self._to_xml_tree_from_string(xml) - self._etree = etree + tree = self._to_xml_tree_from_string(xml) + self._etree = tree self._xml = xml def load(self, xml: etree): From b30e6bf7fc944bae0d0fed8272f5162d7032cb41 Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Wed, 22 Dec 2021 09:37:45 +0100 Subject: [PATCH 200/217] modification: convert marcxml converting marc21xml to internal representation in the Marc21metadata module --- invenio_records_marc21/services/record/metadata.py | 2 ++ invenio_records_marc21/services/services.py | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/invenio_records_marc21/services/record/metadata.py b/invenio_records_marc21/services/record/metadata.py index 0745973b..a96e37fc 100644 --- a/invenio_records_marc21/services/record/metadata.py +++ b/invenio_records_marc21/services/record/metadata.py @@ -34,6 +34,8 @@ def __init__(self, leader: LeaderField = LeaderField()): @property def json(self): """Metadata json getter method.""" + record = create_record(self._etree) + self._json = {"metadata": marc21.do(record)} return self._json @property diff --git a/invenio_records_marc21/services/services.py b/invenio_records_marc21/services/services.py index 5b8e0f97..6babc009 100644 --- a/invenio_records_marc21/services/services.py +++ b/invenio_records_marc21/services/services.py @@ -44,8 +44,7 @@ def _create_data(self, identity, data, metadata, files=False, access=None): :rtype: dict """ if data is None: - record = create_record(metadata.etree) - data = {"metadata": marc21.do(record)} + data = metadata.json data["files"] = {"enabled": files} if "access" not in data: default_access = { From 939b5a40339c53c2286c946325b5035ac311e3a3 Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Wed, 22 Dec 2021 09:44:05 +0100 Subject: [PATCH 201/217] modification: use lxml using lxml as backend for emplace functions --- .../services/record/metadata.py | 114 ++++++++++-------- 1 file changed, 64 insertions(+), 50 deletions(-) diff --git a/invenio_records_marc21/services/record/metadata.py b/invenio_records_marc21/services/record/metadata.py index a96e37fc..1d5a96d5 100644 --- a/invenio_records_marc21/services/record/metadata.py +++ b/invenio_records_marc21/services/record/metadata.py @@ -14,22 +14,25 @@ from os import linesep from os.path import dirname, join +from dojson.contrib.marc21 import marc21 +from dojson.contrib.marc21.utils import create_record from lxml import etree - -from .fields import ControlField, DataField, LeaderField, SubField +from lxml.builder import ElementMaker class Marc21Metadata(object): """MARC21 Record class to facilitate storage of records in MARC21 format.""" - def __init__(self, leader: LeaderField = LeaderField()): + def __init__(self): """Default constructor of the class.""" self._xml = "" self._json = {} - self._etree = None - self.leader = leader - self.controlfields = list() - self.datafields = list() + self._etree = etree.Element( + "record", xmlns="http://www.loc.gov/MARC21/slim", type="Bibliographic" + ) + leader = etree.Element("leader") + leader.text = "00000nam a2200000zca4500" + self._etree.append(leader) @property def json(self): @@ -74,45 +77,47 @@ def load(self, xml: etree): def _to_xml_tree_from_string(self, xml: str): """Xml string to internal representation method.""" tree = etree.parse(StringIO(xml)) - self._to_xml_tree(tree) return tree - def _to_xml_tree(self, xml: etree): - """Xml to internal representation method.""" - for element in xml.iter(): - if "leader" in element.tag: - self.leader = LeaderField(data=element.text) - elif "datafield" in element.tag: - self.datafields.append( - DataField(**element.attrib, subfields=element.getchildren()) - ) - elif "controlfield" in element.tag: - self.controlfields.append(ControlField(**element.attrib)) - def _to_string(self, tagsep: str = linesep, indent: int = 4) -> str: """Get a pretty-printed XML string of the record.""" - self._xml = "" - self._xml += '' - self._xml += tagsep - if self.leader: - self._xml += self.leader.to_xml_tag(tagsep, indent) - for controlfield in self.controlfields: - self._xml += controlfield.to_xml_tag(tagsep, indent) - for datafield in self.datafields: - self._xml += datafield.to_xml_tag(tagsep, indent) - self._xml += "" - - def contains(self, ref_df: DataField, ref_sf: SubField) -> bool: - """Return True if record contains reference datafield, which contains reference subfield.""" - for df in self.datafields: - if ( - df.tag == ref_df.tag and df.ind1 == ref_df.ind1 - ) and df.ind2 == ref_df.ind2: - for sf in df.subfields: - if sf.code == ref_sf.code and sf.value == ref_sf.value: - return True + self._xml = etree.tostring(self._etree, pretty_print=True).decode("UTF-8") + + def contains(self, ref_df: dict, ref_sf: dict): + """Return True if record contains reference datafield, which contains reference subfield. + + @param ref_df dict: datafield element specific information, containing keys [tag,ind1,ind2] + @param ref_sf dict: subfield element specific information, containing keys [code,value] + @return bool: true if a datafield with the subfield are found + """ + element = self._etree.xpath( + ".//datafield[@ind1='{ind1}' and @ind2='{ind2}' and @tag='{tag}']//subfield[@code='{code}']".format( + **ref_df, code=ref_sf["code"] + ) + ) + if element and len(element) > 0 and element[0].text == ref_sf["value"]: + return True + return False + def emplace_leader( + self, + value: str = "", + ): + """Change leader string in record.""" + for leader in self._etree.iter("leader"): + leader.text = value + + def emplace_controlfield( + self, + tag: str = "", + value: str = "", + ): + """Add value to record for given datafield and subfield.""" + controlfield = etree.Element("controlfield", tag=tag) + controlfield.text = value + self._etree.append(controlfield) + def emplace_field( self, tag: str = "", @@ -122,10 +127,13 @@ def emplace_field( value: str = "", ) -> None: """Add value to record for given datafield and subfield.""" - datafield = DataField(tag, ind1, ind2) - subfield = SubField(code, value) - datafield.subfields.append(subfield) - self.datafields.append(datafield) + datafield = etree.Element( + "datafield", tag=tag, ind1=ind1, ind2=ind2 + ) # DataField(tag, ind1, ind2) + subfield = etree.Element("subfield", code=code) + subfield.text = value + datafield.append(subfield) + self._etree.append(datafield) def emplace_unique_field( self, @@ -134,13 +142,19 @@ def emplace_unique_field( ind2: str = " ", code: str = "", value: str = "", - ) -> None: + ): """Add value to record if it doesn't already contain it.""" - datafield = DataField(tag, ind1, ind2) - subfield = SubField(code, value) - if not self.contains(datafield, subfield): - datafield.subfields.append(subfield) - self.datafields.append(datafield) + subfield = etree.Element("subfield", code=code) + subfield.text = value + datafield = self._etree.xpath( + ".//datafield[@ind1='{ind1}' and @ind2='{ind2}' and @tag='{tag}']" + ) + if not datafield: + datafield = etree.Element( + "datafield", tag=tag, ind1=ind1, ind2=ind2 + ) # DataField(tag, ind1, ind2) + datafield.append(subfield) + self._etree.append(datafield) def is_valid_marc21_xml_string(self) -> bool: """Validate the record against a Marc21XML Schema.""" From 1c3ef456b5404346091f11852cf5e0ccea2217d7 Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Wed, 22 Dec 2021 09:45:47 +0100 Subject: [PATCH 202/217] modification(tests): marc21metadata testset --- tests/services/record/test_marc21_metadata.py | 146 ++++++++++-------- tests/services/schemas/test_metadata.py | 18 +++ 2 files changed, 99 insertions(+), 65 deletions(-) create mode 100644 tests/services/schemas/test_metadata.py diff --git a/tests/services/record/test_marc21_metadata.py b/tests/services/record/test_marc21_metadata.py index fef5b914..075b2bde 100644 --- a/tests/services/record/test_marc21_metadata.py +++ b/tests/services/record/test_marc21_metadata.py @@ -10,30 +10,17 @@ """Tests for record MetadataSchema.""" import pytest +from lxml import etree from invenio_records_marc21.services.record import Marc21Metadata -from invenio_records_marc21.services.record.fields import ( - ControlField, - DataField, - LeaderField, - SubField, -) def test_create_metadata(): metadata = Marc21Metadata() - assert metadata.leader.to_xml_tag() == LeaderField().to_xml_tag() - assert metadata.controlfields == list() - assert metadata.datafields == list() - assert "" in metadata.xml assert 'laborum sunt ut nulla' in metadata.xml @@ -89,16 +103,58 @@ def test_uniqueness_metadata(): tag="245", ind1="1", ind2="0", code="a", value="laborum sunt ut nulla" ) + datafield = metadata.etree.find(".//datafield") + assert datafield is not None + assert not datafield.text + assert len(metadata.etree.findall(".//datafield")) == 1 + assert datafield.attrib == {"tag": "245", "ind1": "1", "ind2": "0"} + + subfield = metadata.etree.find(".//subfield") + assert subfield is not None + assert len(datafield.findall(".//subfield")) == 1 + assert subfield.attrib == {"code": "a"} + assert subfield.text == "laborum sunt ut nulla" + metadata.emplace_unique_field( tag="245", ind1="1", ind2="0", code="a", value="laborum sunt ut nulla" ) - assert len(metadata.datafields) == 1 - assert len(metadata.controlfields) == 0 - assert len(metadata.datafields[0].subfields) == 1 + datafield = metadata.etree.find(".//datafield") + assert datafield + assert not datafield.text + assert len(metadata.etree.findall(".//datafield")) == 1 + assert datafield.attrib == {"tag": "245", "ind1": "1", "ind2": "0"} + + subfield = metadata.etree.find(".//subfield") + assert subfield is not None + assert len(datafield.findall(".//subfield")) == 1 + assert subfield.attrib == {"code": "a"} + assert subfield.text == "laborum sunt ut nulla" + assert metadata.is_valid_marc21_xml_string() +def test_contains_metadata(): + metadata = Marc21Metadata() + + assert not metadata.contains( + ref_df={"tag": "245", "ind1": "1", "ind2": "0"}, + ref_sf={"code": "a", "value": "laborum sunt ut nulla"}, + ) + metadata.emplace_field( + tag="246", ind1="1", ind2="0", code="a", value="laborum sunt ut nulla" + ) + assert not metadata.contains( + ref_df={"tag": "246", "ind1": "1", "ind2": "0"}, + ref_sf={"code": "b", "value": "laborum sunt ut nulla"}, + ) + + assert metadata.contains( + ref_df={"tag": "246", "ind1": "1", "ind2": "0"}, + ref_sf={"code": "a", "value": "laborum sunt ut nulla"}, + ) + + def test_xml_type(): metadata = Marc21Metadata() @@ -116,48 +172,8 @@ def test_json_type(): test = dict() metadata.json = test - assert metadata.json == {} + assert "metadata" in metadata.json.keys() test = "" with pytest.raises(TypeError): metadata.json = test - - -def test_xml_metadata(): - metadata = Marc21Metadata() - test = DataField(tag="245", ind1="0", ind2="0") - - metadata.xml = test.to_xml_tag() - - assert metadata.leader.to_xml_tag() == LeaderField().to_xml_tag() - assert len(metadata.datafields) == 1 - assert len(metadata.controlfields) == 0 - assert len(metadata.datafields[0].subfields) == 0 - assert test.to_xml_tag() in metadata.xml - - test.subfields.append(SubField(code="a", value="Brain-Computer Interface")) - metadata = Marc21Metadata() - metadata.xml = test.to_xml_tag() - - assert len(metadata.datafields) == 1 - assert len(metadata.controlfields) == 0 - assert len(metadata.datafields[0].subfields) == 1 - assert test.to_xml_tag() in metadata.xml - - test.subfields.append(SubField(code="b", value="Subtitle field.")) - metadata = Marc21Metadata() - metadata.xml = test.to_xml_tag() - - assert len(metadata.datafields) == 1 - assert len(metadata.controlfields) == 0 - assert len(metadata.datafields[0].subfields) == 2 - assert test.to_xml_tag() in metadata.xml - - test.subfields.append(SubField(code="c", value="hrsg. von Josef Frank")) - metadata = Marc21Metadata() - metadata.xml = test.to_xml_tag() - - assert len(metadata.datafields) == 1 - assert len(metadata.controlfields) == 0 - assert len(metadata.datafields[0].subfields) == 3 - assert test.to_xml_tag() in metadata.xml diff --git a/tests/services/schemas/test_metadata.py b/tests/services/schemas/test_metadata.py new file mode 100644 index 00000000..045ceac1 --- /dev/null +++ b/tests/services/schemas/test_metadata.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + + +"""Test Marc21Metadata.""" + +from invenio_records_marc21.services.record import Marc21Metadata + + +def test_metadata_emplace_field(): + metadata = Marc21Metadata() From 183f6bb5a56452481358bad7a0bd27ac1db52a03 Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Wed, 22 Dec 2021 10:04:48 +0100 Subject: [PATCH 203/217] bugfix: format search string --- .../services/record/metadata.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/invenio_records_marc21/services/record/metadata.py b/invenio_records_marc21/services/record/metadata.py index 1d5a96d5..cbe5e9e7 100644 --- a/invenio_records_marc21/services/record/metadata.py +++ b/invenio_records_marc21/services/record/metadata.py @@ -144,17 +144,22 @@ def emplace_unique_field( value: str = "", ): """Add value to record if it doesn't already contain it.""" - subfield = etree.Element("subfield", code=code) - subfield.text = value datafield = self._etree.xpath( - ".//datafield[@ind1='{ind1}' and @ind2='{ind2}' and @tag='{tag}']" + f".//datafield[@ind1='{ind1}' and @ind2='{ind2}' and @tag='{tag}']" ) if not datafield: datafield = etree.Element( "datafield", tag=tag, ind1=ind1, ind2=ind2 ) # DataField(tag, ind1, ind2) - datafield.append(subfield) - self._etree.append(datafield) + else: + datafield = datafield[0] + subfield = self._etree.xpath(f".//subfield[@code='{code}']") + if not subfield: + subfield = etree.Element("subfield", code=code) + subfield.text = value + datafield.append(subfield) + self._etree.append(datafield) + pass def is_valid_marc21_xml_string(self) -> bool: """Validate the record against a Marc21XML Schema.""" From 66a1128fbf4765d11e76221e83560063ce7d2f03 Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Wed, 12 Jan 2022 11:04:52 +0100 Subject: [PATCH 204/217] modification: imporve codestyle --- invenio_records_marc21/services/record/metadata.py | 3 +-- invenio_records_marc21/services/services.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/invenio_records_marc21/services/record/metadata.py b/invenio_records_marc21/services/record/metadata.py index cbe5e9e7..97cc9f19 100644 --- a/invenio_records_marc21/services/record/metadata.py +++ b/invenio_records_marc21/services/record/metadata.py @@ -129,7 +129,7 @@ def emplace_field( """Add value to record for given datafield and subfield.""" datafield = etree.Element( "datafield", tag=tag, ind1=ind1, ind2=ind2 - ) # DataField(tag, ind1, ind2) + ) subfield = etree.Element("subfield", code=code) subfield.text = value datafield.append(subfield) @@ -159,7 +159,6 @@ def emplace_unique_field( subfield.text = value datafield.append(subfield) self._etree.append(datafield) - pass def is_valid_marc21_xml_string(self) -> bool: """Validate the record against a Marc21XML Schema.""" diff --git a/invenio_records_marc21/services/services.py b/invenio_records_marc21/services/services.py index 6babc009..502d504b 100644 --- a/invenio_records_marc21/services/services.py +++ b/invenio_records_marc21/services/services.py @@ -10,8 +10,7 @@ """Marc21 Record Service.""" -from dojson.contrib.marc21 import marc21 -from dojson.contrib.marc21.utils import create_record + from invenio_db import db from invenio_drafts_resources.services.records import RecordService from invenio_rdm_records.records.systemfields.access.field.record import ( From 99558016009cef74fbdedf1997bf3e0c71e852d3 Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Wed, 12 Jan 2022 11:05:05 +0100 Subject: [PATCH 205/217] modification: imporve codequality --- .../services/record/__init__.py | 5 +- .../services/record/fields/__init__.py | 23 ---- .../services/record/fields/control.py | 32 ------ .../services/record/fields/data.py | 48 -------- .../services/record/fields/leader.py | 84 -------------- .../services/record/fields/sub.py | 31 ----- tests/services/record/fields/conftest.py | 40 ------- .../record/fields/test_controlfield.py | 44 ------- .../services/record/fields/test_datafield.py | 52 --------- .../record/fields/test_leaderfield.py | 107 ------------------ tests/services/record/fields/test_subfield.py | 43 ------- 11 files changed, 1 insertion(+), 508 deletions(-) delete mode 100644 invenio_records_marc21/services/record/fields/__init__.py delete mode 100644 invenio_records_marc21/services/record/fields/control.py delete mode 100644 invenio_records_marc21/services/record/fields/data.py delete mode 100644 invenio_records_marc21/services/record/fields/leader.py delete mode 100644 invenio_records_marc21/services/record/fields/sub.py delete mode 100644 tests/services/record/fields/conftest.py delete mode 100644 tests/services/record/fields/test_controlfield.py delete mode 100644 tests/services/record/fields/test_datafield.py delete mode 100644 tests/services/record/fields/test_leaderfield.py delete mode 100644 tests/services/record/fields/test_subfield.py diff --git a/invenio_records_marc21/services/record/__init__.py b/invenio_records_marc21/services/record/__init__.py index d06119ca..d25026e8 100644 --- a/invenio_records_marc21/services/record/__init__.py +++ b/invenio_records_marc21/services/record/__init__.py @@ -10,12 +10,9 @@ """Marc21 field class.""" -from .fields import ControlField, DataField, SubField + from .metadata import Marc21Metadata __all__ = ( "Marc21Metadata", - "ControlField", - "DataField", - "SubField", ) diff --git a/invenio_records_marc21/services/record/fields/__init__.py b/invenio_records_marc21/services/record/fields/__init__.py deleted file mode 100644 index c27c8e60..00000000 --- a/invenio_records_marc21/services/record/fields/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ -# -*- coding: utf-8 -*- -# -# This file is part of Invenio. -# -# Copyright (C) 2021 Graz University of Technology. -# -# Invenio-Records-Marc21 is free software; you can redistribute it and/or -# modify it under the terms of the MIT License; see LICENSE file for more -# details. - -"""Marc21 field class.""" - -from .control import ControlField -from .data import DataField -from .leader import LeaderField -from .sub import SubField - -__all__ = ( - "ControlField", - "DataField", - "LeaderField", - "SubField", -) diff --git a/invenio_records_marc21/services/record/fields/control.py b/invenio_records_marc21/services/record/fields/control.py deleted file mode 100644 index f451a349..00000000 --- a/invenio_records_marc21/services/record/fields/control.py +++ /dev/null @@ -1,32 +0,0 @@ -# -*- coding: utf-8 -*- -# -# This file is part of Invenio. -# -# Copyright (C) 2021 Graz University of Technology. -# -# Invenio-Records-Marc21 is free software; you can redistribute it and/or -# modify it under the terms of the MIT License; see LICENSE file for more -# details. - -"""Marc21 control field class.""" - - -from os import linesep - - -class ControlField(object): - """ControlField class representing the controlfield HTML tag in MARC21 XML.""" - - def __init__(self, tag: str = "", value: str = ""): - """Default constructor of the class.""" - self.tag = tag - self.value = value - - def to_xml_tag(self, tagsep: str = linesep, indent: int = 4) -> str: - """Get the Marc21 Controlfield XML tag as string.""" - controlfield_tag = " " * indent - controlfield_tag += f'{self.value}' - controlfield_tag += " " * indent - controlfield_tag += "" - controlfield_tag += tagsep - return controlfield_tag diff --git a/invenio_records_marc21/services/record/fields/data.py b/invenio_records_marc21/services/record/fields/data.py deleted file mode 100644 index fbbc7bd0..00000000 --- a/invenio_records_marc21/services/record/fields/data.py +++ /dev/null @@ -1,48 +0,0 @@ -# -*- coding: utf-8 -*- -# -# This file is part of Invenio. -# -# Copyright (C) 2021 Graz University of Technology. -# -# Invenio-Records-Marc21 is free software; you can redistribute it and/or -# modify it under the terms of the MIT License; see LICENSE file for more -# details. - -"""Marc21 data field class.""" - - -from os import linesep - -from .sub import SubField - - -class DataField(object): - """DataField class representing the datafield HTML tag in MARC21 XML.""" - - def __init__(self, tag: str = "", ind1: str = " ", ind2: str = " ", subfields=None): - """Default constructor of the class.""" - self.tag = tag - self.ind1 = ind1 - self.ind2 = ind2 - self.subfields = list() - if subfields: - self.init_subfields(subfields) - - def init_subfields(self, subfields): - """Init containing subfields.""" - for subfield in subfields: - self.subfields.append(SubField(**subfield.attrib, value=subfield.text)) - - def to_xml_tag(self, tagsep: str = linesep, indent: int = 4) -> str: - """Get the Marc21 Datafield XML tag as string.""" - datafield_tag = " " * indent - datafield_tag += ( - f'' - ) - datafield_tag += tagsep - for subfield in self.subfields: - datafield_tag += subfield.to_xml_tag(tagsep, indent) - datafield_tag += " " * indent - datafield_tag += "" - datafield_tag += tagsep - return datafield_tag diff --git a/invenio_records_marc21/services/record/fields/leader.py b/invenio_records_marc21/services/record/fields/leader.py deleted file mode 100644 index 4a0d6ac9..00000000 --- a/invenio_records_marc21/services/record/fields/leader.py +++ /dev/null @@ -1,84 +0,0 @@ -# -*- coding: utf-8 -*- -# -# This file is part of Invenio. -# -# Copyright (C) 2021 Graz University of Technology. -# -# Invenio-Records-Marc21 is free software; you can redistribute it and/or -# modify it under the terms of the MIT License; see LICENSE file for more -# details. - -"""Marc21 leader field class.""" - - -from os import linesep - - -class LeaderField(object): - """LeaderField class representing the leaderfield HTML tag in MARC21 XML.""" - - def __init__(self, data=None, **kwargs): - """Default constructor of the class.""" - if data is not None: - self._load_from_str(data) - else: - self._load_from_dict(**kwargs) - - def _load_from_str(self, data: str): - if len(data) != 24: - raise ValueError("Leader must have 24 characters!!") - self.length = data[0:5] - self.status = data[5] - self.type = data[6] - self.level = data[7] - self.control = data[8] - self.charset = data[9] - - self.ind_count = data[10] - self.sub_count = data[11] - self.address = data[12:17] - self.encoding = data[17] - self.description = data[18] - self.multipart_resource_record_level = data[19] - self.length_field_position = data[20] - self.length_starting_character_position_portion = data[21] - self.length_implementation_defined_portion = data[22] - self.undefined = data[23] - - def _load_from_dict(self, **kwargs): - self.length = kwargs.get("length", "00000") # 00-04 - self.status = kwargs.get("status", "n") # 05 - self.type = kwargs.get("type", "a") # 06 - self.level = kwargs.get("level", "m") # 07 - self.control = kwargs.get("control", " ") # 08 - self.charset = kwargs.get("charset", "a") # 09 - - self.ind_count = kwargs.get("ind_count", "2") # 10 - self.sub_count = kwargs.get("sub_count", "2") # 11 - self.address = kwargs.get("address", "00000") # 12-16 - self.encoding = kwargs.get("encoding", "z") # 17 - self.description = kwargs.get("description", "c") # 18 - self.multipart_resource_record_level = kwargs.get( - "multipart_resource_record_level", "a" - ) # 19 - self.length_field_position = kwargs.get("length_field_position", "4") # 20 - self.length_starting_character_position_portion = kwargs.get( - "length_starting_character_position_portion", "5" - ) # 21 - self.length_implementation_defined_portion = kwargs.get( - "length_implementation_defined_portion", "0" - ) # 22 - self.undefined = kwargs.get("undefined", "0") # 23 - - def to_xml_tag(self, tagsep: str = linesep, indent: int = 4) -> str: - """Get the Marc21 Leaderfield XML tag as string.""" - leader = " " * indent - leader += "" - leader += f"{self.length}{self.status}{self.type}{self.level}{self.control}{self.charset}" - leader += f"{self.ind_count}{self.sub_count}{self.address}{self.encoding}{self.description}" - leader += f"{self.multipart_resource_record_level}{self.length_field_position}" - leader += f"{self.length_starting_character_position_portion}{self.length_implementation_defined_portion}" - leader += f"{self.undefined}" - leader += "" - leader += tagsep - return leader diff --git a/invenio_records_marc21/services/record/fields/sub.py b/invenio_records_marc21/services/record/fields/sub.py deleted file mode 100644 index 70b92dab..00000000 --- a/invenio_records_marc21/services/record/fields/sub.py +++ /dev/null @@ -1,31 +0,0 @@ -# -*- coding: utf-8 -*- -# -# This file is part of Invenio. -# -# Copyright (C) 2021 Graz University of Technology. -# -# Invenio-Records-Marc21 is free software; you can redistribute it and/or -# modify it under the terms of the MIT License; see LICENSE file for more -# details. - -"""Marc21 sub field class.""" - - -from os import linesep - - -class SubField(object): - """SubField class representing the subfield HTML tag in MARC21 XML.""" - - def __init__(self, code: str = "", value: str = ""): - """Default constructor of the class.""" - self.code = code - self.value = value - - def to_xml_tag(self, tagsep: str = linesep, indent: int = 4) -> str: - """Get the Marc21 Subfield XML tag as string.""" - subfield_tag = 2 * " " * indent - subfield_tag += f'{self.value}' - subfield_tag += f"" - subfield_tag += tagsep - return subfield_tag diff --git a/tests/services/record/fields/conftest.py b/tests/services/record/fields/conftest.py deleted file mode 100644 index ff5ec3c5..00000000 --- a/tests/services/record/fields/conftest.py +++ /dev/null @@ -1,40 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2021 Graz University of Technology. -# -# Invenio-Records-Marc21 is free software; you can redistribute it and/or -# modify it under the terms of the MIT License; see LICENSE file for more -# details. - - -"""Pytest configuration. - -See https://pytest-invenio.readthedocs.io/ for documentation on which test -fixtures are available. -""" - -import pytest - - -@pytest.fixture() -def leader_kwargs(): - """Input data (as coming from the view layer).""" - leader = {} - leader["length"] = "00000" - leader["status"] = "n" - leader["type"] = "a" - leader["level"] = "m" - leader["control"] = " " - leader["charset"] = "a" - - leader["ind_count"] = "2" - leader["sub_count"] = "2" - leader["address"] = "00000" - leader["encoding"] = "z" - leader["description"] = "c" - leader["multipart_resource_record_level"] = "a" - leader["length_field_position"] = "4" - leader["length_starting_character_position_portion"] = "5" - leader["length_implementation_defined_portion"] = "0" - leader["undefined"] = "0" - return leader diff --git a/tests/services/record/fields/test_controlfield.py b/tests/services/record/fields/test_controlfield.py deleted file mode 100644 index 2a650066..00000000 --- a/tests/services/record/fields/test_controlfield.py +++ /dev/null @@ -1,44 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2021 Graz University of Technology. -# -# Invenio-Records-Marc21 is free software; you can redistribute it and/or -# modify it under the terms of the MIT License; see LICENSE file for more -# details. - - -"""Pytest configuration. - -See https://pytest-invenio.readthedocs.io/ for documentation on which test -fixtures are available. -""" - -import pytest - -from invenio_records_marc21.services.record import ControlField - - -def test_controlfield(): - controlfield = ControlField() - assert controlfield.tag == "" - assert controlfield.value == "" - - controlfield = ControlField("12") - assert controlfield.tag == "12" - assert controlfield.value == "" - - controlfield = ControlField(tag="123", value="laborum sunt ut nulla") - assert controlfield.tag == "123" - assert controlfield.value == "laborum sunt ut nulla" - - -def test_controlfield_to_xml(): - controlfield = ControlField(tag="123", value="laborum sunt ut nulla") - xml = controlfield.to_xml_tag() - assert 'laborum sunt ut nulla' in xml - assert "" in xml - assert xml.startswith(" ") - assert xml.endswith("\n") - - xml = controlfield.to_xml_tag(tagsep="", indent=0) - assert 'laborum sunt ut nulla' == xml diff --git a/tests/services/record/fields/test_datafield.py b/tests/services/record/fields/test_datafield.py deleted file mode 100644 index 1590fb52..00000000 --- a/tests/services/record/fields/test_datafield.py +++ /dev/null @@ -1,52 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2021 Graz University of Technology. -# -# Invenio-Records-Marc21 is free software; you can redistribute it and/or -# modify it under the terms of the MIT License; see LICENSE file for more -# details. - - -"""Pytest configuration. - -See https://pytest-invenio.readthedocs.io/ for documentation on which test -fixtures are available. -""" - -import pytest - -from invenio_records_marc21.services.record import DataField, SubField - - -def test_datafield(): - datafield = DataField() - assert datafield.tag == "" - assert datafield.ind1 == " " - assert datafield.ind2 == " " - assert datafield.subfields == list() - - datafield = DataField("laborum", "a", "b") - assert datafield.tag == "laborum" - assert datafield.ind1 == "a" - assert datafield.ind2 == "b" - - subfield = SubField(code="123", value="laborum sunt ut nulla") - datafield.subfields.append(subfield) - assert len(datafield.subfields) == 1 - - -def test_datafield_to_xml(): - subfield = SubField(code="123", value="laborum sunt ut nulla") - datafield = DataField("laborum") - datafield.subfields.append(subfield) - - xml = datafield.to_xml_tag() - assert '\n' in xml - assert 'laborum sunt ut nulla' in xml - assert xml.startswith(" ") - assert xml.endswith("\n") - - xml = datafield.to_xml_tag(tagsep="", indent=0) - assert xml.startswith( - 'laborum sunt ut nulla' - ) diff --git a/tests/services/record/fields/test_leaderfield.py b/tests/services/record/fields/test_leaderfield.py deleted file mode 100644 index 0718e686..00000000 --- a/tests/services/record/fields/test_leaderfield.py +++ /dev/null @@ -1,107 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2021 Graz University of Technology. -# -# Invenio-Records-Marc21 is free software; you can redistribute it and/or -# modify it under the terms of the MIT License; see LICENSE file for more -# details. - - -"""Pytest configuration. - -See https://pytest-invenio.readthedocs.io/ for documentation on which test -fixtures are available. -""" - -import pytest - -from invenio_records_marc21.services.record.fields import LeaderField - - -def _assert_field_value(fields, object, expected): - for key in fields: - assert getattr(object, key) == expected[key] - - -def test_leaderfield(leader_kwargs): - leaderfield = LeaderField() - _assert_field_value(leader_kwargs.keys(), leaderfield, leader_kwargs) - - leader_kwargs["length"] = "99999" - leaderfield = LeaderField(**leader_kwargs) - _assert_field_value(leader_kwargs.keys(), leaderfield, leader_kwargs) - - leader_kwargs["status"] = "d" - leaderfield = LeaderField(**leader_kwargs) - _assert_field_value(leader_kwargs.keys(), leaderfield, leader_kwargs) - - leader_kwargs["type"] = "g" - leaderfield = LeaderField(**leader_kwargs) - _assert_field_value(leader_kwargs.keys(), leaderfield, leader_kwargs) - - leader_kwargs["level"] = "c" - leaderfield = LeaderField(**leader_kwargs) - _assert_field_value(leader_kwargs.keys(), leaderfield, leader_kwargs) - - leader_kwargs["control"] = "a" - leaderfield = LeaderField(**leader_kwargs) - _assert_field_value(leader_kwargs.keys(), leaderfield, leader_kwargs) - - leader_kwargs["charset"] = " " - leaderfield = LeaderField(**leader_kwargs) - _assert_field_value(leader_kwargs.keys(), leaderfield, leader_kwargs) - - leader_kwargs["ind_count"] = "6" - leaderfield = LeaderField(**leader_kwargs) - _assert_field_value(leader_kwargs.keys(), leaderfield, leader_kwargs) - - leader_kwargs["sub_count"] = "6" - leaderfield = LeaderField(**leader_kwargs) - _assert_field_value(leader_kwargs.keys(), leaderfield, leader_kwargs) - - leader_kwargs["ind_count"] = "6" - leaderfield = LeaderField(**leader_kwargs) - _assert_field_value(leader_kwargs.keys(), leaderfield, leader_kwargs) - - leader_kwargs["address"] = "99999" - leaderfield = LeaderField(**leader_kwargs) - _assert_field_value(leader_kwargs.keys(), leaderfield, leader_kwargs) - - leader_kwargs["encoding"] = "u" - leaderfield = LeaderField(**leader_kwargs) - _assert_field_value(leader_kwargs.keys(), leaderfield, leader_kwargs) - - leader_kwargs["description"] = "a" - leaderfield = LeaderField(**leader_kwargs) - _assert_field_value(leader_kwargs.keys(), leaderfield, leader_kwargs) - - leader_kwargs["multipart_resource_record_level"] = " " - leaderfield = LeaderField(**leader_kwargs) - _assert_field_value(leader_kwargs.keys(), leaderfield, leader_kwargs) - - leader_kwargs["length_field_position"] = "9" - leaderfield = LeaderField(**leader_kwargs) - _assert_field_value(leader_kwargs.keys(), leaderfield, leader_kwargs) - - leader_kwargs["length_starting_character_position_portion"] = "9" - leaderfield = LeaderField(**leader_kwargs) - _assert_field_value(leader_kwargs.keys(), leaderfield, leader_kwargs) - - leader_kwargs["length_implementation_defined_portion"] = "9" - leaderfield = LeaderField(**leader_kwargs) - _assert_field_value(leader_kwargs.keys(), leaderfield, leader_kwargs) - - leader_kwargs["undefined"] = "9" - leaderfield = LeaderField(**leader_kwargs) - _assert_field_value(leader_kwargs.keys(), leaderfield, leader_kwargs) - - -def test_leaderfield_to_xml(leader_kwargs): - leaderfield = LeaderField(**leader_kwargs) - xml = leaderfield.to_xml_tag() - assert "00000nam a2200000zca4500" in xml - assert xml.startswith(" ") - assert xml.endswith("\n") - - xml = leaderfield.to_xml_tag(tagsep="", indent=0) - assert "00000nam a2200000zca4500" == xml diff --git a/tests/services/record/fields/test_subfield.py b/tests/services/record/fields/test_subfield.py deleted file mode 100644 index 2e4df78f..00000000 --- a/tests/services/record/fields/test_subfield.py +++ /dev/null @@ -1,43 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2021 Graz University of Technology. -# -# Invenio-Records-Marc21 is free software; you can redistribute it and/or -# modify it under the terms of the MIT License; see LICENSE file for more -# details. - - -"""Pytest configuration. - -See https://pytest-invenio.readthedocs.io/ for documentation on which test -fixtures are available. -""" - -import pytest - -from invenio_records_marc21.services.record import SubField - - -def test_subfield(): - subfield = SubField() - assert subfield.code == "" - assert subfield.value == "" - - subfield = SubField("12") - assert subfield.code == "12" - assert subfield.value == "" - - subfield = SubField(code="123", value="laborum sunt ut nulla") - assert subfield.code == "123" - assert subfield.value == "laborum sunt ut nulla" - - -def test_subfield_to_xml(): - subfield = SubField(code="123", value="laborum sunt ut nulla") - xml = subfield.to_xml_tag() - assert 'laborum sunt ut nulla' in xml - assert xml.startswith(" ") - assert xml.endswith("\n") - - xml = subfield.to_xml_tag(tagsep="", indent=0) - assert 'laborum sunt ut nulla' == xml From ecc5141e616da905c159a0abbf201566bbc48391 Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Wed, 12 Jan 2022 13:50:35 +0100 Subject: [PATCH 206/217] modification: improve codequality --- invenio_records_marc21/services/record/metadata.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/invenio_records_marc21/services/record/metadata.py b/invenio_records_marc21/services/record/metadata.py index 97cc9f19..95922663 100644 --- a/invenio_records_marc21/services/record/metadata.py +++ b/invenio_records_marc21/services/record/metadata.py @@ -95,10 +95,7 @@ def contains(self, ref_df: dict, ref_sf: dict): **ref_df, code=ref_sf["code"] ) ) - if element and len(element) > 0 and element[0].text == ref_sf["value"]: - return True - - return False + return element and len(element) > 0 and element[0].text == ref_sf["value"] def emplace_leader( self, From 27532fc75bd69a25761def337ecbe19f66f62363 Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Mon, 17 Jan 2022 16:57:31 +0100 Subject: [PATCH 207/217] bugfix: remove deprecated function call --- invenio_records_marc21/services/record/metadata.py | 1 - 1 file changed, 1 deletion(-) diff --git a/invenio_records_marc21/services/record/metadata.py b/invenio_records_marc21/services/record/metadata.py index 95922663..9836afab 100644 --- a/invenio_records_marc21/services/record/metadata.py +++ b/invenio_records_marc21/services/record/metadata.py @@ -72,7 +72,6 @@ def xml(self, xml: str): def load(self, xml: etree): """Load metadata from etree.""" self._etree = xml - self._to_xml_tree(xml) def _to_xml_tree_from_string(self, xml: str): """Xml string to internal representation method.""" From 9ccac8c9d70f5b530e9854739d4390fefb00295c Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Mon, 17 Jan 2022 17:20:31 +0100 Subject: [PATCH 208/217] modification: codequality improved --- invenio_records_marc21/services/record/metadata.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/invenio_records_marc21/services/record/metadata.py b/invenio_records_marc21/services/record/metadata.py index 9836afab..90082604 100644 --- a/invenio_records_marc21/services/record/metadata.py +++ b/invenio_records_marc21/services/record/metadata.py @@ -65,8 +65,7 @@ def xml(self, xml: str): if not isinstance(xml, str): raise TypeError("xml must be from type str") - tree = self._to_xml_tree_from_string(xml) - self._etree = tree + self._etree = self._to_xml_tree_from_string(xml) self._xml = xml def load(self, xml: etree): From b0427d4d0fefb2ee8dc8fdba05a4c93a4ba36a4e Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Mon, 17 Jan 2022 17:21:21 +0100 Subject: [PATCH 209/217] tests: adding marc21 metadata load etree testset --- tests/services/record/test_marc21_metadata.py | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/tests/services/record/test_marc21_metadata.py b/tests/services/record/test_marc21_metadata.py index 075b2bde..46eb2f94 100644 --- a/tests/services/record/test_marc21_metadata.py +++ b/tests/services/record/test_marc21_metadata.py @@ -155,6 +155,50 @@ def test_contains_metadata(): ) +def test_load_metadata(): + metadata = Marc21Metadata() + + etree_metadata = etree.Element("record") + control = etree.Element("controlfield", tag="001") + control.text = "990079940640203331" + etree_metadata.append(control) + + datafield = etree.Element("datafield", tag="245", ind1="0", ind2="0") + title = etree.Element("subfield", code="a") + title.text = "International Brain-Computer Interface" + datafield.append(title) + + subtitle = etree.Element("subfield", code="b") + subtitle.text = "Subtitle field" + datafield.append(subtitle) + + etree_metadata.append(datafield) + + metadata.load(etree_metadata) + assert metadata._etree == etree_metadata + + controlfield = metadata._etree.xpath(".//controlfield[@tag='001']") + assert controlfield + assert len(controlfield) == 1 + assert controlfield[0].text == "990079940640203331" + + datafield = metadata._etree.xpath(".//datafield[@tag='245' and @ind2='0' and @ind1='0']") + assert datafield + assert len(datafield) == 1 + + title_field = datafield[0].xpath(".//subfield[@code='a']") + + assert title_field + assert len(title_field) == 1 + assert title_field[0].text == "International Brain-Computer Interface" + + subfields = datafield[0].xpath(".//subfield[@code='b']") + + assert subfields + assert len(subfields) == 1 + subfields[0].text = "Subtitle field" + + def test_xml_type(): metadata = Marc21Metadata() From dc3b3b33eff5f695d3363193da89eb576658732c Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Tue, 1 Feb 2022 09:38:32 +0100 Subject: [PATCH 210/217] build: update dependencies --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 908a9b55..54d4e31b 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ tests_require = [ "pytest-invenio>=1.4.0,<2.0.0", "invenio-app>=1.3.0,<2.0.0", - "mock>=4.0.3", + "pytest-mock>=1.6.0", "invenio-previewer>=1.3.4", ] @@ -57,7 +57,7 @@ "arrow>=1.0.0", "dojson>=1.4.0", "lxml>=4.6.2", - "invenio-rdm-records>=0.32.2,<0.34.0", + "invenio-rdm-records>=0.33.2,<0.34.0", ] packages = find_packages() From 6abb12ad870cb0c7d2b2633b05e2d7166208c1ce Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Tue, 1 Feb 2022 10:03:17 +0100 Subject: [PATCH 211/217] modification: codestyle --- MANIFEST.in | 2 +- invenio_records_marc21/cli.py | 8 +++++--- .../records/systemfields/has_draft.py | 2 +- invenio_records_marc21/services/record/__init__.py | 4 +--- invenio_records_marc21/services/services.py | 14 ++++++++++---- tests/records/test_jsonschema.py | 1 - 6 files changed, 18 insertions(+), 13 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index a3df44dd..73ed5a71 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -43,4 +43,4 @@ recursive-include invenio_records_marc21 *.xsd recursive-include invenio_records_marc21 *.less recursive-include tests *.py recursive-include tests *.json -recursive-include tests *.xml \ No newline at end of file +recursive-include tests *.xml diff --git a/invenio_records_marc21/cli.py b/invenio_records_marc21/cli.py index 1810be02..e7db7ea7 100644 --- a/invenio_records_marc21/cli.py +++ b/invenio_records_marc21/cli.py @@ -97,18 +97,20 @@ def create_fake_record(filename): """Create records for demo purposes.""" data_to_use = _load_json(filename) metadata_access = fake_access_right() - data_acces = { + data_access = { "owned_by": [{"user": system_identity().id}], "files": AccessStatusEnum.OPEN.value, "record": metadata_access, } + if metadata_access == AccessStatusEnum.EMBARGOED.value: - data_acces.update({"embargo_date": fake_feature_date()}) + data_access.update({"embargo_date": fake_feature_date()}) + data_access.update({"record": AccessStatusEnum.RESTRICTED.value}) service = current_records_marc21.records_service draft = service.create( - data=data_to_use, identity=system_identity(), access=data_acces + data=data_to_use, identity=system_identity(), access=data_access, files=False ) record = service.publish(id_=draft.id, identity=system_identity()) diff --git a/invenio_records_marc21/records/systemfields/has_draft.py b/invenio_records_marc21/records/systemfields/has_draft.py index 004610bb..adb8629d 100644 --- a/invenio_records_marc21/records/systemfields/has_draft.py +++ b/invenio_records_marc21/records/systemfields/has_draft.py @@ -60,7 +60,7 @@ def pre_dump(self, record, data, **kwargs): dict_set( data, self.key, - record.has_draft + record.has_draft, ) def post_load(self, record, data, **kwargs): diff --git a/invenio_records_marc21/services/record/__init__.py b/invenio_records_marc21/services/record/__init__.py index d25026e8..73a5a4f3 100644 --- a/invenio_records_marc21/services/record/__init__.py +++ b/invenio_records_marc21/services/record/__init__.py @@ -13,6 +13,4 @@ from .metadata import Marc21Metadata -__all__ = ( - "Marc21Metadata", -) +__all__ = ("Marc21Metadata",) diff --git a/invenio_records_marc21/services/services.py b/invenio_records_marc21/services/services.py index 502d504b..febbc141 100644 --- a/invenio_records_marc21/services/services.py +++ b/invenio_records_marc21/services/services.py @@ -12,10 +12,10 @@ from invenio_db import db -from invenio_drafts_resources.services.records import RecordService from invenio_rdm_records.records.systemfields.access.field.record import ( AccessStatusEnum, ) +from invenio_rdm_records.services import RDMRecordService from invenio_records_resources.services.files.service import FileService from invenio_records_resources.services.records.results import RecordItem @@ -24,7 +24,7 @@ from .record import Marc21Metadata -class Marc21RecordService(RecordService): +class Marc21RecordService(RDMRecordService): """Marc21 record service class.""" def _create_data(self, identity, data, metadata, files=False, access=None): @@ -59,7 +59,13 @@ def _create_data(self, identity, data, metadata, files=False, access=None): return data def create( - self, identity, data=None, metadata=Marc21Metadata(), files=False, access=None + self, + identity, + data=None, + metadata=Marc21Metadata(), + files=False, + access=None, + uow=None, ) -> RecordItem: """Create a draft record. @@ -76,7 +82,7 @@ def create( :rtype: `invenio_records_resources.services.records.results.RecordItem` """ data = self._create_data(identity, data, metadata, files, access) - return super().create(identity, data) + return super().create(identity=identity, data=data) def update_draft( self, diff --git a/tests/records/test_jsonschema.py b/tests/records/test_jsonschema.py index f3e94687..0dac2e72 100644 --- a/tests/records/test_jsonschema.py +++ b/tests/records/test_jsonschema.py @@ -12,7 +12,6 @@ """JSONSchema tests.""" import json -import unittest.mock from os.path import dirname, join import pytest From 642649afb249e8ca27f9512f33a4847c4592e9ef Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Tue, 1 Feb 2022 10:10:13 +0100 Subject: [PATCH 212/217] feature: external pid field adding external pid field to the marc21 records and updating the records component --- invenio_records_marc21/records/api.py | 2 + .../services/components/__init__.py | 2 + .../services/components/pids.py | 56 +++++++++++++++++++ invenio_records_marc21/services/config.py | 44 +++++++++++++-- .../services/permissions.py | 8 +++ .../services/schemas/__init__.py | 23 ++++++-- 6 files changed, 125 insertions(+), 10 deletions(-) create mode 100644 invenio_records_marc21/services/components/pids.py diff --git a/invenio_records_marc21/records/api.py b/invenio_records_marc21/records/api.py index abc74b48..b975c8b6 100644 --- a/invenio_records_marc21/records/api.py +++ b/invenio_records_marc21/records/api.py @@ -100,6 +100,8 @@ class Marc21Draft(Draft): bucket = ModelField(dump=False) + pids = DictField("pids") + DraftFile.record_cls = Marc21Draft diff --git a/invenio_records_marc21/services/components/__init__.py b/invenio_records_marc21/services/components/__init__.py index d3700820..1c87318f 100644 --- a/invenio_records_marc21/services/components/__init__.py +++ b/invenio_records_marc21/services/components/__init__.py @@ -13,9 +13,11 @@ from .access import AccessComponent from .metadata import MetadataComponent from .pid import PIDComponent +from .pids import PIDsComponent __all__ = ( "AccessComponent", "PIDComponent", "MetadataComponent", + "PIDsComponent", ) diff --git a/invenio_records_marc21/services/components/pids.py b/invenio_records_marc21/services/components/pids.py new file mode 100644 index 00000000..570be207 --- /dev/null +++ b/invenio_records_marc21/services/components/pids.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2020-2021 CERN. +# Copyright (C) 2020 Northwestern University. +# Copyright (C) 2021 TU Wien. +# Copyright (C) 2021 Graz University of Technology. +# +# 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. + +"""RDM service component for PIDs.""" + +from copy import copy + +from invenio_rdm_records.services.components import PIDsComponent as BasePIDsComponent +from invenio_records_resources.services.uow import TaskOp + +from ..pids.tasks import register_or_update_pid + + +class PIDsComponent(BasePIDsComponent): + """Service component for PIDs.""" + + def publish(self, identity, draft=None, record=None): + """Publish handler.""" + draft_pids = draft.get("pids", {}) + record_pids = copy(record.get("pids", {})) + draft_schemes = set(draft_pids.keys()) + record_schemes = set(record_pids.keys()) + + missing_required_schemes = ( + set(self.service.config.pids_required) - record_schemes - draft_schemes + ) + + self.service.pids.pid_manager.validate(draft_pids, record, raise_errors=True) + + changed_pids = {} + for scheme in draft_schemes.intersection(record_schemes): + record_id = record_pids[scheme]["identifier"] + draft_id = draft_pids[scheme]["identifier"] + if record_id != draft_id: + changed_pids[scheme] = record_pids[scheme] + + self.service.pids.pid_manager.discard_all(changed_pids) + + pids = self.service.pids.pid_manager.create_all( + draft, + pids=draft_pids, + schemes=missing_required_schemes, + ) + + self.service.pids.pid_manager.reserve_all(draft, pids) + record.pids = pids + + for scheme in pids.keys(): + self.uow.register(TaskOp(register_or_update_pid, record["id"], scheme)) diff --git a/invenio_records_marc21/services/config.py b/invenio_records_marc21/services/config.py index b07442ca..99f0b6d1 100644 --- a/invenio_records_marc21/services/config.py +++ b/invenio_records_marc21/services/config.py @@ -23,17 +23,24 @@ from invenio_rdm_records.records.systemfields.access.field.record import ( AccessStatusEnum, ) +from invenio_rdm_records.services.config import has_doi, is_record_and_has_doi +from invenio_rdm_records.services.customizations import ( + FileConfigMixin, + RecordConfigMixin, + SearchOptionsMixin, +) from invenio_records_resources.services import ( ConditionalLink, FileServiceConfig, pagination_links, ) +from invenio_records_resources.services.base.links import Link from invenio_records_resources.services.files.links import FileLink from invenio_records_resources.services.records.facets import TermsFacet from invenio_records_resources.services.records.links import RecordLink from ..records import Marc21Draft, Marc21Parent, Marc21Record -from .components import AccessComponent, MetadataComponent, PIDComponent +from .components import AccessComponent, MetadataComponent, PIDComponent, PIDsComponent from .permissions import Marc21RecordPermissionPolicy from .schemas import Marc21ParentSchema, Marc21RecordSchema @@ -54,7 +61,7 @@ ) -class Marc21SearchOptions(SearchOptions): +class Marc21SearchOptions(SearchOptions, SearchOptionsMixin): """Search options for record search.""" facets = { @@ -62,7 +69,7 @@ class Marc21SearchOptions(SearchOptions): } -class Marc21SearchDraftsOptions(SearchDraftsOptions): +class Marc21SearchDraftsOptions(SearchDraftsOptions, SearchOptionsMixin): """Search options for drafts search.""" facets = { @@ -71,7 +78,7 @@ class Marc21SearchDraftsOptions(SearchDraftsOptions): } -class Marc21RecordServiceConfig(RecordServiceConfig): +class Marc21RecordServiceConfig(RecordServiceConfig, RecordConfigMixin): """Marc21 record service config.""" # Record class @@ -99,6 +106,7 @@ class Marc21RecordServiceConfig(RecordServiceConfig): AccessComponent, DraftFilesComponent, PIDComponent, + PIDsComponent, ] links_item = { @@ -112,6 +120,26 @@ class Marc21RecordServiceConfig(RecordServiceConfig): if_=RecordLink("{+ui}/marc21/{id}"), else_=RecordLink("{+ui}/marc21/uploads/{id}"), ), + "self_doi": Link( + "{+ui}/doi/{+pid_doi}", + when=is_record_and_has_doi, + vars=lambda record, vars: vars.update( + { + f"pid_{scheme}": pid["identifier"] + for (scheme, pid) in record.pids.items() + } + ), + ), + "doi": Link( + "https://doi.org/{+pid_doi}", + when=has_doi, + vars=lambda record, vars: vars.update( + { + f"pid_{scheme}": pid["identifier"] + for (scheme, pid) in record.pids.items() + } + ), + ), "latest": RecordLink("{+api}/marc21/{id}/versions/latest"), "latest_html": RecordLink("{+ui}/marc21/{id}/latest"), "draft": RecordLink("{+api}/marc21/{id}/draft", when=is_record), @@ -121,11 +149,15 @@ class Marc21RecordServiceConfig(RecordServiceConfig): "versions": RecordLink("{+api}/marc21/{id}/versions"), } + # PIDs providers - set from config in customizations. + pids_providers = {} + pids_required = [] + # # Record files # -class Marc21RecordFilesServiceConfig(FileServiceConfig): +class Marc21RecordFilesServiceConfig(FileServiceConfig, FileConfigMixin): """Marc21 record files service configuration.""" record_cls = Marc21Record @@ -145,7 +177,7 @@ class Marc21RecordFilesServiceConfig(FileServiceConfig): # # Draft files # -class Marc21DraftFilesServiceConfig(FileServiceConfig): +class Marc21DraftFilesServiceConfig(FileServiceConfig, FileConfigMixin): """Marc21 draft files service configuration.""" record_cls = Marc21Draft diff --git a/invenio_records_marc21/services/permissions.py b/invenio_records_marc21/services/permissions.py index f706288a..ca0f439b 100644 --- a/invenio_records_marc21/services/permissions.py +++ b/invenio_records_marc21/services/permissions.py @@ -52,3 +52,11 @@ class Marc21RecordPermissionPolicy(RecordPermissionPolicy): can_create_files = [AnyUser(), SystemProcess()] can_update_files = [AnyUser()] can_delete_files = [AnyUser()] + + # + # PIDs + can_pid_create = [AnyUser()] + can_pid_register = [AnyUser(), SystemProcess()] + can_pid_update = [AnyUser()] + can_pid_discard = [AnyUser()] + can_pid_delete = [AnyUser()] diff --git a/invenio_records_marc21/services/schemas/__init__.py b/invenio_records_marc21/services/schemas/__init__.py index ccbaa5d1..c7286b3f 100644 --- a/invenio_records_marc21/services/schemas/__init__.py +++ b/invenio_records_marc21/services/schemas/__init__.py @@ -9,13 +9,17 @@ # details. """Marc21 record schemas.""" + +from flask import current_app +from flask_babelex import lazy_gettext as _ from invenio_drafts_resources.services.records.schema import ParentSchema from invenio_rdm_records.services.schemas.access import AccessSchema from invenio_rdm_records.services.schemas.parent.access import ParentAccessSchema from invenio_records_resources.services.records.schema import BaseRecordSchema +from marshmallow import ValidationError from marshmallow.decorators import post_dump -from marshmallow.fields import Boolean, Integer, List, Nested, Str -from marshmallow_utils.fields import NestedAttribute +from marshmallow.fields import Boolean, Dict, Integer, Nested, Str +from marshmallow_utils.fields import NestedAttribute, SanitizedUnicode from marshmallow_utils.permissions import FieldPermissionsMixin from .files import FilesSchema @@ -24,6 +28,12 @@ from .versions import VersionsSchema +def validate_scheme(scheme): + """Validate a PID scheme.""" + if scheme not in current_app.config["INVENIO_MARC21_PERSISTENT_IDENTIFIERS"]: + raise ValidationError(_("Invalid persistent identifier scheme.")) + + class Marc21ParentSchema(ParentSchema): """Record schema.""" @@ -41,8 +51,11 @@ class Marc21RecordSchema(BaseRecordSchema, FieldPermissionsMixin): "files": "update_draft", } id = Str() - # pid - pids = List(NestedAttribute(PIDSchema)) + + pids = Dict( + keys=SanitizedUnicode(validate=validate_scheme), + values=Nested(PIDSchema), + ) parent = NestedAttribute(Marc21ParentSchema, dump_only=True) @@ -72,6 +85,8 @@ def default_nested(self, data, many, **kwargs): """ if not data.get("metadata"): data["metadata"] = {} + if not data.get("pids"): + data["pids"] = {} return data From 3bae2d1861d286daaaff61aac135c0a667b468ed Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Tue, 1 Feb 2022 10:12:47 +0100 Subject: [PATCH 213/217] modification: customization service config adding a customization of the service configuration using default the permission policy of rdm and external pid providers --- invenio_records_marc21/ext.py | 46 ++++++++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/invenio_records_marc21/ext.py b/invenio_records_marc21/ext.py index d85f882e..ed51f4e4 100644 --- a/invenio_records_marc21/ext.py +++ b/invenio_records_marc21/ext.py @@ -13,6 +13,7 @@ from __future__ import absolute_import, print_function import six +from invenio_rdm_records.services.pids import PIDManager, PIDsService from invenio_records_resources.resources import FileResource from invenio_records_resources.services import FileService from werkzeug.utils import import_string @@ -72,7 +73,7 @@ def init_config(self, app): """ with_endpoints = app.config.get("INVENIO_MARC21_ENDPOINTS_ENABLED", True) for k in dir(config): - if k.startswith("INVENIO_MARC21_"): + if k.startswith("INVENIO_MARC21_") or k.startswith("DATACITE_"): app.config.setdefault(k, getattr(config, k)) elif k == "SEARCH_UI_JSTEMPLATE_RESULTS": app.config["SEARCH_UI_JSTEMPLATE_RESULTS"] = getattr(config, k) @@ -90,17 +91,44 @@ def init_config(self, app): app.config.setdefault(n, {}) app.config[n].update(getattr(config, k)) + def service_configs(self, app): + """Customized service configs.""" + permission_policy = app.config.get( + "RDM_PERMISSION_POLICY", Marc21RecordPermissionPolicy + ) + + doi_enabled = app.config["DATACITE_ENABLED"] + pid_providers = app.config["INVENIO_MARC21_PERSISTENT_IDENTIFIER_PROVIDERS"] + pids = app.config["INVENIO_MARC21_PERSISTENT_IDENTIFIERS"] + + class ServiceConfigs: + record = Marc21RecordServiceConfig.customize( + permission_policy=permission_policy, + pid_providers=pid_providers, + pids=pids, + doi_enabled=doi_enabled, + # search=search_opts, + # search_drafts=search_drafts_opts, + # search_versions=search_versions_opts, + ) + file = Marc21RecordFilesServiceConfig.customize( + permission_policy=permission_policy, + ) + file_draft = Marc21DraftFilesServiceConfig.customize( + permission_policy=permission_policy, + ) + + return ServiceConfigs + def init_services(self, app): """Initialize services.""" - service_config = Marc21RecordServiceConfig - service_config.permission_policy_cls = obj_or_import_string( - app.config.get("RECORDS_PERMISSIONS_RECORD_POLICY1"), - default=Marc21RecordPermissionPolicy, - ) + service_config = self.service_configs(app) + self.records_service = Marc21RecordService( - config=service_config, - files_service=FileService(Marc21RecordFilesServiceConfig), - draft_files_service=FileService(Marc21DraftFilesServiceConfig), + config=service_config.record, + files_service=FileService(service_config.file), + draft_files_service=FileService(service_config.file_draft), + pids_service=PIDsService(service_config.record, PIDManager), ) self.templates_service = Marc21TemplateService( config=Marc21TemplateConfig, From 5367706f171cc824d95b87e0b40a6643b3f11c9f Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Tue, 1 Feb 2022 12:59:24 +0100 Subject: [PATCH 214/217] modification: init configuration doi --- invenio_records_marc21/config.py | 103 +++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/invenio_records_marc21/config.py b/invenio_records_marc21/config.py index ab562ac5..48f73358 100644 --- a/invenio_records_marc21/config.py +++ b/invenio_records_marc21/config.py @@ -12,7 +12,17 @@ from __future__ import absolute_import, print_function +import idutils from celery.schedules import crontab +from invenio_rdm_records.services.pids import providers + +from .resources.serializers.datacite import Marc21DataCite43JSONSerializer + + +def _(x): + """Identity function for string extraction.""" + return x + INVENIO_MARC21_BASE_TEMPLATE = "invenio_records_marc21/base.html" @@ -73,3 +83,96 @@ }, } """Celery tasks for the module.""" + + +# +# Persistent identifiers configuration +# +INVENIO_MARC21_PERSISTENT_IDENTIFIER_PROVIDERS = [ + # DataCite DOI provider + providers.DataCitePIDProvider( + "datacite", + client=providers.DataCiteClient("datacite", config_prefix="DATACITE"), + pid_type="doi", + serializer=Marc21DataCite43JSONSerializer(), + label=_("DOI"), + ), + # DOI provider for externally managed DOIs + providers.ExternalPIDProvider( + "external", + "doi", + validators=[providers.BlockedPrefixes(config_names=["DATACITE_PREFIX"])], + label=_("DOI"), + ), +] +"""A list of configured persistent identifier providers. + +ATTENTION: All providers (and clients) takes a name as the first parameter. +This name is stored in the database and used in the REST API in order to +identify the given provider and client. + +The name is further used to configure the desired persistent identifiers (see +``INVENIO_MARC21_PERSISTENT_IDENTIFIER_PROVIDERS`` below) +""" + + +INVENIO_MARC21_IDENTIFIERS_SCHEMES = { + "doi": {"label": _("DOI"), "validator": idutils.is_doi, "datacite": "DOI"}, +} +"""These are used for main, alternate and related identifiers.""" + +INVENIO_MARC21_PERSISTENT_IDENTIFIERS = { + "doi": { + "providers": ["datacite", "external"], + "required": True, + "label": _("DOI"), + "validator": idutils.is_doi, + "normalizer": idutils.normalize_doi, + }, +} +"""The configured persistent identifiers for records. + +.. code-block:: python + + "": { + "providers": ["", "", ...], + "required": True/False, + } +""" + +# Configuration for the DataCiteClient used by the DataCitePIDProvider +# Configuration may come from RDM records module +DATACITE_ENABLED = False +"""Flag to enable/disable DOI registration.""" + + +DATACITE_USERNAME = "" +"""DataCite username.""" + + +DATACITE_PASSWORD = "" +"""DataCite password.""" + + +DATACITE_PREFIX = "" +"""DataCite DOI prefix.""" + + +DATACITE_TEST_MODE = True +"""DataCite test mode enabled.""" + + +DATACITE_FORMAT = "{prefix}/{id}" +"""A string used for formatting the DOI or a callable. + +If set to a string, you can used ``{prefix}`` and ``{id}`` inside the string. + +You can also provide a callable instead: + +.. code-block:: python + + def make_doi(prefix, record): + return f"{prefix}/{record.pid.pid_value}" + + DATACITE_FORMAT = make_doi +""" From 18c24e7014d678d79677a402b22da6017b248300 Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Tue, 1 Feb 2022 13:15:58 +0100 Subject: [PATCH 215/217] feature: marc21 datacite json serializer --- .../resources/serializers/__init__.py | 2 + .../serializers/datacite/__init__.py | 24 +++++ .../resources/serializers/datacite/schema.py | 94 +++++++++++++++++++ 3 files changed, 120 insertions(+) create mode 100644 invenio_records_marc21/resources/serializers/datacite/__init__.py create mode 100644 invenio_records_marc21/resources/serializers/datacite/schema.py diff --git a/invenio_records_marc21/resources/serializers/__init__.py b/invenio_records_marc21/resources/serializers/__init__.py index 7c248209..8af72a76 100644 --- a/invenio_records_marc21/resources/serializers/__init__.py +++ b/invenio_records_marc21/resources/serializers/__init__.py @@ -12,10 +12,12 @@ from __future__ import absolute_import, print_function +from .datacite import Marc21DataCite43JSONSerializer from .errors import Marc21XMLConvertError from .serializer import Marc21BASESerializer, Marc21JSONSerializer, Marc21XMLSerializer __all__ = ( + "Marc21DataCite43JSONSerializer", "Marc21XMLConvertError", "Marc21BASESerializer", "Marc21JSONSerializer", diff --git a/invenio_records_marc21/resources/serializers/datacite/__init__.py b/invenio_records_marc21/resources/serializers/datacite/__init__.py new file mode 100644 index 00000000..17fc06f4 --- /dev/null +++ b/invenio_records_marc21/resources/serializers/datacite/__init__.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + + +"""DataCite Serializers for Invenio Marc21 Records.""" + +from flask_resources.serializers import MarshmallowJSONSerializer + +from .schema import Marc21DataCite43Schema + + +class Marc21DataCite43JSONSerializer(MarshmallowJSONSerializer): + """Marshmallow based DataCite serializer for records.""" + + def __init__(self, **options): + """Constructor.""" + super().__init__(schema_cls=Marc21DataCite43Schema, **options) diff --git a/invenio_records_marc21/resources/serializers/datacite/schema.py b/invenio_records_marc21/resources/serializers/datacite/schema.py new file mode 100644 index 00000000..b1cda34b --- /dev/null +++ b/invenio_records_marc21/resources/serializers/datacite/schema.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2021 CERN. +# Copyright (C) 2021 Northwestern University. +# +# 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. + + +"""DataCite based Schema for Invenio RDM Records.""" + +from flask import current_app +from flask_babelex import lazy_gettext as _ +from marshmallow import Schema, fields, missing + + +def get_scheme_datacite(scheme, config_name, default=None): + """Returns the datacite equivalent of a scheme.""" + config_item = current_app.config[config_name] + return config_item.get(scheme, {}).get("datacite", default) + + +class CreatorSchema43(Schema): + """Creator schema for v4.""" + + name = fields.Str(attribute="personal_name") + + +class Marc21DataCite43Schema(Schema): + """DataCite JSON 4.3 Marshmallow Schema.""" + + # PIDS-FIXME: What about versioning links and related ids + identifiers = fields.Method("get_identifiers") + types = fields.Method("get_type") + titles = fields.Method("get_titles") + creators = fields.Nested(CreatorSchema43, attribute="metadata.main_entry_personal_name") + publisher = fields.Str(attribute="metadata.publisher") + publicationYear = fields.Method("get_publication_year") + schemaVersion = fields.Constant("http://datacite.org/schema/kernel-4") + + def get_type(self, obj): + """Get resource type.""" + # FIXME: The metadatafield 970 from dojson module needed + return { + "resourceTypeGeneral": "Other", + "resourceType": "Text", + } + + def get_titles(self, obj): + """Get titles list.""" + titles = obj["metadata"].get("title_statement", {}) + return {"title": titles.get("title", "")} + + def get_publication_year(self, obj): + """Get publication year from edtf date.""" + publication_dates = obj["metadata"].get( + "dates_of_publication_and_or_sequential_designation", {} + ) + publication_date = publication_dates.get( + "dates_of_publication_and_or_sequential_designation", "" + ) + return publication_date + + def get_language(self, obj): + """Get language.""" + languages = obj["metadata"].get("languages", []) + if languages: + # DataCite support only one language, so we take the first. + return languages[0]["id"] + + return missing + + def get_identifiers(self, obj): + """Get (main and alternate) identifiers list.""" + serialized_identifiers = [] + + # pids go first so the DOI from the record is included + pids = obj["pids"] + for scheme, id_ in pids.items(): + id_scheme = get_scheme_datacite( + scheme, + "INVENIO_MARC21_IDENTIFIERS_SCHEMES", + default=scheme, + ) + + if id_scheme: + serialized_identifiers.append( + { + "identifier": id_["identifier"], + "identifierType": id_scheme, + } + ) + + return serialized_identifiers or missing From aaf6b4140b0d1e31f60c09e70bbbb6ef47ed792d Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Tue, 1 Feb 2022 13:16:54 +0100 Subject: [PATCH 216/217] feature: pids tasks register external pid --- .../services/pids/__init__.py | 15 +++++++++++ invenio_records_marc21/services/pids/tasks.py | 26 +++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 invenio_records_marc21/services/pids/__init__.py create mode 100644 invenio_records_marc21/services/pids/tasks.py diff --git a/invenio_records_marc21/services/pids/__init__.py b/invenio_records_marc21/services/pids/__init__.py new file mode 100644 index 00000000..fb1c51b6 --- /dev/null +++ b/invenio_records_marc21/services/pids/__init__.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + +"""PIDs module.""" + +from .tasks import register_or_update_pid + +__all__ = ("register_or_update_pid",) diff --git a/invenio_records_marc21/services/pids/tasks.py b/invenio_records_marc21/services/pids/tasks.py new file mode 100644 index 00000000..3b18677c --- /dev/null +++ b/invenio_records_marc21/services/pids/tasks.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + +"""RDM PIDs Service tasks.""" + +from celery import shared_task +from invenio_access.permissions import system_identity + +from ...proxies import current_records_marc21 + + +@shared_task(ignore_result=True) +def register_or_update_pid(recid, scheme): + """Update a PID on the remote provider.""" + current_records_marc21.records_service.pids.register_or_update( + id_=recid, + identity=system_identity, + scheme=scheme, + ) From 9382bbdab63a4f4401eb55f367d685c8bc6a0bba Mon Sep 17 00:00:00 2001 From: Philipp Gualdi Date: Tue, 1 Feb 2022 13:17:29 +0100 Subject: [PATCH 217/217] tests: external pids testset adding external pids pytests --- .../resources/serializers/datacite/schema.py | 4 +- tests/conftest.py | 38 ++- tests/resources/serializers/conftest.py | 251 ++++++++++-------- .../serializers/test_datacite_serializer.py | 36 +++ tests/services/conftest.py | 21 +- tests/services/record/test_marc21_metadata.py | 4 +- tests/services/test_create_record.py | 16 +- tests/services/test_pids_service.py | 125 +++++++++ tests/services/test_pids_tasks.py | 46 ++++ tests/services/test_record_service.py | 53 ++-- 10 files changed, 418 insertions(+), 176 deletions(-) create mode 100644 tests/resources/serializers/test_datacite_serializer.py create mode 100644 tests/services/test_pids_service.py create mode 100644 tests/services/test_pids_tasks.py diff --git a/invenio_records_marc21/resources/serializers/datacite/schema.py b/invenio_records_marc21/resources/serializers/datacite/schema.py index b1cda34b..4813d917 100644 --- a/invenio_records_marc21/resources/serializers/datacite/schema.py +++ b/invenio_records_marc21/resources/serializers/datacite/schema.py @@ -33,7 +33,9 @@ class Marc21DataCite43Schema(Schema): identifiers = fields.Method("get_identifiers") types = fields.Method("get_type") titles = fields.Method("get_titles") - creators = fields.Nested(CreatorSchema43, attribute="metadata.main_entry_personal_name") + creators = fields.Nested( + CreatorSchema43, attribute="metadata.main_entry_personal_name" + ) publisher = fields.Str(attribute="metadata.publisher") publicationYear = fields.Method("get_publication_year") schemaVersion = fields.Constant("http://datacite.org/schema/kernel-4") diff --git a/tests/conftest.py b/tests/conftest.py index 165dc97f..96363fa4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -21,8 +21,17 @@ import pytest from invenio_app.factory import create_api from invenio_files_rest.models import Location +from invenio_rdm_records.services.pids import PIDManager, PIDsService +from invenio_records_resources.services import FileService -from invenio_records_marc21.records import Marc21Draft, Marc21Parent +from invenio_records_marc21.records import Marc21Draft, Marc21Parent, Marc21Record +from invenio_records_marc21.services import ( + Marc21DraftFilesServiceConfig, + Marc21RecordFilesServiceConfig, + Marc21RecordPermissionPolicy, + Marc21RecordService, + Marc21RecordServiceConfig, +) @pytest.fixture() @@ -103,10 +112,37 @@ def app_config(app_config): # Variable not used. We set it to silent warnings app_config["JSONSCHEMAS_HOST"] = "not-used" + app_config["RDM_PERMISSION_POLICY"] = Marc21RecordPermissionPolicy + # Enable DOI miting + app_config["DATACITE_ENABLED"] = True + app_config["DATACITE_USERNAME"] = "INVALID" + app_config["DATACITE_PASSWORD"] = "INVALID" + app_config["DATACITE_PREFIX"] = "10.123" return app_config +@pytest.fixture(scope="module") +def service(app): + """Service instance.""" + doi_enabled = False + pid_providers = app.config["INVENIO_MARC21_PERSISTENT_IDENTIFIER_PROVIDERS"] + pids = app.config["INVENIO_MARC21_PERSISTENT_IDENTIFIERS"] + + config = Marc21RecordServiceConfig.customize( + permission_policy=Marc21RecordPermissionPolicy, + pid_providers=pid_providers, + pids=pids, + doi_enabled=doi_enabled, + ) + return Marc21RecordService( + config=config, + files_service=FileService(Marc21RecordFilesServiceConfig()), + draft_files_service=FileService(Marc21DraftFilesServiceConfig()), + pids_service=PIDsService(config, PIDManager), + ) + + @pytest.fixture(scope="module") def app(base_app, database): """Application with just a database. diff --git a/tests/resources/serializers/conftest.py b/tests/resources/serializers/conftest.py index 03157eea..ead83180 100644 --- a/tests/resources/serializers/conftest.py +++ b/tests/resources/serializers/conftest.py @@ -24,130 +24,139 @@ def marc21_metadata(): """Record UI metadata.""" return { - "json": { - "leader": { - "undefined": 0, - "record_length": 0, - "record_status": "new", - "encoding_level": "not_applicable", - "type_of_record": "language_material", - "indicator_count": 2, - "bibliographic_level": "monograph_item", - "subfield_code_count": 2, - "base_address_of_data": 0, - "character_coding_scheme": "ucs_unicode", - "descriptive_cataloging_form": "isbd_punctuation_omitteed", - "multipart_resource_record_level": "set", - "length_of_the_length_of_field_portion": 4, - "length_of_the_implementation_defined_portion": 0, - "length_of_the_starting_character_position_portion": 5, - }, - "summary": [ - { - "summary": "A wonderful serenity has taken possession of my entire soul, like these sweet mornings of spring which I enjoy with my whole heart. I am alone, and feel the charm of existence in this spot, which was created for the bliss of souls like mine. I am so happy, my dear friend, so absorbed in the exquisite sense of mere tranquil existence, that I neglect my talents. I should be incapable of drawing a single stroke at the present moment; and yet I feel that I never was a greater artist than now. When, while the lovely valley teems with vapour around me, and the meridian sun strikes the upper surface of the impenetrable foliage of my trees, and but a few stray gleams steal into the inner sanctuary, I throw myself down among the tall grass by the trickling stream; and, as I lie close to the earth, a thousand unknown plants are noticed by me: when I hear the buzz of the little world among the stalks, and grow familiar with the countless indescribable forms of the insects and flies, then I feel the presence of the Almighty, who formed us in his own image, and the breath ", - "__order__": ["summary", "display_constant_controller"], - "display_constant_controller": "Summary", - } + "leader": { + "undefined": 0, + "record_length": 0, + "record_status": "new", + "encoding_level": "not_applicable", + "type_of_record": "language_material", + "indicator_count": 2, + "bibliographic_level": "monograph_item", + "subfield_code_count": 2, + "base_address_of_data": 0, + "character_coding_scheme": "ucs_unicode", + "descriptive_cataloging_form": "isbd_punctuation_omitteed", + "multipart_resource_record_level": "set", + "length_of_the_length_of_field_portion": 4, + "length_of_the_implementation_defined_portion": 0, + "length_of_the_starting_character_position_portion": 5, + }, + "summary": [ + { + "summary": "A wonderful serenity has taken possession of my entire soul, like these sweet mornings of spring which I enjoy with my whole heart. I am alone, and feel the charm of existence in this spot, which was created for the bliss of souls like mine. I am so happy, my dear friend, so absorbed in the exquisite sense of mere tranquil existence, that I neglect my talents. I should be incapable of drawing a single stroke at the present moment; and yet I feel that I never was a greater artist than now. When, while the lovely valley teems with vapour around me, and the meridian sun strikes the upper surface of the impenetrable foliage of my trees, and but a few stray gleams steal into the inner sanctuary, I throw myself down among the tall grass by the trickling stream; and, as I lie close to the earth, a thousand unknown plants are noticed by me: when I hear the buzz of the little world among the stalks, and grow familiar with the countless indescribable forms of the insects and flies, then I feel the presence of the Almighty, who formed us in his own image, and the breath ", + "__order__": ["summary", "display_constant_controller"], + "display_constant_controller": "Summary", + } + ], + "__order__": [ + "leader", + "title_statement", + "subject_added_entry_topical_term", + "subject_added_entry_topical_term", + "subject_added_entry_topical_term", + "subject_added_entry_topical_term", + "summary", + "production_publication_distribution_manufacture_and_copyright_notice", + "main_entry_personal_name", + ], + "title_statement": { + "__order__": [ + "title", + "remainder_of_title", + "statement_of_responsibility", + "title_added_entry", + "nonfiling_characters", ], + "title": "Proceedings of the 3rd International Brain-Computer Interface Workshop and Training Course", + "title_added_entry": "No added entry", + "remainder_of_title": "Subtitle field.", + "nonfiling_characters": "0", + "statement_of_responsibility": "hrsg. von Josef Frank", + }, + "main_entry_personal_name": { "__order__": [ - "leader", - "title_statement", - "subject_added_entry_topical_term", - "subject_added_entry_topical_term", - "subject_added_entry_topical_term", - "subject_added_entry_topical_term", - "summary", - "production_publication_distribution_manufacture_and_copyright_notice", - "main_entry_personal_name", + "affiliation", + "type_of_personal_name_entry_element", + "personal_name", ], - "title_statement": { - "title": "Proceedings of the 3rd International Brain-Computer Interface Workshop and Training Course", + "affiliation": "Institute of Solid State Physics (5130)", + "type_of_personal_name_entry_element": "Surname", + "personal_name": "Philipp", + }, + "complex_see_reference_subject": { + "__order__": ["heading_referred_to"], + "heading_referred_to": "TU Graz", + }, + "dates_of_publication_and_or_sequential_designation": { + "__order__": ["dates_of_publication_and_or_sequential_designation"], + "dates_of_publication_and_or_sequential_designation": "2022", + }, + "subject_added_entry_topical_term": [ + { "__order__": [ - "title", - "remainder_of_title", - "statement_of_responsibility", - "title_added_entry", - "nonfiling_characters", + "miscellaneous_information", + "level_of_subject", + "thesaurus", ], - "title_added_entry": "No added entry", - "remainder_of_title": "Subtitle field.", - "nonfiling_characters": "0", - "statement_of_responsibility": "hrsg. von Josef Frank", + "thesaurus": "Source not specified", + "level_of_subject": "No information provided", + "miscellaneous_information": ["Test"], }, - "main_entry_personal_name": { - "__order__": ["affiliation", "type_of_personal_name_entry_element"], - "affiliation": "Institute of Solid State Physics (5130)", - "type_of_personal_name_entry_element": "Surname", + { + "__order__": [ + "miscellaneous_information", + "level_of_subject", + "thesaurus", + ], + "thesaurus": "Source not specified", + "level_of_subject": "No information provided", + "miscellaneous_information": ["Invenio"], }, - "subject_added_entry_topical_term": [ - { - "__order__": [ - "miscellaneous_information", - "level_of_subject", - "thesaurus", - ], - "thesaurus": "Source not specified", - "level_of_subject": "No information provided", - "miscellaneous_information": ["Test"], - }, - { - "__order__": [ - "miscellaneous_information", - "level_of_subject", - "thesaurus", - ], - "thesaurus": "Source not specified", - "level_of_subject": "No information provided", - "miscellaneous_information": ["Invenio"], - }, - { - "__order__": [ - "miscellaneous_information", - "level_of_subject", - "thesaurus", - ], - "thesaurus": "Source not specified", - "level_of_subject": "No information provided", - "miscellaneous_information": ["TUGraz"], - }, - { - "__order__": [ - "topical_term_or_geographic_name_entry_element", - "level_of_subject", - "thesaurus", - ], - "thesaurus": "Source not specified", - "level_of_subject": "No information provided", - "topical_term_or_geographic_name_entry_element": "Marc21", - }, - ], - "production_publication_distribution_manufacture_and_copyright_notice": [ - { - "__order__": [ - "place_of_production_publication_distribution_manufacture", - "name_of_producer_publisher_distributor_manufacturer", - "name_of_producer_publisher_distributor_manufacturer", - "name_of_producer_publisher_distributor_manufacturer", - "name_of_producer_publisher_distributor_manufacturer", - "date_of_production_publication_distribution_manufacture_or_copyright_notice", - "sequence_of_statements", - ], - "sequence_of_statements": "Not applicable/No information provided/Earliest", - "name_of_producer_publisher_distributor_manufacturer": [ - "Hulk", - "Thor", - "Captain", - "Black Widow", - ], - "place_of_production_publication_distribution_manufacture": [ - "Tu Graz" - ], - "date_of_production_publication_distribution_manufacture_or_copyright_notice": [ - "2004" - ], - } - ], - } + { + "__order__": [ + "miscellaneous_information", + "level_of_subject", + "thesaurus", + ], + "thesaurus": "Source not specified", + "level_of_subject": "No information provided", + "miscellaneous_information": ["TUGraz"], + }, + { + "__order__": [ + "topical_term_or_geographic_name_entry_element", + "level_of_subject", + "thesaurus", + ], + "thesaurus": "Source not specified", + "level_of_subject": "No information provided", + "topical_term_or_geographic_name_entry_element": "Marc21", + }, + ], + "production_publication_distribution_manufacture_and_copyright_notice": [ + { + "__order__": [ + "place_of_production_publication_distribution_manufacture", + "name_of_producer_publisher_distributor_manufacturer", + "name_of_producer_publisher_distributor_manufacturer", + "name_of_producer_publisher_distributor_manufacturer", + "name_of_producer_publisher_distributor_manufacturer", + "date_of_production_publication_distribution_manufacture_or_copyright_notice", + "sequence_of_statements", + ], + "sequence_of_statements": "Not applicable/No information provided/Earliest", + "name_of_producer_publisher_distributor_manufacturer": [ + "Hulk", + "Thor", + "Captain", + "Black Widow", + ], + "place_of_production_publication_distribution_manufacture": ["Tu Graz"], + "date_of_production_publication_distribution_manufacture_or_copyright_notice": [ + "2004" + ], + } + ], } @@ -162,7 +171,13 @@ def full_record(marc21_record, marc21_metadata): "obj_type": "rec", "pid_type": "marcid", } - + marc21_record["pids"] = { + "doi": { + "identifier": "10.5281/inveniordm.1234", + "provider": "datacite", + "client": "inveniordm", + }, + } marc21_record["files"] = {"enabled": True} marc21_record["access"] = { "files": "restricted", diff --git a/tests/resources/serializers/test_datacite_serializer.py b/tests/resources/serializers/test_datacite_serializer.py new file mode 100644 index 00000000..3ea33e46 --- /dev/null +++ b/tests/resources/serializers/test_datacite_serializer.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + +"""Resources serializers tests.""" + + +from invenio_records_marc21.resources.serializers import Marc21DataCite43JSONSerializer + + +def test_datacite43_serializer(app, full_record): + """Test serializer to DataCite 4.3 JSON""" + expected_data = { + "schemaVersion": "http://datacite.org/schema/kernel-4", + "publicationYear": "2022", + "identifiers": [ + {"identifier": "10.5281/inveniordm.1234", "identifierType": "DOI"} + ], + "titles": { + "title": "Proceedings of the 3rd International Brain-Computer Interface Workshop and Training Course" + }, + "creators": {"name": "Philipp"}, + "types": {"resourceTypeGeneral": "Other", "resourceType": "Text"}, + } + + with app.app_context(): + serializer = Marc21DataCite43JSONSerializer() + serialized_record = serializer.dump_one(full_record) + + assert serialized_record == expected_data diff --git a/tests/services/conftest.py b/tests/services/conftest.py index 2eedbf2d..bdc9ff9d 100644 --- a/tests/services/conftest.py +++ b/tests/services/conftest.py @@ -19,31 +19,22 @@ from flask_principal import Identity from invenio_access import any_user from invenio_app.factory import create_api +from invenio_rdm_records.services.pids import PIDManager, PIDsService from invenio_records_marc21.proxies import current_records_marc21 -from invenio_records_marc21.services import ( - Marc21RecordService, - Marc21RecordServiceConfig, -) from invenio_records_marc21.services.record import Marc21Metadata - -@pytest.fixture(scope="module") -def create_app(instance_path): - """Application factory fixture.""" - return create_api - - RunningApp = namedtuple("RunningApp", ["app", "service", "identity_simple"]) @pytest.fixture(scope="module") -def running_app(app, service, identity_simple): +def running_app(app, identity_simple): """This fixture provides an app with the typically needed db data loaded. All of these fixtures are often needed together, so collecting them under a semantic umbrella makes sense. """ + service = app.extensions["invenio-records-marc21"].records_service return RunningApp(app, service, identity_simple) @@ -55,12 +46,6 @@ def identity_simple(): return i -@pytest.fixture(scope="module") -def service(appctx): - """Service instance.""" - return Marc21RecordService(config=Marc21RecordServiceConfig()) - - @pytest.fixture(scope="session") def metadata(): """Input data (as coming from the view layer).""" diff --git a/tests/services/record/test_marc21_metadata.py b/tests/services/record/test_marc21_metadata.py index 46eb2f94..4677972c 100644 --- a/tests/services/record/test_marc21_metadata.py +++ b/tests/services/record/test_marc21_metadata.py @@ -182,7 +182,9 @@ def test_load_metadata(): assert len(controlfield) == 1 assert controlfield[0].text == "990079940640203331" - datafield = metadata._etree.xpath(".//datafield[@tag='245' and @ind2='0' and @ind1='0']") + datafield = metadata._etree.xpath( + ".//datafield[@tag='245' and @ind2='0' and @ind1='0']" + ) assert datafield assert len(datafield) == 1 diff --git a/tests/services/test_create_record.py b/tests/services/test_create_record.py index 8000b5f6..2e9e2063 100644 --- a/tests/services/test_create_record.py +++ b/tests/services/test_create_record.py @@ -15,11 +15,6 @@ import arrow import pytest -from invenio_records_marc21.services import ( - Marc21RecordService, - Marc21RecordServiceConfig, -) - def _assert_fields_exists(fields, data): for key in fields: @@ -37,9 +32,8 @@ def marc21(): return {"metadata": {}} -def test_create_with_service(running_app, marc21): +def test_create_with_service(running_app, service, marc21): identity_simple = running_app.identity_simple - service = Marc21RecordService(config=Marc21RecordServiceConfig) draft = service.create(data=marc21, identity=identity_simple, access=None) @@ -70,7 +64,7 @@ def test_create_with_service(running_app, marc21): @pytest.fixture() def empty_data(): """marc21 record.""" - return {"metadata": {"xml": ""}} + return {"metadata": {}} @pytest.mark.parametrize( @@ -137,11 +131,11 @@ def empty_data(): }, ], ) -def test_create_with_access(running_app, empty_data, access): +def test_create_with_access(running_app, service, empty_data, access): identity_simple = running_app.identity_simple - service = Marc21RecordService(config=Marc21RecordServiceConfig) + draft = service.create( - data=empty_data, identity=identity_simple, access=access["input"] + identity=identity_simple, data=empty_data, access=access["input"] ) record = service.publish(id_=draft.id, identity=identity_simple) _assert_fields( diff --git a/tests/services/test_pids_service.py b/tests/services/test_pids_service.py new file mode 100644 index 00000000..8a8e93b8 --- /dev/null +++ b/tests/services/test_pids_service.py @@ -0,0 +1,125 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + +"""PID related tests for Invenio RDM Records. + +This tests both the PIDsService and the RDMService behaviour related to pids. +""" + +import pytest +from invenio_pidstore.errors import PIDDoesNotExistError +from invenio_pidstore.models import PIDStatus + +from invenio_records_marc21.proxies import current_records_marc21 + + +@pytest.fixture() +def mock_public_doi(mocker): + def public_doi(self, *args, **kwargs): + # success + pass + + mocker.patch( + "invenio_rdm_records.services.pids.providers.datacite." + "DataCiteRESTClient.public_doi", + public_doi, + ) + + +@pytest.fixture() +def mock_hide_doi(mocker): + def hide_doi(self, *args, **kwargs): + # success + pass + + mocker.patch( + "invenio_rdm_records.services.pids.providers.datacite." + "DataCiteRESTClient.hide_doi", + hide_doi, + ) + + +# +# Reserve & Discard +# +def test_resolve_pid(running_app, full_metadata): + """Resolve a PID.""" + + service = current_records_marc21.records_service + identity_simple = running_app.identity_simple + # create the draft + draft = service.create(identity=identity_simple, metadata=full_metadata) + # publish the record + record = service.publish(identity=identity_simple, id_=draft.id) + doi = record["pids"]["doi"]["identifier"] + + # test resolution + resolved_record = service.pids.resolve( + identity=identity_simple, id_=doi, scheme="doi" + ) + assert resolved_record.id == record.id + assert resolved_record["pids"]["doi"]["identifier"] == doi + + +def test_resolve_non_existing_pid(running_app, full_metadata): + """Resolve non existing a PID with error.""" + service = current_records_marc21.records_service + identity_simple = running_app.identity_simple + + draft = service.create(identity=identity_simple, metadata=full_metadata) + + service.publish(identity=identity_simple, id_=draft.id) + + fake_doi = "10.4321/client.12345-invalid" + with pytest.raises(PIDDoesNotExistError): + service.pids.resolve(identity=identity_simple, id_=fake_doi, scheme="doi") + + +def test_reserve_pid(running_app, full_metadata): + """Reserve a new PID.""" + service = current_records_marc21.records_service + identity_simple = running_app.identity_simple + + draft = service.create(identity=identity_simple, metadata=full_metadata) + draft = service.pids.create(identity=identity_simple, id_=draft.id, scheme="doi") + + doi = draft["pids"]["doi"]["identifier"] + provider = service.pids.pid_manager._get_provider("doi", "datacite") + pid = provider.get(pid_value=doi) + assert pid.status == PIDStatus.NEW + + +def test_discard_existing_pid(running_app, full_metadata): + """Discard a PID without error.""" + service = current_records_marc21.records_service + identity_simple = running_app.identity_simple + + draft = service.create(identity=identity_simple, metadata=full_metadata) + + draft = service.pids.create(identity=identity_simple, id_=draft.id, scheme="doi") + + doi = draft["pids"]["doi"]["identifier"] + provider = service.pids.pid_manager._get_provider("doi", "datacite") + pid = provider.get(pid_value=doi) + assert pid.status == PIDStatus.NEW + draft = service.pids.discard(identity=identity_simple, id_=draft.id, scheme="doi") + assert not draft["pids"].get("doi") + with pytest.raises(PIDDoesNotExistError): + pid = provider.get(pid_value=doi) + + +def test_discard_non_exisisting_pid(running_app, full_metadata): + """Discard a PID with error.""" + service = current_records_marc21.records_service + identity_simple = running_app.identity_simple + + draft = service.create(identity=identity_simple, metadata=full_metadata) + with pytest.raises(PIDDoesNotExistError): + service.pids.discard(identity=identity_simple, id_=draft.id, scheme="doi") diff --git a/tests/services/test_pids_tasks.py b/tests/services/test_pids_tasks.py new file mode 100644 index 00000000..e96a7b62 --- /dev/null +++ b/tests/services/test_pids_tasks.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Invenio. +# +# Copyright (C) 2021 Graz University of Technology. +# +# Invenio-Records-Marc21 is free software; you can redistribute it and/or +# modify it under the terms of the MIT License; see LICENSE file for more +# details. + +"""PID service tasks tests.""" + +from invenio_pidstore.models import PIDStatus + +from invenio_records_marc21.proxies import current_records_marc21 +from invenio_records_marc21.services.pids.tasks import register_or_update_pid + + +def test_register_pid(running_app, full_metadata, mocker, identity_simple): + """Registers a PID.""" + + def public_doi(self, metadata, url, doi): + """Mock doi deletion.""" + pass + + mocker.patch( + "invenio_rdm_records.services.pids.providers.datacite." + "DataCiteRESTClient.public_doi", + public_doi, + ) + service = current_records_marc21.records_service + draft = service.create(identity=identity_simple, metadata=full_metadata) + draft = service.pids.create(identity=identity_simple, id_=draft.id, scheme="doi") + doi = draft["pids"]["doi"]["identifier"] + provider = service.pids.pid_manager._get_provider("doi", "datacite") + pid = provider.get(pid_value=doi) + record = service.record_cls.publish(draft._record) + record.pids = {pid.pid_type: {"identifier": pid.pid_value, "provider": "datacite"}} + record.metadata = draft["metadata"] + record.register() + record.commit() + assert pid.status == PIDStatus.NEW + pid.reserve() + assert pid.status == PIDStatus.RESERVED + register_or_update_pid(recid=record["id"], scheme="doi") + assert pid.status == PIDStatus.REGISTERED diff --git a/tests/services/test_record_service.py b/tests/services/test_record_service.py index 7a01542c..2b246ab3 100644 --- a/tests/services/test_record_service.py +++ b/tests/services/test_record_service.py @@ -79,13 +79,12 @@ def test_create_empty_draft(running_app): def test_read_draft(running_app, metadata): """Test read a draft can be created.""" - service = running_app.service identity_simple = running_app.identity_simple - draft = service.create(identity_simple, metadata=metadata) + draft = service.create(identity=identity_simple, metadata=metadata) assert draft.id - draft_2 = service.read_draft(draft.id, identity_simple) + draft_2 = service.read_draft(identity=identity_simple, id_=draft.id) assert draft.id == draft_2.id @@ -97,12 +96,12 @@ def test_delete_draft(running_app, metadata): draft = service.create(identity=identity_simple, metadata=metadata) assert draft.id - success = service.delete_draft(draft.id, identity_simple) + success = service.delete_draft(identity=identity_simple, id_=draft.id) assert success # Check draft deleted with pytest.raises(PIDDoesNotExistError): - delete_draft = service.read_draft(draft.id, identity=identity_simple) + delete_draft = service.read_draft(identity=identity_simple, id_=draft.id) def _create_and_publish(service, metadata, identity_simple): @@ -110,7 +109,7 @@ def _create_and_publish(service, metadata, identity_simple): # Cannot create with record service due to the lack of versioning draft = service.create(identity=identity_simple, metadata=metadata) - record = service.publish(draft.id, identity=identity_simple) + record = service.publish(identity=identity_simple, id_=draft.id) assert record.id == draft.id @@ -155,11 +154,11 @@ def test_update_draft(running_app, metadata, metadata2): # Update draft content update_draft = service.update_draft( - draft.id, identity=identity_simple, metadata=metadata2 + identity=identity_simple, id_=draft.id, metadata=metadata2 ) # Check the updates where savedif "json" in data: - read_draft = service.read_draft(id_=draft.id, identity=identity_simple) + read_draft = service.read_draft(identity=identity_simple, id_=draft.id) assert draft.id == update_draft.id _test_metadata( @@ -179,7 +178,7 @@ def test_create_publish_new_version(running_app, metadata): marcid = record.id # Create new version - draft = service.new_version(marcid, identity_simple) + draft = service.new_version(identity=identity_simple, id_=marcid) # files attribute in record causes at create change the revision_id twice assert draft._record.revision_id == 3 @@ -187,7 +186,7 @@ def test_create_publish_new_version(running_app, metadata): assert draft._record.pid.status == PIDStatus.NEW # Publish it - record_2 = service.publish(draft.id, identity_simple) + record_2 = service.publish(identity=identity_simple, id_=draft.id) assert record_2.id assert record_2._record.pid.status == PIDStatus.REGISTERED @@ -203,6 +202,7 @@ def test_create_publish_new_version(running_app, metadata): def test_embargo_lift_without_draft(mock_arrow, running_app, marc21_record): identity_simple = running_app.identity_simple service = current_records_marc21.records_service + # Add embargo to record marc21_record["access"]["files"] = "restricted" marc21_record["access"]["status"] = "embargoed" @@ -211,12 +211,12 @@ def test_embargo_lift_without_draft(mock_arrow, running_app, marc21_record): ) # We need to set the current date in the past to pass the validations mock_arrow.return_value = arrow.get(datetime(1954, 9, 29), tz.gettz("UTC")) - draft = service.create(identity_simple, marc21_record) - record = service.publish(id_=draft.id, identity=identity_simple) + draft = service.create(identity=identity_simple, data=marc21_record) + record = service.publish(identity=identity_simple, id_=draft.id) # Recover current date mock_arrow.return_value = arrow.get(datetime.utcnow()) - service.lift_embargo(_id=record["id"], identity=identity_simple) + service.lift_embargo(identity=identity_simple, _id=draft.id) record_lifted = service.record_cls.pid.resolve(record["id"]) assert not record_lifted.access.embargo.active @@ -237,14 +237,14 @@ def test_embargo_lift_with_draft(mock_arrow, running_app, marc21_record): ) mock_arrow.return_value = arrow.get(datetime(1954, 9, 29), tz.gettz("UTC")) - draft = service.create(identity_simple, marc21_record) - record = service.publish(id_=draft.id, identity=identity_simple) + draft = service.create(identity=identity_simple, data=marc21_record) + record = service.publish(identity=identity_simple, id_=draft.id) # This draft simulates an existing one while lifting the record - ongoing_draft = service.edit(id_=draft.id, identity=identity_simple) + ongoing_draft = service.edit(identity=identity_simple, id_=draft.id) mock_arrow.return_value = arrow.get(datetime.utcnow()) - service.lift_embargo(_id=record["id"], identity=identity_simple) + service.lift_embargo(identity=identity_simple, _id=record["id"]) record_lifted = service.record_cls.pid.resolve(record["id"]) draft_lifted = service.draft_cls.pid.resolve(ongoing_draft["id"]) @@ -267,12 +267,13 @@ def test_embargo_lift_with_updated_draft(mock_arrow, running_app, marc21_record) marc21_record["access"]["embargo"] = dict( active=True, until="2020-06-01", reason=None ) + # We need to set the current date in the past to pass the validations mock_arrow.return_value = arrow.get(datetime(1954, 9, 29), tz.gettz("UTC")) - draft = service.create(identity_simple, marc21_record) - record = service.publish(id_=draft.id, identity=identity_simple) + draft = service.create(identity=identity_simple, data=marc21_record) + record = service.publish(identity=identity_simple, id_=draft.id) # This draft simulates an existing one while lifting the record - service.edit(id_=draft.id, identity=identity_simple) + service.edit(identity=identity_simple, id_=draft.id) # Recover current date mock_arrow.return_value = arrow.get(datetime.utcnow()) @@ -282,18 +283,18 @@ def test_embargo_lift_with_updated_draft(mock_arrow, running_app, marc21_record) marc21_record["access"]["embargo"] = dict(active=False, until=None, reason=None) # Update the ongoing draft with the new data simulating the user's input ongoing_draft = service.update_draft( - id_=draft.id, identity=identity_simple, data=marc21_record + identity=identity_simple, id_=draft.id, data=marc21_record ) service.lift_embargo(_id=record["id"], identity=identity_simple) record_lifted = service.record_cls.pid.resolve(record["id"]) draft_lifted = service.draft_cls.pid.resolve(ongoing_draft["id"]) - assert record_lifted.access.embargo.active is False + assert not record_lifted.access.embargo.active assert record_lifted.access.protection.files == "public" assert record_lifted.access.protection.record == "public" - assert draft_lifted.access.embargo.active is False + assert not draft_lifted.access.embargo.active assert draft_lifted.access.protection.files == "restricted" assert draft_lifted.access.protection.record == "public" @@ -307,9 +308,9 @@ def test_embargo_lift_with_error(running_app, marc21_record): marc21_record["access"]["embargo"] = dict( active=True, until="3220-06-01", reason=None ) - draft = service.create(identity_simple, marc21_record) - record = service.publish(id_=draft.id, identity=identity_simple) + draft = service.create(identity=identity_simple, data=marc21_record) + record = service.publish(identity=identity_simple, id_=draft.id) # Record should not be lifted since it didn't expire (until 3220) with pytest.raises(EmbargoNotLiftedError): - service.lift_embargo(_id=record["id"], identity=identity_simple) + service.lift_embargo(identity=identity_simple, _id=record["id"])