Skip to content

Commit

Permalink
Merge pull request #1 from akaene/record-manager-ui#37-records-export
Browse files Browse the repository at this point in the history
Record manager UI#37 records export
  • Loading branch information
ledsoft authored Dec 13, 2023
2 parents 2807546 + bd2957b commit 3a35d84
Show file tree
Hide file tree
Showing 12 changed files with 447 additions and 51 deletions.
6 changes: 3 additions & 3 deletions src/main/java/cz/cvut/kbss/study/model/Institution.java
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,8 @@ public void setReferenceId(Integer referenceId) {

@Override
public String toString() {
return "Institution{" +
"name='" + name + '\'' +
"} " + super.toString();
return "Institution{<" + uri +
">, name='" + name + '\'' +
"}";
}
}
12 changes: 7 additions & 5 deletions src/main/java/cz/cvut/kbss/study/model/PatientRecord.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@
import cz.cvut.kbss.jopa.model.annotations.ParticipationConstraints;
import cz.cvut.kbss.study.model.qam.Question;
import cz.cvut.kbss.study.model.util.HasOwlKey;
import cz.cvut.kbss.study.model.util.HasUri;

import java.io.Serializable;
import java.net.URI;
import java.util.Date;

@OWLClass(iri = Vocabulary.s_c_patient_record)
public class PatientRecord implements Serializable, HasOwlKey {
public class PatientRecord implements Serializable, HasOwlKey, HasUri {

@Id
private URI uri;
Expand Down Expand Up @@ -49,9 +50,10 @@ public class PatientRecord implements Serializable, HasOwlKey {
private String formTemplate;

@OWLObjectProperty(iri = Vocabulary.s_p_has_question, cascade = {CascadeType.MERGE,
CascadeType.REMOVE}, fetch = FetchType.EAGER)
CascadeType.REMOVE}, fetch = FetchType.EAGER)
private Question question;

@Override
public URI getUri() {
return uri;
}
Expand Down Expand Up @@ -136,10 +138,10 @@ public void setFormTemplate(String formTemplate) {

@Override
public String toString() {
return "PatientRecord{" +
"localName=" + localName +
return "PatientRecord{<" + uri +
">, localName=" + localName +
", dateCreated=" + dateCreated +
", institution=" + institution +
"} " + super.toString();
"}";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@

import java.math.BigInteger;
import java.net.URI;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneOffset;
import java.util.List;
import java.util.Objects;

Expand Down Expand Up @@ -164,4 +167,71 @@ public void requireUniqueNonEmptyLocalName(PatientRecord entity) {
}
em.clear();
}

/**
* Retrieves records modified (created or modified) in the specified time interval.
* <p>
* Since the record modification is tracked by a timestamp and the arguments here are dates, this method uses
* beginning of the min date and end of the max date.
*
* @param minDate Minimum date of modification of matching records, inclusive
* @param maxDate Maximum date of modification of matching records, inclusive
* @return List of matching records
*/
public List<PatientRecord> findAllFull(LocalDate minDate, LocalDate maxDate) {
Objects.requireNonNull(minDate);
Objects.requireNonNull(maxDate);

final Instant min = minDate.atStartOfDay(ZoneOffset.UTC).toInstant();
final Instant max = maxDate.plusDays(1).atStartOfDay(ZoneOffset.UTC).toInstant();

return em.createNativeQuery("SELECT ?r WHERE {" +
"?r a ?type ;" +
"?hasCreatedDate ?created ." +
"OPTIONAL { ?r ?hasLastModified ?lastModified . }" +
"BIND (IF (BOUND(?lastModified), ?lastModified, ?created) AS ?edited)" +
"FILTER (?edited >= ?minDate && ?edited < ?maxDate)" +
"} ORDER BY DESC(?edited)", PatientRecord.class)
.setParameter("type", typeUri)
.setParameter("hasCreatedDate", URI.create(Vocabulary.s_p_created))
.setParameter("hasLastModified", URI.create(Vocabulary.s_p_modified))
.setParameter("minDate", min)
.setParameter("maxDate", max).getResultList();
}

/**
* Retrieves records modified (created or modified) in the specified time interval.
* <p>
* Since the record modification is tracked by a timestamp and the arguments here are dates, this method uses
* beginning of the min date and end of the max date.
*
* @param institution Institution with which matching records have to be associated
* @param minDate Minimum date of modification of matching records, inclusive
* @param maxDate Maximum date of modification of matching records, inclusive
* @return List of matching records
*/
public List<PatientRecord> findAllFull(Institution institution, LocalDate minDate, LocalDate maxDate) {
Objects.requireNonNull(institution);
Objects.requireNonNull(minDate);
Objects.requireNonNull(maxDate);

final Instant min = minDate.atStartOfDay(ZoneOffset.UTC).toInstant();
final Instant max = maxDate.plusDays(1).atStartOfDay(ZoneOffset.UTC).toInstant();

return em.createNativeQuery("SELECT ?r WHERE {" +
"?r a ?type ; " +
"?hasCreatedDate ?created ; " +
"?hasInstitution ?institution . " +
"OPTIONAL { ?r ?hasLastModified ?lastModified . } " +
"BIND (IF (BOUND(?lastModified), ?lastModified, ?created) AS ?edited) " +
"FILTER (?edited >= ?minDate && ?edited < ?maxDate)" +
"} ORDER BY DESC(?edited)", PatientRecord.class)
.setParameter("type", typeUri)
.setParameter("hasInstitution", URI.create(Vocabulary.s_p_was_treated_at))
.setParameter("institution", institution)
.setParameter("hasCreatedDate", URI.create(Vocabulary.s_p_created))
.setParameter("hasLastModified", URI.create(Vocabulary.s_p_modified))
.setParameter("minDate", min)
.setParameter("maxDate", max).getResultList();
}
}
39 changes: 33 additions & 6 deletions src/main/java/cz/cvut/kbss/study/rest/PatientRecordController.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,25 @@
import cz.cvut.kbss.study.security.SecurityConstants;
import cz.cvut.kbss.study.service.InstitutionService;
import cz.cvut.kbss.study.service.PatientRecordService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalDate;
import java.util.List;
import java.util.Optional;

@RestController
@PreAuthorize("hasRole('" + SecurityConstants.ROLE_USER + "')")
Expand All @@ -35,16 +45,33 @@ public PatientRecordController(PatientRecordService recordService, InstitutionSe

@PreAuthorize("hasRole('" + SecurityConstants.ROLE_ADMIN + "') or @securityUtils.isMemberOfInstitution(#institutionKey)")
@GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
public List<PatientRecordDto> getRecords(@RequestParam(value = "institution", required = false) String institutionKey) {
return institutionKey != null ? findByInstitution(institutionKey) : recordService.findAllRecords();
public List<PatientRecordDto> getRecords(
@RequestParam(value = "institution", required = false) String institutionKey) {
return institutionKey != null ? recordService.findByInstitution(getInstitution(institutionKey)) :
recordService.findAllRecords();
}

private List<PatientRecordDto> findByInstitution(String institutionKey) {
private Institution getInstitution(String institutionKey) {
final Institution institution = institutionService.findByKey(institutionKey);
if (institution == null) {
throw NotFoundException.create("Institution", institutionKey);
}
return recordService.findByInstitution(institution);
return institution;
}

@PreAuthorize(
"hasRole('" + SecurityConstants.ROLE_ADMIN + "') or @securityUtils.isMemberOfInstitution(#institutionKey)")
@GetMapping(value = "/export", produces = MediaType.APPLICATION_JSON_VALUE)
public List<PatientRecord> exportRecords(
@RequestParam(value = "institution", required = false) String institutionKey,
@RequestParam(name = "minDate", required = false) Optional<LocalDate> minDateParam,
@RequestParam(name = "maxDate", required = false) Optional<LocalDate> maxDateParam) {
final LocalDate minDate = minDateParam.orElse(LocalDate.EPOCH);
final LocalDate maxDate = maxDateParam.orElse(LocalDate.now());
if (institutionKey != null) {
return recordService.findAllFull(getInstitution(institutionKey), minDate, maxDate);
}
return recordService.findAllFull(minDate, maxDate);
}

@PreAuthorize("hasRole('" + SecurityConstants.ROLE_ADMIN + "') or @securityUtils.isRecordInUsersInstitution(#key)")
Expand Down
25 changes: 22 additions & 3 deletions src/main/java/cz/cvut/kbss/study/rest/util/RestUtils.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
package cz.cvut.kbss.study.rest.util;

import cz.cvut.kbss.study.exception.WebServiceIntegrationException;
import cz.cvut.kbss.study.util.Constants;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;

import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.time.format.DateTimeParseException;

public class RestUtils {

Expand Down Expand Up @@ -79,4 +80,22 @@ public static String getCookie(HttpServletRequest request, String cookieName) {
}
return null;
}

/**
* Parses the specified date string.
* <p>
* The parameter is expected to be in the ISO format.
*
* @param dateStr Date string
* @return {@code LocalDate} object corresponding to the specified date string
* @throws ResponseStatusException Bad request is thrown if the date string is not parseable
*/
public static LocalDate parseDate(String dateStr) {
try {
return LocalDate.parse(dateStr);
} catch (DateTimeParseException | NullPointerException e) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Value '" + dateStr + "' is not a valid date in ISO format.");
}
}

}
28 changes: 28 additions & 0 deletions src/main/java/cz/cvut/kbss/study/service/PatientRecordService.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import cz.cvut.kbss.study.model.PatientRecord;
import cz.cvut.kbss.study.model.User;

import java.time.LocalDate;
import java.util.List;

public interface PatientRecordService extends BaseService<PatientRecord> {
Expand Down Expand Up @@ -39,4 +40,31 @@ public interface PatientRecordService extends BaseService<PatientRecord> {
* @return Records of matching patients
*/
List<PatientRecordDto> findAllRecords();

/**
* Finds all records that were created or modified in the specified date interval.
* <p>
* In contrast to {@link #findAll()}, this method returns full records, not DTOs.
*
* @param minDate Minimum date of modification of returned records, inclusive
* @param maxDate Maximum date of modification of returned records, inclusive
* @return List of matching records
* @see #findAllFull(Institution, LocalDate, LocalDate)
* @see #findAllRecords()
*/
List<PatientRecord> findAllFull(LocalDate minDate, LocalDate maxDate);

/**
* Finds all records that were created or modified at the specified institution in the specified date interval.
* <p>
* In contrast to {@link #findByInstitution(Institution)}, this method returns full records, not DTOs.
*
* @param institution Institution with which the records are associated
* @param minDate Minimum date of modification of returned records, inclusive
* @param maxDate Maximum date of modification of returned records, inclusive
* @return List of matching records
* @see #findAllFull(LocalDate, LocalDate)
* @see #findByInstitution(Institution)
*/
List<PatientRecord> findAllFull(Institution institution, LocalDate minDate, LocalDate maxDate);
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDate;
import java.util.Date;
import java.util.List;

Expand Down Expand Up @@ -52,6 +53,18 @@ public List<PatientRecordDto> findAllRecords() {
return recordDao.findAllRecords();
}

@Transactional(readOnly = true)
@Override
public List<PatientRecord> findAllFull(LocalDate minDate, LocalDate maxDate) {
return recordDao.findAllFull(minDate, maxDate);
}

@Transactional(readOnly = true)
@Override
public List<PatientRecord> findAllFull(Institution institution, LocalDate minDate, LocalDate maxDate) {
return recordDao.findAllFull(institution, minDate, maxDate);
}

@Override
protected void prePersist(PatientRecord instance) {
final User author = securityUtils.getCurrentUser();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* TermIt
* Copyright (C) 2023 Czech Technical University in Prague
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package cz.cvut.kbss.study.environment.util;

import cz.cvut.kbss.study.model.util.HasUri;
import org.hamcrest.Description;
import org.hamcrest.TypeSafeMatcher;

import java.util.Collection;
import java.util.Objects;

/**
* Checks whether the provided collection contains the same entities as the expected one.
* <p>
* The membership check is done based on entity URIs.
*/
public class ContainsSameEntities extends TypeSafeMatcher<Collection<? extends HasUri>> {

private final Collection<? extends HasUri> expected;

public ContainsSameEntities(Collection<? extends HasUri> expected) {
this.expected = Objects.requireNonNull(expected);
}

@Override
protected boolean matchesSafely(Collection<? extends HasUri> actual) {
if (actual == null || actual.size() != expected.size()) {
return false;
}
for (HasUri e : expected) {
if (actual.stream().noneMatch(ee -> Objects.equals(e.getUri(), ee.getUri()))) {
return false;
}
}
return true;
}

@Override
public void describeTo(Description description) {
description.appendValueList("[", ", ", "]", expected);
}

public static ContainsSameEntities containsSameEntities(Collection<? extends HasUri> expected) {
return new ContainsSameEntities(expected);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ public class Environment {

private static ObjectMapper objectMapper;

public static long MILLIS_PER_DAY = 24 * 3600 * 1000L;

private Environment() {
throw new AssertionError();
}
Expand Down
Loading

0 comments on commit 3a35d84

Please sign in to comment.