diff --git a/src/main/java/no/bioforsk/vips/logic/controller/servlet/ForecastConfigurationController.java b/src/main/java/no/bioforsk/vips/logic/controller/servlet/ForecastConfigurationController.java new file mode 100644 index 0000000000000000000000000000000000000000..4e31f707c26a506580fbb47aaf7bf0f427d8c3f5 --- /dev/null +++ b/src/main/java/no/bioforsk/vips/logic/controller/servlet/ForecastConfigurationController.java @@ -0,0 +1,240 @@ +package no.bioforsk.vips.logic.controller.servlet; + +import java.io.IOException; +import java.util.List; +import java.util.Map; +import javax.persistence.EntityManager; +import javax.persistence.PersistenceContext; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import no.bioforsk.vips.logic.controller.session.ForecastBean; +import no.bioforsk.vips.logic.controller.session.UserBean; +import no.bioforsk.vips.logic.entity.PointOfInterest; +import no.bioforsk.vips.logic.entity.PointOfInterestWeatherStation; +import no.bioforsk.vips.logic.entity.VipsLogicRole; +import no.bioforsk.vips.logic.entity.VipsLogicUser; +import no.bioforsk.vips.logic.scheduling.model.ForecastConfiguration; +import no.bioforsk.vips.logic.util.SessionControllerGetter; +import no.bioforsk.vips.util.ServletUtil; +import no.bioforsk.web.forms.FormField; +import no.bioforsk.web.forms.FormValidation; +import no.bioforsk.web.forms.FormValidator; + +/** + * Handles form configuration actions + * @copyright 2013 <a href="http://www.bioforsk.no/">Bioforsk</a> + * @author Tor-Einar Skog <tor-einar.skog@bioforsk.no> + */ +public class ForecastConfigurationController extends HttpServlet { + + @PersistenceContext(unitName="VIPSLogic-PU") + EntityManager em; + + /** + * Processes requests for both HTTP <code>GET</code> and <code>POST</code> + * methods. + * + * @param request servlet request + * @param response servlet response + * @throws ServletException if a servlet-specific error occurs + * @throws IOException if an I/O error occurs + */ + protected void processRequest(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + response.setContentType("text/html;charset=UTF-8"); + + String action = request.getParameter("action"); + VipsLogicUser user = (VipsLogicUser) request.getSession().getAttribute("user"); + ForecastBean forecastBean = SessionControllerGetter.getForecastBean(); + UserBean userBean = SessionControllerGetter.getUserBean(); + + // Default: View list of users + // for SUPERUSERS and ORGANIZATION ADMINS + if(action == null) + { + if(userBean.authorizeUser(user, VipsLogicRole.ORGANIZATION_ADMINISTRATOR, VipsLogicRole.SUPERUSER)) + { + List<ForecastConfiguration> forecasts; + if(user.isSuperUser()) + { + forecasts = forecastBean.getForecastConfigurations(); + } + else + { + forecasts = forecastBean.getForecastConfigurations(user.getOrganizationId()); + } + request.setAttribute("forecastConfigurations", forecasts); + request.setAttribute("modelInformation", forecastBean.getIndexedModelInformation()); + // If this is a redirect from a controller, with a message to be passed on + request.setAttribute("messageKey", request.getParameter("messageKey")); + request.getRequestDispatcher("/forecastConfigurationList.ftl").forward(request, response); + } + else + { + response.sendError(403,"Access not authorized"); // HTTP Forbidden + } + + } + + // View and edit a forecast configuration + // for SUPERUSERS and ORGANIZATION ADMINS + else if(action.equals("viewForecastConfiguration")) + { + if(userBean.authorizeUser(user, VipsLogicRole.ORGANIZATION_ADMINISTRATOR, VipsLogicRole.SUPERUSER)) + { + try + { + Long forecastConfigurationId = Long.valueOf(request.getParameter("forecastConfigurationId")); + ForecastConfiguration forecastConfiguration = em.find(ForecastConfiguration.class, forecastConfigurationId); + // No forecastconfiguration found, assuming user want to register new + if(forecastConfiguration == null) + { + forecastConfiguration = new ForecastConfiguration(); + } + // Only superusers can view and edit forecasts from other organizations + if(! user.isSuperUser() && forecastConfiguration.getVipsLogicUserId() != null && !forecastConfiguration.getVipsLogicUserId().getOrganizationId().equals(user.getOrganizationId())) + { + response.sendError(403,"Access not authorized"); // HTTP Forbidden + } + else + { + // We must get date formats! + Map<String, FormField> formFields = FormValidator.getFormFields("forecastConfigurationForm",getServletContext()); + // TODO: More intelligent selection of locations, weather stations and users + request.setAttribute("locationPointOfInterests", em.createNamedQuery("PointOfInterest.findAll").getResultList()); + request.setAttribute("weatherStationPointOfInterests", em.createNamedQuery("PointOfInterestWeatherStation.findAll").getResultList()); + request.setAttribute("vipsLogicUsers", em.createNamedQuery("VipsLogicUser.findAll").getResultList()); + request.setAttribute("dateStart_dateFormat", formFields.get("dateStart").getDateFormat()); + request.setAttribute("dateEnd_dateFormat", formFields.get("dateEnd").getDateFormat()); + request.setAttribute("forecastConfiguration", forecastConfiguration); + request.setAttribute("modelInformations", em.createNamedQuery("ModelInformation.findAll").getResultList()); + request.setAttribute("messageKey", request.getParameter("messageKey")); + request.getRequestDispatcher("/forecastConfigurationForm.ftl").forward(request, response); + + } + } + catch(NullPointerException | NumberFormatException ex) + { + response.sendError(500, "Invalid forecast configurationId " + request.getParameter("forecastConfigurationId")); + } + + } + else + { + response.sendError(403,"Access not authorized"); // HTTP Forbidden + } + } + + // Store forecast configuration + // Authorization: SUPERUSERS and ORGANIZATION ADMINS + else if(action.equals("forecastConfigurationFormSubmit")) + { + if(userBean.authorizeUser(user, VipsLogicRole.ORGANIZATION_ADMINISTRATOR, VipsLogicRole.SUPERUSER)) + { + try + { + Long forecastConfigurationId = Long.valueOf(request.getParameter("forecastConfigurationId")); + ForecastConfiguration forecastConfiguration = em.find(ForecastConfiguration.class, forecastConfigurationId); + // No forecastconfiguration found, assuming user want to register new + if(forecastConfiguration == null) + { + forecastConfiguration = new ForecastConfiguration(); + } + // Only superusers can view and edit forecasts from other organizations + if(! user.isSuperUser() && forecastConfiguration.getVipsLogicUserId() != null && !forecastConfiguration.getVipsLogicUserId().getOrganizationId().equals(user.getOrganizationId())) + { + response.sendError(403,"Access not authorized"); // HTTP Forbidden + } + else + { + // Standard form validation + FormValidation formValidation = FormValidator.validateForm("forecastConfigurationForm",request,getServletContext()); + if(formValidation.isValid()) + { + // Store form config + forecastConfiguration.setModelId(formValidation.getFormField("modelId").getWebValue()); + PointOfInterest locationPoi = em.find(PointOfInterest.class, formValidation.getFormField("locationPointOfInterestId").getValueAsInteger()); + forecastConfiguration.setLocationPointOfInterestId(locationPoi); + PointOfInterest weatherStationPoi = em.find(PointOfInterestWeatherStation.class, formValidation.getFormField("weatherStationPointOfInterestId").getValueAsInteger()); + forecastConfiguration.setWeatherStationPointOfInterestId(weatherStationPoi); + forecastConfiguration.setDateStart(formValidation.getFormField("dateStart").getValueAsDate()); + forecastConfiguration.setDateEnd(formValidation.getFormField("dateEnd").getValueAsDate()); + VipsLogicUser forecastConfigurationUser = em.find(VipsLogicUser.class, formValidation.getFormField("vipsLogicUserId").getValueAsInteger()); + forecastConfiguration.setVipsCoreUserId(forecastConfigurationUser); + forecastBean.storeForecastConfiguration(forecastConfiguration); + + request.setAttribute("messageKey", request.getParameter("formConfigurationUpdated")); + response.sendRedirect(new StringBuilder("http://").append(ServletUtil.getServerName(request)).append("/forecastConfiguration?action=viewForecastConfiguration&forecastConfigurationId=").append(forecastConfiguration.getForecastConfigurationId()).append("&messageKey=").append("forecastConfigurationUpdated").toString()); + } + else + { + // Return to form with error messages + request.setAttribute("formValidation", formValidation); + // We must get date formats! + Map<String, FormField> formFields = FormValidator.getFormFields("forecastConfigurationForm",getServletContext()); + // TODO: More intelligent selection of locations, weather stations and users + request.setAttribute("locationPointOfInterests", em.createNamedQuery("PointOfInterest.findAll").getResultList()); + request.setAttribute("weatherStationPointOfInterests", em.createNamedQuery("PointOfInterestWeatherStation.findAll").getResultList()); + request.setAttribute("vipsLogicUsers", em.createNamedQuery("VipsLogicUser.findAll").getResultList()); + request.setAttribute("dateStart_dateFormat", formFields.get("dateStart").getDateFormat()); + request.setAttribute("dateEnd_dateFormat", formFields.get("dateEnd").getDateFormat()); + request.setAttribute("modelInformations", em.createNamedQuery("ModelInformation.findAll").getResultList()); + request.setAttribute("forecastConfiguration", forecastConfiguration); + request.getRequestDispatcher("/forecastConfigurationForm.ftl").forward(request, response); + } + } + } + catch(NullPointerException | NumberFormatException ex) + { + response.sendError(500, "Invalid forecast configurationId " + request.getParameter("forecastConfigurationId")); + } + } + else + { + response.sendError(403,"Access not authorized"); // HTTP Forbidden + } + } + } + + // <editor-fold defaultstate="collapsed" desc="HttpServlet methods. Click on the + sign on the left to edit the code."> + /** + * Handles the HTTP <code>GET</code> method. + * + * @param request servlet request + * @param response servlet response + * @throws ServletException if a servlet-specific error occurs + * @throws IOException if an I/O error occurs + */ + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + processRequest(request, response); + } + + /** + * Handles the HTTP <code>POST</code> method. + * + * @param request servlet request + * @param response servlet response + * @throws ServletException if a servlet-specific error occurs + * @throws IOException if an I/O error occurs + */ + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + processRequest(request, response); + } + + /** + * Returns a short description of the servlet. + * + * @return a String containing servlet description + */ + @Override + public String getServletInfo() { + return "Short description"; + }// </editor-fold> + +} diff --git a/src/main/java/no/bioforsk/vips/logic/controller/servlet/UserControllerServlet.java b/src/main/java/no/bioforsk/vips/logic/controller/servlet/UserController.java similarity index 99% rename from src/main/java/no/bioforsk/vips/logic/controller/servlet/UserControllerServlet.java rename to src/main/java/no/bioforsk/vips/logic/controller/servlet/UserController.java index 71240593b0af1c0c2b1f88aef4f774d1ca154e45..1041bfbfc431e58b2efa3c59c1e5c9bbeab103b0 100644 --- a/src/main/java/no/bioforsk/vips/logic/controller/servlet/UserControllerServlet.java +++ b/src/main/java/no/bioforsk/vips/logic/controller/servlet/UserController.java @@ -29,7 +29,7 @@ import no.bioforsk.web.forms.FormValidator; * @copyright 2013 <a href="http://www.bioforsk.no/">Bioforsk</a> * @author Tor-Einar Skog <tor-einar.skog@bioforsk.no> */ -public class UserControllerServlet extends HttpServlet { +public class UserController extends HttpServlet { @PersistenceContext(unitName="VIPSLogic-PU") EntityManager em; diff --git a/src/main/java/no/bioforsk/vips/logic/controller/session/ForecastBean.java b/src/main/java/no/bioforsk/vips/logic/controller/session/ForecastBean.java index fc7bef3b3919b49ecfeaaa5a26f456f3713a8d75..0aa541fbb07827cb6274860e000cd04aea61e3fb 100644 --- a/src/main/java/no/bioforsk/vips/logic/controller/session/ForecastBean.java +++ b/src/main/java/no/bioforsk/vips/logic/controller/session/ForecastBean.java @@ -1,12 +1,25 @@ package no.bioforsk.vips.logic.controller.session; +import java.util.Date; +import java.util.HashMap; import java.util.List; +import java.util.Map; import javax.ejb.Stateless; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import javax.persistence.Query; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.Response; +import no.bioforsk.vips.coremanager.service.ManagerResource; import no.bioforsk.vips.logic.entity.ForecastResult; +import no.bioforsk.vips.logic.entity.ModelInformation; +import no.bioforsk.vips.logic.entity.Organization; +import no.bioforsk.vips.logic.entity.VipsLogicUser; import no.bioforsk.vips.logic.scheduling.model.ForecastConfiguration; +import org.codehaus.jackson.JsonNode; +import org.jboss.resteasy.client.jaxrs.ResteasyWebTarget; /** * @copyright 2013 <a href="http://www.bioforsk.no/">Bioforsk</a> @@ -51,6 +64,24 @@ public class ForecastBean { return q.getResultList(); } + /** + * Returns _ALL_ forecasts. Not for the faint hearted + * @return + */ + public List<ForecastConfiguration> getForecastConfigurations() + { + return em.createNamedQuery("ForecastConfiguration.findAll").getResultList(); + } + + public List<ForecastConfiguration> getForecastConfigurations(Organization organization) + { + List<VipsLogicUser> organizationUsers = em + .createNamedQuery("VipsLogicUser.findByOrganizationId") + .setParameter("organizationId", organization) + .getResultList(); + return em.createNamedQuery("ForecastConfiguration.findByVipsLogicUserIds").setParameter("vipsLogicUserIds", organizationUsers).getResultList(); + } + /** * Fetches one specific forecast configuration * @param forecastConfigurationId @@ -60,4 +91,70 @@ public class ForecastBean { { return em.find(ForecastConfiguration.class, forecastConfigurationId); } + + /** + * Requests all info about models currently available in VIPSCoreManager + * Stores in local db for easy access. + */ + public void updateModelInformation() + { + // Get all model Ids from Core Manager + Response resp = this.getManagerResource().printModelListJSON(); + for(JsonNode modelIdItem: resp.readEntity(JsonNode.class).findValues("modelId")) + { + String modelId = modelIdItem.getValueAsText(); + + // We get the corresponding modelInformation entry + ModelInformation modelInformation = em.find(ModelInformation.class, modelId); + if(modelInformation == null) + { + modelInformation = new ModelInformation(modelId); + em.persist(modelInformation); + modelInformation.setDateFirstRegistered(new Date()); + } + + // Retrieve and store information + modelInformation.setDefaultName(this.getManagerResource().printModelName(modelId).readEntity(String.class)); + modelInformation.setDefaultDescription(this.getManagerResource().printModelDescription(modelId).readEntity(String.class)); + modelInformation.setLicense(this.getManagerResource().printModelLicense(modelId).readEntity(String.class)); + modelInformation.setCopyrightHolder(this.getManagerResource().printModelCopyright(modelId).readEntity(String.class)); + modelInformation.setUsage(this.getManagerResource().printModelUsage(modelId).readEntity(String.class)); + modelInformation.setSampleConfig(this.getManagerResource().printModelSampleConfig(modelId).readEntity(String.class)); + modelInformation.setDateLastRegistered(new Date()); + } + + } + + /** + * + * @return All registered models accessible by ModelId as key + */ + public Map<String,ModelInformation> getIndexedModelInformation() + { + Map<String, ModelInformation> retVal = new HashMap<>(); + for(ModelInformation mi: (List<ModelInformation>) em.createNamedQuery("ModelInformation.findAll").getResultList()) + { + retVal.put(mi.getModelId(), mi); + } + return retVal; + } + + public void storeForecastConfiguration(ForecastConfiguration forecastConfiguration) + { + em.merge(forecastConfiguration); + } + + + /** + * Get the interface for REST resources in VIPSCoreManager + * @return + */ + private ManagerResource getManagerResource() + { + Client client = ClientBuilder.newClient(); + WebTarget target = client.target(System.getProperty("no.bioforsk.vips.coremanager.VIPSCOREMANAGER_URL")); + ResteasyWebTarget rTarget = (ResteasyWebTarget) target; + ManagerResource resource = rTarget.proxy(ManagerResource.class); + return resource; + } } diff --git a/src/main/java/no/bioforsk/vips/logic/controller/session/UserBean.java b/src/main/java/no/bioforsk/vips/logic/controller/session/UserBean.java index b3d97e2ba91c1895c1f303c5d3c3c93634e4bf5c..1c9c2d7fe2443a559becb7849a5de475f9f56653 100644 --- a/src/main/java/no/bioforsk/vips/logic/controller/session/UserBean.java +++ b/src/main/java/no/bioforsk/vips/logic/controller/session/UserBean.java @@ -123,15 +123,6 @@ public class UserBean { public void deleteUser(VipsLogicUser user) { user = em.find(VipsLogicUser.class, user.getUserId()); - - // Remove dependent stuff - /* User authentication for this user - for(UserAuthentication auth: user.getUserAuthenticationSet()) - { - System.out.println("Auth[DEBUG]=" + auth.getUsername()); - em.remove(auth); - } - */ em.remove(user); } diff --git a/src/main/java/no/bioforsk/vips/logic/entity/ModelInformation.java b/src/main/java/no/bioforsk/vips/logic/entity/ModelInformation.java new file mode 100644 index 0000000000000000000000000000000000000000..4ed6fd0c8321b08d4c9fc18c65827c38ce01004b --- /dev/null +++ b/src/main/java/no/bioforsk/vips/logic/entity/ModelInformation.java @@ -0,0 +1,173 @@ +package no.bioforsk.vips.logic.entity; + +import java.io.Serializable; +import java.util.Date; +import javax.persistence.Basic; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import javax.xml.bind.annotation.XmlRootElement; + +/** + * @copyright 2013 <a href="http://www.bioforsk.no/">Bioforsk</a> + * @author Tor-Einar Skog <tor-einar.skog@bioforsk.no> + */ +@Entity +@Table(name = "model_information") +@XmlRootElement +@NamedQueries({ + @NamedQuery(name = "ModelInformation.findAll", query = "SELECT m FROM ModelInformation m"), + @NamedQuery(name = "ModelInformation.findByModelId", query = "SELECT m FROM ModelInformation m WHERE m.modelId = :modelId"), + @NamedQuery(name = "ModelInformation.findByDateFirstRegistered", query = "SELECT m FROM ModelInformation m WHERE m.dateFirstRegistered = :dateFirstRegistered"), + @NamedQuery(name = "ModelInformation.findByDateLastRegistered", query = "SELECT m FROM ModelInformation m WHERE m.dateLastRegistered = :dateLastRegistered"), + @NamedQuery(name = "ModelInformation.findByDefaultName", query = "SELECT m FROM ModelInformation m WHERE m.defaultName = :defaultName"), + @NamedQuery(name = "ModelInformation.findByCopyrightHolder", query = "SELECT m FROM ModelInformation m WHERE m.copyrightHolder = :copyrightHolder"), + @NamedQuery(name = "ModelInformation.findByLicense", query = "SELECT m FROM ModelInformation m WHERE m.license = :license"), + @NamedQuery(name = "ModelInformation.findByDefaultDescription", query = "SELECT m FROM ModelInformation m WHERE m.defaultDescription = :defaultDescription"), + @NamedQuery(name = "ModelInformation.findBySampleConfig", query = "SELECT m FROM ModelInformation m WHERE m.sampleConfig = :sampleConfig"), + @NamedQuery(name = "ModelInformation.findByUsage", query = "SELECT m FROM ModelInformation m WHERE m.usage = :usage")}) +public class ModelInformation implements Serializable { + private static final long serialVersionUID = 1L; + @Id + @Basic(optional = false) + @NotNull + @Size(min = 1, max = 10) + @Column(name = "model_id") + private String modelId; + @Column(name = "date_first_registered") + @Temporal(TemporalType.DATE) + private Date dateFirstRegistered; + @Column(name = "date_last_registered") + @Temporal(TemporalType.DATE) + private Date dateLastRegistered; + @Size(max = 63) + @Column(name = "default_name") + private String defaultName; + @Size(max = 255) + @Column(name = "copyright_holder") + private String copyrightHolder; + @Size(max = 2147483647) + @Column(name = "license") + private String license; + @Size(max = 2147483647) + @Column(name = "default_description") + private String defaultDescription; + @Size(max = 2147483647) + @Column(name = "sample_config") + private String sampleConfig; + @Size(max = 2147483647) + @Column(name = "usage") + private String usage; + + public ModelInformation() { + } + + public ModelInformation(String modelId) { + this.modelId = modelId; + } + + public String getModelId() { + return modelId; + } + + public void setModelId(String modelId) { + this.modelId = modelId; + } + + public Date getDateFirstRegistered() { + return dateFirstRegistered; + } + + public void setDateFirstRegistered(Date dateFirstRegistered) { + this.dateFirstRegistered = dateFirstRegistered; + } + + public Date getDateLastRegistered() { + return dateLastRegistered; + } + + public void setDateLastRegistered(Date dateLastRegistered) { + this.dateLastRegistered = dateLastRegistered; + } + + public String getDefaultName() { + return defaultName; + } + + public void setDefaultName(String defaultName) { + this.defaultName = defaultName; + } + + public String getCopyrightHolder() { + return copyrightHolder; + } + + public void setCopyrightHolder(String copyrightHolder) { + this.copyrightHolder = copyrightHolder; + } + + public String getLicense() { + return license; + } + + public void setLicense(String license) { + this.license = license; + } + + public String getDefaultDescription() { + return defaultDescription; + } + + public void setDefaultDescription(String defaultDescription) { + this.defaultDescription = defaultDescription; + } + + public String getSampleConfig() { + return sampleConfig; + } + + public void setSampleConfig(String sampleConfig) { + this.sampleConfig = sampleConfig; + } + + public String getUsage() { + return usage; + } + + public void setUsage(String usage) { + this.usage = usage; + } + + @Override + public int hashCode() { + int hash = 0; + hash += (modelId != null ? modelId.hashCode() : 0); + return hash; + } + + @Override + public boolean equals(Object object) { + // TODO: Warning - this method won't work in the case the id fields are not set + if (!(object instanceof ModelInformation)) { + return false; + } + ModelInformation other = (ModelInformation) object; + if ((this.modelId == null && other.modelId != null) || (this.modelId != null && !this.modelId.equals(other.modelId))) { + return false; + } + return true; + } + + @Override + public String toString() { + return "no.bioforsk.vips.logic.entity.ModelInformation[ modelId=" + modelId + " ]"; + } + +} diff --git a/src/main/java/no/bioforsk/vips/logic/scheduling/model/ForecastConfiguration.java b/src/main/java/no/bioforsk/vips/logic/scheduling/model/ForecastConfiguration.java index 26e612e81d9f8b9776d6cbdd7ae39565b11f59ee..fdb5ab3307dda9100ec8543a72da8f829c850d4a 100644 --- a/src/main/java/no/bioforsk/vips/logic/scheduling/model/ForecastConfiguration.java +++ b/src/main/java/no/bioforsk/vips/logic/scheduling/model/ForecastConfiguration.java @@ -22,6 +22,7 @@ import javax.validation.constraints.Size; import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.XmlTransient; import no.bioforsk.vips.logic.entity.PointOfInterest; +import no.bioforsk.vips.logic.entity.VipsLogicUser; import org.codehaus.jackson.annotate.JsonIgnore; /** @@ -37,7 +38,9 @@ import org.codehaus.jackson.annotate.JsonIgnore; @NamedQuery(name = "ForecastConfiguration.findByModelId", query = "SELECT f FROM ForecastConfiguration f WHERE f.modelId = :modelId"), @NamedQuery(name = "ForecastConfiguration.findByDateStart", query = "SELECT f FROM ForecastConfiguration f WHERE f.dateStart = :dateStart"), @NamedQuery(name = "ForecastConfiguration.findByDateEnd", query = "SELECT f FROM ForecastConfiguration f WHERE f.dateEnd = :dateEnd"), - @NamedQuery(name = "ForecastConfiguration.findByVipsCoreUserId", query = "SELECT f FROM ForecastConfiguration f WHERE f.vipsCoreUserId = :vipsCoreUserId")}) + @NamedQuery(name = "ForecastConfiguration.findByVipsLogicUserId", query = "SELECT f FROM ForecastConfiguration f WHERE f.vipsLogicUserId = :vipsLogicUserId"), + @NamedQuery(name = "ForecastConfiguration.findByVipsLogicUserIds", query = "SELECT f FROM ForecastConfiguration f WHERE f.vipsLogicUserId IN (:vipsLogicUserIds)") +}) public class ForecastConfiguration implements Serializable { @OneToMany(cascade = CascadeType.ALL, mappedBy = "forecastConfiguration") private Set<ForecastModelConfiguration> forecastModelConfigurationSet; @@ -56,8 +59,9 @@ public class ForecastConfiguration implements Serializable { @Column(name = "date_end") @Temporal(TemporalType.DATE) private Date dateEnd; - @Column(name = "vips_core_user_id") - private Integer vipsCoreUserId; + @JoinColumn(name = "vips_logic_user_id", referencedColumnName = "user_id") + @ManyToOne(optional = false) + private VipsLogicUser vipsLogicUserId; @JoinColumn(name = "location_point_of_interest_id", referencedColumnName = "point_of_interest_id") @ManyToOne private PointOfInterest locationPointOfInterestId; @@ -104,12 +108,12 @@ public class ForecastConfiguration implements Serializable { this.dateEnd = dateEnd; } - public Integer getVipsCoreUserId() { - return vipsCoreUserId; + public VipsLogicUser getVipsLogicUserId() { + return vipsLogicUserId; } - public void setVipsCoreUserId(Integer vipsCoreUserId) { - this.vipsCoreUserId = vipsCoreUserId; + public void setVipsCoreUserId(VipsLogicUser vipsLogicUserId) { + this.vipsLogicUserId = vipsLogicUserId; } public PointOfInterest getLocationPointOfInterestId() { diff --git a/src/main/java/no/bioforsk/web/forms/FormField.java b/src/main/java/no/bioforsk/web/forms/FormField.java index a6106482f80a957f0a8de8b528d855d711d5d54a..8a09c075974276343abcc139066dde3893e7f18d 100644 --- a/src/main/java/no/bioforsk/web/forms/FormField.java +++ b/src/main/java/no/bioforsk/web/forms/FormField.java @@ -1,5 +1,8 @@ package no.bioforsk.web.forms; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; import org.codehaus.jackson.annotate.JsonIgnore; /** @@ -17,12 +20,14 @@ public class FormField { public static String FORM_FIELD_TYPE_STRING = "STRING"; public static String FORM_FIELD_TYPE_INTEGER = "INTEGER"; public static String FORM_FIELD_TYPE_DOUBLE = "DOUBLE"; + public static String FORM_FIELD_TYPE_DATE = "DATE"; public static String FORM_FIELD_TYPE_PASSWORD = "PASSWORD"; public static String FORM_FIELD_TYPE_EMAIL = "EMAIL"; public static String FORM_FIELD_TYPE_SELECT_SINGLE = "SELECT_SINGLE"; public static String FORM_FIELD_TYPE_SELECT_MULTIPLE = "SELECT_MULTIPLE"; - + // Standard format for dates is the ISO-8861-format + public static String DATE_DEFAULT_FORMAT = "yyyy-MM-dd"; private String name; private String label; @@ -36,6 +41,7 @@ public class FormField { private Integer minValue; private Integer maxValue; private String nullValue; + private String dateFormat; public Integer getValueAsInteger() @@ -53,6 +59,17 @@ public class FormField { return Double.valueOf(this.webValue[0]); } + public Date getValueAsDate() + { + try + { + return new SimpleDateFormat(this.getDateFormat()).parse(this.getWebValue()); + } + catch(ParseException ex) + { + return null; + } + } /** @@ -253,5 +270,26 @@ public class FormField { this.nullValue = nullValue; } + /** + * @return the dateFormat (if not set, it returns FormField.DATE_DEFAULT_FORMAT + */ + public String getDateFormat() { + if(this.dateFormat != null) + { + return dateFormat; + } + else + { + return DATE_DEFAULT_FORMAT; + } + } + + /** + * @param dateFormat the dateFormat to set + */ + public void setDateFormat(String dateFormat) { + this.dateFormat = dateFormat; + } + } diff --git a/src/main/java/no/bioforsk/web/forms/FormValidator.java b/src/main/java/no/bioforsk/web/forms/FormValidator.java index 8fe7e1c7574079e921b474e00ed199a18cd29c24..278430f08322e2f07a60e50670588eb3267884dc 100644 --- a/src/main/java/no/bioforsk/web/forms/FormValidator.java +++ b/src/main/java/no/bioforsk/web/forms/FormValidator.java @@ -3,8 +3,13 @@ package no.bioforsk.web.forms; import java.io.IOException; import java.io.InputStream; import java.text.MessageFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; import java.util.List; import java.util.Locale; +import java.util.Map; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import no.bioforsk.vips.logic.authenticate.PasswordValidationException; @@ -27,15 +32,14 @@ import org.codehaus.jackson.type.TypeReference; * @author Tor-Einar Skog <tor-einar.skog@bioforsk.no> */ public class FormValidator { + + public static String RELATION_TYPE_EQUALS = "EQUALS"; + public static String RELATION_TYPE_AFTER = "AFTER"; public static FormValidation validateForm(String formName, HttpServletRequest request, ServletContext servletContext) throws IOException { - ObjectMapper mapper = new ObjectMapper(); - JsonFactory factory = mapper.getJsonFactory(); - InputStream in = servletContext.getResourceAsStream("/formdefinitions/" + formName + ".json"); - - JsonParser parser = factory.createJsonParser(in); - JsonNode formDefinition = mapper.readTree(parser); - List<FormField> fields = mapper.convertValue(formDefinition.findValue("fields"), new TypeReference<List<FormField>>(){}); + + JsonNode formDefinition = getFormDefinition(formName, servletContext); + List<FormField> fields = new ObjectMapper().convertValue(formDefinition.findValue("fields"), new TypeReference<List<FormField>>(){}); FormValidation retVal = new FormValidation(); for(FormField field: fields) @@ -63,6 +67,23 @@ public class FormValidator { } } + // Dates + // We try to conver to date using the given format + if(field.getType().equals(FormField.FORM_FIELD_TYPE_DATE)) + { + SimpleDateFormat format = new SimpleDateFormat(field.getDateFormat()); + try + { + Date date = format.parse(field.getWebValue()); + } + catch(ParseException ex) + { + field.setValid(false); + field.setValidationMessage(MessageFormat.format(SessionLocaleUtil.getI18nText(request, "doesNotMatchDateFormat"), field.getDateFormat())); + } + + } + if(field.getType().equals(FormField.FORM_FIELD_TYPE_PASSWORD)) { try @@ -147,34 +168,93 @@ public class FormValidator { } // Check repeats - JsonNode repeatFields = formDefinition.findValue("repeatFields"); - if(repeatFields != null) + JsonNode relations = formDefinition.findValue("relations"); + if(relations != null) { - for(JsonNode item: repeatFields) + for(JsonNode item: relations) { + String relationType = item.findValue("relationType").getTextValue(); String primaryFieldName = item.findValue("primaryField").getTextValue(); - String repeatFieldName = item.findValue("repeatField").getTextValue(); + String secondaryFieldName = item.findValue("secondaryField").getTextValue(); FormField primaryField = retVal.getFormField(primaryFieldName); - FormField repeatField = retVal.getFormField(repeatFieldName); - if(primaryField == null || repeatField == null) + FormField secondaryField = retVal.getFormField(secondaryFieldName); + if(primaryField == null || secondaryField == null) { continue; } - - if(!primaryField.getWebValue().equals(repeatField.getWebValue())) + + // Repetition of strings + if(relationType.equals(RELATION_TYPE_EQUALS)) { - repeatField.setValid(false); - repeatField.setValidationMessage( - MessageFormat.format(SessionLocaleUtil.getI18nText(request, "xIsNotEqualToY"), - SessionLocaleUtil.getI18nText(request, repeatFieldName), - SessionLocaleUtil.getI18nText(request, primaryFieldName) - ) - ); + if(!primaryField.getWebValue().equals(secondaryField.getWebValue())) + { + primaryField.setValid(false); + primaryField.setValidationMessage( + MessageFormat.format(SessionLocaleUtil.getI18nText(request, "xIsNotEqualToY"), + SessionLocaleUtil.getI18nText(request, primaryFieldName), + SessionLocaleUtil.getI18nText(request, secondaryFieldName) + ) + ); + } + } + // Ordering of dates + else if(relationType.equals(RELATION_TYPE_AFTER)) + { + if(primaryField.getValueAsDate().compareTo(secondaryField.getValueAsDate()) < 0) + { + primaryField.setValid(false); + primaryField.setValidationMessage( + MessageFormat.format(SessionLocaleUtil.getI18nText(request, "xIsNotAfterY"), + SessionLocaleUtil.getI18nText(request, primaryFieldName), + SessionLocaleUtil.getI18nText(request, secondaryFieldName) + ) + ); + } } } } return retVal; } + + /** + * Fetches all fields and their constrants from form definition + * @param formName name of JSON file with form definition + * @param servletContext + * @return + * @throws IOException + */ + public static JsonNode getFormDefinition(String formName, ServletContext servletContext) throws IOException + { + ObjectMapper mapper = new ObjectMapper(); + JsonFactory factory = mapper.getJsonFactory(); + InputStream in = servletContext.getResourceAsStream("/formdefinitions/" + formName + ".json"); + + JsonParser parser = factory.createJsonParser(in); + JsonNode formDefinition = mapper.readTree(parser); + return formDefinition; + } + + /** + * + * @param formName + * @param servletContext + * @return all form fields form the form definition, indexed by their name + * @throws IOException + */ + public static Map<String, FormField> getFormFields(String formName, ServletContext servletContext) throws IOException + { + JsonNode formDefinition = getFormDefinition(formName, servletContext); + ObjectMapper mapper = new ObjectMapper(); + JsonNode fields = formDefinition.findValue("fields"); + Map<String, FormField> retVal = new HashMap<>(); + for(JsonNode fieldNode:fields) + { + FormField formField = mapper.convertValue(fieldNode, FormField.class); + retVal.put(formField.getName(), formField); + } + + return retVal; + } } diff --git a/src/main/resources/no/bioforsk/vips/logic/i18n/vipslogictexts.properties b/src/main/resources/no/bioforsk/vips/logic/i18n/vipslogictexts.properties index 56b1dc3277bb893d21ea5e4994b14889fe3ccc83..ba0447b82ea290f11359c333573798d7af25146b 100644 --- a/src/main/resources/no/bioforsk/vips/logic/i18n/vipslogictexts.properties +++ b/src/main/resources/no/bioforsk/vips/logic/i18n/vipslogictexts.properties @@ -73,3 +73,19 @@ userUpdated=User was updated delete=Delete confirmDelete=Do you really want to delete? userDeleted=User was deleted +forecasts=Forecasts +dateStart=Date start +dateEnd=Date end +poi=Point of interest +viewForecastConfiguration=View forecast configuration +forecastConfigurationUpdated=Forecast configuration was updated +forecastConfigurationId=Forecast configuration +vipsLogicUserId=User +doesNotMatchDateFormat=Does not match format {0} +modelId=Forecasting model +APPLESCABM=Apple scab model +NAERSTADMO=N\u00e6rstad's model +locationPointOfInterestId=Location +weatherStationPointOfInterestId=Weather station +addNew=Add new +xIsNotAfterY={0} is not after {1} diff --git a/src/main/resources/no/bioforsk/vips/logic/i18n/vipslogictexts_no.properties b/src/main/resources/no/bioforsk/vips/logic/i18n/vipslogictexts_no.properties index 1aebcd732bd035b7e54ad74cd47013dcd9d841f7..b6b50b109f54cc769b4bd2fd46054ec899ae97f3 100644 --- a/src/main/resources/no/bioforsk/vips/logic/i18n/vipslogictexts_no.properties +++ b/src/main/resources/no/bioforsk/vips/logic/i18n/vipslogictexts_no.properties @@ -73,3 +73,19 @@ userUpdated=Brukeren ble oppdatert delete=Slett confirmDelete=\u00d8nsker du virkelig \u00e5 slette? userDeleted=Brukeren ble slettet +forecasts=Varsler +dateStart=Startdato +dateEnd=Sluttdato +poi=Geografisk punkt +viewForecastConfiguration=Se varseloppsett +forecastConfigurationUpdated=Varseloppsettet ble oppdatert +forecastConfigurationId=Varseloppsett +vipsLogicUserId=Bruker +doesNotMatchDateFormat=Stemmer ikke med formatet {0} +modelId=Varslingsmodell +APPLESCABM=Epleskurvmodell +NAERSTADMO=N\u00e6rstads modell +locationPointOfInterestId=Lokalitet +weatherStationPointOfInterestId=M\u00e5lestasjon +addNew=Legg til ny +xIsNotAfterY={0} er ikke etter {1} diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml index 952cb690fac186ec930118e1b9383fc592fb6bff..b7f8ad6c2c644ed640248c172583afbd057add1b 100644 --- a/src/main/webapp/WEB-INF/web.xml +++ b/src/main/webapp/WEB-INF/web.xml @@ -18,7 +18,7 @@ </servlet> <servlet> <servlet-name>UserControllerServlet</servlet-name> - <servlet-class>no.bioforsk.vips.logic.controller.servlet.UserControllerServlet</servlet-class> + <servlet-class>no.bioforsk.vips.logic.controller.servlet.UserController</servlet-class> </servlet> <servlet> <servlet-name>ResourceBundleJSServlet</servlet-name> @@ -28,6 +28,10 @@ <servlet-name>HttpErrorServlet</servlet-name> <servlet-class>no.bioforsk.vips.logic.controller.servlet.HttpErrorServlet</servlet-class> </servlet> + <servlet> + <servlet-name>ForecastController</servlet-name> + <servlet-class>no.bioforsk.vips.logic.controller.servlet.ForecastConfigurationController</servlet-class> + </servlet> <servlet-mapping> <servlet-name>PointOfInterestController</servlet-name> <url-pattern>/poi/*</url-pattern> @@ -58,6 +62,10 @@ <url-pattern>/error/404</url-pattern> <url-pattern>/error/403</url-pattern> </servlet-mapping> + <servlet-mapping> + <servlet-name>ForecastController</servlet-name> + <url-pattern>/forecastConfiguration</url-pattern> + </servlet-mapping> <welcome-file-list> <welcome-file>index.html</welcome-file> </welcome-file-list> diff --git a/src/main/webapp/formdefinitions/forecastConfigurationForm.json b/src/main/webapp/formdefinitions/forecastConfigurationForm.json new file mode 100644 index 0000000000000000000000000000000000000000..a5a08dbc69566202a6055f398c8a26cd0cf5d1fb --- /dev/null +++ b/src/main/webapp/formdefinitions/forecastConfigurationForm.json @@ -0,0 +1,51 @@ +{ + "_comment" : "Structure of the forecastConfigurationForm and how to validate it", + "fields": [ + { + "name" : "forecastConfigurationId", + "type" : "INTEGER", + "required" : true + }, + { + "name" : "vipsLogicUserId", + "type" : "INTEGER", + "type" : "SELECT_SINGLE", + "nullValue": "-1", + "required" : true + }, + { + "name" : "modelId", + "type" : "SELECT_SINGLE", + "nullValue": "-1", + "required" : true + }, + { + "name" : "locationPointOfInterestId", + "type" : "SELECT_SINGLE", + "nullValue": "-1", + "required" : true + }, + { + "name" : "weatherStationPointOfInterestId", + "type" : "SELECT_SINGLE", + "nullValue": "-1", + "required" : true + }, + { + "name" : "dateStart", + "type" : "DATE", + "dateFormat" : "yyyy-MM-dd", + "required" : true + }, + { + "name" : "dateEnd", + "type" : "DATE", + "dateFormat" : "yyyy-MM-dd", + "required" : true + } + + ], + "relations":[ + {"primaryField":"dateEnd","secondaryField":"dateStart", "relationType": "AFTER"} + ] +} diff --git a/src/main/webapp/formdefinitions/userRegistrationForm.json b/src/main/webapp/formdefinitions/userRegistrationForm.json index 7fc292325e9946f493c17f5931cc28672f908202..08e602f803a86efff6585a80160f5b00be19adc6 100644 --- a/src/main/webapp/formdefinitions/userRegistrationForm.json +++ b/src/main/webapp/formdefinitions/userRegistrationForm.json @@ -49,7 +49,7 @@ } ], - "repeatFields":[ - {"primaryField":"password","repeatField":"password2"} + "relations":[ + {"primaryField":"password2","secondaryField":"password", "relationType": "EQUALS"} ] } diff --git a/src/main/webapp/js/moment.min.js b/src/main/webapp/js/moment.min.js new file mode 100644 index 0000000000000000000000000000000000000000..568ad05cecad8a0cde991c6f744df72f9ac8ddea --- /dev/null +++ b/src/main/webapp/js/moment.min.js @@ -0,0 +1,6 @@ +//! moment.js +//! version : 2.4.0 +//! authors : Tim Wood, Iskren Chernev, Moment.js contributors +//! license : MIT +//! momentjs.com +(function(a){function b(a,b){return function(c){return i(a.call(this,c),b)}}function c(a,b){return function(c){return this.lang().ordinal(a.call(this,c),b)}}function d(){}function e(a){u(a),g(this,a)}function f(a){var b=o(a),c=b.year||0,d=b.month||0,e=b.week||0,f=b.day||0,g=b.hour||0,h=b.minute||0,i=b.second||0,j=b.millisecond||0;this._input=a,this._milliseconds=+j+1e3*i+6e4*h+36e5*g,this._days=+f+7*e,this._months=+d+12*c,this._data={},this._bubble()}function g(a,b){for(var c in b)b.hasOwnProperty(c)&&(a[c]=b[c]);return b.hasOwnProperty("toString")&&(a.toString=b.toString),b.hasOwnProperty("valueOf")&&(a.valueOf=b.valueOf),a}function h(a){return 0>a?Math.ceil(a):Math.floor(a)}function i(a,b){for(var c=a+"";c.length<b;)c="0"+c;return c}function j(a,b,c,d){var e,f,g=b._milliseconds,h=b._days,i=b._months;g&&a._d.setTime(+a._d+g*c),(h||i)&&(e=a.minute(),f=a.hour()),h&&a.date(a.date()+h*c),i&&a.month(a.month()+i*c),g&&!d&&bb.updateOffset(a),(h||i)&&(a.minute(e),a.hour(f))}function k(a){return"[object Array]"===Object.prototype.toString.call(a)}function l(a){return"[object Date]"===Object.prototype.toString.call(a)||a instanceof Date}function m(a,b,c){var d,e=Math.min(a.length,b.length),f=Math.abs(a.length-b.length),g=0;for(d=0;e>d;d++)(c&&a[d]!==b[d]||!c&&q(a[d])!==q(b[d]))&&g++;return g+f}function n(a){if(a){var b=a.toLowerCase().replace(/(.)s$/,"$1");a=Kb[a]||Lb[b]||b}return a}function o(a){var b,c,d={};for(c in a)a.hasOwnProperty(c)&&(b=n(c),b&&(d[b]=a[c]));return d}function p(b){var c,d;if(0===b.indexOf("week"))c=7,d="day";else{if(0!==b.indexOf("month"))return;c=12,d="month"}bb[b]=function(e,f){var g,h,i=bb.fn._lang[b],j=[];if("number"==typeof e&&(f=e,e=a),h=function(a){var b=bb().utc().set(d,a);return i.call(bb.fn._lang,b,e||"")},null!=f)return h(f);for(g=0;c>g;g++)j.push(h(g));return j}}function q(a){var b=+a,c=0;return 0!==b&&isFinite(b)&&(c=b>=0?Math.floor(b):Math.ceil(b)),c}function r(a,b){return new Date(Date.UTC(a,b+1,0)).getUTCDate()}function s(a){return t(a)?366:365}function t(a){return 0===a%4&&0!==a%100||0===a%400}function u(a){var b;a._a&&-2===a._pf.overflow&&(b=a._a[gb]<0||a._a[gb]>11?gb:a._a[hb]<1||a._a[hb]>r(a._a[fb],a._a[gb])?hb:a._a[ib]<0||a._a[ib]>23?ib:a._a[jb]<0||a._a[jb]>59?jb:a._a[kb]<0||a._a[kb]>59?kb:a._a[lb]<0||a._a[lb]>999?lb:-1,a._pf._overflowDayOfYear&&(fb>b||b>hb)&&(b=hb),a._pf.overflow=b)}function v(a){a._pf={empty:!1,unusedTokens:[],unusedInput:[],overflow:-2,charsLeftOver:0,nullInput:!1,invalidMonth:null,invalidFormat:!1,userInvalidated:!1,iso:!1}}function w(a){return null==a._isValid&&(a._isValid=!isNaN(a._d.getTime())&&a._pf.overflow<0&&!a._pf.empty&&!a._pf.invalidMonth&&!a._pf.nullInput&&!a._pf.invalidFormat&&!a._pf.userInvalidated,a._strict&&(a._isValid=a._isValid&&0===a._pf.charsLeftOver&&0===a._pf.unusedTokens.length)),a._isValid}function x(a){return a?a.toLowerCase().replace("_","-"):a}function y(a,b){return b.abbr=a,mb[a]||(mb[a]=new d),mb[a].set(b),mb[a]}function z(a){delete mb[a]}function A(a){var b,c,d,e,f=0,g=function(a){if(!mb[a]&&nb)try{require("./lang/"+a)}catch(b){}return mb[a]};if(!a)return bb.fn._lang;if(!k(a)){if(c=g(a))return c;a=[a]}for(;f<a.length;){for(e=x(a[f]).split("-"),b=e.length,d=x(a[f+1]),d=d?d.split("-"):null;b>0;){if(c=g(e.slice(0,b).join("-")))return c;if(d&&d.length>=b&&m(e,d,!0)>=b-1)break;b--}f++}return bb.fn._lang}function B(a){return a.match(/\[[\s\S]/)?a.replace(/^\[|\]$/g,""):a.replace(/\\/g,"")}function C(a){var b,c,d=a.match(rb);for(b=0,c=d.length;c>b;b++)d[b]=Pb[d[b]]?Pb[d[b]]:B(d[b]);return function(e){var f="";for(b=0;c>b;b++)f+=d[b]instanceof Function?d[b].call(e,a):d[b];return f}}function D(a,b){return a.isValid()?(b=E(b,a.lang()),Mb[b]||(Mb[b]=C(b)),Mb[b](a)):a.lang().invalidDate()}function E(a,b){function c(a){return b.longDateFormat(a)||a}var d=5;for(sb.lastIndex=0;d>=0&&sb.test(a);)a=a.replace(sb,c),sb.lastIndex=0,d-=1;return a}function F(a,b){var c;switch(a){case"DDDD":return vb;case"YYYY":case"GGGG":case"gggg":return wb;case"YYYYY":case"GGGGG":case"ggggg":return xb;case"S":case"SS":case"SSS":case"DDD":return ub;case"MMM":case"MMMM":case"dd":case"ddd":case"dddd":return zb;case"a":case"A":return A(b._l)._meridiemParse;case"X":return Cb;case"Z":case"ZZ":return Ab;case"T":return Bb;case"SSSS":return yb;case"MM":case"DD":case"YY":case"GG":case"gg":case"HH":case"hh":case"mm":case"ss":case"M":case"D":case"d":case"H":case"h":case"m":case"s":case"w":case"ww":case"W":case"WW":case"e":case"E":return tb;default:return c=new RegExp(N(M(a.replace("\\","")),"i"))}}function G(a){var b=(Ab.exec(a)||[])[0],c=(b+"").match(Hb)||["-",0,0],d=+(60*c[1])+q(c[2]);return"+"===c[0]?-d:d}function H(a,b,c){var d,e=c._a;switch(a){case"M":case"MM":null!=b&&(e[gb]=q(b)-1);break;case"MMM":case"MMMM":d=A(c._l).monthsParse(b),null!=d?e[gb]=d:c._pf.invalidMonth=b;break;case"D":case"DD":null!=b&&(e[hb]=q(b));break;case"DDD":case"DDDD":null!=b&&(c._dayOfYear=q(b));break;case"YY":e[fb]=q(b)+(q(b)>68?1900:2e3);break;case"YYYY":case"YYYYY":e[fb]=q(b);break;case"a":case"A":c._isPm=A(c._l).isPM(b);break;case"H":case"HH":case"h":case"hh":e[ib]=q(b);break;case"m":case"mm":e[jb]=q(b);break;case"s":case"ss":e[kb]=q(b);break;case"S":case"SS":case"SSS":case"SSSS":e[lb]=q(1e3*("0."+b));break;case"X":c._d=new Date(1e3*parseFloat(b));break;case"Z":case"ZZ":c._useUTC=!0,c._tzm=G(b);break;case"w":case"ww":case"W":case"WW":case"d":case"dd":case"ddd":case"dddd":case"e":case"E":a=a.substr(0,1);case"gg":case"gggg":case"GG":case"GGGG":case"GGGGG":a=a.substr(0,2),b&&(c._w=c._w||{},c._w[a]=b)}}function I(a){var b,c,d,e,f,g,h,i,j,k,l=[];if(!a._d){for(d=K(a),a._w&&null==a._a[hb]&&null==a._a[gb]&&(f=function(b){return b?b.length<3?parseInt(b,10)>68?"19"+b:"20"+b:b:null==a._a[fb]?bb().weekYear():a._a[fb]},g=a._w,null!=g.GG||null!=g.W||null!=g.E?h=X(f(g.GG),g.W||1,g.E,4,1):(i=A(a._l),j=null!=g.d?T(g.d,i):null!=g.e?parseInt(g.e,10)+i._week.dow:0,k=parseInt(g.w,10)||1,null!=g.d&&j<i._week.dow&&k++,h=X(f(g.gg),k,j,i._week.doy,i._week.dow)),a._a[fb]=h.year,a._dayOfYear=h.dayOfYear),a._dayOfYear&&(e=null==a._a[fb]?d[fb]:a._a[fb],a._dayOfYear>s(e)&&(a._pf._overflowDayOfYear=!0),c=S(e,0,a._dayOfYear),a._a[gb]=c.getUTCMonth(),a._a[hb]=c.getUTCDate()),b=0;3>b&&null==a._a[b];++b)a._a[b]=l[b]=d[b];for(;7>b;b++)a._a[b]=l[b]=null==a._a[b]?2===b?1:0:a._a[b];l[ib]+=q((a._tzm||0)/60),l[jb]+=q((a._tzm||0)%60),a._d=(a._useUTC?S:R).apply(null,l)}}function J(a){var b;a._d||(b=o(a._i),a._a=[b.year,b.month,b.day,b.hour,b.minute,b.second,b.millisecond],I(a))}function K(a){var b=new Date;return a._useUTC?[b.getUTCFullYear(),b.getUTCMonth(),b.getUTCDate()]:[b.getFullYear(),b.getMonth(),b.getDate()]}function L(a){a._a=[],a._pf.empty=!0;var b,c,d,e,f,g=A(a._l),h=""+a._i,i=h.length,j=0;for(d=E(a._f,g).match(rb)||[],b=0;b<d.length;b++)e=d[b],c=(F(e,a).exec(h)||[])[0],c&&(f=h.substr(0,h.indexOf(c)),f.length>0&&a._pf.unusedInput.push(f),h=h.slice(h.indexOf(c)+c.length),j+=c.length),Pb[e]?(c?a._pf.empty=!1:a._pf.unusedTokens.push(e),H(e,c,a)):a._strict&&!c&&a._pf.unusedTokens.push(e);a._pf.charsLeftOver=i-j,h.length>0&&a._pf.unusedInput.push(h),a._isPm&&a._a[ib]<12&&(a._a[ib]+=12),a._isPm===!1&&12===a._a[ib]&&(a._a[ib]=0),I(a),u(a)}function M(a){return a.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(a,b,c,d,e){return b||c||d||e})}function N(a){return a.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}function O(a){var b,c,d,e,f;if(0===a._f.length)return a._pf.invalidFormat=!0,a._d=new Date(0/0),void 0;for(e=0;e<a._f.length;e++)f=0,b=g({},a),v(b),b._f=a._f[e],L(b),w(b)&&(f+=b._pf.charsLeftOver,f+=10*b._pf.unusedTokens.length,b._pf.score=f,(null==d||d>f)&&(d=f,c=b));g(a,c||b)}function P(a){var b,c=a._i,d=Db.exec(c);if(d){for(a._pf.iso=!0,b=4;b>0;b--)if(d[b]){a._f=Fb[b-1]+(d[6]||" ");break}for(b=0;4>b;b++)if(Gb[b][1].exec(c)){a._f+=Gb[b][0];break}Ab.exec(c)&&(a._f+="Z"),L(a)}else a._d=new Date(c)}function Q(b){var c=b._i,d=ob.exec(c);c===a?b._d=new Date:d?b._d=new Date(+d[1]):"string"==typeof c?P(b):k(c)?(b._a=c.slice(0),I(b)):l(c)?b._d=new Date(+c):"object"==typeof c?J(b):b._d=new Date(c)}function R(a,b,c,d,e,f,g){var h=new Date(a,b,c,d,e,f,g);return 1970>a&&h.setFullYear(a),h}function S(a){var b=new Date(Date.UTC.apply(null,arguments));return 1970>a&&b.setUTCFullYear(a),b}function T(a,b){if("string"==typeof a)if(isNaN(a)){if(a=b.weekdaysParse(a),"number"!=typeof a)return null}else a=parseInt(a,10);return a}function U(a,b,c,d,e){return e.relativeTime(b||1,!!c,a,d)}function V(a,b,c){var d=eb(Math.abs(a)/1e3),e=eb(d/60),f=eb(e/60),g=eb(f/24),h=eb(g/365),i=45>d&&["s",d]||1===e&&["m"]||45>e&&["mm",e]||1===f&&["h"]||22>f&&["hh",f]||1===g&&["d"]||25>=g&&["dd",g]||45>=g&&["M"]||345>g&&["MM",eb(g/30)]||1===h&&["y"]||["yy",h];return i[2]=b,i[3]=a>0,i[4]=c,U.apply({},i)}function W(a,b,c){var d,e=c-b,f=c-a.day();return f>e&&(f-=7),e-7>f&&(f+=7),d=bb(a).add("d",f),{week:Math.ceil(d.dayOfYear()/7),year:d.year()}}function X(a,b,c,d,e){var f,g,h=new Date(Date.UTC(a,0)).getUTCDay();return c=null!=c?c:e,f=e-h+(h>d?7:0),g=7*(b-1)+(c-e)+f+1,{year:g>0?a:a-1,dayOfYear:g>0?g:s(a-1)+g}}function Y(a){var b=a._i,c=a._f;return"undefined"==typeof a._pf&&v(a),null===b?bb.invalid({nullInput:!0}):("string"==typeof b&&(a._i=b=A().preparse(b)),bb.isMoment(b)?(a=g({},b),a._d=new Date(+b._d)):c?k(c)?O(a):L(a):Q(a),new e(a))}function Z(a,b){bb.fn[a]=bb.fn[a+"s"]=function(a){var c=this._isUTC?"UTC":"";return null!=a?(this._d["set"+c+b](a),bb.updateOffset(this),this):this._d["get"+c+b]()}}function $(a){bb.duration.fn[a]=function(){return this._data[a]}}function _(a,b){bb.duration.fn["as"+a]=function(){return+this/b}}function ab(a){var b=!1,c=bb;"undefined"==typeof ender&&(this.moment=a?function(){return!b&&console&&console.warn&&(b=!0,console.warn("Accessing Moment through the global scope is deprecated, and will be removed in an upcoming release.")),c.apply(null,arguments)}:bb)}for(var bb,cb,db="2.4.0",eb=Math.round,fb=0,gb=1,hb=2,ib=3,jb=4,kb=5,lb=6,mb={},nb="undefined"!=typeof module&&module.exports,ob=/^\/?Date\((\-?\d+)/i,pb=/(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/,qb=/^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/,rb=/(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,4}|X|zz?|ZZ?|.)/g,sb=/(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g,tb=/\d\d?/,ub=/\d{1,3}/,vb=/\d{3}/,wb=/\d{1,4}/,xb=/[+\-]?\d{1,6}/,yb=/\d+/,zb=/[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i,Ab=/Z|[\+\-]\d\d:?\d\d/i,Bb=/T/i,Cb=/[\+\-]?\d+(\.\d{1,3})?/,Db=/^\s*\d{4}-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?([\+\-]\d\d:?\d\d|Z)?)?$/,Eb="YYYY-MM-DDTHH:mm:ssZ",Fb=["YYYY-MM-DD","GGGG-[W]WW","GGGG-[W]WW-E","YYYY-DDD"],Gb=[["HH:mm:ss.SSSS",/(T| )\d\d:\d\d:\d\d\.\d{1,3}/],["HH:mm:ss",/(T| )\d\d:\d\d:\d\d/],["HH:mm",/(T| )\d\d:\d\d/],["HH",/(T| )\d\d/]],Hb=/([\+\-]|\d\d)/gi,Ib="Date|Hours|Minutes|Seconds|Milliseconds".split("|"),Jb={Milliseconds:1,Seconds:1e3,Minutes:6e4,Hours:36e5,Days:864e5,Months:2592e6,Years:31536e6},Kb={ms:"millisecond",s:"second",m:"minute",h:"hour",d:"day",D:"date",w:"week",W:"isoWeek",M:"month",y:"year",DDD:"dayOfYear",e:"weekday",E:"isoWeekday",gg:"weekYear",GG:"isoWeekYear"},Lb={dayofyear:"dayOfYear",isoweekday:"isoWeekday",isoweek:"isoWeek",weekyear:"weekYear",isoweekyear:"isoWeekYear"},Mb={},Nb="DDD w W M D d".split(" "),Ob="M D H h m s w W".split(" "),Pb={M:function(){return this.month()+1},MMM:function(a){return this.lang().monthsShort(this,a)},MMMM:function(a){return this.lang().months(this,a)},D:function(){return this.date()},DDD:function(){return this.dayOfYear()},d:function(){return this.day()},dd:function(a){return this.lang().weekdaysMin(this,a)},ddd:function(a){return this.lang().weekdaysShort(this,a)},dddd:function(a){return this.lang().weekdays(this,a)},w:function(){return this.week()},W:function(){return this.isoWeek()},YY:function(){return i(this.year()%100,2)},YYYY:function(){return i(this.year(),4)},YYYYY:function(){return i(this.year(),5)},gg:function(){return i(this.weekYear()%100,2)},gggg:function(){return this.weekYear()},ggggg:function(){return i(this.weekYear(),5)},GG:function(){return i(this.isoWeekYear()%100,2)},GGGG:function(){return this.isoWeekYear()},GGGGG:function(){return i(this.isoWeekYear(),5)},e:function(){return this.weekday()},E:function(){return this.isoWeekday()},a:function(){return this.lang().meridiem(this.hours(),this.minutes(),!0)},A:function(){return this.lang().meridiem(this.hours(),this.minutes(),!1)},H:function(){return this.hours()},h:function(){return this.hours()%12||12},m:function(){return this.minutes()},s:function(){return this.seconds()},S:function(){return q(this.milliseconds()/100)},SS:function(){return i(q(this.milliseconds()/10),2)},SSS:function(){return i(this.milliseconds(),3)},SSSS:function(){return i(this.milliseconds(),3)},Z:function(){var a=-this.zone(),b="+";return 0>a&&(a=-a,b="-"),b+i(q(a/60),2)+":"+i(q(a)%60,2)},ZZ:function(){var a=-this.zone(),b="+";return 0>a&&(a=-a,b="-"),b+i(q(10*a/6),4)},z:function(){return this.zoneAbbr()},zz:function(){return this.zoneName()},X:function(){return this.unix()}},Qb=["months","monthsShort","weekdays","weekdaysShort","weekdaysMin"];Nb.length;)cb=Nb.pop(),Pb[cb+"o"]=c(Pb[cb],cb);for(;Ob.length;)cb=Ob.pop(),Pb[cb+cb]=b(Pb[cb],2);for(Pb.DDDD=b(Pb.DDD,3),g(d.prototype,{set:function(a){var b,c;for(c in a)b=a[c],"function"==typeof b?this[c]=b:this["_"+c]=b},_months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),months:function(a){return this._months[a.month()]},_monthsShort:"Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"),monthsShort:function(a){return this._monthsShort[a.month()]},monthsParse:function(a){var b,c,d;for(this._monthsParse||(this._monthsParse=[]),b=0;12>b;b++)if(this._monthsParse[b]||(c=bb.utc([2e3,b]),d="^"+this.months(c,"")+"|^"+this.monthsShort(c,""),this._monthsParse[b]=new RegExp(d.replace(".",""),"i")),this._monthsParse[b].test(a))return b},_weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),weekdays:function(a){return this._weekdays[a.day()]},_weekdaysShort:"Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),weekdaysShort:function(a){return this._weekdaysShort[a.day()]},_weekdaysMin:"Su_Mo_Tu_We_Th_Fr_Sa".split("_"),weekdaysMin:function(a){return this._weekdaysMin[a.day()]},weekdaysParse:function(a){var b,c,d;for(this._weekdaysParse||(this._weekdaysParse=[]),b=0;7>b;b++)if(this._weekdaysParse[b]||(c=bb([2e3,1]).day(b),d="^"+this.weekdays(c,"")+"|^"+this.weekdaysShort(c,"")+"|^"+this.weekdaysMin(c,""),this._weekdaysParse[b]=new RegExp(d.replace(".",""),"i")),this._weekdaysParse[b].test(a))return b},_longDateFormat:{LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D YYYY",LLL:"MMMM D YYYY LT",LLLL:"dddd, MMMM D YYYY LT"},longDateFormat:function(a){var b=this._longDateFormat[a];return!b&&this._longDateFormat[a.toUpperCase()]&&(b=this._longDateFormat[a.toUpperCase()].replace(/MMMM|MM|DD|dddd/g,function(a){return a.slice(1)}),this._longDateFormat[a]=b),b},isPM:function(a){return"p"===(a+"").toLowerCase().charAt(0)},_meridiemParse:/[ap]\.?m?\.?/i,meridiem:function(a,b,c){return a>11?c?"pm":"PM":c?"am":"AM"},_calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},calendar:function(a,b){var c=this._calendar[a];return"function"==typeof c?c.apply(b):c},_relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},relativeTime:function(a,b,c,d){var e=this._relativeTime[c];return"function"==typeof e?e(a,b,c,d):e.replace(/%d/i,a)},pastFuture:function(a,b){var c=this._relativeTime[a>0?"future":"past"];return"function"==typeof c?c(b):c.replace(/%s/i,b)},ordinal:function(a){return this._ordinal.replace("%d",a)},_ordinal:"%d",preparse:function(a){return a},postformat:function(a){return a},week:function(a){return W(a,this._week.dow,this._week.doy).week},_week:{dow:0,doy:6},_invalidDate:"Invalid date",invalidDate:function(){return this._invalidDate}}),bb=function(b,c,d,e){return"boolean"==typeof d&&(e=d,d=a),Y({_i:b,_f:c,_l:d,_strict:e,_isUTC:!1})},bb.utc=function(b,c,d,e){var f;return"boolean"==typeof d&&(e=d,d=a),f=Y({_useUTC:!0,_isUTC:!0,_l:d,_i:b,_f:c,_strict:e}).utc()},bb.unix=function(a){return bb(1e3*a)},bb.duration=function(a,b){var c,d,e,g=bb.isDuration(a),h="number"==typeof a,i=g?a._input:h?{}:a,j=null;return h?b?i[b]=a:i.milliseconds=a:(j=pb.exec(a))?(c="-"===j[1]?-1:1,i={y:0,d:q(j[hb])*c,h:q(j[ib])*c,m:q(j[jb])*c,s:q(j[kb])*c,ms:q(j[lb])*c}):(j=qb.exec(a))&&(c="-"===j[1]?-1:1,e=function(a){var b=a&&parseFloat(a.replace(",","."));return(isNaN(b)?0:b)*c},i={y:e(j[2]),M:e(j[3]),d:e(j[4]),h:e(j[5]),m:e(j[6]),s:e(j[7]),w:e(j[8])}),d=new f(i),g&&a.hasOwnProperty("_lang")&&(d._lang=a._lang),d},bb.version=db,bb.defaultFormat=Eb,bb.updateOffset=function(){},bb.lang=function(a,b){var c;return a?(b?y(x(a),b):null===b?(z(a),a="en"):mb[a]||A(a),c=bb.duration.fn._lang=bb.fn._lang=A(a),c._abbr):bb.fn._lang._abbr},bb.langData=function(a){return a&&a._lang&&a._lang._abbr&&(a=a._lang._abbr),A(a)},bb.isMoment=function(a){return a instanceof e},bb.isDuration=function(a){return a instanceof f},cb=Qb.length-1;cb>=0;--cb)p(Qb[cb]);for(bb.normalizeUnits=function(a){return n(a)},bb.invalid=function(a){var b=bb.utc(0/0);return null!=a?g(b._pf,a):b._pf.userInvalidated=!0,b},bb.parseZone=function(a){return bb(a).parseZone()},g(bb.fn=e.prototype,{clone:function(){return bb(this)},valueOf:function(){return+this._d+6e4*(this._offset||0)},unix:function(){return Math.floor(+this/1e3)},toString:function(){return this.clone().lang("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},toDate:function(){return this._offset?new Date(+this):this._d},toISOString:function(){return D(bb(this).utc(),"YYYY-MM-DD[T]HH:mm:ss.SSS[Z]")},toArray:function(){var a=this;return[a.year(),a.month(),a.date(),a.hours(),a.minutes(),a.seconds(),a.milliseconds()]},isValid:function(){return w(this)},isDSTShifted:function(){return this._a?this.isValid()&&m(this._a,(this._isUTC?bb.utc(this._a):bb(this._a)).toArray())>0:!1},parsingFlags:function(){return g({},this._pf)},invalidAt:function(){return this._pf.overflow},utc:function(){return this.zone(0)},local:function(){return this.zone(0),this._isUTC=!1,this},format:function(a){var b=D(this,a||bb.defaultFormat);return this.lang().postformat(b)},add:function(a,b){var c;return c="string"==typeof a?bb.duration(+b,a):bb.duration(a,b),j(this,c,1),this},subtract:function(a,b){var c;return c="string"==typeof a?bb.duration(+b,a):bb.duration(a,b),j(this,c,-1),this},diff:function(a,b,c){var d,e,f=this._isUTC?bb(a).zone(this._offset||0):bb(a).local(),g=6e4*(this.zone()-f.zone());return b=n(b),"year"===b||"month"===b?(d=432e5*(this.daysInMonth()+f.daysInMonth()),e=12*(this.year()-f.year())+(this.month()-f.month()),e+=(this-bb(this).startOf("month")-(f-bb(f).startOf("month")))/d,e-=6e4*(this.zone()-bb(this).startOf("month").zone()-(f.zone()-bb(f).startOf("month").zone()))/d,"year"===b&&(e/=12)):(d=this-f,e="second"===b?d/1e3:"minute"===b?d/6e4:"hour"===b?d/36e5:"day"===b?(d-g)/864e5:"week"===b?(d-g)/6048e5:d),c?e:h(e)},from:function(a,b){return bb.duration(this.diff(a)).lang(this.lang()._abbr).humanize(!b)},fromNow:function(a){return this.from(bb(),a)},calendar:function(){var a=this.diff(bb().zone(this.zone()).startOf("day"),"days",!0),b=-6>a?"sameElse":-1>a?"lastWeek":0>a?"lastDay":1>a?"sameDay":2>a?"nextDay":7>a?"nextWeek":"sameElse";return this.format(this.lang().calendar(b,this))},isLeapYear:function(){return t(this.year())},isDST:function(){return this.zone()<this.clone().month(0).zone()||this.zone()<this.clone().month(5).zone()},day:function(a){var b=this._isUTC?this._d.getUTCDay():this._d.getDay();return null!=a?(a=T(a,this.lang()),this.add({d:a-b})):b},month:function(a){var b,c=this._isUTC?"UTC":"";return null!=a?"string"==typeof a&&(a=this.lang().monthsParse(a),"number"!=typeof a)?this:(b=this.date(),this.date(1),this._d["set"+c+"Month"](a),this.date(Math.min(b,this.daysInMonth())),bb.updateOffset(this),this):this._d["get"+c+"Month"]()},startOf:function(a){switch(a=n(a)){case"year":this.month(0);case"month":this.date(1);case"week":case"isoWeek":case"day":this.hours(0);case"hour":this.minutes(0);case"minute":this.seconds(0);case"second":this.milliseconds(0)}return"week"===a?this.weekday(0):"isoWeek"===a&&this.isoWeekday(1),this},endOf:function(a){return a=n(a),this.startOf(a).add("isoWeek"===a?"week":a,1).subtract("ms",1)},isAfter:function(a,b){return b="undefined"!=typeof b?b:"millisecond",+this.clone().startOf(b)>+bb(a).startOf(b)},isBefore:function(a,b){return b="undefined"!=typeof b?b:"millisecond",+this.clone().startOf(b)<+bb(a).startOf(b)},isSame:function(a,b){return b="undefined"!=typeof b?b:"millisecond",+this.clone().startOf(b)===+bb(a).startOf(b)},min:function(a){return a=bb.apply(null,arguments),this>a?this:a},max:function(a){return a=bb.apply(null,arguments),a>this?this:a},zone:function(a){var b=this._offset||0;return null==a?this._isUTC?b:this._d.getTimezoneOffset():("string"==typeof a&&(a=G(a)),Math.abs(a)<16&&(a=60*a),this._offset=a,this._isUTC=!0,b!==a&&j(this,bb.duration(b-a,"m"),1,!0),this)},zoneAbbr:function(){return this._isUTC?"UTC":""},zoneName:function(){return this._isUTC?"Coordinated Universal Time":""},parseZone:function(){return"string"==typeof this._i&&this.zone(this._i),this},hasAlignedHourOffset:function(a){return a=a?bb(a).zone():0,0===(this.zone()-a)%60},daysInMonth:function(){return r(this.year(),this.month())},dayOfYear:function(a){var b=eb((bb(this).startOf("day")-bb(this).startOf("year"))/864e5)+1;return null==a?b:this.add("d",a-b)},weekYear:function(a){var b=W(this,this.lang()._week.dow,this.lang()._week.doy).year;return null==a?b:this.add("y",a-b)},isoWeekYear:function(a){var b=W(this,1,4).year;return null==a?b:this.add("y",a-b)},week:function(a){var b=this.lang().week(this);return null==a?b:this.add("d",7*(a-b))},isoWeek:function(a){var b=W(this,1,4).week;return null==a?b:this.add("d",7*(a-b))},weekday:function(a){var b=(this.day()+7-this.lang()._week.dow)%7;return null==a?b:this.add("d",a-b)},isoWeekday:function(a){return null==a?this.day()||7:this.day(this.day()%7?a:a-7)},get:function(a){return a=n(a),this[a]()},set:function(a,b){return a=n(a),"function"==typeof this[a]&&this[a](b),this},lang:function(b){return b===a?this._lang:(this._lang=A(b),this)}}),cb=0;cb<Ib.length;cb++)Z(Ib[cb].toLowerCase().replace(/s$/,""),Ib[cb]);Z("year","FullYear"),bb.fn.days=bb.fn.day,bb.fn.months=bb.fn.month,bb.fn.weeks=bb.fn.week,bb.fn.isoWeeks=bb.fn.isoWeek,bb.fn.toJSON=bb.fn.toISOString,g(bb.duration.fn=f.prototype,{_bubble:function(){var a,b,c,d,e=this._milliseconds,f=this._days,g=this._months,i=this._data;i.milliseconds=e%1e3,a=h(e/1e3),i.seconds=a%60,b=h(a/60),i.minutes=b%60,c=h(b/60),i.hours=c%24,f+=h(c/24),i.days=f%30,g+=h(f/30),i.months=g%12,d=h(g/12),i.years=d},weeks:function(){return h(this.days()/7)},valueOf:function(){return this._milliseconds+864e5*this._days+2592e6*(this._months%12)+31536e6*q(this._months/12)},humanize:function(a){var b=+this,c=V(b,!a,this.lang());return a&&(c=this.lang().pastFuture(b,c)),this.lang().postformat(c)},add:function(a,b){var c=bb.duration(a,b);return this._milliseconds+=c._milliseconds,this._days+=c._days,this._months+=c._months,this._bubble(),this},subtract:function(a,b){var c=bb.duration(a,b);return this._milliseconds-=c._milliseconds,this._days-=c._days,this._months-=c._months,this._bubble(),this},get:function(a){return a=n(a),this[a.toLowerCase()+"s"]()},as:function(a){return a=n(a),this["as"+a.charAt(0).toUpperCase()+a.slice(1)+"s"]()},lang:bb.fn.lang,toIsoString:function(){var a=Math.abs(this.years()),b=Math.abs(this.months()),c=Math.abs(this.days()),d=Math.abs(this.hours()),e=Math.abs(this.minutes()),f=Math.abs(this.seconds()+this.milliseconds()/1e3);return this.asSeconds()?(this.asSeconds()<0?"-":"")+"P"+(a?a+"Y":"")+(b?b+"M":"")+(c?c+"D":"")+(d||e||f?"T":"")+(d?d+"H":"")+(e?e+"M":"")+(f?f+"S":""):"P0D"}});for(cb in Jb)Jb.hasOwnProperty(cb)&&(_(cb,Jb[cb]),$(cb.toLowerCase()));_("Weeks",6048e5),bb.duration.fn.asMonths=function(){return(+this-31536e6*this.years())/2592e6+12*this.years()},bb.lang("en",{ordinal:function(a){var b=a%10,c=1===q(a%100/10)?"th":1===b?"st":2===b?"nd":3===b?"rd":"th";return a+c}}),nb?(module.exports=bb,ab(!0)):"function"==typeof define&&define.amd?define("moment",function(b,c,d){return d.config().noGlobal!==!0&&ab(d.config().noGlobal===a),bb}):ab()}).call(this); \ No newline at end of file diff --git a/src/main/webapp/js/validateForm.js b/src/main/webapp/js/validateForm.js index fd16c95c820452cc4bf63924623680592399ffa5..a8d4a64e70b860b71227272f92a5212d2b6766fe 100644 --- a/src/main/webapp/js/validateForm.js +++ b/src/main/webapp/js/validateForm.js @@ -9,10 +9,23 @@ var formFieldTypes = { TYPE_STRING: "STRING", TYPE_INTEGER: "INTEGER", TYPE_DOUBLE: "DOUBLE", + TYPE_DATE: "DATE", TYPE_PASSWORD: "PASSWORD", TYPE_EMAIL: "EMAIL", TYPE_SELECT_SINGLE: "SELECT_SINGLE", TYPE_SELECT_MULTIPLE: "SELECT_MULTIPLE" + +}; + + +var defaultValues = { + DEFAULT_DATE_FORMAT: "yyyy-MM-dd" +}; + +// Primary field relates to secondary field as TYPE +var relationTypes = { + RELATION_TYPE_EQUALS: "EQUALS", + RELATION_TYPE_AFTER: "AFTER" }; // Storage for all form definitions for current page @@ -35,6 +48,7 @@ var emailReg = /^([\w-\.]+@([\w-]+\.)+[\w-]{2,4})?$/; * <li>The form must have an id that corresponds to a JSON file accessible in "/formdefinitions/[form_id].json"</li> * <li>A DOM element with id "errorMsgEl" for where to put validation error messages</li> * <li>jQuery for Ajax calls</li> + * <li><a href="http://momentjs.com/">moment.js</a> for date validation</li> * </ul> * * This function prepares for the validateFormActual function. @@ -73,23 +87,41 @@ function validateFormActual(theForm) } } - // Check repeats - for(var i in formDefinition.repeatFields) + // Check relations + for(var i in formDefinition.relations) { - var item = formDefinition.repeatFields[i]; + var item = formDefinition.relations[i]; var primaryFieldWebValue = theForm[item.primaryField].value; - var repeatFieldWebValue = theForm[item.repeatField].value; - if(primaryFieldWebValue === null || repeatFieldWebValue === null) + var secondaryFieldWebValue = theForm[item.secondaryField].value; + if(primaryFieldWebValue === null || secondaryFieldWebValue === null) { continue; } - if(primaryFieldWebValue !== repeatFieldWebValue) + + // String equality NOW! + console.log("relationTYpe=" + item.relationType); + if(item.relationType === relationTypes.RELATION_TYPE_EQUALS) { - isValid = false; - invalidizeField(theForm[item.repeatField], theForm, getI18nMsg("xIsNotEqualToY",[ - getI18nMsg(item.repeatField), - getI18nMsg(item.primaryField).toLowerCase() - ])); + if(primaryFieldWebValue !== secondaryFieldWebValue) + { + isValid = false; + invalidizeField(theForm[item.primaryField], theForm, getI18nMsg("xIsNotEqualToY",[ + getI18nMsg(item.primaryField), + getI18nMsg(item.secondaryField).toLowerCase() + ])); + } + } + // Primary field is expected to be a date AFTER secondary key + else if(item.relationType === relationTypes.RELATION_TYPE_AFTER) + { + if(!moment(primaryFieldWebValue).isAfter(moment(secondaryFieldWebValue))) + { + isValid = false; + invalidizeField(theForm[item.primaryField], theForm, getI18nMsg("xIsNotAfterY",[ + getI18nMsg(item.primaryField), + getI18nMsg(item.secondaryField).toLowerCase() + ])); + } } //console.log(item.primaryField); } @@ -193,6 +225,22 @@ function validateFieldActual(fieldEl, theForm) return true; } } + + // Dates: check date format + if(fieldDefinition.type === formFieldTypes.TYPE_DATE) + { + var validFormat = (fieldDefinition.dateFormat !== null && fieldDefinition.dateFormat !== undefined) ? fieldDefinition.dateFormat : defaultValues.DEFAULT_DATE_FORMAT; + if(!moment(webValue, validFormat.toUpperCase(),true).isValid()) + { + invalidizeField(fieldEl, theForm, getI18nMsg("doesNotMatchDateFormat",[validFormat])); + return false; + } + else + { + validizeField(fieldEl, theForm); + return true; + } + } // Check email format // We use a simple regExp for this @@ -368,6 +416,11 @@ function evaluatePassword(passwordEl) */ function invalidizeField(inputEl, theForm, message) { + // Make sure we don't try to manipulate hidden fields + if(inputEl.type === "hidden") + { + return; + } styleInvalid(theForm[inputEl.name]); getValidationOutputEl(inputEl, theForm).innerHTML = message; } @@ -380,6 +433,11 @@ function invalidizeField(inputEl, theForm, message) */ function validizeField(inputEl, theForm) { + // Make sure we don't try to manipulate hidden fields + if(inputEl.type === "hidden") + { + return; + } styleValid(theForm[inputEl.name]); getValidationOutputEl(inputEl, theForm).innerHTML = ""; } diff --git a/src/main/webapp/templates/forecastConfigurationForm.ftl b/src/main/webapp/templates/forecastConfigurationForm.ftl new file mode 100644 index 0000000000000000000000000000000000000000..81c0d8ea63cb19d45a508d679b4c3bb8be2ac6a8 --- /dev/null +++ b/src/main/webapp/templates/forecastConfigurationForm.ftl @@ -0,0 +1,91 @@ +<#include "master.ftl"> +<#macro page_head> + <title></title> +</#macro> +<#macro custom_js> + <script src="/js/resourcebundle.js"></script> + <script src="/js/validateForm.js"></script> + <script src="http://code.jquery.com/ui/1.10.3/jquery-ui.min.js"></script> + <link href="http://code.jquery.com/ui/1.10.3/themes/redmond/jquery-ui.css" rel="stylesheet" /> + <script type="text/javascript" src="/js/modernizr_custom.js"></script> + <script type="text/javascript" src="/js/moment.min.js"></script> + <script type="text/javascript"> + // Make sure that there is a date picker present for HTML5 + // date input fields + if (!Modernizr.inputtypes.date) { + $('input[type=date]').datepicker({ dateFormat: 'yy-mm-dd' }); + } + </script> +</#macro> +<#macro page_contents> + <h1>${i18nBundle.viewForecastConfiguration}</h1> + <div id="errorMsgEl" class="alert alert-danger" <#if !formValidation?has_content> style="display:none;"</#if>> + <#if formValidation?has_content>${formValidation.validationMessages?replace("\n", "<br>")}</#if> + </div> + <#if messageKey?has_content> + <div class="alert alert-success">${i18nBundle(messageKey)}</div> + </#if> + <#assign formId = "forecastConfigurationForm"> + <form id="${formId}" role="form" action="/forecastConfiguration?action=forecastConfigurationFormSubmit" method="POST" onsubmit="try{return validateForm(this);}catch(err){alert(err);return false;}"> + <input type="hidden" name="forecastConfigurationId" value="${forecastConfiguration.forecastConfigurationId!"-1"}"/> + <div class="form-group"> + <label for="modelId">${i18nBundle.modelId}</label> + <select class="form-control" name="modelId" onblur="validateField(this);"> + <option value="-1">${i18nBundle.pleaseSelect} ${i18nBundle.modelId?lower_case}</option> + <#list modelInformations as modelInformation> + <option value="${modelInformation.modelId}"<#if forecastConfiguration.modelId?has_content && modelInformation.modelId == forecastConfiguration.modelId> selected="selected"</#if>> + <#if i18nBundle.containsKey(modelInformation.modelId)> + ${i18nBundle[modelInformation.modelId]} + <#else> + ${modelInformation.defaultName} + </#if> + </option> + </#list> + </select> + <span class="help-block" id="${formId}_modelId_validation"></span> + </div> + <div class="form-group"> + <label for="locationPointOfInterestId">${i18nBundle.locationPointOfInterestId}</label> + <select class="form-control" name="locationPointOfInterestId" onblur="validateField(this);"> + <option value="-1">${i18nBundle.pleaseSelect} ${i18nBundle.locationPointOfInterestId?lower_case}</option> + <#list locationPointOfInterests?sort_by("name") as poi> + <option value="${poi.pointOfInterestId}"<#if forecastConfiguration.locationPointOfInterestId?has_content && poi.pointOfInterestId == forecastConfiguration.locationPointOfInterestId.pointOfInterestId> selected="selected"</#if>>${poi.name}</option> + </#list> + </select> + <span class="help-block" id="${formId}_locationPointOfInterestId_validation"></span> + </div> + <div class="form-group"> + <label for="weatherStationPointOfInterestId">${i18nBundle.weatherStationPointOfInterestId}</label> + <select class="form-control" name="weatherStationPointOfInterestId" onblur="validateField(this);"> + <option value="-1">${i18nBundle.pleaseSelect} ${i18nBundle.weatherStationPointOfInterestId?lower_case}</option> + <#list weatherStationPointOfInterests?sort_by("name") as poi> + <option value="${poi.pointOfInterestId}"<#if forecastConfiguration.weatherStationPointOfInterestId?has_content && poi.pointOfInterestId == forecastConfiguration.weatherStationPointOfInterestId.pointOfInterestId> selected="selected"</#if>>${poi.name}</option> + </#list> + </select> + <span class="help-block" id="${formId}_weatherStationPointOfInterestId_validation"></span> + </div> + <div class="form-group"> + <label for="dateStart">${i18nBundle.dateStart}</label> + <input type="date" class="form-control" name="dateStart" placeholder="${i18nBundle.dateStart}" value="${(forecastConfiguration.dateStart?string(dateStart_dateFormat))!""}" onblur="validateField(this);" /> + <span class="help-block" id="${formId}_dateStart_validation"></span> + </div> + <div class="form-group"> + <label for="dateEnd">${i18nBundle.dateEnd}</label> + <input type="date" class="form-control" name="dateEnd" placeholder="${i18nBundle.dateEnd}" value="${(forecastConfiguration.dateEnd?string(dateEnd_dateFormat))!""}" onblur="validateField(this);" /> + <span class="help-block" id="${formId}_dateEnd_validation"></span> + </div> + <div class="form-group"> + <label for="vipsLogicUserId">${i18nBundle.vipsLogicUserId}</label> + <select class="form-control" name="vipsLogicUserId" onblur="validateField(this);"> + <option value="-1">${i18nBundle.pleaseSelect} ${i18nBundle.vipsLogicUserId?lower_case}</option> + <#list vipsLogicUsers?sort_by("lastName") as user> + <option value="${user.userId}"<#if forecastConfiguration.vipsLogicUserId?has_content && user.userId == forecastConfiguration.vipsLogicUserId.userId> selected="selected"</#if>>${user.lastName}, ${user.firstName}</option> + </#list> + </select> + <span class="help-block" id="${formId}_vipsLogicUserId_validation"></span> + </div> + <button type="submit" class="btn btn-default">${i18nBundle.submit}</button> + <button type="button" class="btn btn-danger" onclick="if(confirm('${i18nBundle.confirmDelete}')){alert('Not implemented');}">${i18nBundle.delete}</button> + </form> +</#macro> +<@page_html/> diff --git a/src/main/webapp/templates/forecastConfigurationList.ftl b/src/main/webapp/templates/forecastConfigurationList.ftl new file mode 100644 index 0000000000000000000000000000000000000000..09416bb8cd1a0f0ef1e647afe0ad487e0211f8a2 --- /dev/null +++ b/src/main/webapp/templates/forecastConfigurationList.ftl @@ -0,0 +1,38 @@ +<#include "master.ftl"> +<#macro page_head> + <title>${i18nBundle.forecasts}</title> +</#macro> +<#macro page_contents> + <h1>${i18nBundle.forecasts}</h1> + <#if messageKey?has_content> + <div class="alert alert-success">${i18nBundle(messageKey)}</div> + </#if> + <button type="button" class="btn btn-default" onclick="window.location.href='/forecastConfiguration?action=viewForecastConfiguration&forecastConfigurationId=-1'">${i18nBundle.addNew}</button> + <div class="table-responsive"> + <table class="table table-striped"> + <thead> + <th>${i18nBundle.modelId}</th> + <th>${i18nBundle.poi}</th> + <th>${i18nBundle.dateStart}</th> + <th>${i18nBundle.dateEnd}</th> + </thead> + <tbody> + <#list forecastConfigurations?sort_by("dateStart")?reverse as forecastConfiguration> + <tr style="cursor: pointer;" onclick="window.location.href='/forecastConfiguration?action=viewForecastConfiguration&forecastConfigurationId=${forecastConfiguration.forecastConfigurationId}';"> + <td> + <#if i18nBundle.containsKey(forecastConfiguration.modelId)> + ${i18nBundle[forecastConfiguration.modelId]} + <#else> + ${modelInformation[forecastConfiguration.modelId].defaultName} + </#if> + </td> + <td>${forecastConfiguration.locationPointOfInterestId.name}</td> + <td>${forecastConfiguration.dateStart}</td> + <td>${forecastConfiguration.dateEnd}</td> + </tr> + </#list> + </tbody> + </table> + </div> +</#macro> +<@page_html/> diff --git a/src/main/webapp/templates/master.ftl b/src/main/webapp/templates/master.ftl index f1b3c539ad41a6991803cf635b57b4979a3e6a57..349e8d8c38fcfc184ac3f3ce731d8f7165b24464 100644 --- a/src/main/webapp/templates/master.ftl +++ b/src/main/webapp/templates/master.ftl @@ -31,8 +31,8 @@ <li class="dropdown"> <a href="#" class="dropdown-toggle" data-toggle="dropdown">Admin<b class="caret"></b></a> <ul class="dropdown-menu"> - <li><a href="/user">Users</a></li> - <li><a href="#">Another action</a></li> + <li><a href="/user">${i18nBundle.users}</a></li> + <li><a href="/forecastConfiguration">${i18nBundle.forecasts}</a></li> <li><a href="#">Something else here</a></li> <li><a href="#">Separated link</a></li> <li><a href="#">One more separated link</a></li> diff --git a/src/main/webapp/templates/userForm.ftl b/src/main/webapp/templates/userForm.ftl index 05894d41a07adc63e0298cd51c3a56bdbd22c1eb..c8705e91955b903a773fc7b9a8afc022cf8c005b 100644 --- a/src/main/webapp/templates/userForm.ftl +++ b/src/main/webapp/templates/userForm.ftl @@ -15,7 +15,7 @@ <div class="alert alert-success">${i18nBundle(messageKey)}</div> </#if> <#assign formId = "userForm"> - <form id="${formId}" role="form" action="/user?action=userFormSubmit" method="POST" onsubmit="return validateForm(this);" accept-charset="UTF-8"> + <form id="${formId}" role="form" action="/user?action=userFormSubmit" method="POST" onsubmit="return validateForm(this);"> <input type="hidden" name="userId" value="${viewUser.userId}"/> <!-- Authentication info should be dealt with outside this form --> <div class="form-group"> diff --git a/src/main/webapp/test/updatemodelinfo.jsp b/src/main/webapp/test/updatemodelinfo.jsp new file mode 100644 index 0000000000000000000000000000000000000000..1835bd0e3ff65454f4026bbcbf62f63a1ba352c4 --- /dev/null +++ b/src/main/webapp/test/updatemodelinfo.jsp @@ -0,0 +1,23 @@ +<%-- + Document : updatemodelinfo + Created on : Dec 6, 2013, 5:11:53 PM + Author : treinar +--%> +<%@page import="no.bioforsk.vips.logic.controller.session.ForecastBean"%> +<%@page import="no.bioforsk.vips.logic.util.SessionControllerGetter"%> +<% + ForecastBean fb = SessionControllerGetter.getForecastBean(); + + fb.updateModelInformation(); +%> +<%@page contentType="text/html" pageEncoding="UTF-8"%> +<!DOCTYPE html> +<html> + <head> + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> + <title>Update model info</title> + </head> + <body> + <h1>This page calls the methods for updating model info. It also displays the updated model info(?)</h1> + </body> +</html>