diff --git a/.gitignore b/.gitignore index ac4263da3..ac6c79ab1 100644 --- a/.gitignore +++ b/.gitignore @@ -74,6 +74,7 @@ target/ sonar/modules/documents/jsonschemas/documents/document-v1.0.0.json sonar/modules/deposits/jsonschemas/deposits/deposit-v1.0.0.json sonar/resources/projects/jsonschemas/projects/project-v1.0.0.json +sonar/dedicated/*/*/jsonschemas/*/*/*-v1.0.0.json # Generated JSON files data/backups/ diff --git a/scripts/bootstrap b/scripts/bootstrap index 83d10eb12..494c64ba1 100755 --- a/scripts/bootstrap +++ b/scripts/bootstrap @@ -91,6 +91,7 @@ section "Compile JSON schemas" "info" 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/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 # Compile translations catalogs section "Compile translations catalogs" "info" diff --git a/setup.py b/setup.py index 5bc8703d5..6e8d66f8f 100644 --- a/setup.py +++ b/setup.py @@ -105,6 +105,7 @@ 'users = sonar.modules.users.jsonschemas', 'deposits = sonar.modules.deposits.jsonschemas', 'projects = sonar.resources.projects.jsonschemas', + 'projects_hepvs = sonar.dedicated.hepvs.projects.jsonschemas', 'common = sonar.common.jsonschemas' ], 'invenio_search.mappings': [ diff --git a/sonar/config_sonar.py b/sonar/config_sonar.py index 21f67909b..03b73b6c3 100644 --- a/sonar/config_sonar.py +++ b/sonar/config_sonar.py @@ -17,7 +17,6 @@ """Specific configuration SONAR.""" - SONAR_APP_API_URL = 'https://localhost:5000/api/' SONAR_APP_ANGULAR_URL = 'https://localhost:5000/manage/' @@ -66,10 +65,16 @@ ] """List of extensions for which files can be previewed.""" - SONAR_APP_WEBDAV_HEG_HOST = 'https://share.rero.ch/HEG' SONAR_APP_WEBDAV_HEG_USER = None SONAR_APP_WEBDAV_HEG_PASSWORD = None """Connection data to webdav for HEG.""" SONAR_APP_HEG_DATA_DIRECTORY = './data/heg' + +SONAR_APP_ORGANISATION_CONFIG = { + 'hepvs': { + 'projects': True + } +} +# Custom resources for organisations diff --git a/sonar/dedicated/hepvs/projects/__init__.py b/sonar/dedicated/hepvs/projects/__init__.py new file mode 100644 index 000000000..c0b2577c4 --- /dev/null +++ b/sonar/dedicated/hepvs/projects/__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 . + +"""RERO specific project resource.""" diff --git a/sonar/dedicated/hepvs/projects/jsonschemas/__init__.py b/sonar/dedicated/hepvs/projects/jsonschemas/__init__.py new file mode 100644 index 000000000..167f42929 --- /dev/null +++ b/sonar/dedicated/hepvs/projects/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 . + +"""JSONSchema directory for projects.""" diff --git a/sonar/dedicated/hepvs/projects/jsonschemas/hepvs/projects/project-v1.0.0_src.json b/sonar/dedicated/hepvs/projects/jsonschemas/hepvs/projects/project-v1.0.0_src.json new file mode 100644 index 000000000..a51500b63 --- /dev/null +++ b/sonar/dedicated/hepvs/projects/jsonschemas/hepvs/projects/project-v1.0.0_src.json @@ -0,0 +1,942 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "https://sonar.ch/schemas/projects/project-v1.0.0.json", + "title": "Research project", + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "title": "Identifier", + "type": "string", + "minLength": 1 + }, + "$schema": { + "title": "Schema", + "type": "string", + "minLength": 1 + }, + "pid": { + "title": "Persistent identifier", + "type": "object", + "additionalProperties": false, + "properties": { + "obj_type": { + "title": "Object type", + "type": "string", + "minLength": 1 + }, + "pid_type": { + "title": "PID type", + "type": "string", + "minLength": 1 + }, + "pk": { + "title": "Primary key", + "type": "integer", + "minLength": 1 + }, + "status": { + "title": "Status", + "type": "string", + "minLength": 1 + } + } + }, + "metadata": { + "type": "object", + "additionalProperties": false, + "properties": { + "$schema": { + "type": "string", + "default": "https://sonar.ch/schemas/projects/project-v1.0.0.json" + }, + "pid": { + "title": "Identifier", + "type": "string", + "minLength": 1 + }, + "name": { + "title": "Name", + "type": "string", + "minLength": 1 + }, + "description": { + "title": "Résumé du projet (250 mots)", + "type": "string", + "minLength": 1, + "form": { + "type": "textarea", + "templateOptions": { + "rows": 5, + "attributes": { + "maxlength": 250 + } + } + } + }, + "startDate": { + "title": "Start date", + "description": "Enter the date in the format YYYY-MM-DD.", + "type": "string", + "format": "date", + "pattern": "^[0-9]{4}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])$", + "form": { + "type": "datepicker", + "templateOptions": { + "placeholder": "Example: 2020-12-01" + } + } + }, + "endDate": { + "title": "End date", + "description": "Enter the date in the format YYYY-MM-DD.", + "type": "string", + "format": "date", + "pattern": "^[0-9]{4}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])$", + "form": { + "type": "datepicker", + "templateOptions": { + "placeholder": "Example: 2020-12-01" + } + } + }, + "identifiedBy": { + "title": "Identifier", + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "title": "Type", + "type": "string", + "enum": [ + "bf:Identifier", + "bf:Local" + ], + "form": { + "options": [ + { + "label": "bf:Identifier", + "value": "bf:Identifier" + }, + { + "label": "bf:Local", + "value": "bf:Local" + } + ] + } + }, + "source": { + "title": "Source", + "type": "string", + "minLength": 1, + "form": { + "hideExpression": "!model || model.type !== 'bf:Local'", + "expressionProperties": { + "templateOptions.required": "model && model.type === 'bf:Local'" + } + } + }, + "value": { + "title": "Value", + "type": "string", + "minLength": 1 + } + }, + "propertiesOrder": [ + "type", + "source", + "value" + ], + "required": [ + "type", + "value" + ], + "form": { + "hide": true + } + }, + "investigators": { + "title": "Investigators", + "type": "array", + "minItems": 0, + "items": { + "title": "Investigator", + "type": "object", + "additionalProperties": false, + "properties": { + "agent": { + "title": "Agent", + "type": "object", + "additionalProperties": false, + "properties": { + "preferred_name": { + "title": "Preferred name", + "type": "string", + "minLength": 1 + } + }, + "propertiesOrder": [ + "preferred_name" + ], + "required": [ + "preferred_name" + ] + }, + "role": { + "title": "Roles", + "type": "array", + "minItems": 1, + "items": { + "title": "Role", + "type": "string", + "enum": [ + "investigator", + "coinvestigator" + ], + "default": "investigator", + "form": { + "options": [ + { + "label": "investigator", + "value": "investigator" + }, + { + "label": "coinvestigator", + "value": "coinvestigator" + } + ] + } + } + }, + "affiliation": { + "title": "Affiliation", + "type": "string", + "minLength": 1 + }, + "controlledAffiliation": { + "title": "Controlled affiliations", + "type": "array", + "minItems": 1, + "items": { + "title": "Controlled affiliation", + "type": "string", + "minLength": 1 + } + }, + "identifiedBy": { + "$ref": "identifiedby-v1.0.0.json" + } + }, + "propertiesOrder": [ + "agent", + "role", + "affiliation", + "controlledAffiliation", + "identifiedBy" + ], + "required": [ + "agent", + "role" + ] + }, + "form": { + "hide": true + } + }, + "funding_organisations": { + "title": "Funding organisations", + "type": "array", + "minItems": 0, + "items": { + "title": "Funding organisation", + "type": "object", + "additionalProperties": false, + "properties": { + "agent": { + "title": "Agent", + "type": "object", + "additionalProperties": false, + "properties": { + "preferred_name": { + "title": "Preferred name", + "type": "string", + "minLength": 1 + } + }, + "propertiesOrder": [ + "preferred_name" + ], + "required": [ + "preferred_name" + ] + }, + "identifiedBy": { + "$ref": "identifiedby-v1.0.0.json" + } + }, + "propertiesOrder": [ + "agent", + "identifiedBy" + ], + "required": [ + "agent" + ] + }, + "form": { + "hide": true + } + }, + "organisation": { + "title": "Organisation", + "type": "object", + "properties": { + "$ref": { + "type": "string", + "pattern": "^https://sonar.ch/api/organisations/.*?$", + "form": { + "remoteOptions": { + "type": "organisations" + } + } + } + }, + "required": [ + "$ref" + ], + "form": { + "expressionProperties": { + "templateOptions.required": "true" + } + } + }, + "user": { + "title": "User", + "type": "object", + "additionalProperties": false, + "properties": { + "$ref": { + "title": "User", + "type": "string", + "pattern": "^https://sonar.ch/api/users/.*?$", + "form": { + "remoteOptions": { + "type": "users" + } + } + } + }, + "required": [ + "$ref" + ], + "form": { + "expressionProperties": { + "templateOptions.required": "true" + } + } + }, + "approvalDate": { + "title": "Date d'approbation par le.la Team Leader", + "type": "string", + "form": { + "type": "datepicker" + } + }, + "projectSponsor": { + "title": "Répondant.e du projet", + "type": "string", + "minLength": 1, + "form": { + "remoteTypeahead": { + "type": "projects", + "field": "projectSponsor", + "label": "projectSponsor", + "allowAdd": true + } + } + }, + "statusHep": { + "title": "Statut HEP", + "type": "string", + "enum": [ + "Chargé.e d'enseignement/professeur.e", + "Chargé.e de recherche", + "Doctorant.e", + "Post-doctorante", + "Chercheur.e junior", + "Professeur.e HEP associé.e", + "Professeur.e HEP ordinaire", + "Professeur.e HEP" + ], + "default": "Chargé.e d'enseignement/professeur.e" + }, + "mainTeam": { + "title": "Equipe principale", + "type": "string", + "enum": [ + "Éducation, enfance et société apprenante 21", + "Émotions, apprentissage et bien-être à l'école", + "Apprentissages fondamentaux", + "Créativité, transformations et innovations en éducation", + "Langues, arts, cultures : médiation et enseignement", + "Formation et professionnalisation" + ], + "default": "Éducation, enfance et société apprenante 21" + }, + "innerSearcher": { + "title": "Chercheur.e.s associé.e.s internes", + "type": "array", + "minItems": 1, + "items": { + "title": "Prénom et nom", + "type": "string", + "minLength": 1, + "form": { + "remoteTypeahead": { + "type": "users", + "field": "last_name", + "label": "last_name", + "allowAdd": true + } + } + } + }, + "secondaryTeam": { + "title": "Equipe secondaire", + "type": "string", + "enum": [ + "Éducation, enfance et société apprenante 21", + "Émotions, apprentissage et bien-être à l'école", + "Apprentissages fondamentaux", + "Créativité, transformations et innovations en éducation", + "Langues, arts, cultures : médiation et enseignement", + "Formation et professionnalisation" + ] + }, + "status": { + "title": "Etat du projet", + "type": "string", + "enum": [ + "En cours", + "Achevé", + "Abandonné", + "Suspendu" + ], + "default": "En cours" + }, + "externalPartners": { + "title": "Partenaires externes", + "type": "object", + "additionalProperties": false, + "properties": { + "choice": { + "title": "Non / Oui", + "type": "boolean", + "default": false + }, + "list": { + "title": "Liste", + "type": "array", + "minItems": 1, + "items": { + "title": "Partenaire externe", + "type": "object", + "additionalProperties": false, + "properties": { + "searcherName": { + "title": "Nom", + "type": "string", + "minLength": 1 + }, + "institution": { + "title": "Institution", + "type": "string", + "minLength": 1, + "form": { + "hide": true + } + } + }, + "propertiesOrder": [ + "searcherName", + "institution" + ], + "required": [ + "searcherName" + ] + }, + "form": { + "hideExpression": "!field.parent.model || !field.parent.model.choice" + } + } + }, + "propertiesOrder": [ + "choice", + "list" + ], + "required": [ + "choice" + ] + }, + "keywords": { + "title": "Mots-clés", + "type": "array", + "minItems": 1, + "maxItems": 5, + "items": { + "type": "string", + "minLength": 1, + "form": { + "remoteTypeahead": { + "type": "projects", + "field": "keywords", + "label": "keywords", + "allowAdd": true + } + } + } + }, + "realizationFramework": { + "title": "Cadre de réalisation", + "type": "array", + "minItems": 1, + "items": { + "type": "string", + "minLength": 1 + }, + "form": { + "type": "multicheckbox", + "templateOptions": { + "type": "array", + "options": [ + { + "value": "Master" + }, + { + "value": "Master of Advanced Studies" + }, + { + "value": "Doctorat soutenu par la HEP-VS" + }, + { + "value": "doctorat non soutenu par la HEP-VS" + }, + { + "value": "recherche interne" + }, + { + "value": "recherche subventionnée" + }, + { + "value": "Post doctorat soutenu par la HEP-VS" + }, + { + "value": "CAS ou DAS" + } + ] + } + } + }, + "funding": { + "title": "Ce projet a-t-il fait l'objet d'une demande de financement", + "type": "object", + "additionalProperties": false, + "properties": { + "choice": { + "title": "Non / Oui", + "type": "boolean", + "default": false + }, + "funder": { + "title": "Bailleur de fonds", + "type": "object", + "additionalProperties": false, + "properties": { + "type": { + "type": "string", + "enum": [ + "Fonds National Suisse", + "Swissuniversities", + "HES-SO Valais-Wallis", + "HES-SO", + "Fondation privée", + "Fondation publique", + "Entreprise ", + "Autre (champ libre)" + ] + }, + "number": { + "title": "Numéro de financement", + "type": "string", + "minLength": 1, + "form": { + "hideExpression": "!model || !model.type || (model.type !== 'Fonds National Suisse' && model.type !== 'Swissuniversities')", + "expressionProperties": { + "templateOptions.required": "model && (model.type === 'Fonds National Suisse' || model.type === 'Swissuniversities')" + } + } + }, + "name": { + "title": "Nom", + "type": "string", + "minLength": 1, + "form": { + "hideExpression": "!model || !model.type || model.type === 'Fonds National Suisse' || model.type === 'Swissuniversities'", + "expressionProperties": { + "templateOptions.required": "model && model.type !== 'Fonds National Suisse' && model.type !== 'Swissuniversities'" + } + } + } + }, + "propertiesOrder": [ + "type", + "number", + "name" + ], + "required": [ + "type" + ], + "form": { + "hideExpression": "field.parent.model.choice === false" + } + }, + "fundingReceived": { + "title": "Ce projet-a-t-il obtenu le financement", + "type": "boolean", + "default": true, + "form": { + "hideExpression": "field.parent.model.choice === false" + } + } + }, + "propertiesOrder": [ + "choice", + "funder", + "fundingReceived" + ], + "required": [ + "choice" + ] + }, + "actorsInvolved": { + "title": "Qui sont les acteurs·trices impliqué·e·s dans le terrain", + "type": "array", + "minItems": 1, + "items": { + "title": "Acteur impliqué", + "type": "object", + "additionalProperties": false, + "properties": { + "choice": { + "title": "Acteur", + "type": "string", + "enum": [ + "Apprenti·e", + "Assistant·e·s social·e", + "Conseiller·ère en orientation", + "Directeur·trice, responsable d'établissement", + "Directeur·trice, responsable de formation HE", + "Doyen·ne d'établissement", + "Vice-recteur·trice HE", + "Elève", + "Elève allophone", + "Elève en enseignement spécialisé", + "Enseignant·e primaire", + "Enseignant·e secondaire", + "Enseignant·e tertiaire", + "Enseignant·e spécialisé·e ", + "Etudiant·e en formation", + "Formateurs·trice", + "Inspecteur·trice", + "Logopédiste", + "Maître·sse de classe", + "Médiateur·trice", + "Parents", + "Praticien·ne formateur·trice", + "Psychologue", + "Pychomotricien·ne", + "Animateur·trice", + "Autre" + ] + }, + "other": { + "title": "Autre", + "type": "string", + "minLength": 1, + "form": { + "hideExpression": "!model.choice || model.choice !== 'Autre'" + } + }, + "count": { + "title": "Nombre", + "type": "integer", + "minLength": 1 + } + }, + "propertiesOrder": [ + "choice", + "other", + "count" + ], + "required": [ + "choice" + ] + } + }, + "benefits": { + "title": "Quels sont les bénéfices et améliorations de la qualité dans la recherche dans ce projet (250 mots)", + "type": "string", + "minLength": 1, + "form": { + "type": "textarea", + "templateOptions": { + "rows": 5, + "attributes": { + "maxlength": 250 + } + } + } + }, + "impactOnFormation": { + "title": "Quelles sont les retombées de la recherche en formation (250 mots)", + "type": "string", + "minLength": 1, + "form": { + "type": "textarea", + "templateOptions": { + "rows": 5, + "attributes": { + "maxlength": 250 + } + } + } + }, + "impactOnProfessionalEnvironment": { + "title": "Quelles sont les retombées de la recherche dans le milieu professionnel (250 mots)", + "type": "string", + "minLength": 1, + "form": { + "type": "textarea", + "templateOptions": { + "rows": 5, + "attributes": { + "maxlength": 250 + } + } + } + }, + "impactOnPublicAction": { + "title": "Quelles sont les retombées des recherches sur l'action publique ou sur la gouvernance interne ou externe (250 mots)", + "type": "string", + "minLength": 1, + "form": { + "type": "textarea", + "templateOptions": { + "rows": 5, + "attributes": { + "maxlength": 250 + } + } + } + }, + "promoteInnovation": { + "title": "Ce projet favorise-t-il l'innovation pédagogique ou technologique", + "type": "object", + "additionalProperties": false, + "properties": { + "choice": { + "title": "Non / Oui", + "type": "boolean", + "default": false + }, + "reason": { + "title": "Pourquoi", + "type": "string", + "minLength": 1, + "form": { + "hideExpression": "!model || !model.choice", + "expressionProperties": { + "templateOptions.required": "model && model.choice" + } + } + } + }, + "propertiesOrder": [ + "choice", + "reason" + ], + "required": [ + "choice" + ] + }, + "relatedToMandate": { + "title": "Cette recherche est-elle liée à un mandat", + "type": "object", + "additionalProperties": false, + "properties": { + "choice": { + "title": "Non / Oui", + "type": "boolean", + "default": false + }, + "mandate": { + "title": "Mandat", + "type": "string", + "default": "État du Valais, Service de l'enseignement", + "enum": [ + "État du Valais, Service de l'enseignement", + "État du Valais, Service des hautes écoles", + "État du Valais, autres services", + "État du Valais, institution paraétatique", + "Commune (Valais)", + "Autre canton (Suisse)", + "Autre commune (Suisse)", + "Autre" + ], + "form": { + "hideExpression": "!model || !model.choice", + "expressionProperties": { + "templateOptions.required": "model && model.choice" + } + } + }, + "name": { + "title": "Nom", + "type": "string", + "minLength": 1, + "form": { + "hideExpression": "!model || !model.mandate || ['État du Valais, Service de l\\'enseignement', 'État du Valais, Service des hautes écoles'].includes(model.mandate)", + "expressionProperties": { + "templateOptions.required": "model && model.mandate && !['État du Valais, Service de l\\'enseignement', 'État du Valais, Service des hautes écoles'].includes(model.mandate)" + } + } + }, + "briefDescription": { + "title": "Description brève du mandat", + "type": "string", + "minLength": 1, + "form": { + "type": "textarea", + "templateOptions": { + "rows": 5, + "attributes": { + "maxlength": 250 + } + }, + "hideExpression": "!model || !model.choice", + "expressionProperties": { + "templateOptions.required": "model && model.choice" + } + } + } + }, + "propertiesOrder": [ + "choice", + "mandate", + "name", + "briefDescription" + ], + "required": [ + "choice" + ] + }, + "educationalDocument": { + "title": "Ce projet fait-il l'objet d'un document pédagogique ou rapports à la cité ou la science", + "type": "object", + "additionalProperties": false, + "properties": { + "choice": { + "title": "Non / Oui", + "type": "boolean", + "default": false + }, + "briefDescription": { + "title": "Description brève du rapport", + "type": "string", + "minLength": 1, + "form": { + "type": "textarea", + "templateOptions": { + "rows": 5 + }, + "hideExpression": "!model || !model.choice", + "expressionProperties": { + "templateOptions.required": "model && model.choice" + } + } + } + }, + "propertiesOrder": [ + "choice", + "briefDescription" + ], + "required": [ + "choice" + ] + }, + "searchResultsValorised": { + "title": "Comment les résultats de la recherche sont-ils valorisés dans la formation?", + "type": "string", + "minLength": 1, + "form": { + "type": "textarea", + "templateOptions": { + "rows": 5 + } + } + } + }, + "propertiesOrder": [ + "name", + "approvalDate", + "projectSponsor", + "statusHep", + "mainTeam", + "innerSearcher", + "secondaryTeam", + "status", + "externalPartners", + "startDate", + "endDate", + "description", + "keywords", + "realizationFramework", + "funding", + "actorsInvolved", + "benefits", + "impactOnFormation", + "impactOnProfessionalEnvironment", + "impactOnPublicAction", + "promoteInnovation", + "relatedToMandate", + "educationalDocument", + "searchResultsValorised", + "organisation" + ], + "required": [ + "name", + "approvalDate", + "projectSponsor", + "statusHep", + "mainTeam", + "innerSearcher", + "status", + "externalPartners", + "startDate", + "endDate", + "description", + "keywords", + "realizationFramework", + "funding", + "actorsInvolved", + "benefits", + "impactOnFormation", + "impactOnProfessionalEnvironment", + "impactOnPublicAction", + "promoteInnovation", + "relatedToMandate", + "educationalDocument", + "searchResultsValorised" + ] + } + } +} diff --git a/sonar/dedicated/hepvs/projects/schema.py b/sonar/dedicated/hepvs/projects/schema.py new file mode 100644 index 000000000..4e1db2b5c --- /dev/null +++ b/sonar/dedicated/hepvs/projects/schema.py @@ -0,0 +1,55 @@ +# -*- 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 . + +"""Projects schema.""" + +from marshmallow import fields + +from sonar.resources.projects.schema import \ + MetadataSchema as BaseMetadataSchema +from sonar.resources.projects.schema import RecordSchema as BaseRecordSchema + + +class MetadataSchema(BaseMetadataSchema): + """Schema for the project metadata.""" + + projectSponsor = fields.Str() + approvalDate = fields.Str() + statusHep = fields.Str() + innerSearcher = fields.List(fields.Str()) + externalPartners = fields.Dict() + mainTeam = fields.Str() + secondaryTeam = fields.Str() + status = fields.Str() + keywords = fields.List(fields.Str()) + realizationFramework = fields.List(fields.Str()) + funding = fields.Dict() + actorsInvolved = fields.List(fields.Dict()) + benefits = fields.Str() + impactOnFormation = fields.Str() + impactOnProfessionalEnvironment = fields.Str() + impactOnPublicAction = fields.Str() + promoteInnovation = fields.Dict() + relatedToMandate = fields.Dict() + educationalDocument = fields.Dict() + searchResultsValorised = fields.Str() + + +class RecordSchema(BaseRecordSchema): + """Schema for records v1 in JSON.""" + + metadata = fields.Nested(MetadataSchema) diff --git a/sonar/modules/utils.py b/sonar/modules/utils.py index b769d3f25..8cae2167a 100644 --- a/sonar/modules/utils.py +++ b/sonar/modules/utils.py @@ -220,3 +220,20 @@ def chunks(records, size): def remove_html(content): """Remove html tags from content.""" return re.sub(re.compile('<.*?>'), '', content) + + +def has_custom_resource(resource_type): + """Check if user's organisation has a custom resource. + + :param resource_type: Type of resource. + :returns: True if resource is custom for organisation. + """ + # Mandatory to import current_organisation here, to avoid an error during + # tests. + from sonar.modules.organisations.api import current_organisation + + if not current_organisation or not current_organisation.get('code'): + return False + + return current_app.config.get('SONAR_APP_ORGANISATION_CONFIG').get( + current_organisation['code'], {}).get(resource_type) diff --git a/sonar/resources/projects/api.py b/sonar/resources/projects/api.py index 721193d06..12451b666 100644 --- a/sonar/resources/projects/api.py +++ b/sonar/resources/projects/api.py @@ -24,10 +24,13 @@ from invenio_records_resources.records.systemfields import IndexField, PIDField from invenio_records_resources.services.records.components import \ ServiceComponent +from werkzeug.utils import cached_property from sonar.affiliations import AffiliationResolver -from sonar.modules.organisations.api import OrganisationRecord +from sonar.modules.organisations.api import OrganisationRecord, \ + current_organisation from sonar.modules.users.api import UserRecord +from sonar.modules.utils import has_custom_resource from . import models @@ -59,15 +62,22 @@ class Record(BaseRecord): model_cls = models.RecordMetadata # System fields - schema = ConstantField( - '$schema', 'https://sonar.ch/schemas/projects/project-v1.0.0.json') - index = IndexField('projects-project-v1.0.0', search_alias='projects') pid = PIDField('id', pid_type='proj', provider=RecordIdProvider) dumper = ElasticsearchDumper(extensions=[ElasticsearchDumperObjectsExt()]) + @cached_property + def schema(self): + """Return the schema.""" + schema_key = 'projects' if not has_custom_resource( + 'projects') else f'{current_organisation["code"]}/projects' + + schema = f'https://sonar.ch/schemas/{schema_key}/project-v1.0.0.json' + + return ConstantField('$schema', schema) + class RecordComponent(ServiceComponent): """Custom action for projects records.""" diff --git a/sonar/resources/projects/jsonschemas/projects/project-v1.0.0_src.json b/sonar/resources/projects/jsonschemas/projects/project-v1.0.0_src.json index 21f9902eb..552299867 100644 --- a/sonar/resources/projects/jsonschemas/projects/project-v1.0.0_src.json +++ b/sonar/resources/projects/jsonschemas/projects/project-v1.0.0_src.json @@ -78,6 +78,7 @@ "format": "date", "pattern": "^[0-9]{4}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])$", "form": { + "type": "datepicker", "templateOptions": { "placeholder": "Example: 2020-12-01" } @@ -90,6 +91,7 @@ "format": "date", "pattern": "^[0-9]{4}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])$", "form": { + "type": "datepicker", "templateOptions": { "placeholder": "Example: 2020-12-01" } diff --git a/sonar/resources/projects/service.py b/sonar/resources/projects/service.py index d0851cc43..c3737a490 100644 --- a/sonar/resources/projects/service.py +++ b/sonar/resources/projects/service.py @@ -19,15 +19,19 @@ from invenio_records_resources.services import \ RecordServiceConfig as BaseRecordServiceConfig +from invenio_records_resources.services.records.schema import \ + MarshmallowServiceSchema +from invenio_records_rest.utils import obj_or_import_string from sonar.config import DEFAULT_AGGREGATION_SIZE +from sonar.modules.organisations.api import current_organisation from sonar.modules.query import and_term_filter +from sonar.modules.utils import has_custom_resource from ..service import RecordService as BaseRecordService from .api import Record, RecordComponent from .permissions import RecordPermissionPolicy from .results import RecordList -from .schema import RecordSchema class RecordServiceConfig(BaseRecordServiceConfig): @@ -36,7 +40,6 @@ class RecordServiceConfig(BaseRecordServiceConfig): permission_policy_cls = RecordPermissionPolicy record_cls = Record result_list_cls = RecordList - schema = RecordSchema search_facets_options = { 'aggs': { 'user': { @@ -64,3 +67,16 @@ class RecordService(BaseRecordService): """Projects service.""" default_config = RecordServiceConfig + + @property + def schema(self): + """Returns the data schema instance.""" + schema_path = 'sonar.resources.projects.schema:RecordSchema' + + if has_custom_resource('projects'): + schema_path = f'sonar.dedicated.{current_organisation["code"]}.' \ + 'projects.schema:RecordSchema' + + schema = obj_or_import_string(schema_path) + + return MarshmallowServiceSchema(self, schema=schema) diff --git a/sonar/theme/views.py b/sonar/theme/views.py index 1f10b3e3c..5df91853a 100644 --- a/sonar/theme/views.py +++ b/sonar/theme/views.py @@ -39,10 +39,12 @@ from sonar.modules.deposits.permissions import DepositPermission from sonar.modules.documents.permissions import DocumentPermission +from sonar.modules.organisations.api import current_organisation from sonar.modules.organisations.permissions import OrganisationPermission from sonar.modules.permissions import can_access_manage_view from sonar.modules.users.api import current_user_record from sonar.modules.users.permissions import UserPermission +from sonar.modules.utils import has_custom_resource from sonar.resources.projects.permissions import RecordPermissionPolicy blueprint = Blueprint('sonar', @@ -172,6 +174,10 @@ def schemas(record_type): try: current_jsonschemas.get_schema.cache_clear() schema_name = '{}/{}-v1.0.0.json'.format(record_type, rec_type) + + if has_custom_resource(record_type): + schema_name = f'{current_organisation["code"]}/{schema_name}' + schema = current_jsonschemas.get_schema(schema_name) # TODO: Maybe find a proper way to do this. diff --git a/tests/ui/test_utils.py b/tests/ui/test_utils.py index b12d73801..a072d47eb 100644 --- a/tests/ui/test_utils.py +++ b/tests/ui/test_utils.py @@ -21,6 +21,8 @@ import pytest from flask import g +from flask_security import url_for_security +from invenio_accounts.testutils import login_user_via_view from sonar.modules.documents.views import store_organisation from sonar.modules.utils import * @@ -162,3 +164,26 @@ def test_remove_html(): """Test remove html markup from string.""" assert remove_html('No HTML') == 'No HTML' assert remove_html('

Title

') == 'Title' + + +def test_has_custom_resource(client, make_user, monkeypatch): + """Test if user's organisation has a custom resource.""" + # User not logged + assert not has_custom_resource('projects') + + # Custom resource found + user = make_user('admin', 'hepvs') + login_user_via_view(client, email=user['email'], password='123456') + assert has_custom_resource('projects') + + client.get(url_for_security('logout')) + + # No organisation associated to user + user = make_user('admin') + login_user_via_view(client, email=user['email'], password='123456') + assert not has_custom_resource('projects') + + # No organisation code found for user's organisation + monkeypatch.setattr('sonar.modules.organisations.api.current_organisation', + {}) + assert not has_custom_resource('projects') diff --git a/tests/unit/dedicated/hepvs/test_dedicated_projects_hepvs.py b/tests/unit/dedicated/hepvs/test_dedicated_projects_hepvs.py new file mode 100644 index 000000000..765ee124e --- /dev/null +++ b/tests/unit/dedicated/hepvs/test_dedicated_projects_hepvs.py @@ -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 . + +"""Test dedicated features for HEP-VS.""" + +from invenio_accounts.testutils import login_user_via_view + +from sonar.dedicated.hepvs.projects.schema import RecordSchema +from sonar.proxies import sonar +from sonar.resources.projects.api import Record +from sonar.theme.views import schemas + + +def test_json_schema(client, make_user): + """Test JSON schema.""" + user = make_user('admin', 'hepvs') + + login_user_via_view(client, email=user['email'], password='123456') + + result = schemas('projects') + assert result.json['schema']['properties']['metadata']['properties'][ + 'projectSponsor'] + + +def test_service(client, make_user): + """Test service.""" + user = make_user('admin', 'hepvs') + + login_user_via_view(client, email=user['email'], password='123456') + + assert isinstance(sonar.resources['projects'].service.schema.schema(), + RecordSchema) + + +def test_api(client, make_user): + """Test API.""" + user = make_user('admin', 'hepvs') + + login_user_via_view(client, email=user['email'], password='123456') + + assert Record({}).schema.value == 'https://sonar.ch/schemas/' \ + 'hepvs/projects/project-v1.0.0.json'