From 2f4301c2a811348aa09472badcf325edc80cb172 Mon Sep 17 00:00:00 2001
From: Olivier Maury <Olivier.Maury@inrae.fr>
Date: Wed, 19 Jun 2024 10:04:17 +0200
Subject: [PATCH 1/5] Ordonner les constantes I18n client

---
 .../client/i18n/AppConstants_fr.properties    | 20 +++++++++----------
 1 file changed, 10 insertions(+), 10 deletions(-)

diff --git a/www-client/src/main/resources/fr/agrometinfo/www/client/i18n/AppConstants_fr.properties b/www-client/src/main/resources/fr/agrometinfo/www/client/i18n/AppConstants_fr.properties
index 61962f4..4d98357 100644
--- a/www-client/src/main/resources/fr/agrometinfo/www/client/i18n/AppConstants_fr.properties
+++ b/www-client/src/main/resources/fr/agrometinfo/www/client/i18n/AppConstants_fr.properties
@@ -22,41 +22,41 @@ contactUs = Contactez-nous
 credits = Crédits
 creditsPath = credits.html
 dailyValues = Valeurs journalières
-downloadChart = Télécharger le graphique
 documentation = Documentation
 documentationPath = decouvrir.html
+downloadChart = Télécharger le graphique
 failureBody = Corps :
 failureHeaders = Entêtes HTTP :
 failureStatusText = Texte d’état HTTP :
+ignore = Ignorer
 inraeUrl = https://www.inrae.fr/
 invalidEmailAddress = Adresse courriel invalide
 legalNotice = Mentions légales
 legalNoticePath = legal-notice.html
-messageSent = Votre message a bien été envoyé à l’équipe d’AgroMetInfo.
-ignore = Ignorer
-login = Se connecter
 loginOrSignIn = ou s’inscrire avec
+login = Se connecter
 logout = Se déconnecter
+messageSent = Votre message a bien été envoyé à l’équipe d’AgroMetInfo.
 metropolitanFrance = France métropolitaine
 no = Non
 normalComparison= Comparaison à la normale
 normalComparisonTooltip= <b>La comparaison à la normale</b> se calcule en soustrayant <b>la moyenne de l’indicateur choisi</b> pour les trente dernières années (1990-2020) de <b>l’année sélectionnée</b>.
 otherAgroclimApps = Autres services et outils d’AgroClim
-requiredErrorMessage = * Ce champ est obligatoire.
 releaseNotes = Notes de version
 releaseNotesPath = release-notes.html
 reloadingApplication = Rechargement de l'application pour une nouvelle version\u2026
+requiredErrorMessage = * Ce champ est obligatoire.
 seePrivacyPolicy = Consultez le paragraphe « Données personnelles » dans les mentions légales.
 selectPrompt = -- sélectionner --
-surveyFormTitle = Bienvenu.e sur AgroMetInfo
 surveyFormDescription = Afin de nous aider à la faire évoluer et l'adapter le plus possible à vos besoins, <b>merci de renseigner</b> la petite enquête ci-dessous (cela prendra 2 minutes maximum). L'ensemble des informations est anonyme, mais nous aidera à mieux comprendre l'utilisation de l'application.<br/>N'hésitez pas à <b>nous laisser votre adresse courriel</b> pour vous tenir informés de toutes les évolutions dans les mois et années à venir.
-surveyFromEmailDescription = Votre adresse courriel
+surveyFormFail = Vos réponses au formulaire n'ont pas pu être enregistrées, mais vous pouvez tout de même utiliser AgroMetInfo.
 surveyFormOtherTextCheckbox = Autre, à préciser
 surveyFormSuccess = Vos réponses au formulaire d'enquête ont bien été enregistrées.
-surveyFormFail = Vos réponses au formulaire n'ont pas pu être enregistrées, mais vous pouvez tout de même utiliser AgroMetInfo.
+surveyFormTitle = Bienvenue sur AgroMetInfo
+surveyFromEmailDescription = Votre adresse courriel
 toggleRightPanel = Afficher / masquer le panneau de droite
-yes = Oui
 userProfile = Compte et paramètres
 validate = Valider
 whyConnectionIsRequired = Vous devez vous identifier pour accéder à AgroMetInfo en raison des accords avec Météo-France relatifs aux échanges de données SAFRAN avec AgroClim.
-yes= Oui
\ No newline at end of file
+yes= Oui
+
-- 
GitLab


From eca93aba506b6946f1cbaf60dad1075a9bdb480a Mon Sep 17 00:00:00 2001
From: Olivier Maury <Olivier.Maury@inrae.fr>
Date: Wed, 19 Jun 2024 10:04:35 +0200
Subject: [PATCH 2/5] Typo

---
 .../main/java/fr/agrometinfo/www/client/view/SurveyView.java   | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/www-client/src/main/java/fr/agrometinfo/www/client/view/SurveyView.java b/www-client/src/main/java/fr/agrometinfo/www/client/view/SurveyView.java
index cd9b11f..61f2fe1 100644
--- a/www-client/src/main/java/fr/agrometinfo/www/client/view/SurveyView.java
+++ b/www-client/src/main/java/fr/agrometinfo/www/client/view/SurveyView.java
@@ -124,6 +124,7 @@ public final class SurveyView extends AbstractBaseView<SurveyPresenter> implemen
             otherCb.addChangeHandler((v) -> {
                 otherText.setDisabled(!otherCb.getValue());
                 if (otherCb.getValue()) {
+                    // TODO : rechercher hide() / show()
                     otherText.removeCss(CSS_HIDE_TEXTAREA)
                     .focus();
                 } else {
@@ -134,7 +135,7 @@ public final class SurveyView extends AbstractBaseView<SurveyPresenter> implemen
             this.modal.appendChild(otherCb).appendChild(otherText);
             otherTextArea.put(k.getId(), otherText);
         }
-        // Validate button is activ if if at least one answer is checked
+        // Validate button is active if if at least one answer is checked
         this.checkBoxList.forEach((c) -> {
             c.addChangeHandler((v) -> this.activateValidateButton());
         });
-- 
GitLab


From fde65cb7b1b1cdc1d04e44b876c3d2b501a8a02e Mon Sep 17 00:00:00 2001
From: Olivier Maury <Olivier.Maury@inrae.fr>
Date: Wed, 19 Jun 2024 10:25:58 +0200
Subject: [PATCH 3/5] Typo

---
 sql/init_data.postgresql.sql | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/sql/init_data.postgresql.sql b/sql/init_data.postgresql.sql
index 4a60713..1e387c8 100644
--- a/sql/init_data.postgresql.sql
+++ b/sql/init_data.postgresql.sql
@@ -1,5 +1,4 @@
--- Initialization script for PostgreSQL database with sample data
--- also do update.
+-- Initialization and update script for PostgreSQL database with data.
 
 DELETE FROM normalvalue;
 DELETE FROM dailyvalue;
-- 
GitLab


From 2525f18591a886f8338dd1bf46e7b7f66f9f544b Mon Sep 17 00:00:00 2001
From: Olivier Maury <Olivier.Maury@inrae.fr>
Date: Wed, 19 Jun 2024 10:42:52 +0200
Subject: [PATCH 4/5] Correction ordre arguments assertEquals

---
 .../www/server/SurveyFormHibernateTest.java   | 85 +++++++++----------
 1 file changed, 42 insertions(+), 43 deletions(-)

diff --git a/www-server/src/test/java/fr/agrometinfo/www/server/SurveyFormHibernateTest.java b/www-server/src/test/java/fr/agrometinfo/www/server/SurveyFormHibernateTest.java
index 6e5f606..8265ff3 100644
--- a/www-server/src/test/java/fr/agrometinfo/www/server/SurveyFormHibernateTest.java
+++ b/www-server/src/test/java/fr/agrometinfo/www/server/SurveyFormHibernateTest.java
@@ -2,7 +2,6 @@ package fr.agrometinfo.www.server;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertNotEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -35,14 +34,14 @@ import fr.agrometinfo.www.server.model.UserResponse;
  */
 public class SurveyFormHibernateTest {
     /** DAO for user's responses. */
-    private UserResponsesDao userResponsesDao = new UserResponsesDaoHibernate();
+    private final UserResponsesDao userResponsesDao = new UserResponsesDaoHibernate();
     /** DAO for questions. */
-    private SurveyQuestionDao questionsDao = new SurveyQuestionDaoHibernate();
+    private final SurveyQuestionDao questionsDao = new SurveyQuestionDaoHibernate();
     /** DAO for responses. */
-    private SurveyOptionDao responsesDao = new SurveyOptionDaoHibernate();
+    private final SurveyOptionDao responsesDao = new SurveyOptionDaoHibernate();
     /** DAO for email address of user. */
-    private UserEmailDao userEmailDao = new UserEmailDaoHibernate();
-    
+    private final UserEmailDao userEmailDao = new UserEmailDaoHibernate();
+
     @Test
     public void getQuestion() {
         final List<SurveyQuestion> list = questionsDao.findAll();
@@ -51,10 +50,10 @@ public class SurveyFormHibernateTest {
             // search question by reference.
             final SurveyQuestion foundedByRef = questionsDao.findByRef(q.getId());
             assertNotNull(foundedByRef, "Object for question « " + q.getId() + " » is null");
-            assertEquals(foundedByRef, q, "Object corresponding to question « " + q.getId() + " » doesn't corresponding");
+            assertEquals(q, foundedByRef, "Object corresponding to question « " + q.getId() + " » doesn't corresponding");
         }
     }
-    
+
     @Test
     public void getResponses() {
         final List<SurveyQuestion> questions = questionsDao.findAll();
@@ -62,22 +61,22 @@ public class SurveyFormHibernateTest {
         assertFalse(responses.isEmpty(), "Options list is empty");
         for (final SurveyOption r : responses) {
             assertTrue(questions.contains(r.getQuestion()), "Questions list doesn't contains question " + r.getQuestion().getId());
-            
+
             // Test from question corresponding to response
             final SurveyQuestion q = r.getQuestion();
             assertNotNull(q, "Question corresponding to response « " + r.getId() + " » is null");
-            
+
             final SurveyQuestion foundedByRef = questionsDao.findByRef(q.getId());
             assertNotNull(foundedByRef, "Object corresponding to question « " + q.getId() + " » doesn't corresponding");
-            assertEquals(foundedByRef, r.getQuestion(), "Object corresponding to question « " + q.getId() + " » doesn't corresponding");
-            
+            assertEquals(r.getQuestion(), foundedByRef, "Object corresponding to question « " + q.getId() + " » doesn't corresponding");
+
             // Search response by reference
             final SurveyOption responseByRef = responsesDao.findByRef(r.getId());
             assertNotNull(responseByRef, "Object corresponding to response " + r.getId() + " is null");
-            assertEquals(responseByRef, r, "Object corresponding to response " + r.getId() + " is empty");
+            assertEquals(r, responseByRef, "Object corresponding to response " + r.getId() + " is empty");
         }
     }
-    
+
     @Test
     public void setUserResponses() {
         this.resetUserResponses();
@@ -90,19 +89,19 @@ public class SurveyFormHibernateTest {
         for (final SurveyOption r : responses) {
             assertEquals(r.getQuestion(), profession, "Question of responses doesn't corresponding to original question");
         }
-        
+
         // Inserting a single response
         final List<SurveyOption> listOfResponses = new ArrayList<>();   // list of inserted answers
         SurveyOption r = responses.get(ThreadLocalRandom.current().nextInt(0, responses.size() - 1));
         userResponsesDao.insertResponse(profession, r, null);
         listOfResponses.add(r);
-        
+
         List<UserResponse> list = userResponsesDao.findAllByQuestion(profession.getId());
-        assertEquals(list.size(), 1, "The user's response list must contain an item only");
-        assertEquals(list.get(0).getQuestion(), profession, "The question in the user's response does not match the original question");
-        assertEquals(list.get(0).getOption(), r, "The answer contained in the user's response does not match the inserted answer");
+        assertEquals(1, list.size(), "The user's response list must contain an item only");
+        assertEquals(profession, list.get(0).getQuestion(), "The question in the user's response does not match the original question");
+        assertEquals(r, list.get(0).getOption(), "The answer contained in the user's response does not match the inserted answer");
         assertNull(list.get(0).getOtherText(), "Free response must be null");
-        
+
         // Inserting of multiple responses
         final int nbToInsert = 3;
         for (final SurveyOption res : pickNRandom(responses, nbToInsert)) {
@@ -112,58 +111,58 @@ public class SurveyFormHibernateTest {
         list = userResponsesDao.findAllByQuestion(profession.getId());
         assertEquals(list.size(), (1 + nbToInsert), "The user's answer list must contain the previous answer + " + nbToInsert + " additional answers");
         for (final UserResponse ur : list) {
-            assertEquals(ur.getQuestion(), profession, "The original question doesn't match with the Question object");
+            assertEquals(profession, ur.getQuestion(), "The original question doesn't match with the Question object");
             assertTrue(listOfResponses.contains(ur.getOption()), "The answer contained in the user's answer is not in the list of inserted answers");
-            assertEquals(ur.getOption().getQuestion(), profession, "The original question does not match with the Question object present in the Option object");
+            assertEquals(profession, ur.getOption().getQuestion(), "The original question does not match with the Question object present in the Option object");
             assertNull(ur.getOtherText(), "The text must be null if it is an answer from a choice");
         }
 
         final int newNbResponses = 1 + nbToInsert;
-        
+
         // Insertion of a free choice answer
         String other = "This is a free answer";
         userResponsesDao.insertResponse(profession, null, other);
-        
+
         list = userResponsesDao.findAllByQuestion(profession.getId());
         assertEquals(list.size(), (newNbResponses + 1), "The user's response list must contain the " + newNbResponses + "+ the free answer");
         for (final UserResponse ur : list) {
-            assertEquals(ur.getQuestion(), profession, "The original question does not match with the Question object in the user's response");
+            assertEquals(profession, ur.getQuestion(), "The original question does not match with the Question object in the user's response");
             if (ur.getOtherText() != null) {    // free response
                 assertNull(ur.getOption(), "The Option object must be null if it is a free response");
-                assertEquals(ur.getOtherText(), other, "The text does not correspond to what was inserted in the base");
+                assertEquals(other, ur.getOtherText(), "The text does not correspond to what was inserted in the base");
             } else {    // predefined response
-                assertEquals(ur.getOption().getQuestion(), profession, "The original question does not match with the Question object present in the Option object");
+                assertEquals(profession, ur.getOption().getQuestion(), "The original question does not match with the Question object present in the Option object");
                 assertNull(ur.getOtherText(), "The text must be null if it is an answer from a choice");
             }
         }
-        
+
         final SurveyQuestion useCase = questions.stream().filter((q) -> q.getId() == 3).findFirst().get();
         assertNotNull(useCase, "No object match the use cases question");
         responses = responsesDao.findAllByQuestion(useCase.getId());
         assertNotNull(responses, "No answer matches the use case question object");
         assertFalse(responses.isEmpty(), "The answer list for the use case question object is empty");
-        
+
         r = responses.get(ThreadLocalRandom.current().nextInt(0, responses.size() - 1));
         userResponsesDao.insertResponse(useCase, r, null);
-        
+
         list = userResponsesDao.findAllByQuestion(useCase.getId());
-        assertEquals(list.size(), 1, "The user answer list for the use case question must contain an item only");
-        assertEquals(list.get(0).getQuestion(), useCase, "The question in the user's response does not match the original question");
-        assertEquals(list.get(0).getOption(), r, "The answer contained in the user's response does not match the inserted answer");
+        assertEquals(1, list.size(), "The user answer list for the use case question must contain an item only");
+        assertEquals(useCase, list.get(0).getQuestion(), "The question in the user's response does not match the original question");
+        assertEquals(r, list.get(0).getOption(), "The answer contained in the user's response does not match the inserted answer");
         assertNull(list.get(0).getOtherText(), "Free text must be null");
-        
+
         other = other.concat(" for the question of use cases");
         userResponsesDao.insertResponse(useCase, null, other);
-        
+
         list = userResponsesDao.findAllByQuestion(useCase.getId());
-        assertEquals(list.size(), 2, "The user answer list for the use case question must contain two items");
+        assertEquals(2, list.size(), "The user answer list for the use case question must contain two items");
         for (final UserResponse ur : list) {
-            assertEquals(ur.getQuestion(), useCase, "The original question does not match with the Question object in the user's response");
+            assertEquals(useCase, ur.getQuestion(), "The original question does not match with the Question object in the user's response");
             if (ur.getOtherText() != null) {    // free response
                 assertNull(ur.getOption(), "The response object must be null if it is a free response");
-                assertEquals(ur.getOtherText(), other, "The text does not correspond to what was inserted in the base");
+                assertEquals(other, ur.getOtherText(), "The text does not correspond to what was inserted in the base");
             } else {    // predefined response
-                assertEquals(ur.getOption().getQuestion(), useCase, "The original question does not match with the Question object present in the Option object");
+                assertEquals(useCase, ur.getOption().getQuestion(), "The original question does not match with the Question object present in the Option object");
                 assertNull(ur.getOtherText(), "The text must be null if it is an answer from a choice");
             }
         }
@@ -177,10 +176,10 @@ public class SurveyFormHibernateTest {
         assertNotNull(list, "The email list is not initialized");
         assertTrue(list.isEmpty(), "The email list must be empty");
         this.userEmailDao.insertEmailUserAddress(email, LocalDateTime.now());
-        
+
         list = this.userEmailDao.getEmailAddressList();
-        assertEquals(list.size(), 1, "The email list must contain an item");
-        assertEquals(list.get(0).getEmail(), email, "The saved item does not match the inserted email « " + email + " »");
+        assertEquals(1, list.size(), "The email list must contain an item");
+        assertEquals(email, list.get(0).getEmail(), "The saved item does not match the inserted email « " + email + " »");
     }
     /**
      * Return randomly responses list
@@ -189,7 +188,7 @@ public class SurveyFormHibernateTest {
      * @return
      */
     private List<SurveyOption> pickNRandom(final List<SurveyOption> list, final int nbElements) {
-        final List<SurveyOption> copy = new ArrayList<SurveyOption>(list);
+        final List<SurveyOption> copy = new ArrayList<>(list);
         Collections.shuffle(copy);
         if (nbElements > copy.size()) {
             return copy.subList(0, copy.size());
-- 
GitLab


From 9d1fde25ff5d0849ea303bfc2e6716f4d875d30e Mon Sep 17 00:00:00 2001
From: Olivier Maury <Olivier.Maury@inrae.fr>
Date: Wed, 19 Jun 2024 13:47:21 +0200
Subject: [PATCH 5/5] Modifications SQL

---
 sql/init_data.h2.sql                       |  6 +--
 sql/init_data.postgresql.sql               | 38 +++++++------
 sql/migration.sql                          | 28 ++++++++++
 sql/responses.csv                          | 19 -------
 sql/schema.tables.sql                      | 63 +++++++++++-----------
 sql/surveyoptions.csv                      | 19 +++++++
 sql/{questions.csv => surveyquestions.csv} |  0
 7 files changed, 104 insertions(+), 69 deletions(-)
 delete mode 100644 sql/responses.csv
 create mode 100644 sql/surveyoptions.csv
 rename sql/{questions.csv => surveyquestions.csv} (100%)

diff --git a/sql/init_data.h2.sql b/sql/init_data.h2.sql
index c40a7c0..3d2bd84 100644
--- a/sql/init_data.h2.sql
+++ b/sql/init_data.h2.sql
@@ -128,8 +128,8 @@ INSERT INTO simulation (date, simulationid, started, ended) VALUES
 REFRESH MATERIALIZED VIEW v_pra_dailyvalue;
 
 INSERT INTO surveyquestion(id, description)
-	SELECT * FROM CSVREAD('../sql/questions.csv');
+	SELECT * FROM CSVREAD('../sql/surveyquestions.csv');
 
-INSERT INTO surveyoption(surveyquestion, description)
-	SELECT * FROM CSVREAD('../sql/responses.csv', null, 'fieldSeparator=;');
+INSERT INTO surveyoption(id, surveyquestion, description)
+	SELECT * FROM CSVREAD('../sql/surveyoptions.csv', null, 'fieldSeparator=;');
 
diff --git a/sql/init_data.postgresql.sql b/sql/init_data.postgresql.sql
index 1e387c8..4da8a27 100644
--- a/sql/init_data.postgresql.sql
+++ b/sql/init_data.postgresql.sql
@@ -1,18 +1,5 @@
 -- Initialization and update script for PostgreSQL database with data.
 
-DELETE FROM normalvalue;
-DELETE FROM dailyvalue;
-DELETE FROM cell;
-DELETE FROM department;
-DELETE FROM region;
-DELETE FROM indicator;
-DELETE FROM period;
-DELETE FROM i18n;
-DELETE FROM i18nkey;
-DELETE FROM locale;
-DELETE FROM surveyquestion;
-DELETE FROM surveyoption;
-
 -- translations
 CREATE TEMPORARY TABLE IF NOT EXISTS tmp_translation (
     key VARCHAR,
@@ -169,9 +156,28 @@ INSERT INTO normalvalue (indicator, cell, doy, medianvalue, q5, q95)
     ON CONFLICT ON CONSTRAINT "UK_normalvalue" DO NOTHING;
 
 -- questions
-\COPY surveyquestion(id, description) FROM questions.csv WITH DELIMITER ',' HEADER CSV;
+\COPY surveyquestion(id, description) FROM surveyquestions.csv WITH DELIMITER ',' HEADER CSV;
+CREATE TEMPORARY TABLE IF NOT EXISTS tmp_surveyquestion (
+    id SERIAL NOT NULL,
+    description VARCHAR(255) NULL
+);
+\COPY tmp_surveyquestion (id, description) FROM questions.csv WITH DELIMITER ',' HEADER CSV;
+INSERT INTO surveyquestion (id, description)
+    SELECT id, description
+    FROM tmp_surveyquestion
+    ON CONFLICT ON CONSTRAINT "PK_surveyquestion" DO
+        UPDATE SET description=EXCLUDED.description;
 
 -- responses
 -- WARNING : responses CSV file use semicolon (« ; ») as separator !!
-\COPY surveyoption(surveyquestion, description) FROM responses.csv WITH DELIMITER ';' HEADER CSV;
-
+CREATE TEMPORARY TABLE IF NOT EXISTS tmp_surveyoption (
+    id SERIAL NOT NULL,
+    surveyquestion INT4 NOT NULL,
+    description VARCHAR NULL
+);
+\COPY tmp_surveyoption (id, surveyquestion, description) FROM responses.csv WITH DELIMITER ';' HEADER CSV;
+INSERT INTO surveyoption (id, surveyquestion, description)
+    SELECT id, surveyquestion, description
+    FROM tmp_surveyoption
+    ON CONFLICT ON CONSTRAINT "PK_surveyoption" DO
+        UPDATE SET surveyquestion=EXCLUDED.surveyquestion, description=EXCLUDED.description;
diff --git a/sql/migration.sql b/sql/migration.sql
index 423dc0d..a67e071 100644
--- a/sql/migration.sql
+++ b/sql/migration.sql
@@ -180,6 +180,34 @@ END;
 $BODY$
 language plpgsql;
 
+--
+-- #5
+--
+CREATE OR REPLACE FUNCTION upgrade20240612() RETURNS boolean AS $BODY$
+BEGIN
+    ALTER TABLE surveyoption ADD created timestamp DEFAULT now() NULL;
+    ALTER TABLE surveyoption ALTER COLUMN description SET NOT NULL;
+    UPDATE surveyoption SET created = now();
+    ALTER TABLE surveyoption ADD CONSTRAINT "UK_surveyoption" UNIQUE (surveyquestion, description);
+    ALTER TABLE surveyoption RENAME CONSTRAINT pk_surveyoption TO "PK_surveyoption";
+    ALTER TABLE surveyoption RENAME CONSTRAINT fk_surveyoption_surveyquestion TO "FK_surveyoption_surveyquestion";
+
+    ALTER TABLE surveyquestion ADD created timestamp DEFAULT now() NULL;
+    ALTER TABLE surveyquestion ALTER COLUMN description TYPE VARCHAR USING description::VARCHAR;
+    ALTER TABLE surveyquestion ALTER COLUMN description SET NOT NULL;
+    UPDATE surveyquestion SET created = now();
+    ALTER TABLE surveyquestion RENAME CONSTRAINT pk_surveyquestion TO "PK_surveyquestion";
+
+    ALTER TABLE useremail RENAME CONSTRAINT pk_useremail TO "PK_useremail";
+
+    ALTER TABLE userresponse RENAME CONSTRAINT pk_userresponse TO "PK_userresponse";
+    ALTER TABLE userresponse RENAME CONSTRAINT fk_userresponse_surveyoption TO "FK_userresponse_surveyoption";
+    ALTER TABLE userresponse RENAME CONSTRAINT fk_userresponse_surveyquestion TO "FK_userresponse_surveyquestion";
+    RETURN true;
+END;
+$BODY$
+language plpgsql;
+
 ---
 --
 -- Keep this call at the end to apply migration functions.
diff --git a/sql/responses.csv b/sql/responses.csv
deleted file mode 100644
index 173c16e..0000000
--- a/sql/responses.csv
+++ /dev/null
@@ -1,19 +0,0 @@
-surveyquestion;description
-1;Agriculteur
-1;Conseiller agricole (Chambre d'agriculture, Entreprise de service)
-1;Scientifique
-1;Journaliste
-1;Administration
-1;Enseignant
-1;Citoyen curieux (c'est bien)
-2;Internet
-2;Via le site internet d'AgroClim
-2;Via le site internet INRAE
-2;Média (TV, journaux, radio)
-2;Réseaux sociaux (Linkedin, Facebook, X…)
-2;Bouche à oreilles
-3;S'informer
-3;Enseigner
-3;Conseiller
-3;Publier un article dans les médias
-3;Faire une présentation
diff --git a/sql/schema.tables.sql b/sql/schema.tables.sql
index 47eab0f..02873ea 100644
--- a/sql/schema.tables.sql
+++ b/sql/schema.tables.sql
@@ -216,38 +216,39 @@ COMMENT ON TABLE dailyvisit IS 'Number of visits per day.';
 
 -- Tables for survey form - #5
 CREATE TABLE IF NOT EXISTS surveyquestion (
-	id SERIAL NOT NULL,
-	description VARCHAR(255) NULL,
-	CONSTRAINT PK_surveyquestion PRIMARY KEY (id)
+    id SERIAL NOT NULL,
+    description VARCHAR NOT NULL,
+    CONSTRAINT "PK_surveyquestion" PRIMARY KEY (id)
 );
 COMMENT ON TABLE surveyquestion IS 'Questions for survey form';
 
 CREATE TABLE IF NOT EXISTS surveyoption (
-	id SERIAL NOT NULL,
-	surveyquestion INT4 NOT NULL,
-	description VARCHAR NULL,
-	CONSTRAINT PK_surveyoption PRIMARY KEY (id),
-	CONSTRAINT FK_surveyoption_surveyquestion FOREIGN KEY (surveyquestion) REFERENCES surveyquestion(id)
+    id SERIAL NOT NULL,
+    surveyquestion INTEGER NOT NULL,
+    description VARCHAR NOT NULL,
+    CONSTRAINT "PK_surveyoption" PRIMARY KEY (id),
+    CONSTRAINT "FK_surveyoption_surveyquestion" FOREIGN KEY (surveyquestion) REFERENCES surveyquestion(id),
+    CONSTRAINT "UK_surveyoption" UNIQUE (surveyquestion, description)
 );
 COMMENT ON TABLE surveyoption IS 'Options responses for questions.';
 
 CREATE TABLE IF NOT EXISTS userresponse (
-	id SERIAL NOT NULL,
-	datetime TIMESTAMP NOT NULL,
-	surveyquestion INT4 NOT NULL,
-	surveyoption INT4 NULL,
-	othertext VARCHAR NULL,
-	CONSTRAINT PK_userresponse PRIMARY KEY (id),
-	CONSTRAINT FK_userresponse_surveyquestion FOREIGN KEY (surveyquestion) REFERENCES surveyquestion(id),
-	CONSTRAINT FK_userresponse_surveyoption FOREIGN KEY (surveyoption) REFERENCES surveyoption(id)
+    id SERIAL NOT NULL,
+    datetime TIMESTAMP NOT NULL,
+    surveyquestion INTEGER NOT NULL,
+    surveyoption INTEGER NULL,
+    othertext VARCHAR NULL,
+    CONSTRAINT "PK_userresponse" PRIMARY KEY (id),
+    CONSTRAINT "FK_userresponse_surveyquestion" FOREIGN KEY (surveyquestion) REFERENCES surveyquestion(id),
+    CONSTRAINT "FK_userresponse_surveyoption" FOREIGN KEY (surveyoption) REFERENCES surveyoption(id)
 );
 COMMENT ON TABLE userresponse IS 'Responses of user to questions, and other text if present';
 
 CREATE TABLE IF NOT EXISTS useremail (
-	id SERIAL NOT NULL,
-	datetime TIMESTAMP NOT NULL,
-	email VARCHAR NOT NULL,
-	CONSTRAINT PK_useremail PRIMARY KEY (id)
+    id SERIAL NOT NULL,
+    datetime TIMESTAMP NOT NULL,
+    email VARCHAR NOT NULL,
+    CONSTRAINT "PK_useremail" PRIMARY KEY (id)
 );
 COMMENT ON TABLE useremail IS 'Simple table for register email of user, when he fills out survey';
 
@@ -260,17 +261,17 @@ AS SELECT l.languagetag,
 
 CREATE MATERIALIZED VIEW IF NOT EXISTS v_pra_dailyvalue AS
 SELECT
-	MIN(d.id) AS id,
-	d.indicator,
-	cp.pra,
-	d.date,
-	SUM(d.computedvalue * cp.ratio) AS computedvalue,
-	SUM(d.comparedvalue * cp.ratio) AS comparedvalue,
-	SUM(n.q5 * cp.ratio) AS q5,
-	SUM(n.medianvalue * cp.ratio) AS medianvalue,
-	SUM(n.q95 * cp.ratio) AS q95
+    MIN(d.id) AS id,
+    d.indicator,
+    cp.pra,
+    d.date,
+    SUM(d.computedvalue * cp.ratio) AS computedvalue,
+    SUM(d.comparedvalue * cp.ratio) AS comparedvalue,
+    SUM(n.q5 * cp.ratio) AS q5,
+    SUM(n.medianvalue * cp.ratio) AS medianvalue,
+    SUM(n.q95 * cp.ratio) AS q95
 FROM dailyvalue AS d
-	JOIN cellpra AS cp ON cp.cell=d.cell
-	LEFT JOIN normalvalue AS n ON n.cell=d.cell AND n.doy=EXTRACT(DOY FROM d.date) AND n.indicator=d.indicator
+    JOIN cellpra AS cp ON cp.cell=d.cell
+    LEFT JOIN normalvalue AS n ON n.cell=d.cell AND n.doy=EXTRACT(DOY FROM d.date) AND n.indicator=d.indicator
 GROUP BY pra, d.indicator, date;
 COMMENT ON VIEW v_pra_dailyvalue IS 'Daily values weighted by PRA.';
diff --git a/sql/surveyoptions.csv b/sql/surveyoptions.csv
new file mode 100644
index 0000000..49075dd
--- /dev/null
+++ b/sql/surveyoptions.csv
@@ -0,0 +1,19 @@
+id;surveyquestion;description
+1;1;Agriculteur
+2;1;Conseiller agricole (Chambre d'agriculture, Entreprise de service)
+3;1;Scientifique
+4;1;Journaliste
+5;1;Administration
+6;1;Enseignant
+7;1;Citoyen curieux (c'est bien)
+8;2;Internet
+9;2;Via le site internet d'AgroClim
+10;2;Via le site internet INRAE
+11;2;Média (TV, journaux, radio)
+12;2;Réseaux sociaux (Linkedin, Facebook, X…)
+13;2;Bouche à oreilles
+14;3;S'informer
+15;3;Enseigner
+16;3;Conseiller
+17;3;Publier un article dans les médias
+18;3;Faire une présentation
\ No newline at end of file
diff --git a/sql/questions.csv b/sql/surveyquestions.csv
similarity index 100%
rename from sql/questions.csv
rename to sql/surveyquestions.csv
-- 
GitLab