From 70ad66a18e628a78b11ab040df74f0f84eaf5bc5 Mon Sep 17 00:00:00 2001 From: Tor-Einar Skog <tor-einar.skog@nibio.no> Date: Mon, 28 Feb 2022 08:25:14 +0000 Subject: [PATCH] Major merge to get develop up to speed before season starts --- .../vips/logic/VIPSLogicApplication.java | 6 +- .../servlet/ObservationController.java | 11 +- .../servlet/PointOfInterestController.java | 28 +-- .../controller/session/ObservationBean.java | 108 +++++++-- .../controller/session/OrganismBean.java | 12 + .../session/PointOfInterestBean.java | 2 +- .../logic/controller/session/UserBean.java | 17 +- .../vips/logic/entity/AppObservation.java | 72 ++++++ .../nibio/vips/logic/entity/Observation.java | 11 + .../logic/entity/ObservationSyncInfo.java | 53 +++++ .../no/nibio/vips/logic/entity/Organism.java | 11 + .../vips/logic/entity/PointOfInterest.java | 55 ++++- .../entity/PointOfInterestWeatherStation.java | 6 +- .../logic/service/AuthenticationService.java | 4 +- .../vips/logic/service/LogicService.java | 12 +- .../logic/service/ObservationService.java | 194 ++++++++++++++- .../nibio/vips/logic/service/POIService.java | 223 ++++++++++++++++++ .../observationdata/ObservationDataBean.java | 114 +++++++++ .../ObservationDataService.java | 18 +- .../util/weather/WeatherDataSourceUtil.java | 2 +- .../V10__POI_add_last_edited_date.sql | 7 + .../V9__Observation_add_last_edited_date.sql | 6 + .../vips/logic/i18n/vipslogictexts.properties | 24 +- .../logic/i18n/vipslogictexts_bs.properties | 25 +- .../logic/i18n/vipslogictexts_hr.properties | 24 +- .../logic/i18n/vipslogictexts_nb.properties | 23 +- .../logic/i18n/vipslogictexts_sr.properties | 24 +- .../i18n/vipslogictexts_zh_CN.properties | 25 +- src/main/webapp/templates/observationForm.ftl | 9 +- src/main/webapp/templates/poiForm.ftl | 2 +- src/main/webapp/templates/poiList.ftl | 2 +- 31 files changed, 994 insertions(+), 136 deletions(-) create mode 100644 src/main/java/no/nibio/vips/logic/entity/AppObservation.java create mode 100644 src/main/java/no/nibio/vips/logic/entity/ObservationSyncInfo.java create mode 100644 src/main/java/no/nibio/vips/logic/service/POIService.java create mode 100644 src/main/java/no/nibio/vips/observationdata/ObservationDataBean.java create mode 100644 src/main/resources/db/migration/V10__POI_add_last_edited_date.sql create mode 100644 src/main/resources/db/migration/V9__Observation_add_last_edited_date.sql diff --git a/src/main/java/no/nibio/vips/logic/VIPSLogicApplication.java b/src/main/java/no/nibio/vips/logic/VIPSLogicApplication.java index 59995d75..97a8feb4 100755 --- a/src/main/java/no/nibio/vips/logic/VIPSLogicApplication.java +++ b/src/main/java/no/nibio/vips/logic/VIPSLogicApplication.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014 NIBIO <http://www.nibio.no/>. + * Copyright (c) 2021 NIBIO <http://www.nibio.no/>. * * This file is part of VIPSLogic. * VIPSLogic is free software: you can redistribute it and/or modify @@ -25,7 +25,7 @@ import javax.ws.rs.core.Application; /** * Responsible for adding REST resources - * @copyright 2016 {@link http://www.nibio.no NIBIO} + * @copyright 2016-2021 {@link http://www.nibio.no NIBIO} * @author Tor-Einar Skog <tor-einar.skog@nibio.no> */ @ApplicationPath("/rest/") @@ -46,6 +46,7 @@ public class VIPSLogicApplication extends Application */ private void addRestResourceClassesManually(Set<Class<?>> resources) { resources.add(no.nibio.vips.logic.service.LogicService.class); + resources.add(no.nibio.vips.logic.service.POIService.class); resources.add(no.nibio.vips.logic.service.AuthenticationService.class); resources.add(no.nibio.vips.logic.service.VIPSMobileService.class); resources.add(no.nibio.vips.logic.modules.barleynetblotch.BarleyNetBlotchModelService.class); @@ -83,6 +84,7 @@ public class VIPSLogicApplication extends Application resources.add(no.nibio.vips.logic.service.LogicService.class); resources.add(no.nibio.vips.logic.service.ModelFormService.class); resources.add(no.nibio.vips.logic.service.ObservationService.class); + resources.add(no.nibio.vips.logic.service.POIService.class); resources.add(no.nibio.vips.logic.service.VIPSMobileService.class); resources.add(no.nibio.vips.observationdata.ObservationDataService.class); } diff --git a/src/main/java/no/nibio/vips/logic/controller/servlet/ObservationController.java b/src/main/java/no/nibio/vips/logic/controller/servlet/ObservationController.java index eb18dd3d..8cdcbb17 100755 --- a/src/main/java/no/nibio/vips/logic/controller/servlet/ObservationController.java +++ b/src/main/java/no/nibio/vips/logic/controller/servlet/ObservationController.java @@ -447,10 +447,7 @@ public class ObservationController extends HttpServlet { observation.setOrganism(em.find(Organism.class, formValidation.getFormField("organismId").getValueAsInteger())); observation.setCropOrganism(em.find(Organism.class, formValidation.getFormField("cropOrganismId").getValueAsInteger())); } - //observation.setDenominator(formValidation.getFormField("denominator").getValueAsInteger()); - //observation.setObservationMethodId(em.find(ObservationMethod.class, formValidation.getFormField("observationMethodId").getWebValue())); observation.setTimeOfObservation(formValidation.getFormField("timeOfObservation").getValueAsTimestamp()); - //observation.setObservedValue(formValidation.getFormField("observedValue").getValueAsDouble()); if(observationId <= 0) { observation.setUserId(user.getUserId()); @@ -459,7 +456,7 @@ public class ObservationController extends HttpServlet { { observation.setLastEditedBy(user.getUserId()); } - + observation.setLastEditedTime(new Date()); observation.setObservationHeading(formValidation.getFormField("observationHeading").getWebValue()); observation.setObservationText(formValidation.getFormField("observationText").getWebValue()); observation.setObservationData( @@ -515,10 +512,10 @@ public class ObservationController extends HttpServlet { // Image handling // Delete the current illustration - String deleteIllustration = formValidation.getFormField("deleteIllustration").getWebValue(); - if(deleteIllustration != null && deleteIllustration.equals("true")) + String[] deleteIllustrations = parameterMap.get("deleteIllustration"); + if(deleteIllustrations != null && deleteIllustrations.length > 0) { - observation = observationBean.deleteObservationIllustration(observation); + observation = observationBean.deleteObservationIllustration(observation, deleteIllustrations); } // Store the new illustration (replaces former illustration if not already deleted) diff --git a/src/main/java/no/nibio/vips/logic/controller/servlet/PointOfInterestController.java b/src/main/java/no/nibio/vips/logic/controller/servlet/PointOfInterestController.java index 97ce9d8d..962930a1 100755 --- a/src/main/java/no/nibio/vips/logic/controller/servlet/PointOfInterestController.java +++ b/src/main/java/no/nibio/vips/logic/controller/servlet/PointOfInterestController.java @@ -192,7 +192,7 @@ public class PointOfInterestController extends HttpServlet { request.setAttribute("defaultMapCenter",user.getOrganizationId().getDefaultMapCenter()); request.setAttribute("defaultMapZoom", user.getOrganizationId().getDefaultMapZoom()); request.setAttribute("messageKey", request.getParameter("messageKey")); - request.setAttribute("returnURL","weatherStation?organizationId=" + weatherStation.getUserId().getOrganizationId().getOrganizationId()); + request.setAttribute("returnURL","weatherStation?organizationId=" + weatherStation.getUser().getOrganizationId().getOrganizationId()); request.getRequestDispatcher("/weatherstationView.ftl").forward(request, response); } @@ -263,7 +263,7 @@ public class PointOfInterestController extends HttpServlet { Collections.sort(users); request.getSession().setAttribute("users", users); } - request.setAttribute("returnURL","weatherStation?organizationId=" + weatherStation.getUserId().getOrganizationId().getOrganizationId()); + request.setAttribute("returnURL","weatherStation?organizationId=" + weatherStation.getUser().getOrganizationId().getOrganizationId()); request.getRequestDispatcher("/weatherstationForm.ftl").forward(request, response); } catch(NullPointerException | NumberFormatException ex) @@ -331,12 +331,12 @@ public class PointOfInterestController extends HttpServlet { if(user.isSuperUser() && !formValidation.getFormField("userId").isEmpty()) { - weatherStation.setUserId(em.find(VipsLogicUser.class, formValidation.getFormField("userId").getValueAsInteger())); + weatherStation.setUser(em.find(VipsLogicUser.class, formValidation.getFormField("userId").getValueAsInteger())); } // If user is not set, use current user - else if(weatherStation.getUserId() == null) + else if(weatherStation.getUser() == null) { - weatherStation.setUserId(user); + weatherStation.setUser(user); } // Store weatherStation = pointOfInterestBean.storeWeatherStation(weatherStation); @@ -530,7 +530,7 @@ public class PointOfInterestController extends HttpServlet { request.setAttribute("defaultMapCenter",user.getOrganizationId().getDefaultMapCenter()); request.setAttribute("defaultMapZoom", user.getOrganizationId().getDefaultMapZoom()); request.setAttribute("messageKey", request.getParameter("messageKey")); - request.setAttribute("returnURL","poi?organizationId=" + poi.getUserId().getOrganizationId().getOrganizationId()); + request.setAttribute("returnURL","poi?organizationId=" + poi.getUser().getOrganizationId().getOrganizationId()); request.getRequestDispatcher("/poiView.ftl").forward(request, response); } @@ -585,7 +585,7 @@ public class PointOfInterestController extends HttpServlet { PointOfInterest poi = em.find(PointOfInterest.class, pointOfInterestId); // Does the current user have the rights to edit this poi? if(userBean.authorizeUser(user, VipsLogicRole.ORGANIZATION_ADMINISTRATOR, VipsLogicRole.SUPERUSER) - || Objects.equals(user.getUserId(), poi.getUserId().getUserId()) + || Objects.equals(user.getUserId(), poi.getUser().getUserId()) ) { request.getSession().setAttribute("poi", poi); @@ -613,7 +613,7 @@ public class PointOfInterestController extends HttpServlet { .setParameter("organizationId", user.getOrganizationId()).getResultList() ); } - request.setAttribute("returnURL","poi?organizationId=" + poi.getUserId().getOrganizationId().getOrganizationId()); + request.setAttribute("returnURL","poi?organizationId=" + poi.getUser().getOrganizationId().getOrganizationId()); request.getRequestDispatcher("/poiForm.ftl").forward(request, response); } else @@ -638,7 +638,7 @@ public class PointOfInterestController extends HttpServlet { : PointOfInterestFactory.getPointOfInterest(formValidation.getFormField("pointOfInterestTypeId").getValueAsInteger()); if(userBean.authorizeUser(user, VipsLogicRole.ORGANIZATION_ADMINISTRATOR, VipsLogicRole.SUPERUSER) || poi.getPointOfInterestId() == null - || Objects.equals(user.getUserId(), poi.getUserId().getUserId()) + || Objects.equals(user.getUserId(), poi.getUser().getUserId()) ) { Boolean poiNameAlreadyExists = pointOfInterestBean.getPointOfInterest(formValidation.getFormField("name").getWebValue()) != null; @@ -687,12 +687,12 @@ public class PointOfInterestController extends HttpServlet { if((user.isSuperUser() || user.isOrganizationAdmin()) && !formValidation.getFormField("userId").isEmpty()) { - poi.setUserId(em.find(VipsLogicUser.class, formValidation.getFormField("userId").getValueAsInteger())); + poi.setUser(em.find(VipsLogicUser.class, formValidation.getFormField("userId").getValueAsInteger())); } // If user is not set, use current user - else if(poi.getUserId() == null) + else if(poi.getUser() == null) { - poi.setUserId(user); + poi.setUser(user); } // Store poi = pointOfInterestBean.storePoi(poi); @@ -783,7 +783,7 @@ public class PointOfInterestController extends HttpServlet { PointOfInterest poi = em.find(PointOfInterest.class, pointOfInterestId); if( userBean.authorizeUser(user, VipsLogicRole.ORGANIZATION_ADMINISTRATOR, VipsLogicRole.SUPERUSER) - || user.getUserId().equals(poi.getUserId().getUserId()) + || user.getUserId().equals(poi.getUser().getUserId()) ) { try @@ -832,7 +832,7 @@ public class PointOfInterestController extends HttpServlet { PointOfInterest poi = em.find(PointOfInterest.class, pointOfInterestId); if( userBean.authorizeUser(user, VipsLogicRole.ORGANIZATION_ADMINISTRATOR, VipsLogicRole.SUPERUSER) - || user.getUserId().equals(poi.getUserId().getUserId()) + || user.getUserId().equals(poi.getUser().getUserId()) ) { try diff --git a/src/main/java/no/nibio/vips/logic/controller/session/ObservationBean.java b/src/main/java/no/nibio/vips/logic/controller/session/ObservationBean.java index 412b2332..ddcc2fe2 100755 --- a/src/main/java/no/nibio/vips/logic/controller/session/ObservationBean.java +++ b/src/main/java/no/nibio/vips/logic/controller/session/ObservationBean.java @@ -26,6 +26,9 @@ import com.ibm.icu.util.ULocale; import java.io.File; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; @@ -60,6 +63,7 @@ import no.nibio.vips.logic.entity.PolygonService; import no.nibio.vips.logic.entity.VipsLogicUser; import no.nibio.vips.logic.i18n.SessionLocaleUtil; import no.nibio.vips.logic.util.SystemTime; +import org.apache.commons.codec.binary.Base64; import no.nibio.vips.observationdata.ObservationDataSchema; import no.nibio.vips.observationdata.ObservationDataSchemaPK; import org.apache.commons.fileupload.FileItem; @@ -305,11 +309,21 @@ public class ObservationBean { public void deleteObservation(Integer observationId) { Observation observation = em.find(Observation.class, observationId); - // Delete all current group memberships - em.createNativeQuery("DELETE FROM public.organization_group_observation WHERE observation_id=:observationId") - .setParameter("observationId", observation.getObservationId()) - .executeUpdate(); - em.remove(observation); + if(observation != null) + { + // Delete all current group memberships + em.createNativeQuery("DELETE FROM public.organization_group_observation WHERE observation_id=:observationId") + .setParameter("observationId", observation.getObservationId()) + .executeUpdate(); + + // Delete all illustrations (including removing files on disk) + String[] filesToDelete = observation.getObservationIllustrationSet().stream() + .map(ill->ill.getObservationIllustrationPK().getFileName()) + .collect(Collectors.toList()) + .toArray(new String[0]); + this.deleteObservationIllustration(observation, filesToDelete); + em.remove(observation); + } } /** @@ -379,18 +393,46 @@ public class ObservationBean { /** * - * @param message - * @return [OBSERVATION_ILLUSTRATION_PATH]/[ORGANIZATION_ID]/ + * @param observation + * @return [OBSERVATION_ILLUSTRATION_PATH]/[ORGANISM_ID]/ */ private String getFilePath(Observation observation) { return System.getProperty("no.nibio.vips.logic.OBSERVATION_ILLUSTRATION_PATH") + "/" + observation.getOrganismId(); } + + public Observation storeObservationIllustration(Observation observation, String fileName, String base64Data) + { + String[] metaAndData = base64Data.split(","); + byte[] imageData = Base64.decodeBase64(metaAndData[1]); + java.nio.file.Path path = Paths.get(this.getFilePath(observation) + "/" + fileName); + try { + // Make sure the directory exists + File testDirectoryfile = new File(this.getFilePath(observation)); + if(!testDirectoryfile.exists()) + { + testDirectoryfile.mkdirs(); + } + Files.write(path,imageData, StandardOpenOption.CREATE); + ObservationIllustration newIllustration = new ObservationIllustration(new ObservationIllustrationPK(observation.getObservationId(), fileName)); + newIllustration = em.merge(newIllustration); + + // Add the new illustration + if(observation.getObservationIllustrationSet() == null) + { + observation.setObservationIllustrationSet(new HashSet<ObservationIllustration>()); + } + observation.getObservationIllustrationSet().add(newIllustration); + + return observation; + } + catch(IOException ex) {ex.printStackTrace(); return observation;} + } public Observation storeObservationIllustration(Observation observation, FileItem item) throws Exception { // Create a candidate filename - // [MESSAGE_ILLUSTRATION_PATH]/[ORGANIZATION_ID]/[MESSAGE_ID]_illustration.[fileExtension] + // [OBSERVATION_ILLUSTRATION_PATH]/[ORGANIZATION_ID]/[OBSERVATION_ID]_illustration.[fileExtension] String filePath = this.getFilePath(observation); String fileName = observation.getObservationId() + "_illustration." + FilenameUtils.getExtension(item.getName()); // Check availability, and adapt filename until available @@ -415,18 +457,7 @@ public class ObservationBean { // Update MessageIllustrations observation = em.merge(observation); // Remove the old illustration(s) - List <ObservationIllustration> formerIllustrations = em.createNamedQuery("ObservationIllustration.findByObservationId").setParameter("observationId", observation.getObservationId()).getResultList(); - for(ObservationIllustration formerIllustration:formerIllustrations) - { - System.out.println("removing " + formerIllustration.toString()); - em.remove(formerIllustration); - } - // Also remove their relation to message. - if(observation.getObservationIllustrationSet() != null) - { - observation.getObservationIllustrationSet().clear(); - } - + ObservationIllustration newIllustration = new ObservationIllustration(new ObservationIllustrationPK(observation.getObservationId(), fileName)); em.persist(newIllustration); @@ -436,19 +467,41 @@ public class ObservationBean { observation.setObservationIllustrationSet(new HashSet<ObservationIllustration>()); } observation.getObservationIllustrationSet().add(newIllustration); - //message.getMessageIllustrationSet().add(newIllustration); return observation; } - public Observation deleteObservationIllustration(Observation observation) { + public Observation deleteObservationIllustration(Observation observation, String[] deleteIllustrations) { observation = em.merge(observation); Set <ObservationIllustration> formerIllustrations = observation.getObservationIllustrationSet(); - for(ObservationIllustration formerIllustration:formerIllustrations) + if(formerIllustrations == null) { - em.remove(formerIllustration); + return observation; + } + + Set <ObservationIllustration> deleteThese = new HashSet<>(); + + for(String deleteIllustration:deleteIllustrations) + { + for(ObservationIllustration formerIllustration:formerIllustrations) + { + if(formerIllustration.getObservationIllustrationPK().getFileName() + .equals(deleteIllustration)) + { + deleteThese.add(formerIllustration); + } + } } - observation.getObservationIllustrationSet().clear(); + + for(ObservationIllustration ill: deleteThese) + { + observation.getObservationIllustrationSet().remove(ill); + em.remove(ill); + // Physically remove it too + File fileToDelete = new File(this.getFilePath(observation) + "/" + ill.getObservationIllustrationPK().getFileName()); + fileToDelete.delete(); + } + return observation; } @@ -756,6 +809,11 @@ public class ObservationBean { } + public PolygonService getPolygonService(Integer polygonServiceId) + { + return em.find(PolygonService.class, polygonServiceId); + } + public List<PolygonService> getPolygonServicesForOrganization(Integer organizationId) { return em.createNativeQuery("SELECT * FROM polygon_service p WHERE p.polygon_service_id IN (SELECT polygon_service_id FROM public.organization_polygon_service WHERE organization_id=:organizationId)", PolygonService.class) .setParameter("organizationId", organizationId) diff --git a/src/main/java/no/nibio/vips/logic/controller/session/OrganismBean.java b/src/main/java/no/nibio/vips/logic/controller/session/OrganismBean.java index e6707013..88676131 100755 --- a/src/main/java/no/nibio/vips/logic/controller/session/OrganismBean.java +++ b/src/main/java/no/nibio/vips/logic/controller/session/OrganismBean.java @@ -19,6 +19,8 @@ package no.nibio.vips.logic.controller.session; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import com.ibm.icu.util.ULocale; import java.time.Instant; @@ -371,6 +373,7 @@ public class OrganismBean { .getResultList(); } + /** * Searching recursively upwards in organism tree to find a cropPest. * @param cropOrganismId id of the crop @@ -444,6 +447,15 @@ public class OrganismBean { return organism; } + /** + * + * @param organismId + * @return + */ + public Organism getOrganism(Integer organismId) { + return em.find(Organism.class, organismId); + } + public List<Organism> sortOrganismsByLocalName(List<Organism> organisms, final String locale) { Collections.sort(organisms, new Comparator<Organism>() { diff --git a/src/main/java/no/nibio/vips/logic/controller/session/PointOfInterestBean.java b/src/main/java/no/nibio/vips/logic/controller/session/PointOfInterestBean.java index 0e029a33..6f624f3d 100755 --- a/src/main/java/no/nibio/vips/logic/controller/session/PointOfInterestBean.java +++ b/src/main/java/no/nibio/vips/logic/controller/session/PointOfInterestBean.java @@ -421,7 +421,7 @@ public class PointOfInterestBean { // double catching of privately owned weather station retVal.addAll(this.getWeatherstationsForOrganization(user.getOrganizationId(), Boolean.TRUE) .stream() - .filter(weatherStation -> ! weatherStation.getUserId().getUserId().equals(user.getUserId())) + .filter(weatherStation -> ! weatherStation.getUser().getUserId().equals(user.getUserId())) .collect(Collectors.toList()) ); diff --git a/src/main/java/no/nibio/vips/logic/controller/session/UserBean.java b/src/main/java/no/nibio/vips/logic/controller/session/UserBean.java index cb6f26ed..cf84f1c2 100755 --- a/src/main/java/no/nibio/vips/logic/controller/session/UserBean.java +++ b/src/main/java/no/nibio/vips/logic/controller/session/UserBean.java @@ -49,8 +49,11 @@ import javax.persistence.NoResultException; import javax.persistence.NonUniqueResultException; import javax.persistence.PersistenceContext; import javax.persistence.Query; +import javax.servlet.http.HttpServletRequest; import javax.validation.ConstraintViolation; import javax.validation.ConstraintViolationException; +import javax.ws.rs.core.HttpHeaders; + import no.nibio.vips.logic.authenticate.PasswordValidationException; import no.nibio.vips.logic.entity.Country; import no.nibio.vips.logic.entity.ForecastConfiguration; @@ -278,7 +281,7 @@ public class UserBean { public void transferUserResources(VipsLogicUser fromUser, VipsLogicUser toUser) { UserResources userResources = this.getUserResources(fromUser); (userResources.getPois() != null ? userResources.getPois() : Collections.<PointOfInterest>emptyList()).forEach((ws) -> { - ws.setUserId(toUser); + ws.setUser(toUser); }); (userResources.getMessageLocales() != null ? userResources.getMessageLocales() : Collections.<MessageLocale>emptyList()).forEach((ml) -> { ml.setCreatedBy(toUser); @@ -973,4 +976,16 @@ public class UserBean { .setParameter("vipsLogicRoleIds", Arrays.asList(vipsLogicRoleIds)) .getResultList(); } + + public VipsLogicUser getUserFromUUID(HttpServletRequest request) + { + String uuidStr = request.getHeader(HttpHeaders.AUTHORIZATION); + if(uuidStr == null) + { + return null; + } + UUID uuid = UUID.fromString(uuidStr); + VipsLogicUser user = SessionControllerGetter.getUserBean().findVipsLogicUser(uuid); + return user; + } } diff --git a/src/main/java/no/nibio/vips/logic/entity/AppObservation.java b/src/main/java/no/nibio/vips/logic/entity/AppObservation.java new file mode 100644 index 00000000..bfb76f2b --- /dev/null +++ b/src/main/java/no/nibio/vips/logic/entity/AppObservation.java @@ -0,0 +1,72 @@ +package no.nibio.vips.logic.entity; + +import java.util.Date; + +public class AppObservation implements no.nibio.vips.observation.Observation{ + private Date timeOfObservation; + private String geoinfo, observationData, name; + + + + /** + * @return the timeOfObservation + */ + public Date getTimeOfObservation() { + return timeOfObservation; + } + + /** + * @param timeOfObservation the timeOfObservation to set + */ + public void setTimeOfObservation(Date timeOfObservation) { + this.timeOfObservation = timeOfObservation; + } + + /** + * @return the geoinfo + */ + public String getGeoinfo() { + return geoinfo; + } + + /** + * @param geoinfo the geoinfo to set + */ + public void setGeoinfo(String geoinfo) { + this.geoinfo = geoinfo; + } + + /** + * @return the observationData + */ + public String getObservationData() { + return observationData; + } + + /** + * @param observationData the observationData to set + */ + public void setObservationData(String observationData) { + this.observationData = observationData; + } + + /** + * @return the name + */ + public String getName() { + return name; + } + + /** + * @param name the name to set + */ + public void setName(String name) { + this.name = name; + } + + @Override + public int compareTo(Object t) { + throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + } + +} diff --git a/src/main/java/no/nibio/vips/logic/entity/Observation.java b/src/main/java/no/nibio/vips/logic/entity/Observation.java index d10dc810..d34522ac 100755 --- a/src/main/java/no/nibio/vips/logic/entity/Observation.java +++ b/src/main/java/no/nibio/vips/logic/entity/Observation.java @@ -101,6 +101,7 @@ public class Observation implements Serializable, no.nibio.vips.observation.Obse private Integer statusTypeId; private Integer statusChangedByUserId; private Date statusChangedTime; + private Date lastEditedTime; private String statusRemarks; private String observationData; private Boolean isQuantified; @@ -654,5 +655,15 @@ public class Observation implements Serializable, no.nibio.vips.observation.Obse observationDataSchema ); } + + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "last_edited_time") + public Date getLastEditedTime() { + return lastEditedTime; + } + + public void setLastEditedTime(Date lastEditedTime) { + this.lastEditedTime = lastEditedTime; + } } diff --git a/src/main/java/no/nibio/vips/logic/entity/ObservationSyncInfo.java b/src/main/java/no/nibio/vips/logic/entity/ObservationSyncInfo.java new file mode 100644 index 00000000..5702cfe1 --- /dev/null +++ b/src/main/java/no/nibio/vips/logic/entity/ObservationSyncInfo.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2021 NIBIO <http://www.nibio.no/>. + * + * This file is part of VIPSLogic. + * VIPSLogic is free software: you can redistribute it and/or modify + * it under the terms of the NIBIO Open Source License as published by + * NIBIO, either version 1 of the License, or (at your option) any + * later version. + * + * VIPSLogic 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 + * NIBIO Open Source License for more details. + * + * You should have received a copy of the NIBIO Open Source License + * along with VIPSLogic. If not, see <http://www.nibio.no/licenses/>. + * + */ + +package no.nibio.vips.logic.entity; + +import java.util.Date; + +/** + * A minimized object for serialization when only sync related + * information is needed + * + * @author Tor-Einar Skog <tor-einar.skog@nibio.no> + * + */ +public class ObservationSyncInfo { + private Integer observationId; + private Date lastEditedTime; + + public ObservationSyncInfo(Observation obs) + { + this.observationId = obs.getObservationId(); + this.lastEditedTime = obs.getLastEditedTime(); + } + + public Integer getObservationId() { + return observationId; + } + public void setObservationId(Integer observationId) { + this.observationId = observationId; + } + public Date getLastEditedTime() { + return lastEditedTime; + } + public void setLastEditedTime(Date lastEditedTime) { + this.lastEditedTime = lastEditedTime; + } +} diff --git a/src/main/java/no/nibio/vips/logic/entity/Organism.java b/src/main/java/no/nibio/vips/logic/entity/Organism.java index 3e2ba63f..612cfaec 100755 --- a/src/main/java/no/nibio/vips/logic/entity/Organism.java +++ b/src/main/java/no/nibio/vips/logic/entity/Organism.java @@ -98,6 +98,9 @@ public class Organism implements Serializable { @Transient private Map<String,Object> extraProperties; + @Transient + private String observationDataSchema; + /*@OneToMany(cascade = CascadeType.ALL, mappedBy = "organism") private Set<OrganismLocale> organismLocaleSet; @OneToMany(cascade = CascadeType.ALL, mappedBy = "organism") @@ -346,4 +349,12 @@ public class Organism implements Serializable { public void setExtraProperties(Map<String,Object> extraProperties) { this.extraProperties = extraProperties; } + + public String getObservationDataSchema() { + return observationDataSchema; + } + + public void setObservationDataSchema(String observationDataSchema) { + this.observationDataSchema = observationDataSchema; + } } diff --git a/src/main/java/no/nibio/vips/logic/entity/PointOfInterest.java b/src/main/java/no/nibio/vips/logic/entity/PointOfInterest.java index f8325ed7..db7fc933 100755 --- a/src/main/java/no/nibio/vips/logic/entity/PointOfInterest.java +++ b/src/main/java/no/nibio/vips/logic/entity/PointOfInterest.java @@ -19,6 +19,7 @@ package no.nibio.vips.logic.entity; import java.io.Serializable; +import java.util.Date; import java.util.HashMap; import java.util.Map; import javax.persistence.Basic; @@ -62,15 +63,11 @@ import no.nibio.vips.gis.GISUtil; @NamedQuery(name = "PointOfInterest.findAll", query = "SELECT p FROM PointOfInterest p ORDER BY p.name ASC"), @NamedQuery(name = "PointOfInterest.findByPointOfInterestId", query = "SELECT p FROM PointOfInterest p WHERE p.pointOfInterestId = :pointOfInterestId"), @NamedQuery(name = "PointOfInterest.findByPointOfInterestIds", query = "SELECT p FROM PointOfInterest p WHERE p.pointOfInterestId IN :pointOfInterestIds"), - //@NamedQuery(name = "PointOfInterest.findByPointOfInterestTypeId", query = "SELECT p FROM PointOfInterest p WHERE p.pointOfInterestType = :pointOfInterestType"), - @NamedQuery(name = "PointOfInterest.findByOrganizationId", query = "SELECT p FROM PointOfInterest p WHERE p.userId IN(SELECT u.userId FROM VipsLogicUser u WHERE u.organizationId=:organizationId OR u.organizationId IN (SELECT o.organizationId FROM Organization o WHERE o.parentOrganizationId = :organizationId)) ORDER BY p.name ASC"), - @NamedQuery(name = "PointOfInterest.findForecastLocationsByOrganizationId", query = "SELECT p FROM PointOfInterest p WHERE p.isForecastLocation IS TRUE AND p.userId IN(SELECT u.userId FROM VipsLogicUser u WHERE u.organizationId=:organizationId OR u.organizationId IN (SELECT o.organizationId FROM Organization o WHERE o.parentOrganizationId = :organizationId)) ORDER BY p.name ASC"), + @NamedQuery(name = "PointOfInterest.findByOrganizationId", query = "SELECT p FROM PointOfInterest p WHERE p.user IN(SELECT u.userId FROM VipsLogicUser u WHERE u.organizationId=:organizationId OR u.organizationId IN (SELECT o.organizationId FROM Organization o WHERE o.parentOrganizationId = :organizationId)) ORDER BY p.name ASC"), + @NamedQuery(name = "PointOfInterest.findForecastLocationsByOrganizationId", query = "SELECT p FROM PointOfInterest p WHERE p.isForecastLocation IS TRUE AND p.user IN(SELECT u.userId FROM VipsLogicUser u WHERE u.organizationId=:organizationId OR u.organizationId IN (SELECT o.organizationId FROM Organization o WHERE o.parentOrganizationId = :organizationId)) ORDER BY p.name ASC"), @NamedQuery(name = "PointOfInterest.findByName", query = "SELECT p FROM PointOfInterest p WHERE p.name = :name"), @NamedQuery(name = "PointOfInterest.findByNameCaseInsensitive", query = "SELECT p FROM PointOfInterest p WHERE lower(p.name) = lower(:name)"), - //@NamedQuery(name = "PointOfInterest.findByLongitude", query = "SELECT p FROM PointOfInterest p WHERE p.longitude = :longitude"), - //@NamedQuery(name = "PointOfInterest.findByLatitude", query = "SELECT p FROM PointOfInterest p WHERE p.latitude = :latitude"), - //@NamedQuery(name = "PointOfInterest.findByAltitude", query = "SELECT p FROM PointOfInterest p WHERE p.altitude = :altitude"), - @NamedQuery(name = "PointOfInterest.findByUserId", query = "SELECT p FROM PointOfInterest p WHERE p.userId = :userId ORDER BY p.name ASC") + @NamedQuery(name = "PointOfInterest.findByUserId", query = "SELECT p FROM PointOfInterest p WHERE p.user = :userId ORDER BY p.name ASC") }) public class PointOfInterest implements Serializable, Comparable { private Set<PointOfInterestExternalResource> pointOfInterestExternalResourceSet; @@ -92,6 +89,7 @@ public class PointOfInterest implements Serializable, Comparable { private WeatherForecastProvider weatherForecastProviderId; private Boolean isForecastLocation; private Integer pointOfInterestTypeId; + private Date lastEditedTime; // For attaching ad-hoc properties @@ -107,6 +105,26 @@ public class PointOfInterest implements Serializable, Comparable { public PointOfInterest(Integer pointOfInterestId) { this.pointOfInterestId = pointOfInterestId; } + + public static PointOfInterest getInstance(Integer typeId) + { + PointOfInterest instance; + + switch(typeId) + { + case 1: instance = new PointOfInterestWeatherStation(); + break; + case 2: instance = new PointOfInterestTypeFarm(); + break; + case 3: instance = new PointOfInterestTypeField(); + break; + case 4: instance = new PointOfInterestTypeRegion(); + break; + default: instance = null; + break; + } + return instance; + } @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -263,16 +281,26 @@ public class PointOfInterest implements Serializable, Comparable { @JoinColumn(name = "user_id", referencedColumnName = "user_id") @ManyToOne @JsonIgnore - public VipsLogicUser getUserId() { + public VipsLogicUser getUser() { return userId; } /** * @param userId the userId to set */ - public void setUserId(VipsLogicUser userId) { + public void setUser(VipsLogicUser userId) { this.userId = userId; } + + /** + * For serialization + * @return + */ + @Transient + public Integer getUserId() + { + return this.getUser().getUserId(); + } /** * @return the weatherForecastProviderId @@ -356,4 +384,13 @@ public class PointOfInterest implements Serializable, Comparable { public void setIsForecastLocation(Boolean isForecastLocation) { this.isForecastLocation = isForecastLocation; } + + @Column(name = "last_edited_time") + public Date getLastEditedTime() { + return lastEditedTime; + } + + public void setLastEditedTime(Date lastEditedTime) { + this.lastEditedTime = lastEditedTime; + } } diff --git a/src/main/java/no/nibio/vips/logic/entity/PointOfInterestWeatherStation.java b/src/main/java/no/nibio/vips/logic/entity/PointOfInterestWeatherStation.java index 81827edc..85804f24 100755 --- a/src/main/java/no/nibio/vips/logic/entity/PointOfInterestWeatherStation.java +++ b/src/main/java/no/nibio/vips/logic/entity/PointOfInterestWeatherStation.java @@ -47,9 +47,9 @@ import javax.persistence.Transient; @NamedQuery(name = "PointOfInterestWeatherStation.findAll", query = "SELECT p FROM PointOfInterestWeatherStation p"), @NamedQuery(name = "PointOfInterestWeatherStation.findAllByActivity", query = "SELECT p FROM PointOfInterestWeatherStation p WHERE p.active = :active"), @NamedQuery(name = "PointOfInterestWeatherStation.findByPointOfInterestId", query = "SELECT p FROM PointOfInterestWeatherStation p WHERE p.pointOfInterestId = :pointOfInterestId"), - @NamedQuery(name = "PointOfInterestWeatherStation.findByOrganizationId", query = "SELECT p FROM PointOfInterestWeatherStation p WHERE p.userId IN(SELECT u.userId FROM VipsLogicUser u WHERE u.organizationId=:organizationId)"), - @NamedQuery(name = "PointOfInterestWeatherStation.findByActivityAndOrganizationId", query = "SELECT p FROM PointOfInterestWeatherStation p WHERE p.active = :active AND p.userId IN(SELECT u.userId FROM VipsLogicUser u WHERE u.organizationId=:organizationId)"), - @NamedQuery(name = "PointOfInterestWeatherStation.findByUserId", query = "SELECT p FROM PointOfInterestWeatherStation p WHERE p.userId = :userId"), + @NamedQuery(name = "PointOfInterestWeatherStation.findByOrganizationId", query = "SELECT p FROM PointOfInterestWeatherStation p WHERE p.user IN(SELECT u.userId FROM VipsLogicUser u WHERE u.organizationId=:organizationId)"), + @NamedQuery(name = "PointOfInterestWeatherStation.findByActivityAndOrganizationId", query = "SELECT p FROM PointOfInterestWeatherStation p WHERE p.active = :active AND p.user IN(SELECT u.userId FROM VipsLogicUser u WHERE u.organizationId=:organizationId)"), + @NamedQuery(name = "PointOfInterestWeatherStation.findByUserId", query = "SELECT p FROM PointOfInterestWeatherStation p WHERE p.user = :userId"), @NamedQuery(name = "PointOfInterestWeatherStation.findByNames", query = "SELECT p FROM PointOfInterestWeatherStation p WHERE p.name IN :names") }) public class PointOfInterestWeatherStation extends PointOfInterest implements Serializable { diff --git a/src/main/java/no/nibio/vips/logic/service/AuthenticationService.java b/src/main/java/no/nibio/vips/logic/service/AuthenticationService.java index abb63297..114ffc24 100644 --- a/src/main/java/no/nibio/vips/logic/service/AuthenticationService.java +++ b/src/main/java/no/nibio/vips/logic/service/AuthenticationService.java @@ -79,7 +79,7 @@ public class AuthenticationService { // Get username and password from Json String username = credentials.get("username").asText(); String password = credentials.get("password").asText(); - Map<String,String> creds = new HashMap(); + Map<String,String> creds = new HashMap<>(); creds.put("username", username); creds.put("password", password); // Authenticate @@ -119,7 +119,7 @@ public class AuthenticationService { { // Also, renew the uuid by default length userBean.renewUserUuid(uuid); - return Response.ok().entity(user).build(); + return Response.ok().entity(user).build(); } else { diff --git a/src/main/java/no/nibio/vips/logic/service/LogicService.java b/src/main/java/no/nibio/vips/logic/service/LogicService.java index 655cf843..703e2d5f 100755 --- a/src/main/java/no/nibio/vips/logic/service/LogicService.java +++ b/src/main/java/no/nibio/vips/logic/service/LogicService.java @@ -71,6 +71,7 @@ import no.nibio.vips.logic.entity.PointOfInterestType; import no.nibio.vips.logic.entity.PointOfInterestWeatherStation; import no.nibio.vips.logic.entity.VipsLogicUser; import no.nibio.vips.logic.util.SystemTime; +import no.nibio.vips.observationdata.ObservationDataBean; import no.nibio.vips.util.CSVPrintUtil; import no.nibio.vips.util.ServletUtil; import no.nibio.vips.util.SolarRadiationUtil; @@ -101,6 +102,8 @@ public class LogicService { PointOfInterestBean pointOfInterestBean; @EJB MessageBean messageBean; + @EJB + ObservationDataBean observationDataBean; /** * Get all results for one pest prediction @@ -679,6 +682,7 @@ public class LogicService { return Response.ok().entity(retVal).build(); } + /** * Get a list of locations (pois) for a given organization @@ -809,6 +813,7 @@ public class LogicService { /** * Get a list of all pests, OR if cropOrganismId is specified, * get a list of all pests that are connected with this crop + * @param organization Id optional if set, observation data schemas are added * @return */ @GET @@ -816,7 +821,8 @@ public class LogicService { @Produces("application/json;charset=UTF-8") @Facet("restricted") public Response getPestOrganismList( - @QueryParam("cropOrganismId") Integer cropOrganismId + @QueryParam("cropOrganismId") Integer cropOrganismId, + @QueryParam("organizationId") Integer organizationId ) { List<Organism> organismList; @@ -828,6 +834,10 @@ public class LogicService { { organismList = organismBean.getCropPests(cropOrganismId); } + if(organizationId != null) + { + organismList = observationDataBean.decoratePestsWithOrganismDataSchema(organismList, organizationId); + } return Response.ok().entity(organismList).build(); } diff --git a/src/main/java/no/nibio/vips/logic/service/ObservationService.java b/src/main/java/no/nibio/vips/logic/service/ObservationService.java index cf695a52..aea53b42 100755 --- a/src/main/java/no/nibio/vips/logic/service/ObservationService.java +++ b/src/main/java/no/nibio/vips/logic/service/ObservationService.java @@ -19,17 +19,27 @@ package no.nibio.vips.logic.service; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import com.ibm.icu.util.ULocale; + +import java.io.File; import java.io.IOException; import java.net.URI; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.nio.file.Files; import java.text.ParseException; import java.text.SimpleDateFormat; import java.time.Instant; import java.util.ArrayList; +import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; import javax.ejb.EJB; @@ -57,6 +67,10 @@ import no.nibio.vips.logic.controller.session.UserBean; import no.nibio.vips.logic.entity.Gis; import no.nibio.vips.logic.entity.Observation; +import no.nibio.vips.logic.entity.ObservationIllustrationPK; +import no.nibio.vips.logic.entity.ObservationStatusType; +import no.nibio.vips.logic.entity.ObservationSyncInfo; +import no.nibio.vips.logic.entity.Organism; import no.nibio.vips.logic.entity.PolygonService; import no.nibio.vips.logic.entity.VipsLogicRole; import no.nibio.vips.logic.entity.VipsLogicUser; @@ -68,6 +82,8 @@ import no.nibio.vips.logic.util.Globals; import org.jboss.resteasy.annotations.GZIP; import org.wololo.geojson.Feature; +import org.apache.commons.codec.binary.Base64; + /** * @copyright 2016-2021 <a href="http://www.nibio.no/">NIBIO</a> * @author Tor-Einar Skog <tor-einar.skog@nibio.no> @@ -314,21 +330,63 @@ public class ObservationService { @GET @Path("list/user") @Produces("application/json;charset=UTF-8") - public Response getObservationsForUser() + public Response getObservationsForUser( + @QueryParam("observationIds") String observationIds + ) { - String uuidStr = httpServletRequest.getHeader(HttpHeaders.AUTHORIZATION); - UUID uuid = UUID.fromString(uuidStr); - VipsLogicUser user = userBean.findVipsLogicUser(uuid); + try + { + VipsLogicUser user = userBean.getUserFromUUID(httpServletRequest); + if(user != null) + { + List<Observation> allObs = observationBean.getObservationsForUser(user); + if(observationIds != null) + { + Set<Integer> observationIdSet = Arrays.asList(observationIds.split(",")).stream() + .map(s->Integer.valueOf(s)) + .collect(Collectors.toSet()); + return Response.ok().entity( + allObs.stream() + .filter(obs->observationIdSet.contains(obs.getObservationId())) + .collect(Collectors.toList()) + ) + .build(); + } + return Response.ok().entity(allObs).build(); + } + else + { + return Response.status(Status.UNAUTHORIZED).build(); + } + } + catch(Exception e) + { + return Response.serverError().entity(e.getMessage()).build(); + } + } + + /** + * Get observations for a user + * Requires a valid UUID to be provided in the Authorization header + * @return + */ + @GET + @Path("list/minimized/user") + @Produces("application/json;charset=UTF-8") + public Response getMinimizedObservationsForUser() + { + VipsLogicUser user = userBean.getUserFromUUID(httpServletRequest); if(user != null) { - return Response.ok().entity(observationBean.getObservationsForUser(user)).build(); + return Response.ok().entity(observationBean.getObservationsForUser(user).stream() + .map(obs->new ObservationSyncInfo(obs)).collect(Collectors.toList())).build(); } else { - return Response.status(Status.NOT_FOUND).build(); + return Response.status(Status.UNAUTHORIZED).build(); } } - + /** * Publicly available observations per organization * @param organizationId @@ -425,6 +483,16 @@ public class ObservationService { return Response.ok().entity(o).build(); } + @GET + @Path("polygonservices/{organizationId}") + @Produces("application/json;charset=UTF-8") + public Response getPolygonServicesForOrganization( + @PathParam("organizationId") Integer organizationId + ) + { + return Response.ok().entity(observationBean.getPolygonServicesForOrganization(organizationId)).build(); + } + /** * Deletes a gis entity and its corresponding observation */ @@ -641,4 +709,116 @@ public class ObservationService { return observations; } + @POST + @Path("syncobservationfromapp") + @Consumes("application/json;charset=UTF-8") + @Produces("application/json;charset=UTF-8") + public Response syncObservationFromApp( + String observationJson + ) + { + try + { + VipsLogicUser user = userBean.getUserFromUUID(httpServletRequest); + if(user == null) + { + return Response.status(Status.UNAUTHORIZED).build(); + } + ObjectMapper oM = new ObjectMapper(); + SimpleDateFormat df = new SimpleDateFormat(Globals.defaultTimestampFormat); + try { + Map<Object,Object> mapFromApp = oM.readValue(observationJson, new TypeReference<HashMap<Object,Object>>(){}); + // Check if it is marked as deleted or not + if(mapFromApp.get("deleted") != null && ((Boolean)mapFromApp.get("deleted").equals(true))) + { + if(observationBean.getObservation((Integer)mapFromApp.get("observationId")) != null) + { + observationBean.deleteObservation((Integer)mapFromApp.get("observationId")); + return Response.ok().build(); + } + else + { + return Response.status(Status.NOT_FOUND).build(); + } + } + else + { + Observation mergeObs = ((Integer)mapFromApp.get("observationId")) > 0 ? observationBean.getObservation((Integer)mapFromApp.get("observationId")): new Observation(); + // Trying to sync a non-existing observation + if(mergeObs == null) + { + return Response.status(Status.NOT_FOUND).build(); + } + // Pest organism + mergeObs.setOrganism(organismBean.getOrganism((Integer)mapFromApp.get("organismId"))); + // Crop organism + mergeObs.setCropOrganism(organismBean.getOrganism((Integer)mapFromApp.get("cropOrganismId"))); + // Other properties + mergeObs.setTimeOfObservation(oM.convertValue(mapFromApp.get("timeOfObservation"), new TypeReference<Date>(){})); + mergeObs.setUserId(mapFromApp.get("userId") != null ? Integer.valueOf((Integer)mapFromApp.get("userId")): user.getUserId()); + mergeObs.setGeoinfo((String)mapFromApp.get("geoinfo")); + mergeObs.setLocationPointOfInterestId(mapFromApp.get("locationPointOfInterestId") != null ? (Integer) mapFromApp.get("locationPointOfInterestId") : null); + mergeObs.setObservationHeading(mapFromApp.get("observationHeading") != null ? (String) mapFromApp.get("observationHeading") : null); + mergeObs.setObservationText(mapFromApp.get("observationText") != null ? (String) mapFromApp.get("observationText") : null); + mergeObs.setBroadcastMessage(mapFromApp.get("broadcastMessage") != null ? (Boolean) mapFromApp.get("broadcastMessage") : false); + mergeObs.setStatusTypeId(Integer.valueOf((Integer)mapFromApp.get("statusTypeId"))); + // If the user has the role of observation approver, change to approved if set to pending + if(mergeObs.getStatusTypeId().equals(ObservationStatusType.STATUS_PENDING) && user.isObservationAuthority()) + { + mergeObs.setStatusTypeId(ObservationStatusType.STATUS_APPROVED); + } + mergeObs.setStatusChangedByUserId(mapFromApp.get("statusChangedByUserId") != null ? (Integer) mapFromApp.get("statusChangedByUserId") : null); + mergeObs.setStatusChangedTime(mapFromApp.get("timeOfObservation") != null ? oM.convertValue(mapFromApp.get("timeOfObservation"), new TypeReference<Date>(){}) : null); + mergeObs.setStatusRemarks(mapFromApp.get("statusRemarks") != null ? (String) mapFromApp.get("statusRemarks") : null); + mergeObs.setIsQuantified(mapFromApp.get("isQuantified") != null ? (Boolean) mapFromApp.get("isQuantified") : false); + mergeObs.setLocationIsPrivate(mapFromApp.get("locationIsPrivate") != null ? (Boolean) mapFromApp.get("locationIsPrivate") : false); + mergeObs.setPolygonService(mapFromApp.get("polygonService") != null && ! ((String)mapFromApp.get("polygonService")).isEmpty() ? oM.convertValue(mapFromApp.get("polygonService"), new TypeReference<PolygonService>(){}) : null); + mergeObs.setObservationDataSchema(observationBean.getObservationDataSchema(user.getOrganization_id(), mergeObs.getOrganismId())); + mergeObs.setObservationData(mapFromApp.get("observationData") != null ? mapFromApp.get("observationData").toString() : null); + mergeObs.setLastEditedBy(user.getUserId()); + mergeObs.setLastEditedTime(new Date()); + + // Input check before storing + // Location must be set + if((mergeObs.getGeoinfo() == null || mergeObs.getGeoinfo().trim().isEmpty()) && mergeObs.getLocationPointOfInterestId() == null) + { + return Response.status(Status.BAD_REQUEST).entity("The observation is missing location data.").build(); + } + + // We need to get an observation Id before storing the illustrations! + mergeObs = observationBean.storeObservation(mergeObs); + + // ObservationIllustrationSet + // Including data that may need to be stored + if(mapFromApp.get("observationIllustrationSet") != null) + { + List<Map<Object,Object>> illusMaps = (List<Map<Object,Object>>) mapFromApp.get("observationIllustrationSet"); + for(Map<Object,Object> illusMap:illusMaps) + { + ObservationIllustrationPK pk = oM.convertValue(illusMap.get("observationIllustrationPK"), new TypeReference<ObservationIllustrationPK>(){}); + + if(illusMap.get("deleted") != null && ((Boolean) illusMap.get("deleted")) == true) + { + mergeObs = observationBean.deleteObservationIllustration(mergeObs, new String[] {pk.getFileName()}); + } + else if(illusMap.get("uploaded") != null && ((Boolean) illusMap.get("uploaded")) == false && illusMap.get("imageTextData") != null) + { + mergeObs = observationBean.storeObservationIllustration(mergeObs, pk.getFileName(), (String)illusMap.get("imageTextData")); + } + } + } + + + return Response.ok().entity(mergeObs).build(); + } + } catch (IOException e) { + return Response.serverError().entity(e).build(); + } + } + catch(Exception e) + { + return Response.serverError().entity(e).build(); + } + + } } diff --git a/src/main/java/no/nibio/vips/logic/service/POIService.java b/src/main/java/no/nibio/vips/logic/service/POIService.java new file mode 100644 index 00000000..7333c185 --- /dev/null +++ b/src/main/java/no/nibio/vips/logic/service/POIService.java @@ -0,0 +1,223 @@ +/* + * Copyright (c) 2021 NIBIO <http://www.nibio.no/>. + * + * This file is part of VIPSLogic. + * VIPSLogic is free software: you can redistribute it and/or modify + * it under the terms of the NIBIO Open Source License as published by + * NIBIO, either version 1 of the License, or (at your option) any + * later version. + * + * VIPSLogic 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 + * NIBIO Open Source License for more details. + * + * You should have received a copy of the NIBIO Open Source License + * along with VIPSLogic. If not, see <http://www.nibio.no/licenses/>. + * + */ +package no.nibio.vips.logic.service; + +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; + +import org.jboss.resteasy.spi.HttpRequest; +import org.locationtech.jts.geom.Coordinate; +import org.locationtech.jts.geom.Geometry; +import org.locationtech.jts.geom.Point; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.webcohesion.enunciate.metadata.Facet; + +import no.nibio.vips.gis.GISUtil; +import no.nibio.vips.logic.entity.Country; +import no.nibio.vips.logic.entity.Observation; +import no.nibio.vips.logic.entity.ObservationIllustrationPK; +import no.nibio.vips.logic.entity.Organization; +import no.nibio.vips.logic.entity.PointOfInterest; +import no.nibio.vips.logic.entity.PointOfInterestWeatherStation; +import no.nibio.vips.logic.entity.PolygonService; +import no.nibio.vips.logic.entity.VipsLogicUser; +import no.nibio.vips.logic.util.Globals; +import no.nibio.vips.logic.controller.session.SessionControllerGetter; + +/** + * @copyright 2021 <a href="http://www.nibio.no/">NIBIO</a> + * @author Tor-Einar Skog <tor-einar.skog@nibio.no> + */ +@Path("rest/poi") +public class POIService { + + @Context + private HttpRequest httpRequest; + @Context + private HttpServletRequest httpServletRequest; + + + /** + * Get a list of locations (pois) for a given organization + * @param organizationId + * @return + */ + @GET + @Path("organization/{organizationId}") + @Produces("application/json;charset=UTF-8") + public Response getPoisForOrganization(@PathParam("organizationId") Integer organizationId) + { + Organization organization = SessionControllerGetter.getUserBean().getOrganization(organizationId); + List<PointOfInterestWeatherStation> retVal = SessionControllerGetter.getPointOfInterestBean().getWeatherstationsForOrganization(organization, Boolean.TRUE); + return Response.ok().entity(retVal).build(); + } + + /** + * + * @param pointOfInterestId + * @return a particular POI (Point of interest) + */ + @GET + @Path("{pointOfInterestId}") + @Produces("application/json;charset=UTF-8") + public Response getPoi(@PathParam("pointOfInterestId") Integer pointOfInterestId) + { + PointOfInterest retVal = SessionControllerGetter.getPointOfInterestBean().getPointOfInterest(pointOfInterestId); + return Response.ok().entity(retVal).build(); + } + + /** + * Find a POI (Point of interest) by name + * @param poiName + * @return + */ + @GET + @Path("name/{poiName}") + @Produces("application/json;charset=UTF-8") + public Response getPoiByName(@PathParam("poiName") String poiName) + { + PointOfInterest retVal = SessionControllerGetter.getPointOfInterestBean().getPointOfInterest(poiName); + return retVal != null ? Response.ok().entity(retVal).build() : Response.noContent().build(); + } + + /** + * If used outside of VIPSLogic: Requires a valid UUID to be provided in the Authorization header + * @return + */ + @GET + @Path("user") + @Produces("application/json;charset=UTF-8") + @Facet("restricted") + public Response getPoisForCurrentUser() + { + VipsLogicUser user = (VipsLogicUser) httpServletRequest.getSession().getAttribute("user"); + // Could be the VIPS obs app or some other client using UUID + if(user == null) + { + String uuidStr = httpServletRequest.getHeader(HttpHeaders.AUTHORIZATION); + UUID uuid = UUID.fromString(uuidStr); + user = SessionControllerGetter.getUserBean().findVipsLogicUser(uuid); + if(user == null) + { + return Response.status(Status.UNAUTHORIZED).build(); + } + } + List<PointOfInterest> retVal = SessionControllerGetter.getPointOfInterestBean().getRelevantPointOfInterestsForUser(user); + return Response.ok().entity(retVal).build(); + } + + @POST + @Path("syncpoifromapp") + @Consumes("application/json;charset=UTF-8") + @Produces("application/json;charset=UTF-8") + public Response syncPOIFromApp( + String poiJson + ) + { + VipsLogicUser user = SessionControllerGetter.getUserBean().getUserFromUUID(httpServletRequest); + if(user == null) + { + return Response.status(Status.UNAUTHORIZED).build(); + } + ObjectMapper oM = new ObjectMapper(); + SimpleDateFormat df = new SimpleDateFormat(Globals.defaultTimestampFormat); + try { + Map<Object,Object> mapFromApp = oM.readValue(poiJson, new TypeReference<HashMap<Object,Object>>(){}); + // Check if it is marked as deleted or not + if(mapFromApp.get("deleted") != null && ((Boolean)mapFromApp.get("deleted").equals(true))) + { + if(SessionControllerGetter.getPointOfInterestBean().getPointOfInterest((Integer)mapFromApp.get("pointOfInterestId")) != null) + { + SessionControllerGetter.getPointOfInterestBean().deletePoi((Integer)mapFromApp.get("pointOfInterestId")); + return Response.ok().build(); + } + else + { + return Response.status(Status.NOT_FOUND).build(); + } + } + else + { + PointOfInterest mergePoi = ((Integer)mapFromApp.get("pointOfInterestId")) > 0 ? SessionControllerGetter.getPointOfInterestBean().getPointOfInterest((Integer)mapFromApp.get("pointOfInterestId")): PointOfInterest.getInstance((Integer)mapFromApp.get("pointOfInterestTypeId")); + // Trying to sync a non-existing observation + if(mergePoi == null) + { + return Response.status(Status.NOT_FOUND).build(); + } + + mergePoi.setName((String)mapFromApp.get("name")); + mergePoi.setTimeZone(mapFromApp.get("timeZone") != null ? + (String) mapFromApp.get("timeZone") + : user.getOrganizationId().getDefaultTimeZone() + ); + mergePoi.setLongitude((Double) mapFromApp.get("longitude")); + mergePoi.setLatitude((Double) mapFromApp.get("latitude")); + try { + Double altitude = mapFromApp.get("altitude") instanceof Integer ? + ((Integer) mapFromApp.get("altitude")).doubleValue() + :(Double) mapFromApp.get("altitude"); + mergePoi.setAltitude(altitude); + } + catch(NullPointerException | ClassCastException ex) + { + mergePoi.setAltitude(0.0); + } + mergePoi.setCountryCode( + mapFromApp.get("countryCode") != null ? + new Country((String)(((Map<Object,Object>)mapFromApp.get("countryCode")).get("countryCode"))) + : user.getOrganizationId().getCountryCode() + ); + mergePoi.setUser(user); + mergePoi.setLastEditedTime(new Date()); + GISUtil gisUtil = new GISUtil(); + Coordinate coordinate = new Coordinate(mergePoi.getLongitude(), mergePoi.getLatitude(), mergePoi.getAltitude()); + Point p3d = gisUtil.createPointWGS84(coordinate); + mergePoi.setGisGeom(p3d); + + mergePoi = SessionControllerGetter.getPointOfInterestBean().getPointOfInterest(SessionControllerGetter.getPointOfInterestBean().storePoi(mergePoi).getPointOfInterestId()); + + + return Response.ok().entity(mergePoi).build(); + } + } catch (IOException e) { + return Response.serverError().entity(e).build(); + } + + } + +} diff --git a/src/main/java/no/nibio/vips/observationdata/ObservationDataBean.java b/src/main/java/no/nibio/vips/observationdata/ObservationDataBean.java new file mode 100644 index 00000000..1bbfff03 --- /dev/null +++ b/src/main/java/no/nibio/vips/observationdata/ObservationDataBean.java @@ -0,0 +1,114 @@ +package no.nibio.vips.observationdata; + +import java.io.IOException; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.ResourceBundle; +import java.util.Map.Entry; + +import javax.ejb.Stateless; +import javax.persistence.EntityManager; +import javax.persistence.NoResultException; +import javax.persistence.PersistenceContext; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import no.nibio.vips.logic.entity.Organism; + +@Stateless +public class ObservationDataBean { + @PersistenceContext(unitName="VIPSLogic-PU") + EntityManager em; + + public JsonNode getSchema(Integer organizationId, Integer organismId, ResourceBundle bundle){ + ObservationDataSchema ods = null; + ObjectMapper m = new ObjectMapper(); + try + { + //System.out.println("organizationId = " + organizationId + ", organismId = " + organismId); + ods = em.createNamedQuery("ObservationDataSchema.findByPK", ObservationDataSchema.class) + .setParameter("organizationId", organizationId) + .setParameter("organismId", organismId) + .getSingleResult(); + + + + // We iterate the schema, replacing default field labels with + // translated ones + // First: Convert to Jackson JsonNode tree + JsonNode rootNode = m.readTree(ods.getDataSchema()); + if(bundle != null) + { + Iterator<Entry<String, JsonNode>> nodeIterator = rootNode.fields(); + + String fieldKeyPrefix = "observationDataField_"; + // Loop through each field + while (nodeIterator.hasNext()) { + Map.Entry<String, JsonNode> schemaPropertyField = (Map.Entry<String, JsonNode>) nodeIterator.next(); + // Get the property field key (e.g. "counting2") + String fieldKey = schemaPropertyField.getKey(); + // Find a translation. + if(bundle.containsKey(fieldKeyPrefix + fieldKey)) + { + // If found, replace with translation + // Get the property field (e.g. {"title":"Counting 2"} ) + JsonNode schemaProperty = schemaPropertyField.getValue(); + ((ObjectNode)schemaProperty).put("title", bundle.getString(fieldKeyPrefix + fieldKey)); + ((ObjectNode)rootNode).replace(fieldKey, schemaProperty); + } + } + } + return rootNode; + + }catch(IOException | NoResultException ex){} + + // If not found, return standard nominator/denominator (unit) form + try { + return m.readTree(getStandardSchema()); + } catch (IOException e) { + return m.createObjectNode(); + } + } + + + public String getStandardSchema(){ + return "{\n" + + " \"$schema\": \"http://json-schema.org/draft-04/schema#\",\n" + + " \"type\": \"object\",\n" + + " \"title\": \" \",\n" + + " \"properties\": {" + + "\"number\":{\"title\":\"Number\", \"type\":\"string\"}," + + "\"unit\":{\"title\":\"Unit\", \"type\":\"string\"}" + + "}}"; + } + + public String getStandardModel(){ + return "{" + + "\"number\":0," + + "\"unit\":\"Number\"" + + "}"; + } + + public List<Organism> decoratePestsWithOrganismDataSchema(List<Organism> pests, Integer organizationId ) + { + try + { + + ObjectMapper om = new ObjectMapper(); + for(Organism pest:pests) + { + pest.setObservationDataSchema(om.writeValueAsString(this.getSchema(organizationId, pest.getOrganismId(), null))); + } + return pests; + } + catch(JsonProcessingException ex) + { + ex.printStackTrace(); + return pests; + } + } +} diff --git a/src/main/java/no/nibio/vips/observationdata/ObservationDataService.java b/src/main/java/no/nibio/vips/observationdata/ObservationDataService.java index d02ba09d..d4475d13 100755 --- a/src/main/java/no/nibio/vips/observationdata/ObservationDataService.java +++ b/src/main/java/no/nibio/vips/observationdata/ObservationDataService.java @@ -50,6 +50,8 @@ public class ObservationDataService { UserBean userBean; @EJB ObservationBean observationBean; + @EJB + ObservationDataBean observationDataBean; @Context private HttpServletRequest httpServletRequest; @@ -80,9 +82,9 @@ public class ObservationDataService { { return Response.serverError().entity(ex).build(); } - } + @GET @Path("model/{organizationId}/{organismId}") @Produces("application/json;charset=UTF-8") @@ -98,20 +100,8 @@ public class ObservationDataService { .getSingleResult(); }catch(NoResultException nre){} // If not found, return standard nominator/denominator (unit) form - return Response.ok().entity(ods != null ? ods.getDataModel() : this.getStandardModel()).build(); + return Response.ok().entity(ods != null ? ods.getDataModel() : observationDataBean.getStandardModel()).build(); } - private String getStandardSchema(){ - return "{" - + "\"number\":{\"title\":\"Number\"}," - + "\"unit\":{\"title\":\"Unit\"}" - + "}"; - } - private String getStandardModel(){ - return "{" - + "\"number\":0," - + "\"unit\":\"Number\"" - + "}"; - } } diff --git a/src/main/java/no/nibio/vips/util/weather/WeatherDataSourceUtil.java b/src/main/java/no/nibio/vips/util/weather/WeatherDataSourceUtil.java index ad85e6ae..f0746db3 100755 --- a/src/main/java/no/nibio/vips/util/weather/WeatherDataSourceUtil.java +++ b/src/main/java/no/nibio/vips/util/weather/WeatherDataSourceUtil.java @@ -50,7 +50,7 @@ import org.apache.commons.io.IOUtils; */ public class WeatherDataSourceUtil { - private final boolean DEBUG = false; + private final boolean DEBUG = true; /** * Fetches measured data from the stations weather data source, and optionally diff --git a/src/main/resources/db/migration/V10__POI_add_last_edited_date.sql b/src/main/resources/db/migration/V10__POI_add_last_edited_date.sql new file mode 100644 index 00000000..30408a32 --- /dev/null +++ b/src/main/resources/db/migration/V10__POI_add_last_edited_date.sql @@ -0,0 +1,7 @@ +ALTER TABLE public.point_of_interest +ADD COLUMN last_edited_time TIMESTAMP WITH TIME ZONE DEFAULT now(); + +-- One time update +UPDATE public.point_of_interest +SET last_edited_time = '2021-01-01'; + diff --git a/src/main/resources/db/migration/V9__Observation_add_last_edited_date.sql b/src/main/resources/db/migration/V9__Observation_add_last_edited_date.sql new file mode 100644 index 00000000..243e8bcf --- /dev/null +++ b/src/main/resources/db/migration/V9__Observation_add_last_edited_date.sql @@ -0,0 +1,6 @@ +ALTER TABLE public.observation +ADD COLUMN last_edited_time TIMESTAMP WITH TIME ZONE DEFAULT now(); + +-- One-time update of existing records +UPDATE public.observation +SET last_edited_time = status_changed_time; diff --git a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts.properties b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts.properties index d68d35f5..8ecc2a34 100755 --- a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts.properties +++ b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts.properties @@ -1,12 +1,21 @@ #Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/) # - -\#\ Copyright\ (c)\ 2014\ NIBIO\ <http = //www.nibio.no/>. - -\#\ VIPSLogic\ is\ free\ software = you can redistribute it and/or modify - -\#\ along\ with\ VIPSLogic.\ \ If\ not,\ see\ <http = //www.nibio.no/licenses/>. - + # Copyright (c) 2014 NIBIO <http://www.nibio.no/>. + # + # This file is part of VIPSLogic. + # VIPSLogic is free software: you can redistribute it and/or modify + # it under the terms of the NIBIO Open Source License as published by + # NIBIO, either version 1 of the License, or (at your option) any + # later version. + # + # VIPSLogic 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 + # NIBIO Open Source License for more details. + # + # You should have received a copy of the NIBIO Open Source License + # along with VIPSLogic. If not, see <http://www.nibio.no/licenses/>. + # ALTERNARIA = Alternaria Model APPLESCABM = Apple scab model @@ -1028,3 +1037,4 @@ dd_lower=Day degree lower cutoffs dd_upper=Day degree upper cutoffs observedPhase=Observed phase at biofix date YSTEMBTEMP=Yellow Stemborer Temperature Model +addIllustration=Add illustration diff --git a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_bs.properties b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_bs.properties index 0c111166..1afc139b 100755 --- a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_bs.properties +++ b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_bs.properties @@ -1,12 +1,20 @@ -#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/) # - -\#\ Copyright\ (c)\ 2014\ NIBIO\ <http = //www.nibio.no/>. - -\#\ VIPSLogic\ is\ free\ software = you can redistribute it and/or modify - -\#\ along\ with\ VIPSLogic.\ \ If\ not,\ see\ <http = //www.nibio.no/licenses/>. - + # Copyright (c) 2014 NIBIO <http://www.nibio.no/>. + # + # This file is part of VIPSLogic. + # VIPSLogic is free software: you can redistribute it and/or modify + # it under the terms of the NIBIO Open Source License as published by + # NIBIO, either version 1 of the License, or (at your option) any + # later version. + # + # VIPSLogic 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 + # NIBIO Open Source License for more details. + # + # You should have received a copy of the NIBIO Open Source License + # along with VIPSLogic. If not, see <http://www.nibio.no/licenses/>. + # ALTERNARIA = Alternaria Model APPLESCABM = Apple scab model @@ -1022,3 +1030,4 @@ dd_lower=Day degree lower cutoffs dd_upper=Day degree upper cutoffs observedPhase=Observed phase at biofix date YSTEMBTEMP=Yellow Stemborer Temperature Model +addIllustration=Add illustration diff --git a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_hr.properties b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_hr.properties index 27f573bf..1bb1784d 100755 --- a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_hr.properties +++ b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_hr.properties @@ -1,11 +1,20 @@ -#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/) # - -\#\ Copyright\ (c)\ 2014\ NIBIO\ <http = //www.nibio.no/>. - -\#\ VIPSLogic\ is\ free\ software = you can redistribute it and/or modify - -\#\ along\ with\ VIPSLogic.\ \ If\ not,\ see\ <http = //www.nibio.no/licenses/>. + # Copyright (c) 2014 NIBIO <http://www.nibio.no/>. + # + # This file is part of VIPSLogic. + # VIPSLogic is free software: you can redistribute it and/or modify + # it under the terms of the NIBIO Open Source License as published by + # NIBIO, either version 1 of the License, or (at your option) any + # later version. + # + # VIPSLogic 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 + # NIBIO Open Source License for more details. + # + # You should have received a copy of the NIBIO Open Source License + # along with VIPSLogic. If not, see <http://www.nibio.no/licenses/>. + # ALTERNARIA = Alternaria Model @@ -1020,3 +1029,4 @@ dd_lower=Day degree lower cutoffs dd_upper=Day degree upper cutoffs observedPhase=Observed phase at biofix date YSTEMBTEMP=Yellow Stemborer Temperature Model +addIllustration=Add illustration diff --git a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_nb.properties b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_nb.properties index 4544e4c7..20a2a531 100755 --- a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_nb.properties +++ b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_nb.properties @@ -1,11 +1,21 @@ -#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/) # + # Copyright (c) 2014 NIBIO <http://www.nibio.no/>. + # + # This file is part of VIPSLogic. + # VIPSLogic is free software: you can redistribute it and/or modify + # it under the terms of the NIBIO Open Source License as published by + # NIBIO, either version 1 of the License, or (at your option) any + # later version. + # + # VIPSLogic 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 + # NIBIO Open Source License for more details. + # + # You should have received a copy of the NIBIO Open Source License + # along with VIPSLogic. If not, see <http://www.nibio.no/licenses/>. + # -\#\ Copyright\ (c)\ 2014\ NIBIO\ <http = //www.nibio.no/>. - -\#\ VIPSLogic\ is\ free\ software = you can redistribute it and/or modify - -\#\ along\ with\ VIPSLogic.\ \ If\ not,\ see\ <http = //www.nibio.no/licenses/>. ALTERNARIA = Alternariamodell @@ -1028,3 +1038,4 @@ dd_lower=Minimumstemperaturer d\u00f8gngradberegning dd_upper=Maksimumstemperaturer d\u00f8gngradberegning observedPhase=Observert utviklingsstadium ved biofix-dato YSTEMBTEMP=Yellow Stemborer temperaturmodell +addIllustration=Legg til illustrasjon diff --git a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_sr.properties b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_sr.properties index 591da1f9..fee3ba47 100755 --- a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_sr.properties +++ b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_sr.properties @@ -1,11 +1,20 @@ -#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/) # - -\#\ Copyright\ (c)\ 2014\ NIBIO\ <http = //www.nibio.no/>. - -\#\ VIPSLogic\ is\ free\ software = you can redistribute it and/or modify - -\#\ along\ with\ VIPSLogic.\ \ If\ not,\ see\ <http = //www.nibio.no/licenses/>. + # Copyright (c) 2014 NIBIO <http://www.nibio.no/>. + # + # This file is part of VIPSLogic. + # VIPSLogic is free software: you can redistribute it and/or modify + # it under the terms of the NIBIO Open Source License as published by + # NIBIO, either version 1 of the License, or (at your option) any + # later version. + # + # VIPSLogic 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 + # NIBIO Open Source License for more details. + # + # You should have received a copy of the NIBIO Open Source License + # along with VIPSLogic. If not, see <http://www.nibio.no/licenses/>. + # ALTERNARIA = Alternaria Model @@ -1022,3 +1031,4 @@ dd_lower=Day degree lower cutoffs dd_upper=Day degree upper cutoffs observedPhase=Observed phase at biofix date YSTEMBTEMP=Yellow Stemborer Temperature Model +addIllustration=Add illustration diff --git a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_zh_CN.properties b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_zh_CN.properties index 599a97ca..778dbb65 100755 --- a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_zh_CN.properties +++ b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts_zh_CN.properties @@ -1,12 +1,20 @@ -#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/) # - -\#\ Copyright\ (c)\ 2014\ NIBIO\ <http = //www.nibio.no/>. - -\#\ VIPSLogic\ is\ free\ software = you can redistribute it and/or modify - -\#\ along\ with\ VIPSLogic.\ \ If\ not,\ see\ <http = //www.nibio.no/licenses/>. - + # Copyright (c) 2014 NIBIO <http://www.nibio.no/>. + # + # This file is part of VIPSLogic. + # VIPSLogic is free software: you can redistribute it and/or modify + # it under the terms of the NIBIO Open Source License as published by + # NIBIO, either version 1 of the License, or (at your option) any + # later version. + # + # VIPSLogic 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 + # NIBIO Open Source License for more details. + # + # You should have received a copy of the NIBIO Open Source License + # along with VIPSLogic. If not, see <http://www.nibio.no/licenses/>. + # ALTERNARIA = Alternaria Model APPLESCABM = \u82f9\u679c\u9ed1\u661f\u75c5\u6a21\u578b @@ -1016,3 +1024,4 @@ dd_lower=Day degree lower cutoffs dd_upper=Day degree upper cutoffs observedPhase=Observed phase at biofix date YSTEMBTEMP=Yellow Stemborer Temperature Model +addIllustration=Add illustration diff --git a/src/main/webapp/templates/observationForm.ftl b/src/main/webapp/templates/observationForm.ftl index ba748445..e195bd66 100755 --- a/src/main/webapp/templates/observationForm.ftl +++ b/src/main/webapp/templates/observationForm.ftl @@ -689,19 +689,20 @@ <textarea class="form-control" name="observationText" placeholder="" <#if editAccess!="W">readonly="readonly"</#if>>${observation.observationText!""}</textarea> <span class="help-block" id="${formId}_observationText_validation"></span> </div> - <#if observation.observationIllustrationSet?has_content && observation.observationIllustrationSet?size == 1> - <#assign illustration = observation.observationIllustrationSet?first> + <#if observation.observationIllustrationSet?has_content && observation.observationIllustrationSet?size gt 0> + <#list observation.observationIllustrationSet as illustration> <img src="/static/images/observations/${observation.organismId}/${illustration.observationIllustrationPK.fileName}" alt="TODO: Add describing text" class="img-responsive"/> <div class="checkbox"> <label> - <input type="checkbox" name="deleteIllustration" value="true"/> + <input type="checkbox" name="deleteIllustration" value="${illustration.observationIllustrationPK.fileName}"/> ${i18nBundle.deleteIllustration} </label> </div> + </#list> </#if> <div class="form-group"> <div class="input-group"> - <label for="illustration"><#if observation.observationIllustrationSet?has_content && observation.observationIllustrationSet?size == 1>${i18nBundle.replaceIllustration}<#else>${i18nBundle.newIllustration}</#if></label><br/> + <label for="illustration"><#if observation.observationIllustrationSet?has_content && observation.observationIllustrationSet?size gt 0>${i18nBundle.addIllustration}<#else>${i18nBundle.newIllustration}</#if></label><br/> <span class="btn btn-default btn-file">${i18nBundle.browse}<input type="file" name="illustration"></span> <input type="text" class="form-control" readonly> </div> diff --git a/src/main/webapp/templates/poiForm.ftl b/src/main/webapp/templates/poiForm.ftl index ca9d861c..74555332 100755 --- a/src/main/webapp/templates/poiForm.ftl +++ b/src/main/webapp/templates/poiForm.ftl @@ -172,7 +172,7 @@ <select class="form-control" name="userId" onblur="validateField(this);"> <option value="-1">${i18nBundle.pleaseSelect} ${i18nBundle.vipsLogicUserId?lower_case}</option> <#list users as user> - <option value="${user.userId}"<#if poi.userId?has_content && user.userId == poi.userId.userId + <option value="${user.userId}"<#if poi.user?has_content && user.userId == poi.user.userId > selected="selected"</#if>>${user.lastName}, ${user.firstName} (${user.organizationId.organizationName})</option> </#list> </select> diff --git a/src/main/webapp/templates/poiList.ftl b/src/main/webapp/templates/poiList.ftl index 6e8e0b3a..12bd313f 100755 --- a/src/main/webapp/templates/poiList.ftl +++ b/src/main/webapp/templates/poiList.ftl @@ -88,7 +88,7 @@ ${i18nBundle["pointOfInterestType_" + poi.pointOfInterestTypeId]} </td> <td> - <#if user.isSuperUser() || user.isOrganizationAdmin() || poi.userId.userId == user.userId> + <#if user.isSuperUser() || user.isOrganizationAdmin() || poi.user.userId == user.userId> <a href="/poi?action=editPoiForm&pointOfInterestId=${poi.pointOfInterestId}" class="btn btn-default" role="button">${i18nBundle.edit}</a> </#if> </td> -- GitLab