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'