diff --git a/.gitignore b/.gitignore index cc0bc7b15..b84df624e 100644 --- a/.gitignore +++ b/.gitignore @@ -74,6 +74,7 @@ target/ sonar/modules/organisations/jsonschemas/organisations/organisation-v1.0.0.json sonar/modules/documents/jsonschemas/documents/document-v1.0.0.json sonar/modules/deposits/jsonschemas/deposits/deposit-v1.0.0.json +sonar/modules/collections/jsonschemas/collections/collection-v1.0.0.json sonar/resources/projects/jsonschemas/projects/project-v1.0.0.json sonar/dedicated/*/*/jsonschemas/*/*/*-v1.0.0.json diff --git a/MANIFEST.in b/MANIFEST.in index 4bff454e9..d80ddf697 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -16,6 +16,7 @@ exclude sonar/modules/organisations/jsonschemas/organisations/organisation-v1.0. exclude sonar/modules/documents/jsonschemas/documents/document-v1.0.0.json exclude sonar/modules/deposits/jsonschemas/deposits/deposit-v1.0.0.json exclude sonar/modules/projects/jsonschemas/projects/project-v1.0.0.json +exclude sonar/modules/collections/jsonschemas/collections/collection-v1.0.0.json include *.html include *.inv diff --git a/babel.ini b/babel.ini index 55103ac22..ba5da037c 100644 --- a/babel.ini +++ b/babel.ini @@ -36,5 +36,6 @@ extract_messages = $._, jQuery._ [ignore: **/**/document-v1.0.0.json] [ignore: **/**/deposit-v1.0.0.json] [ignore: **/**/project-v1.0.0.json] +[ignore: **/**/collection-v1.0.0.json] [json: **.json] keys_to_translate = ['^title$', '^label$', '^description$', '^placeholder$', '^.*Message$'] diff --git a/scripts/bootstrap b/scripts/bootstrap index 2f5ad709c..57e7e805d 100755 --- a/scripts/bootstrap +++ b/scripts/bootstrap @@ -91,6 +91,7 @@ section "Compile JSON schemas" "info" invenio utils compile-json ./sonar/modules/organisations/jsonschemas/organisations/organisation-v1.0.0_src.json -o ./sonar/modules/organisations/jsonschemas/organisations/organisation-v1.0.0.json invenio utils compile-json ./sonar/modules/documents/jsonschemas/documents/document-v1.0.0_src.json -o ./sonar/modules/documents/jsonschemas/documents/document-v1.0.0.json invenio utils compile-json ./sonar/modules/deposits/jsonschemas/deposits/deposit-v1.0.0_src.json -o ./sonar/modules/deposits/jsonschemas/deposits/deposit-v1.0.0.json +invenio utils compile-json ./sonar/modules/collections/jsonschemas/collections/collection-v1.0.0_src.json -o ./sonar/modules/collections/jsonschemas/collections/collection-v1.0.0.json invenio utils compile-json ./sonar/resources/projects/jsonschemas/projects/project-v1.0.0_src.json -o ./sonar/resources/projects/jsonschemas/projects/project-v1.0.0.json invenio utils compile-json ./sonar/dedicated/hepvs/projects/jsonschemas/hepvs/projects/project-v1.0.0_src.json -o ./sonar/dedicated/hepvs/projects/jsonschemas/hepvs/projects/project-v1.0.0.json diff --git a/setup.py b/setup.py index 11c3c7138..ae6f08257 100644 --- a/setup.py +++ b/setup.py @@ -78,7 +78,8 @@ sonar.modules.shibboleth_authenticator.views.client:blueprint', 'pdf_extractor = \ sonar.modules.pdf_extractor.views.client:blueprint', - 'validation = sonar.modules.validation.views:blueprint' + 'validation = sonar.modules.validation.views:blueprint', + 'collections = sonar.modules.collections.views:blueprint' ], 'invenio_base.api_blueprints': [ 'pdf_extractor = sonar.modules.pdf_extractor.views.api:blueprint', @@ -110,6 +111,7 @@ 'deposits = sonar.modules.deposits.jsonschemas', 'projects = sonar.resources.projects.jsonschemas', 'projects_hepvs = sonar.dedicated.hepvs.projects.jsonschemas', + 'collections = sonar.modules.collections.jsonschemas', 'common = sonar.common.jsonschemas' ], 'invenio_search.mappings': [ @@ -117,7 +119,8 @@ 'organisations = sonar.modules.organisations.mappings', 'users = sonar.modules.users.mappings', 'deposits = sonar.modules.deposits.mappings', - 'projects = sonar.resources.projects.mappings' + 'projects = sonar.resources.projects.mappings', + 'collections = sonar.modules.collections.mappings' ], 'invenio_search.templates': [ 'base-record = sonar.es_templates:list_es_templates' @@ -130,7 +133,9 @@ 'user_id = \ sonar.modules.users.api:user_pid_minter', 'deposit_id = \ - sonar.modules.deposits.api:deposit_pid_minter' + sonar.modules.deposits.api:deposit_pid_minter', + 'collections_id = \ + sonar.modules.collections.api:pid_minter' ], 'invenio_pidstore.fetchers': [ 'document_id = \ @@ -140,13 +145,16 @@ 'user_id = \ sonar.modules.users.api:user_pid_fetcher', 'deposit_id = \ - sonar.modules.deposits.api:deposit_pid_fetcher' + sonar.modules.deposits.api:deposit_pid_fetcher', + 'collections_id = \ + sonar.modules.collections.api:pid_fetcher' ], "invenio_records.jsonresolver": [ "organisation = sonar.modules.organisations.jsonresolvers", "user = sonar.modules.users.jsonresolvers", "document = sonar.modules.documents.jsonresolvers", - "project = sonar.resources.projects.jsonresolvers" + "project = sonar.resources.projects.jsonresolvers", + "collections = sonar.modules.collections.jsonresolvers" ], 'invenio_celery.tasks' : [ 'documents = sonar.modules.documents.tasks' diff --git a/sonar/config.py b/sonar/config.py index b681adbae..5dd503cb7 100644 --- a/sonar/config.py +++ b/sonar/config.py @@ -32,6 +32,8 @@ from invenio_records_rest.facets import range_filter from invenio_stats.processors import EventsIndexer +from sonar.modules.collections.config import \ + Configuration as CollectionConfiguration from sonar.modules.deposits.api import DepositRecord, DepositSearch from sonar.modules.deposits.permissions import DepositPermission from sonar.modules.documents.api import DocumentRecord, DocumentSearch @@ -41,7 +43,8 @@ from sonar.modules.organisations.permissions import OrganisationPermission from sonar.modules.permissions import record_permission_factory, \ wiki_edit_permission -from sonar.modules.query import and_term_filter, missing_field_filter +from sonar.modules.query import and_term_filter, collection_filter, \ + missing_field_filter from sonar.modules.users.api import UserRecord, UserSearch from sonar.modules.users.permissions import UserPermission from sonar.modules.utils import get_current_language @@ -308,7 +311,26 @@ def _(x): 'view_imp': 'sonar.resources.projects.views:detail', 'record_class': 'sonar.resources.projects.api:Record', 'template': 'sonar/projects/detail.html' - } + }, + 'coll': { + 'pid_type': 'coll', + 'route': '//collections/', + 'view_imp': 'sonar.modules.collections.views:detail', + 'record_class': 'sonar.modules.collections.api:Record', + 'template': 'sonar/collections/detail.html' + }, + 'coll_previewer': { + 'pid_type': 'coll', + 'route': '/collections//preview/', + 'view_imp': 'invenio_previewer.views:preview', + 'record_class': 'sonar.modules.collections.api:Record' + }, + 'coll_files': { + 'pid_type': 'coll', + 'route': '/collections//files/', + 'view_imp': 'invenio_records_files.utils:file_download_ui', + 'record_class': 'invenio_records_files.api:Record' + }, } """Records UI for sonar.""" @@ -470,6 +492,9 @@ def _(x): list_permission_factory_imp=lambda record: record_permission_factory( action='list', record=record, cls=DepositPermission)), } + +# Add endpoint for collections +RECORDS_REST_ENDPOINTS['coll'] = CollectionConfiguration.rest_endpoint """REST endpoints.""" DEFAULT_AGGREGATION_SIZE = 50 @@ -478,16 +503,41 @@ def _(x): RECORDS_REST_FACETS = { 'documents': dict(aggs=dict( - sections=dict(terms=dict(field='sections', - size=DEFAULT_AGGREGATION_SIZE)), + sections=dict( + terms=dict(field='sections', size=DEFAULT_AGGREGATION_SIZE)), organisation=dict(terms=dict(field='organisation.pid', size=DEFAULT_AGGREGATION_SIZE)), language=dict( terms=dict(field='language.value', size=DEFAULT_AGGREGATION_SIZE)), subject=dict( terms=dict(field='facet_subjects', size=DEFAULT_AGGREGATION_SIZE)), - specific_collection=dict(terms=dict(field='specificCollections', - size=DEFAULT_AGGREGATION_SIZE)), + collection={ + 'nested': { + 'path': 'collections' + }, + 'aggs': { + 'collection': { + 'terms': { + 'field': 'collections.collection0.keyword' + }, + 'aggs': { + 'collection1': { + 'terms': { + 'field': 'collections.collection1.keyword' + }, + 'aggs': { + 'collection2': { + 'terms': { + 'field': + 'collections.collection2.keyword' + } + } + } + } + } + } + } + }, document_type=dict( terms=dict(field='documentType', size=DEFAULT_AGGREGATION_SIZE)), controlled_affiliation=dict( @@ -515,8 +565,12 @@ def _(x): and_term_filter('language.value'), 'subject': and_term_filter('facet_subjects'), - 'specific_collection': - and_term_filter('specificCollections'), + 'collection': + collection_filter('collections.collection0.keyword'), + 'collection1': + collection_filter('collections.collection1.keyword'), + 'collection2': + collection_filter('collections.collection2.keyword'), 'document_type': and_term_filter('documentType'), 'controlled_affiliation': @@ -601,7 +655,8 @@ def _(x): 'RECORDS_REST_ENDPOINTS': { 'doc': '/files', 'depo': '/files', - 'org': '/files' + 'org': '/files', + 'coll': '/files' } } diff --git a/sonar/ext.py b/sonar/ext.py index f8cbd0ee8..1927d3b6b 100644 --- a/sonar/ext.py +++ b/sonar/ext.py @@ -28,6 +28,7 @@ from invenio_indexer.signals import before_record_index from werkzeug.datastructures import MIMEAccept +from sonar.modules.collections.receivers import enrich_collection_data from sonar.modules.permissions import has_admin_access, has_submitter_access, \ has_superuser_access from sonar.modules.receivers import file_deleted_listener, \ @@ -102,6 +103,9 @@ def init_app(self, app): # Add user's full name before record index before_record_index.connect(add_full_name, weak=False) + # Enrich collection's data + before_record_index.connect(enrich_collection_data, weak=False) + def init_config(self, app): """Initialize configuration.""" for k in dir(config_sonar): diff --git a/sonar/modules/api.py b/sonar/modules/api.py index c45e1a6c8..f495540f5 100644 --- a/sonar/modules/api.py +++ b/sonar/modules/api.py @@ -214,6 +214,8 @@ def get_record_by_bucket(bucket): if not record_class: raise Exception('Class for record not found.') + record_class = obj_or_import_string(record_class) + # Load record by its PID. return record_class.get_record_by_pid(pid.pid_value) except Exception: diff --git a/sonar/modules/collections/__init__.py b/sonar/modules/collections/__init__.py new file mode 100644 index 000000000..f711a845b --- /dev/null +++ b/sonar/modules/collections/__init__.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# +# Swiss Open Access Repository +# Copyright (C) 2021 RERO +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +"""Resource.""" diff --git a/sonar/modules/collections/api.py b/sonar/modules/collections/api.py new file mode 100644 index 000000000..8851ded6b --- /dev/null +++ b/sonar/modules/collections/api.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- +# +# Swiss Open Access Repository +# Copyright (C) 2021 RERO +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +"""Record API.""" + +from functools import partial + +from ..api import SonarIndexer, SonarRecord, SonarSearch +from ..fetchers import id_fetcher +from ..providers import Provider +from .config import Configuration +from .minters import id_minter + +# provider +RecordProvider = type('RecordProvider', (Provider, ), + dict(pid_type=Configuration.pid_type)) +# minter +pid_minter = partial(id_minter, provider=RecordProvider) +# fetcher +pid_fetcher = partial(id_fetcher, provider=RecordProvider) + + +class Record(SonarRecord): + """Record.""" + + minter = pid_minter + fetcher = pid_fetcher + provider = RecordProvider + schema = Configuration.schema + + @classmethod + def create(cls, data, id_=None, dbcommit=False, with_bucket=True, + **kwargs): + """Create record. + + :param dict data: Metadata of the new record + :param str id_: UUID to use if not generated + :param bool dbcommit: Wether to commit into DB during creation + :param bool with_bucket: Wether to create a bucket for record + :return: New record instance + :rtype: Record + """ + return super().create(data, + id_=id_, + dbcommit=dbcommit, + with_bucket=with_bucket, + **kwargs) + + @classmethod + def get_pid_by_hash_key(cls, hash_key): + """Get a record by a hash key. + + :param str hash_key: Hash key to find. + :return: The record found. + :rtype: SonarRecord. + """ + result = RecordSearch().filter( + 'term', hashKey=hash_key).source(includes='pid').scan() + try: + return next(result).pid + except StopIteration: + return None + + +class RecordSearch(SonarSearch): + """Record search.""" + + class Meta: + """Search only on item index.""" + + index = Configuration.index + doc_types = [] + + +class RecordIndexer(SonarIndexer): + """Indexing documents in Elasticsearch.""" + + record_cls = Record diff --git a/sonar/modules/collections/config.py b/sonar/modules/collections/config.py new file mode 100644 index 000000000..36452783b --- /dev/null +++ b/sonar/modules/collections/config.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- +# +# Swiss Open Access Repository +# Copyright (C) 2021 RERO +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +"""Configuration.""" + +from sonar.modules.permissions import record_permission_factory + +# Resource name +RESOURCE_NAME = 'collections' + +# JSON schema name +JSON_SCHEMA_NAME = RESOURCE_NAME[:-1] + +# Module path +MODULE_PATH = f'sonar.modules.{RESOURCE_NAME}' + +# PID type +PID_TYPE = 'coll' + + +class Configuration: + """Resource configuration.""" + + index = f'{RESOURCE_NAME}' + schema = f'{RESOURCE_NAME}/{JSON_SCHEMA_NAME}-v1.0.0.json' + pid_type = PID_TYPE + resolver_url = f'/api/{RESOURCE_NAME}/' + rest_endpoint = { + 'pid_type': + PID_TYPE, + 'pid_minter': + f'{RESOURCE_NAME}_id', + 'pid_fetcher': + f'{RESOURCE_NAME}_id', + 'default_endpoint_prefix': + True, + 'record_class': + f'{MODULE_PATH}.api:Record', + 'search_class': + f'{MODULE_PATH}.api:RecordSearch', + 'indexer_class': + f'{MODULE_PATH}.api:RecordIndexer', + 'search_index': + RESOURCE_NAME, + 'search_type': + None, + 'record_serializers': { + 'application/json': (f'{MODULE_PATH}.serializers' + ':json_v1_response'), + }, + 'search_serializers': { + 'application/json': (f'{MODULE_PATH}.serializers' + ':json_v1_search'), + }, + 'record_loaders': { + 'application/json': (f'{MODULE_PATH}.loaders' + ':json_v1'), + }, + 'list_route': + f'/{RESOURCE_NAME}/', + 'item_route': + f'/{RESOURCE_NAME}/', + 'default_media_type': + 'application/json', + 'max_result_window': + 10000, + 'search_factory_imp': + f'{MODULE_PATH}.query:search_factory', + 'create_permission_factory_imp': + lambda record: record_permission_factory( + action='create', cls=f'{MODULE_PATH}.permissions:RecordPermission' + ), + 'read_permission_factory_imp': + lambda record: record_permission_factory( + action='read', + record=record, + cls=f'{MODULE_PATH}.permissions:RecordPermission'), + 'update_permission_factory_imp': + lambda record: record_permission_factory( + action='update', + record=record, + cls=f'{MODULE_PATH}.permissions:RecordPermission'), + 'delete_permission_factory_imp': + lambda record: record_permission_factory( + action='delete', + record=record, + cls=f'{MODULE_PATH}.permissions:RecordPermission'), + 'list_permission_factory_imp': + lambda record: record_permission_factory( + action='list', + record=record, + cls=f'{MODULE_PATH}.permissions:RecordPermission') + } diff --git a/sonar/modules/collections/jsonresolvers.py b/sonar/modules/collections/jsonresolvers.py new file mode 100644 index 000000000..2163565c8 --- /dev/null +++ b/sonar/modules/collections/jsonresolvers.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +# +# Swiss Open Access Repository +# Copyright (C) 2021 RERO +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +"""JSON resolvers.""" + +import jsonresolver +from invenio_pidstore.resolver import Resolver +from invenio_records.api import Record + +from ...config import JSONSCHEMAS_HOST +from .config import Configuration + + +@jsonresolver.route(Configuration.resolver_url, host=JSONSCHEMAS_HOST) +def json_resolver(pid): + """Resolve record. + + :param str pid: PID value. + :return: Record instance. + :rtype: Record + """ + resolver = Resolver(pid_type=Configuration.pid_type, + object_type="rec", + getter=Record.get_record) + _, record = resolver.resolve(pid) + + if record.get('$schema'): + del record['$schema'] + + return record diff --git a/sonar/modules/collections/jsonschemas/__init__.py b/sonar/modules/collections/jsonschemas/__init__.py new file mode 100644 index 000000000..6430fc89e --- /dev/null +++ b/sonar/modules/collections/jsonschemas/__init__.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# +# Swiss Open Access Repository +# Copyright (C) 2021 RERO +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +"""JSON schemas.""" diff --git a/sonar/modules/collections/jsonschemas/collections/collection-v1.0.0.json b/sonar/modules/collections/jsonschemas/collections/collection-v1.0.0.json deleted file mode 100644 index 366958d88..000000000 --- a/sonar/modules/collections/jsonschemas/collections/collection-v1.0.0.json +++ /dev/null @@ -1,5060 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - "id": "https://sonar.ch/schemas/collections/collection-v1.0.0.json", - "title": "Collections", - "type": "object", - "additionalProperties": false, - "properties": { - "$schema": { - "type": "string", - "default": "https://sonar.ch/schemas/collections/collection-v1.0.0.json" - }, - "pid": { - "title": "Identifier", - "type": "string", - "minLength": 1 - }, - "hashKey": { - "title": "Hash key", - "type": "string", - "minLength": 1 - }, - "name": { - "title": "Names", - "type": "array", - "minItems": 1, - "items": { - "title": "Name", - "type": "object", - "additionalProperties": false, - "properties": { - "value": { - "title": "Value", - "type": "string", - "minLength": 1 - }, - "language": { - "title": "Language", - "type": "string", - "enum": [ - "aar", - "abk", - "ace", - "ach", - "ada", - "ady", - "afa", - "afh", - "afr", - "ain", - "aka", - "akk", - "alb", - "ale", - "alg", - "alt", - "amh", - "ang", - "anp", - "apa", - "ara", - "arc", - "arg", - "arm", - "arn", - "arp", - "art", - "arw", - "asm", - "ast", - "ath", - "aus", - "ava", - "ave", - "awa", - "aym", - "aze", - "bad", - "bai", - "bak", - "bal", - "bam", - "ban", - "baq", - "bas", - "bat", - "bej", - "bel", - "bem", - "ben", - "ber", - "bho", - "bih", - "bik", - "bin", - "bis", - "bla", - "bnt", - "bos", - "bra", - "bre", - "btk", - "bua", - "bug", - "bul", - "bur", - "byn", - "cad", - "cai", - "car", - "cat", - "cau", - "ceb", - "cel", - "cha", - "chb", - "che", - "chg", - "chi", - "chk", - "chm", - "chn", - "cho", - "chp", - "chr", - "chu", - "chv", - "chy", - "cmc", - "cnr", - "cop", - "cor", - "cos", - "cpe", - "cpf", - "cpp", - "cre", - "crh", - "crp", - "csb", - "cus", - "cze", - "dak", - "dan", - "dar", - "day", - "del", - "den", - "dgr", - "din", - "div", - "doi", - "dra", - "dsb", - "dua", - "dum", - "dut", - "dyu", - "dzo", - "efi", - "egy", - "eka", - "elx", - "eng", - "enm", - "epo", - "est", - "ewe", - "ewo", - "fan", - "fao", - "fat", - "fij", - "fil", - "fin", - "fiu", - "fon", - "fre", - "frm", - "fro", - "frr", - "frs", - "fry", - "ful", - "fur", - "gaa", - "gay", - "gba", - "gem", - "geo", - "ger", - "gez", - "gil", - "gla", - "gle", - "glg", - "glv", - "gmh", - "goh", - "gon", - "gor", - "got", - "grb", - "grc", - "gre", - "grn", - "gsw", - "guj", - "gwi", - "hai", - "hat", - "hau", - "haw", - "heb", - "her", - "hil", - "him", - "hin", - "hit", - "hmn", - "hmo", - "hrv", - "hsb", - "hun", - "hup", - "iba", - "ibo", - "ice", - "ido", - "iii", - "ijo", - "iku", - "ile", - "ilo", - "ina", - "inc", - "ind", - "ine", - "inh", - "ipk", - "ira", - "iro", - "ita", - "jav", - "jbo", - "jpn", - "jpr", - "jrb", - "kaa", - "kab", - "kac", - "kal", - "kam", - "kan", - "kar", - "kas", - "kau", - "kaw", - "kaz", - "kbd", - "kha", - "khi", - "khm", - "kho", - "kik", - "kin", - "kir", - "kmb", - "kok", - "kom", - "kon", - "kor", - "kos", - "kpe", - "krc", - "krl", - "kro", - "kru", - "kua", - "kum", - "kur", - "kut", - "lad", - "lah", - "lam", - "lao", - "lat", - "lav", - "lez", - "lim", - "lin", - "lit", - "lol", - "loz", - "ltz", - "lua", - "lub", - "lug", - "lui", - "lun", - "luo", - "lus", - "mac", - "mad", - "mag", - "mah", - "mai", - "mak", - "mal", - "man", - "mao", - "map", - "mar", - "mas", - "may", - "mdf", - "mdr", - "men", - "mga", - "mic", - "min", - "mis", - "mkh", - "mlg", - "mlt", - "mnc", - "mni", - "mno", - "moh", - "mon", - "mos", - "mul", - "mun", - "mus", - "mwl", - "mwr", - "myn", - "myv", - "nah", - "nai", - "nap", - "nau", - "nav", - "nbl", - "nde", - "ndo", - "nds", - "nep", - "new", - "nia", - "nic", - "niu", - "nno", - "nob", - "nog", - "non", - "nor", - "nqo", - "nso", - "nub", - "nwc", - "nya", - "nym", - "nyn", - "nyo", - "nzi", - "oci", - "oji", - "ori", - "orm", - "osa", - "oss", - "ota", - "oto", - "paa", - "pag", - "pal", - "pam", - "pan", - "pap", - "pau", - "peo", - "per", - "phi", - "phn", - "pli", - "pol", - "pon", - "por", - "pra", - "pro", - "pus", - "que", - "raj", - "rap", - "rar", - "roa", - "roh", - "rom", - "rum", - "run", - "rup", - "rus", - "sad", - "sag", - "sah", - "sai", - "sal", - "sam", - "san", - "sas", - "sat", - "scn", - "sco", - "sel", - "sem", - "sga", - "sgn", - "shn", - "sid", - "sin", - "sio", - "sit", - "sla", - "slo", - "slv", - "sma", - "sme", - "smi", - "smj", - "smn", - "smo", - "sms", - "sna", - "snd", - "snk", - "sog", - "som", - "son", - "sot", - "spa", - "srd", - "srn", - "srp", - "srr", - "ssa", - "ssw", - "suk", - "sun", - "sus", - "sux", - "swa", - "swe", - "syc", - "syr", - "tah", - "tai", - "tam", - "tat", - "tel", - "tem", - "ter", - "tet", - "tgk", - "tgl", - "tha", - "tib", - "tig", - "tir", - "tiv", - "tkl", - "tlh", - "tli", - "tmh", - "tog", - "ton", - "tpi", - "tsi", - "tsn", - "tso", - "tuk", - "tum", - "tup", - "tur", - "tut", - "tvl", - "twi", - "tyv", - "udm", - "uga", - "uig", - "ukr", - "umb", - "und", - "urd", - "uzb", - "vai", - "ven", - "vie", - "vol", - "vot", - "wak", - "wal", - "war", - "was", - "wel", - "wen", - "wln", - "wol", - "xal", - "xho", - "yao", - "yap", - "yid", - "yor", - "ypk", - "zap", - "zbl", - "zen", - "zha", - "znd", - "zul", - "zun", - "zxx", - "zza" - ], - "form": { - "templateOptions": { - "sort": true, - "wrappers": [ - "card" - ] - }, - "options": [ - { - "label": "lang_aar", - "value": "aar" - }, - { - "label": "lang_abk", - "value": "abk" - }, - { - "label": "lang_ace", - "value": "ace" - }, - { - "label": "lang_ach", - "value": "ach" - }, - { - "label": "lang_ada", - "value": "ada" - }, - { - "label": "lang_ady", - "value": "ady" - }, - { - "label": "lang_afa", - "value": "afa" - }, - { - "label": "lang_afh", - "value": "afh" - }, - { - "label": "lang_afr", - "value": "afr" - }, - { - "label": "lang_ain", - "value": "ain" - }, - { - "label": "lang_aka", - "value": "aka" - }, - { - "label": "lang_akk", - "value": "akk" - }, - { - "label": "lang_alb", - "value": "alb" - }, - { - "label": "lang_ale", - "value": "ale" - }, - { - "label": "lang_alg", - "value": "alg" - }, - { - "label": "lang_alt", - "value": "alt" - }, - { - "label": "lang_amh", - "value": "amh" - }, - { - "label": "lang_ang", - "value": "ang" - }, - { - "label": "lang_anp", - "value": "anp" - }, - { - "label": "lang_apa", - "value": "apa" - }, - { - "label": "lang_ara", - "value": "ara" - }, - { - "label": "lang_arc", - "value": "arc" - }, - { - "label": "lang_arg", - "value": "arg" - }, - { - "label": "lang_arm", - "value": "arm" - }, - { - "label": "lang_arn", - "value": "arn" - }, - { - "label": "lang_arp", - "value": "arp" - }, - { - "label": "lang_art", - "value": "art" - }, - { - "label": "lang_arw", - "value": "arw" - }, - { - "label": "lang_asm", - "value": "asm" - }, - { - "label": "lang_ast", - "value": "ast" - }, - { - "label": "lang_ath", - "value": "ath" - }, - { - "label": "lang_aus", - "value": "aus" - }, - { - "label": "lang_ava", - "value": "ava" - }, - { - "label": "lang_ave", - "value": "ave" - }, - { - "label": "lang_awa", - "value": "awa" - }, - { - "label": "lang_aym", - "value": "aym" - }, - { - "label": "lang_aze", - "value": "aze" - }, - { - "label": "lang_bad", - "value": "bad" - }, - { - "label": "lang_bai", - "value": "bai" - }, - { - "label": "lang_bak", - "value": "bak" - }, - { - "label": "lang_bal", - "value": "bal" - }, - { - "label": "lang_bam", - "value": "bam" - }, - { - "label": "lang_ban", - "value": "ban" - }, - { - "label": "lang_baq", - "value": "baq" - }, - { - "label": "lang_bas", - "value": "bas" - }, - { - "label": "lang_bat", - "value": "bat" - }, - { - "label": "lang_bej", - "value": "bej" - }, - { - "label": "lang_bel", - "value": "bel" - }, - { - "label": "lang_bem", - "value": "bem" - }, - { - "label": "lang_ben", - "value": "ben" - }, - { - "label": "lang_ber", - "value": "ber" - }, - { - "label": "lang_bho", - "value": "bho" - }, - { - "label": "lang_bih", - "value": "bih" - }, - { - "label": "lang_bik", - "value": "bik" - }, - { - "label": "lang_bin", - "value": "bin" - }, - { - "label": "lang_bis", - "value": "bis" - }, - { - "label": "lang_bla", - "value": "bla" - }, - { - "label": "lang_bnt", - "value": "bnt" - }, - { - "label": "lang_bos", - "value": "bos" - }, - { - "label": "lang_bra", - "value": "bra" - }, - { - "label": "lang_bre", - "value": "bre" - }, - { - "label": "lang_btk", - "value": "btk" - }, - { - "label": "lang_bua", - "value": "bua" - }, - { - "label": "lang_bug", - "value": "bug" - }, - { - "label": "lang_bul", - "value": "bul" - }, - { - "label": "lang_bur", - "value": "bur" - }, - { - "label": "lang_byn", - "value": "byn" - }, - { - "label": "lang_cad", - "value": "cad" - }, - { - "label": "lang_cai", - "value": "cai" - }, - { - "label": "lang_car", - "value": "car" - }, - { - "label": "lang_cat", - "value": "cat" - }, - { - "label": "lang_cau", - "value": "cau" - }, - { - "label": "lang_ceb", - "value": "ceb" - }, - { - "label": "lang_cel", - "value": "cel" - }, - { - "label": "lang_cha", - "value": "cha" - }, - { - "label": "lang_chb", - "value": "chb" - }, - { - "label": "lang_che", - "value": "che" - }, - { - "label": "lang_chg", - "value": "chg" - }, - { - "label": "lang_chi", - "value": "chi" - }, - { - "label": "lang_chk", - "value": "chk" - }, - { - "label": "lang_chm", - "value": "chm" - }, - { - "label": "lang_chn", - "value": "chn" - }, - { - "label": "lang_cho", - "value": "cho" - }, - { - "label": "lang_chp", - "value": "chp" - }, - { - "label": "lang_chr", - "value": "chr" - }, - { - "label": "lang_chu", - "value": "chu" - }, - { - "label": "lang_chv", - "value": "chv" - }, - { - "label": "lang_chy", - "value": "chy" - }, - { - "label": "lang_cmc", - "value": "cmc" - }, - { - "label": "lang_cnr", - "value": "cnr" - }, - { - "label": "lang_cop", - "value": "cop" - }, - { - "label": "lang_cor", - "value": "cor" - }, - { - "label": "lang_cos", - "value": "cos" - }, - { - "label": "lang_cpe", - "value": "cpe" - }, - { - "label": "lang_cpf", - "value": "cpf" - }, - { - "label": "lang_cpp", - "value": "cpp" - }, - { - "label": "lang_cre", - "value": "cre" - }, - { - "label": "lang_crh", - "value": "crh" - }, - { - "label": "lang_crp", - "value": "crp" - }, - { - "label": "lang_csb", - "value": "csb" - }, - { - "label": "lang_cus", - "value": "cus" - }, - { - "label": "lang_cze", - "value": "cze" - }, - { - "label": "lang_dak", - "value": "dak" - }, - { - "label": "lang_dan", - "value": "dan" - }, - { - "label": "lang_dar", - "value": "dar" - }, - { - "label": "lang_day", - "value": "day" - }, - { - "label": "lang_del", - "value": "del" - }, - { - "label": "lang_den", - "value": "den" - }, - { - "label": "lang_dgr", - "value": "dgr" - }, - { - "label": "lang_din", - "value": "din" - }, - { - "label": "lang_div", - "value": "div" - }, - { - "label": "lang_doi", - "value": "doi" - }, - { - "label": "lang_dra", - "value": "dra" - }, - { - "label": "lang_dsb", - "value": "dsb" - }, - { - "label": "lang_dua", - "value": "dua" - }, - { - "label": "lang_dum", - "value": "dum" - }, - { - "label": "lang_dut", - "value": "dut" - }, - { - "label": "lang_dyu", - "value": "dyu" - }, - { - "label": "lang_dzo", - "value": "dzo" - }, - { - "label": "lang_efi", - "value": "efi" - }, - { - "label": "lang_egy", - "value": "egy" - }, - { - "label": "lang_eka", - "value": "eka" - }, - { - "label": "lang_elx", - "value": "elx" - }, - { - "label": "lang_eng", - "value": "eng", - "preferred": true - }, - { - "label": "lang_enm", - "value": "enm" - }, - { - "label": "lang_epo", - "value": "epo" - }, - { - "label": "lang_est", - "value": "est" - }, - { - "label": "lang_ewe", - "value": "ewe" - }, - { - "label": "lang_ewo", - "value": "ewo" - }, - { - "label": "lang_fan", - "value": "fan" - }, - { - "label": "lang_fao", - "value": "fao" - }, - { - "label": "lang_fat", - "value": "fat" - }, - { - "label": "lang_fij", - "value": "fij" - }, - { - "label": "lang_fil", - "value": "fil" - }, - { - "label": "lang_fin", - "value": "fin" - }, - { - "label": "lang_fiu", - "value": "fiu" - }, - { - "label": "lang_fon", - "value": "fon" - }, - { - "label": "lang_fre", - "value": "fre", - "preferred": true - }, - { - "label": "lang_frm", - "value": "frm" - }, - { - "label": "lang_fro", - "value": "fro" - }, - { - "label": "lang_frr", - "value": "frr" - }, - { - "label": "lang_frs", - "value": "frs" - }, - { - "label": "lang_fry", - "value": "fry" - }, - { - "label": "lang_ful", - "value": "ful" - }, - { - "label": "lang_fur", - "value": "fur" - }, - { - "label": "lang_gaa", - "value": "gaa" - }, - { - "label": "lang_gay", - "value": "gay" - }, - { - "label": "lang_gba", - "value": "gba" - }, - { - "label": "lang_gem", - "value": "gem" - }, - { - "label": "lang_geo", - "value": "geo" - }, - { - "label": "lang_ger", - "value": "ger", - "preferred": true - }, - { - "label": "lang_gez", - "value": "gez" - }, - { - "label": "lang_gil", - "value": "gil" - }, - { - "label": "lang_gla", - "value": "gla" - }, - { - "label": "lang_gle", - "value": "gle" - }, - { - "label": "lang_glg", - "value": "glg" - }, - { - "label": "lang_glv", - "value": "glv" - }, - { - "label": "lang_gmh", - "value": "gmh" - }, - { - "label": "lang_goh", - "value": "goh" - }, - { - "label": "lang_gon", - "value": "gon" - }, - { - "label": "lang_gor", - "value": "gor" - }, - { - "label": "lang_got", - "value": "got" - }, - { - "label": "lang_grb", - "value": "grb" - }, - { - "label": "lang_grc", - "value": "grc" - }, - { - "label": "lang_gre", - "value": "gre" - }, - { - "label": "lang_grn", - "value": "grn" - }, - { - "label": "lang_gsw", - "value": "gsw" - }, - { - "label": "lang_guj", - "value": "guj" - }, - { - "label": "lang_gwi", - "value": "gwi" - }, - { - "label": "lang_hai", - "value": "hai" - }, - { - "label": "lang_hat", - "value": "hat" - }, - { - "label": "lang_hau", - "value": "hau" - }, - { - "label": "lang_haw", - "value": "haw" - }, - { - "label": "lang_heb", - "value": "heb" - }, - { - "label": "lang_her", - "value": "her" - }, - { - "label": "lang_hil", - "value": "hil" - }, - { - "label": "lang_him", - "value": "him" - }, - { - "label": "lang_hin", - "value": "hin" - }, - { - "label": "lang_hit", - "value": "hit" - }, - { - "label": "lang_hmn", - "value": "hmn" - }, - { - "label": "lang_hmo", - "value": "hmo" - }, - { - "label": "lang_hrv", - "value": "hrv" - }, - { - "label": "lang_hsb", - "value": "hsb" - }, - { - "label": "lang_hun", - "value": "hun" - }, - { - "label": "lang_hup", - "value": "hup" - }, - { - "label": "lang_iba", - "value": "iba" - }, - { - "label": "lang_ibo", - "value": "ibo" - }, - { - "label": "lang_ice", - "value": "ice" - }, - { - "label": "lang_ido", - "value": "ido" - }, - { - "label": "lang_iii", - "value": "iii" - }, - { - "label": "lang_ijo", - "value": "ijo" - }, - { - "label": "lang_iku", - "value": "iku" - }, - { - "label": "lang_ile", - "value": "ile" - }, - { - "label": "lang_ilo", - "value": "ilo" - }, - { - "label": "lang_ina", - "value": "ina" - }, - { - "label": "lang_inc", - "value": "inc" - }, - { - "label": "lang_ind", - "value": "ind" - }, - { - "label": "lang_ine", - "value": "ine" - }, - { - "label": "lang_inh", - "value": "inh" - }, - { - "label": "lang_ipk", - "value": "ipk" - }, - { - "label": "lang_ira", - "value": "ira" - }, - { - "label": "lang_iro", - "value": "iro" - }, - { - "label": "lang_ita", - "value": "ita", - "preferred": true - }, - { - "label": "lang_jav", - "value": "jav" - }, - { - "label": "lang_jbo", - "value": "jbo" - }, - { - "label": "lang_jpn", - "value": "jpn" - }, - { - "label": "lang_jpr", - "value": "jpr" - }, - { - "label": "lang_jrb", - "value": "jrb" - }, - { - "label": "lang_kaa", - "value": "kaa" - }, - { - "label": "lang_kab", - "value": "kab" - }, - { - "label": "lang_kac", - "value": "kac" - }, - { - "label": "lang_kal", - "value": "kal" - }, - { - "label": "lang_kam", - "value": "kam" - }, - { - "label": "lang_kan", - "value": "kan" - }, - { - "label": "lang_kar", - "value": "kar" - }, - { - "label": "lang_kas", - "value": "kas" - }, - { - "label": "lang_kau", - "value": "kau" - }, - { - "label": "lang_kaw", - "value": "kaw" - }, - { - "label": "lang_kaz", - "value": "kaz" - }, - { - "label": "lang_kbd", - "value": "kbd" - }, - { - "label": "lang_kha", - "value": "kha" - }, - { - "label": "lang_khi", - "value": "khi" - }, - { - "label": "lang_khm", - "value": "khm" - }, - { - "label": "lang_kho", - "value": "kho" - }, - { - "label": "lang_kik", - "value": "kik" - }, - { - "label": "lang_kin", - "value": "kin" - }, - { - "label": "lang_kir", - "value": "kir" - }, - { - "label": "lang_kmb", - "value": "kmb" - }, - { - "label": "lang_kok", - "value": "kok" - }, - { - "label": "lang_kom", - "value": "kom" - }, - { - "label": "lang_kon", - "value": "kon" - }, - { - "label": "lang_kor", - "value": "kor" - }, - { - "label": "lang_kos", - "value": "kos" - }, - { - "label": "lang_kpe", - "value": "kpe" - }, - { - "label": "lang_krc", - "value": "krc" - }, - { - "label": "lang_krl", - "value": "krl" - }, - { - "label": "lang_kro", - "value": "kro" - }, - { - "label": "lang_kru", - "value": "kru" - }, - { - "label": "lang_kua", - "value": "kua" - }, - { - "label": "lang_kum", - "value": "kum" - }, - { - "label": "lang_kur", - "value": "kur" - }, - { - "label": "lang_kut", - "value": "kut" - }, - { - "label": "lang_lad", - "value": "lad" - }, - { - "label": "lang_lah", - "value": "lah" - }, - { - "label": "lang_lam", - "value": "lam" - }, - { - "label": "lang_lao", - "value": "lao" - }, - { - "label": "lang_lat", - "value": "lat" - }, - { - "label": "lang_lav", - "value": "lav" - }, - { - "label": "lang_lez", - "value": "lez" - }, - { - "label": "lang_lim", - "value": "lim" - }, - { - "label": "lang_lin", - "value": "lin" - }, - { - "label": "lang_lit", - "value": "lit" - }, - { - "label": "lang_lol", - "value": "lol" - }, - { - "label": "lang_loz", - "value": "loz" - }, - { - "label": "lang_ltz", - "value": "ltz" - }, - { - "label": "lang_lua", - "value": "lua" - }, - { - "label": "lang_lub", - "value": "lub" - }, - { - "label": "lang_lug", - "value": "lug" - }, - { - "label": "lang_lui", - "value": "lui" - }, - { - "label": "lang_lun", - "value": "lun" - }, - { - "label": "lang_luo", - "value": "luo" - }, - { - "label": "lang_lus", - "value": "lus" - }, - { - "label": "lang_mac", - "value": "mac" - }, - { - "label": "lang_mad", - "value": "mad" - }, - { - "label": "lang_mag", - "value": "mag" - }, - { - "label": "lang_mah", - "value": "mah" - }, - { - "label": "lang_mai", - "value": "mai" - }, - { - "label": "lang_mak", - "value": "mak" - }, - { - "label": "lang_mal", - "value": "mal" - }, - { - "label": "lang_man", - "value": "man" - }, - { - "label": "lang_mao", - "value": "mao" - }, - { - "label": "lang_map", - "value": "map" - }, - { - "label": "lang_mar", - "value": "mar" - }, - { - "label": "lang_mas", - "value": "mas" - }, - { - "label": "lang_may", - "value": "may" - }, - { - "label": "lang_mdf", - "value": "mdf" - }, - { - "label": "lang_mdr", - "value": "mdr" - }, - { - "label": "lang_men", - "value": "men" - }, - { - "label": "lang_mga", - "value": "mga" - }, - { - "label": "lang_mic", - "value": "mic" - }, - { - "label": "lang_min", - "value": "min" - }, - { - "label": "lang_mis", - "value": "mis" - }, - { - "label": "lang_mkh", - "value": "mkh" - }, - { - "label": "lang_mlg", - "value": "mlg" - }, - { - "label": "lang_mlt", - "value": "mlt" - }, - { - "label": "lang_mnc", - "value": "mnc" - }, - { - "label": "lang_mni", - "value": "mni" - }, - { - "label": "lang_mno", - "value": "mno" - }, - { - "label": "lang_moh", - "value": "moh" - }, - { - "label": "lang_mon", - "value": "mon" - }, - { - "label": "lang_mos", - "value": "mos" - }, - { - "label": "lang_mul", - "value": "mul" - }, - { - "label": "lang_mun", - "value": "mun" - }, - { - "label": "lang_mus", - "value": "mus" - }, - { - "label": "lang_mwl", - "value": "mwl" - }, - { - "label": "lang_mwr", - "value": "mwr" - }, - { - "label": "lang_myn", - "value": "myn" - }, - { - "label": "lang_myv", - "value": "myv" - }, - { - "label": "lang_nah", - "value": "nah" - }, - { - "label": "lang_nai", - "value": "nai" - }, - { - "label": "lang_nap", - "value": "nap" - }, - { - "label": "lang_nau", - "value": "nau" - }, - { - "label": "lang_nav", - "value": "nav" - }, - { - "label": "lang_nbl", - "value": "nbl" - }, - { - "label": "lang_nde", - "value": "nde" - }, - { - "label": "lang_ndo", - "value": "ndo" - }, - { - "label": "lang_nds", - "value": "nds" - }, - { - "label": "lang_nep", - "value": "nep" - }, - { - "label": "lang_new", - "value": "new" - }, - { - "label": "lang_nia", - "value": "nia" - }, - { - "label": "lang_nic", - "value": "nic" - }, - { - "label": "lang_niu", - "value": "niu" - }, - { - "label": "lang_nno", - "value": "nno" - }, - { - "label": "lang_nob", - "value": "nob" - }, - { - "label": "lang_nog", - "value": "nog" - }, - { - "label": "lang_non", - "value": "non" - }, - { - "label": "lang_nor", - "value": "nor" - }, - { - "label": "lang_nqo", - "value": "nqo" - }, - { - "label": "lang_nso", - "value": "nso" - }, - { - "label": "lang_nub", - "value": "nub" - }, - { - "label": "lang_nwc", - "value": "nwc" - }, - { - "label": "lang_nya", - "value": "nya" - }, - { - "label": "lang_nym", - "value": "nym" - }, - { - "label": "lang_nyn", - "value": "nyn" - }, - { - "label": "lang_nyo", - "value": "nyo" - }, - { - "label": "lang_nzi", - "value": "nzi" - }, - { - "label": "lang_oci", - "value": "oci" - }, - { - "label": "lang_oji", - "value": "oji" - }, - { - "label": "lang_ori", - "value": "ori" - }, - { - "label": "lang_orm", - "value": "orm" - }, - { - "label": "lang_osa", - "value": "osa" - }, - { - "label": "lang_oss", - "value": "oss" - }, - { - "label": "lang_ota", - "value": "ota" - }, - { - "label": "lang_oto", - "value": "oto" - }, - { - "label": "lang_paa", - "value": "paa" - }, - { - "label": "lang_pag", - "value": "pag" - }, - { - "label": "lang_pal", - "value": "pal" - }, - { - "label": "lang_pam", - "value": "pam" - }, - { - "label": "lang_pan", - "value": "pan" - }, - { - "label": "lang_pap", - "value": "pap" - }, - { - "label": "lang_pau", - "value": "pau" - }, - { - "label": "lang_peo", - "value": "peo" - }, - { - "label": "lang_per", - "value": "per" - }, - { - "label": "lang_phi", - "value": "phi" - }, - { - "label": "lang_phn", - "value": "phn" - }, - { - "label": "lang_pli", - "value": "pli" - }, - { - "label": "lang_pol", - "value": "pol" - }, - { - "label": "lang_pon", - "value": "pon" - }, - { - "label": "lang_por", - "value": "por" - }, - { - "label": "lang_pra", - "value": "pra" - }, - { - "label": "lang_pro", - "value": "pro" - }, - { - "label": "lang_pus", - "value": "pus" - }, - { - "label": "lang_que", - "value": "que" - }, - { - "label": "lang_raj", - "value": "raj" - }, - { - "label": "lang_rap", - "value": "rap" - }, - { - "label": "lang_rar", - "value": "rar" - }, - { - "label": "lang_roa", - "value": "roa" - }, - { - "label": "lang_roh", - "value": "roh" - }, - { - "label": "lang_rom", - "value": "rom" - }, - { - "label": "lang_rum", - "value": "rum" - }, - { - "label": "lang_run", - "value": "run" - }, - { - "label": "lang_rup", - "value": "rup" - }, - { - "label": "lang_rus", - "value": "rus" - }, - { - "label": "lang_sad", - "value": "sad" - }, - { - "label": "lang_sag", - "value": "sag" - }, - { - "label": "lang_sah", - "value": "sah" - }, - { - "label": "lang_sai", - "value": "sai" - }, - { - "label": "lang_sal", - "value": "sal" - }, - { - "label": "lang_sam", - "value": "sam" - }, - { - "label": "lang_san", - "value": "san" - }, - { - "label": "lang_sas", - "value": "sas" - }, - { - "label": "lang_sat", - "value": "sat" - }, - { - "label": "lang_scn", - "value": "scn" - }, - { - "label": "lang_sco", - "value": "sco" - }, - { - "label": "lang_sel", - "value": "sel" - }, - { - "label": "lang_sem", - "value": "sem" - }, - { - "label": "lang_sga", - "value": "sga" - }, - { - "label": "lang_sgn", - "value": "sgn" - }, - { - "label": "lang_shn", - "value": "shn" - }, - { - "label": "lang_sid", - "value": "sid" - }, - { - "label": "lang_sin", - "value": "sin" - }, - { - "label": "lang_sio", - "value": "sio" - }, - { - "label": "lang_sit", - "value": "sit" - }, - { - "label": "lang_sla", - "value": "sla" - }, - { - "label": "lang_slo", - "value": "slo" - }, - { - "label": "lang_slv", - "value": "slv" - }, - { - "label": "lang_sma", - "value": "sma" - }, - { - "label": "lang_sme", - "value": "sme" - }, - { - "label": "lang_smi", - "value": "smi" - }, - { - "label": "lang_smj", - "value": "smj" - }, - { - "label": "lang_smn", - "value": "smn" - }, - { - "label": "lang_smo", - "value": "smo" - }, - { - "label": "lang_sms", - "value": "sms" - }, - { - "label": "lang_sna", - "value": "sna" - }, - { - "label": "lang_snd", - "value": "snd" - }, - { - "label": "lang_snk", - "value": "snk" - }, - { - "label": "lang_sog", - "value": "sog" - }, - { - "label": "lang_som", - "value": "som" - }, - { - "label": "lang_son", - "value": "son" - }, - { - "label": "lang_sot", - "value": "sot" - }, - { - "label": "lang_spa", - "value": "spa" - }, - { - "label": "lang_srd", - "value": "srd" - }, - { - "label": "lang_srn", - "value": "srn" - }, - { - "label": "lang_srp", - "value": "srp" - }, - { - "label": "lang_srr", - "value": "srr" - }, - { - "label": "lang_ssa", - "value": "ssa" - }, - { - "label": "lang_ssw", - "value": "ssw" - }, - { - "label": "lang_suk", - "value": "suk" - }, - { - "label": "lang_sun", - "value": "sun" - }, - { - "label": "lang_sus", - "value": "sus" - }, - { - "label": "lang_sux", - "value": "sux" - }, - { - "label": "lang_swa", - "value": "swa" - }, - { - "label": "lang_swe", - "value": "swe" - }, - { - "label": "lang_syc", - "value": "syc" - }, - { - "label": "lang_syr", - "value": "syr" - }, - { - "label": "lang_tah", - "value": "tah" - }, - { - "label": "lang_tai", - "value": "tai" - }, - { - "label": "lang_tam", - "value": "tam" - }, - { - "label": "lang_tat", - "value": "tat" - }, - { - "label": "lang_tel", - "value": "tel" - }, - { - "label": "lang_tem", - "value": "tem" - }, - { - "label": "lang_ter", - "value": "ter" - }, - { - "label": "lang_tet", - "value": "tet" - }, - { - "label": "lang_tgk", - "value": "tgk" - }, - { - "label": "lang_tgl", - "value": "tgl" - }, - { - "label": "lang_tha", - "value": "tha" - }, - { - "label": "lang_tib", - "value": "tib" - }, - { - "label": "lang_tig", - "value": "tig" - }, - { - "label": "lang_tir", - "value": "tir" - }, - { - "label": "lang_tiv", - "value": "tiv" - }, - { - "label": "lang_tkl", - "value": "tkl" - }, - { - "label": "lang_tlh", - "value": "tlh" - }, - { - "label": "lang_tli", - "value": "tli" - }, - { - "label": "lang_tmh", - "value": "tmh" - }, - { - "label": "lang_tog", - "value": "tog" - }, - { - "label": "lang_ton", - "value": "ton" - }, - { - "label": "lang_tpi", - "value": "tpi" - }, - { - "label": "lang_tsi", - "value": "tsi" - }, - { - "label": "lang_tsn", - "value": "tsn" - }, - { - "label": "lang_tso", - "value": "tso" - }, - { - "label": "lang_tuk", - "value": "tuk" - }, - { - "label": "lang_tum", - "value": "tum" - }, - { - "label": "lang_tup", - "value": "tup" - }, - { - "label": "lang_tur", - "value": "tur" - }, - { - "label": "lang_tut", - "value": "tut" - }, - { - "label": "lang_tvl", - "value": "tvl" - }, - { - "label": "lang_twi", - "value": "twi" - }, - { - "label": "lang_tyv", - "value": "tyv" - }, - { - "label": "lang_udm", - "value": "udm" - }, - { - "label": "lang_uga", - "value": "uga" - }, - { - "label": "lang_uig", - "value": "uig" - }, - { - "label": "lang_ukr", - "value": "ukr" - }, - { - "label": "lang_umb", - "value": "umb" - }, - { - "label": "lang_und", - "value": "und" - }, - { - "label": "lang_urd", - "value": "urd" - }, - { - "label": "lang_uzb", - "value": "uzb" - }, - { - "label": "lang_vai", - "value": "vai" - }, - { - "label": "lang_ven", - "value": "ven" - }, - { - "label": "lang_vie", - "value": "vie" - }, - { - "label": "lang_vol", - "value": "vol" - }, - { - "label": "lang_vot", - "value": "vot" - }, - { - "label": "lang_wak", - "value": "wak" - }, - { - "label": "lang_wal", - "value": "wal" - }, - { - "label": "lang_war", - "value": "war" - }, - { - "label": "lang_was", - "value": "was" - }, - { - "label": "lang_wel", - "value": "wel" - }, - { - "label": "lang_wen", - "value": "wen" - }, - { - "label": "lang_wln", - "value": "wln" - }, - { - "label": "lang_wol", - "value": "wol" - }, - { - "label": "lang_xal", - "value": "xal" - }, - { - "label": "lang_xho", - "value": "xho" - }, - { - "label": "lang_yao", - "value": "yao" - }, - { - "label": "lang_yap", - "value": "yap" - }, - { - "label": "lang_yid", - "value": "yid" - }, - { - "label": "lang_yor", - "value": "yor" - }, - { - "label": "lang_ypk", - "value": "ypk" - }, - { - "label": "lang_zap", - "value": "zap" - }, - { - "label": "lang_zbl", - "value": "zbl" - }, - { - "label": "lang_zen", - "value": "zen" - }, - { - "label": "lang_zha", - "value": "zha" - }, - { - "label": "lang_znd", - "value": "znd" - }, - { - "label": "lang_zul", - "value": "zul" - }, - { - "label": "lang_zun", - "value": "zun" - }, - { - "label": "lang_zxx", - "value": "zxx" - }, - { - "label": "lang_zza", - "value": "zza" - } - ] - } - } - }, - "propertiesOrder": [ - "language", - "value" - ], - "required": [ - "value", - "language" - ] - } - }, - "description": { - "title": "Descriptions", - "type": "array", - "minItems": 0, - "items": { - "title": "Description", - "type": "object", - "additionalProperties": false, - "properties": { - "value": { - "title": "Value", - "type": "string", - "minLength": 1, - "form": { - "type": "textarea", - "templateOptions": { - "rows": 5 - } - } - }, - "language": { - "title": "Language", - "type": "string", - "enum": [ - "aar", - "abk", - "ace", - "ach", - "ada", - "ady", - "afa", - "afh", - "afr", - "ain", - "aka", - "akk", - "alb", - "ale", - "alg", - "alt", - "amh", - "ang", - "anp", - "apa", - "ara", - "arc", - "arg", - "arm", - "arn", - "arp", - "art", - "arw", - "asm", - "ast", - "ath", - "aus", - "ava", - "ave", - "awa", - "aym", - "aze", - "bad", - "bai", - "bak", - "bal", - "bam", - "ban", - "baq", - "bas", - "bat", - "bej", - "bel", - "bem", - "ben", - "ber", - "bho", - "bih", - "bik", - "bin", - "bis", - "bla", - "bnt", - "bos", - "bra", - "bre", - "btk", - "bua", - "bug", - "bul", - "bur", - "byn", - "cad", - "cai", - "car", - "cat", - "cau", - "ceb", - "cel", - "cha", - "chb", - "che", - "chg", - "chi", - "chk", - "chm", - "chn", - "cho", - "chp", - "chr", - "chu", - "chv", - "chy", - "cmc", - "cnr", - "cop", - "cor", - "cos", - "cpe", - "cpf", - "cpp", - "cre", - "crh", - "crp", - "csb", - "cus", - "cze", - "dak", - "dan", - "dar", - "day", - "del", - "den", - "dgr", - "din", - "div", - "doi", - "dra", - "dsb", - "dua", - "dum", - "dut", - "dyu", - "dzo", - "efi", - "egy", - "eka", - "elx", - "eng", - "enm", - "epo", - "est", - "ewe", - "ewo", - "fan", - "fao", - "fat", - "fij", - "fil", - "fin", - "fiu", - "fon", - "fre", - "frm", - "fro", - "frr", - "frs", - "fry", - "ful", - "fur", - "gaa", - "gay", - "gba", - "gem", - "geo", - "ger", - "gez", - "gil", - "gla", - "gle", - "glg", - "glv", - "gmh", - "goh", - "gon", - "gor", - "got", - "grb", - "grc", - "gre", - "grn", - "gsw", - "guj", - "gwi", - "hai", - "hat", - "hau", - "haw", - "heb", - "her", - "hil", - "him", - "hin", - "hit", - "hmn", - "hmo", - "hrv", - "hsb", - "hun", - "hup", - "iba", - "ibo", - "ice", - "ido", - "iii", - "ijo", - "iku", - "ile", - "ilo", - "ina", - "inc", - "ind", - "ine", - "inh", - "ipk", - "ira", - "iro", - "ita", - "jav", - "jbo", - "jpn", - "jpr", - "jrb", - "kaa", - "kab", - "kac", - "kal", - "kam", - "kan", - "kar", - "kas", - "kau", - "kaw", - "kaz", - "kbd", - "kha", - "khi", - "khm", - "kho", - "kik", - "kin", - "kir", - "kmb", - "kok", - "kom", - "kon", - "kor", - "kos", - "kpe", - "krc", - "krl", - "kro", - "kru", - "kua", - "kum", - "kur", - "kut", - "lad", - "lah", - "lam", - "lao", - "lat", - "lav", - "lez", - "lim", - "lin", - "lit", - "lol", - "loz", - "ltz", - "lua", - "lub", - "lug", - "lui", - "lun", - "luo", - "lus", - "mac", - "mad", - "mag", - "mah", - "mai", - "mak", - "mal", - "man", - "mao", - "map", - "mar", - "mas", - "may", - "mdf", - "mdr", - "men", - "mga", - "mic", - "min", - "mis", - "mkh", - "mlg", - "mlt", - "mnc", - "mni", - "mno", - "moh", - "mon", - "mos", - "mul", - "mun", - "mus", - "mwl", - "mwr", - "myn", - "myv", - "nah", - "nai", - "nap", - "nau", - "nav", - "nbl", - "nde", - "ndo", - "nds", - "nep", - "new", - "nia", - "nic", - "niu", - "nno", - "nob", - "nog", - "non", - "nor", - "nqo", - "nso", - "nub", - "nwc", - "nya", - "nym", - "nyn", - "nyo", - "nzi", - "oci", - "oji", - "ori", - "orm", - "osa", - "oss", - "ota", - "oto", - "paa", - "pag", - "pal", - "pam", - "pan", - "pap", - "pau", - "peo", - "per", - "phi", - "phn", - "pli", - "pol", - "pon", - "por", - "pra", - "pro", - "pus", - "que", - "raj", - "rap", - "rar", - "roa", - "roh", - "rom", - "rum", - "run", - "rup", - "rus", - "sad", - "sag", - "sah", - "sai", - "sal", - "sam", - "san", - "sas", - "sat", - "scn", - "sco", - "sel", - "sem", - "sga", - "sgn", - "shn", - "sid", - "sin", - "sio", - "sit", - "sla", - "slo", - "slv", - "sma", - "sme", - "smi", - "smj", - "smn", - "smo", - "sms", - "sna", - "snd", - "snk", - "sog", - "som", - "son", - "sot", - "spa", - "srd", - "srn", - "srp", - "srr", - "ssa", - "ssw", - "suk", - "sun", - "sus", - "sux", - "swa", - "swe", - "syc", - "syr", - "tah", - "tai", - "tam", - "tat", - "tel", - "tem", - "ter", - "tet", - "tgk", - "tgl", - "tha", - "tib", - "tig", - "tir", - "tiv", - "tkl", - "tlh", - "tli", - "tmh", - "tog", - "ton", - "tpi", - "tsi", - "tsn", - "tso", - "tuk", - "tum", - "tup", - "tur", - "tut", - "tvl", - "twi", - "tyv", - "udm", - "uga", - "uig", - "ukr", - "umb", - "und", - "urd", - "uzb", - "vai", - "ven", - "vie", - "vol", - "vot", - "wak", - "wal", - "war", - "was", - "wel", - "wen", - "wln", - "wol", - "xal", - "xho", - "yao", - "yap", - "yid", - "yor", - "ypk", - "zap", - "zbl", - "zen", - "zha", - "znd", - "zul", - "zun", - "zxx", - "zza" - ], - "form": { - "templateOptions": { - "sort": true, - "wrappers": [ - "card" - ] - }, - "options": [ - { - "label": "lang_aar", - "value": "aar" - }, - { - "label": "lang_abk", - "value": "abk" - }, - { - "label": "lang_ace", - "value": "ace" - }, - { - "label": "lang_ach", - "value": "ach" - }, - { - "label": "lang_ada", - "value": "ada" - }, - { - "label": "lang_ady", - "value": "ady" - }, - { - "label": "lang_afa", - "value": "afa" - }, - { - "label": "lang_afh", - "value": "afh" - }, - { - "label": "lang_afr", - "value": "afr" - }, - { - "label": "lang_ain", - "value": "ain" - }, - { - "label": "lang_aka", - "value": "aka" - }, - { - "label": "lang_akk", - "value": "akk" - }, - { - "label": "lang_alb", - "value": "alb" - }, - { - "label": "lang_ale", - "value": "ale" - }, - { - "label": "lang_alg", - "value": "alg" - }, - { - "label": "lang_alt", - "value": "alt" - }, - { - "label": "lang_amh", - "value": "amh" - }, - { - "label": "lang_ang", - "value": "ang" - }, - { - "label": "lang_anp", - "value": "anp" - }, - { - "label": "lang_apa", - "value": "apa" - }, - { - "label": "lang_ara", - "value": "ara" - }, - { - "label": "lang_arc", - "value": "arc" - }, - { - "label": "lang_arg", - "value": "arg" - }, - { - "label": "lang_arm", - "value": "arm" - }, - { - "label": "lang_arn", - "value": "arn" - }, - { - "label": "lang_arp", - "value": "arp" - }, - { - "label": "lang_art", - "value": "art" - }, - { - "label": "lang_arw", - "value": "arw" - }, - { - "label": "lang_asm", - "value": "asm" - }, - { - "label": "lang_ast", - "value": "ast" - }, - { - "label": "lang_ath", - "value": "ath" - }, - { - "label": "lang_aus", - "value": "aus" - }, - { - "label": "lang_ava", - "value": "ava" - }, - { - "label": "lang_ave", - "value": "ave" - }, - { - "label": "lang_awa", - "value": "awa" - }, - { - "label": "lang_aym", - "value": "aym" - }, - { - "label": "lang_aze", - "value": "aze" - }, - { - "label": "lang_bad", - "value": "bad" - }, - { - "label": "lang_bai", - "value": "bai" - }, - { - "label": "lang_bak", - "value": "bak" - }, - { - "label": "lang_bal", - "value": "bal" - }, - { - "label": "lang_bam", - "value": "bam" - }, - { - "label": "lang_ban", - "value": "ban" - }, - { - "label": "lang_baq", - "value": "baq" - }, - { - "label": "lang_bas", - "value": "bas" - }, - { - "label": "lang_bat", - "value": "bat" - }, - { - "label": "lang_bej", - "value": "bej" - }, - { - "label": "lang_bel", - "value": "bel" - }, - { - "label": "lang_bem", - "value": "bem" - }, - { - "label": "lang_ben", - "value": "ben" - }, - { - "label": "lang_ber", - "value": "ber" - }, - { - "label": "lang_bho", - "value": "bho" - }, - { - "label": "lang_bih", - "value": "bih" - }, - { - "label": "lang_bik", - "value": "bik" - }, - { - "label": "lang_bin", - "value": "bin" - }, - { - "label": "lang_bis", - "value": "bis" - }, - { - "label": "lang_bla", - "value": "bla" - }, - { - "label": "lang_bnt", - "value": "bnt" - }, - { - "label": "lang_bos", - "value": "bos" - }, - { - "label": "lang_bra", - "value": "bra" - }, - { - "label": "lang_bre", - "value": "bre" - }, - { - "label": "lang_btk", - "value": "btk" - }, - { - "label": "lang_bua", - "value": "bua" - }, - { - "label": "lang_bug", - "value": "bug" - }, - { - "label": "lang_bul", - "value": "bul" - }, - { - "label": "lang_bur", - "value": "bur" - }, - { - "label": "lang_byn", - "value": "byn" - }, - { - "label": "lang_cad", - "value": "cad" - }, - { - "label": "lang_cai", - "value": "cai" - }, - { - "label": "lang_car", - "value": "car" - }, - { - "label": "lang_cat", - "value": "cat" - }, - { - "label": "lang_cau", - "value": "cau" - }, - { - "label": "lang_ceb", - "value": "ceb" - }, - { - "label": "lang_cel", - "value": "cel" - }, - { - "label": "lang_cha", - "value": "cha" - }, - { - "label": "lang_chb", - "value": "chb" - }, - { - "label": "lang_che", - "value": "che" - }, - { - "label": "lang_chg", - "value": "chg" - }, - { - "label": "lang_chi", - "value": "chi" - }, - { - "label": "lang_chk", - "value": "chk" - }, - { - "label": "lang_chm", - "value": "chm" - }, - { - "label": "lang_chn", - "value": "chn" - }, - { - "label": "lang_cho", - "value": "cho" - }, - { - "label": "lang_chp", - "value": "chp" - }, - { - "label": "lang_chr", - "value": "chr" - }, - { - "label": "lang_chu", - "value": "chu" - }, - { - "label": "lang_chv", - "value": "chv" - }, - { - "label": "lang_chy", - "value": "chy" - }, - { - "label": "lang_cmc", - "value": "cmc" - }, - { - "label": "lang_cnr", - "value": "cnr" - }, - { - "label": "lang_cop", - "value": "cop" - }, - { - "label": "lang_cor", - "value": "cor" - }, - { - "label": "lang_cos", - "value": "cos" - }, - { - "label": "lang_cpe", - "value": "cpe" - }, - { - "label": "lang_cpf", - "value": "cpf" - }, - { - "label": "lang_cpp", - "value": "cpp" - }, - { - "label": "lang_cre", - "value": "cre" - }, - { - "label": "lang_crh", - "value": "crh" - }, - { - "label": "lang_crp", - "value": "crp" - }, - { - "label": "lang_csb", - "value": "csb" - }, - { - "label": "lang_cus", - "value": "cus" - }, - { - "label": "lang_cze", - "value": "cze" - }, - { - "label": "lang_dak", - "value": "dak" - }, - { - "label": "lang_dan", - "value": "dan" - }, - { - "label": "lang_dar", - "value": "dar" - }, - { - "label": "lang_day", - "value": "day" - }, - { - "label": "lang_del", - "value": "del" - }, - { - "label": "lang_den", - "value": "den" - }, - { - "label": "lang_dgr", - "value": "dgr" - }, - { - "label": "lang_din", - "value": "din" - }, - { - "label": "lang_div", - "value": "div" - }, - { - "label": "lang_doi", - "value": "doi" - }, - { - "label": "lang_dra", - "value": "dra" - }, - { - "label": "lang_dsb", - "value": "dsb" - }, - { - "label": "lang_dua", - "value": "dua" - }, - { - "label": "lang_dum", - "value": "dum" - }, - { - "label": "lang_dut", - "value": "dut" - }, - { - "label": "lang_dyu", - "value": "dyu" - }, - { - "label": "lang_dzo", - "value": "dzo" - }, - { - "label": "lang_efi", - "value": "efi" - }, - { - "label": "lang_egy", - "value": "egy" - }, - { - "label": "lang_eka", - "value": "eka" - }, - { - "label": "lang_elx", - "value": "elx" - }, - { - "label": "lang_eng", - "value": "eng", - "preferred": true - }, - { - "label": "lang_enm", - "value": "enm" - }, - { - "label": "lang_epo", - "value": "epo" - }, - { - "label": "lang_est", - "value": "est" - }, - { - "label": "lang_ewe", - "value": "ewe" - }, - { - "label": "lang_ewo", - "value": "ewo" - }, - { - "label": "lang_fan", - "value": "fan" - }, - { - "label": "lang_fao", - "value": "fao" - }, - { - "label": "lang_fat", - "value": "fat" - }, - { - "label": "lang_fij", - "value": "fij" - }, - { - "label": "lang_fil", - "value": "fil" - }, - { - "label": "lang_fin", - "value": "fin" - }, - { - "label": "lang_fiu", - "value": "fiu" - }, - { - "label": "lang_fon", - "value": "fon" - }, - { - "label": "lang_fre", - "value": "fre", - "preferred": true - }, - { - "label": "lang_frm", - "value": "frm" - }, - { - "label": "lang_fro", - "value": "fro" - }, - { - "label": "lang_frr", - "value": "frr" - }, - { - "label": "lang_frs", - "value": "frs" - }, - { - "label": "lang_fry", - "value": "fry" - }, - { - "label": "lang_ful", - "value": "ful" - }, - { - "label": "lang_fur", - "value": "fur" - }, - { - "label": "lang_gaa", - "value": "gaa" - }, - { - "label": "lang_gay", - "value": "gay" - }, - { - "label": "lang_gba", - "value": "gba" - }, - { - "label": "lang_gem", - "value": "gem" - }, - { - "label": "lang_geo", - "value": "geo" - }, - { - "label": "lang_ger", - "value": "ger", - "preferred": true - }, - { - "label": "lang_gez", - "value": "gez" - }, - { - "label": "lang_gil", - "value": "gil" - }, - { - "label": "lang_gla", - "value": "gla" - }, - { - "label": "lang_gle", - "value": "gle" - }, - { - "label": "lang_glg", - "value": "glg" - }, - { - "label": "lang_glv", - "value": "glv" - }, - { - "label": "lang_gmh", - "value": "gmh" - }, - { - "label": "lang_goh", - "value": "goh" - }, - { - "label": "lang_gon", - "value": "gon" - }, - { - "label": "lang_gor", - "value": "gor" - }, - { - "label": "lang_got", - "value": "got" - }, - { - "label": "lang_grb", - "value": "grb" - }, - { - "label": "lang_grc", - "value": "grc" - }, - { - "label": "lang_gre", - "value": "gre" - }, - { - "label": "lang_grn", - "value": "grn" - }, - { - "label": "lang_gsw", - "value": "gsw" - }, - { - "label": "lang_guj", - "value": "guj" - }, - { - "label": "lang_gwi", - "value": "gwi" - }, - { - "label": "lang_hai", - "value": "hai" - }, - { - "label": "lang_hat", - "value": "hat" - }, - { - "label": "lang_hau", - "value": "hau" - }, - { - "label": "lang_haw", - "value": "haw" - }, - { - "label": "lang_heb", - "value": "heb" - }, - { - "label": "lang_her", - "value": "her" - }, - { - "label": "lang_hil", - "value": "hil" - }, - { - "label": "lang_him", - "value": "him" - }, - { - "label": "lang_hin", - "value": "hin" - }, - { - "label": "lang_hit", - "value": "hit" - }, - { - "label": "lang_hmn", - "value": "hmn" - }, - { - "label": "lang_hmo", - "value": "hmo" - }, - { - "label": "lang_hrv", - "value": "hrv" - }, - { - "label": "lang_hsb", - "value": "hsb" - }, - { - "label": "lang_hun", - "value": "hun" - }, - { - "label": "lang_hup", - "value": "hup" - }, - { - "label": "lang_iba", - "value": "iba" - }, - { - "label": "lang_ibo", - "value": "ibo" - }, - { - "label": "lang_ice", - "value": "ice" - }, - { - "label": "lang_ido", - "value": "ido" - }, - { - "label": "lang_iii", - "value": "iii" - }, - { - "label": "lang_ijo", - "value": "ijo" - }, - { - "label": "lang_iku", - "value": "iku" - }, - { - "label": "lang_ile", - "value": "ile" - }, - { - "label": "lang_ilo", - "value": "ilo" - }, - { - "label": "lang_ina", - "value": "ina" - }, - { - "label": "lang_inc", - "value": "inc" - }, - { - "label": "lang_ind", - "value": "ind" - }, - { - "label": "lang_ine", - "value": "ine" - }, - { - "label": "lang_inh", - "value": "inh" - }, - { - "label": "lang_ipk", - "value": "ipk" - }, - { - "label": "lang_ira", - "value": "ira" - }, - { - "label": "lang_iro", - "value": "iro" - }, - { - "label": "lang_ita", - "value": "ita", - "preferred": true - }, - { - "label": "lang_jav", - "value": "jav" - }, - { - "label": "lang_jbo", - "value": "jbo" - }, - { - "label": "lang_jpn", - "value": "jpn" - }, - { - "label": "lang_jpr", - "value": "jpr" - }, - { - "label": "lang_jrb", - "value": "jrb" - }, - { - "label": "lang_kaa", - "value": "kaa" - }, - { - "label": "lang_kab", - "value": "kab" - }, - { - "label": "lang_kac", - "value": "kac" - }, - { - "label": "lang_kal", - "value": "kal" - }, - { - "label": "lang_kam", - "value": "kam" - }, - { - "label": "lang_kan", - "value": "kan" - }, - { - "label": "lang_kar", - "value": "kar" - }, - { - "label": "lang_kas", - "value": "kas" - }, - { - "label": "lang_kau", - "value": "kau" - }, - { - "label": "lang_kaw", - "value": "kaw" - }, - { - "label": "lang_kaz", - "value": "kaz" - }, - { - "label": "lang_kbd", - "value": "kbd" - }, - { - "label": "lang_kha", - "value": "kha" - }, - { - "label": "lang_khi", - "value": "khi" - }, - { - "label": "lang_khm", - "value": "khm" - }, - { - "label": "lang_kho", - "value": "kho" - }, - { - "label": "lang_kik", - "value": "kik" - }, - { - "label": "lang_kin", - "value": "kin" - }, - { - "label": "lang_kir", - "value": "kir" - }, - { - "label": "lang_kmb", - "value": "kmb" - }, - { - "label": "lang_kok", - "value": "kok" - }, - { - "label": "lang_kom", - "value": "kom" - }, - { - "label": "lang_kon", - "value": "kon" - }, - { - "label": "lang_kor", - "value": "kor" - }, - { - "label": "lang_kos", - "value": "kos" - }, - { - "label": "lang_kpe", - "value": "kpe" - }, - { - "label": "lang_krc", - "value": "krc" - }, - { - "label": "lang_krl", - "value": "krl" - }, - { - "label": "lang_kro", - "value": "kro" - }, - { - "label": "lang_kru", - "value": "kru" - }, - { - "label": "lang_kua", - "value": "kua" - }, - { - "label": "lang_kum", - "value": "kum" - }, - { - "label": "lang_kur", - "value": "kur" - }, - { - "label": "lang_kut", - "value": "kut" - }, - { - "label": "lang_lad", - "value": "lad" - }, - { - "label": "lang_lah", - "value": "lah" - }, - { - "label": "lang_lam", - "value": "lam" - }, - { - "label": "lang_lao", - "value": "lao" - }, - { - "label": "lang_lat", - "value": "lat" - }, - { - "label": "lang_lav", - "value": "lav" - }, - { - "label": "lang_lez", - "value": "lez" - }, - { - "label": "lang_lim", - "value": "lim" - }, - { - "label": "lang_lin", - "value": "lin" - }, - { - "label": "lang_lit", - "value": "lit" - }, - { - "label": "lang_lol", - "value": "lol" - }, - { - "label": "lang_loz", - "value": "loz" - }, - { - "label": "lang_ltz", - "value": "ltz" - }, - { - "label": "lang_lua", - "value": "lua" - }, - { - "label": "lang_lub", - "value": "lub" - }, - { - "label": "lang_lug", - "value": "lug" - }, - { - "label": "lang_lui", - "value": "lui" - }, - { - "label": "lang_lun", - "value": "lun" - }, - { - "label": "lang_luo", - "value": "luo" - }, - { - "label": "lang_lus", - "value": "lus" - }, - { - "label": "lang_mac", - "value": "mac" - }, - { - "label": "lang_mad", - "value": "mad" - }, - { - "label": "lang_mag", - "value": "mag" - }, - { - "label": "lang_mah", - "value": "mah" - }, - { - "label": "lang_mai", - "value": "mai" - }, - { - "label": "lang_mak", - "value": "mak" - }, - { - "label": "lang_mal", - "value": "mal" - }, - { - "label": "lang_man", - "value": "man" - }, - { - "label": "lang_mao", - "value": "mao" - }, - { - "label": "lang_map", - "value": "map" - }, - { - "label": "lang_mar", - "value": "mar" - }, - { - "label": "lang_mas", - "value": "mas" - }, - { - "label": "lang_may", - "value": "may" - }, - { - "label": "lang_mdf", - "value": "mdf" - }, - { - "label": "lang_mdr", - "value": "mdr" - }, - { - "label": "lang_men", - "value": "men" - }, - { - "label": "lang_mga", - "value": "mga" - }, - { - "label": "lang_mic", - "value": "mic" - }, - { - "label": "lang_min", - "value": "min" - }, - { - "label": "lang_mis", - "value": "mis" - }, - { - "label": "lang_mkh", - "value": "mkh" - }, - { - "label": "lang_mlg", - "value": "mlg" - }, - { - "label": "lang_mlt", - "value": "mlt" - }, - { - "label": "lang_mnc", - "value": "mnc" - }, - { - "label": "lang_mni", - "value": "mni" - }, - { - "label": "lang_mno", - "value": "mno" - }, - { - "label": "lang_moh", - "value": "moh" - }, - { - "label": "lang_mon", - "value": "mon" - }, - { - "label": "lang_mos", - "value": "mos" - }, - { - "label": "lang_mul", - "value": "mul" - }, - { - "label": "lang_mun", - "value": "mun" - }, - { - "label": "lang_mus", - "value": "mus" - }, - { - "label": "lang_mwl", - "value": "mwl" - }, - { - "label": "lang_mwr", - "value": "mwr" - }, - { - "label": "lang_myn", - "value": "myn" - }, - { - "label": "lang_myv", - "value": "myv" - }, - { - "label": "lang_nah", - "value": "nah" - }, - { - "label": "lang_nai", - "value": "nai" - }, - { - "label": "lang_nap", - "value": "nap" - }, - { - "label": "lang_nau", - "value": "nau" - }, - { - "label": "lang_nav", - "value": "nav" - }, - { - "label": "lang_nbl", - "value": "nbl" - }, - { - "label": "lang_nde", - "value": "nde" - }, - { - "label": "lang_ndo", - "value": "ndo" - }, - { - "label": "lang_nds", - "value": "nds" - }, - { - "label": "lang_nep", - "value": "nep" - }, - { - "label": "lang_new", - "value": "new" - }, - { - "label": "lang_nia", - "value": "nia" - }, - { - "label": "lang_nic", - "value": "nic" - }, - { - "label": "lang_niu", - "value": "niu" - }, - { - "label": "lang_nno", - "value": "nno" - }, - { - "label": "lang_nob", - "value": "nob" - }, - { - "label": "lang_nog", - "value": "nog" - }, - { - "label": "lang_non", - "value": "non" - }, - { - "label": "lang_nor", - "value": "nor" - }, - { - "label": "lang_nqo", - "value": "nqo" - }, - { - "label": "lang_nso", - "value": "nso" - }, - { - "label": "lang_nub", - "value": "nub" - }, - { - "label": "lang_nwc", - "value": "nwc" - }, - { - "label": "lang_nya", - "value": "nya" - }, - { - "label": "lang_nym", - "value": "nym" - }, - { - "label": "lang_nyn", - "value": "nyn" - }, - { - "label": "lang_nyo", - "value": "nyo" - }, - { - "label": "lang_nzi", - "value": "nzi" - }, - { - "label": "lang_oci", - "value": "oci" - }, - { - "label": "lang_oji", - "value": "oji" - }, - { - "label": "lang_ori", - "value": "ori" - }, - { - "label": "lang_orm", - "value": "orm" - }, - { - "label": "lang_osa", - "value": "osa" - }, - { - "label": "lang_oss", - "value": "oss" - }, - { - "label": "lang_ota", - "value": "ota" - }, - { - "label": "lang_oto", - "value": "oto" - }, - { - "label": "lang_paa", - "value": "paa" - }, - { - "label": "lang_pag", - "value": "pag" - }, - { - "label": "lang_pal", - "value": "pal" - }, - { - "label": "lang_pam", - "value": "pam" - }, - { - "label": "lang_pan", - "value": "pan" - }, - { - "label": "lang_pap", - "value": "pap" - }, - { - "label": "lang_pau", - "value": "pau" - }, - { - "label": "lang_peo", - "value": "peo" - }, - { - "label": "lang_per", - "value": "per" - }, - { - "label": "lang_phi", - "value": "phi" - }, - { - "label": "lang_phn", - "value": "phn" - }, - { - "label": "lang_pli", - "value": "pli" - }, - { - "label": "lang_pol", - "value": "pol" - }, - { - "label": "lang_pon", - "value": "pon" - }, - { - "label": "lang_por", - "value": "por" - }, - { - "label": "lang_pra", - "value": "pra" - }, - { - "label": "lang_pro", - "value": "pro" - }, - { - "label": "lang_pus", - "value": "pus" - }, - { - "label": "lang_que", - "value": "que" - }, - { - "label": "lang_raj", - "value": "raj" - }, - { - "label": "lang_rap", - "value": "rap" - }, - { - "label": "lang_rar", - "value": "rar" - }, - { - "label": "lang_roa", - "value": "roa" - }, - { - "label": "lang_roh", - "value": "roh" - }, - { - "label": "lang_rom", - "value": "rom" - }, - { - "label": "lang_rum", - "value": "rum" - }, - { - "label": "lang_run", - "value": "run" - }, - { - "label": "lang_rup", - "value": "rup" - }, - { - "label": "lang_rus", - "value": "rus" - }, - { - "label": "lang_sad", - "value": "sad" - }, - { - "label": "lang_sag", - "value": "sag" - }, - { - "label": "lang_sah", - "value": "sah" - }, - { - "label": "lang_sai", - "value": "sai" - }, - { - "label": "lang_sal", - "value": "sal" - }, - { - "label": "lang_sam", - "value": "sam" - }, - { - "label": "lang_san", - "value": "san" - }, - { - "label": "lang_sas", - "value": "sas" - }, - { - "label": "lang_sat", - "value": "sat" - }, - { - "label": "lang_scn", - "value": "scn" - }, - { - "label": "lang_sco", - "value": "sco" - }, - { - "label": "lang_sel", - "value": "sel" - }, - { - "label": "lang_sem", - "value": "sem" - }, - { - "label": "lang_sga", - "value": "sga" - }, - { - "label": "lang_sgn", - "value": "sgn" - }, - { - "label": "lang_shn", - "value": "shn" - }, - { - "label": "lang_sid", - "value": "sid" - }, - { - "label": "lang_sin", - "value": "sin" - }, - { - "label": "lang_sio", - "value": "sio" - }, - { - "label": "lang_sit", - "value": "sit" - }, - { - "label": "lang_sla", - "value": "sla" - }, - { - "label": "lang_slo", - "value": "slo" - }, - { - "label": "lang_slv", - "value": "slv" - }, - { - "label": "lang_sma", - "value": "sma" - }, - { - "label": "lang_sme", - "value": "sme" - }, - { - "label": "lang_smi", - "value": "smi" - }, - { - "label": "lang_smj", - "value": "smj" - }, - { - "label": "lang_smn", - "value": "smn" - }, - { - "label": "lang_smo", - "value": "smo" - }, - { - "label": "lang_sms", - "value": "sms" - }, - { - "label": "lang_sna", - "value": "sna" - }, - { - "label": "lang_snd", - "value": "snd" - }, - { - "label": "lang_snk", - "value": "snk" - }, - { - "label": "lang_sog", - "value": "sog" - }, - { - "label": "lang_som", - "value": "som" - }, - { - "label": "lang_son", - "value": "son" - }, - { - "label": "lang_sot", - "value": "sot" - }, - { - "label": "lang_spa", - "value": "spa" - }, - { - "label": "lang_srd", - "value": "srd" - }, - { - "label": "lang_srn", - "value": "srn" - }, - { - "label": "lang_srp", - "value": "srp" - }, - { - "label": "lang_srr", - "value": "srr" - }, - { - "label": "lang_ssa", - "value": "ssa" - }, - { - "label": "lang_ssw", - "value": "ssw" - }, - { - "label": "lang_suk", - "value": "suk" - }, - { - "label": "lang_sun", - "value": "sun" - }, - { - "label": "lang_sus", - "value": "sus" - }, - { - "label": "lang_sux", - "value": "sux" - }, - { - "label": "lang_swa", - "value": "swa" - }, - { - "label": "lang_swe", - "value": "swe" - }, - { - "label": "lang_syc", - "value": "syc" - }, - { - "label": "lang_syr", - "value": "syr" - }, - { - "label": "lang_tah", - "value": "tah" - }, - { - "label": "lang_tai", - "value": "tai" - }, - { - "label": "lang_tam", - "value": "tam" - }, - { - "label": "lang_tat", - "value": "tat" - }, - { - "label": "lang_tel", - "value": "tel" - }, - { - "label": "lang_tem", - "value": "tem" - }, - { - "label": "lang_ter", - "value": "ter" - }, - { - "label": "lang_tet", - "value": "tet" - }, - { - "label": "lang_tgk", - "value": "tgk" - }, - { - "label": "lang_tgl", - "value": "tgl" - }, - { - "label": "lang_tha", - "value": "tha" - }, - { - "label": "lang_tib", - "value": "tib" - }, - { - "label": "lang_tig", - "value": "tig" - }, - { - "label": "lang_tir", - "value": "tir" - }, - { - "label": "lang_tiv", - "value": "tiv" - }, - { - "label": "lang_tkl", - "value": "tkl" - }, - { - "label": "lang_tlh", - "value": "tlh" - }, - { - "label": "lang_tli", - "value": "tli" - }, - { - "label": "lang_tmh", - "value": "tmh" - }, - { - "label": "lang_tog", - "value": "tog" - }, - { - "label": "lang_ton", - "value": "ton" - }, - { - "label": "lang_tpi", - "value": "tpi" - }, - { - "label": "lang_tsi", - "value": "tsi" - }, - { - "label": "lang_tsn", - "value": "tsn" - }, - { - "label": "lang_tso", - "value": "tso" - }, - { - "label": "lang_tuk", - "value": "tuk" - }, - { - "label": "lang_tum", - "value": "tum" - }, - { - "label": "lang_tup", - "value": "tup" - }, - { - "label": "lang_tur", - "value": "tur" - }, - { - "label": "lang_tut", - "value": "tut" - }, - { - "label": "lang_tvl", - "value": "tvl" - }, - { - "label": "lang_twi", - "value": "twi" - }, - { - "label": "lang_tyv", - "value": "tyv" - }, - { - "label": "lang_udm", - "value": "udm" - }, - { - "label": "lang_uga", - "value": "uga" - }, - { - "label": "lang_uig", - "value": "uig" - }, - { - "label": "lang_ukr", - "value": "ukr" - }, - { - "label": "lang_umb", - "value": "umb" - }, - { - "label": "lang_und", - "value": "und" - }, - { - "label": "lang_urd", - "value": "urd" - }, - { - "label": "lang_uzb", - "value": "uzb" - }, - { - "label": "lang_vai", - "value": "vai" - }, - { - "label": "lang_ven", - "value": "ven" - }, - { - "label": "lang_vie", - "value": "vie" - }, - { - "label": "lang_vol", - "value": "vol" - }, - { - "label": "lang_vot", - "value": "vot" - }, - { - "label": "lang_wak", - "value": "wak" - }, - { - "label": "lang_wal", - "value": "wal" - }, - { - "label": "lang_war", - "value": "war" - }, - { - "label": "lang_was", - "value": "was" - }, - { - "label": "lang_wel", - "value": "wel" - }, - { - "label": "lang_wen", - "value": "wen" - }, - { - "label": "lang_wln", - "value": "wln" - }, - { - "label": "lang_wol", - "value": "wol" - }, - { - "label": "lang_xal", - "value": "xal" - }, - { - "label": "lang_xho", - "value": "xho" - }, - { - "label": "lang_yao", - "value": "yao" - }, - { - "label": "lang_yap", - "value": "yap" - }, - { - "label": "lang_yid", - "value": "yid" - }, - { - "label": "lang_yor", - "value": "yor" - }, - { - "label": "lang_ypk", - "value": "ypk" - }, - { - "label": "lang_zap", - "value": "zap" - }, - { - "label": "lang_zbl", - "value": "zbl" - }, - { - "label": "lang_zen", - "value": "zen" - }, - { - "label": "lang_zha", - "value": "zha" - }, - { - "label": "lang_znd", - "value": "znd" - }, - { - "label": "lang_zul", - "value": "zul" - }, - { - "label": "lang_zun", - "value": "zun" - }, - { - "label": "lang_zxx", - "value": "zxx" - }, - { - "label": "lang_zza", - "value": "zza" - } - ] - } - } - }, - "propertiesOrder": [ - "language", - "value" - ], - "required": [ - "value", - "language" - ] - } - }, - "organisation": { - "title": "Organisation", - "type": "object", - "properties": { - "$ref": { - "title": "Organisation", - "type": "string", - "pattern": "^https://sonar.ch/api/organisations/.*?$", - "form": { - "remoteOptions": { - "type": "organisations" - } - } - } - }, - "required": [ - "$ref" - ], - "form": { - "expressionProperties": { - "templateOptions.required": "true" - } - } - }, - "_bucket": { - "title": "Bucket UUID", - "type": "string", - "minLength": 1 - }, - "_files": { - "title": "Files", - "description": "List of files attached to the record.", - "type": "array", - "items": { - "title": "File item", - "description": "Describes the information of a single file in the record.", - "additionalProperties": false, - "type": "object", - "properties": { - "bucket": { - "title": "Bucket UUID", - "type": "string", - "minLength": 1 - }, - "file_id": { - "title": "File UUID", - "type": "string", - "minLength": 1 - }, - "version_id": { - "title": "Version UUID", - "type": "string", - "minLength": 1 - }, - "key": { - "title": "Key", - "type": "string", - "minLength": 1 - }, - "mimetype": { - "title": "Mimetype", - "type": "string", - "minLength": 1 - }, - "checksum": { - "title": "Checksum", - "description": "MD5 checksum of the file.", - "type": "string", - "minLength": 1 - }, - "size": { - "title": "Size", - "description": "Size of the file in bytes.", - "type": "integer" - } - }, - "required": [ - "bucket", - "file_id", - "version_id", - "key" - ] - } - } - }, - "propertiesOrder": [ - "name", - "description" - ], - "required": [ - "name" - ] -} \ No newline at end of file diff --git a/sonar/modules/collections/jsonschemas/collections/collection-v1.0.0_src.json b/sonar/modules/collections/jsonschemas/collections/collection-v1.0.0_src.json new file mode 100644 index 000000000..8c1ba54b3 --- /dev/null +++ b/sonar/modules/collections/jsonschemas/collections/collection-v1.0.0_src.json @@ -0,0 +1,210 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "https://sonar.ch/schemas/collections/collection-v1.0.0.json", + "title": "Collections", + "type": "object", + "additionalProperties": false, + "properties": { + "$schema": { + "type": "string", + "default": "https://sonar.ch/schemas/collections/collection-v1.0.0.json" + }, + "pid": { + "title": "Identifier", + "type": "string", + "minLength": 1 + }, + "hashKey": { + "title": "Hash key", + "type": "string", + "minLength": 1 + }, + "name": { + "title": "Names", + "type": "array", + "minItems": 1, + "items": { + "title": "Name", + "type": "object", + "additionalProperties": false, + "properties": { + "value": { + "title": "Value", + "type": "string", + "minLength": 1 + }, + "language": { + "$ref": "language-v1.0.0.json" + } + }, + "propertiesOrder": [ + "language", + "value" + ], + "required": [ + "value", + "language" + ] + } + }, + "description": { + "title": "Descriptions", + "type": "array", + "minItems": 0, + "items": { + "title": "Description", + "type": "object", + "additionalProperties": false, + "properties": { + "value": { + "title": "Value", + "type": "string", + "minLength": 1, + "form": { + "type": "textarea", + "templateOptions": { + "rows": 5 + } + } + }, + "language": { + "$ref": "language-v1.0.0.json" + } + }, + "propertiesOrder": [ + "language", + "value" + ], + "required": [ + "value", + "language" + ] + }, + "form": { + "hide": true, + "navigation": { + "essential": true + } + } + }, + "organisation": { + "title": "Organisation", + "type": "object", + "properties": { + "$ref": { + "title": "Organisation", + "type": "string", + "pattern": "^https://sonar.ch/api/organisations/.*?$", + "form": { + "remoteOptions": { + "type": "organisations" + } + } + } + }, + "required": [ + "$ref" + ], + "form": { + "expressionProperties": { + "templateOptions.required": "true" + } + } + }, + "parent": { + "title": "Parent collection", + "type": "object", + "additionalProperties": false, + "properties": { + "$ref": { + "type": "string", + "pattern": "^https://sonar.ch/api/collections/.*?$", + "form": { + "remoteTypeahead": { + "type": "collections", + "field": "name.value.suggest", + "label": "label" + } + } + } + }, + "required": [ + "$ref" + ], + "form": { + "hide": true, + "navigation": { + "essential": true + } + } + }, + "_bucket": { + "title": "Bucket UUID", + "type": "string", + "minLength": 1 + }, + "_files": { + "title": "Files", + "description": "List of files attached to the record.", + "type": "array", + "items": { + "title": "File item", + "description": "Describes the information of a single file in the record.", + "additionalProperties": false, + "type": "object", + "properties": { + "bucket": { + "title": "Bucket UUID", + "type": "string", + "minLength": 1 + }, + "file_id": { + "title": "File UUID", + "type": "string", + "minLength": 1 + }, + "version_id": { + "title": "Version UUID", + "type": "string", + "minLength": 1 + }, + "key": { + "title": "Key", + "type": "string", + "minLength": 1 + }, + "mimetype": { + "title": "Mimetype", + "type": "string", + "minLength": 1 + }, + "checksum": { + "title": "Checksum", + "description": "MD5 checksum of the file.", + "type": "string", + "minLength": 1 + }, + "size": { + "title": "Size", + "description": "Size of the file in bytes.", + "type": "integer" + } + }, + "required": [ + "bucket", + "file_id", + "version_id", + "key" + ] + } + } + }, + "propertiesOrder": [ + "name", + "description", + "parent" + ], + "required": [ + "name" + ] +} diff --git a/sonar/modules/collections/loaders/__init__.py b/sonar/modules/collections/loaders/__init__.py new file mode 100644 index 000000000..667803166 --- /dev/null +++ b/sonar/modules/collections/loaders/__init__.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# +# Swiss Open Access Repository +# Copyright (C) 2021 RERO +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +"""Loaders.""" + +from invenio_records_rest.loaders.marshmallow import marshmallow_loader + +from ..schemas import RecordMetadataSchema + +#: JSON loader using Marshmallow for data validation. +json_v1 = marshmallow_loader(RecordMetadataSchema) + +__all__ = ('json_v1', ) diff --git a/sonar/modules/collections/mappings/__init__.py b/sonar/modules/collections/mappings/__init__.py new file mode 100644 index 000000000..1bd3117c4 --- /dev/null +++ b/sonar/modules/collections/mappings/__init__.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# +# Swiss Open Access Repository +# Copyright (C) 2021 RERO +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +"""Elasticsearch mappings.""" diff --git a/sonar/modules/collections/mappings/v7/__init__.py b/sonar/modules/collections/mappings/v7/__init__.py new file mode 100644 index 000000000..1bd3117c4 --- /dev/null +++ b/sonar/modules/collections/mappings/v7/__init__.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# +# Swiss Open Access Repository +# Copyright (C) 2021 RERO +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +"""Elasticsearch mappings.""" diff --git a/sonar/modules/collections/mappings/v7/collections/collection-v1.0.0.json b/sonar/modules/collections/mappings/v7/collections/collection-v1.0.0.json new file mode 100644 index 000000000..4e6d306a6 --- /dev/null +++ b/sonar/modules/collections/mappings/v7/collections/collection-v1.0.0.json @@ -0,0 +1,87 @@ +{ + "settings": { + "number_of_shards": 8, + "number_of_replicas": 2, + "max_result_window": 20000 + }, + "mappings": { + "date_detection": false, + "numeric_detection": false, + "properties": { + "$schema": { + "type": "keyword" + }, + "pid": { + "type": "keyword" + }, + "hashKey": { + "type": "keyword" + }, + "name": { + "type": "object", + "properties": { + "language": { + "type": "keyword" + }, + "value": { + "type": "text", + "fields": { + "suggest": { + "type": "text", + "analyzer": "autocomplete", + "search_analyzer": "standard" + } + } + } + } + }, + "description": { + "type": "object", + "properties": { + "language": { + "type": "keyword" + }, + "value": { + "type": "text" + } + } + }, + "organisation": { + "type": "object", + "properties": { + "pid": { + "type": "keyword" + } + } + }, + "parent": { + "type": "object", + "properties": { + "pid": { + "type": "keyword" + }, + "name": { + "type": "object", + "properties": { + "value": { + "type": "text" + }, + "language": { + "type": "keyword" + } + } + } + } + }, + "path": { + "type": "text" + }, + "_created": { + "type": "date" + }, + "_updated": { + "type": "date" + } + } + } +} diff --git a/sonar/modules/collections/minters.py b/sonar/modules/collections/minters.py new file mode 100644 index 000000000..0160b923d --- /dev/null +++ b/sonar/modules/collections/minters.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# +# Swiss Open Access Repository +# Copyright (C) 2021 RERO +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +"""Minters.""" + + +def id_minter(record_uuid, data, provider, pid_key='pid', object_type='rec'): + """PID minter. + + :param str record_uuid: UUID of the record + :param dict data: Data of the record + :param RecordProvider provider: PID provider + :param str pid_key: PIF key + :param str object_type: Object type + :return: PID value + :rtype: str + """ + # Create persistent identifier + provider = provider.create(object_type=object_type, + object_uuid=record_uuid, + pid_value=data.get(pid_key)) + pid = provider.pid + data[pid_key] = pid.pid_value + return pid diff --git a/sonar/modules/collections/permissions.py b/sonar/modules/collections/permissions.py new file mode 100644 index 000000000..89c322393 --- /dev/null +++ b/sonar/modules/collections/permissions.py @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- +# +# Swiss Open Access Repository +# Copyright (C) 2021 RERO +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +"""Record permissions.""" + +from elasticsearch_dsl import Q + +from sonar.modules.documents.api import DocumentSearch +from sonar.modules.organisations.api import current_organisation +from sonar.modules.permissions import RecordPermission as BaseRecordPermission + +from .api import Record + + +class RecordPermission(BaseRecordPermission): + """Record permissions.""" + + @classmethod + def list(cls, user, record=None): + """List permission check. + + :param UserRecord user: Current user record + :param Record record: Record to check + :return: True is action can be done + :rtype: bool + """ + return user and user.is_moderator + + @classmethod + def create(cls, user, record=None): + """Create permission check. + + :param UserRecord user: Current user record + :param Record record: Record to check + :return: True is action can be done + :rtype: bool + """ + return user and user.is_moderator + + @classmethod + def read(cls, user, record): + """Read permission check. + + :param UserRecord user: Current user record + :param Record record: Record to check + :return: True is action can be done + :rtype: bool + """ + # Only for moderator users + if not user or not user.is_moderator: + return False + + # Super user is allowed + if user.is_superuser: + return True + + record = Record.get_record_by_pid(record['pid']) + record = record.replace_refs() + + # For moderator users, they can read only their own records. + return current_organisation['pid'] == record['organisation']['pid'] + + @classmethod + def update(cls, user, record): + """Update permission check. + + :param UserRecord user: Current user record + :param Record record: Record to check + :return: True is action can be done + :rtype: bool + """ + return cls.read(user, record) + + @classmethod + def delete(cls, user, record): + """Delete permission check. + + :param UserRecord user: Current user record + :param Record record: Record to check + :return: True if action can be done + :rtype: bool + """ + results = DocumentSearch().query( + Q('nested', + path='collections', + query=Q('bool', must=Q( + 'term', + collections__pid=record['pid'])))).source(includes=['pid']) + + # Cannot remove collection associated to a record + if results.count(): + return False + + return cls.read(user, record) diff --git a/sonar/modules/collections/query.py b/sonar/modules/collections/query.py new file mode 100644 index 000000000..a3d68db35 --- /dev/null +++ b/sonar/modules/collections/query.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# +# Swiss Open Access Repository +# Copyright (C) 2021 RERO +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +"""Query.""" + +from elasticsearch_dsl import Q +from flask import current_app, request + +from sonar.modules.organisations.api import current_organisation +from sonar.modules.query import default_search_factory +from sonar.modules.users.api import current_user_record + + +def search_factory(self, search): + """Search factory. + + :param Search search: Search instance + :return: Tuple with search instance and URL arguments + :rtype: tuple + """ + search, urlkwargs = default_search_factory(self, search) + + if current_app.config.get('SONAR_APP_DISABLE_PERMISSION_CHECKS'): + return (search, urlkwargs) + + # Cannot suggest a record which is the current collection or the current + # collection is one of the parents. + if '.suggest' in request.args.get('q', + '') and request.args.get('currentPid'): + search = search.query( + Q('bool', + must_not=[Q('match', + path='/' + request.args.get('currentPid'))])) + + # Records are not filtered for superusers. + if current_user_record.is_superuser: + return (search, urlkwargs) + + # For admins, records are filtered by organisation of the current user. + search = search.filter('term', + organisation__pid=current_organisation['pid']) + + return (search, urlkwargs) diff --git a/sonar/modules/collections/receivers.py b/sonar/modules/collections/receivers.py new file mode 100644 index 000000000..ba167742d --- /dev/null +++ b/sonar/modules/collections/receivers.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +# +# Swiss Open Access Repository +# Copyright (C) 2021 RERO +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +"""Signal receivers for collections.""" + + +def enrich_collection_data(sender=None, + record=None, + json=None, + index=None, + **kwargs): + """Receive a signal before record is indexed, to enrich the data. + + :param sender: Sender of the signal. + :param Record record: Record to index. + :param dict json: JSON that will be indexed. + :param str index: Name of the index in which record will be sent. + """ + if not index.startswith('collections'): + return + + def _build_path(parent, path): + """Recursive call to build the path from parent node to leaf. + + :param parent: Parent item. + :param path: List of PIDs. + """ + if not parent: + return + + path.insert(0, parent['pid']) + _build_path(parent.get('parent'), path) + + # Make the pass + path = [json['pid']] + _build_path(json.get('parent'), path) + path.insert(0, '') + json['path'] = '/'.join(path) diff --git a/sonar/modules/collections/schemas.py b/sonar/modules/collections/schemas.py new file mode 100644 index 000000000..20a0257fb --- /dev/null +++ b/sonar/modules/collections/schemas.py @@ -0,0 +1,116 @@ +# -*- coding: utf-8 -*- +# +# Swiss Open Access Repository +# Copyright (C) 2021 RERO +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +"""Marshmallow schemas.""" + +from functools import partial + +from invenio_records_rest.schemas import StrictKeysMixin +from invenio_records_rest.schemas.fields import GenFunction, \ + PersistentIdentifier, SanitizedUnicode +from marshmallow import fields, pre_dump, pre_load + +from sonar.modules.serializers import schema_from_context +from sonar.modules.users.api import current_user_record +from sonar.modules.utils import get_language_value + +from .api import Record +from .permissions import RecordPermission + +schema_from_record = partial(schema_from_context, schema=Record.schema) + + +class RecordMetadataSchema(StrictKeysMixin): + """Schema for record metadata.""" + + pid = PersistentIdentifier() + name = fields.List(fields.Dict(), required=True) + description = fields.List(fields.Dict()) + organisation = fields.Dict() + parent = fields.Dict() + permissions = fields.Dict(dump_only=True) + label = fields.Method('get_label') + # When loading, if $schema is not provided, it's retrieved by + # Record.schema property. + schema = GenFunction(load_only=True, + attribute="$schema", + data_key="$schema", + deserialize=schema_from_record) + _files = fields.List(fields.Dict()) + _bucket = SanitizedUnicode() + + def get_label(self, obj): + """Get label.""" + return get_language_value(obj['name']) + + @pre_load + def remove_fields(self, data, **kwargs): + """Removes computed fields. + + :param dict data: Record data + :return: Modified data + :rtype: dict + """ + data.pop('permissions', None) + data.pop('label', None) + + return data + + @pre_load + def guess_organisation(self, data, **kwargs): + """Guess organisation from current logged user. + + :param dict data: Record data + :return: Modified data + :rtype: dict + """ + # Organisation already attached to project, we do nothing. + if data.get('organisation'): + return data + + # Store current user organisation in new project. + if current_user_record.get('organisation'): + data['organisation'] = current_user_record['organisation'] + + return data + + @pre_dump + def add_permissions(self, item, **kwargs): + """Add permissions to record. + + :param dict item: Record data + :return: Modified item + :rtype: dict + """ + item['permissions'] = { + 'read': RecordPermission.read(current_user_record, item), + 'update': RecordPermission.update(current_user_record, item), + 'delete': RecordPermission.delete(current_user_record, item) + } + + return item + + +class RecordSchema(StrictKeysMixin): + """Schema for record.""" + + metadata = fields.Nested(RecordMetadataSchema) + created = fields.Str(dump_only=True) + updated = fields.Str(dump_only=True) + links = fields.Dict(dump_only=True) + id = PersistentIdentifier() + explanation = fields.Raw(dump_only=True) diff --git a/sonar/modules/collections/serializers/__init__.py b/sonar/modules/collections/serializers/__init__.py new file mode 100644 index 000000000..017e32c48 --- /dev/null +++ b/sonar/modules/collections/serializers/__init__.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# +# Swiss Open Access Repository +# Copyright (C) 2021 RERO +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +"""Serializers.""" + +from invenio_records_rest.serializers.response import record_responsify, \ + search_responsify + +from sonar.modules.serializers import JSONSerializer + +from ..schemas import RecordSchema + +# Serializers +# =========== +#: JSON serializer definition. +json_v1 = JSONSerializer(RecordSchema) + +# Records-REST serializers +# ======================== +#: JSON record serializer for individual records. +json_v1_response = record_responsify(json_v1, 'application/json') +#: JSON record serializer for search results. +json_v1_search = search_responsify(json_v1, 'application/json') + +__all__ = ( + 'json_v1', + 'json_v1_response', + 'json_v1_search', +) diff --git a/sonar/modules/collections/templates/collections/index.html b/sonar/modules/collections/templates/collections/index.html new file mode 100644 index 000000000..f9ffe7de0 --- /dev/null +++ b/sonar/modules/collections/templates/collections/index.html @@ -0,0 +1,34 @@ +{# -*- coding: utf-8 -*- +Swiss Open Access Repository +Copyright (C) 2021 RERO + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, version 3 of the License. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see +. +#} +{%- extends config.RECORDS_UI_BASE_TEMPLATE %} + +{%- block body %} +

{{ _('Collections') }}

+{% if records | length %} +
    + {% for record in records %} +
  • + {% set collection = record.to_dict() %} + {% include 'collections/item.html' %} +
  • + {% endfor %} +
+{% else %} +
{{ _('No collection found') }}
+{% endif %} +{% endblock %} diff --git a/sonar/modules/collections/templates/collections/item.html b/sonar/modules/collections/templates/collections/item.html new file mode 100644 index 000000000..208faf672 --- /dev/null +++ b/sonar/modules/collections/templates/collections/item.html @@ -0,0 +1,56 @@ +{# -*- coding: utf-8 -*- +Swiss Open Access Repository +Copyright (C) 2021 RERO + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, version 3 of the License. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see +. +#} +{% set name = collection.name | language_value %} +
+ {% set thumbnail = collection | record_image_url %} + {% if thumbnail %} +
+ {{ name }} +
+ {% endif %} +
+

{{ name }}

+ {% if collection.description %} +
+ {{ collection.description | language_value | safe }} +
+ {% endif %} + +
+
diff --git a/sonar/modules/collections/views.py b/sonar/modules/collections/views.py new file mode 100644 index 000000000..1f391e8a1 --- /dev/null +++ b/sonar/modules/collections/views.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- +# +# Swiss Open Access Repository +# Copyright (C) 2021 RERO +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +"""Collections views.""" + +from elasticsearch_dsl import Q +from flask import Blueprint, abort, current_app, redirect, render_template, \ + url_for + +from sonar.modules.collections.api import RecordSearch +from sonar.modules.documents.api import DocumentSearch + +blueprint = Blueprint('collections', + __name__, + template_folder='templates', + url_prefix='//collections') + + +@blueprint.route('') +def index(**kwargs): + r"""Collection index view. + + :param \*\*kwargs: Additional view arguments based on URL rule. + :returns: The rendered template. + """ + # No collection for global view. + if kwargs.get('view') == current_app.config.get( + 'SONAR_APP_DEFAULT_ORGANISATION'): + abort(404) + + records = RecordSearch().filter('term', + organisation__pid=kwargs['view']).scan() + + def filter_result(item): + """Keep only collections that have documents attached.""" + documents = DocumentSearch().query( + Q('nested', + path='collections', + query=Q('bool', must=Q('term', collections__pid=item['pid'])))) + + return documents.count() + + return render_template('collections/index.html', + records=list(filter(filter_result, records))) + + +def detail(pid, record, **kwargs): + r"""Collection detail view. + + :param pid: PID object. + :param record: Record object. + :param template: Template to render. + :param \*\*kwargs: Additional view arguments based on URL rule. + :returns: Redirection to the documents search with collection context. + """ + record = record.replace_refs() + + # Only accessible in organisation's view. + if record['organisation']['pid'] != kwargs.get('view'): + abort(404) + + return redirect( + url_for('documents.search', + view=kwargs.get('view'), + collection_view=record['pid'])) diff --git a/sonar/modules/deposits/api.py b/sonar/modules/deposits/api.py index 547c586dd..b9f5305ae 100644 --- a/sonar/modules/deposits/api.py +++ b/sonar/modules/deposits/api.py @@ -24,6 +24,7 @@ from flask import g from sonar.modules.api import SonarRecord +from sonar.modules.collections.api import Record as CollectionRecord from sonar.modules.documents.api import DocumentRecord from sonar.modules.users.api import current_user_record from sonar.proxies import sonar @@ -102,6 +103,7 @@ def log_action(self, user, action, comment=None): def create_document(self): """Create document from deposit.""" + # TODO : Do this whole process with a marshmallow schema serializer. metadata = {} # Organisation @@ -225,10 +227,27 @@ def create_document(self): 'publicNote': link['publicNote'] } for link in self['metadata']['otherElectronicVersions']] - # Specific collections - if self['metadata'].get('specificCollections'): - metadata['specificCollections'] = self['metadata'][ - 'specificCollections'] + # Collections + if self['metadata'].get('collections'): + collections = [] + for collection in self['metadata'].get('collections'): + # Create a new project + if not collection.get('$ref'): + data = collection.copy() + # Store organisation + data['organisation'] = current_user_record['organisation'] + collection_record = CollectionRecord.create(data) + collection_record.reindex() + collection = { + '$ref': + SonarRecord.get_ref_link('collections', + collection_record['pid']) + } + + collections.append(collection) + + if collections: + metadata['collections'] = collections # Classification if self['metadata'].get('classification'): diff --git a/sonar/modules/deposits/jsonschemas/deposits/deposit-v1.0.0_src.json b/sonar/modules/deposits/jsonschemas/deposits/deposit-v1.0.0_src.json index fa809dc08..e4788ab17 100644 --- a/sonar/modules/deposits/jsonschemas/deposits/deposit-v1.0.0_src.json +++ b/sonar/modules/deposits/jsonschemas/deposits/deposit-v1.0.0_src.json @@ -270,7 +270,7 @@ "publication", "dissertation", "otherElectronicVersions", - "specificCollections", + "collections", "classification", "abstracts", "subjects" @@ -655,13 +655,110 @@ } } }, - "specificCollections": { - "title": "Specific collections", + "collections": { + "title": "Collections", "description": "The names of the organisation's specific/patrimonial collections to which this document belongs", "type": "array", "items": { - "type": "string", - "minLength": 1 + "title": "Collection", + "type": "object", + "additionnalProperties": false, + "oneOf": [ + { + "title": "Existing collection", + "properties": { + "$ref": { + "type": "string", + "pattern": "^https://sonar.ch/api/collections/.*?$", + "form": { + "remoteTypeahead": { + "type": "collections", + "field": "name.value.suggest", + "label": "label" + } + } + } + }, + "required": [ + "$ref" + ] + }, + { + "title": "Add a new collection", + "properties": { + "name": { + "title": "Names", + "type": "array", + "minItems": 1, + "items": { + "title": "Name", + "type": "object", + "additionalProperties": false, + "properties": { + "value": { + "title": "Value", + "type": "string", + "minLength": 1 + }, + "language": { + "$ref": "language-v1.0.0.json" + } + }, + "propertiesOrder": [ + "language", + "value" + ], + "required": [ + "value", + "language" + ] + } + + }, + "description": { + "title": "Descriptions", + "type": "array", + "minItems": 0, + "items": { + "title": "Description", + "type": "object", + "additionalProperties": false, + "properties": { + "value": { + "title": "Value", + "type": "string", + "minLength": 1, + "form": { + "type": "textarea", + "templateOptions": { + "rows": 5 + } + } + }, + "language": { + "$ref": "language-v1.0.0.json" + } + }, + "propertiesOrder": [ + "language", + "value" + ], + "required": [ + "value", + "language" + ] + } + } + }, + "propertiesOrder": [ + "name", + "description" + ], + "required": [ + "name" + ] + } + ] }, "form": { "templateOptions": { diff --git a/sonar/modules/deposits/mappings/v7/deposits/deposit-v1.0.0.json b/sonar/modules/deposits/mappings/v7/deposits/deposit-v1.0.0.json index bf46d5863..da38d5ea3 100644 --- a/sonar/modules/deposits/mappings/v7/deposits/deposit-v1.0.0.json +++ b/sonar/modules/deposits/mappings/v7/deposits/deposit-v1.0.0.json @@ -165,8 +165,32 @@ } } }, - "specificCollections": { - "type": "text" + "collections": { + "type": "object", + "properties": { + "name": { + "type": "object", + "properties": { + "language": { + "type": "keyword" + }, + "value": { + "type": "text" + } + } + }, + "description": { + "type": "object", + "properties": { + "language": { + "type": "keyword" + }, + "value": { + "type": "text" + } + } + } + } }, "classification": { "type": "keyword" diff --git a/sonar/modules/documents/dojson/rerodoc/model.py b/sonar/modules/documents/dojson/rerodoc/model.py index 053a4d107..bda7a9859 100644 --- a/sonar/modules/documents/dojson/rerodoc/model.py +++ b/sonar/modules/documents/dojson/rerodoc/model.py @@ -17,11 +17,14 @@ """RERODOC MARC21 model definition.""" +import hashlib import re from dojson import utils from flask import current_app +from invenio_db import db +from sonar.modules.collections.api import Record as CollectionRecord from sonar.modules.documents.dojson.rerodoc.overdo import Overdo from sonar.modules.organisations.api import OrganisationRecord from sonar.modules.utils import remove_trailing_punctuation @@ -590,12 +593,47 @@ def marc21_to_other_edition(self, key, value): } -@overdo.over('specificCollections', '^982..') +@overdo.over('collections', '^982..') @utils.for_each_value @utils.ignore_value def marc21_to_specific_collection(self, key, value): """Extract collection for record.""" - return value.get('a') + if not value.get('a'): + return None + + # No organisation found, the collection is not imported. + if not self.get('organisation'): + return None + + organisation_pid = OrganisationRecord.get_pid_by_ref_link( + self['organisation'][0]['$ref']) + + hash_key = hashlib.md5( + (value.get('a') + organisation_pid).encode()).hexdigest() + + collection_pid = CollectionRecord.get_pid_by_hash_key(hash_key) + + # No collection found + if not collection_pid: + collection = CollectionRecord.create( + { + 'name': [{ + 'language': 'eng', + 'value': value.get('a') + }], + 'organisation': { + '$ref': self['organisation'][0]['$ref'] + }, + 'hashKey': hash_key + }) + collection.commit() + collection.reindex() + db.session.commit() + collection_pid = collection['pid'] + + return { + '$ref': CollectionRecord.get_ref_link('collections', collection_pid) + } @overdo.over('classification', '^080..') diff --git a/sonar/modules/documents/dojson/rerodoc/overdo.py b/sonar/modules/documents/dojson/rerodoc/overdo.py index 1b7699770..6f1402334 100644 --- a/sonar/modules/documents/dojson/rerodoc/overdo.py +++ b/sonar/modules/documents/dojson/rerodoc/overdo.py @@ -44,14 +44,12 @@ def create_organisation(organisation_key): if not organisation: # Create organisation record - organisation = OrganisationRecord.create( - { - 'code': organisation_key, - 'name': organisation_key, - 'isShared': False, - 'isDedicated': False - }, - dbcommit=True) + organisation = OrganisationRecord.create({ + 'code': organisation_key, + 'name': organisation_key, + 'isShared': False, + 'isDedicated': False + }) organisation.reindex() @staticmethod diff --git a/sonar/modules/documents/jsonschemas/documents/document-v1.0.0_src.json b/sonar/modules/documents/jsonschemas/documents/document-v1.0.0_src.json index 223856c0f..cef3da7f1 100644 --- a/sonar/modules/documents/jsonschemas/documents/document-v1.0.0_src.json +++ b/sonar/modules/documents/jsonschemas/documents/document-v1.0.0_src.json @@ -991,14 +991,30 @@ "hide": true } }, - "specificCollections": { + "collections": { "title": "Collections", "type": "array", - "minItems": 1, + "minItems": 0, "items": { - "title": "Collection", - "type": "string", - "minLength": 1 + "title": "Collections", + "type": "object", + "additionalProperties": false, + "properties": { + "$ref": { + "type": "string", + "pattern": "^https://sonar.ch/api/collections/.*?$", + "form": { + "remoteTypeahead": { + "type": "collections", + "field": "name.value.suggest", + "label": "label" + } + } + } + }, + "required": [ + "$ref" + ] }, "form": { "hide": true, @@ -1739,8 +1755,7 @@ "form": { "remoteTypeahead": { "type": "projects", - "field": "metadata.name.suggest", - "label": "name" + "field": "metadata.name.suggest" } } } @@ -1872,7 +1887,7 @@ "otherEdition", "provisionActivity", "partOf", - "specificCollections", + "collections", "extent", "formats", "notes", diff --git a/sonar/modules/documents/loaders/schemas/rerodoc.py b/sonar/modules/documents/loaders/schemas/rerodoc.py index e696353d8..92c3d6d38 100644 --- a/sonar/modules/documents/loaders/schemas/rerodoc.py +++ b/sonar/modules/documents/loaders/schemas/rerodoc.py @@ -47,7 +47,7 @@ class RerodocSchema(Marc21Schema): identifiedBy = fields.List(fields.Dict()) subjects = fields.List(fields.Dict()) classification = fields.List(fields.Dict()) - specificCollections = fields.List(SanitizedUnicode()) + collections = fields.List(fields.Dict()) dissertation = fields.Dict() otherEdition = fields.List(fields.Dict()) usageAndAccessPolicy = fields.Dict() diff --git a/sonar/modules/documents/mappings/v7/documents/document-v1.0.0.json b/sonar/modules/documents/mappings/v7/documents/document-v1.0.0.json index 148e212a9..a9b75e487 100644 --- a/sonar/modules/documents/mappings/v7/documents/document-v1.0.0.json +++ b/sonar/modules/documents/mappings/v7/documents/document-v1.0.0.json @@ -309,8 +309,24 @@ } } }, - "specificCollections": { - "type": "keyword" + "collections": { + "type": "nested", + "properties": { + "pid": { + "type": "keyword" + }, + "name": { + "type": "object", + "properties": { + "value": { + "type": "text" + }, + "language": { + "type": "keyword" + } + } + } + } }, "classification": { "type": "object", diff --git a/sonar/modules/documents/marshmallow/json.py b/sonar/modules/documents/marshmallow/json.py index 74daf78f4..d2205ad44 100644 --- a/sonar/modules/documents/marshmallow/json.py +++ b/sonar/modules/documents/marshmallow/json.py @@ -93,7 +93,7 @@ class DocumentMetadataSchemaV1(StrictKeysMixin): identifiedBy = fields.List(fields.Dict()) subjects = fields.List(fields.Dict()) classification = fields.List(fields.Dict()) - specificCollections = fields.List(SanitizedUnicode()) + collections = fields.List(fields.Dict()) dissertation = fields.Dict() otherEdition = fields.List(fields.Dict()) usageAndAccessPolicy = fields.Dict() diff --git a/sonar/modules/documents/query.py b/sonar/modules/documents/query.py index a04a8951a..ff7c58b89 100644 --- a/sonar/modules/documents/query.py +++ b/sonar/modules/documents/query.py @@ -85,6 +85,16 @@ def search_factory(self, search, query_parser=None): if view != current_app.config.get('SONAR_APP_DEFAULT_ORGANISATION'): search = search.filter('term', organisation__pid=view) + # Filter collection + if request.args.get('collection_view'): + search = search.query( + Q('nested', + path='collections', + query=Q( + 'bool', + must=Q( + 'term', + collections__pid=request.args['collection_view'])))) # Admin else: # Filters records by user's organisation diff --git a/sonar/modules/documents/receivers.py b/sonar/modules/documents/receivers.py index a3f1572ad..27e1a497a 100644 --- a/sonar/modules/documents/receivers.py +++ b/sonar/modules/documents/receivers.py @@ -109,6 +109,27 @@ def enrich_document_data(sender=None, # Check if record is open access. json['isOpenAccess'] = record.is_open_access() + # Adds collections for building aggregations. + def _get_collections_pids(collection): + """Builds the list of collections PIDs from parent to leaf. + + :param dict collection: Collection dictionary. + :returns: List of PIDs. + :rtype: list + """ + pids = [collection['pid']] + if collection.get('parent'): + pids = _get_collections_pids(collection['parent']) + pids + + return pids + + for collection in json.get('collections', []): + pids = _get_collections_pids(collection) + i = 0 + while i < len(pids): + collection[f'collection{i}'] = pids[i] + i += 1 + # No files are present in record if not record.files: return diff --git a/sonar/modules/documents/serializers/__init__.py b/sonar/modules/documents/serializers/__init__.py index 4703d09e8..ea7b75ac5 100644 --- a/sonar/modules/documents/serializers/__init__.py +++ b/sonar/modules/documents/serializers/__init__.py @@ -25,6 +25,7 @@ from invenio_records_rest.serializers.response import record_responsify, \ search_responsify +from sonar.modules.collections.api import Record as CollectionRecord from sonar.modules.documents.serializers.dc import SonarDublinCoreSerializer from sonar.modules.documents.serializers.google_scholar import \ SonarGoogleScholarSerializer @@ -97,6 +98,34 @@ def post_process_serialize_search(self, results, pid_fetcher): organisation[f'documentsCustomField{i}'] ['label']) + # Dont display collection aggregation in the collection context + if request.args.get('collection_view'): + results['aggregations'].pop('collection', None) + + if results['aggregations'].get('collection'): + # Normalize collections as it is a nested aggregation. + results['aggregations']['collection'] = results['aggregations'][ + 'collection']['collection'] + + # Add collection name + def _process_collection_level(buckets, level=1): + """Recursively add collection names for each buckets.""" + for bucket in buckets: + collection = CollectionRecord.get_record_by_pid( + bucket['key']) + if collection: + bucket['name'] = collection['name'][0]['value'] + + facet_name = f'collection{level}' + if bucket.get(facet_name): + level += 1 + _process_collection_level( + bucket[facet_name]['buckets'], level) + + _process_collection_level( + results.get('aggregations', {}).get('collection', + {}).get('buckets', [])) + return super(JSONSerializer, self).post_process_serialize_search(results, pid_fetcher) diff --git a/sonar/modules/documents/templates/documents/record.html b/sonar/modules/documents/templates/documents/record.html index 22a80acef..d806433df 100644 --- a/sonar/modules/documents/templates/documents/record.html +++ b/sonar/modules/documents/templates/documents/record.html @@ -282,6 +282,24 @@
{% endif %} {% endfor %} + + {% if record.collections %} +
+ {{ _('Collections') }} +
+
+ +
+ {% endif %} + {% if record.usageAndAccessPolicy %}
diff --git a/sonar/modules/documents/views.py b/sonar/modules/documents/views.py index fb308fe4e..dfca567b7 100644 --- a/sonar/modules/documents/views.py +++ b/sonar/modules/documents/views.py @@ -21,11 +21,12 @@ import json -from flask import Blueprint, current_app, render_template +from flask import Blueprint, current_app, render_template, request from flask_babelex import gettext as _ from invenio_i18n.ext import current_i18n from invenio_records_ui.signals import record_viewed +from sonar.modules.collections.api import Record as CollectionRecord from sonar.modules.documents.utils import has_external_urls_for_files, \ populate_files_properties from sonar.modules.utils import format_date, \ @@ -49,7 +50,13 @@ @blueprint.route('//search/documents') def search(view): """Search results page.""" - return render_template('sonar/search.html') + # Load collection if arg is in URL. + collection = None + if request.args.get('collection_view'): + collection = CollectionRecord.get_record_by_pid( + request.args['collection_view']) + + return render_template('sonar/search.html', collection=collection) def detail(pid, record, template=None, **kwargs): @@ -100,7 +107,6 @@ def detail(pid, record, template=None, **kwargs): google_scholar_data=google_scholar_data) - @blueprint.app_template_filter() def title_format(title, language): """Format title for template. diff --git a/sonar/modules/permissions.py b/sonar/modules/permissions.py index d705c2de7..899d2153d 100644 --- a/sonar/modules/permissions.py +++ b/sonar/modules/permissions.py @@ -23,6 +23,7 @@ from flask_login import current_user from flask_principal import ActionNeed, RoleNeed from invenio_access import Permission +from invenio_records_rest.utils import obj_or_import_string from sonar.modules.users.api import current_user_record @@ -89,6 +90,8 @@ def record_permission_factory(record=None, action=None, cls=None): if not cls: cls = RecordPermission + cls = obj_or_import_string(cls) + return cls.create_permission(record, action) diff --git a/sonar/modules/query.py b/sonar/modules/query.py index 170fe0273..c95e5fa52 100644 --- a/sonar/modules/query.py +++ b/sonar/modules/query.py @@ -125,3 +125,17 @@ def missing_field_filter(field): def inner(values): return Q('bool', must_not=[Q('exists', field=field)]) return inner + + +def collection_filter(field): + """Filter for collections.""" + def inner(values): + must = [] + for value in values: + must.append( + Q('nested', + path='collections', + query=Q('bool', must=Q('term', **{field: value})))) + return Q('bool', must=must) + + return inner diff --git a/sonar/theme/templates/sonar/search.html b/sonar/theme/templates/sonar/search.html index a40644332..6fde7b5aa 100644 --- a/sonar/theme/templates/sonar/search.html +++ b/sonar/theme/templates/sonar/search.html @@ -1,6 +1,10 @@ {% extends 'sonar/page.html' %} {%- block body %} +{% if collection %} +{% include 'collections/item.html' %} +
+{% endif %} {% endblock %} diff --git a/sonar/theme/views.py b/sonar/theme/views.py index 062c38aaf..387b61c66 100644 --- a/sonar/theme/views.py +++ b/sonar/theme/views.py @@ -37,6 +37,8 @@ from invenio_pidstore.models import PersistentIdentifier from sonar.json_schemas.factory import JSONSchemaFactory +from sonar.modules.collections.permissions import \ + RecordPermission as CollectionPermission from sonar.modules.deposits.permissions import DepositPermission from sonar.modules.documents.permissions import DocumentPermission from sonar.modules.organisations.permissions import OrganisationPermission @@ -151,6 +153,10 @@ def logged_user(): 'projects': { 'add': RecordPermissionPolicy('create').can(), 'list': RecordPermissionPolicy('search').can() + }, + 'collections': { + 'add': CollectionPermission.create(user), + 'list': CollectionPermission.list(user) } } diff --git a/tests/api/collections/test_collections_documents_facets.py b/tests/api/collections/test_collections_documents_facets.py new file mode 100644 index 000000000..9e065c3bb --- /dev/null +++ b/tests/api/collections/test_collections_documents_facets.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +# +# Swiss Open Access Repository +# Copyright (C) 2021 RERO +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +"""Test collections facets in documents.""" + +from flask import url_for +from invenio_accounts.testutils import login_user_via_session + + +def test_list(app, db, client, document, collection, superuser): + document['collections'] = [{ + '$ref': + 'https://sonar.ch/api/collections/{pid}'.format(pid=collection['pid']) + }] + document.commit() + db.session.commit() + document.reindex() + + login_user_via_session(client, email=superuser['email']) + res = client.get(url_for('invenio_records_rest.doc_list')) + assert res.status_code == 200 + assert res.json['hits']['total']['value'] == 1 + assert res.json['aggregations']['collection']['buckets'] == [{ + 'key': + '2', + 'doc_count': + 1, + 'collection1': { + 'doc_count_error_upper_bound': + 0, + 'sum_other_doc_count': + 0, + 'buckets': [{ + 'key': '3', + 'doc_count': 1, + 'collection2': { + 'doc_count_error_upper_bound': 0, + 'sum_other_doc_count': 0, + 'buckets': [] + }, + 'name': 'Collection name' + }] + }, + 'name': + 'Parent collection' + }] + + # Test with collection filter + login_user_via_session(client, email=superuser['email']) + res = client.get(url_for('invenio_records_rest.doc_list', collection='2')) + assert res.status_code == 200 + assert res.json['hits']['total']['value'] == 1 + + # Test with sub collection filter + login_user_via_session(client, email=superuser['email']) + res = client.get(url_for('invenio_records_rest.doc_list', collection1='3')) + assert res.status_code == 200 + assert res.json['hits']['total']['value'] == 1 diff --git a/tests/api/collections/test_collections_permissions.py b/tests/api/collections/test_collections_permissions.py new file mode 100644 index 000000000..32fed464c --- /dev/null +++ b/tests/api/collections/test_collections_permissions.py @@ -0,0 +1,340 @@ +# -*- coding: utf-8 -*- +# +# Swiss Open Access Repository +# Copyright (C) 2021 RERO +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +"""Test collections permissions.""" + +import json + +from flask import url_for +from invenio_accounts.testutils import login_user_via_session + + +def test_list(app, client, make_organisation, make_collection, superuser, + admin, moderator, submitter, user): + """Test list collections permissions.""" + make_organisation('org2') + make_collection('org') + make_collection('org2') + + # Not logged + res = client.get(url_for('invenio_records_rest.coll_list')) + assert res.status_code == 401 + + # Not logged but permission checks disabled + app.config.update(SONAR_APP_DISABLE_PERMISSION_CHECKS=True) + res = client.get(url_for('invenio_records_rest.coll_list')) + assert res.status_code == 200 + assert res.json['hits']['total']['value'] == 2 + app.config.update(SONAR_APP_DISABLE_PERMISSION_CHECKS=False) + + # Logged as user + login_user_via_session(client, email=user['email']) + res = client.get(url_for('invenio_records_rest.coll_list')) + assert res.status_code == 403 + + # Logged as submitter + login_user_via_session(client, email=submitter['email']) + res = client.get(url_for('invenio_records_rest.coll_list')) + assert res.status_code == 403 + + # Logged as moderator + login_user_via_session(client, email=moderator['email']) + res = client.get(url_for('invenio_records_rest.coll_list')) + assert res.status_code == 200 + assert res.json['hits']['total']['value'] == 1 + + # Logged as admin + login_user_via_session(client, email=admin['email']) + res = client.get(url_for('invenio_records_rest.coll_list')) + assert res.status_code == 200 + assert res.json['hits']['total']['value'] == 1 + + # Logged as superuser + login_user_via_session(client, email=superuser['email']) + res = client.get(url_for('invenio_records_rest.coll_list')) + assert res.status_code == 200 + assert res.json['hits']['total']['value'] == 2 + + +def test_create(client, superuser, admin, moderator, submitter, user): + """Test create collections permissions.""" + data = {'name': [{'language': 'eng', 'value': 'Name'}]} + + headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + + # Not logged + res = client.post(url_for('invenio_records_rest.coll_list'), + data=json.dumps(data), + headers=headers) + assert res.status_code == 401 + + # User + login_user_via_session(client, email=user['email']) + res = client.post(url_for('invenio_records_rest.coll_list'), + data=json.dumps(data), + headers=headers) + assert res.status_code == 403 + + # submitter + login_user_via_session(client, email=submitter['email']) + res = client.post(url_for('invenio_records_rest.coll_list'), + data=json.dumps(data), + headers=headers) + assert res.status_code == 403 + + # Moderator + login_user_via_session(client, email=moderator['email']) + res = client.post(url_for('invenio_records_rest.coll_list'), + data=json.dumps(data), + headers=headers) + assert res.status_code == 201 + + # Admin + login_user_via_session(client, email=admin['email']) + res = client.post(url_for('invenio_records_rest.coll_list'), + data=json.dumps(data), + headers=headers) + assert res.status_code == 201 + + # Super user + login_user_via_session(client, email=superuser['email']) + res = client.post(url_for('invenio_records_rest.coll_list'), + data=json.dumps(data), + headers=headers) + assert res.status_code == 201 + + +def test_read(client, make_organisation, make_collection, superuser, admin, + moderator, submitter, user): + """Test read collections permissions.""" + make_organisation('org2') + collection1 = make_collection('org') + collection2 = make_collection('org2') + + # Not logged + res = client.get( + url_for('invenio_records_rest.coll_item', + pid_value=collection1['pid'])) + assert res.status_code == 401 + + # Logged as user + login_user_via_session(client, email=user['email']) + res = client.get( + url_for('invenio_records_rest.coll_item', + pid_value=collection1['pid'])) + assert res.status_code == 403 + + # Logged as submitter + login_user_via_session(client, email=submitter['email']) + res = client.get( + url_for('invenio_records_rest.coll_item', + pid_value=collection1['pid'])) + assert res.status_code == 403 + + # Logged as moderator + login_user_via_session(client, email=moderator['email']) + res = client.get( + url_for('invenio_records_rest.coll_item', + pid_value=collection1['pid'])) + assert res.status_code == 200 + + # Logged as admin + login_user_via_session(client, email=admin['email']) + res = client.get( + url_for('invenio_records_rest.coll_item', + pid_value=collection1['pid'])) + assert res.status_code == 200 + assert res.json['metadata']['permissions'] == { + 'delete': True, + 'read': True, + 'update': True + } + + # Logged as admin of other organisation + res = client.get( + url_for('invenio_records_rest.coll_item', + pid_value=collection2['pid'])) + assert res.status_code == 403 + + # Logged as superuser + login_user_via_session(client, email=superuser['email']) + res = client.get( + url_for('invenio_records_rest.coll_item', + pid_value=collection1['pid'])) + assert res.status_code == 200 + + login_user_via_session(client, email=superuser['email']) + res = client.get( + url_for('invenio_records_rest.coll_item', + pid_value=collection2['pid'])) + assert res.status_code == 200 + assert res.json['metadata']['permissions'] == { + 'delete': True, + 'read': True, + 'update': True + } + + +def test_update(client, make_organisation, make_collection, superuser, admin, + moderator, submitter, user): + """Test update collections permissions.""" + headers = { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + + make_organisation('org2') + collection1 = make_collection('org') + collection2 = make_collection('org2') + + # Not logged + res = client.put(url_for('invenio_records_rest.coll_item', + pid_value=collection1['pid']), + data=json.dumps(collection1.dumps()), + headers=headers) + assert res.status_code == 401 + + # Logged as user + login_user_via_session(client, email=user['email']) + res = client.put(url_for('invenio_records_rest.coll_item', + pid_value=collection1['pid']), + data=json.dumps(collection1.dumps()), + headers=headers) + assert res.status_code == 403 + + # Logged as submitter + login_user_via_session(client, email=submitter['email']) + res = client.put(url_for('invenio_records_rest.coll_item', + pid_value=collection1['pid']), + data=json.dumps(collection1.dumps()), + headers=headers) + assert res.status_code == 403 + + # Logged as moderator + login_user_via_session(client, email=moderator['email']) + res = client.put(url_for('invenio_records_rest.coll_item', + pid_value=collection1['pid']), + data=json.dumps(collection1.dumps()), + headers=headers) + assert res.status_code == 200 + + # Logged as admin + login_user_via_session(client, email=admin['email']) + res = client.put(url_for('invenio_records_rest.coll_item', + pid_value=collection1['pid']), + data=json.dumps(collection1.dumps()), + headers=headers) + assert res.status_code == 200 + + # Logged as admin of other organisation + res = client.put(url_for('invenio_records_rest.coll_item', + pid_value=collection2['pid']), + data=json.dumps(collection2.dumps()), + headers=headers) + assert res.status_code == 403 + + # Logged as superuser + login_user_via_session(client, email=superuser['email']) + res = client.put(url_for('invenio_records_rest.coll_item', + pid_value=collection1['pid']), + data=json.dumps(collection1.dumps()), + headers=headers) + assert res.status_code == 200 + + login_user_via_session(client, email=superuser['email']) + res = client.put(url_for('invenio_records_rest.coll_item', + pid_value=collection1['pid']), + data=json.dumps(collection1.dumps()), + headers=headers) + assert res.status_code == 200 + + +def test_delete(client, db, document, collection, make_organisation, + make_collection, superuser, admin, moderator, submitter, user): + """Test delete collections permissions.""" + # Not logged + res = client.delete( + url_for('invenio_records_rest.coll_item', pid_value=collection['pid'])) + assert res.status_code == 401 + + # Logged as user + login_user_via_session(client, email=user['email']) + res = client.delete( + url_for('invenio_records_rest.coll_item', pid_value=collection['pid'])) + assert res.status_code == 403 + + # Logged as submitter + login_user_via_session(client, email=submitter['email']) + res = client.delete( + url_for('invenio_records_rest.coll_item', pid_value=collection['pid'])) + assert res.status_code == 403 + + # Logged as moderator + login_user_via_session(client, email=moderator['email']) + res = client.delete( + url_for('invenio_records_rest.coll_item', pid_value=collection['pid'])) + assert res.status_code == 204 + + make_organisation('org2') + collection2 = make_collection('org2') + + # Cannot remove collection from other organisation + res = client.delete( + url_for('invenio_records_rest.coll_item', + pid_value=collection2['pid'])) + assert res.status_code == 403 + + collection = make_collection('org') + + # Logged as admin + login_user_via_session(client, email=admin['email']) + res = client.delete( + url_for('invenio_records_rest.coll_item', pid_value=collection['pid'])) + assert res.status_code == 204 + + collection = make_collection('org') + + # Logged as superuser + login_user_via_session(client, email=superuser['email']) + res = client.delete( + url_for('invenio_records_rest.coll_item', pid_value=collection['pid'])) + assert res.status_code == 204 + + # Can remove any collection + login_user_via_session(client, email=superuser['email']) + res = client.delete( + url_for('invenio_records_rest.coll_item', + pid_value=collection2['pid'])) + assert res.status_code == 204 + + collection = make_collection('org') + + # Cannot remove collection as it is linked to document. + document['collections'] = [{ + '$ref': + 'https://sonar.ch/api/collections/{pid}'.format(pid=collection['pid']) + }] + document.commit() + db.session.commit() + document.reindex() + + res = client.delete( + url_for('invenio_records_rest.coll_item', pid_value=collection['pid'])) + assert res.status_code == 403 diff --git a/tests/api/collections/test_collections_suggestions.py b/tests/api/collections/test_collections_suggestions.py new file mode 100644 index 000000000..37d22cb07 --- /dev/null +++ b/tests/api/collections/test_collections_suggestions.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# +# Swiss Open Access Repository +# Copyright (C) 2021 RERO +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +"""Test collections suggestions.""" + +from flask import url_for +from invenio_accounts.testutils import login_user_via_session + + +def test_suggestions(client, admin, collection, make_collection): + """Test list collections permissions.""" + make_collection('org') + + login_user_via_session(client, email=admin['email']) + + # 2 results + res = client.get(url_for('invenio_records_rest.coll_list')) + assert res.status_code == 200 + assert res.json['hits']['total']['value'] == 2 + + # Suggestions does not return the collection corresponding to current PID + res = client.get( + url_for('invenio_records_rest.coll_list', + q='name.value.suggest:Collection name', + currentPid=collection['pid'])) + assert res.status_code == 200 + assert res.json['hits']['total']['value'] == 1 diff --git a/tests/api/deposits/test_deposits_rest.py b/tests/api/deposits/test_deposits_rest.py index de8abea25..033f14333 100644 --- a/tests/api/deposits/test_deposits_rest.py +++ b/tests/api/deposits/test_deposits_rest.py @@ -20,6 +20,7 @@ import json from flask import url_for +from invenio_accounts.testutils import login_user_via_view from sonar.modules.deposits.rest import FilesResource from sonar.modules.users.api import UserRecord @@ -121,6 +122,10 @@ def test_publish(client, db, user, moderator, deposit): response = client.post(url, data={}) assert response.status_code == 400 + login_user_via_view(client, + email=moderator['email'], + password='123456') + # Test the publication by a moderator deposit['status'] = 'in_progress' deposit.commit() @@ -176,6 +181,10 @@ def test_review(client, db, user, moderator, deposit): headers=headers) assert response.status_code == 403 + login_user_via_view(client, + email=moderator['email'], + password='123456') + # Valid approval request response = client.post(url, data=json.dumps({ diff --git a/tests/api/documents/test_documents_query.py b/tests/api/documents/test_documents_query.py new file mode 100644 index 000000000..1511ae52a --- /dev/null +++ b/tests/api/documents/test_documents_query.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# +# Swiss Open Access Repository +# Copyright (C) 2021 RERO +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +"""Test documents query.""" + +from flask import url_for + + +def test_collection_query(db, client, document, collection): + document['collections'] = [{ + '$ref': + 'https://sonar.ch/api/collections/{pid}'.format(pid=collection['pid']) + }] + document.commit() + db.session.commit() + document.reindex() + + res = client.get( + url_for('invenio_records_rest.doc_list', + view='org', + collection_view=collection['pid'])) + assert res.status_code == 200 + assert res.json['hits']['total']['value'] == 1 + assert not res.json['aggregations'].get('collection') diff --git a/tests/api/monitoring/test_monitoring_views.py b/tests/api/monitoring/test_monitoring_views.py index c2d0f5720..41b2a4350 100644 --- a/tests/api/monitoring/test_monitoring_views.py +++ b/tests/api/monitoring/test_monitoring_views.py @@ -180,6 +180,12 @@ def test_data_info(client, es_clear, superuser, document, monkeypatch): 'es': 1, 'db-es': 0, 'index': 'users' + }, + 'coll': { + 'db': 0, + 'es': 0, + 'db-es': 0, + 'index': 'collections' } } } diff --git a/tests/conftest.py b/tests/conftest.py index 17b4a4404..71ee9710c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -28,6 +28,7 @@ from invenio_accounts.ext import hash_password from invenio_files_rest.models import Location +from sonar.modules.collections.api import Record as CollectionRecord from sonar.modules.deposits.api import DepositRecord from sonar.modules.documents.api import DocumentRecord from sonar.modules.organisations.api import OrganisationRecord @@ -428,7 +429,7 @@ def document_with_file(make_document): @pytest.fixture() -def deposit_json(): +def deposit_json(collection): """Deposit JSON.""" return { '$schema': @@ -473,7 +474,15 @@ def deposit_json(): 'publicNote': 'Published version', 'url': 'https://some.url/document.pdf' }], - 'specificCollections': ['Collection 1', 'Collection 2'], + 'collections': [{ + '$ref': + f'https://sonar.ch/api/collections/{collection["pid"]}' + }, { + 'name': [{ + 'language': 'eng', + 'value': 'New collection' + }] + }], 'classification': '543', 'abstracts': [{ @@ -691,6 +700,79 @@ def project(app, db, es, admin, organisation, project_json): return project +@pytest.fixture() +def collection_json(): + """Collection JSON.""" + return { + 'name': [{ + 'language': 'eng', + 'value': 'Collection name' + }], + 'description': [{ + 'language': 'eng', + 'value': 'Collection description' + }] + } + + +@pytest.fixture() +def make_collection(app, db, collection_json): + """Factory for creating collection.""" + + def _make_collection(organisation=None): + collection_json['organisation'] = { + '$ref': + 'https://sonar.ch/api/organisations/{pid}'.format(pid=organisation) + } + + collection_json.pop('pid', None) + + collection = CollectionRecord.create(collection_json, + dbcommit=True, + with_bucket=True) + collection.commit() + collection.reindex() + db.session.commit() + return collection + + return _make_collection + + +@pytest.fixture() +def collection(app, db, es, admin, organisation, collection_json): + """Collection fixture.""" + parent_collection_json = { + 'name': [{ + 'language': 'eng', + 'value': 'Parent collection' + }] + } + parent_collection = CollectionRecord.create(parent_collection_json, + dbcommit=True, + with_bucket=True) + parent_collection.commit() + parent_collection.reindex() + db.session.commit() + + json = copy.deepcopy(collection_json) + json['organisation'] = { + '$ref': + 'https://sonar.ch/api/organisations/{pid}'.format( + pid=organisation['pid']) + } + json['parent'] = { + '$ref': + 'https://sonar.ch/api/collections/{pid}'.format( + pid=parent_collection['pid']) + } + + collection = CollectionRecord.create(json, dbcommit=True, with_bucket=True) + collection.commit() + collection.reindex() + db.session.commit() + return collection + + @pytest.fixture() def bucket_location(app, db): """Create a default location for managing files.""" diff --git a/tests/ui/collections/test_collections_views.py b/tests/ui/collections/test_collections_views.py new file mode 100644 index 000000000..6a3a4e44d --- /dev/null +++ b/tests/ui/collections/test_collections_views.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +# +# Swiss Open Access Repository +# Copyright (C) 2021 RERO +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +"""Test collections views.""" + +from flask import url_for + + +def test_index(app, db, client, document, organisation, collection): + """Test list of collections.""" + # No collection index for global view + assert client.get(url_for('collections.index', + view='global')).status_code == 404 + + # OK in organisation context but no collection listed because there's no + # document linked. + result = client.get(url_for('collections.index', view='org')) + assert result.status_code == 200 + assert b'No collection found' in result.data + + # OK in organisation context and the collection has a document linked. + document['collections'] = [{ + '$ref': + 'https://sonar.ch/api/collections/{pid}'.format(pid=collection['pid']) + }] + document.commit() + db.session.commit() + document.reindex() + result = client.get(url_for('collections.index', view='org')) + assert result.status_code == 200 + assert b'

Collection name

' in result.data + + +def test_detail(app, client, organisation, collection): + """Test collection detail.""" + # No detail view in global context + assert client.get( + url_for('invenio_records_ui.coll', + view='global', + pid_value=collection['pid'])).status_code == 404 + + # OK in organisation context + result = client.get( + url_for('invenio_records_ui.coll', + view='org', + pid_value=collection['pid'])) + assert result.status_code == 302 + assert result.location.find(f'collection_view={collection["pid"]}') != -1 diff --git a/tests/ui/deposits/test_deposits_api.py b/tests/ui/deposits/test_deposits_api.py index 70f4a127f..658552d32 100644 --- a/tests/ui/deposits/test_deposits_api.py +++ b/tests/ui/deposits/test_deposits_api.py @@ -106,7 +106,7 @@ def test_create_document(app, db, project, client, deposit, submitter): }, 'publicNote': 'Published version' }] - assert document['specificCollections'] == ['Collection 1', 'Collection 2'] + assert len(document['collections']) == 2 assert document['classification'] == [{ 'type': 'bf:ClassificationUdc', 'classificationPortion': '543' diff --git a/tests/ui/documents/dojson/rerodoc/test_rerodoc_model.py b/tests/ui/documents/dojson/rerodoc/test_rerodoc_model.py index 2c8c77cdc..97804b850 100644 --- a/tests/ui/documents/dojson/rerodoc/test_rerodoc_model.py +++ b/tests/ui/documents/dojson/rerodoc/test_rerodoc_model.py @@ -1615,55 +1615,74 @@ def test_marc21_to_other_edition(): assert not data.get('otherEdition') -def test_marc21_to_specific_collection(): +def test_marc21_to_specific_collection(app, bucket_location, + without_oaiset_signals): """Test extracting collection from file 982.""" - # Extract collection OK + # No code a marc21xml = """ - Treize étoiles + Treize étoiles """ marc21json = create_record(marc21xml) data = overdo.do(marc21json) - assert data.get('specificCollections') == ['Treize étoiles'] + assert not data.get('collections') - # Multiple collections + # Not field 982 + marc21xml = """ + + + """ + marc21json = create_record(marc21xml) + data = overdo.do(marc21json) + assert not data.get('collections') + + # No organisation marc21xml = """ - Collection 1 - - - Collection 2 + Treize étoiles """ marc21json = create_record(marc21xml) data = overdo.do(marc21json) - assert data.get('specificCollections') == ['Collection 1', 'Collection 2'] + assert not data.get('collections') - # No code a + # OK marc21xml = """ + + test-org + - Treize étoiles + Treize étoiles """ marc21json = create_record(marc21xml) data = overdo.do(marc21json) - assert not data.get('specificCollections') + assert data['collections'] - # Not field 982 + # Multiple collections marc21xml = """ + + test-org + + + Collection 1 + + + Collection 2 + """ marc21json = create_record(marc21xml) data = overdo.do(marc21json) - assert not data.get('specificCollections') + assert len(data['collections']) == 2 def test_marc21_to_classification_from_field_080(): diff --git a/tests/ui/documents/test_documents_views.py b/tests/ui/documents/test_documents_views.py index 807d7639b..a6178d83c 100644 --- a/tests/ui/documents/test_documents_views.py +++ b/tests/ui/documents/test_documents_views.py @@ -52,11 +52,21 @@ def test_index(client): assert client.get('/').status_code == 200 -def test_search(app, client): +def test_search(app, client, organisation, collection): """Test search.""" assert client.get( url_for('documents.search', view='global', resource_type='documents')).status_code == 200 + + # Test search with collection + result = client.get( + url_for('documents.search', + view=organisation['pid'], + collection_view=collection['pid'], + resource_type='documents')) + assert result.status_code == 200 + assert result.data.find(b'

Collection name

') != -1 + assert client.get( url_for('documents.search', view='not-existing', diff --git a/tests/unit/documents/loaders/test_rerodoc_loader.py b/tests/unit/documents/loaders/test_rerodoc_loader.py index 57602fa95..fe4d8ad82 100644 --- a/tests/unit/documents/loaders/test_rerodoc_loader.py +++ b/tests/unit/documents/loaders/test_rerodoc_loader.py @@ -135,7 +135,9 @@ def test_rerodoc_loader(app, organisation): 'value': '(NATIONALLICENCE)oxford-10.1093/mnras/stu2500' }], - 'specificCollections': ['National Licences: uzh'], + 'collections': [{ + '$ref': 'https://sonar.ch/api/collections/1' + }], 'usageAndAccessPolicy': { 'label': '© 2015 The Authors Published by Oxford University Press on '