diff --git a/config/pmd-suppressions.properties b/config/pmd-suppressions.properties index 11439c609a026b20bde6d2eed37aa7a4777cabd7..a22f2263faba8628c510bb2b0f9cde2bb32e2e57 100644 --- a/config/pmd-suppressions.properties +++ b/config/pmd-suppressions.properties @@ -8,9 +8,17 @@ fr.agrometinfo.www.shared.dto.IndicatorDTOBeanJsonDeserializerImpl=UnnecessaryIm fr.agrometinfo.www.shared.dto.IndicatorDTOBeanJsonSerializerImpl=UnnecessaryImport fr.agrometinfo.www.shared.dto.IndicatorDTO_MapperImpl=UnnecessaryImport fr.agrometinfo.www.shared.dto.MessageDTOBeanJsonSerializerImpl=UnnecessaryImport +fr.agrometinfo.www.shared.dto.SurveyResponseDTOBeanJsonSerializerImpl=UnnecessaryImport fr.agrometinfo.www.shared.dto.PeriodDTOBeanJsonDeserializerImpl=UnnecessaryImport fr.agrometinfo.www.shared.dto.PeriodDTOBeanJsonSerializerImpl=UnnecessaryImport fr.agrometinfo.www.shared.dto.PeriodDTO_MapperImpl=UnnecessaryImport +fr.agrometinfo.www.shared.dto.SurveyQuestionDTOBeanJsonDeserializerImpl=UnnecessaryImport +fr.agrometinfo.www.shared.dto.SurveyQuestionDTOBeanJsonSerializerImpl=UnnecessaryImport +fr.agrometinfo.www.shared.dto.SurveyQuestionDTO_MapperImpl=UnnecessaryImport +fr.agrometinfo.www.shared.dto.SurveyOptionDTOBeanJsonSerializerImpl=UnnecessaryImport +fr.agrometinfo.www.shared.dto.SurveyOptionDTOBeanJsonDeserializerImpl=UnnecessaryImport +fr.agrometinfo.www.shared.dto.SurveyOptionDTO_MapperImpl=UnnecessaryImport +fr.agrometinfo.www.shared.service.SurveyOptionDTO_MapperImpl=UnnecessaryImport fr.agrometinfo.www.shared.dto.SimpleFeatureBeanJsonDeserializerImpl=UnnecessaryImport fr.agrometinfo.www.shared.dto.SimpleFeatureBeanJsonSerializerImpl=UnnecessaryImport fr.agrometinfo.www.shared.dto.SummaryDTOBeanJsonDeserializerImpl=UnnecessaryImport @@ -18,6 +26,9 @@ fr.agrometinfo.www.shared.dto.SummaryDTOBeanJsonSerializerImpl=UnnecessaryImport fr.agrometinfo.www.shared.dto.SummaryDTO_MapperImpl=UnnecessaryImport fr.agrometinfo.www.shared.service.ApplicationServiceFactory=UnnecessaryImport fr.agrometinfo.www.shared.service.IndicatorServiceFactory=UnnecessaryImport +fr.agrometinfo.www.shared.service.SurveyFormServiceFactory=UnnecessaryImport +fr.agrometinfo.www.shared.service.SurveyQuestionDTO_MapperImpl=UnnecessaryImport +fr.agrometinfo.www.shared.service.SurveyOptionDTOBeanJsonDeserializerImpl=UnnecessaryImport org.geojson.FeatureBeanJsonDeserializerImpl=UnnecessaryImport org.geojson.FeatureBeanJsonSerializerImpl=UnnecessaryImport org.geojson.FeatureCollectionBeanJsonDeserializerImpl=UnnecessaryImport diff --git a/pom.xml b/pom.xml index 5cfaed5bbd2b7a80bb1833919e373070ab669038..ee1f2a43d0dcff775eec437cd37a1bdc575e5131 100644 --- a/pom.xml +++ b/pom.xml @@ -433,6 +433,7 @@ <failOnViolation>true</failOnViolation> <propertyExpansion>basedir=${project.basedir}</propertyExpansion> <violationSeverity>warning</violationSeverity> + <consoleOutput>true</consoleOutput> </configuration> </plugin> <plugin> diff --git a/sql/drop.sql b/sql/drop.sql index ee674b8e63962ad585a1efb419d6870178047f00..24a594226e87e68eb646db987652eb0d95c29a7a 100644 --- a/sql/drop.sql +++ b/sql/drop.sql @@ -10,3 +10,7 @@ DROP TABLE period; DROP TABLE i18n; DROP TABLE i18nkey; DROP TABLE locale; +DROP TABLE userresponse; +DROP TABLE surveyoption; +DROP TABLE surveyquestion; +DROP TABLE useremail; diff --git a/sql/init_data.h2.sql b/sql/init_data.h2.sql index c89864532000e5c3071d864534c454efa80ad4bc..c40a7c014d33c74508b1bcb607abf1a24e87c520 100644 --- a/sql/init_data.h2.sql +++ b/sql/init_data.h2.sql @@ -127,3 +127,9 @@ 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'); + +INSERT INTO surveyoption(surveyquestion, description) + SELECT * FROM CSVREAD('../sql/responses.csv', null, 'fieldSeparator=;'); + diff --git a/sql/init_data.postgresql.sql b/sql/init_data.postgresql.sql index 6648baa2a6c92daf1a8a1e7bfe656f6875315b56..4a607133135e996c0c7bd05f64b570afc9301475 100644 --- a/sql/init_data.postgresql.sql +++ b/sql/init_data.postgresql.sql @@ -1,6 +1,19 @@ -- Initialization script for PostgreSQL database with sample data -- also do update. +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, @@ -155,3 +168,11 @@ INSERT INTO normalvalue (indicator, cell, doy, medianvalue, q5, q95) JOIN period AS p ON p.id=i.period WHERE p.code=t.period ON CONFLICT ON CONSTRAINT "UK_normalvalue" DO NOTHING; + +-- questions +\COPY surveyquestion(id, description) FROM questions.csv WITH DELIMITER ',' HEADER CSV; + +-- responses +-- WARNING : responses CSV file use semicolon (« ; ») as separator !! +\COPY surveyoption(surveyquestion, description) FROM responses.csv WITH DELIMITER ';' HEADER CSV; + diff --git a/sql/migration.sql b/sql/migration.sql index cc5cb5de1cf70c003b4cf2e4aedf4b45b4c18836..423dc0df5163deeae232d83fcfb9e3f32835ce2f 100644 --- a/sql/migration.sql +++ b/sql/migration.sql @@ -85,65 +85,6 @@ END $$ LANGUAGE plpgsql; COMMENT ON FUNCTION drop_applied_migration_functions() IS 'Purge database from applied migration functions.'; ---- ---- #8 ---- -CREATE OR REPLACE FUNCTION upgrade20231023() RETURNS boolean AS $BODY$ -BEGIN - ALTER TABLE indicator DROP COLUMN colorsequence; - DROP TABLE colorsequence; - ALTER TABLE indicator ADD COLUMN colorsequencename VARCHAR; - ALTER TABLE indicator ADD COLUMN nbofclasses INTEGER; - CREATE TYPE QUANTILETYPE AS ENUM('CENTILES_05', 'QUANTILES', 'DECILES', 'QUINTILES', 'QUARTILES'); - ALTER TABLE indicator ADD COLUMN quantiletype QUANTILETYPE; - UPDATE indicator SET quantiletype='QUINTILES'; - UPDATE indicator SET colorsequencename='Precipitation' WHERE code='rainsum'; - UPDATE indicator SET colorsequencename='Blues' WHERE code='frostdaystmin'; - UPDATE indicator SET colorsequencename='Temperature' WHERE code IN ('mint', 'meant', 'maxt'); - UPDATE indicator SET colorsequencename='YlOrRd' WHERE code IN ('hdaystmax', 'hdaystmax1'); - ALTER TABLE indicator ADD CONSTRAINT "CK_indicator_colorsequence" CHECK (nbofclasses IS NOT NULL OR quantiletype IS NOT NULL); - RETURN true; -END -$BODY$ -language plpgsql; - ---- ---- #8 ---- -CREATE OR REPLACE FUNCTION upgrade20231030() RETURNS boolean AS $BODY$ -BEGIN - ALTER TABLE "indicator" ALTER COLUMN quantiletype TYPE VARCHAR USING QUANTILETYPE::VARCHAR; - DROP TYPE QUANTILETYPE; - CREATE TYPE QUANTILETYPE AS ENUM('CENTILES_05', 'QUANTILES', 'DECILES', 'QUINTILES', 'QUARTILES', 'SEXTILES'); - ALTER TABLE "indicator" ALTER COLUMN quantiletype TYPE QUANTILETYPE USING quantiletype::quantiletype; - UPDATE indicator SET quantiletype='SEXTILES'; - --- #12 - CREATE TYPE AGGREGATIONTYPE AS ENUM('AVG', 'MAX'); - ALTER TABLE "indicator" ADD COLUMN aggregationtype AGGREGATIONTYPE; - UPDATE indicator SET unit='mm' WHERE code IN ('rainsum'); - UPDATE indicator SET unit='j' WHERE code IN ('frostdaystmin', 'hdaystmax', 'hdaystmax1'); - UPDATE indicator SET unit='°C' WHERE code IN ('maxt', 'meant', 'mint'); - UPDATE indicator SET aggregationtype='MAX' WHERE code IN ('frostdaystmin', 'hdaystmax', 'hdaystmax1', 'rainsum'); - UPDATE indicator SET aggregationtype='AVG' WHERE code IN ('maxt', 'meant', 'mint'); - ALTER TABLE "indicator" ALTER COLUMN aggregationtype SET NOT NULL; - --- #8 - UPDATE indicator SET colorsequencename='TemperatureReversed' WHERE code IN ('mint', 'meant', 'maxt'); - RETURN true; -END -$BODY$ -language plpgsql; - --- --- 31 --- -CREATE OR REPLACE FUNCTION upgrade20231215() RETURNS boolean AS $BODY$ -BEGIN - INSERT INTO department (id, "name", region) VALUES(75, '75 - Paris', 75); - RETURN true; -END -$BODY$ -language plpgsql; - -- -- 47 -- @@ -232,8 +173,8 @@ language plpgsql; CREATE OR REPLACE FUNCTION upgrade20240513() RETURNS boolean AS $BODY$ BEGIN UPDATE dailyvalue AS d - FROM normalvalue AS n ON n.indicator=d.indicator AND n.cell=d.cell AND n.doy=EXTRACT(DOY FROM d.date) - SET d.comparedvalue=COALESCE(d.computedvalue - n."medianvalue", 0); + SET comparedvalue=COALESCE(d.computedvalue - n."medianvalue", 0) + FROM normalvalue AS n WHERE n.indicator=d.indicator AND n.cell=d.cell AND n.doy=EXTRACT(DOY FROM d.date); RETURN true; END; $BODY$ diff --git a/sql/questions.csv b/sql/questions.csv new file mode 100644 index 0000000000000000000000000000000000000000..f85176616d008b40ef3fa639cc9b17d949742a28 --- /dev/null +++ b/sql/questions.csv @@ -0,0 +1,4 @@ +id,description +1,Quelle est votre profession ? +2,Comment avez-vous connu AgroMetInfo ? +3,Dans quel but voulez-vous utiliser cette application ? diff --git a/sql/responses.csv b/sql/responses.csv new file mode 100644 index 0000000000000000000000000000000000000000..173c16eeba41b7ae9e819305cac4d8d80c9f6452 --- /dev/null +++ b/sql/responses.csv @@ -0,0 +1,19 @@ +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 77eff33edcc7ade9a4851183ae2f56ac837bca37..47eab0f8cc02e07a1e7e79db315edd5f14833eeb 100644 --- a/sql/schema.tables.sql +++ b/sql/schema.tables.sql @@ -214,6 +214,43 @@ CREATE TABLE IF NOT EXISTS dailyvisit( ); 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) +); +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) +); +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) +); +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) +); +COMMENT ON TABLE useremail IS 'Simple table for register email of user, when he fills out survey'; + CREATE OR REPLACE VIEW v_i18n AS SELECT l.languagetag, i.i18nkey, diff --git a/www-client/src/main/java/fr/agrometinfo/www/client/App.java b/www-client/src/main/java/fr/agrometinfo/www/client/App.java index 920e6230623c4ed6296263f11a65335d47a755fa..542e4c03fad3a1501ee0fdf566b8212e1a1b5302 100644 --- a/www-client/src/main/java/fr/agrometinfo/www/client/App.java +++ b/www-client/src/main/java/fr/agrometinfo/www/client/App.java @@ -22,6 +22,7 @@ import fr.agrometinfo.www.client.i18n.AppMessages; import fr.agrometinfo.www.client.presenter.LayoutPresenter; import fr.agrometinfo.www.client.util.ApplicationUtils; import fr.agrometinfo.www.shared.service.ApplicationServiceFactory; +import fr.agrometinfo.www.client.presenter.SurveyPresenter; /** * Entry point classes define <code>onModuleLoad()</code>. @@ -82,7 +83,6 @@ public class App implements EntryPoint { public static EventBus getEventBus() { return EVENT_BUS; } - /** * This is the entry point method. */ @@ -100,5 +100,8 @@ public class App implements EntryPoint { Charba.enable(); new LayoutPresenter().start(); + + // display survey form + new SurveyPresenter().start(); } } diff --git a/www-client/src/main/java/fr/agrometinfo/www/client/i18n/AppConstants.java b/www-client/src/main/java/fr/agrometinfo/www/client/i18n/AppConstants.java index ec07adbce5689c43834798e29625b87e70cd0fe7..6a0dee6eb73b3c7fb1680f423366df9272444150 100644 --- a/www-client/src/main/java/fr/agrometinfo/www/client/i18n/AppConstants.java +++ b/www-client/src/main/java/fr/agrometinfo/www/client/i18n/AppConstants.java @@ -183,6 +183,12 @@ public interface AppConstants extends com.google.gwt.i18n.client.ConstantsWithLo @DefaultStringValue("HTTP status text:") String failureStatusText(); + /** + * @return translation + */ + @DefaultStringValue("Ignorer") + String ignore(); + /** * @return translation */ @@ -314,8 +320,8 @@ public interface AppConstants extends com.google.gwt.i18n.client.ConstantsWithLo /** * @return translation */ - @DefaultStringValue("Toggle right panel") - String toggleRightPanel(); + @DefaultStringValue("Formulaire d'enquête") + String surveyFormTitle(); /** * @return translation @@ -327,16 +333,82 @@ public interface AppConstants extends com.google.gwt.i18n.client.ConstantsWithLo + "or use the contact form.") String welcomeBody(); + /** + * @return translation + */ + @DefaultStringValue("Welcome to the new version of AgroMetInfo !<br/>" + + "To help us improve the application and make it as responsive as possible to your needs, " + + "<b>please fill in the short survey below</b> (it will take 2 minutes maximum).<br/>" + + "All information is anonymous, but will help us to better understand how you use the application." + + "<br/><b>Don't hesitate to give us your email address</b> " + + "so that we can keep you informed of all developments in the coming months and years." + + "<br/><br/>The AgroMetInfo development team") + String surveyFormDescription(); + + /** + * @return translation + */ + @DefaultStringValue("Your e-mail address (optional) :") + String surveyFromEmailDescription(); + /** * @return translation */ @DefaultStringValue("Welcome to AgroMetInfo") String welcomeTitle(); + /** + * @return translation + */ + @DefaultStringValue("Other") + String surveyFormOtherTextCheckbox(); + + /** + * @return translation + */ + @DefaultStringValue("You must click on checkbox above for activate this input field.") + String surveyFormOtherTextTooltip(); + + /** + * @return translation + */ + @DefaultStringValue("Your responses have been recorded.") + String surveyFormSuccess(); + + /** + * @return translation + */ + @DefaultStringValue("Your responses cannot been recorded, but you can use AgroMetInfo application.") + String surveyFormFail(); + + /** + * @return translation + */ + @DefaultStringValue("Toggle right panel") + String toggleRightPanel(); + + /** + * @return translation + */ + @DefaultStringValue("Profile and settings") + String userProfile(); + + /** + * @return translation + */ + @DefaultStringValue("Valider") + String validate(); + + /** + * @return translation + */ + @DefaultStringValue("You must be identified to access AgroMetInfo due to Meteo-France agreements related to " + + "SAFRAN data exchanges with AgroClim.") + String whyConnectionIsRequired(); + /** * @return translation */ @DefaultStringValue("Yes") String yes(); - } diff --git a/www-client/src/main/java/fr/agrometinfo/www/client/presenter/SurveyPresenter.java b/www-client/src/main/java/fr/agrometinfo/www/client/presenter/SurveyPresenter.java new file mode 100644 index 0000000000000000000000000000000000000000..00b81aa2a622d310136ad87c1b2d974bee6f502c --- /dev/null +++ b/www-client/src/main/java/fr/agrometinfo/www/client/presenter/SurveyPresenter.java @@ -0,0 +1,138 @@ +package fr.agrometinfo.www.client.presenter; + +import java.util.ArrayList; +import java.util.List; + +import org.dominokit.rest.shared.request.FailedResponseBean; + +import com.google.gwt.core.client.GWT; + +import fr.agrometinfo.www.client.view.BaseView; +import fr.agrometinfo.www.client.view.SurveyView; +import fr.agrometinfo.www.shared.dto.SurveyQuestionDTO; +import fr.agrometinfo.www.shared.dto.SurveyOptionDTO; +import fr.agrometinfo.www.shared.dto.SurveyResponseDTO; +import fr.agrometinfo.www.shared.service.SurveyFormServiceFactory; + +import java.util.HashMap; +import java.util.Map; + +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * Presenter for survey form. + * + * @author Olivier Maury + * @author Jérémie Décome + */ +public final class SurveyPresenter implements Presenter { + + /** + * Interface for the related view. + */ + public interface View extends BaseView<SurveyPresenter> { + /** + * Display notification on failure. + * + * @param failedResponse failure response + */ + void failureNotification(FailedResponseBean failedResponse); + /** + * Close modal. + */ + void close(); + /** + * @param map list of availables responses. + */ + void setResponses(Map<SurveyQuestionDTO, List<SurveyOptionDTO>> map); + /** + * Showing success login. + * @param msg returned message from webservice + */ + void displaySuccessLogin(String msg); + } + + /** + * Related view. + */ + private final SurveyView view = new SurveyView(); + /** List of responses from webservice. */ + private List<SurveyOptionDTO> responses; + /** List of questions, extracted from responses list. */ + private List<SurveyQuestionDTO> questions; + /** + * Set responses and extract questions from responses list. + * @param list list of responses + */ + private void setResponses(final List<SurveyOptionDTO> list) { + GWT.log("SurveyPresenter.setResponses(" + list.size() + ") enter"); + this.responses = list; + final Map<SurveyQuestionDTO, List<SurveyOptionDTO>> responsesMap = new HashMap<>(); + + // extract questions from responses via webservice + for (final SurveyOptionDTO dto : list) { + if (!responsesMap.containsKey(dto.getQuestion())) { + responsesMap.put(dto.getQuestion(), new ArrayList<SurveyOptionDTO>()); + } + responsesMap.get(dto.getQuestion()).add(dto); + } + this.questions = new ArrayList<>(responsesMap.keySet()); + view.setResponses(responsesMap); + + view.init(); + GWT.log("SurveyPresenter.setResponses() end"); + } + @Override + public void start() { + view.setPresenter(this); + + SurveyFormServiceFactory.INSTANCE.getResponses() + .onSuccess(this::setResponses) + .onFailed(view::failureNotification) + .send(); + } + public void insertUserResponses( + final List<Long> responsesRef, + final Map<Long, String> otherTextMap, + final String email) { + + // processing of predefined responses + final Set<SurveyQuestionDTO> questionsDto = new LinkedHashSet<>(); + final Set<SurveyOptionDTO> responsesDto = new LinkedHashSet<>(); + + for (final Long ref : responsesRef) { + // getting response by reference + final SurveyOptionDTO rDto = this.responses.stream().filter(r -> r.getId() == ref).findFirst().get(); + if (rDto != null) { + responsesDto.add(rDto); + + // getting question corresponding of response + questionsDto.add(rDto.getQuestion()); + } + } + // addition of the question, if it only has a free answer + this.questions.forEach((q) -> { + if (otherTextMap.containsKey(q.getId())) { + questionsDto.add(q); + } + }); + + // call to webservice for inserting data + final SurveyResponseDTO data = new SurveyResponseDTO( + new ArrayList<SurveyQuestionDTO>(questionsDto), + new ArrayList<SurveyOptionDTO>(responsesDto), + otherTextMap); + + // if user has entered their email address, it is added to object + if (!email.isEmpty()) { + data.setEmail(email); + } + + SurveyFormServiceFactory.INSTANCE.insertAllResponses(data) + .onSuccess(view::displaySuccessLogin) + .onFailed(view::failureNotification) + .send(); + } + +} diff --git a/www-client/src/main/java/fr/agrometinfo/www/client/view/AbstractBaseView.java b/www-client/src/main/java/fr/agrometinfo/www/client/view/AbstractBaseView.java index 789fd1cddb005553ac903ffa01b41a9a0656db5e..5521df97505cc5131da12578a68a035002e8fbbd 100644 --- a/www-client/src/main/java/fr/agrometinfo/www/client/view/AbstractBaseView.java +++ b/www-client/src/main/java/fr/agrometinfo/www/client/view/AbstractBaseView.java @@ -1,5 +1,16 @@ package fr.agrometinfo.www.client.view; +import java.util.StringJoiner; + +import org.dominokit.domino.ui.notifications.Notification; +import org.dominokit.rest.shared.request.FailedResponseBean; + +import com.google.gwt.core.client.GWT; + +import elemental2.dom.DomGlobal; +import fr.agrometinfo.www.client.i18n.AppConstants; +import fr.agrometinfo.www.client.i18n.AppMessages; + /** * Abstract class for all view implementations. * @@ -12,6 +23,30 @@ public abstract class AbstractBaseView<T> implements BaseView<T> { * Related presenter. */ private T presenter; + /** + * I18N constants. + */ + private static final AppConstants CSTS = GWT.create(AppConstants.class); + /** + * I18N messages. + */ + private static final AppMessages MSGS = GWT.create(AppMessages.class); + /** + * + * @param failure + * @return details of failure object + */ + protected static String getDetails(final FailedResponseBean failure) { + final StringJoiner sj = new StringJoiner("<br/>"); + sj.add(CSTS.failureBody()); + sj.add(failure.getBody()); + sj.add(CSTS.failureHeaders()); + sj.add(failure.getHeaders().toString()); + sj.add(MSGS.failureStatusCode(failure.getStatusCode())); + sj.add(CSTS.failureStatusText()); + sj.add(failure.getStatusText()); + return sj.toString(); + } /** * @return related presenter @@ -19,10 +54,18 @@ public abstract class AbstractBaseView<T> implements BaseView<T> { protected final T getPresenter() { return presenter; } - + /** */ @Override public final void setPresenter(final T value) { this.presenter = value; } - + /** + * Show notification with message. + * @param msg message to display + * @return notification object + */ + protected Notification notification(final String msg) { + DomGlobal.console.info("Notification!"); + return Notification.create(msg).setPosition(Notification.TOP_LEFT).show(); + } } diff --git a/www-client/src/main/java/fr/agrometinfo/www/client/view/LayoutView.java b/www-client/src/main/java/fr/agrometinfo/www/client/view/LayoutView.java index 5f7e8ba0cb5dc30ee7d10d08b4ea8cfd1d23a29e..7380db74dcb09afcfbe03e4ea272ce323798d80c 100644 --- a/www-client/src/main/java/fr/agrometinfo/www/client/view/LayoutView.java +++ b/www-client/src/main/java/fr/agrometinfo/www/client/view/LayoutView.java @@ -6,7 +6,6 @@ import static org.jboss.elemento.Elements.img; import static org.jboss.elemento.Elements.li; import java.util.List; -import java.util.StringJoiner; import org.dominokit.domino.ui.grid.flex.FlexItem; import org.dominokit.domino.ui.icons.Icons; @@ -16,7 +15,6 @@ import org.dominokit.domino.ui.loaders.Loader; import org.dominokit.domino.ui.loaders.LoaderEffect; import org.dominokit.domino.ui.menu.Menu; import org.dominokit.domino.ui.menu.MenuItem; -import org.dominokit.domino.ui.notifications.Notification; import org.dominokit.domino.ui.style.Styles; import org.dominokit.domino.ui.utils.DominoElement; import org.dominokit.domino.ui.utils.DominoUIConfig; @@ -89,17 +87,6 @@ implements LayoutPresenter.View, LoadingHandler { addMenuItem(menu, text, icon, runnable); } - private static String getDetails(final FailedResponseBean failure) { - final StringJoiner sj = new StringJoiner("<br/>"); - sj.add(CSTS.failureBody()); - sj.add(failure.getBody()); - sj.add(CSTS.failureHeaders()); - sj.add(failure.getHeaders().toString()); - sj.add(MSGS.failureStatusCode(failure.getStatusCode())); - sj.add(CSTS.failureStatusText()); - sj.add(failure.getStatusText()); - return sj.toString(); - } /** * @param relativePath documentation path relative to documentation root @@ -389,11 +376,6 @@ implements LayoutPresenter.View, LoadingHandler { return layout.isRightPanelVisible(); } - private Notification notification(final String msg) { - DomGlobal.console.info("Notification! " + msg); - return Notification.create(msg).setPosition(Notification.TOP_LEFT).show(); - } - @Override public void onLoading(final LoadingEvent event) { GWT.log("LayoutView.onLoading() " + event); 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 new file mode 100644 index 0000000000000000000000000000000000000000..b98369a45295171993ca500bc06578430d0f00de --- /dev/null +++ b/www-client/src/main/java/fr/agrometinfo/www/client/view/SurveyView.java @@ -0,0 +1,187 @@ +package fr.agrometinfo.www.client.view; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.ArrayList; +import java.util.HashMap; + +import org.dominokit.domino.ui.button.Button; +import org.dominokit.domino.ui.forms.CheckBox; +import org.dominokit.domino.ui.forms.TextArea; +import org.dominokit.domino.ui.forms.TextBox; +import org.dominokit.domino.ui.modals.ModalDialog; +import org.dominokit.rest.shared.request.FailedResponseBean; +import org.gwtproject.safehtml.shared.SafeHtmlBuilder; +import org.jboss.elemento.Elements; + +import com.google.gwt.core.client.GWT; +import com.google.gwt.storage.client.Storage; + +import fr.agrometinfo.www.client.i18n.AppConstants; +import fr.agrometinfo.www.client.presenter.SurveyPresenter; +import fr.agrometinfo.www.shared.dto.SurveyQuestionDTO; +import fr.agrometinfo.www.shared.dto.SurveyOptionDTO; + +/** + * View for survey form. + * + * @author Olivier Maury + * @author Jérémie Décome + */ +public final class SurveyView extends AbstractBaseView<SurveyPresenter> implements SurveyPresenter.View { + /** + * I18N constants. + */ + private static final AppConstants CSTS = GWT.create(AppConstants.class); + /** + * Is survey was already validated by user ? Key for LocalStorage. + */ + private static final String IS_VALIDATED_KEY = "isValidated"; + /** + * Level for questions. + */ + private static final int SECTION_TITLE_LEVEL = 5; + /** + * List of available responses. + */ + private Map<SurveyQuestionDTO, List<SurveyOptionDTO>> availableResponses; + /** + * The modal used to display the login form. + */ + private ModalDialog modal; + /** Validate button of modal. */ + private Button validate; + /** List of all checkbox on modal. */ + private List<CheckBox> checkBoxList = new ArrayList<>(); + /** + * E-mail field. + */ + private TextBox email; + + @Override + public void close() { + modal.close(); + } + + @Override + public void init() { + GWT.log("LoginView.init()"); + // check if survey form has not already been completed by user, via the browser's LocalStorage + final Storage ls = Storage.getLocalStorageIfSupported(); + if (ls != null && ls.getItem(IS_VALIDATED_KEY) != null && ls.getItem(IS_VALIDATED_KEY).equals("true")) { + GWT.log("Survey was already validated for this user, don't showing again"); + return; + } + + final Map<Long, TextArea> otherTextArea = new HashMap<>(); + + this.modal = ModalDialog.create(CSTS.surveyFormTitle()).large().setAutoClose(true); + this.modal + .appendChild(Elements.p() + .innerHtml(new SafeHtmlBuilder().appendHtmlConstant(CSTS.surveyFormDescription()).toSafeHtml()) + ); + + for (Map.Entry<SurveyQuestionDTO, List<SurveyOptionDTO>> entry : this.availableResponses.entrySet()) { + final SurveyQuestionDTO k = entry.getKey(); // For each question ... + + // displaying label + this.modal.appendChild(Elements.h(SECTION_TITLE_LEVEL, "• " + k.getDescription())); + + // displaying availables responses + entry.getValue().forEach((r) -> { + final CheckBox cb = CheckBox.create(r.getDescription()).id(Long.toString(r.getId())) + .css("login-checkbox"); + this.modal.appendChild(cb); + this.checkBoxList.add(cb); + }); + + // displaying free response field + final CheckBox otherCb = CheckBox.create(CSTS.surveyFormOtherTextCheckbox()) + .css("login-checkbox"); + final TextArea otherText = TextArea.create() + .setDisabled(true) + .setTooltip(CSTS.surveyFormOtherTextTooltip()) + .setRows(2) + .css("login-textarea"); + + otherCb.addChangeHandler((v) -> { + otherText.setDisabled(!otherCb.getValue()); + if (!otherText.isDisabled()) { + otherText.removeTooltip(); + } else { + otherText.setTooltip(CSTS.surveyFormOtherTextTooltip()); + } + }); + this.checkBoxList.add(otherCb); + this.modal.appendChild(otherCb).appendChild(otherText); + otherTextArea.put(k.getId(), otherText); + } + // Validate button is activ if if at least one answer is checked + this.checkBoxList.forEach((c) -> { + c.addChangeHandler((v) -> this.activateValidateButton()); + }); + + // User's e-mail address + this.modal.appendChild(Elements.p().textContent(CSTS.surveyFromEmailDescription())); + this.email = TextBox.create().setType("email"); + this.modal.appendChild(this.email); + + // Window's button + this.validate = Button.create(CSTS.validate()).linkify(); + this.validate.addClickListener((evt) -> { + final HashMap<Long, String> otherTextMap = new HashMap<>(); + + // process the predefined responses (retrieval of response references), + // excluding checkbox for free responses + final List<Long> selectedResponsesRef = this.checkBoxList.stream() + .filter((v) -> v.getValue() && v.getId().matches("[0-9]+")) + .map(v -> Long.parseLong(v.getId())).collect(Collectors.toList()); + + // process the free responses + otherTextArea.forEach((k, v) -> { + // if textarea is activ, there is (potentially) a response and if textarea is not empty + if (!v.isDisabled() && !v.getValue().equals("")) { + otherTextMap.put(k, v.getValue()); + } + }); + + this.getPresenter().insertUserResponses(selectedResponsesRef, otherTextMap, this.email.getValue()); + + ls.setItem(IS_VALIDATED_KEY, "true"); + this.modal.close(); + }); + this.validate.setDisabled(true); // Validate button is disable by default + this.modal.appendFooterChild(validate); + + final Button ignore = Button.create(CSTS.ignore()).linkify(); + ignore.addClickListener((evt) -> { + this.modal.close(); + }); + this.modal.appendFooterChild(ignore); + + this.modal.open(); + GWT.log("LoginView.init() end"); + } + + @Override + public void failureNotification(final FailedResponseBean failedResponse) { + this.notification(CSTS.surveyFormFail()); + } + + @Override + public void setResponses(final Map<SurveyQuestionDTO, List<SurveyOptionDTO>> map) { + this.availableResponses = map; + } + + @Override + public void displaySuccessLogin(final String msg) { + this.notification(CSTS.surveyFormSuccess()); + } + /** + * Enable or disable validate button depending of checkbox status. + */ + private void activateValidateButton() { + this.validate.setDisabled(!this.checkBoxList.stream().anyMatch((c) -> c.getValue())); + } +} 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 3436fb93eb3c312d0265620558ded14dc70a0729..611ec51de775bf5a9963f65d6ffb2aad93726ed4 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 @@ -33,6 +33,10 @@ 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 +logout = Se déconnecter metropolitanFrance = France métropolitaine no = Non normalComparison= Comparaison à la normale @@ -43,9 +47,21 @@ releaseNotes = Notes de version releaseNotesPath = release-notes.html reloadingApplication = Rechargement de l'application pour une nouvelle version\u2026 seePrivacyPolicy = Consultez le paragraphe « Données personnelles » dans les mentions légales. +selectPrompt = -- sélectionner -- +surveyFormTitle = Formulaire d'enquête +surveyFormDescription = Bienvenu.e.s à la nouvelle version d'AgroMetInfo !<br/>Afin de nous aider à la faire évoluer et l'adapter le plus possible à vos besoins, <b>merci de renseigner</b> la petite enquête çi dessous (cela prendra 2 minutes maximum). L'ensemble des informations est anonyme, mais nous aidera à mieux comprendre l'utilisation de l'application. 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.<br/><br/>L'équipe AgroMetInfo. +surveyFromEmailDescription = Votre adresse courriel (facultatif) : +surveyFormOtherTextCheckbox = Autre, à préciser +surveyFormOtherTextTooltip = Vous devez cliquer sur la case à cocher ci-dessus pour activer ce champ de saisie. +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. toggleRightPanel = Afficher / masquer le panneau de droite welcomeBody = AgroMetInfo est en cours de refonte.<br/>\ Pour rester au courant des évolutions, vous pouvez nous envoyer un message à \ <a href="mailto:support-agrometinfo@inrae.fr?subject=AgroMetInfo:%20contact">support-agrometinfo@inrae.fr</a> ou utiliser le formulaire de contact. welcomeTitle = Bienvenue sur AgroMetInfo 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 diff --git a/www-client/src/main/resources/fr/agrometinfo/www/client/public/style.css b/www-client/src/main/resources/fr/agrometinfo/www/client/public/style.css index bdbff6757d2439f49e3878c52edc502ef39cc5f0..4dac15b38a4b85d8db91be4b8998b54512979a16 100644 --- a/www-client/src/main/resources/fr/agrometinfo/www/client/public/style.css +++ b/www-client/src/main/resources/fr/agrometinfo/www/client/public/style.css @@ -348,3 +348,21 @@ body > .modal-backdrop { --rightsidebar-width: 600px; } } + +/* Formulaire d'enquête */ +.login-paragraph { + font-size: 15px; +} +.login-checkbox { + margin: 0 0 2px 5px; +} +.login-checkbox .field-cntr { + padding: 0 10px !important; +} +.login-checkbox label { + top: 0px !important; + margin-bottom: 0px !important; +} +.login-textarea .field-cntr { + padding-top: 5px; +} \ No newline at end of file diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/AgroMetInfoConfiguration.java b/www-server/src/main/java/fr/agrometinfo/www/server/AgroMetInfoConfiguration.java index 9e95b3a7f50f990a88bb30955c8767f658199d38..5eb7981bee22fecd61a4aa6685ec498bab57187e 100644 --- a/www-server/src/main/java/fr/agrometinfo/www/server/AgroMetInfoConfiguration.java +++ b/www-server/src/main/java/fr/agrometinfo/www/server/AgroMetInfoConfiguration.java @@ -67,7 +67,6 @@ public class AgroMetInfoConfiguration { * E-mail address for support. */ SUPPORT_EMAIL("support.email"); - /** * Key in config.properties. */ diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/dao/DaoHibernate.java b/www-server/src/main/java/fr/agrometinfo/www/server/dao/DaoHibernate.java index b9b9c20ad5c038bd46df971558c3d2c943b7fedc..48899a6cbea234de1ec8f0dbd2f080536b198000 100644 --- a/www-server/src/main/java/fr/agrometinfo/www/server/dao/DaoHibernate.java +++ b/www-server/src/main/java/fr/agrometinfo/www/server/dao/DaoHibernate.java @@ -86,7 +86,21 @@ public abstract class DaoHibernate<T> { return 0L; } } - + /** + * Delete all row in table. + */ + public void deleteAll() { + LOGGER.traceEntry(); + try (ScopedEntityManager em = getScopedEntityManager()) { + final Query q = em.createQuery("DELETE FROM " + clazz.getName() + " t"); + em.getTransaction().begin(); + q.executeUpdate(); + em.getTransaction().commit(); + } catch (final Exception e) { + LOGGER.catching(e); + } + LOGGER.traceExit(); + } /** * Use a consumer to execute JPA operations in a transaction. * @@ -304,4 +318,16 @@ public abstract class DaoHibernate<T> { protected final ScopedEntityManager getScopedEntityManager() { return PersistenceManager.getInstance().createScopedEntityManager(); } + /** + * Save a new entity in database. + * @param object + * new entity to save + */ + protected final void save(final Object object) { + try (ScopedEntityManager em = getScopedEntityManager()) { + em.executeTransaction(() -> em.persist(object)); + } catch (final Exception e) { + LOGGER.catching(e); + } + } } diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/dao/SurveyOptionDao.java b/www-server/src/main/java/fr/agrometinfo/www/server/dao/SurveyOptionDao.java new file mode 100644 index 0000000000000000000000000000000000000000..214e608a6f384cf55ba92e2fb0b315fc832f8c17 --- /dev/null +++ b/www-server/src/main/java/fr/agrometinfo/www/server/dao/SurveyOptionDao.java @@ -0,0 +1,29 @@ +package fr.agrometinfo.www.server.dao; + +import fr.agrometinfo.www.server.model.SurveyOption; + +import java.util.List; + +/** + * DAO for {@link SurveyOption}. + * @author jdecome + */ +public interface SurveyOptionDao { + /** + * Find all responses in database. + * @return all responses + */ + List<SurveyOption> findAll(); + /** + * Find all responses by question. + * @param questionRef + * @return list of responses + */ + List<SurveyOption> findAllByQuestion(long questionRef); + /** + * Get response with reference. + * @param responseRef + * @return response + */ + SurveyOption findByRef(long responseRef); +} diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/dao/SurveyOptionDaoHibernate.java b/www-server/src/main/java/fr/agrometinfo/www/server/dao/SurveyOptionDaoHibernate.java new file mode 100644 index 0000000000000000000000000000000000000000..6b98c993dfed5058a1aa473f79c322d8a625ab55 --- /dev/null +++ b/www-server/src/main/java/fr/agrometinfo/www/server/dao/SurveyOptionDaoHibernate.java @@ -0,0 +1,31 @@ +package fr.agrometinfo.www.server.dao; + +import java.util.List; +import java.util.Map; + +import fr.agrometinfo.www.server.model.SurveyOption; +import jakarta.enterprise.context.ApplicationScoped; + +/** + * Implementation of {@link SurveyOptionDao}. + * @author jdecome + * + */ +@ApplicationScoped +public final class SurveyOptionDaoHibernate extends DaoHibernate<SurveyOption> implements SurveyOptionDao { + /** + * Constructor. + */ + public SurveyOptionDaoHibernate() { + super(SurveyOption.class); + } + @Override + public List<SurveyOption> findAllByQuestion(final long questionRef) { + final String jpql = "SELECT r FROM SurveyOption r WHERE r.question.id = :ref"; + return super.findAllByJPQL(jpql, Map.of("ref", questionRef), SurveyOption.class); + } + @Override + public SurveyOption findByRef(final long responseRef) { + return super.find(responseRef); + } +} diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/dao/SurveyQuestionDao.java b/www-server/src/main/java/fr/agrometinfo/www/server/dao/SurveyQuestionDao.java new file mode 100644 index 0000000000000000000000000000000000000000..6a73dc530011898fdf5f1f82daf0692406540bee --- /dev/null +++ b/www-server/src/main/java/fr/agrometinfo/www/server/dao/SurveyQuestionDao.java @@ -0,0 +1,23 @@ +package fr.agrometinfo.www.server.dao; + +import java.util.List; + +import fr.agrometinfo.www.server.model.SurveyQuestion; + +/** + * DAO for {@link SurveyQuestion}. + * @author jdecome + * + */ +public interface SurveyQuestionDao { + /** + * @return all of Questions + */ + List<SurveyQuestion> findAll(); + /** + * Find question object by reference. + * @param questionRef question_ref + * @return SurveyQuestion object + */ + SurveyQuestion findByRef(long questionRef); +} diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/dao/SurveyQuestionDaoHibernate.java b/www-server/src/main/java/fr/agrometinfo/www/server/dao/SurveyQuestionDaoHibernate.java new file mode 100644 index 0000000000000000000000000000000000000000..0ef2956db04bdcb3c81040397ec2393e3f56d1c1 --- /dev/null +++ b/www-server/src/main/java/fr/agrometinfo/www/server/dao/SurveyQuestionDaoHibernate.java @@ -0,0 +1,23 @@ +package fr.agrometinfo.www.server.dao; + +import fr.agrometinfo.www.server.model.SurveyQuestion; +import jakarta.enterprise.context.ApplicationScoped; + +/** + * Implementation of {@link SurveyQuestionDao}. + * @author jdecome + * + */ +@ApplicationScoped +public final class SurveyQuestionDaoHibernate extends DaoHibernate<SurveyQuestion> implements SurveyQuestionDao { + /** + * Constructor. + */ + public SurveyQuestionDaoHibernate() { + super(SurveyQuestion.class); + } + @Override + public SurveyQuestion findByRef(final long ref) { + return super.find(ref); + } +} diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/dao/UserEmailDao.java b/www-server/src/main/java/fr/agrometinfo/www/server/dao/UserEmailDao.java new file mode 100644 index 0000000000000000000000000000000000000000..e38e5b7f02ae81ec346987e36b3f012c16f82dc3 --- /dev/null +++ b/www-server/src/main/java/fr/agrometinfo/www/server/dao/UserEmailDao.java @@ -0,0 +1,29 @@ +package fr.agrometinfo.www.server.dao; + +import java.time.LocalDateTime; +import java.util.List; + +import fr.agrometinfo.www.server.model.UserEmail; + +/** + * DAO for {@link UserEmail}. + * @author jdecome + * + */ +public interface UserEmailDao { + /** + * Insert e-mail address in table. + * @param email + * @param datetime provided localdatetime + */ + void insertEmailUserAddress(String email, LocalDateTime datetime); + /** + * Getting all email address in database. + * @return list of email address + */ + List<UserEmail> getEmailAddressList(); + /** + * Delete all rows in table. + */ + void deleteAll(); +} diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/dao/UserEmailDaoHibernate.java b/www-server/src/main/java/fr/agrometinfo/www/server/dao/UserEmailDaoHibernate.java new file mode 100644 index 0000000000000000000000000000000000000000..e45b99c28e61a9daff90964f9e87610535fbe890 --- /dev/null +++ b/www-server/src/main/java/fr/agrometinfo/www/server/dao/UserEmailDaoHibernate.java @@ -0,0 +1,32 @@ +package fr.agrometinfo.www.server.dao; + +import java.time.LocalDateTime; +import java.util.List; + +import fr.agrometinfo.www.server.model.UserEmail; +import jakarta.enterprise.context.ApplicationScoped; + +/** + * Implementation of {@link UserEmailDao}. + * @author jdecome + * + */ +@ApplicationScoped +public final class UserEmailDaoHibernate extends DaoHibernate<UserEmail> implements UserEmailDao { + /** Default constructor. */ + public UserEmailDaoHibernate() { + super(UserEmail.class); + } + @Override + public void insertEmailUserAddress(final String email, final LocalDateTime datetime) { + final UserEmail um = new UserEmail(); + um.setEmail(email); + um.setDatetime(datetime); + this.save(um); + } + @Override + public List<UserEmail> getEmailAddressList() { + return super.findAll(); + } + +} diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/dao/UserResponsesDao.java b/www-server/src/main/java/fr/agrometinfo/www/server/dao/UserResponsesDao.java new file mode 100644 index 0000000000000000000000000000000000000000..3a364db7cc2685cf9378f24998a5fd7f06e5bfca --- /dev/null +++ b/www-server/src/main/java/fr/agrometinfo/www/server/dao/UserResponsesDao.java @@ -0,0 +1,49 @@ +package fr.agrometinfo.www.server.dao; + +import java.time.LocalDateTime; +import java.util.List; + +import fr.agrometinfo.www.server.model.SurveyQuestion; +import fr.agrometinfo.www.server.model.SurveyOption; +import fr.agrometinfo.www.server.model.UserResponse; +/** + * DAO for {@link UserResponse}. + * @author jdecome + */ +public interface UserResponsesDao { + /** + * Get all user responses by question. + * @param questionRef + * @return list of user's responses + */ + List<UserResponse> findAllByQuestion(long questionRef); + /** + * Insert response or text for specified question.<br> + * If r is provided, {@code otherText} must be null.<br> + * If otherText is provided, {@code r} must be null.<br> + * datetime is defined by {@link LocalDateTime#now} in this method. + * @param q question to answer + * @param r response + * @param otherText text if response is other + */ + void insertResponse(SurveyQuestion q, SurveyOption r, String otherText); + /** + * Insert response or text for specified question.<br> + * If r is provided, {@code otherText} must be null.<br> + * If otherText is provided, {@code r} must be null. + * @param q question to answer + * @param r response + * @param otherText text if response is other + * @param datetime provided localdatetime + */ + void insertResponse(SurveyQuestion q, SurveyOption r, String otherText, LocalDateTime datetime); + /** + * Get all user responses. + * @return number of responses + */ + int getNbUserResponses(); + /** + * Delete all rows in table. + */ + void deleteAll(); +} diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/dao/UserResponsesDaoHibernate.java b/www-server/src/main/java/fr/agrometinfo/www/server/dao/UserResponsesDaoHibernate.java new file mode 100644 index 0000000000000000000000000000000000000000..01cd1ff53ac68a5214d47a10d015a2acd7e6a00b --- /dev/null +++ b/www-server/src/main/java/fr/agrometinfo/www/server/dao/UserResponsesDaoHibernate.java @@ -0,0 +1,48 @@ +package fr.agrometinfo.www.server.dao; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +import fr.agrometinfo.www.server.model.SurveyQuestion; +import fr.agrometinfo.www.server.model.SurveyOption; +import fr.agrometinfo.www.server.model.UserResponse; +import jakarta.enterprise.context.ApplicationScoped; + +/** + * Implementation of {@link UserResponsesDao}. + * @author jdecome + * + */ +@ApplicationScoped +public final class UserResponsesDaoHibernate extends DaoHibernate<UserResponse> implements UserResponsesDao { + /** Default constructor. */ + public UserResponsesDaoHibernate() { + super(UserResponse.class); + } + @Override + public List<UserResponse> findAllByQuestion(final long questionRef) { + final String jpql = "SELECT u FROM UserResponse u WHERE u.question.id = :ref"; + return super.findAllByJPQL(jpql, Map.of("ref", questionRef), UserResponse.class); + } + @Override + public void insertResponse( + final SurveyQuestion q, final SurveyOption r, + final String otherText, final LocalDateTime datetime) { + final UserResponse ur = new UserResponse(); + ur.setDateTime(datetime); + ur.setQuestion(q); + ur.setOption(r); + ur.setOtherText(otherText); + + this.save(ur); + } + @Override + public void insertResponse(final SurveyQuestion q, final SurveyOption r, final String otherText) { + this.insertResponse(q, r, otherText, LocalDateTime.now()); + } + @Override + public int getNbUserResponses() { + return super.findAll().size(); + } +} diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/model/SurveyOption.java b/www-server/src/main/java/fr/agrometinfo/www/server/model/SurveyOption.java new file mode 100644 index 0000000000000000000000000000000000000000..ddc5fe7a42c1ef20c8b5e0e5055e78e3f06f1a94 --- /dev/null +++ b/www-server/src/main/java/fr/agrometinfo/www/server/model/SurveyOption.java @@ -0,0 +1,40 @@ +package fr.agrometinfo.www.server.model; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import lombok.Data; + +/** + * Option (response) for questions of survey form. + * @author jdecome + */ +@Data +@Entity +@Table(name = "surveyoption") +public class SurveyOption { + /** + * Reference of the option.<br> + * Primary key. + */ + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + @Column(name = "id") + private long id; + /** + * Question related to the option. + */ + @OneToOne + @JoinColumn(name = "surveyquestion") + private SurveyQuestion question; + /** + * Label of the option. + */ + @Column(name = "description") + private String description; +} diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/model/SurveyQuestion.java b/www-server/src/main/java/fr/agrometinfo/www/server/model/SurveyQuestion.java new file mode 100644 index 0000000000000000000000000000000000000000..6596c6d0966067a9e32cc2f252a14a05a80b672c --- /dev/null +++ b/www-server/src/main/java/fr/agrometinfo/www/server/model/SurveyQuestion.java @@ -0,0 +1,32 @@ +package fr.agrometinfo.www.server.model; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Data; + +/** + * Questions for survey form. + * @author jdecome + */ +@Data +@Entity +@Table(name = "surveyquestion") +public class SurveyQuestion { + /** + * Reference of the question.<br> + * Primary key. + */ + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + @Column(name = "id") + private long id; + /** + * Label of the question. + */ + @Column(name = "description") + private String description; +} diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/model/UserEmail.java b/www-server/src/main/java/fr/agrometinfo/www/server/model/UserEmail.java new file mode 100644 index 0000000000000000000000000000000000000000..8c54efc95ebf9fed393b0440dcd0b14ee08a1f82 --- /dev/null +++ b/www-server/src/main/java/fr/agrometinfo/www/server/model/UserEmail.java @@ -0,0 +1,38 @@ +package fr.agrometinfo.www.server.model; + +import java.time.LocalDateTime; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Data; + +/** + * Table for storage email adress.<br> + * @author jdecome + */ +@Data +@Entity +@Table(name = "useremail") +public class UserEmail { + /** + * Primary key. + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + /** + * Date time of user response (creation date). + */ + @Column(name = "datetime") + private LocalDateTime datetime; + /** + * E-mail address. + */ + @Column(name = "email") + private String email; +} diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/model/UserResponse.java b/www-server/src/main/java/fr/agrometinfo/www/server/model/UserResponse.java new file mode 100644 index 0000000000000000000000000000000000000000..b6e61475ec38667cdd806f13a00e55bdb7870dd8 --- /dev/null +++ b/www-server/src/main/java/fr/agrometinfo/www/server/model/UserResponse.java @@ -0,0 +1,54 @@ +package fr.agrometinfo.www.server.model; + +import java.time.LocalDateTime; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import lombok.Data; + +/** + * User's responses values of survey form. + * @author jdecome + */ +@Data +@Entity +@Table(name = "userresponse") +public class UserResponse { + /** + * Primary key. + */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + /** + * Date time of user responses (creation date). + */ + @Column(name = "datetime") + private LocalDateTime dateTime; + /** + * Related question. + */ + @OneToOne + @JoinColumn(name = "surveyquestion") + private SurveyQuestion question; + /** + * Related option.<br> + * Can be null if it's a other response. + */ + @OneToOne + @JoinColumn(name = "surveyoption") + private SurveyOption option; + /** + * Other text response.<br> + * Can be null if it's a related response. + */ + @Column(name = "othertext") + private String otherText; +} diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/rs/ApplicationConfig.java b/www-server/src/main/java/fr/agrometinfo/www/server/rs/ApplicationConfig.java index 26fef07458cd2a65255a88a869f67a0263d17ced..b6ff6d4573d4c0cd97b92ea948f682291470f661 100644 --- a/www-server/src/main/java/fr/agrometinfo/www/server/rs/ApplicationConfig.java +++ b/www-server/src/main/java/fr/agrometinfo/www/server/rs/ApplicationConfig.java @@ -39,11 +39,12 @@ public class ApplicationConfig extends Application { public final Set<Class<?>> getClasses() { return Set.of( // OpenAPI / Swagger - OpenApiResource.class, AcceptHeaderOpenApiResource.class, + AcceptHeaderOpenApiResource.class, OpenApiResource.class, // Jackson configuration JacksonConfig.class, // JAX-RS resources - ApplicationResource.class, GeometryResource.class, IndicatorResource.class, UserResource.class, + ApplicationResource.class, GeometryResource.class, IndicatorResource.class, + SurveyFormResource.class, UserResource.class, // POJO // Dependencies of resources LogFilter.class diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/rs/ApplicationResource.java b/www-server/src/main/java/fr/agrometinfo/www/server/rs/ApplicationResource.java index c843f96655d8eb0b84909a67b9703560495d4678..6b9e2e35b14856119fe0c242d6da2264ad85f184 100644 --- a/www-server/src/main/java/fr/agrometinfo/www/server/rs/ApplicationResource.java +++ b/www-server/src/main/java/fr/agrometinfo/www/server/rs/ApplicationResource.java @@ -11,7 +11,7 @@ import fr.agrometinfo.www.server.exception.AgroMetInfoException; import fr.agrometinfo.www.server.model.DailyVisit; import fr.agrometinfo.www.server.service.CacheService; import fr.agrometinfo.www.server.service.MailService; -import fr.agrometinfo.www.server.service.MailService.Mail; +import fr.agrometinfo.www.server.service.MailServiceImpl.Mail; import fr.agrometinfo.www.server.util.AppVersion; import fr.agrometinfo.www.server.util.ST; import fr.agrometinfo.www.server.util.ST.Key; diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/rs/SurveyFormResource.java b/www-server/src/main/java/fr/agrometinfo/www/server/rs/SurveyFormResource.java new file mode 100644 index 0000000000000000000000000000000000000000..9ac3ddcd563035ab96d1cafbf114a44a62a3128d --- /dev/null +++ b/www-server/src/main/java/fr/agrometinfo/www/server/rs/SurveyFormResource.java @@ -0,0 +1,180 @@ +package fr.agrometinfo.www.server.rs; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +import fr.agrometinfo.www.server.dao.SurveyQuestionDao; +import fr.agrometinfo.www.server.dao.SurveyOptionDao; +import fr.agrometinfo.www.server.dao.UserEmailDao; +import fr.agrometinfo.www.server.dao.UserResponsesDao; +import fr.agrometinfo.www.server.model.SurveyQuestion; +import fr.agrometinfo.www.server.model.SurveyOption; +import fr.agrometinfo.www.server.service.MailService; +import fr.agrometinfo.www.server.util.EmailUtils; +import fr.agrometinfo.www.shared.dto.ErrorResponseDTO; +import fr.agrometinfo.www.shared.dto.SurveyResponseDTO; +import fr.agrometinfo.www.shared.dto.SurveyQuestionDTO; +import fr.agrometinfo.www.shared.dto.SurveyOptionDTO; +import fr.agrometinfo.www.shared.service.SurveyFormService; +import jakarta.enterprise.context.RequestScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.WebApplicationException; +import jakarta.ws.rs.core.MediaType; + +/** + * Endpoint for WS login form. + * @author jdecome + * + */ +@Path(SurveyFormService.PATH) +@RequestScoped +public class SurveyFormResource implements SurveyFormService { + /** + * Convert {@link SurveyQuestion} entity to dto. + * @param question entity + * @return dto + */ + static SurveyQuestionDTO toDto(final SurveyQuestion question) { + final SurveyQuestionDTO dto = new SurveyQuestionDTO(); + dto.setId(question.getId()); + dto.setDescription(question.getDescription()); + return dto; + } + /** + * Convert {@link SurveyOption} entity to dto. + * @param response entity + * @return dto + */ + static SurveyOptionDTO toDto(final SurveyOption response) { + final SurveyOptionDTO dto = new SurveyOptionDTO(); + dto.setId(response.getId()); + dto.setQuestion(toDto(response.getQuestion())); + dto.setDescription(response.getDescription()); + return dto; + } + /** + * Convert {@link SurveyQuestionDTO} dto to entity. + * @param dto question + * @return entity + */ + private static SurveyQuestion toEntity(final SurveyQuestionDTO dto) { + final SurveyQuestion question = new SurveyQuestion(); + question.setId(dto.getId()); + question.setDescription(dto.getDescription()); + return question; + } + /** + * Convert {@link SurveyOptionDTO} dto to entity. + * @param dto response + * @return entity + */ + private static SurveyOption toEntity(final SurveyOptionDTO dto) { + final SurveyOption response = new SurveyOption(); + response.setId(dto.getId()); + response.setQuestion(toEntity(dto.getQuestion())); + response.setDescription(dto.getDescription()); + return response; + } + /** + * DAO for questions ({@link SurveyQuestionDao}). + */ + @Inject + private SurveyQuestionDao questionsDao; + /** + * DAO for options ({@link SurveyOptionDao}). + */ + @Inject + private SurveyOptionDao responsesDao; + /** + * DAO for user responses ({@link UserResponsesDao}. + */ + @Inject + private UserResponsesDao userResponsesDao; + /** + * DAO for user email {@link UserEmailDao}. + */ + @Inject + private UserEmailDao userEmailDao; + /** + * Mail service. + */ + @Inject + private MailService mailService; + /** + * Ensure the value of query parameter is not null and not blanck. + * @param value value of query parameter to check + * @param queryParamName name of query parameter + * @throws WebApplicationException if validation failed + */ + private void checkRequired(final Object value, final String queryParamName) { + if (value instanceof final String str && str.isBlank() || value == null) { + final jakarta.ws.rs.core.Response.Status badRequest = jakarta.ws.rs.core.Response.Status.BAD_REQUEST; + throw new WebApplicationException( + jakarta.ws.rs.core.Response.status(badRequest).entity(ErrorResponseDTO.of( + badRequest.getStatusCode(), + badRequest.getReasonPhrase(), + queryParamName + " parameter is mandatory") + ).build()); + } + } + @GET + @Path(SurveyFormService.PATH_RESPONSES_LIST) + @Produces(MediaType.APPLICATION_JSON) + @Override + public List<SurveyOptionDTO> getResponses() { + return responsesDao.findAll().stream() + .map(SurveyFormResource::toDto).collect(Collectors.toList()); + } + @POST + @Path(SurveyFormService.PATH_INSERT_RESPONSE) + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Override + public String insertAllResponses(final SurveyResponseDTO data) { + this.checkRequired(data, "data"); + // common datetime to all records (user responses and user email) + final LocalDateTime datetime = LocalDateTime.now(); + + final List<SurveyQuestion> questions = questionsDao.findAll(); + + // proccessing of predefined responses + for (final SurveyQuestionDTO dto : data.getQuestions()) { + final SurveyQuestion q = toEntity(dto); + // if question from survey doesn't exist, don't inserting response + if (!questions.contains(q)) { + continue; + } + + final Long qRef = dto.getId(); + List<SurveyOptionDTO> associatedResponses = data.getResponses() + .stream().filter(f -> f.getQuestion().getId() == qRef) + .collect(Collectors.toList()); + + for (final SurveyOptionDTO rDto : associatedResponses) { + final SurveyOption r = toEntity(rDto); + this.userResponsesDao.insertResponse(q, r, null, datetime); + } + + // if free response present, inserted on database + if (data.getOtherTextMap().containsKey(dto.getId()) + && data.getOtherTextMap().get(dto.getId()) != null) { + this.userResponsesDao.insertResponse(q, null, data.getOtherTextMap().get(q.getId()), datetime); + } + } + + // if the user provided their email address and is vaild + if (EmailUtils.isEmailAddressValid(data.getEmail())) { + this.userEmailDao.insertEmailUserAddress(data.getEmail(), datetime); + } + + // once everything is inserted, send an email to support + this.mailService.sendSurveyFilled(data); + return "0"; + } +} diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/service/MailService.java b/www-server/src/main/java/fr/agrometinfo/www/server/service/MailService.java index f3f025a93096a0b1717f5f0c1562a41731f4b4cf..e85fe40f830569a09178e1974aaf3624126b0a51 100644 --- a/www-server/src/main/java/fr/agrometinfo/www/server/service/MailService.java +++ b/www-server/src/main/java/fr/agrometinfo/www/server/service/MailService.java @@ -1,319 +1,23 @@ package fr.agrometinfo.www.server.service; -import java.io.Serializable; -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; -import java.util.StringJoiner; - -import org.apache.logging.log4j.Level; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.core.Appender; -import org.apache.logging.log4j.core.Filter; -import org.apache.logging.log4j.core.Layout; -import org.apache.logging.log4j.core.LogEvent; -import org.apache.logging.log4j.core.LoggerContext; -import org.apache.logging.log4j.core.appender.AbstractAppender; -import org.apache.logging.log4j.core.config.Configuration; -import org.apache.logging.log4j.core.config.LoggerConfig; -import org.apache.logging.log4j.core.layout.PatternLayout; - -import fr.agrometinfo.www.server.AgroMetInfoConfiguration; -import fr.agrometinfo.www.server.AgroMetInfoConfiguration.ConfigurationKey; -import fr.agrometinfo.www.server.exception.AgroMetInfoErrorCategory; import fr.agrometinfo.www.server.exception.AgroMetInfoException; -import fr.agrometinfo.www.server.exception.ErrorCategory; -import fr.agrometinfo.www.server.exception.ErrorType; -import jakarta.annotation.PostConstruct; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; -import jakarta.mail.Address; -import jakarta.mail.Authenticator; -import jakarta.mail.Message; -import jakarta.mail.MessagingException; -import jakarta.mail.PasswordAuthentication; -import jakarta.mail.SendFailedException; -import jakarta.mail.Session; -import jakarta.mail.Transport; -import jakarta.mail.internet.InternetAddress; -import jakarta.mail.internet.MimeMessage; -import lombok.Data; -import lombok.Getter; -import lombok.extern.log4j.Log4j2; - -/** - * Service to send e-mails. - * - * @author Olivier Maury - */ -@ApplicationScoped -@Log4j2 -public class MailService { - - /** - * Text message. - */ - @Data - public static class Mail { - /** - * Body. - */ - private String content; - /** - * Sender's e-mail address. - */ - private String fromAddress; - /** - * Subject. - */ - private String subject; - /** - * Recipients' e-mail addresses. - */ - private List<String> toAddresses; - } +import fr.agrometinfo.www.server.service.MailServiceImpl.Mail; +import fr.agrometinfo.www.shared.dto.SurveyResponseDTO; +public interface MailService { /** - * Log4j2 appender to send error logs by e-mail. + * Sending e-mail. + * @param mail object to send + * @throws AgroMetInfoException */ - private class MailAppender extends AbstractAppender { - /** - * Logger name. - */ - private static final String NAME = "MailAppender"; - - /** - * Constructor. - * - * @param filter The Filter to associate with the Appender. - * @param layout The layout to use to format the event. - * @param ignoreExceptions If true, exceptions will be logged and suppressed. If - * false errors will be logged and then passed to the - * application. - */ - MailAppender(final Filter filter, final Layout<? extends Serializable> layout, final boolean ignoreExceptions) { - super(NAME, filter, layout, ignoreExceptions, null); - - } - - @Override - public void append(final LogEvent event) { - final Mail mail = new Mail(); - mail.setSubject("error log"); - mail.setContent(super.toSerializable(event).toString()); - mail.setToAddresses(List.of(configuration.get(ConfigurationKey.LOG_EMAIL))); - try { - send(mail); - } catch (final AgroMetInfoException e) { - // do nothing - LOGGER.info("Cannot send email: {}", e.getMessage()); - } - } - } - + void send(Mail mail) throws AgroMetInfoException; /** - * Keys from messages.properties used to warn about errors. + * Send mail when application started. */ - public enum MailErrorType implements ErrorType { - /** - * Cannot get message details. - */ - DETAILS("01"), - /** - * Generic exception. - */ - SEND_FAILED("02"), - /** - * Some e-mail addresses are not valid. - */ - SEND_FAILED_INVALID("03"), - /** - * Some messages failed to be sent to valid e-mail addresses. - */ - SEND_FAILED_VALID("04"); - - /** - * Subcode for the error. - */ - @Getter - private final String subCode; - - /** - * Constructor. - * - * @param c Subcode for the error. - */ - MailErrorType(final String c) { - this.subCode = c; - } - - @Override - public ErrorCategory getCategory() { - return AgroMetInfoErrorCategory.MAIL; - } - - @Override - public String getName() { - return name(); - } - - } - - /** - * Application configuration. - */ - @Inject - private AgroMetInfoConfiguration configuration; - - /** - * Session created from parameters in context.xml on Tomcat. - */ - private Session session; - - /** - * Initialization of session and log appender. - */ - @PostConstruct - public void init() { - final Properties props = new Properties(); - props.put("mail.smtp.auth", "true"); - props.put("mail.smtp.host", configuration.get(ConfigurationKey.SMTP_HOST)); - props.put("mail.smtp.port", configuration.get(ConfigurationKey.SMTP_PORT)); - final Authenticator authenticator = new Authenticator() { - @Override - protected PasswordAuthentication getPasswordAuthentication() { - return new PasswordAuthentication(configuration.get(ConfigurationKey.SMTP_USER), - configuration.get(ConfigurationKey.SMTP_PASSWORD)); - } - }; - session = Session.getInstance(props, authenticator); - // Send e-mails for ERROR level messages - final LoggerContext ctx = (LoggerContext) LogManager.getContext(false); - final Configuration config = ctx.getConfiguration(); - final String pattern = """ - date : %d - level : %-5p - class : %c#%M() - line : %L - msg : %m - throwable : %throwable - """; - final PatternLayout layout = PatternLayout.newBuilder() // - .withConfiguration(config) // - .withPattern(pattern).build(); - final Appender appender = new MailAppender(null, layout, true); - appender.start(); - config.addAppender(appender); - final LoggerConfig loggerConfig = LoggerConfig.newBuilder() // - .withAdditivity(false) // - .withConfig(config) // - .withLevel(Level.ERROR) // - .withLoggerName(MailAppender.NAME) // - .build(); - loggerConfig.addAppender(appender, null, null); - config.addLogger("fr.agrometinfo", loggerConfig); - ctx.updateLoggers(); - } - - /** - * @param mail text message to send - * @throws AgroMetInfoException exception - */ - public void send(final Mail mail) throws AgroMetInfoException { - send(toMessage(mail)); - } - - /** - * @param message jakarta message to send - * @throws AgroMetInfoException exception - */ - private void send(final Message message) throws AgroMetInfoException { - final String subject; - final String recipients; - try { - subject = message.getSubject(); - recipients = toString(message.getAllRecipients()); - } catch (final MessagingException e) { - throw new AgroMetInfoException(MailErrorType.DETAILS, e.getMessage()); - } - try { - Transport.send(message); - } catch (final MessagingException e) { - if (e instanceof final SendFailedException sfe) { - final Address[] invalid = sfe.getInvalidAddresses(); - if (invalid != null) { - throw new AgroMetInfoException(MailErrorType.SEND_FAILED_INVALID, subject, toString(invalid)); - } - final Address[] validUnsent = sfe.getValidUnsentAddresses(); - if (validUnsent != null) { - throw new AgroMetInfoException(MailErrorType.SEND_FAILED_VALID, subject, toString(validUnsent)); - } - } - throw new AgroMetInfoException(MailErrorType.SEND_FAILED, subject, recipients, e.getMessage()); - } - } - - /** - * Send an e-mail to log e-mail address at application start. - */ - public void sendApplicationStarted() { - final Mail mail = new Mail(); - mail.setContent("Démarrage de AgroMetInfo-www : " + LocalDateTime.now()); - mail.setFromAddress(configuration.get(ConfigurationKey.APP_EMAIL)); - mail.setSubject("Démarrage"); - mail.setToAddresses(List.of(configuration.get(ConfigurationKey.LOG_EMAIL))); - try { - send(mail); - } catch (final AgroMetInfoException e) { - LOGGER.info("failed to send e-mail : {}", e); - } - } - - /** - * @param mail text message to convert - * @return jakarta converted message - * @throws AgroMetInfoException exception - */ - private Message toMessage(final Mail mail) throws AgroMetInfoException { - final String environment = configuration.get(ConfigurationKey.ENVIRONMENT); - try { - final Message message = new MimeMessage(session); - if (mail.getFromAddress() == null) { - mail.setFromAddress(configuration.get(ConfigurationKey.APP_EMAIL)); - } - message.setFrom(new InternetAddress(mail.getFromAddress())); - final List<InternetAddress> to = new ArrayList<>(); - if (mail.getToAddresses() == null || mail.getToAddresses().isEmpty()) { - mail.setToAddresses(List.of(configuration.get(ConfigurationKey.SUPPORT_EMAIL))); - } - for (final String addr : mail.getToAddresses()) { - to.add(new InternetAddress(addr)); - } - message.setRecipients(Message.RecipientType.TO, to.toArray(InternetAddress[]::new)); - if ("prod".equals(environment)) { - message.setSubject("AgroMetInfo : " + mail.getSubject()); - } else { - message.setSubject("AgroMetInfo " + environment + " : " + mail.getSubject()); - } - final String content = mail.getContent() + "\n-- \n" + configuration.get(ConfigurationKey.APP_URL); - message.setContent(content, "text/plain; charset=UTF-8"); - message.setHeader("X-Environment", environment); - return message; - } catch (final MessagingException e) { - throw new AgroMetInfoException(MailErrorType.DETAILS, e.getLocalizedMessage()); - } - } - + void sendApplicationStarted(); /** - * @param addresses e-mail addresses - * @return string representation of addresses + * Send mail when user fill login form, with questions and responses. + * @param data */ - private String toString(final Address[] addresses) { - final StringJoiner sj = new StringJoiner(", "); - for (final Address addr : addresses) { - sj.add(addr.toString()); - } - return sj.toString(); - } + void sendSurveyFilled(SurveyResponseDTO data); } diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/service/MailServiceImpl.java b/www-server/src/main/java/fr/agrometinfo/www/server/service/MailServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..a6c2c4210ba266959c02c8298003a12343757358 --- /dev/null +++ b/www-server/src/main/java/fr/agrometinfo/www/server/service/MailServiceImpl.java @@ -0,0 +1,372 @@ +package fr.agrometinfo.www.server.service; + +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; +import java.util.StringJoiner; +import java.util.stream.Collectors; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; + +import org.apache.logging.log4j.core.Appender; +import org.apache.logging.log4j.core.Filter; +import org.apache.logging.log4j.core.Layout; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.appender.AbstractAppender; +import org.apache.logging.log4j.core.config.Configuration; +import org.apache.logging.log4j.core.config.LoggerConfig; +import org.apache.logging.log4j.core.layout.PatternLayout; + +import fr.agrometinfo.www.server.AgroMetInfoConfiguration; +import fr.agrometinfo.www.server.AgroMetInfoConfiguration.ConfigurationKey; +import fr.agrometinfo.www.server.exception.AgroMetInfoErrorCategory; +import fr.agrometinfo.www.server.exception.AgroMetInfoException; +import fr.agrometinfo.www.server.exception.ErrorCategory; +import fr.agrometinfo.www.server.exception.ErrorType; +import fr.agrometinfo.www.shared.dto.SurveyResponseDTO; +import jakarta.annotation.PostConstruct; +import jakarta.mail.Address; +import jakarta.mail.Authenticator; +import jakarta.mail.Message; +import jakarta.mail.MessagingException; +import jakarta.mail.PasswordAuthentication; +import jakarta.mail.SendFailedException; +import jakarta.mail.Session; +import jakarta.mail.Transport; +import jakarta.mail.internet.InternetAddress; +import jakarta.mail.internet.MimeMessage; +import lombok.Data; +import lombok.Getter; +import lombok.extern.log4j.Log4j2; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +/** + * Service to send e-mails. + * + * @author Olivier Maury + */ +@ApplicationScoped +@Log4j2 +public class MailServiceImpl implements MailService { + + /** + * Text message. + */ + @Data + public static class Mail { + /** + * Body. + */ + private String content; + /** + * Sender's e-mail address. + */ + private String fromAddress; + /** + * Subject. + */ + private String subject; + /** + * Recipients' e-mail addresses. + */ + private List<String> toAddresses; + } + + /** + * Log4j2 appender to send error logs by e-mail. + */ + private class MailAppender extends AbstractAppender { + /** + * Logger name. + */ + private static final String NAME = "MailAppender"; + + /** + * Constructor. + * + * @param filter The Filter to associate with the Appender. + * @param layout The layout to use to format the event. + * @param ignoreExceptions If true, exceptions will be logged and suppressed. If + * false errors will be logged and then passed to the + * application. + */ + MailAppender(final Filter filter, final Layout<? extends Serializable> layout, final boolean ignoreExceptions) { + super(NAME, filter, layout, ignoreExceptions, null); + + } + + @Override + public void append(final LogEvent event) { + Mail mail = new Mail(); + mail.setFromAddress("agrometinfo@inrae.fr"); + mail.setSubject("AgroMetInfo - error log"); + mail.setContent(super.toSerializable(event).toString()); + mail.setToAddresses(List.of(configuration.get(ConfigurationKey.LOG_EMAIL))); + try { + send(mail); + } catch (AgroMetInfoException e) { + // do nothing + LOGGER.info("Cannot send email: {}", e.getMessage()); + } + } + } + + /** + * Keys from messages.properties used to warn about errors. + */ + public enum MailErrorType implements ErrorType { + /** + * Cannot get message details. + */ + DETAILS("01"), + /** + * Generic exception. + */ + SEND_FAILED("02"), + /** + * Some e-mail addresses are not valid. + */ + SEND_FAILED_INVALID("03"), + /** + * Some messages failed to be sent to valid e-mail addresses. + */ + SEND_FAILED_VALID("04"); + + /** + * Subcode for the error. + */ + @Getter + private final String subCode; + + /** + * Constructor. + * + * @param c Subcode for the error. + */ + MailErrorType(final String c) { + this.subCode = c; + } + + @Override + public ErrorCategory getCategory() { + return AgroMetInfoErrorCategory.MAIL; + } + + @Override + public String getName() { + return name(); + } + + } + + /** + * Application configuration. + */ + @Inject + private AgroMetInfoConfiguration configuration; + + /** + * Session created from parameters in context.xml on Tomcat. + */ + private Session session; + + /** + * Initialization of session and log appender. + */ + @PostConstruct + public void init() { + Properties props = new Properties(); + props.put("mail.smtp.auth", "true"); + props.put("mail.smtp.host", configuration.get(ConfigurationKey.SMTP_HOST)); + props.put("mail.smtp.port", configuration.get(ConfigurationKey.SMTP_PORT)); + final Authenticator authenticator = new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(configuration.get(ConfigurationKey.SMTP_USER), + configuration.get(ConfigurationKey.SMTP_PASSWORD)); + } + }; + session = Session.getInstance(props, authenticator); + // Send e-mails for ERROR level messages + final LoggerContext ctx = (LoggerContext) LogManager.getContext(false); + final Configuration config = ctx.getConfiguration(); + final String pattern = """ + date : %d + level : %-5p + class : %c#%M() + line : %L + msg : %m + throwable : %throwable + """; + final PatternLayout layout = PatternLayout.newBuilder() // + .withConfiguration(config) // + .withPattern(pattern).build(); + final Appender appender = new MailAppender(null, layout, true); + appender.start(); + config.addAppender(appender); + LoggerConfig loggerConfig = LoggerConfig.newBuilder() // + .withAdditivity(false) // + .withConfig(config) // + .withLevel(Level.ERROR) // + .withLoggerName(MailAppender.NAME) // + .build(); + loggerConfig.addAppender(appender, null, null); + config.addLogger("fr.agrometinfo", loggerConfig); + ctx.updateLoggers(); + } + + /** + * @param mail text message to send + * @throws AgroMetInfoException exception + */ + public void send(final Mail mail) throws AgroMetInfoException { + send(toMessage(mail)); + } + + /** + * @param message jakarta message to send + * @throws AgroMetInfoException exception + */ + private void send(final Message message) throws AgroMetInfoException { + final String subject; + final String recipients; + try { + subject = message.getSubject(); + recipients = toString(message.getAllRecipients()); + } catch (MessagingException e) { + throw new AgroMetInfoException(MailErrorType.DETAILS, e.getMessage()); + } + try { + Transport.send(message); + } catch (MessagingException e) { + if (e instanceof SendFailedException sfe) { + final Address[] invalid = sfe.getInvalidAddresses(); + if (invalid != null) { + throw new AgroMetInfoException(MailErrorType.SEND_FAILED_INVALID, subject, toString(invalid)); + } + final Address[] validUnsent = sfe.getValidUnsentAddresses(); + if (validUnsent != null) { + throw new AgroMetInfoException(MailErrorType.SEND_FAILED_VALID, subject, toString(validUnsent)); + } + } + throw new AgroMetInfoException(MailErrorType.SEND_FAILED, subject, recipients, e.getMessage()); + } + } + + /** + * @param mail text message to convert + * @return jakarta converted message + * @throws AgroMetInfoException exception + */ + private Message toMessage(final Mail mail) throws AgroMetInfoException { + final String environment = configuration.get(ConfigurationKey.ENVIRONMENT); + try { + final Message message = new MimeMessage(session); + if (mail.getFromAddress() == null) { + mail.setFromAddress(configuration.get(ConfigurationKey.APP_EMAIL)); + } + message.setFrom(new InternetAddress(mail.getFromAddress())); + List<InternetAddress> to = new ArrayList<>(); + if (mail.getToAddresses() == null || mail.getToAddresses().isEmpty()) { + mail.setToAddresses(List.of(configuration.get(ConfigurationKey.SUPPORT_EMAIL))); + } + for (String addr : mail.getToAddresses()) { + to.add(new InternetAddress(addr)); + } + message.setRecipients(Message.RecipientType.TO, to.toArray(InternetAddress[]::new)); + if ("prod".equals(environment)) { + message.setSubject("AgroMetInfo : " + mail.getSubject()); + } else { + message.setSubject("AgroMetInfo " + environment + " : " + mail.getSubject()); + } + final String content = mail.getContent() + "\n-- \n" + configuration.get(ConfigurationKey.APP_URL); + message.setContent(content, "text/plain; charset=UTF-8"); + message.setHeader("X-Environment", environment); + return message; + } catch (MessagingException e) { + throw new AgroMetInfoException(MailErrorType.DETAILS, e.getLocalizedMessage()); + } + } + + /** + * @param addresses e-mail addresses + * @return string representation of addresses + */ + private String toString(final Address[] addresses) { + final StringJoiner sj = new StringJoiner(", "); + for (Address addr : addresses) { + sj.add(addr.toString()); + } + return sj.toString(); + } + + /** + * Send an e-mail to log e-mail address at application start. + */ + public void sendApplicationStarted() { + if (configuration.get(ConfigurationKey.ENVIRONMENT).equals("dev")) { + LOGGER.trace("Environment is dev, e-mail isn't send"); + return; + } + Mail mail = new Mail(); + mail.setContent("Démarrage de AgroMetInfo-www : " + LocalDateTime.now() + "\n-- \n" + + configuration.get(ConfigurationKey.APP_URL)); + mail.setFromAddress(configuration.get(ConfigurationKey.APP_EMAIL)); + mail.setSubject("Démarrage"); + mail.setToAddresses(List.of(configuration.get(ConfigurationKey.LOG_EMAIL))); + try { + send(mail); + } catch (AgroMetInfoException e) { + LOGGER.info("failed to send e-mail : {}", e); + } + } + /** + * Send an e-mail when user filled survey form. + * @param data data of survey + */ + public void sendSurveyFilled(final SurveyResponseDTO data) { + final Mail mail = createContentFromData(data); + mail.setSubject("Un utilisateur a rempli le formulaire d'enquête"); + mail.setFromAddress(configuration.get(ConfigurationKey.APP_EMAIL)); + mail.setToAddresses(List.of(configuration.get(ConfigurationKey.LOG_EMAIL))); + try { + send(mail); + } catch (AgroMetInfoException e) { + LOGGER.info("failed to send e-mail : {}", e); + } + } + /** + * Generate body e-mail with survey data.<br> + * Mail object returned is just a mail with content. + * @param data data of survey + * @return mail object + */ + public static Mail createContentFromData(final SurveyResponseDTO data) { + final Mail mail = new Mail(); + + final StringBuilder builder = new StringBuilder(); + builder.append("Bonjour,\n"); + builder.append("Un utilisateur vient de remplir le formulaire d'enquête d'AgroMetInfo à l'instant.\n\n"); + builder.append("Il a répondu à " + data.getQuestions().size() + " question(s) :\n"); + + data.getQuestions().forEach((q) -> { + builder.append("\t• « " + q.getDescription() + " » :\n"); + data.getResponses().stream().filter(f -> f.getQuestion().getId() == q.getId()) + .collect(Collectors.toList()) + .forEach((r) -> builder.append("\t\t- " + r.getDescription() + "\n")); + + if (data.getOtherTextMap().containsKey(q.getId()) + && data.getOtherTextMap().get(q.getId()) != null) { + builder.append("\t\t- Réponse libre : « " + data.getOtherTextMap().get(q.getId()) + " »\n"); + } + builder.append("\n"); + }); + mail.setContent(builder.toString()); + return mail; + } +} diff --git a/www-server/src/main/java/fr/agrometinfo/www/server/util/EmailUtils.java b/www-server/src/main/java/fr/agrometinfo/www/server/util/EmailUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..1006b22714800523db69ff85cbbcdb91901da04a --- /dev/null +++ b/www-server/src/main/java/fr/agrometinfo/www/server/util/EmailUtils.java @@ -0,0 +1,29 @@ +package fr.agrometinfo.www.server.util; + +import java.util.regex.Pattern; + +/** + * @author jdecome + * + */ +public final class EmailUtils { + /** + * Private default constructor. + */ + private EmailUtils() { + + } + /** + * Check if email address provided is a valid address.<br> + * Verification by not null and regex + * @param email address + * @return {@code true} or {@code false} + */ + public static boolean isEmailAddressValid(final String email) { + if (email == null) { + return false; + } + final String emailPattern = "[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,6}"; + return Pattern.matches(emailPattern, email); + } +} diff --git a/www-server/src/main/webapp/index.html b/www-server/src/main/webapp/index.html index 5498ba698e09956810c74697e20bbccb2c1cc1cc..bbf735acbba3dac6369e98dcd86cf0a46b946e9a 100644 --- a/www-server/src/main/webapp/index.html +++ b/www-server/src/main/webapp/index.html @@ -24,11 +24,11 @@ <!-- --> <!-- Consider inlining CSS to reduce the number of requested files --> <!-- --> - <link type="text/css" rel="stylesheet" href="app/style.css"> <link type="text/css" rel="stylesheet" href="app/vendors/ol/ol.css"> <link type="text/css" rel="stylesheet" href="app/vendors/ol/ol-layerswitcher.css"> <link type="text/css" rel="stylesheet" href="app/css/domino-ui.css"> <link type="text/css" rel="stylesheet" href="app/css/themes/all-themes.min.css"> + <link type="text/css" rel="stylesheet" href="app/style.css"> <!-- --> <!-- Any title is fine --> 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 new file mode 100644 index 0000000000000000000000000000000000000000..6e5f606754ddc6886879bd4f93440385c864a483 --- /dev/null +++ b/www-server/src/test/java/fr/agrometinfo/www/server/SurveyFormHibernateTest.java @@ -0,0 +1,204 @@ +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; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +import org.junit.jupiter.api.Test; + +import fr.agrometinfo.www.server.dao.SurveyQuestionDao; +import fr.agrometinfo.www.server.dao.SurveyQuestionDaoHibernate; +import fr.agrometinfo.www.server.dao.SurveyOptionDao; +import fr.agrometinfo.www.server.dao.SurveyOptionDaoHibernate; +import fr.agrometinfo.www.server.dao.UserEmailDao; +import fr.agrometinfo.www.server.dao.UserEmailDaoHibernate; +import fr.agrometinfo.www.server.dao.UserResponsesDao; +import fr.agrometinfo.www.server.dao.UserResponsesDaoHibernate; +import fr.agrometinfo.www.server.model.SurveyQuestion; +import fr.agrometinfo.www.server.model.SurveyOption; +import fr.agrometinfo.www.server.model.UserEmail; +import fr.agrometinfo.www.server.model.UserResponse; + +/** + * Test of UserResponsesDaoHibernate. + * @author jdecome + * + */ +public class SurveyFormHibernateTest { + /** DAO for user's responses. */ + private UserResponsesDao userResponsesDao = new UserResponsesDaoHibernate(); + /** DAO for questions. */ + private SurveyQuestionDao questionsDao = new SurveyQuestionDaoHibernate(); + /** DAO for responses. */ + private SurveyOptionDao responsesDao = new SurveyOptionDaoHibernate(); + /** DAO for email address of user. */ + private UserEmailDao userEmailDao = new UserEmailDaoHibernate(); + + @Test + public void getQuestion() { + final List<SurveyQuestion> list = questionsDao.findAll(); + assertFalse(list.isEmpty(), "Questions list is empty"); + for (final SurveyQuestion q : list) { + // 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"); + } + } + + @Test + public void getResponses() { + final List<SurveyQuestion> questions = questionsDao.findAll(); + final List<SurveyOption> responses = responsesDao.findAll(); + 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"); + + // 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"); + } + } + + @Test + public void setUserResponses() { + this.resetUserResponses(); + final List<SurveyQuestion> questions = questionsDao.findAll(); + final SurveyQuestion profession = questions.stream().filter((q) -> q.getDescription().contains("profession")).findFirst().get(); + assertNotNull(profession, "No question object matching to question of question about profession"); + List<SurveyOption> responses = responsesDao.findAllByQuestion(profession.getId()); + assertNotNull(responses, "No response object matching to question of question about profession"); + assertFalse(responses.isEmpty(), "List of responses about profession question is empty"); + 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"); + 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)) { + userResponsesDao.insertResponse(profession, res, null); + listOfResponses.add(res); + } + 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"); + 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"); + 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"); + 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"); + } else { // predefined response + assertEquals(ur.getOption().getQuestion(), profession, "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"); + 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"); + for (final UserResponse ur : list) { + assertEquals(ur.getQuestion(), useCase, "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"); + } else { // predefined response + assertEquals(ur.getOption().getQuestion(), useCase, "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"); + } + } + } + @Test + public void setUserResponsesWithEmail() { + this.resetUserResponses(); + // just testing insertion of email user, without insertion of responses (already tested) + final String email = "jane.doe@inrae.fr"; + List<UserEmail> list = this.userEmailDao.getEmailAddressList(); + 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 + " »"); + } + /** + * Return randomly responses list + * @param list original list + * @param nbElements to pick + * @return + */ + private List<SurveyOption> pickNRandom(final List<SurveyOption> list, final int nbElements) { + final List<SurveyOption> copy = new ArrayList<SurveyOption>(list); + Collections.shuffle(copy); + if (nbElements > copy.size()) { + return copy.subList(0, copy.size()); + } else { + return copy.subList(0, nbElements); + } + } + private void resetUserResponses() { + this.userResponsesDao.deleteAll(); + this.userEmailDao.deleteAll(); + } +} diff --git a/www-server/src/test/java/fr/agrometinfo/www/server/exception/ErrorTypeTest.java b/www-server/src/test/java/fr/agrometinfo/www/server/exception/ErrorTypeTest.java index abbdc2a8d396a34af4345cb37204c7789db8df6e..678149fe5e01e6cdc5f3e6d8d719f86f8c35a026 100644 --- a/www-server/src/test/java/fr/agrometinfo/www/server/exception/ErrorTypeTest.java +++ b/www-server/src/test/java/fr/agrometinfo/www/server/exception/ErrorTypeTest.java @@ -15,7 +15,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import fr.agrometinfo.www.server.I18n; -import fr.agrometinfo.www.server.service.MailService; +import fr.agrometinfo.www.server.service.MailServiceImpl; /** * Ensure all implementations of {@link ErrorType} are well defined. @@ -33,7 +33,7 @@ public class ErrorTypeTest { * @return classes to test. */ public static List<Class<? extends Enum<?>>> data() { - return Arrays.asList(MailService.MailErrorType.class); + return Arrays.asList(MailServiceImpl.MailErrorType.class); } /** diff --git a/www-server/src/test/java/fr/agrometinfo/www/server/rs/SurveyFormResourceTest.java b/www-server/src/test/java/fr/agrometinfo/www/server/rs/SurveyFormResourceTest.java new file mode 100644 index 0000000000000000000000000000000000000000..6c3a7f41a6ffec54a6e64f2bdcee36d410b11e98 --- /dev/null +++ b/www-server/src/test/java/fr/agrometinfo/www/server/rs/SurveyFormResourceTest.java @@ -0,0 +1,247 @@ +package fr.agrometinfo.www.server.rs; + +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; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.glassfish.hk2.utilities.binding.AbstractBinder; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.jupiter.api.Test; + + +import fr.agrometinfo.www.server.dao.SurveyQuestionDao; +import fr.agrometinfo.www.server.dao.SurveyQuestionDaoHibernate; +import fr.agrometinfo.www.server.dao.SurveyOptionDao; +import fr.agrometinfo.www.server.dao.SurveyOptionDaoHibernate; +import fr.agrometinfo.www.server.dao.UserEmailDao; +import fr.agrometinfo.www.server.dao.UserEmailDaoHibernate; +import fr.agrometinfo.www.server.dao.UserResponsesDao; +import fr.agrometinfo.www.server.dao.UserResponsesDaoHibernate; +import fr.agrometinfo.www.server.exception.AgroMetInfoException; +import fr.agrometinfo.www.server.model.SurveyQuestion; +import fr.agrometinfo.www.server.model.SurveyOption; +import fr.agrometinfo.www.server.model.UserEmail; +import fr.agrometinfo.www.server.model.UserResponse; +import fr.agrometinfo.www.server.service.MailService; +import fr.agrometinfo.www.server.service.MailServiceImpl; +import fr.agrometinfo.www.server.service.MailServiceImpl.Mail; +import fr.agrometinfo.www.shared.dto.SurveyResponseDTO; +import fr.agrometinfo.www.shared.dto.SurveyQuestionDTO; +import fr.agrometinfo.www.shared.dto.SurveyOptionDTO; + +import jakarta.ws.rs.client.Entity; +import jakarta.ws.rs.core.Application; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Form; +import lombok.Getter; +import lombok.extern.log4j.Log4j2; + +/** + * Test survey form webservice. + * @author jdecome + * + */ +@Log4j2 +public class SurveyFormResourceTest extends JerseyTest { + /** + * Mock class for mail service. + * @author jdecome + * + */ + public class MailServiceTest implements MailService { + /** Mail object. */ + @Getter + private Mail mail = null; + @Override + public void sendSurveyFilled(SurveyResponseDTO data) { + this.mail = MailServiceImpl.createContentFromData(data); + } + + @Override + public void sendApplicationStarted() { + // do nothing + } + + @Override + public void send(Mail mail) throws AgroMetInfoException { + // do nothing + } + + } + /** Path separator. */ + private static final String SEP = "/"; + /** Parsing JSON to object. */ + private final ObjectMapper objectMapper = new ObjectMapper(); + /** DAO for Questions. */ + private final SurveyQuestionDao questionsDao = new SurveyQuestionDaoHibernate(); + /** DAO for Responses. */ + private final SurveyOptionDao responsesDao = new SurveyOptionDaoHibernate(); + /** DAO for UserResponses. */ + private final UserResponsesDao userResponsesDao = new UserResponsesDaoHibernate(); + /** DAO for UserMail */ + private final UserEmailDao userEmailDao = new UserEmailDaoHibernate(); + /** Mail service. */ + private final MailService mailService = new MailServiceTest(); + + @Override + protected final Application configure() { + return new ResourceConfig(SurveyFormResource.class).register(new AbstractBinder() { + @Override + protected void configure() { + bind(questionsDao).to(SurveyQuestionDao.class); + bind(responsesDao).to(SurveyOptionDao.class); + bind(userResponsesDao).to(UserResponsesDao.class); + bind(userEmailDao).to(UserEmailDao.class); + bind(mailService).to(MailService.class); + } + }); + } + /** Testing response retrieval from webservice. */ + @Test + public void getResponses() { + try { + // get all questions from DAO for verification. + final List<SurveyQuestion> questions = questionsDao.findAll(); + assertFalse(questions.isEmpty(), "Question list is empty"); + + // Get all responses (options) by webservice + final String jsonAllResponses = target(SurveyFormResource.PATH + SEP + SurveyFormResource.PATH_RESPONSES_LIST).request().get(String.class); + assertNotNull(jsonAllResponses, "Unable to retrieve resource (null object) from responses"); + assertFalse(jsonAllResponses.isEmpty(), "The JSON character string of the responses is empty"); + + final List<SurveyOption> allResponses = objectMapper.readValue(jsonAllResponses, new TypeReference<List<SurveyOption>>() {}); + assertNotNull(allResponses, "Conversion of JSON from responses to list failed"); + assertFalse(allResponses.isEmpty(), "The answer list is empty"); + + for (final SurveyOption r : allResponses) { + assertTrue(questions.contains(r.getQuestion()), "Question of the answer « " + r.getDescription() + " » is not contained in the list of questions"); + } + } catch (final JsonProcessingException e) { + log.info(e.getMessage()); + } + } + /** Test by setting answers for questions. */ + @Test + public void testWithJsonResponses() { + this.resetUserResponses(); + final List<SurveyQuestion> questionsList = questionsDao.findAll(); + final List<SurveyQuestionDTO> questionsDTO = questionsList.stream().map(SurveyFormResource::toDto).toList(); + + final List<SurveyOption> responsesList = new ArrayList<>(); + final HashMap<Long, String> otherTextMap = new HashMap<>(); + + for (final SurveyQuestion q : questionsList) { + final List<SurveyOption> responsesListForQuestion = responsesDao.findAllByQuestion(q.getId()); + + // Getting a randomly list of response + final List<SurveyOption> randomList = this.pickNRandom(responsesListForQuestion, ThreadLocalRandom.current().nextInt(0, responsesListForQuestion.size() - 1)); + + if (randomList.size() > 0) { + responsesList.addAll(randomList); + } + + otherTextMap.put(q.getId(), "Free answer for the question « " + q.getDescription() + " »"); + } + final List<SurveyOptionDTO> responsesDTO = responsesList.stream().map(SurveyFormResource::toDto).toList(); + + final SurveyResponseDTO data = new SurveyResponseDTO(questionsDTO, responsesDTO, otherTextMap); + data.setEmail("john.doe@inrae.fr"); + assertEquals(data.getQuestions(), questionsDTO, "List of questions in LoginFormData object does not match with original list"); + assertEquals(data.getResponses(), responsesDTO, "List of responses in LoginFormData object does not match with the original list"); + assertEquals(data.getOtherTextMap(), otherTextMap, "List of free responses in LoginFormData object does not match with the original list"); + + jakarta.ws.rs.core.Response ret = target(SurveyFormResource.PATH + SEP + SurveyFormResource.PATH_INSERT_RESPONSE) + .request(MediaType.APPLICATION_JSON).post(Entity.entity(data, MediaType.APPLICATION_JSON)); + + assertEquals(ret.getStatus(), jakarta.ws.rs.core.Response.Status.OK.getStatusCode(), "The insert entry point should return status code 200 (OK)"); + + // Retrieves all answers for all questions + List<UserResponse> list = new ArrayList<>(); + for (final SurveyQuestion q : questionsDao.findAll()) { + final List<UserResponse> responsesByQuestion = userResponsesDao.findAllByQuestion(q.getId()); + assertNotNull(responsesByQuestion, "The user's answer list for the question « " + q.getId() + " » must not be null"); + responsesByQuestion.forEach((ur) -> { + assertEquals(ur.getQuestion(), q, "The question in the answer does not match the original question « " + q.getDescription() + " »"); + }); + list.addAll(responsesByQuestion); + } + + // Test on all answers + list.forEach((ur) -> { + assertNotNull(ur.getDateTime(), "The response timestamp must not be null"); + assertTrue(ur.getId() > 0, "Reference of the user response must be greater than 0"); + if (ur.getOption() != null) { + assertNotNull(ur.getOption(), "The user response, for a predefined response, must not be null"); + assertNull(ur.getOtherText(), "The user's free response, for a predefined response, must be null"); + } else { + assertNull(ur.getOption(), "The user's response, for a free response, must be null"); + assertNotNull(ur.getOtherText(), "The user's free response, for a predefined response, must not be null"); + } + }); + + // Check if all records has the same datetime + assertEquals(list.stream().map((u) -> u.getDateTime()).distinct().count(), 1, "Not all answers have the same datetime"); + final LocalDateTime datetimeResponse = list.stream().map((u) -> u.getDateTime()).distinct().findFirst().get(); + assertNotNull(datetimeResponse, "The datetime read in the responses is null"); + + // in this case, email address has inserted + final List<UserEmail> emails = this.userEmailDao.getEmailAddressList(); + assertNotNull(emails, "The email list is not initialized (= null)"); + assertFalse(emails.isEmpty(), "Email list is empty"); + + emails.forEach((e) -> { + assertTrue((e.getId() > 0), "The object id must be greater than 0"); + assertNotNull(e.getDatetime(), "The timestamp of the question must not be null"); + assertNotNull(e.getEmail(), "The email address contained in the subject must not be null"); + }); + + assertEquals(emails.stream().map((e) -> e.getDatetime()).distinct().count(), 1, "Not all email addresses have the same datetime"); + final LocalDateTime datetimeEmail = emails.stream().map((e) -> e.getDatetime()).distinct().findFirst().get(); + assertNotNull(datetimeEmail, "The datetime read in emails is null"); + + assertEquals(datetimeResponse, datetimeEmail, "The two datetimes are not identical"); + + // Check mail sended to support, when user filled survey form + final Mail mail = ((MailServiceTest) this.mailService).getMail(); + assertNotNull(mail, "The mail object must not be null"); + assertNotNull(mail.getContent(), "Message body is not defined (= null)"); + assertFalse(mail.getContent().isEmpty(), "The message body is empty"); + } + /** + * + * @param list origin list + * @param nbElements to pick in list + * @return randomized list with nbElements + */ + private List<SurveyOption> pickNRandom(final List<SurveyOption> list, final int nbElements) { + final List<SurveyOption> copy = new ArrayList<SurveyOption>(list); + Collections.shuffle(copy); + if (nbElements > copy.size()) { + return copy.subList(0, copy.size()); + } else { + return copy.subList(0, nbElements); + } + } + /** + * Reset tables. + */ + private void resetUserResponses() { + this.userResponsesDao.deleteAll(); + this.userEmailDao.deleteAll(); + } +} diff --git a/www-server/src/test/resources/META-INF/persistence.xml b/www-server/src/test/resources/META-INF/persistence.xml index 55e45d5f4a3201beda796c044bc38de54791015a..3a55dd0ce0dc6a93a4abb9fe34fb083dfb5a989d 100644 --- a/www-server/src/test/resources/META-INF/persistence.xml +++ b/www-server/src/test/resources/META-INF/persistence.xml @@ -18,6 +18,10 @@ <class>fr.agrometinfo.www.server.model.PraDailyValue</class> <class>fr.agrometinfo.www.server.model.Region</class> <class>fr.agrometinfo.www.server.model.Simulation</class> + <class>fr.agrometinfo.www.server.model.SurveyQuestion</class> + <class>fr.agrometinfo.www.server.model.SurveyOption</class> + <class>fr.agrometinfo.www.server.model.UserResponse</class> + <class>fr.agrometinfo.www.server.model.UserEmail</class> <properties> <property name="jakarta.persistence.jdbc.url" value="jdbc:h2:mem:agrometinfo;MODE=PostgreSQL;DB_CLOSE_DELAY=-1;INIT=RUNSCRIPT FROM '../sql/schema.types.h2.sql'\;RUNSCRIPT FROM '../sql/schema.tables.sql'\;RUNSCRIPT FROM '../sql/init_data.h2.sql';" /> <property name="jakarta.persistence.jdbc.driver" value="org.h2.Driver" /> diff --git a/www-shared/src/main/java/fr/agrometinfo/www/shared/dto/SurveyOptionDTO.java b/www-shared/src/main/java/fr/agrometinfo/www/shared/dto/SurveyOptionDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..029b6dee2e6003c6a3c3ab0c23e358c03a521793 --- /dev/null +++ b/www-shared/src/main/java/fr/agrometinfo/www/shared/dto/SurveyOptionDTO.java @@ -0,0 +1,60 @@ +package fr.agrometinfo.www.shared.dto; + +import org.dominokit.jackson.annotation.JSONMapper; + +/** + * DTO for options for survey form. + * @author jdecome + * + */ +@JSONMapper +public class SurveyOptionDTO { + /** + * Reference of this response. + */ + private long id; + /** + * DTO of question associated to response. + */ + private SurveyQuestionDTO question; + /** + * Label of response. + */ + private String description; + /** + * @return the responseRef + */ + public long getId() { + return id; + } + /** + * @return the question + */ + public SurveyQuestionDTO getQuestion() { + return question; + } + /** + * @return the fullname + */ + public String getDescription() { + return description; + } + /** + * @param value the responseRef to set + */ + public void setId(final long value) { + this.id = value; + } + /** + * @param dto the question to set + */ + public void setQuestion(final SurveyQuestionDTO dto) { + this.question = dto; + } + /** + * @param value the fullname to set + */ + public void setDescription(final String value) { + this.description = value; + } +} diff --git a/www-shared/src/main/java/fr/agrometinfo/www/shared/dto/SurveyQuestionDTO.java b/www-shared/src/main/java/fr/agrometinfo/www/shared/dto/SurveyQuestionDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..265f2c50b1cd2f56d7a92be27bf0f2985eb8a8d6 --- /dev/null +++ b/www-shared/src/main/java/fr/agrometinfo/www/shared/dto/SurveyQuestionDTO.java @@ -0,0 +1,67 @@ +package fr.agrometinfo.www.shared.dto; + +import java.util.Objects; + +import org.dominokit.jackson.annotation.JSONMapper; + +/** + * DTO for questions of survey form. + * @author jdecome + * + */ +@JSONMapper +public class SurveyQuestionDTO { + /** + * Reference of question. + */ + private long id; + /** + * Label of question. + */ + private String description; + /** + * @return the questionRef + */ + public long getId() { + return id; + } + /** + * @return the fullName + */ + public String getDescription() { + return description; + } + /** + * @param value the questionRef to set + */ + public void setId(final long value) { + this.id = value; + } + /** + * @param value the fullName to set + */ + public void setDescription(final String value) { + this.description = value; + } + /** */ + @Override + public int hashCode() { + return Objects.hash(description, id); + } + /** */ + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + SurveyQuestionDTO other = (SurveyQuestionDTO) obj; + return Objects.equals(description, other.description) + && id == other.id; + } +} diff --git a/www-shared/src/main/java/fr/agrometinfo/www/shared/dto/SurveyResponseDTO.java b/www-shared/src/main/java/fr/agrometinfo/www/shared/dto/SurveyResponseDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..67054b1849cda3224f499d0b9e833a2439244af7 --- /dev/null +++ b/www-shared/src/main/java/fr/agrometinfo/www/shared/dto/SurveyResponseDTO.java @@ -0,0 +1,97 @@ +package fr.agrometinfo.www.shared.dto; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Class contains all data of survey form. + * @author jdecome + * + */ +public class SurveyResponseDTO { + /** + * List of login form questions.<br> + * This list questions contains available questions, obtained by {@link QuestionsDao} + */ + private List<SurveyQuestionDTO> questions; + /** + * List of login form responses.<br> + * This list responses contains available responses, obtained by {@link ResponsesDao} + */ + private List<SurveyOptionDTO> responses; + /** + * Storage, for each response, the other text, if specified. + */ + private Map<Long, String> otherTextMap = new HashMap<>(); + /** + * Email user.<br> + * Can be null if user don't provide it (by called {@link SurveyResponseDTO#setEmail(String)} method only). + */ + private String email = null; + /** + * Default constructor. + */ + public SurveyResponseDTO() { + } + /** + * Constructor. + * @param qList questions list + * @param rList responses list + * @param otherText otherText map + */ + public SurveyResponseDTO(final List<SurveyQuestionDTO> qList, + final List<SurveyOptionDTO> rList, final Map<Long, String> otherText) { + this.questions = qList; + this.responses = rList; + this.otherTextMap = otherText; + } + /** + * @return the questions + */ + public List<SurveyQuestionDTO> getQuestions() { + return questions; + } + /** + * @return the responses + */ + public List<SurveyOptionDTO> getResponses() { + return responses; + } + /** + * @return the otherTextMap + */ + public Map<Long, String> getOtherTextMap() { + return otherTextMap; + } + /** + * @param questionsList the questions to set + */ + public void setQuestions(final List<SurveyQuestionDTO> questionsList) { + this.questions = questionsList; + } + /** + * @param responsesList the responses to set + */ + public void setResponses(final List<SurveyOptionDTO> responsesList) { + this.responses = responsesList; + } + /** + * @param map the otherTextMap to set + */ + public void setOtherTextMap(final HashMap<Long, String> map) { + this.otherTextMap = map; + } + /** + * @return the email + */ + public String getEmail() { + return email; + } + /** + * @param value the email to set + */ + public void setEmail(final String value) { + this.email = value; + } +} diff --git a/www-shared/src/main/java/fr/agrometinfo/www/shared/service/SurveyFormService.java b/www-shared/src/main/java/fr/agrometinfo/www/shared/service/SurveyFormService.java new file mode 100644 index 0000000000000000000000000000000000000000..b41ab7070ac2253b662fb44ae89cf5dec0d8ac47 --- /dev/null +++ b/www-shared/src/main/java/fr/agrometinfo/www/shared/service/SurveyFormService.java @@ -0,0 +1,43 @@ +package fr.agrometinfo.www.shared.service; + +import java.util.List; + +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; + +import org.dominokit.rest.shared.request.service.annotations.RequestFactory; +import org.dominokit.rest.shared.request.service.annotations.RequestBody; + +import fr.agrometinfo.www.shared.dto.SurveyResponseDTO; +import fr.agrometinfo.www.shared.dto.SurveyOptionDTO; + +/** + * Interface for survey form client server resource. + * @author jdecome + * + */ +@RequestFactory +@Path(SurveyFormService.PATH) +public interface SurveyFormService { + /** Service base path. */ + String PATH = "survey"; + /** Path for {@link SurveyFormService#getResponses}. */ + String PATH_RESPONSES_LIST = "responses"; + /** Path for {@link SurveyFormService#insertAllResponses(SurveyResponseDTO)}. */ + String PATH_INSERT_RESPONSE = "insertResponses"; + /** + * @return list of availables options for questions. + */ + @GET + @Path(PATH_RESPONSES_LIST) + List<SurveyOptionDTO> getResponses(); + /** + * Insert all responses of survey form. + * @param data object to insert + * @return return code + */ + @POST + @Path(PATH_INSERT_RESPONSE) + String insertAllResponses(@RequestBody SurveyResponseDTO data); +}