Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[JN-1409] Document library [participant UX] #1369

Merged
merged 23 commits into from
Jan 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/java-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ jobs:
# Run DSP App Sec Trivy Scanner on PRs
dispatch-trivy:
needs: [ build ]
runs-on: ubuntu-latest
runs-on: ubuntu-22.04

if: github.event_name == 'pull_request'

Expand All @@ -110,7 +110,7 @@ jobs:

dispatch-tag:
needs: [ build ]
runs-on: ubuntu-latest
runs-on: ubuntu-22.04

# Only tag a new release if prior steps were successful and triggering commit is a merge to mainline
if: success() && github.ref == 'refs/heads/development'
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/java-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ env:

jobs:
get-version-tag:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
outputs:
tag: ${{ steps.tag.outputs.tag }}
steps:
Expand All @@ -29,7 +29,7 @@ jobs:
permissions:
contents: 'read'
id-token: 'write'
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
outputs:
tag: ${{ steps.build-publish.outputs.published-image }}
steps:
Expand Down Expand Up @@ -64,7 +64,7 @@ jobs:
permissions:
contents: 'read'
id-token: 'write'
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
outputs:
tag: ${{ steps.build-publish.outputs.published-image }}
steps:
Expand Down Expand Up @@ -119,7 +119,7 @@ jobs:
id-token: 'write'

notify-upon-completion:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
if: always()
needs: [set-version-in-dev, get-version-tag]
steps:
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/juniper-eng-infra-upload-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ env:

jobs:
get-version-tag:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
outputs:
tag: ${{ steps.tag.outputs.tag }}
steps:
Expand All @@ -28,7 +28,7 @@ jobs:
permissions:
contents: 'read'
id-token: 'write'
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
outputs:
tag: ${{ steps.juniper-eng-build-publish.outputs.published-image }}
steps:
Expand All @@ -50,7 +50,7 @@ jobs:
permissions:
contents: 'read'
id-token: 'write'
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
outputs:
tag: ${{ steps.juniper-eng-build-publish.outputs.published-image }}
steps:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/require-ticket.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:

jobs:
require_ticket:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
steps:
- name: Check for ticket in PR title
id: validate
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/sonar.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ on:
jobs:
sonar-java:
name: SonarCloud Java
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
with:
Expand Down Expand Up @@ -50,7 +50,7 @@ jobs:

sonar-typescript:
name: SonarCloud TypeScript
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
strategy:
matrix:
subproject: ['ui-admin', 'ui-core', 'ui-participant']
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/tag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ on:

jobs:
tag-job:
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
outputs:
tag: ${{ steps.tag.outputs.tag }}
steps:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package bio.terra.pearl.api.participant.controller.file;

import bio.terra.pearl.api.participant.api.ParticipantFileApi;
import bio.terra.pearl.api.participant.service.RequestUtilService;
import bio.terra.pearl.api.participant.service.file.ParticipantFileExtService;
import bio.terra.pearl.core.model.EnvironmentName;
import bio.terra.pearl.core.model.file.ParticipantFile;
import bio.terra.pearl.core.model.participant.ParticipantUser;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import java.io.InputStream;
import java.util.List;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.multipart.MultipartFile;

@Controller
public class ParticipantFileController implements ParticipantFileApi {
private final ParticipantFileExtService participantFileExtService;
private final HttpServletRequest request;
private final ObjectMapper objectMapper;
private final RequestUtilService requestUtilService;

public ParticipantFileController(
ParticipantFileExtService participantFileExtService,
HttpServletRequest request,
ObjectMapper objectMapper,
RequestUtilService requestUtilService) {
this.participantFileExtService = participantFileExtService;
this.request = request;
this.objectMapper = objectMapper;
this.requestUtilService = requestUtilService;
}

@Override
public ResponseEntity<Resource> download(
String portalShortcode,
String envName,
String studyShortcode,
Comment on lines +40 to +42
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh also, I think we should do a check to verify that the participant is in this study env

String enrolleeShortcode,
String fileName) {
ParticipantUser participantUser = requestUtilService.requireUser(request);

ParticipantFile participantFile =
participantFileExtService.get(
portalShortcode,
EnvironmentName.valueOf(envName),
participantUser,
enrolleeShortcode,
fileName);

InputStream content =
participantFileExtService.downloadFile(
portalShortcode,
EnvironmentName.valueOf(envName),
participantUser,
enrolleeShortcode,
fileName);

MediaType mediaType;
try {
mediaType = MediaType.parseMediaType(participantFile.getFileType());
} catch (Exception e) {
mediaType = MediaType.APPLICATION_OCTET_STREAM;
}

return ResponseEntity.ok().contentType(mediaType).body(new InputStreamResource(content));
}

@Override
public ResponseEntity<Object> upload(
String portalShortcode,
String envName,
String studyShortcode,
String enrolleeShortcode,
MultipartFile participantFile) {
ParticipantUser participantUser = requestUtilService.requireUser(request);

ParticipantFile created =
participantFileExtService.uploadFile(
portalShortcode,
EnvironmentName.valueOf(envName),
participantUser,
enrolleeShortcode,
participantFile);

return ResponseEntity.ok(created);
}

@Override
public ResponseEntity<Object> list(
String portalShortcode, String envName, String studyShortcode, String enrolleeShortcode) {
ParticipantUser participantUser = requestUtilService.requireUser(request);

List<ParticipantFile> participantFiles =
participantFileExtService.list(
portalShortcode, EnvironmentName.valueOf(envName), participantUser, enrolleeShortcode);
return ResponseEntity.ok(participantFiles);
}

@Override
public ResponseEntity<Object> delete(
String portalShortcode,
String envName,
String studyShortcode,
String enrolleeShortcode,
String fileName) {
ParticipantUser participantUser = requestUtilService.requireUser(request);

participantFileExtService.delete(
portalShortcode,
EnvironmentName.valueOf(envName),
participantUser,
enrolleeShortcode,
fileName);

return ResponseEntity.noContent().build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package bio.terra.pearl.api.participant.service.file;

import bio.terra.pearl.api.participant.service.AuthUtilService;
import bio.terra.pearl.core.model.EnvironmentName;
import bio.terra.pearl.core.model.file.ParticipantFile;
import bio.terra.pearl.core.model.participant.Enrollee;
import bio.terra.pearl.core.model.participant.ParticipantUser;
import bio.terra.pearl.core.service.exception.NotFoundException;
import bio.terra.pearl.core.service.file.ParticipantFileService;
import bio.terra.pearl.core.service.file.backends.FileStorageBackend;
import bio.terra.pearl.core.service.file.backends.FileStorageBackendProvider;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.Set;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

@Service
@Slf4j
public class ParticipantFileExtService {

private final ParticipantFileService participantFileService;
private final AuthUtilService authUtilService;
private final FileStorageBackend fileStorageBackend;

public ParticipantFileExtService(
ParticipantFileService participantFileService,
AuthUtilService authUtilService,
FileStorageBackendProvider fileStorageBackendProvider) {
this.participantFileService = participantFileService;
this.authUtilService = authUtilService;
this.fileStorageBackend = fileStorageBackendProvider.get();
}

public ParticipantFile get(
String portalShortcode,
EnvironmentName envName,
ParticipantUser participantUser,
String enrolleeShortcode,
String fileName) {
authUtilService.authParticipantToPortal(participantUser.getId(), portalShortcode, envName);
Enrollee enrollee =
authUtilService.authParticipantUserToEnrollee(participantUser.getId(), enrolleeShortcode);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't have permissions on the PArticipantApi, but we probably should eventually move to annotation-based auth like the admin tool so logic like this gets more foolproof and easier to test. I'll make a ticket

return participantFileService
.findByEnrolleeIdAndFileName(enrollee.getId(), fileName)
.orElseThrow(() -> new NotFoundException("Could not find file"));
}

public InputStream downloadFile(
String portalShortcode,
EnvironmentName envName,
ParticipantUser participantUser,
String enrolleeShortcode,
String fileName) {
authUtilService.authParticipantToPortal(participantUser.getId(), portalShortcode, envName);
Enrollee enrollee =
authUtilService.authParticipantUserToEnrollee(participantUser.getId(), enrolleeShortcode);
ParticipantFile participantFile =
participantFileService
.findByEnrolleeIdAndFileName(enrollee.getId(), fileName)
.orElseThrow(() -> new NotFoundException("Could not find file"));

return fileStorageBackend.downloadFile(participantFile.getExternalFileId());
}

public ParticipantFile uploadFile(
String portalShortcode,
EnvironmentName envName,
ParticipantUser participantUser,
String enrolleeShortcode,
MultipartFile file) {
authUtilService.authParticipantToPortal(participantUser.getId(), portalShortcode, envName);
Enrollee enrollee =
authUtilService.authParticipantUserToEnrollee(participantUser.getId(), enrolleeShortcode);

try {
return participantFileService.uploadFileAndCreate(
ParticipantFile.builder()
.enrolleeId(enrollee.getId())
.fileName(getFileName(file.getOriginalFilename()))
.fileType(file.getContentType())
.build(),
file.getInputStream());
} catch (IOException e) {
throw new RuntimeException("Error uploading file");
}
}

public List<ParticipantFile> list(
String portalShortcode,
EnvironmentName envName,
ParticipantUser participantUser,
String enrolleeShortcode) {
authUtilService.authParticipantToPortal(participantUser.getId(), portalShortcode, envName);
Enrollee enrollee =
authUtilService.authParticipantUserToEnrollee(participantUser.getId(), enrolleeShortcode);
return participantFileService.findByEnrolleeId(enrollee.getId());
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this method could use a comment and possibly a rename, since I at first thought it was doing something similar to the cleanFileName in SiteContentImage.

// Returns the name of the file without the preceding path
public String getFileName(String fileName) {
if (fileName == null) {
return "";
}
String[] split = fileName.split("\\[/\\\\]");
return split[split.length - 1];
}

public void delete(
String portalShortcode,
EnvironmentName envName,
ParticipantUser participantUser,
String enrolleeShortcode,
String fileName) {
authUtilService.authParticipantToPortal(participantUser.getId(), portalShortcode, envName);
Enrollee enrollee =
authUtilService.authParticipantUserToEnrollee(participantUser.getId(), enrolleeShortcode);
ParticipantFile participantFile =
participantFileService
.findByEnrolleeIdAndFileName(enrollee.getId(), fileName)
.orElseThrow(() -> new NotFoundException("Could not find file"));

participantFileService.delete(participantFile.getId(), Set.of());
fileStorageBackend.deleteFile(participantFile.getExternalFileId());
}
}
Loading
Loading