diff --git a/pom.xml b/pom.xml index b942c53a5dd01548fee6ae7d81bd110e56e68088..89120e15b262d8dcf44c39784557233bbb7774fa 100755 --- a/pom.xml +++ b/pom.xml @@ -266,6 +266,21 @@ <version>2.0.11</version> <scope>provided</scope> </dependency> + <dependency> + <groupId>org.apache.poi</groupId> + <artifactId>poi</artifactId> + <version>5.3.0</version> + </dependency> + <dependency> + <groupId>org.apache.poi</groupId> + <artifactId>poi-ooxml</artifactId> + <version>5.3.0</version> + </dependency> + <dependency> + <groupId>commons-io</groupId> + <artifactId>commons-io</artifactId> + <version>2.17.0</version> + </dependency> </dependencies> <build> 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 e205348f4a9e392b952760fb525d084c284033a6..99a5f3c304414d0c40f218fd7a03323e694c7631 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 @@ -61,7 +61,7 @@ import org.wololo.geojson.GeoJSONFactory; */ @Stateless public class ObservationBean { - private static Logger LOGGER = LoggerFactory.getLogger(ObservationBean.class); + private static final Logger LOGGER = LoggerFactory.getLogger(ObservationBean.class); @PersistenceContext(unitName = "VIPSLogic-PU") EntityManager em; @EJB @@ -520,10 +520,6 @@ public class ObservationBean { .filter(o -> o.getObservationTimeSeriesId() != null) .forEach(o -> o.setObservationTimeSeries(timeSeriesMap.get(o.getObservationTimeSeriesId()))); - for (Observation o : observations) { - LOGGER.info("{}", o); - } - return observations; } private List<Observation> getObservationsWithObservers(List<Observation> observations) { 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 241e0c14ad16f334804c251a2f47dc698315c2f1..0b244dcfbb9170f91e9fe3c0eb296c30cd90d419 100755 --- a/src/main/java/no/nibio/vips/logic/entity/Observation.java +++ b/src/main/java/no/nibio/vips/logic/entity/Observation.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014 NIBIO <http://www.nibio.no/>. + * Copyright (c) 2014 NIBIO <http://www.nibio.no/>. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -95,15 +95,15 @@ public class Observation implements Serializable, no.nibio.vips.observation.Obse private Boolean broadcastMessage; private Boolean locationIsPrivate; private PolygonService polygonService; - + private ObservationDataSchema observationDataSchema; - + private VipsLogicUser user; // Transient private VipsLogicUser lastEditedByUser; // private PointOfInterest location; // Transient - + private Set<ObservationIllustration> observationIllustrationSet; - + private GISEntityUtil GISEntityUtil; private GISUtil GISUtil; @@ -156,7 +156,7 @@ public class Observation implements Serializable, no.nibio.vips.observation.Obse this.timeOfObservation = timeOfObservation; } - + // Using PostGIS + Hibernate-spatial + Java Topology Suite to make this work /*@JsonIgnore @Type(type = "org.hibernate.spatial.GeometryType") @@ -168,12 +168,12 @@ public class Observation implements Serializable, no.nibio.vips.observation.Obse public void setLocation(Point location) { this.location = location; }*/ - + public void setGeoinfos(List<Gis> geoinfo) { this.geoinfo = geoinfo; } - + public void addGeoInfo(Gis geoinfo) { if(this.geoinfo == null) @@ -182,20 +182,20 @@ public class Observation implements Serializable, no.nibio.vips.observation.Obse } this.geoinfo.add(geoinfo); } - + @JsonIgnore @Transient public List<Gis> getGeoinfos() { return this.geoinfo; } - - + + public void setGeoinfo(String json) { this.setGeoinfos(this.GISEntityUtil.getGisFromGeoJSON(json)); } - + @Transient @Override public String getGeoinfo() @@ -269,37 +269,37 @@ public class Observation implements Serializable, no.nibio.vips.observation.Obse @Override public String toString() { return "Observation{" + - "observationId=" + observationId + - ", timeOfObservation=" + timeOfObservation + - ", organism=" + organism + - ", cropOrganism=" + cropOrganism + - ", userId=" + userId + - ", lastEditedBy=" + lastEditedBy + - ", geoinfo=" + geoinfo + - ", observationTimeSeries=" + observationTimeSeries + - ", locationPointOfInterestId=" + locationPointOfInterestId + - ", observationHeading='" + observationHeading + '\'' + - ", observationText='" + observationText + '\'' + - ", statusTypeId=" + statusTypeId + - ", statusChangedByUserId=" + statusChangedByUserId + - ", statusChangedTime=" + statusChangedTime + - ", lastEditedTime=" + lastEditedTime + - ", statusRemarks='" + statusRemarks + '\'' + - ", observationData='" + observationData + '\'' + - ", isQuantified=" + isQuantified + - ", isPositive=" + isPositive + - ", broadcastMessage=" + broadcastMessage + - ", locationIsPrivate=" + locationIsPrivate + - ", polygonService=" + polygonService + - ", observationDataSchema=" + observationDataSchema + - ", user=" + user + - ", lastEditedByUser=" + lastEditedByUser + - ", location=" + location + - ", observationIllustrationSet=" + observationIllustrationSet + - ", GISEntityUtil=" + GISEntityUtil + - ", GISUtil=" + GISUtil + - ", source=" + source + - '}'; + "observationId=" + observationId + + ", timeOfObservation=" + timeOfObservation + + ", organism=" + organism + + ", cropOrganism=" + cropOrganism + + ", userId=" + userId + + ", lastEditedBy=" + lastEditedBy + + ", geoinfo=" + geoinfo + + ", observationTimeSeries=" + observationTimeSeries + + ", locationPointOfInterestId=" + locationPointOfInterestId + + ", observationHeading='" + observationHeading + '\'' + + ", observationText='" + observationText + '\'' + + ", statusTypeId=" + statusTypeId + + ", statusChangedByUserId=" + statusChangedByUserId + + ", statusChangedTime=" + statusChangedTime + + ", lastEditedTime=" + lastEditedTime + + ", statusRemarks='" + statusRemarks + '\'' + + ", observationData='" + observationData + '\'' + + ", isQuantified=" + isQuantified + + ", isPositive=" + isPositive + + ", broadcastMessage=" + broadcastMessage + + ", locationIsPrivate=" + locationIsPrivate + + ", polygonService=" + polygonService + + ", observationDataSchema=" + observationDataSchema + + ", user=" + user + + ", lastEditedByUser=" + lastEditedByUser + + ", location=" + location + + ", observationIllustrationSet=" + observationIllustrationSet + + ", GISEntityUtil=" + GISEntityUtil + + ", GISUtil=" + GISUtil + + ", source=" + source + + '}'; } /** @@ -338,13 +338,13 @@ public class Observation implements Serializable, no.nibio.vips.observation.Obse public void setOrganism(Organism organism) { this.organism = organism; } - + @JoinColumn(name = "polygon_service_id", referencedColumnName = "polygon_service_id") @ManyToOne public PolygonService getPolygonService(){ return this.polygonService; } - + public void setPolygonService(PolygonService polygonService) { this.polygonService = polygonService; @@ -363,7 +363,7 @@ public class Observation implements Serializable, no.nibio.vips.observation.Obse return this.getLocationCoordinate().x; } */ - + @Override @Transient public String getName() { @@ -480,13 +480,13 @@ public class Observation implements Serializable, no.nibio.vips.observation.Obse public void setObservationData(String observationData) { this.observationData = observationData; } - + @Transient public ObservationDataSchema getObservationDataSchema() { return this.observationDataSchema != null ? this.observationDataSchema : null; } - + public void setObservationDataSchema(ObservationDataSchema observationDataSchema) { this.observationDataSchema = observationDataSchema; @@ -522,7 +522,7 @@ public class Observation implements Serializable, no.nibio.vips.observation.Obse public void setCropOrganism(Organism cropOrganism) { this.cropOrganism = cropOrganism; } - + @Transient public Integer getCropOrganismId() { return this.getCropOrganism() != null ? this.getCropOrganism().getOrganismId() : null; @@ -564,7 +564,7 @@ public class Observation implements Serializable, no.nibio.vips.observation.Obse } /** - * @return the observation time series + * @return the observation time series */ @JoinColumn(name = "observation_time_series_id", referencedColumnName = "observation_time_series_id") @ManyToOne @@ -678,7 +678,7 @@ public class Observation implements Serializable, no.nibio.vips.observation.Obse /** * Simplifies the public JSON object * @param locale - * @return + * @return */ public ObservationListItem getListItem(String locale, ObservationDataSchema observationDataSchema) { @@ -690,36 +690,41 @@ public class Observation implements Serializable, no.nibio.vips.observation.Obse this.location.addProperty("timestamp", this.getTimeOfObservation().getTime()); } return new ObservationListItem( - this.getObservationId(), - this.getObservationTimeSeriesId(), - this.getTimeOfObservation(), - this.getOrganismId(), - ! this.getOrganism().getLocalName(locale).trim().isBlank() ? this.getOrganism().getLocalName(locale) : this.getOrganism().getLatinName(), - this.getCropOrganismId(), - ! this.getCropOrganism().getLocalName(locale).trim().isBlank() ? this.getCropOrganism().getLocalName(locale) : this.getCropOrganism().getLatinName(), - this.observationTimeSeries != null ? this.observationTimeSeries.getLabel() : null, - // Specific geoInfo trumps location. This is to be interpreted - // as that the observation has been geographically masked by - // choice of the observer - this.location != null && this.geoinfo == null ? this.location.getGeoJSON() : this.getGeoinfo(), - this.getObservationHeading(), - this.getBroadcastMessage(), - this.getLocationIsPrivate(), - this.getIsPositive(), - this.getObservationData(), - observationDataSchema + this.getObservationId(), + this.userId, + this.user != null ? this.user.getFullName() : null, + this.getObservationTimeSeriesId(), + this.getTimeOfObservation(), + this.getOrganismId(), + ! this.getOrganism().getLocalName(locale).trim().isBlank() ? this.getOrganism().getLocalName(locale) : this.getOrganism().getLatinName(), + this.getCropOrganismId(), + ! this.getCropOrganism().getLocalName(locale).trim().isBlank() ? this.getCropOrganism().getLocalName(locale) : this.getCropOrganism().getLatinName(), + this.observationTimeSeries != null ? this.observationTimeSeries.getLabel() : null, + // Specific geoInfo trumps location. This is to be interpreted + // as that the observation has been geographically masked by + // choice of the observer + this.location != null ? this.location.getPointOfInterestId() : null, + this.location != null ? this.location.getName() : null, + this.location != null && this.geoinfo == null ? this.location.getGeoJSON() : this.getGeoinfo(), + this.getObservationHeading(), + this.getObservationText(), + this.getBroadcastMessage(), + this.getLocationIsPrivate(), + this.getIsPositive(), + this.getObservationData(), + observationDataSchema ); } @Temporal(TemporalType.TIMESTAMP) @Column(name = "last_edited_time") - public Date getLastEditedTime() { - return lastEditedTime; - } + public Date getLastEditedTime() { + return lastEditedTime; + } - public void setLastEditedTime(Date lastEditedTime) { - this.lastEditedTime = lastEditedTime; - } + public void setLastEditedTime(Date lastEditedTime) { + this.lastEditedTime = lastEditedTime; + } @Column(name = "is_positive") public Boolean getIsPositive() { diff --git a/src/main/java/no/nibio/vips/logic/entity/VipsLogicUser.java b/src/main/java/no/nibio/vips/logic/entity/VipsLogicUser.java index c8d96e9477c78f3cac0cb482c7168e210f084eb1..27102b4dd8893a1bc2857c365a4cec3b6a7f6446 100755 --- a/src/main/java/no/nibio/vips/logic/entity/VipsLogicUser.java +++ b/src/main/java/no/nibio/vips/logic/entity/VipsLogicUser.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015-2019 NIBIO <http://www.nibio.no/>. + * Copyright (c) 2015-2019 NIBIO <http://www.nibio.no/>. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -63,7 +63,7 @@ import java.util.UUID; @NamedQuery(name = "VipsLogicUser.findByCompletePhoneNumber", query = "SELECT v FROM VipsLogicUser v WHERE v.phoneCountryCode || v.phone = :completePhoneNumber") }) public class VipsLogicUser implements Serializable, Comparable{ - + private static final long serialVersionUID = 1L; private Integer userId; //if the field contains email address consider using this annotation to enforce field validation @@ -93,7 +93,7 @@ public class VipsLogicUser implements Serializable, Comparable{ private Integer vipsCoreUserId; private boolean approvesSmsBilling; private boolean freeSms; - + private UUID userUuid; public VipsLogicUser() { @@ -143,6 +143,12 @@ public class VipsLogicUser implements Serializable, Comparable{ this.lastName = lastName; } + @JsonIgnore + @Transient + public String getFullName() { + return firstName + " " + lastName; + } + @OneToMany(cascade = CascadeType.ALL, mappedBy = "vipsLogicUser", fetch=FetchType.EAGER) @XmlTransient @JsonIgnore @@ -160,7 +166,7 @@ public class VipsLogicUser implements Serializable, Comparable{ public Organization getOrganizationId() { return organizationId; } - + @Transient public Integer getOrganization_id(){ return organizationId.getOrganizationId(); @@ -249,11 +255,11 @@ public class VipsLogicUser implements Serializable, Comparable{ @ManyToMany(fetch = FetchType.EAGER) @JsonIgnore @JoinTable( - name = "user_vips_logic_role", - joinColumns = { - @JoinColumn(name = "user_id")}, - inverseJoinColumns = { - @JoinColumn(name = "vips_logic_role_id")} + name = "user_vips_logic_role", + joinColumns = { + @JoinColumn(name = "user_id")}, + inverseJoinColumns = { + @JoinColumn(name = "vips_logic_role_id")} ) public Set<VipsLogicRole> getVipsLogicRoles() { return vipsLogicRoles; @@ -287,7 +293,7 @@ public class VipsLogicUser implements Serializable, Comparable{ } return false; } - + @JsonIgnore @Transient public boolean isObservationAuthority(){ @@ -298,7 +304,7 @@ public class VipsLogicUser implements Serializable, Comparable{ } return false; } - + @JsonIgnore @Transient public boolean isOrganismEditor() { @@ -309,7 +315,7 @@ public class VipsLogicUser implements Serializable, Comparable{ } return false; } - + @JsonIgnore @Transient public boolean isAppleFruitMothAdministrator(){ @@ -320,7 +326,7 @@ public class VipsLogicUser implements Serializable, Comparable{ } return false; } - + @JsonIgnore @Transient public boolean isMessageAuthor(){ @@ -499,7 +505,7 @@ public class VipsLogicUser implements Serializable, Comparable{ public boolean isFreeSms() { return freeSms; } - + /** * @param freeSms the freeSms to set @@ -513,6 +519,6 @@ public class VipsLogicUser implements Serializable, Comparable{ VipsLogicUser other = (VipsLogicUser)o; return (this.getLastName() + ", " + this.getFirstName()).compareTo(other.getLastName() + ", " + other.getFirstName()); } - - + + } diff --git a/src/main/java/no/nibio/vips/logic/entity/rest/ObservationListItem.java b/src/main/java/no/nibio/vips/logic/entity/rest/ObservationListItem.java index a4dd12f901526bfda95fb6c5edadc1c793218772..95d83c07dfcc4494d8fbc4022afebdff8fbb58aa 100644 --- a/src/main/java/no/nibio/vips/logic/entity/rest/ObservationListItem.java +++ b/src/main/java/no/nibio/vips/logic/entity/rest/ObservationListItem.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018 NIBIO <http://www.nibio.no/>. + * Copyright (c) 2018 NIBIO <http://www.nibio.no/>. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -28,11 +28,16 @@ import no.nibio.vips.observationdata.ObservationDataSchema; */ public class ObservationListItem implements Comparable{ private Integer observationId, observationTimeSeriesId, organismId, cropOrganismId; + private Integer observerId; + private String observerName; private Date timeOfObservation; private String organismName, cropOrganismName; private String observationTimeSeriesLabel; + private Integer locationPointOfInterestId; + private String locationPointOfInterestName; private String geoInfo; private String observationHeading; + private String observationText; private String observationData; private ObservationDataSchema observationDataSchema; private Boolean broadcastMessage; @@ -40,23 +45,29 @@ public class ObservationListItem implements Comparable{ private Boolean isPositive; public ObservationListItem( - Integer observationId, - Integer observationTimeSeriesId, - Date timeOfObservation, - Integer organismId, - String organismName, - Integer cropOrganismId, - String cropOrganismName, - String observationTimeSeriesLabel, - String geoinfo, - String observationHeading, - Boolean broadcastMessage, - Boolean locationIsPrivate, - Boolean isPositive, - String observationData, - ObservationDataSchema observationDataSchema - ){ + Integer observationId, + Integer observerId, + String observerName, + Integer observationTimeSeriesId, + Date timeOfObservation, + Integer organismId, + String organismName, + Integer cropOrganismId, + String cropOrganismName, + String observationTimeSeriesLabel, + Integer poiId, + String poiName, + String geoinfo, + String observationHeading, + String observationText, + Boolean broadcastMessage, + Boolean locationIsPrivate, + Boolean isPositive, + String observationData, + ObservationDataSchema observationDataSchema){ this.observationId = observationId; + this.observerId = observerId; + this.observerName = observerName; this.observationTimeSeriesId = observationTimeSeriesId; this.timeOfObservation = timeOfObservation; this.organismId = organismId; @@ -64,15 +75,18 @@ public class ObservationListItem implements Comparable{ this.cropOrganismId = cropOrganismId; this.cropOrganismName = cropOrganismName; this.observationTimeSeriesLabel = observationTimeSeriesLabel; + this.locationPointOfInterestId = poiId; + this.locationPointOfInterestName = poiName; this.geoInfo = geoinfo; this.observationHeading = observationHeading; + this.observationText = observationText; this.broadcastMessage = broadcastMessage; this.locationIsPrivate = locationIsPrivate; this.isPositive = isPositive; this.observationData = observationData; this.observationDataSchema = observationDataSchema; } - + @Override public int compareTo(Object otherOne) @@ -83,7 +97,7 @@ public class ObservationListItem implements Comparable{ } return this.getTimeOfObservation().compareTo(((ObservationListItem) otherOne).getTimeOfObservation()); } - + /** * @return the observationId */ @@ -98,6 +112,34 @@ public class ObservationListItem implements Comparable{ this.observationId = observationId; } + /** + * @return The ID of the observer + */ + public Integer getObserverId() { + return observerId; + } + + /** + * @param observerId The ID to set + */ + public void setObserverId(Integer observerId) { + this.observerId = observerId; + } + + /** + * @return The full name of the observer + */ + public String getObserverName() { + return observerName; + } + + /** + * @param observerName The observer name to set + */ + public void setObserverName(String observerName) { + this.observerName = observerName; + } + /** * @return the observationTimeSeriesId */ @@ -162,6 +204,22 @@ public class ObservationListItem implements Comparable{ this.observationTimeSeriesLabel = observationTimeSeriesLabel; } + public Integer getLocationPointOfInterestId() { + return locationPointOfInterestId; + } + + public void setLocationPointOfInterestId(Integer locationPointOfInterestId) { + this.locationPointOfInterestId = locationPointOfInterestId; + } + + public String getLocationPointOfInterestName() { + return locationPointOfInterestName; + } + + public void setLocationPointOfInterestName(String locationPointOfInterestName) { + this.locationPointOfInterestName = locationPointOfInterestName; + } + /** * @return the geoInfo */ @@ -190,6 +248,20 @@ public class ObservationListItem implements Comparable{ this.observationHeading = observationHeading; } + /** + * @return the observation text + */ + public String getObservationText() { + return observationText; + } + + /** + * @param observationText The observation text to set + */ + public void setObservationText(String observationText) { + this.observationText = observationText; + } + /** * @return the organismId */ 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 daf359258690e26b9c4a7393a5b2ce1e945554c3..75d4ac8ba5def7edab317ebfe851561a08c28b84 100755 --- a/src/main/java/no/nibio/vips/logic/service/ObservationService.java +++ b/src/main/java/no/nibio/vips/logic/service/ObservationService.java @@ -32,6 +32,7 @@ import no.nibio.vips.logic.entity.rest.ObservationListItem; import no.nibio.vips.logic.entity.rest.PointMappingResponse; import no.nibio.vips.logic.entity.rest.ReferencedPoint; import no.nibio.vips.logic.messaging.MessagingBean; +import no.nibio.vips.logic.util.ExcelFileGenerator; import no.nibio.vips.logic.util.GISEntityUtil; import no.nibio.vips.logic.util.Globals; import org.jboss.resteasy.annotations.GZIP; @@ -59,6 +60,8 @@ import java.net.URI; import java.text.ParseException; import java.text.SimpleDateFormat; import java.time.Instant; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; import java.util.*; import java.util.stream.Collectors; @@ -108,34 +111,35 @@ public class ObservationService { @Produces("application/json;charset=UTF-8") @TypeHint(Observation[].class) public Response getFilteredObservations( - @PathParam("organizationId") Integer organizationId, - @QueryParam("observationTimeSeriesId") Integer observationTimeSeriesId, - @QueryParam("pestId") Integer pestId, - @QueryParam("cropId") Integer cropId, - @QueryParam("cropCategoryId") List<Integer> cropCategoryId, - @QueryParam("from") String fromStr, - @QueryParam("to") String toStr, - @QueryParam("isPositive") Boolean isPositive + @PathParam("organizationId") Integer organizationId, + @QueryParam("observationTimeSeriesId") Integer observationTimeSeriesId, + @QueryParam("pestId") Integer pestId, + @QueryParam("cropId") Integer cropId, + @QueryParam("cropCategoryId") List<Integer> cropCategoryId, + @QueryParam("from") String fromStr, + @QueryParam("to") String toStr, + @QueryParam("isPositive") Boolean isPositive ) { return Response.ok().entity(getFilteredObservationsFromBackend( - organizationId, - observationTimeSeriesId, - pestId, - cropId, - cropCategoryId, - fromStr, - toStr, - isPositive + organizationId, + observationTimeSeriesId, + pestId, + cropId, + cropCategoryId, + fromStr, + toStr, + isPositive )).build(); } /** - * @param organizationId Database ID of the organization - * @param pestId Database ID of the pest - * @param cropId Database ID of the crop - * @param cropCategoryId cropCategoryId Database IDs of the crop category/categories - * @param fromStr format "yyyy-MM-dd" - * @param toStr format "yyyy-MM-dd" + * @param organizationId Database ID of the organization + * @param observationTimeSeriesId Database ID of the observation time series + * @param pestId Database ID of the pest + * @param cropId Database ID of the crop + * @param cropCategoryId cropCategoryId Database IDs of the crop category/categories + * @param fromStr format "yyyy-MM-dd" + * @param toStr format "yyyy-MM-dd" * @return Observation objects for which the user is authorized to observe with properties relevant for lists */ @GET @@ -144,59 +148,105 @@ public class ObservationService { @Produces("application/json;charset=UTF-8") @TypeHint(ObservationListItem.class) public Response getFilteredObservationListItemsAsJson( - @PathParam("organizationId") Integer organizationId, - @QueryParam("observationTimeSeriesId") Integer observationTimeSeriesId, - @QueryParam("pestId") Integer pestId, - @QueryParam("cropId") Integer cropId, - @QueryParam("cropCategoryId") List<Integer> cropCategoryId, - @QueryParam("from") String fromStr, - @QueryParam("to") String toStr, - @QueryParam("userUUID") String userUUID, - @QueryParam("locale") String localeStr, - @QueryParam("isPositive") Boolean isPositive + @PathParam("organizationId") Integer organizationId, + @QueryParam("observationTimeSeriesId") Integer observationTimeSeriesId, + @QueryParam("pestId") Integer pestId, + @QueryParam("cropId") Integer cropId, + @QueryParam("cropCategoryId") List<Integer> cropCategoryId, + @QueryParam("from") String fromStr, + @QueryParam("to") String toStr, + @QueryParam("userUUID") String userUUID, + @QueryParam("locale") String localeStr, + @QueryParam("isPositive") Boolean isPositive ) { return Response.ok().entity(this.getFilteredObservationListItems(organizationId, observationTimeSeriesId, pestId, cropId, cropCategoryId, fromStr, toStr, userUUID, localeStr, isPositive)).build(); } - private List<ObservationListItem> getFilteredObservationListItems( - Integer organizationId, - Integer observationTimeSeriesId, - Integer pestId, - Integer cropId, - List<Integer> cropCategoryId, - String fromStr, - String toStr, - String userUUID, - String localeStr, - Boolean isPositive) { - VipsLogicUser user = (VipsLogicUser) httpServletRequest.getSession().getAttribute("user"); + /** + * @param organizationId Database ID of the organization + * @param observationTimeSeriesId Database ID of the observation time series + * @param pestId Database ID of the pest + * @param cropId Database ID of the crop + * @param cropCategoryId cropCategoryId Database IDs of the crop category/categories + * @param fromStr format "yyyy-MM-dd" + * @param toStr format "yyyy-MM-dd" + * @return Observation objects for which the user is authorized to observe with properties relevant for lists + */ + @GET + @Path("list/filter/{organizationId}/xlsx") + @GZIP + @Produces("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + public Response getFilteredObservationListItemsAsXlsx( + @PathParam("organizationId") Integer organizationId, + @QueryParam("observationTimeSeriesId") Integer observationTimeSeriesId, + @QueryParam("pestId") Integer pestId, + @QueryParam("cropId") Integer cropId, + @QueryParam("cropCategoryId") List<Integer> cropCategoryId, + @QueryParam("from") String fromStr, + @QueryParam("to") String toStr, + @QueryParam("userUUID") String userUUID, + @QueryParam("locale") String localeStr, + @QueryParam("isPositive") Boolean isPositive + ) { + VipsLogicUser user = getVipsLogicUser(userUUID); + ULocale locale = new ULocale(localeStr != null ? localeStr : + user != null ? user.getOrganizationId().getDefaultLocale() : + userBean.getOrganization(organizationId).getDefaultLocale()); + LOGGER.info("Generate xlsx file for observations for user {} from {} to {}", user != null ? user.getUserId() : "unregistered", fromStr, toStr); - if (user == null && userUUID != null) { - user = userBean.findVipsLogicUser(UUID.fromString(userUUID)); + LocalDateTime now = LocalDateTime.now(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss"); + String filenameTimestamp = now.format(formatter); + + try { + List<ObservationListItem> observations = getFilteredObservationListItems(organizationId, observationTimeSeriesId, pestId, cropId, cropCategoryId, fromStr, toStr, userUUID, localeStr, isPositive); + byte[] excelFile = ExcelFileGenerator.generateExcel(user, locale, now, fromStr, toStr, observations); + + return Response + .ok(excelFile) + .header("Content-Disposition", "attachment; filename=\"" + filenameTimestamp + "-observations.xlsx\"") + .build(); + } catch (IOException e) { + LOGGER.error(e.getMessage()); + return Response.serverError().entity("Error generating Excel file: " + e.getMessage()).build(); } + } + + private List<ObservationListItem> getFilteredObservationListItems( + Integer organizationId, + Integer observationTimeSeriesId, + Integer pestId, + Integer cropId, + List<Integer> cropCategoryId, + String fromStr, + String toStr, + String userUUID, + String localeStr, + Boolean isPositive) { + VipsLogicUser user = getVipsLogicUser(userUUID); ULocale locale = new ULocale(localeStr != null ? localeStr : - user != null ? user.getOrganizationId().getDefaultLocale() : - userBean.getOrganization(organizationId).getDefaultLocale()); + user != null ? user.getOrganizationId().getDefaultLocale() : + userBean.getOrganization(organizationId).getDefaultLocale()); LOGGER.info("Get filtered observations for user {}", user != null ? user.getUserId() : "<no user>"); List<ObservationListItem> observations = getFilteredObservationsFromBackend( - organizationId, - observationTimeSeriesId, - pestId, - cropId, - cropCategoryId, - fromStr, - toStr, - isPositive, - user + organizationId, + observationTimeSeriesId, + pestId, + cropId, + cropCategoryId, + fromStr, + toStr, + isPositive, + user ).stream().map(obs -> { try { return obs.getListItem(locale.getLanguage(), - observationBean.getLocalizedObservationDataSchema( - observationBean.getObservationDataSchema(organizationId, obs.getOrganismId()), - httpServletRequest, - locale - ) + observationBean.getLocalizedObservationDataSchema( + observationBean.getObservationDataSchema(organizationId, obs.getOrganismId()), + httpServletRequest, + locale + ) ); } catch (IOException e) { LOGGER.error("Exception when getting localized observation data schema for observation " + obs.getObservationId(), e); @@ -222,16 +272,16 @@ public class ObservationService { @Produces("text/csv;charset=UTF-8") @TypeHint(ObservationListItem.class) public Response getFilteredObservationListItemsAsCSV( - @PathParam("organizationId") Integer organizationId, - @QueryParam("observationTimeSeriesId") Integer observationTimeSeriesId, - @QueryParam("pestId") Integer pestId, - @QueryParam("cropId") Integer cropId, - @QueryParam("cropCategoryId") List<Integer> cropCategoryId, - @QueryParam("from") String fromStr, - @QueryParam("to") String toStr, - @QueryParam("userUUID") String userUUID, - @QueryParam("locale") String localeStr, - @QueryParam("isPositive") Boolean isPositive + @PathParam("organizationId") Integer organizationId, + @QueryParam("observationTimeSeriesId") Integer observationTimeSeriesId, + @QueryParam("pestId") Integer pestId, + @QueryParam("cropId") Integer cropId, + @QueryParam("cropCategoryId") List<Integer> cropCategoryId, + @QueryParam("from") String fromStr, + @QueryParam("to") String toStr, + @QueryParam("userUUID") String userUUID, + @QueryParam("locale") String localeStr, + @QueryParam("isPositive") Boolean isPositive ) { List<ObservationListItem> observations = this.getFilteredObservationListItems(organizationId, observationTimeSeriesId, pestId, cropId, cropCategoryId, fromStr, toStr, userUUID, localeStr, isPositive); Collections.sort(observations); @@ -245,12 +295,12 @@ public class ObservationService { c = ((Point) geometries.get(0)).getCoordinate(); } retVal += "\n" + obs.getObservationId() - + ";" + obs.getOrganismName() - + ";" + obs.getCropOrganismName() - + ";" + obs.getTimeOfObservation() - + ";" + (c != null ? c.getY() + "," + c.getX() : "") - + ";" + obs.getObservationHeading() - + ";" + obs.getObservationData(); + + ";" + obs.getOrganismName() + + ";" + obs.getCropOrganismName() + + ";" + obs.getTimeOfObservation() + + ";" + (c != null ? c.getY() + "," + c.getX() : "") + + ";" + obs.getObservationHeading() + + ";" + obs.getObservationData(); } return Response.ok().entity(retVal).build(); } @@ -266,14 +316,14 @@ public class ObservationService { * @return Observation objects for which the user is authorized to observe with properties relevant for lists */ private List<Observation> getFilteredObservationsFromBackend( - Integer organizationId, - Integer observationTimeSeriesId, - Integer pestId, - Integer cropId, - List<Integer> cropCategoryId, - String fromStr, - String toStr, - Boolean isPositive) { + Integer organizationId, + Integer observationTimeSeriesId, + Integer pestId, + Integer cropId, + List<Integer> cropCategoryId, + String fromStr, + String toStr, + Boolean isPositive) { SimpleDateFormat format = new SimpleDateFormat(Globals.defaultDateFormat); //TODO Set correct timeZone!!! Date from = null; @@ -286,14 +336,14 @@ public class ObservationService { } return observationBean.getFilteredObservations( - organizationId, - observationTimeSeriesId, - pestId, - cropId, - cropCategoryId, - from, - to, - isPositive + organizationId, + observationTimeSeriesId, + pestId, + cropId, + cropCategoryId, + from, + to, + isPositive ); } @@ -411,14 +461,14 @@ public class ObservationService { @Produces("application/json;charset=UTF-8") @TypeHint(GeoJSON.class) public Response getFilteredObservationsAsGeoJSON( - @PathParam("organizationId") Integer organizationId, - @QueryParam("observationTimeSeriesId") Integer observationTimeSeriesId, - @QueryParam("pestId") Integer pestId, - @QueryParam("cropId") Integer cropId, - @QueryParam("cropCategoryId") List<Integer> cropCategoryId, - @QueryParam("from") String fromStr, - @QueryParam("to") String toStr, - @QueryParam("isPositive") Boolean isPositive + @PathParam("organizationId") Integer organizationId, + @QueryParam("observationTimeSeriesId") Integer observationTimeSeriesId, + @QueryParam("pestId") Integer pestId, + @QueryParam("cropId") Integer cropId, + @QueryParam("cropCategoryId") List<Integer> cropCategoryId, + @QueryParam("from") String fromStr, + @QueryParam("to") String toStr, + @QueryParam("isPositive") Boolean isPositive ) { SimpleDateFormat format = new SimpleDateFormat(Globals.defaultDateFormat); @@ -433,14 +483,14 @@ public class ObservationService { } List<Observation> filteredObservations = this.getFilteredObservationsFromBackend( - organizationId, - observationTimeSeriesId, - pestId, - cropId, - cropCategoryId, - fromStr, - toStr, - isPositive + organizationId, + observationTimeSeriesId, + pestId, + cropId, + cropCategoryId, + fromStr, + toStr, + isPositive ); GISEntityUtil gisUtil = new GISEntityUtil(); @@ -505,7 +555,7 @@ public class ObservationService { @Produces("application/json;charset=UTF-8") @TypeHint(Observation[].class) public Response getObservationsForUser( - @QueryParam("observationIds") String observationIds + @QueryParam("observationIds") String observationIds ) { LOGGER.info("getObservationsForUser for observationIds={}", observationIds); try { @@ -516,14 +566,14 @@ public class ObservationService { LOGGER.info("Found {} observations for user {}", allObs.size(), user.getUserId()); if (observationIds != null) { Set<Integer> observationIdSet = Arrays.asList(observationIds.split(",")).stream() - .map(s -> Integer.valueOf(s)) - .collect(Collectors.toSet()); + .map(s -> Integer.valueOf(s)) + .collect(Collectors.toSet()); return Response.ok().entity( - allObs.stream() - .filter(obs -> observationIdSet.contains(obs.getObservationId())) - .collect(Collectors.toList()) - ) - .build(); + allObs.stream() + .filter(obs -> observationIdSet.contains(obs.getObservationId())) + .collect(Collectors.toList()) + ) + .build(); } return Response.ok().entity(allObs).build(); } else { @@ -550,7 +600,7 @@ public class ObservationService { VipsLogicUser user = userBean.getUserFromUUID(httpServletRequest); if (user != null) { return Response.ok().entity(observationBean.getObservationsForUser(user).stream() - .map(obs -> new ObservationSyncInfo(obs)).collect(Collectors.toList())).build(); + .map(obs -> new ObservationSyncInfo(obs)).collect(Collectors.toList())).build(); } else { return Response.status(Status.UNAUTHORIZED).build(); } @@ -568,13 +618,13 @@ public class ObservationService { @Produces("application/json;charset=UTF-8") @TypeHint(Observation[].class) public Response getBroadcastObservations( - @PathParam("organizationId") Integer organizationId, - @QueryParam("season") Integer season, - @QueryParam("timeOfObservationFrom") String timeOfObservationFrom, - @QueryParam("timeOfObservationTo") String timeOfObservationTo + @PathParam("organizationId") Integer organizationId, + @QueryParam("season") Integer season, + @QueryParam("timeOfObservationFrom") String timeOfObservationFrom, + @QueryParam("timeOfObservationTo") String timeOfObservationTo ) { if ((timeOfObservationFrom != null && !timeOfObservationFrom.isEmpty()) - || (timeOfObservationTo != null && !timeOfObservationTo.isEmpty())) { + || (timeOfObservationTo != null && !timeOfObservationTo.isEmpty())) { Date from = null; Date to = null; try { @@ -606,8 +656,8 @@ public class ObservationService { @Produces("application/json;charset=UTF-8") @TypeHint(Observation.class) public Response getObservation( - @PathParam("observationId") Integer observationId, - @QueryParam("userUUID") String userUUID + @PathParam("observationId") Integer observationId, + @QueryParam("userUUID") String userUUID ) { // Observation needs to be masked here as well, or does it create trouble for VIPSLogic observation admin? Observation o = observationBean.getObservation(observationId); @@ -636,9 +686,9 @@ public class ObservationService { List<Observation> intermediary = new ArrayList<>(); intermediary.add(o); intermediary = this.maskObservations(o.getPolygonService(), - observationBean.getObservationsWithLocations( - observationBean.getObservationsWithGeoInfo(intermediary) - ) + observationBean.getObservationsWithLocations( + observationBean.getObservationsWithGeoInfo(intermediary) + ) ); o = intermediary.get(0); } @@ -659,7 +709,7 @@ public class ObservationService { @Produces("application/json;charset=UTF-8") @TypeHint(PolygonService[].class) public Response getPolygonServicesForOrganization( - @PathParam("organizationId") Integer organizationId + @PathParam("organizationId") Integer organizationId ) { return Response.ok().entity(observationBean.getPolygonServicesForOrganization(organizationId)).build(); } @@ -679,10 +729,10 @@ public class ObservationService { return Response.status(Response.Status.UNAUTHORIZED).build(); } if (!userBean.authorizeUser(user, - VipsLogicRole.OBSERVER, - VipsLogicRole.OBSERVATION_AUTHORITY, - VipsLogicRole.ORGANIZATION_ADMINISTRATOR, - VipsLogicRole.SUPERUSER + VipsLogicRole.OBSERVER, + VipsLogicRole.OBSERVATION_AUTHORITY, + VipsLogicRole.ORGANIZATION_ADMINISTRATOR, + VipsLogicRole.SUPERUSER ) ) { return Response.status(Response.Status.FORBIDDEN).build(); @@ -712,10 +762,10 @@ public class ObservationService { return Response.status(Response.Status.UNAUTHORIZED).build(); } if (!userBean.authorizeUser(user, - VipsLogicRole.OBSERVER, - VipsLogicRole.OBSERVATION_AUTHORITY, - VipsLogicRole.ORGANIZATION_ADMINISTRATOR, - VipsLogicRole.SUPERUSER + VipsLogicRole.OBSERVER, + VipsLogicRole.OBSERVATION_AUTHORITY, + VipsLogicRole.ORGANIZATION_ADMINISTRATOR, + VipsLogicRole.SUPERUSER ) ) { return Response.status(Response.Status.FORBIDDEN).build(); @@ -744,7 +794,7 @@ public class ObservationService { public Response getFirstObservation(@PathParam("organismId") Integer organismId) { Date firstObsTime = observationBean.getFirstObservationTime(organismId); return firstObsTime != null ? Response.ok().entity(firstObsTime).build() - : Response.status(404).entity("No observations of organism with id=" + organismId).build(); + : Response.status(404).entity("No observations of organism with id=" + organismId).build(); } /** @@ -766,26 +816,26 @@ public class ObservationService { } /** - * @param organizationId Database id of the organization + * @param organizationId Database id of the organization * @param observationTimeSeriesId Database id of the observation time series - * @param pestId Database id of the pest - * @param cropId Database id of the crop - * @param cropCategoryId Database ids of the crop categories - * @param fromStr format "yyyy-MM-dd" - * @param toStr format "yyyy-MM-dd" - * @param user The user that requests this (used for authorization) + * @param pestId Database id of the pest + * @param cropId Database id of the crop + * @param cropCategoryId Database ids of the crop categories + * @param fromStr format "yyyy-MM-dd" + * @param toStr format "yyyy-MM-dd" + * @param user The user that requests this (used for authorization) * @return A list of observations that meets the filter criteria */ private List<Observation> getFilteredObservationsFromBackend( - Integer organizationId, - Integer observationTimeSeriesId, - Integer pestId, - Integer cropId, - List<Integer> cropCategoryId, - String fromStr, - String toStr, - Boolean isPositive, - VipsLogicUser user + Integer organizationId, + Integer observationTimeSeriesId, + Integer pestId, + Integer cropId, + List<Integer> cropCategoryId, + String fromStr, + String toStr, + Boolean isPositive, + VipsLogicUser user ) { List<Observation> filteredObservations = this.getFilteredObservationsFromBackend(organizationId, observationTimeSeriesId, pestId, cropId, cropCategoryId, fromStr, toStr, isPositive); @@ -803,10 +853,10 @@ public class ObservationService { LOGGER.info("Return {} masked public observations for unregistered user", retVal.size()); return sortObservationsByDateAndId(retVal); } - // Else: This is a registered user without special privileges. Show public observations + user's own + // Else: This is a registered user without special privileges. Show public observations + user's own // Making sure we don't add duplicates - Set<Integer> obsIds = retVal.stream().map(o->o.getObservationId()).collect(Collectors.toSet()); - retVal.addAll(observationBean.getObservationsForUser(user).stream().filter(o->!obsIds.contains(o.getObservationId())).collect(Collectors.toList())); + Set<Integer> obsIds = retVal.stream().map(o -> o.getObservationId()).collect(Collectors.toSet()); + retVal.addAll(observationBean.getObservationsForUser(user).stream().filter(o -> !obsIds.contains(o.getObservationId())).collect(Collectors.toList())); LOGGER.info("Return {} masked public observations and user's own observations for registered user {}", retVal.size(), user.getUserId()); return sortObservationsByDateAndId(retVal); } @@ -839,7 +889,7 @@ public class ObservationService { Map<Integer, Observation> maskedObservations = new HashMap<>(); registeredPolygonServicesInObservationList.keySet().forEach((pService) -> { this.maskObservations(pService, registeredPolygonServicesInObservationList.get(pService)) - .forEach(o -> maskedObservations.put(o.getObservationId(), o)); + .forEach(o -> maskedObservations.put(o.getObservationId(), o)); }); // Adding the rest of the observations (the ones that don't need masking) @@ -858,19 +908,19 @@ public class ObservationService { Client client = ClientBuilder.newClient(); WebTarget target = client.target(polygonService.getGisSearchUrlTemplate()); List<ReferencedPoint> points = observations.stream() - .filter(obs -> (obs.getGeoinfos() != null && !obs.getGeoinfos().isEmpty()) || obs.getLocation() != null) - .map(obs -> { - ReferencedPoint rp = new ReferencedPoint(); - rp.setId(String.valueOf(obs.getObservationId())); - if (obs.getGeoinfos() != null) { - rp.setLon(obs.getGeoinfos().get(0).getGisGeom().getCoordinate().x); - rp.setLat(obs.getGeoinfos().get(0).getGisGeom().getCoordinate().y); - } else { - rp.setLon(obs.getLocation().getLongitude()); - rp.setLat(obs.getLocation().getLatitude()); - } - return rp; - }).collect(Collectors.toList()); + .filter(obs -> (obs.getGeoinfos() != null && !obs.getGeoinfos().isEmpty()) || obs.getLocation() != null) + .map(obs -> { + ReferencedPoint rp = new ReferencedPoint(); + rp.setId(String.valueOf(obs.getObservationId())); + if (obs.getGeoinfos() != null) { + rp.setLon(obs.getGeoinfos().get(0).getGisGeom().getCoordinate().x); + rp.setLat(obs.getGeoinfos().get(0).getGisGeom().getCoordinate().y); + } else { + rp.setLon(obs.getLocation().getLongitude()); + rp.setLat(obs.getLocation().getLatitude()); + } + return rp; + }).collect(Collectors.toList()); /*System.out.println("maskobservations - target.request() about to be called"); ObjectMapper oMapper = new ObjectMapper(); try { @@ -879,7 +929,7 @@ public class ObservationService { Logger.getLogger(ObservationService.class.getName()).log(Level.SEVERE, null, ex); }*/ PointMappingResponse response = target.request(MediaType.APPLICATION_JSON) - .post(Entity.entity(points.toArray(new ReferencedPoint[points.size()]), MediaType.APPLICATION_JSON), PointMappingResponse.class); + .post(Entity.entity(points.toArray(new ReferencedPoint[points.size()]), MediaType.APPLICATION_JSON), PointMappingResponse.class); // We need to loop through the observations and find corresponding featurecollections and replace those Map<Integer, Feature> indexedPolygons = new HashMap<>(); for (Feature feature : response.getFeatureCollection().getFeatures()) { @@ -915,7 +965,7 @@ public class ObservationService { @Produces("application/json;charset=UTF-8") @TypeHint(Observation.class) public Response syncObservationFromApp( - String observationJson + String observationJson ) { LOGGER.info("In syncObservationFromApp"); @@ -1074,17 +1124,32 @@ public class ObservationService { */ private List<Observation> sortObservationsByDateAndId(List<Observation> observations) { return observations.stream() - .sorted((o1, o2) -> { - int timeCompare = o2.getTimeOfObservation().compareTo(o1.getTimeOfObservation()); - if (timeCompare != 0) { - return timeCompare; - } else { - return Integer.compare(o2.getObservationId(), o1.getObservationId()); - } - }) - .collect(Collectors.toList()); + .sorted((o1, o2) -> { + int timeCompare = o2.getTimeOfObservation().compareTo(o1.getTimeOfObservation()); + if (timeCompare != 0) { + return timeCompare; + } else { + return Integer.compare(o2.getObservationId(), o1.getObservationId()); + } + }) + .collect(Collectors.toList()); } + /** + * Find VipsLogic user from session or given userUUID + * + * @param userUUID the UUID of the user + * @return the corresponding VipsLogicUser + */ + private VipsLogicUser getVipsLogicUser(String userUUID) { + VipsLogicUser user = (VipsLogicUser) httpServletRequest.getSession().getAttribute("user"); + if (user == null && userUUID != null) { + user = userBean.findVipsLogicUser(UUID.fromString(userUUID)); + } + return user; + } + + /** * Utility method for getting the string value of a given property in a given map. * diff --git a/src/main/java/no/nibio/vips/logic/util/ExcelFileGenerator.java b/src/main/java/no/nibio/vips/logic/util/ExcelFileGenerator.java new file mode 100644 index 0000000000000000000000000000000000000000..242306b53b1674404bb49a9f6503cc17e18e8cf0 --- /dev/null +++ b/src/main/java/no/nibio/vips/logic/util/ExcelFileGenerator.java @@ -0,0 +1,441 @@ +package no.nibio.vips.logic.util; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ibm.icu.util.ULocale; +import no.nibio.vips.logic.entity.VipsLogicUser; +import no.nibio.vips.logic.entity.rest.ObservationListItem; +import no.nibio.vips.observationdata.ObservationDataSchema; +import org.apache.poi.common.usermodel.HyperlinkType; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.apache.poi.ss.usermodel.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.*; + +public final class ExcelFileGenerator { + + private static final Logger LOGGER = LoggerFactory.getLogger(ExcelFileGenerator.class); + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + private static final ObjectMapper objectMapper = new ObjectMapper(); + + private static final String VIPSWEB = "https://www.vips-landbruk.no"; + private static final String VIPSLOGIC = "https://logic.vips.nibio.no"; + + private enum ColumnIndex { + ID(false, 0, 0, "observationId"), + DATE(false, 1, 1, "timeOfObservation"), + POI_NAME(false, 2, 2, "location"), + OBSERVER_NAME(true, null, 3, "observer"), + OBSERVATION_TIME_SERIES_LABEL(false, 3, 4, "observationTimeSeriesLabel"), + ORGANISM(false, 4, 5, "organism"), + CROP_ORGANISM(false, 5, 6, "cropOrganismId"), + HEADING(false, 6, 7, "observationHeading"), + DESCRIPTION(false, 7, 8, "observationText"), + BROADCAST(false, 8, 9, "isBroadcast"), + POSITIVE(false, 9, 10, "isPositiveRegistration"), + INDEX_DATA(false, 10, 11, null); + + private final boolean isSensitive; + private final Integer openIndex; + private final Integer adminIndex; + private final String rbKey; + + ColumnIndex(boolean isSensitive, Integer openIndex, Integer adminIndex, String rbKey) { + this.isSensitive = isSensitive; + this.openIndex = openIndex; + this.adminIndex = adminIndex; + this.rbKey = rbKey; + } + + public static List<ColumnIndex> forUser(boolean isAdmin) { + if (!isAdmin) { + return Arrays.stream(ColumnIndex.values()).filter(columnIndex -> !columnIndex.isSensitive).toList(); + } + return Arrays.stream(ColumnIndex.values()).toList(); + } + + public String getColumnHeading(ResourceBundle rb) { + return rbKey != null && !rbKey.isBlank() ? rb.getString(rbKey) : ""; + } + + public Integer getIndex(boolean admin) { + if (admin) { + return adminIndex; + } + return openIndex; + } + } + + public static byte[] generateExcel(VipsLogicUser user, ULocale locale, LocalDateTime now, String fromStr, String toStr, List<ObservationListItem> observations) throws IOException { + ResourceBundle rb = ResourceBundle.getBundle("no.nibio.vips.logic.i18n.vipslogictexts", locale.toLocale()); + boolean isAdmin = user != null && (user.isSuperUser() || user.isOrganizationAdmin()); + LOGGER.info("Create Excel file containing {} observations for {} user", observations.size(), isAdmin ? "admin" : "regular"); + try (XSSFWorkbook workbook = new XSSFWorkbook(); + ByteArrayOutputStream out = new ByteArrayOutputStream()) { + + Font font = workbook.createFont(); + font.setBold(true); + CellStyle headerStyle = workbook.createCellStyle(); + headerStyle.setFont(font); + + // Create main sheet for all observations, with header row + Sheet mainSheet = workbook.createSheet(rb.getString("allObservations")); + createHeaderRow(isAdmin, mainSheet, headerStyle, rb); + + int mainSheetRowIndex = 1; + // Add one row for each observation in list of all observations + for (ObservationListItem item : observations) { + createItemRow(isAdmin, mainSheet, mainSheetRowIndex++, item, rb); + } + autoSizeColumns(mainSheet, 0, ColumnIndex.INDEX_DATA.getIndex(isAdmin) - 1); + + // Create meta sheet for information about the download + Sheet metaSheet = workbook.createSheet(rb.getString("downloadInfo")); + addMetaInfo(metaSheet, user, now, fromStr, toStr, observations, headerStyle, rb); + + // Prepare list of observations for each type of pest + Map<Integer, List<ObservationListItem>> pestObservations = getObservationsForEachPest(observations); + + // Create sheets for each individual pest type + for (Integer pestId : pestObservations.keySet()) { + List<ObservationListItem> observationsForPest = pestObservations.get(pestId); + ObservationListItem firstObservationForPest = observationsForPest.get(0); + String pestName = firstObservationForPest.getOrganismName(); + Sheet pestSheet = workbook.createSheet(sanitizeSheetName(pestId, pestName)); + Row headerRow = createHeaderRow(isAdmin, pestSheet, headerStyle, rb); + + // Add column titles for observation data + Map<String, String> dataColumnTitles = getObservationDataColumnTitles(firstObservationForPest.getObservationDataSchema()); + int pestSheetColIndex = ColumnIndex.INDEX_DATA.getIndex(isAdmin); + for (String key : dataColumnTitles.keySet()) { + Cell cell = headerRow.createCell(pestSheetColIndex++); + cell.setCellStyle(headerStyle); + cell.setCellValue(dataColumnTitles.get(key)); + } + + int pestSheetRowIndex = 1; + for (ObservationListItem item : observationsForPest) { + Row row = createItemRow(isAdmin, pestSheet, pestSheetRowIndex++, item, rb); + + if (item.getObservationData() != null) { + Map<String, Object> observationDataMap = objectMapper.readValue(item.getObservationData(), HashMap.class); + if (observationDataMap != null) { + pestSheetColIndex = ColumnIndex.INDEX_DATA.getIndex(isAdmin); + for (String key : dataColumnTitles.keySet()) { + pestSheetColIndex = addValueToCell(row, pestSheetColIndex, observationDataMap.get(key)); + } + } + } + } + autoSizeColumns(pestSheet, ColumnIndex.ID.getIndex(isAdmin), ColumnIndex.INDEX_DATA.getIndex(isAdmin) + dataColumnTitles.size()); + } + + workbook.write(out); + return out.toByteArray(); + } + } + + /** + * Add meta information to given sheet + * + * @param metaSheet The sheet in which to add content + * @param user The current user + * @param now The current timestamp + * @param fromStr The start of the period for which we have observations + * @param toStr The end of the period for which we have observations + * @param observations The list of observations + * @param headerStyle How to style the title cells + * @param rb Resource bundle with translations + */ + private static void addMetaInfo(Sheet metaSheet, VipsLogicUser user, LocalDateTime now, String fromStr, String toStr, List<ObservationListItem> observations, CellStyle headerStyle, ResourceBundle rb) { + Row userRow = metaSheet.createRow(0); + Cell downloadedByCell = userRow.createCell(0); + downloadedByCell.setCellStyle(headerStyle); + downloadedByCell.setCellValue(rb.getString("downloadedBy")); + userRow.createCell(1).setCellValue(user != null ? user.getFullName() : rb.getString("unregisteredUser")); + Row timeRow = metaSheet.createRow(1); + Cell downloadedTimeCell = timeRow.createCell(0); + downloadedTimeCell.setCellStyle(headerStyle); + downloadedTimeCell.setCellValue(rb.getString("downloadedTime")); + timeRow.createCell(1).setCellValue(DATE_TIME_FORMATTER.format(now)); + Row fromRow = metaSheet.createRow(2); + Cell dateFromCell = fromRow.createCell(0); + dateFromCell.setCellStyle(headerStyle); + dateFromCell.setCellValue(rb.getString("dateStart")); + fromRow.createCell(1).setCellValue(fromStr); + Row toRow = metaSheet.createRow(3); + Cell dateToCell = toRow.createCell(0); + dateToCell.setCellStyle(headerStyle); + dateToCell.setCellValue(rb.getString("dateEnd")); + toRow.createCell(1).setCellValue(toStr); + Row countRow = metaSheet.createRow(4); + Cell countCell = countRow.createCell(0); + countCell.setCellStyle(headerStyle); + countCell.setCellValue(rb.getString("observationCount")); + countRow.createCell(1).setCellValue(observations.size()); + autoSizeColumns(metaSheet, 0, 1); + } + + /** + * Create sheet name without invalid characters and within size limits + * + * @param pestId The id of the pest + * @param pestName The name of the pest + * @return a sanitized string to be used as sheet name + */ + private static String sanitizeSheetName(Integer pestId, String pestName) { + if (pestName == null || pestName.isBlank()) { + return "Id=" + pestId; + } + return pestName.replaceAll("[\\[\\]\\*/:?\\\\]", "_").substring(0, Math.min(pestName.length(), 31)); + } + + + /** + * Auto-size columns of given sheet, from startIndex to endIndex, to ensure that content fits + * + * @param sheet The sheet of which to auto-size columns + * @param startIndex The index of the first column to auto-size + * @param endIndex The index of the last column to auto-size + */ + private static void autoSizeColumns(Sheet sheet, int startIndex, int endIndex) { + for (int i = startIndex; i <= endIndex; i++) { + sheet.autoSizeColumn(i); + } + } + + /** + * Create map with data property name as key, and corresponding localized name as value + * + * @param observationDataSchema The observation data schema which contains localized names and other info + * @return result map + */ + private static Map<String, String> getObservationDataColumnTitles(ObservationDataSchema observationDataSchema) throws JsonProcessingException { + JsonNode rootNode = objectMapper.readTree(observationDataSchema.getDataSchema()); + JsonNode propertiesNode = rootNode.path("properties"); + Map<String, String> resultMap = new HashMap<>(); + Iterator<Map.Entry<String, JsonNode>> fields = propertiesNode.fields(); + while (fields.hasNext()) { + Map.Entry<String, JsonNode> field = fields.next(); + String key = field.getKey(); + String value = field.getValue().path("title").asText(); + resultMap.put(key, value); + } + return resultMap; + } + + /** + * Create map with pestId as key, and list of corresponding observations as value + * + * @param observations The original list of observations + * @return result map + */ + private static Map<Integer, List<ObservationListItem>> getObservationsForEachPest(List<ObservationListItem> observations) { + Map<Integer, List<ObservationListItem>> pestObservations = new HashMap<>(); + for (ObservationListItem observation : observations) { + Integer pestId = observation.getOrganismId(); + if (!pestObservations.containsKey(pestId)) { + List<ObservationListItem> observationList = new ArrayList<>(); + observationList.add(observation); + pestObservations.put(pestId, observationList); + } else { + pestObservations.get(pestId).add(observation); + } + } + return pestObservations; + } + + /** + * Create first row of given sheet, with column titles dependent on user privileges + * + * @param isAdmin Whether the user is a logged in admin + * @param sheet The sheet to which a row will be added + * @param headerStyle The style to be applied to each cell in the header row + * @param rb A resource bundle enabling localized messages + * @return the newly created header row + */ + public static Row createHeaderRow(boolean isAdmin, Sheet sheet, CellStyle headerStyle, ResourceBundle rb) { + Row headerRow = sheet.createRow(0); + for (ColumnIndex columnIndex : ColumnIndex.forUser(isAdmin)) { + Cell cell = headerRow.createCell(columnIndex.getIndex(isAdmin)); + cell.setCellStyle(headerStyle); + cell.setCellValue(columnIndex.getColumnHeading(rb)); + } + return headerRow; + } + + /** + * Create row with given index, for given observation list item + * + * @param isAdmin Whether the user is a logged in admin + * @param sheet The sheet to which a row will be added + * @param rowIndex The index of the row + * @param item The item of which to add data + * @return the newly created row + */ + private static Row createItemRow(boolean isAdmin, Sheet sheet, int rowIndex, ObservationListItem item, ResourceBundle rb) throws JsonProcessingException { + LocalDate localDateOfObservation = item.getTimeOfObservation().toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); + Row row = sheet.createRow(rowIndex); + addObservationLink(row, ColumnIndex.ID.getIndex(isAdmin), item.getObservationId()); + addValueToCell(row, ColumnIndex.DATE.getIndex(isAdmin), localDateOfObservation.format(DATE_FORMATTER)); + + if (item.getLocationPointOfInterestId() != null) { + Integer poiNameIndex = ColumnIndex.POI_NAME.getIndex(isAdmin); + if(isAdmin) { + addPoiLink(row, poiNameIndex, item.getLocationPointOfInterestId(), item.getLocationPointOfInterestName()); + } else { + addValueToCell(row, poiNameIndex, item.getLocationPointOfInterestName()); + } + } + + if (isAdmin) { + addUserLink(row, ColumnIndex.OBSERVER_NAME.getIndex(isAdmin), item.getObserverId(), item.getObserverName()); + } + if (item.getObservationTimeSeriesId() != null) { + addTimeSeriesLink(row, ColumnIndex.OBSERVATION_TIME_SERIES_LABEL.getIndex(isAdmin), item.getObservationTimeSeriesId(), item.getObservationTimeSeriesLabel()); + } + addValueToCell(row, ColumnIndex.ORGANISM.getIndex(isAdmin), item.getOrganismName()); + addValueToCell(row, ColumnIndex.CROP_ORGANISM.getIndex(isAdmin), item.getCropOrganismName()); + addValueToCell(row, ColumnIndex.HEADING.getIndex(isAdmin), item.getObservationHeading()); + addValueToCell(row, ColumnIndex.DESCRIPTION.getIndex(isAdmin), item.getObservationText()); + + addValueToCell(row, ColumnIndex.BROADCAST.getIndex(isAdmin), getBooleanStringValue(rb, item.getBroadcastMessage())); + addValueToCell(row, ColumnIndex.POSITIVE.getIndex(isAdmin), getBooleanStringValue(rb, item.getIsPositive())); + return row; + } + + /** + * Add value to cell + * + * @param row The row to which the value should be added + * @param colIndex The index of the column to add the value to + * @param value The value + * @return The next index value + */ + private static Integer addValueToCell(Row row, Integer colIndex, Object value) { + Integer index = colIndex; + if (value == null) { + row.createCell(index++).setCellValue(""); + } else if (value instanceof Number) { + row.createCell(index++).setCellValue(((Number) value).intValue()); + } else { + try { + int intValue = Integer.parseInt(value.toString()); + row.createCell(index++).setCellValue(intValue); + } catch (NumberFormatException e) { + row.createCell(index++).setCellValue(value.toString()); + } + } + return index; + } + + /** + * Get a localized String representing either true or false + * + * @param rb The resource bundle to get the localized string from + * @param value Either true or false + * @return A localized String + */ + private static String getBooleanStringValue(ResourceBundle rb, Boolean value) { + if (value == null) { + return null; + } else if (value) { + return rb.getString("yes"); + } + return rb.getString("no"); + } + + /** + * Add link to observation details in column containing the observation Id + * + * @param row A reference to the current row + * @param colIndex The index of the column in which the link should be added + * @param observationId The id of the observation + */ + private static void addObservationLink(Row row, Integer colIndex, Integer observationId) { + Cell cell = row.createCell(colIndex); + cell.setCellValue(observationId); + + Workbook workbook = row.getSheet().getWorkbook(); + cell.setHyperlink(createHyperlink(workbook, VIPSWEB + "/observations/" + observationId)); + cell.setCellStyle(hyperlinkCellStyle(workbook)); + } + + /** + * Add link to timeseries details in column with given index + * + * @param row A reference to the current row + * @param colIndex The index of the column in which the link should be added + * @param timeSeriesId The id of the timeseries + * @param timeSeriesLabel The text which should be displayed in the cell + */ + private static void addTimeSeriesLink(Row row, Integer colIndex, Integer timeSeriesId, String timeSeriesLabel) { + Cell cell = row.createCell(colIndex); + cell.setCellValue(timeSeriesLabel); + + Workbook workbook = row.getSheet().getWorkbook(); + cell.setHyperlink(createHyperlink(workbook, VIPSWEB + "/observations/timeseries/" + timeSeriesId)); + cell.setCellStyle(hyperlinkCellStyle(workbook)); + } + + /** + * Add link to poi details in column with given index + * + * @param row A reference to the current row + * @param colIndex The index of the column in which the link should be added + * @param poiId The id of the poi + * @param poiName The text which should be displayed in the cell + */ + private static void addPoiLink(Row row, Integer colIndex, Integer poiId, String poiName) { + Cell cell = row.createCell(colIndex); + cell.setCellValue(poiName); + + Workbook workbook = row.getSheet().getWorkbook(); + cell.setHyperlink(createHyperlink(workbook, VIPSLOGIC + "/weatherStation?pointOfInterestId=" + poiId)); + cell.setCellStyle(hyperlinkCellStyle(workbook)); + } + + /** + * Add link to user details in column with given index + * + * @param row A reference to the current row + * @param colIndex The index of the column in which the link should be added + * @param userId The id of the user + * @param userName The text which should be displayed in the cell + */ + private static void addUserLink(Row row, Integer colIndex, Integer userId, String userName) { + Cell cell = row.createCell(colIndex); + cell.setCellValue(userName); + + Workbook workbook = row.getSheet().getWorkbook(); + cell.setHyperlink(createHyperlink(workbook, VIPSLOGIC + "/user?action=viewUser&userId=" + userId)); + cell.setCellStyle(hyperlinkCellStyle(workbook)); + } + + private static Hyperlink createHyperlink(Workbook workbook, String url) { + CreationHelper creationHelper = workbook.getCreationHelper(); + Hyperlink hyperlink = creationHelper.createHyperlink(HyperlinkType.URL); + hyperlink.setAddress(url); + return hyperlink; + } + + private static CellStyle hyperlinkCellStyle(Workbook workbook) { + CellStyle hyperlinkStyle = workbook.createCellStyle(); + Font hlinkFont = workbook.createFont(); + hlinkFont.setUnderline(Font.U_SINGLE); + hlinkFont.setColor(IndexedColors.BLUE.getIndex()); + hyperlinkStyle.setFont(hlinkFont); + return hyperlinkStyle; + } + +} 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 80a357d25a61df2d96e5e3f7fbbfb66433f71d15..532895e14fbd20b5fa9b871d174e0ab5b463168f 100755 --- a/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts.properties +++ b/src/main/resources/no/nibio/vips/logic/i18n/vipslogictexts.properties @@ -1,21 +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. - # This program is free software: you can redistribute it and/or modify - # it under the terms of the GNU Affero General Public License as published by - # the Free Software Foundation, either version 3 of the License, or - # (at your option) any later version. - # - # This program is distributed in the hope that it will be useful, - # but WITHOUT ANY WARRANTY; without even the implied warranty of - # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - # GNU Affero General Public License for more details. - # - # You should have received a copy of the GNU Affero General Public License - # along with this program. If not, see <https://www.gnu.org/licenses/>. - # +# Copyright (c) 2014 NIBIO <http://www.nibio.no/>. +# +# This file is part of VIPSLogic. +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. +# ALTERNARIA = Alternaria Model APPLESCABM = Apple scab model @@ -391,7 +391,9 @@ isPest = Is pest isPrivate = Is private isQuantified = Is quantified + isPositiveRegistration=Pest presence confirmed + isRequiredField = Required field language = Language @@ -556,6 +558,8 @@ observationDataField_trapCountCropInside = Number of trap counts inside the fiel observationDataField_unit = Measuring unit +allObservations = All observations + observationDeleted = Observation was deleted observationHeading = Observation heading @@ -592,7 +596,7 @@ observationSiteStored = Observation site was successfully updated observationStored = Observation was stored -observationText = Observation text +observationText = Description observations = Observations @@ -600,6 +604,8 @@ observedDateOfFirstCatch = Observed date of first catch observedValue = Observed value +observerId = Observer ID + observer = Observer older = Older @@ -1052,3 +1058,15 @@ privacyStatement=Privacy statement privacyStatementFileName=Privacy_statement_NIBIO-VIPS.pdf thresholdDSVMax=DSV threshold for high infection risk thresholdDSVTempMin=Minimum temperature for DSV calculation +observationTimeSeriesId=Timeseries +observationTimeSeriesLabel=Timeseries label +observationId=Observation +isBroadcast=Is broadcast +yes=Yes +no=No +downloadInfo=About download +downloadedBy=Downloaded by +unregisteredUser=Unregistered user +downloadedTime=Time of download +observationCount=Observation count + 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 1e03844cc22e2c12fe721b2eb34d16f56e097f44..24728fee46f5f10c94ffa13944a6227f467095b6 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,20 +1,20 @@ # - # Copyright (c) 2014 NIBIO <http://www.nibio.no/>. - # - # This file is part of VIPSLogic. - # This program is free software: you can redistribute it and/or modify - # it under the terms of the GNU Affero General Public License as published by - # the Free Software Foundation, either version 3 of the License, or - # (at your option) any later version. - # - # This program is distributed in the hope that it will be useful, - # but WITHOUT ANY WARRANTY; without even the implied warranty of - # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - # GNU Affero General Public License for more details. - # - # You should have received a copy of the GNU Affero General Public License - # along with this program. If not, see <https://www.gnu.org/licenses/>. - # +# Copyright (c) 2014 NIBIO <http://www.nibio.no/>. +# +# This file is part of VIPSLogic. +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <https://www.gnu.org/licenses/>. +# isPositiveRegistration=Skadegj\u00f8rer p\u00e5vist ALTERNARIA = Alternariamodell @@ -354,7 +354,7 @@ greeting = Velkommen til groupMembers = Gruppemedlemmer -heading = Overskrift +heading = Tittel help = Hjelp @@ -420,13 +420,13 @@ listSelectedCropCategoryOnTop = List kulturer fra valgt gruppe \u00f8verst localName = Lokalt navn -location = Plassering +location=Sted -locationIsPrivate = Lokalitet skal ikke offentliggj\u00f8res +locationIsPrivate = Sted skal ikke offentliggj\u00f8res -locationIsPublic = Lokaliteten kan vises offentlig +locationIsPublic = Sted kan vises offentlig -locationPointOfInterestId = Lokalitet +locationPointOfInterestId=Sted logInterval = M\u00e5leintervall @@ -556,9 +556,11 @@ observationDataField_trapCountCropInside = Antall insekter, fellefangst inne i f observationDataField_unit = M\u00e5leenhet +allObservations = Alle observasjoner + observationDeleted = Observasjonen ble slettet -observationHeading = Observasjons-overskrift +observationHeading = Tittel observationMap = Observasjonskart @@ -592,7 +594,7 @@ observationSiteStored = Rogneb\u00e6rm\u00f8llstasjonen ble oppdatert observationStored = Observasjonen ble lagret -observationText = Observasjonstekst +observationText = Beskrivelse observations = Observasjoner/f\u00f8rstefunn @@ -600,6 +602,8 @@ observedDateOfFirstCatch = Observert dato for f\u00f8rste fellefangst observedValue = Observert verdi +observerId = Observat\u00f8r-Id + observer = Observat\u00f8r older = Eldre @@ -894,7 +898,7 @@ thresholdRelativeHumidity = Terskelverdi relativ luftfuktighet (%) tillageMethod = Jordarbeiding -timeOfObservation = Observasjonstidspunkt +timeOfObservation = Observasjonsdato timeZone = Tidssone @@ -1030,6 +1034,7 @@ weatherStations = M\u00e5lestasjoner xIsNotAfterY = {0} er ikke etter {1} + xIsNotEqualToY = {0} er ikke lik {1} your = Din @@ -1052,3 +1057,15 @@ privacyStatement=Personvernerkl\u00e6ring privacyStatementFileName=Personvernerklaering_NIBIO-VIPS.pdf thresholdDSVMax=DSV-terskel for h\u00f8y infeksjonsrisiko thresholdDSVTempMin=Minimumstemperatur for beregning av DSV +observationTimeSeriesId=Tidsserie-Id +observationTimeSeriesLabel=Tidsserie +observationId=Observasjon-Id +isBroadcast=Er kringkastet +yes=Ja +no=Nei +downloadInfo=Om nedlastingen +downloadedBy=Lastet ned av +unregisteredUser=Uregistrert bruker +downloadedTime=Tidspunkt for nedlasting +observationCount=Antall observasjoner + diff --git a/src/test/resources/HTMLFormGeneratorTest/forecastConfigurationForm_result.html b/src/test/resources/HTMLFormGeneratorTest/forecastConfigurationForm_result.html index 92f82d74c599925b4fa6869a9f780a92c10e9558..4b2e97c64a691fd9ffe8d896aba1cf56feb0c8cb 100755 --- a/src/test/resources/HTMLFormGeneratorTest/forecastConfigurationForm_result.html +++ b/src/test/resources/HTMLFormGeneratorTest/forecastConfigurationForm_result.html @@ -17,7 +17,7 @@ <span class="help-block" id="forecastConfigurationForm_modelId_validation"></span> </div> <div class="form-group"> -<label for="locationPointOfInterestId">Lokalitet</label> +<label for="locationPointOfInterestId">Sted</label> <select class="form-control" name="locationPointOfInterestId" onblur="validateField(this);"> <option value="-1">Vennligst velg målestasjon</option> <option value="38">Ullensvang</option>