This is an automated email from the git hooks/post-receive script. New commit to branch develop in repository pollen. See https://gitlab.nuiton.org/chorem/pollen.git commit 0de236cff0e556d3ecfb8efeace810925c5ca25d Author: Sylvain Bavencoff <bavencoff@codelutin.com> Date: Mon Jul 24 17:43:13 2017 +0200 feedback utilisateur (ref #45) --- .../pollen/rest/api/PollenRestApiApplication.java | 2 + .../rest/api/PollenRestApiRequestFilter.java | 2 + .../pollen/rest/api/beans/Resource64Bean.java | 37 +++++ .../org/chorem/pollen/rest/api/v1/ApiUtils.java | 30 ++++ .../org/chorem/pollen/rest/api/v1/FeedbackApi.java | 29 ++++ .../pollen/rest/api/v1/PollenResourceApi.java | 15 ++ pollen-services/src/main/config/PollenServices.ini | 13 +- .../chorem/pollen/services/bean/FeedbackBean.java | 133 ++++++++++++++++ .../services/config/PollenServicesConfig.java | 9 ++ .../pollen/services/service/FeedbackService.java | 38 +++++ .../pollen/services/service/mail/EmailService.java | 17 +++ .../services/service/mail/FeedbackEmail.java | 72 +++++++++ .../main/resources/email/FeedbackEmail.mustache | 33 ++++ .../main/resources/email/FeedbackEmail_fr.mustache | 32 ++++ .../i18n/pollen-services_en_GB.properties | 3 + .../i18n/pollen-services_fr_FR.properties | 3 + pollen-ui-riot-js/package.json | 2 + pollen-ui-riot-js/src/main/web/i18n.json | 26 +++- pollen-ui-riot-js/src/main/web/index.js | 1 + .../src/main/web/js/FeedbackService.js | 33 ++++ pollen-ui-riot-js/src/main/web/js/Logger.js | 4 + .../src/main/web/js/ResourceService.js | 5 + .../src/main/web/tag/PollenHeader.tag.html | 17 +++ .../src/main/web/tag/popup/FeedbackModal.tag.html | 169 +++++++++++++++++++++ 24 files changed, 722 insertions(+), 3 deletions(-) diff --git a/pollen-rest-api/src/main/java/org/chorem/pollen/rest/api/PollenRestApiApplication.java b/pollen-rest-api/src/main/java/org/chorem/pollen/rest/api/PollenRestApiApplication.java index d7fa862c..9f1711ae 100644 --- a/pollen-rest-api/src/main/java/org/chorem/pollen/rest/api/PollenRestApiApplication.java +++ b/pollen-rest-api/src/main/java/org/chorem/pollen/rest/api/PollenRestApiApplication.java @@ -18,6 +18,7 @@ import org.chorem.pollen.rest.api.v1.ChoiceApi; import org.chorem.pollen.rest.api.v1.CommentApi; import org.chorem.pollen.rest.api.v1.DocApi; import org.chorem.pollen.rest.api.v1.FavoriteListApi; +import org.chorem.pollen.rest.api.v1.FeedbackApi; import org.chorem.pollen.rest.api.v1.PollApi; import org.chorem.pollen.rest.api.v1.PollenResourceApi; import org.chorem.pollen.rest.api.v1.PollenUserApi; @@ -52,6 +53,7 @@ public class PollenRestApiApplication extends Application { new VoteCountingApi(), new VoteCountingTypeApi(), new VoterListApi(), + new FeedbackApi(), new PollenAuthenticationExceptionMapper(), new PollenInvalidSessionTokenExceptionMapper(), new PollenUnauthorizedExceptionMapper(), diff --git a/pollen-rest-api/src/main/java/org/chorem/pollen/rest/api/PollenRestApiRequestFilter.java b/pollen-rest-api/src/main/java/org/chorem/pollen/rest/api/PollenRestApiRequestFilter.java index 815ccd62..5ee83aa9 100644 --- a/pollen-rest-api/src/main/java/org/chorem/pollen/rest/api/PollenRestApiRequestFilter.java +++ b/pollen-rest-api/src/main/java/org/chorem/pollen/rest/api/PollenRestApiRequestFilter.java @@ -36,6 +36,7 @@ import org.chorem.pollen.services.service.ChoiceService; import org.chorem.pollen.services.service.CommentService; import org.chorem.pollen.services.service.FavoriteListService; import org.chorem.pollen.services.service.FeedService; +import org.chorem.pollen.services.service.FeedbackService; import org.chorem.pollen.services.service.NotificationService; import org.chorem.pollen.services.service.PollService; import org.chorem.pollen.services.service.PollenResourceService; @@ -174,6 +175,7 @@ public class PollenRestApiRequestFilter implements ContainerRequestFilter, Conta ResteasyProviderFactory.pushContext(VoteCountingTypeService.class, serviceContext.newService(VoteCountingTypeService.class)); ResteasyProviderFactory.pushContext(VoteService.class, serviceContext.newService(VoteService.class)); ResteasyProviderFactory.pushContext(PollenUserService.class, serviceContext.newService(PollenUserService.class)); + ResteasyProviderFactory.pushContext(FeedbackService.class, serviceContext.newService(FeedbackService.class)); } private PollenUIContext extractUIContext(ContainerRequestContext context) { diff --git a/pollen-rest-api/src/main/java/org/chorem/pollen/rest/api/beans/Resource64Bean.java b/pollen-rest-api/src/main/java/org/chorem/pollen/rest/api/beans/Resource64Bean.java new file mode 100644 index 00000000..ec236f98 --- /dev/null +++ b/pollen-rest-api/src/main/java/org/chorem/pollen/rest/api/beans/Resource64Bean.java @@ -0,0 +1,37 @@ +package org.chorem.pollen.rest.api.beans; + +/** + * @author Sylvain Bavencoff - bavencoff@codelutin.com + */ +public class Resource64Bean { + + protected String data; + + protected String name; + + protected String contentType; + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getContentType() { + return contentType; + } + + public void setContentType(String contentType) { + this.contentType = contentType; + } +} diff --git a/pollen-rest-api/src/main/java/org/chorem/pollen/rest/api/v1/ApiUtils.java b/pollen-rest-api/src/main/java/org/chorem/pollen/rest/api/v1/ApiUtils.java index 43ced8eb..0c8cd409 100644 --- a/pollen-rest-api/src/main/java/org/chorem/pollen/rest/api/v1/ApiUtils.java +++ b/pollen-rest-api/src/main/java/org/chorem/pollen/rest/api/v1/ApiUtils.java @@ -2,6 +2,7 @@ package org.chorem.pollen.rest.api.v1; import com.google.common.base.Charsets; import org.apache.commons.io.IOUtils; +import org.chorem.pollen.rest.api.beans.Resource64Bean; import org.chorem.pollen.services.PollenTechnicalException; import org.chorem.pollen.services.bean.ResourceFileBean; import org.chorem.pollen.services.bean.export.ExportBean; @@ -9,10 +10,12 @@ import org.jboss.resteasy.plugins.providers.multipart.InputPart; import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataInput; import javax.ws.rs.core.Response; +import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; +import java.util.Base64; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -68,4 +71,31 @@ public class ApiUtils { throw new PollenTechnicalException(e); } } + + public static ResourceFileBean resource64ToResourceBean(Resource64Bean resource64Bean) { + try { + + Base64.Decoder decoder = Base64.getDecoder(); + byte[] data = decoder.decode(resource64Bean.getData()); + + java.nio.file.Path tempPath = Files.createTempDirectory("pollen"); + File uploadFile = new File(tempPath.toFile(), resource64Bean.getName()); + InputStream in = new ByteArrayInputStream(data); + Files.copy(in, uploadFile.toPath()); + + ResourceFileBean resourceBean = new ResourceFileBean(); + resourceBean.setFile(uploadFile); + resourceBean.setName(resource64Bean.getName()); + + resourceBean.setContentType(resource64Bean.getContentType()); + resourceBean.setSize(uploadFile.length()); + + + return resourceBean; + + } catch (IOException e) { + throw new PollenTechnicalException(e); + } + + } } diff --git a/pollen-rest-api/src/main/java/org/chorem/pollen/rest/api/v1/FeedbackApi.java b/pollen-rest-api/src/main/java/org/chorem/pollen/rest/api/v1/FeedbackApi.java new file mode 100644 index 00000000..bfa724f9 --- /dev/null +++ b/pollen-rest-api/src/main/java/org/chorem/pollen/rest/api/v1/FeedbackApi.java @@ -0,0 +1,29 @@ +package org.chorem.pollen.rest.api.v1; + +import org.chorem.pollen.services.bean.FeedbackBean; +import org.chorem.pollen.services.service.FeedbackService; + +import javax.ws.rs.Consumes; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; + +/** + * @author Sylvain Bavencoff - bavencoff@codelutin.com + */ + +@Path("") +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.APPLICATION_JSON) +public class FeedbackApi { + + @Path("/feedback") + @POST + public boolean addFeedBack(@Context FeedbackService feedbackService, + FeedbackBean feedbackBean) { + return feedbackService.addFeedback(feedbackBean); + } + +} diff --git a/pollen-rest-api/src/main/java/org/chorem/pollen/rest/api/v1/PollenResourceApi.java b/pollen-rest-api/src/main/java/org/chorem/pollen/rest/api/v1/PollenResourceApi.java index 8345db5a..4b3dee65 100644 --- a/pollen-rest-api/src/main/java/org/chorem/pollen/rest/api/v1/PollenResourceApi.java +++ b/pollen-rest-api/src/main/java/org/chorem/pollen/rest/api/v1/PollenResourceApi.java @@ -22,6 +22,7 @@ package org.chorem.pollen.rest.api.v1; */ import org.chorem.pollen.persistence.entity.PollenResource; +import org.chorem.pollen.rest.api.beans.Resource64Bean; import org.chorem.pollen.services.bean.PollenEntityId; import org.chorem.pollen.services.bean.PollenEntityRef; import org.chorem.pollen.services.bean.ResourceFileBean; @@ -146,6 +147,20 @@ public class PollenResourceApi { return createRef; } + @Path("/resources64") + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public PollenEntityRef<PollenResource> createResource64(@Context PollenResourceService pollenResourceService, + Resource64Bean input) throws InvalidFormException { + + ResourceFileBean resourceBean = ApiUtils.resource64ToResourceBean(input); + + PollenEntityRef<PollenResource> createRef = pollenResourceService.createResource(resourceBean); + + return createRef; + } + @Path("/resources/{resourceId}") @POST @Consumes(MediaType.MULTIPART_FORM_DATA) diff --git a/pollen-services/src/main/config/PollenServices.ini b/pollen-services/src/main/config/PollenServices.ini index 147756c5..93c92d2c 100644 --- a/pollen-services/src/main/config/PollenServices.ini +++ b/pollen-services/src/main/config/PollenServices.ini @@ -170,4 +170,15 @@ defaultValue = "0 0/5 * * * ?" description = pollen.configuration.report.maxScore key = pollen.report.maxScore type = long -defaultValue = 100 \ No newline at end of file +defaultValue = 100 + +[option mailsFeedback] +description = pollen.configuration.feedback.mails +key = pollen.feedback.mails +type = String + +[option localeFeedback] +description = pollen.configuration.feedback.locale +key = pollen.feedback.locale +type = String +defaultValue = en \ No newline at end of file diff --git a/pollen-services/src/main/java/org/chorem/pollen/services/bean/FeedbackBean.java b/pollen-services/src/main/java/org/chorem/pollen/services/bean/FeedbackBean.java new file mode 100644 index 00000000..d76d1c1b --- /dev/null +++ b/pollen-services/src/main/java/org/chorem/pollen/services/bean/FeedbackBean.java @@ -0,0 +1,133 @@ +package org.chorem.pollen.services.bean; + +import org.chorem.pollen.persistence.entity.PollenResource; + +import java.awt.Dimension; +import java.util.Locale; + + +/** + * @author Sylvain Bavencoff - bavencoff@codelutin.com + */ +public class FeedbackBean { + + protected String description; + + protected String category; + + protected String browser; + + protected String operatingSystem; + + protected String platform; + + protected Dimension screenResolution; + + protected Dimension windowDimension; + + protected Locale locale; + + protected String location; + + protected String locationTitle; + + protected PollenEntityId<PollenResource> screenShotId; + + protected String consoleHistory; + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getCategory() { + return category; + } + + public void setCategory(String category) { + this.category = category; + } + + public String getBrowser() { + return browser; + } + + public void setBrowser(String browser) { + this.browser = browser; + } + + public String getOperatingSystem() { + return operatingSystem; + } + + public void setOperatingSystem(String operatingSystem) { + this.operatingSystem = operatingSystem; + } + + public String getPlatform() { + return platform; + } + + public void setPlatform(String platform) { + this.platform = platform; + } + + public Dimension getScreenResolution() { + return screenResolution; + } + + public void setScreenResolution(Dimension screenResolution) { + this.screenResolution = screenResolution; + } + + public Dimension getWindowDimension() { + return windowDimension; + } + + public void setWindowDimension(Dimension windowDimension) { + this.windowDimension = windowDimension; + } + + public Locale getLocale() { + return locale; + } + + public void setLocale(Locale locale) { + this.locale = locale; + } + + public String getLocation() { + return location; + } + + public void setLocation(String location) { + this.location = location; + } + + public String getLocationTitle() { + return locationTitle; + } + + public void setLocationTitle(String locationTitle) { + this.locationTitle = locationTitle; + } + + public PollenEntityId<PollenResource> getScreenShotId() { + return screenShotId; + } + + public void setScreenShotId(PollenEntityId<PollenResource> screenShotId) { + this.screenShotId = screenShotId; + } + + public String getConsoleHistory() { + return consoleHistory; + } + + public void setConsoleHistory(String consoleHistory) { + this.consoleHistory = consoleHistory; + } +} diff --git a/pollen-services/src/main/java/org/chorem/pollen/services/config/PollenServicesConfig.java b/pollen-services/src/main/java/org/chorem/pollen/services/config/PollenServicesConfig.java index c1c4461f..bca8228d 100644 --- a/pollen-services/src/main/java/org/chorem/pollen/services/config/PollenServicesConfig.java +++ b/pollen-services/src/main/java/org/chorem/pollen/services/config/PollenServicesConfig.java @@ -37,6 +37,7 @@ import org.nuiton.config.ApplicationConfig; import org.nuiton.config.ArgumentsParserException; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Properties; @@ -138,4 +139,12 @@ public class PollenServicesConfig extends GeneratedPollenServicesConfig { public Boolean getDefaultVoteNotification() { return Boolean.valueOf(get().getOption(PollenServicesConfigOption.DEFAULT_VOTE_NOTIFICATION.getKey())); } + + public List<String> getMailsFeedbackList() { + return Lists.newArrayList(get().getOption(PollenServicesConfigOption.MAILS_FEEDBACK.getKey()).split(",")); + } + + public Locale getFeedbackLocale() { + return Locale.forLanguageTag(get().getOption(PollenServicesConfigOption.LOCALE_FEEDBACK.getKey())); + } } diff --git a/pollen-services/src/main/java/org/chorem/pollen/services/service/FeedbackService.java b/pollen-services/src/main/java/org/chorem/pollen/services/service/FeedbackService.java new file mode 100644 index 00000000..cb769280 --- /dev/null +++ b/pollen-services/src/main/java/org/chorem/pollen/services/service/FeedbackService.java @@ -0,0 +1,38 @@ +package org.chorem.pollen.services.service; + +import org.chorem.pollen.persistence.entity.PollenUser; +import org.chorem.pollen.services.bean.FeedbackBean; +import org.chorem.pollen.services.bean.PollenUserBean; +import org.chorem.pollen.services.service.mail.EmailService; +import org.chorem.pollen.services.service.mail.FeedbackEmail; + +import java.util.List; +import java.util.Locale; + +/** + * @author Sylvain Bavencoff - bavencoff@codelutin.com + */ +public class FeedbackService extends PollenServiceSupport { + + public boolean addFeedback(FeedbackBean feedbackBean) { + List<String> mailsFeedbackList = getPollenServiceConfig().getMailsFeedbackList(); + Locale locale = getPollenServiceConfig().getFeedbackLocale(); + + PollenUser user = getConnectedUser(); + PollenUserBean userBean = null; + if (user != null) { + userBean = new PollenUserBean(); + userBean.fromEntity(user); + } + + + EmailService emailService = getEmailService(); + FeedbackEmail feedbackEMail = emailService.newFeedbackMail(feedbackBean, userBean, locale); + + feedbackEMail.getTos().addAll(mailsFeedbackList); + emailService.send(feedbackEMail); + + return true; + } + +} diff --git a/pollen-services/src/main/java/org/chorem/pollen/services/service/mail/EmailService.java b/pollen-services/src/main/java/org/chorem/pollen/services/service/mail/EmailService.java index dfd31ced..f1dbe453 100644 --- a/pollen-services/src/main/java/org/chorem/pollen/services/service/mail/EmailService.java +++ b/pollen-services/src/main/java/org/chorem/pollen/services/service/mail/EmailService.java @@ -45,7 +45,9 @@ import org.chorem.pollen.persistence.entity.PollenResource; import org.chorem.pollen.persistence.entity.PollenUser; import org.chorem.pollen.persistence.entity.Report; import org.chorem.pollen.persistence.entity.Vote; +import org.chorem.pollen.services.bean.FeedbackBean; import org.chorem.pollen.services.bean.PollenEntityId; +import org.chorem.pollen.services.bean.PollenUserBean; import org.chorem.pollen.services.config.PollenServicesConfig; import org.chorem.pollen.services.service.PollenServiceSupport; import org.nuiton.topia.persistence.BeanTopiaConfiguration; @@ -558,4 +560,19 @@ public class EmailService extends PollenServiceSupport { } return value; } + + public FeedbackEmail newFeedbackMail(FeedbackBean feedbackBean, PollenUserBean user, Locale locale) { + FeedbackEmail email = new FeedbackEmail(locale); + email.setUser(user); + email.setFeedback(feedbackBean); + email.setFeedbackDate(getNow()); + if (feedbackBean.getScreenShotId() != null) { + String screenShotUrl = getPollenUIUrlRenderService().getResourceUrl( + getUIContext().getResourceUrl(), + feedbackBean.getScreenShotId().getReducedId()); + email.setScreenShotUrl(screenShotUrl); + } + email.setApplicationVersion(getPollenServiceConfig().getVersion()); + return email; + } } diff --git a/pollen-services/src/main/java/org/chorem/pollen/services/service/mail/FeedbackEmail.java b/pollen-services/src/main/java/org/chorem/pollen/services/service/mail/FeedbackEmail.java new file mode 100644 index 00000000..9e341651 --- /dev/null +++ b/pollen-services/src/main/java/org/chorem/pollen/services/service/mail/FeedbackEmail.java @@ -0,0 +1,72 @@ +package org.chorem.pollen.services.service.mail; + +import org.chorem.pollen.services.bean.FeedbackBean; +import org.chorem.pollen.services.bean.PollenUserBean; +import org.nuiton.i18n.I18n; + +import java.util.Date; +import java.util.Locale; + +/** + * @author Sylvain Bavencoff - bavencoff@codelutin.com + */ +public class FeedbackEmail extends PollenMail { + + protected PollenUserBean user; + + protected FeedbackBean feedback; + + protected Date feedbackDate; + + protected String applicationVersion; + private String screenShotUrl; + + public FeedbackEmail(Locale locale) { + super(locale); + } + + @Override + public String getSubject() { + return I18n.l(locale, "pollen.service.mail.feedbackEmail.subject", feedback.getCategory()); + } + + public PollenUserBean getUser() { + return user; + } + + public void setUser(PollenUserBean user) { + this.user = user; + } + + public FeedbackBean getFeedback() { + return feedback; + } + + public void setFeedback(FeedbackBean feedback) { + this.feedback = feedback; + } + + public void setFeedbackDate(Date feedbackDate) { + this.feedbackDate = feedbackDate; + } + + public String getFeedBackDate() { + return formatDate(feedbackDate); + } + + public String getApplicationVersion() { + return applicationVersion; + } + + public void setApplicationVersion(String applicationVersion) { + this.applicationVersion = applicationVersion; + } + + public void setScreenShotUrl(String screenShotUrl) { + this.screenShotUrl = screenShotUrl; + } + + public String getScreenShotUrl() { + return screenShotUrl; + } +} diff --git a/pollen-services/src/main/resources/email/FeedbackEmail.mustache b/pollen-services/src/main/resources/email/FeedbackEmail.mustache new file mode 100644 index 00000000..8aa5d042 --- /dev/null +++ b/pollen-services/src/main/resources/email/FeedbackEmail.mustache @@ -0,0 +1,33 @@ +On {{feedbackDate}} + +Description +----------- +{{feedback.description}} + + +Location +-------- +Page title : {{feedback.locationTitle}} +URL : {{feedback.location}} + +Environment +----------- +Browser : {{feedback.browser}} +Operating System : {{feedback.operatingSystem}} +Platform : {{feedback.platform}} +Screen resolution : {{feedback.screenResolution.width}} x {{feedback.screenResolution.height}} (L x H) +window resolution : {{feedback.windowDimension.width}} x {{feedback.windowDimension.height}} (L x H) +User : {{#user}}{{user.name}} ({{user.email}}){{/user}}{{^user}}Anonymous{{/user}} +Locale : {{feedback.locale}} + +Services version: {{applicationVersion}} + +{{#screenShotUrl}} +Screen shot +----------- +{{screenShotUrl}} +{{/screenShotUrl}} + +Console history +--------------- +{{{feedback.consoleHistory}}} diff --git a/pollen-services/src/main/resources/email/FeedbackEmail_fr.mustache b/pollen-services/src/main/resources/email/FeedbackEmail_fr.mustache new file mode 100644 index 00000000..ae33ed8a --- /dev/null +++ b/pollen-services/src/main/resources/email/FeedbackEmail_fr.mustache @@ -0,0 +1,32 @@ +Le {{feedbackDate}} + +Description +----------- +{{feedback.description}} + + +Emplacement +----------- +Titre de la page : {{feedback.locationTitle}} +URL : {{feedback.location}} + +Environnement +------------- +Navigateur : {{feedback.browser}} +Système d'exploitation : {{feedback.operatingSystem}} +Plateforme : {{feedback.platform}} +Résolution : {{feedback.screenResolution.width}} x {{feedback.screenResolution.height}} (L x H) +Taille d'affichage : {{feedback.windowDimension.width}} x {{feedback.windowDimension.height}} (L x H) +Langue : {{feedback.locale}} +Utilisateur : {{#user}}{{user.name}} ({{user.email}}){{/user}}{{^user}}Anonyme{{/user}} +Version services : {{applicationVersion}} +{{#screenShotUrl}} + + Capture d'écran +--------------- +{{screenShotUrl}} +{{/screenShotUrl}} + +Historique de la console +------------------------ +{{{feedback.consoleHistory}}} \ No newline at end of file diff --git a/pollen-services/src/main/resources/i18n/pollen-services_en_GB.properties b/pollen-services/src/main/resources/i18n/pollen-services_en_GB.properties index 0b78d8bd..1ba0fd60 100644 --- a/pollen-services/src/main/resources/i18n/pollen-services_en_GB.properties +++ b/pollen-services/src/main/resources/i18n/pollen-services_en_GB.properties @@ -19,6 +19,8 @@ pollen.configuration.defaultVoteCountingType=Default vote counting type used whe pollen.configuration.defaultVoteNotification=Default notification type for the votes of a poll pollen.configuration.defaultVoteVisibility=Default vote visiblity pollen.configuration.devMode=Dev mode +pollen.configuration.feedback.locale=locale to send feedback +pollen.configuration.feedback.mails=mails to send feedback pollen.configuration.logConfigurationFile=Path to log configuration file pollen.configuration.registration.emailAddressPattern=Regular expression that the user email address must match for registration pollen.configuration.report.maxScore=Maximum score for reporting before administrators are notified @@ -156,3 +158,4 @@ pollen.service.mail.VoteAddedEmail.subject=[Pollen] A vote was added in poll %s pollen.service.mail.VoteDeletedEmail.subject=[Pollen] A vote was deleted in poll %s pollen.service.mail.VoteEditedEmail.subject=[Pollen] A vote was edited in poll %s pollen.service.mail.VoteSummaryEmail.subject=[Pollen] Summary of the votes since %s for the poll %s +pollen.service.mail.feedbackEmail.subject=[Pollen] feedback - %s diff --git a/pollen-services/src/main/resources/i18n/pollen-services_fr_FR.properties b/pollen-services/src/main/resources/i18n/pollen-services_fr_FR.properties index 2675ce7c..497bc4ab 100644 --- a/pollen-services/src/main/resources/i18n/pollen-services_fr_FR.properties +++ b/pollen-services/src/main/resources/i18n/pollen-services_fr_FR.properties @@ -19,6 +19,8 @@ pollen.configuration.defaultVoteCountingType=Type de dépouillement par défaut pollen.configuration.defaultVoteNotification=Type de notification par défaut pour les votes pollen.configuration.defaultVoteVisibility=Visibilité des votes par défaut pollen.configuration.devMode=Mode développement +pollen.configuration.feedback.locale=La locale pour envoyer les retours utlisateur +pollen.configuration.feedback.mails=Courriel destinataires des retours utilisateur pollen.configuration.logConfigurationFile=Chemin vers le fichier de configuration des logs pollen.configuration.registration.emailAddressPattern=Expression régulière que doivent vérifier les adresses email des utilisateurs lors de l'inscription pollen.configuration.report.maxScore=Score maximum pour un signalement avant que les administrateurs soient avertis @@ -154,3 +156,4 @@ pollen.service.mail.UserAccountPasswordChangedEmail.subject=[Pollen] Confirmatio pollen.service.mail.VoteAddedEmail.subject=[Pollen] Un nouveau vote a été ajouté au sondage %s pollen.service.mail.VoteDeletedEmail.subject=[Pollen] Un vote a été supprimé sur le sondage %s pollen.service.mail.VoteEditedEmail.subject=[Pollen] Un vote a été modifié du sondage %s +pollen.service.mail.feedbackEmail.subject=[Pollen] retour utilisateur - %s diff --git a/pollen-ui-riot-js/package.json b/pollen-ui-riot-js/package.json index 82231d23..27c1099c 100644 --- a/pollen-ui-riot-js/package.json +++ b/pollen-ui-riot-js/package.json @@ -42,7 +42,9 @@ "webpack-dev-server": "^2.5.1" }, "dependencies": { + "console.history": "^1.5.0", "font-awesome": "4.7.0", + "html2canvas": "^0.5.0-beta4", "moment": "^2.17.1", "nprogress": "^0.2.0", "object.values": "^1.0.4", diff --git a/pollen-ui-riot-js/src/main/web/i18n.json b/pollen-ui-riot-js/src/main/web/i18n.json index 8c56ba3d..e75d1078 100644 --- a/pollen-ui-riot-js/src/main/web/i18n.json +++ b/pollen-ui-riot-js/src/main/web/i18n.json @@ -224,6 +224,7 @@ "header_myFavoriteLists": "Mes listes de favoris", "header_createOtherPoll": "Nouveau sondage standard", "header_createDatePoll": "Nouveau sondage de dates", + "header_feedback": "Un bug, une remarque ?", "poll_cancel": "Annuler", "poll_description_next": "Suivant", "poll_description_info": "Informations", @@ -521,7 +522,17 @@ "report_reports_title": "Signalements", "report_toIgnore": "Ignorer", "report_toEnable": "Ré-activer", - "report_ignores": "Ignorés" + "report_ignores": "Ignorés", + "feedback_title": "Envoyer un bug, une remarque", + "feedback_detail": "Si vous voyez un bug ou si vous avez simplement une remarque à faire, remplissez ce formulaire pour nous faire parvenir votre avis. N'hésitez pas à en abuser !", + "feedback_category": "Catégorie", + "feedback_category_bug": "Bug", + "feedback_category_ergonomics": "Ergonomie", + "feedback_category_other": "Autres", + "feedback_description": "Merci de détailler ci-dessous votre rapport de bug ou votre remarque", + "feedback_descriptionNotBlank": "La description ne doit pas être blanc", + "feedback_uploadScreenShot": "Copie d'écran", + "feedback_create_success": "La remarque est envoyée." }, "en": { "main_pollen_title": "Pollen - ", @@ -739,6 +750,7 @@ "header_myPolls": "My polls", "header_myInvitedPolls": "Invited polls", "header_myParticipatedPolls": "Participated polls", + "header_feedback": "Bug, observation ?", "header_polls": "Polls", "header_users": "Users", "header_myFavoriteLists": "My favorite lists", @@ -1024,6 +1036,16 @@ "report_reports_title": "Reports", "report_toIgnore": "Ignore", "report_toEnable": "Enable", - "report_ignores": "Ignores" + "report_ignores": "Ignores", + "feedback_title": "Send a bug, an observation", + "feedback_detail": "If you see a bug or if you have an obvervation to do, set this form for send your opinon. Do not hesitate to abuse it !", + "feedback_category": "Category", + "feedback_category_bug": "Bug", + "feedback_category_ergonomics": "Ergonomics", + "feedback_category_other": "Others", + "feedback_description": "Thank to details below your bug report ou your observation", + "feedback_descriptionNotBlank": "Observation must be not blank", + "feedback_uploadScreenShot": "Screenshot", + "feedback_create_success": "Observation has benn send." } } diff --git a/pollen-ui-riot-js/src/main/web/index.js b/pollen-ui-riot-js/src/main/web/index.js index 1a76a90c..30945fa3 100644 --- a/pollen-ui-riot-js/src/main/web/index.js +++ b/pollen-ui-riot-js/src/main/web/index.js @@ -18,6 +18,7 @@ // * along with this program. If not, see <http://www.gnu.org/licenses/>. // * #L% // */ +require("console.history"); riot.mixin(require("./js/I18nHelper")); riot.mixin(require("./js/UIHelper")); riot.mixin({logger: require("./js/Logger")}); diff --git a/pollen-ui-riot-js/src/main/web/js/FeedbackService.js b/pollen-ui-riot-js/src/main/web/js/FeedbackService.js new file mode 100644 index 00000000..9678daf3 --- /dev/null +++ b/pollen-ui-riot-js/src/main/web/js/FeedbackService.js @@ -0,0 +1,33 @@ +/*- + * #%L + * Pollen :: UI (Riot Js) + * %% + * Copyright (C) 2009 - 2017 CodeLutin + * %% + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * #L% + */ +let singleton = require("./Singleton"); +let FetchService = require("./FetchService"); + +class FeedbackService extends FetchService { + + createfeedback(form) { + let url = "/v1/feedback"; + return this.post(url, form); + } + +} + +module.exports = singleton(FeedbackService); diff --git a/pollen-ui-riot-js/src/main/web/js/Logger.js b/pollen-ui-riot-js/src/main/web/js/Logger.js index 3ce2f687..acb186e2 100644 --- a/pollen-ui-riot-js/src/main/web/js/Logger.js +++ b/pollen-ui-riot-js/src/main/web/js/Logger.js @@ -14,5 +14,9 @@ module.exports = { error(message) { console.error(message); + }, + + getHistory() { + return console.history; } }; diff --git a/pollen-ui-riot-js/src/main/web/js/ResourceService.js b/pollen-ui-riot-js/src/main/web/js/ResourceService.js index 42521b84..8799faf4 100644 --- a/pollen-ui-riot-js/src/main/web/js/ResourceService.js +++ b/pollen-ui-riot-js/src/main/web/js/ResourceService.js @@ -37,6 +37,11 @@ class ResourceService extends FetchService { return this.form(url, {resource: resource}, true); } + create64(resource64) { + let url = "/v1/resources64"; + return this.post(url, resource64); + } + getResource(resourceId) { let url = this._getUrlPrefix(resourceId); return this.get(url); diff --git a/pollen-ui-riot-js/src/main/web/tag/PollenHeader.tag.html b/pollen-ui-riot-js/src/main/web/tag/PollenHeader.tag.html index 7a434820..14e3003e 100644 --- a/pollen-ui-riot-js/src/main/web/tag/PollenHeader.tag.html +++ b/pollen-ui-riot-js/src/main/web/tag/PollenHeader.tag.html @@ -19,6 +19,7 @@ * #L% */ require("./HeaderI18n.tag.html"); +require("./popup/FeedbackModal.tag.html"); <PollenHeader> <a class="header-home instance-title" href="#home" target="_top"></a> @@ -58,6 +59,13 @@ require("./HeaderI18n.tag.html"); <HeaderI18n/> + <a class="header-link" onclick="{openFeedback}" title={__.feedback}> + <i class="fa fa-exclamation" aria-hidden="true"></i> + <i class="fa fa-question" aria-hidden="true"></i> + </a> + + <feedbackModal ref="feedbackModal"> + </div> <script type="es6"> @@ -90,6 +98,15 @@ require("./HeaderI18n.tag.html"); this.listen("user", this.onUserChange); + this.openFeedback = () => { + this.refs.feedbackModal.open().then(() => { + this.update(); + }, () => { + this.update(); + }); + this.update(); + }; + </script> <style> diff --git a/pollen-ui-riot-js/src/main/web/tag/popup/FeedbackModal.tag.html b/pollen-ui-riot-js/src/main/web/tag/popup/FeedbackModal.tag.html new file mode 100644 index 00000000..370ea216 --- /dev/null +++ b/pollen-ui-riot-js/src/main/web/tag/popup/FeedbackModal.tag.html @@ -0,0 +1,169 @@ +require("./Modal.tag.html"); + +<FeedbackModal> + + <modal header={__.title} + onsubmit={sendFeedback} + ref="modal"> + + {parent.__.detail} + + <div class="o-fieldset categories"> + <legend class="o-fieldset__legend">{parent.__.category}</legend> + <label class="c-field c-field--choice"> + <input type="radio" + name="category" + ref="category" + required + value="BUG"> + {parent.__.category_bug} + </label> + <label class="c-field c-field--choice"> + <input type="radio" + name="category" + ref="category" + value="ERGONOMICS"> + {parent.__.category_ergonomics} + </label> + <label class="c-field c-field--choice"> + <input type="radio" + name="category" + ref="category" + value="OTHER"> + {parent.__.category_other} + </label> + </div> + <div class="o-form-element"> + <label class="c-label" for="description">{parent.__.description}</label> + <textarea class="c-field" + name="description" + ref="description" + pattern=".*[^\s]+.*" + title={parent.__.descriptionNotBlank} + required> + </textarea> + </div> + + <div class="o-form-element"> + <label class="c-toggle c-toggle--info"> + {parent.__.uploadScreenShot} + <input type="checkbox" + name="uploadScreenShot" + ref="uploadScreenShot"> + <div class="c-toggle__track"> + <div class="c-toggle__handle"></div> + </div> + </label> + </div> + + </modal> + + <script type="es6"> + let session = require("../../js/Session"); + let Message = require("../../js/Message"); + this.installBundle(session, "feedback"); + let resourceService = require("../../js/ResourceService"); + let feedbackService = require("../../js/FeedbackService"); + let html2canvas = require("html2canvas"); + + this.open = () => { + this.refs.modal.refs.category.forEach(input => {input.checked = false;}); + this.refs.modal.refs.description.value = ""; + this.refs.modal.refs.uploadScreenShot.checked = true; + return this.refs.modal.open(); + }; + + + this.getBrowserNameAndVersion = () => { + let ua = navigator.userAgent, tem, + match = ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*([\d\.]+)/i) || []; + if (/trident/i.test(match[1])) { + tem = /\brv[ :]+(\d+(\.\d+)?)/g.exec(ua) || []; + return "IE " + (tem[1] || ""); + } + match = match[2] ? [match[1], match[2]] : [navigator.appName, navigator.appVersion, "-?"]; + if ((tem = ua.match(/version\/([\.\d]+)/i)) !== null) { + match[2] = tem[1]; + } + return match.join(" "); + }; + + this.getOperatingSystemNameAndVersion = () => { + var OSName; + if (navigator.userAgent.indexOf("Windows NT 6.3") !== -1) {OSName = "Windows 8.1";} + if (navigator.userAgent.indexOf("Windows NT 6.2") !== -1) {OSName = "Windows 8";} + if (navigator.userAgent.indexOf("Windows NT 6.1") !== -1) {OSName = "Windows 7";} + if (navigator.userAgent.indexOf("Windows NT 6.0") !== -1) {OSName = "Windows Vista";} + if (navigator.userAgent.indexOf("Windows NT 5.1") !== -1) {OSName = "Windows XP";} + if (navigator.userAgent.indexOf("Windows NT 5.0") !== -1) {OSName = "Windows 2000";} + if (navigator.userAgent.indexOf("Mac") !== -1) {OSName = "Mac/iOS";} + if (navigator.userAgent.indexOf("X11") !== -1) {OSName = "UNIX";} + if (navigator.userAgent.indexOf("Linux") !== -1) {OSName = "Linux";} + return OSName; + }; + + this.sendFeedback = () => { + let promiseScreenshot; + if (this.refs.modal.refs.uploadScreenShot.checked) { + // take screenshot + this.refs.modal.openModal = false; + this.update(); + promiseScreenshot = html2canvas(document.body).then(canvas => { + let type = "image/jpeg"; + let screenshot = canvas.toDataURL(type); + let data = screenshot.replace("data:" + type + ";base64,", ""); + let resource64 = { + data: data, + name: "screenShot.jpeg", + contentType: type + }; + return resourceService.create64(resource64); + }); + } else { + promiseScreenshot = Promise.resolve(); + } + return promiseScreenshot.then(screenshotId => { + let feedback = { + description: this.refs.modal.refs.description.value, + category: this.refs.modal.refs.category.find(radio => radio.checked).value, + browser: this.getBrowserNameAndVersion(), + operatingSystem: this.getOperatingSystemNameAndVersion(), + platform: navigator.platform, + screenResolution: {width: screen.width, height: screen.height}, + windowDimension: {width: window.innerWidth, height: window.innerHeight}, + locale: navigator.language, + location: window.location.href, + locationTitle: window.document.title, + screenShotId: screenshotId.id, + consoleHistory: JSON.stringify(this.logger.getHistory(), null, 2) + }; + return feedbackService.createfeedback(feedback).then(() => { + this.bus.trigger("message", this.__.create_success, "success"); + }, (error) => { + this.refs.modal.openModal = true; + this.update(); + return Promise.reject(error); + }); + }); + }; + + </script> + + <style> + .categories { + display: flex; + flex-wrap: nowrap; + flex-direction: row; + } + + .categories > * { + width: auto; + } + + .categories .o-fieldset__legend { + padding: 0.5em; + } + + </style> + +</FeedbackModal> -- To stop receiving notification emails like this one, please contact chorem.org SCM administrator <admin+scm@chorem.org>.