Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
09f8ccd
Initial attempt at writing an upload controller.
ocielliottc Aug 14, 2024
de62272
Initial pass at collecting the report data in preparation for report …
ocielliottc Aug 15, 2024
eb07cd3
Attempt to standardize the files to be uploaded and how to access them.
ocielliottc Aug 15, 2024
3966af9
Handle the fact that CSV files will contain data for multiple members.
ocielliottc Aug 15, 2024
b35dbd0
Initialize timestamp of Stored on construction.
ocielliottc Aug 16, 2024
f6d1c21
Inital pass at adding an upload button for one of the required .csv f…
ocielliottc Aug 16, 2024
57792b7
Inital pass at a Report Data DTO.
ocielliottc Aug 16, 2024
97da0fb
Attempt at getting ReportDataDTO back from the ReportDataController.
ocielliottc Aug 16, 2024
738de8f
Indicate that the get should produce json.
ocielliottc Aug 16, 2024
94abf56
With these fixes, we can actually receive report data.
ocielliottc Aug 19, 2024
cc74bff
Added "Current Information" and "Position History".
ocielliottc Aug 19, 2024
771a795
Changes to allow me to test report data generation.
ocielliottc Aug 19, 2024
de94836
Support requesting report data for multiple members.
ocielliottc Aug 20, 2024
33f012f
Switch to using a review period instead of a date range.
ocielliottc Aug 21, 2024
a7f2348
Ignore IOException for now.
ocielliottc Aug 21, 2024
622aae8
Put the review period dates in and only return a single "Current Info…
ocielliottc Aug 21, 2024
dd92482
Generate markdown (currently not stored).
ocielliottc Aug 21, 2024
7278529
Initial code to upload markdown text to the google drive as a google …
ocielliottc Sep 2, 2024
0b62448
Support uploading multiple files at one time and handle errors properly.
ocielliottc Sep 2, 2024
8d66880
Add more information to the exception.
ocielliottc Sep 2, 2024
1fd1a59
Friendly format the review period date and added space between elements.
ocielliottc Sep 2, 2024
f7be83f
Initial pass at adding feedback to the report.
ocielliottc Sep 3, 2024
1393ac8
Use the TemplateQuestionServices for questions to answers, instead of…
ocielliottc Sep 3, 2024
7b3b43f
Added reviews and self-reviews.
ocielliottc Sep 4, 2024
6833f51
Merge branch 'develop' into feature-2571/merit-evaluation-report
ocielliottc Sep 4, 2024
fc9dcc9
Added support for the different types of questions and test data.
ocielliottc Sep 5, 2024
df15dbe
Copy from markdown automatically convert to a google doc.
ocielliottc Sep 5, 2024
9e78366
Added the national role entry for the current information.
ocielliottc Sep 5, 2024
23bfe9e
Fixed a minor error.
ocielliottc Sep 6, 2024
d8f9080
Create separate file selectors for the 3 types of files and no longer…
ocielliottc Sep 6, 2024
81ce7d4
Added permission to create merit reports.
ocielliottc Sep 6, 2024
1d65888
Clean up and simplify the code.
ocielliottc Sep 6, 2024
d6fc0e1
Modified how Kudos are returned so that we can include the sender name.
ocielliottc Sep 6, 2024
bc39797
Added tests for the report data controller.
ocielliottc Sep 9, 2024
9d724f9
Deal with dates in different formats and non-numeric symbols in salar…
ocielliottc Sep 9, 2024
b1850c9
Sort the questions by question number and added more newline characte…
ocielliottc Sep 9, 2024
bee7d06
Initial addition of employee hours.
ocielliottc Sep 9, 2024
3729119
Initial addition of employee hours.
ocielliottc Sep 9, 2024
fd002c9
Added a warning log message for data expiration.
ocielliottc Sep 9, 2024
9a177f4
Merge branch 'develop' into feature-2571/merit-evaluation-report
ocielliottc Sep 9, 2024
b941dc9
Added in fields from Feature 2583.
ocielliottc Sep 9, 2024
b6592b1
Gracefully handle csv parse errors.
ocielliottc Sep 9, 2024
1e7a626
Take the first element of the set of employee hours.
ocielliottc Sep 9, 2024
fc77f50
Synchronize the data services impl methods that modify the stored dat…
ocielliottc Sep 10, 2024
8628863
Minor cleanup and comments.
ocielliottc Sep 10, 2024
44f4849
Factor common code out into a CSV processor class.
ocielliottc Sep 10, 2024
1e337d9
Added missing override annotation.
ocielliottc Sep 10, 2024
3030207
Log errors while trashing documents that about to be replaced.
ocielliottc Sep 10, 2024
c00f63c
Converted classes to records.
ocielliottc Sep 16, 2024
908f1ee
Simplifications from review.
ocielliottc Sep 16, 2024
7601c96
Corrected handling of feedback requests relating to review period and…
ocielliottc Sep 16, 2024
1a87024
Added more data and validation of returned jSON.
ocielliottc Sep 17, 2024
22c8f89
Merge branch 'develop' into feature-2571/merit-evaluation-report
ocielliottc Sep 17, 2024
fe123cd
Merge branch 'develop' into feature-2571/merit-evaluation-report
mkimberlin Sep 17, 2024
06f91fe
Remove unused imports
timyates Sep 17, 2024
c69cda5
Update CompensationHistory and PositionHistory as we did with Current…
timyates Sep 17, 2024
cac00d7
Use specific assertion
timyates Sep 17, 2024
4fe2a03
CSVProcessor is an abstract class
timyates Sep 17, 2024
2fd300e
Merge branch 'develop' into feature-2571/merit-evaluation-report
mkimberlin Sep 18, 2024
97fd7af
Merge branch 'develop' into feature-2571/merit-evaluation-report
mkimberlin Sep 18, 2024
20fe29a
Removed duplicate review period.
ocielliottc Sep 18, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ public FileInfoDTO upload(@NotNull UUID checkInId, CompletedFileUpload file) {
return fileServices.uploadFile(checkInId, file);
}

@Post(consumes = MediaType.MULTIPART_FORM_DATA)
@Status(HttpStatus.CREATED)
public FileInfoDTO uploadDocument(String directory, String name, String text) {
return fileServices.uploadDocument(directory, name, text);
}

/**
* Delete a document from Google Drive
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ public interface FileServices {
Set<FileInfoDTO> findFiles(UUID checkInId);
File downloadFiles(String uploadDocId);
FileInfoDTO uploadFile(UUID checkInID, CompletedFileUpload file);
FileInfoDTO uploadDocument(String directory, String name, String text);
boolean deleteFile(String uploadDocId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.nio.charset.StandardCharsets;
import java.io.InputStream;
import java.io.ByteArrayInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Collections;
Expand Down Expand Up @@ -209,6 +212,101 @@ public FileInfoDTO uploadFile(@NotNull UUID checkInID, @NotNull CompletedFileUpl
}
}

/// Upload a Markdown document to the specified directory and copy it to
/// a Google document, which results in an automatic conversion from
/// Markdown to the Google Doc format.
public FileInfoDTO uploadDocument(String directoryName, String name, String text) {
final String GOOGLE_DOC_TYPE = "application/vnd.google-apps.document";
MemberProfile currentUser = currentUserServices.getCurrentUser();
boolean isAdmin = currentUserServices.isAdmin();
validate(!isAdmin, "You are not authorized to perform this operation");

try {
Drive drive = googleApiAccess.getDrive();
validate(drive == null, "Unable to access Google Drive");

String rootDirId = googleServiceConfiguration.getDirectoryId();
validate(rootDirId == null, "No destination folder has been configured. Contact your administrator for assistance.");

// Check if folder already exists on google drive. If exists, return folderId and name
FileList driveIndex = getFoldersInRoot(drive, rootDirId);
File folderOnDrive = driveIndex.getFiles().stream()
.filter(s -> directoryName.equalsIgnoreCase(s.getName()))
.findFirst()
.orElse(null);

// If folder does not exist on Drive, create a new folder in the format name-date
if (folderOnDrive == null) {
folderOnDrive = createNewDirectoryOnDrive(drive, directoryName, rootDirId);
}

// Set the file metadata
File fileMetadata = new File();
fileMetadata.setName(name);
fileMetadata.setMimeType(MediaType.TEXT_MARKDOWN_TYPE.toString());
fileMetadata.setParents(Collections.singletonList(folderOnDrive.getId()));

// Upload file to google drive
InputStream is = new ByteArrayInputStream(
StandardCharsets.UTF_8.encode(text).array());
InputStreamContent content = new InputStreamContent(
MediaType.TEXT_MARKDOWN_TYPE.toString(), is);
File uploadedFile = drive.files().create(fileMetadata, content)
.setSupportsAllDrives(true)
.setFields("id, size, name")
.execute();

// See if the Google doc already exists. If it does, trash it.
FileList fileList = drive.files().list()
.setSupportsAllDrives(true)
.setIncludeItemsFromAllDrives(true)
.setQ(String.format("'%s' in parents and mimeType = '%s' and trashed != true", folderOnDrive.getId(), GOOGLE_DOC_TYPE))
.setSpaces("drive")
.setFields("files(id, name, parents, size)")
.execute();
for (File file : fileList.getFiles()) {
if (file.getName().equals(name)) {
try {
File trash = new File();
trash.setTrashed(true);
drive.files().update(file.getId(), trash)
.setSupportsAllDrives(true)
.execute();
} catch (GoogleJsonResponseException e) {
LOG.error("Error while trashing " + file.getName(), e);
} catch (IOException e) {
LOG.error("Error while trashing " + file.getName(), e);
}
}
}

// Copy the file to a Google doc
File docFile = new File();
docFile.setName(name);
docFile.setMimeType(GOOGLE_DOC_TYPE);
docFile.setParents(Collections.singletonList(folderOnDrive.getId()));
File copiedFile = drive.files().copy(uploadedFile.getId(), docFile)
.setSupportsAllDrives(true)
.setFields("id, size, name")
.execute();

// Delete the original mark-down file after copying it.
File trash = new File();
trash.setTrashed(true);
drive.files().update(uploadedFile.getId(), trash)
.setSupportsAllDrives(true)
.execute();

return setFileInfo(copiedFile, null);
} catch (GoogleJsonResponseException e) {
LOG.error("Error occurred while accessing Google Drive.", e);
throw new FileRetrievalException(e.getMessage());
} catch (IOException e) {
LOG.error("Unexpected error processing file upload.", e);
throw new FileRetrievalException(e.getMessage());
}
}

private FileList getFoldersInRoot(Drive drive, String rootDirId) throws IOException {
return drive.files().list().setSupportsAllDrives(true)
.setIncludeItemsFromAllDrives(true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public enum Permission {
CAN_VIEW_BIRTHDAY_REPORT("View birthday report", "Reporting"),
CAN_VIEW_PROFILE_REPORT("View profile report", "Reporting"),
CAN_VIEW_CHECKINS_REPORT("View checkins report", "Reporting"),
CAN_CREATE_MERIT_REPORT("Create Merit Reports", "Reporting"),
CAN_CREATE_CHECKINS("Create check-ins", "Check-ins"),
CAN_VIEW_CHECKINS("View check-ins", "Check-ins"),
CAN_UPDATE_CHECKINS("Update check-ins", "Check-ins"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
public interface QuestionServices {
Question saveQuestion(Question question);
Set<Question> readAllQuestions();
Question findById(UUID skillId);
Question findById(UUID id);
Question update(Question question);
Set<Question> findByText(String text);
Set<Question> findByCategoryId(UUID categoryId);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.objectcomputing.checkins.services.reports;

import com.objectcomputing.checkins.services.memberprofile.MemberProfileRepository;
import com.objectcomputing.checkins.exceptions.BadArgException;

import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser;

import java.nio.ByteBuffer;
import java.io.ByteArrayInputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.time.LocalDate;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoField;
import java.util.List;

abstract class CSVProcessor {

public void load(MemberProfileRepository memberProfileRepository,
ByteBuffer dataSource) throws IOException,
BadArgException {
ByteArrayInputStream stream = new ByteArrayInputStream(dataSource.array());
InputStreamReader input = new InputStreamReader(stream);
CSVParser csvParser = CSVFormat.RFC4180
.builder()
.setHeader().setSkipHeaderRecord(true)
.setIgnoreSurroundingSpaces(true)
.setNullString("")
.build()
.parse(input);
loadImpl(memberProfileRepository, csvParser);
}

protected abstract void loadImpl(MemberProfileRepository memberProfileRepository, CSVParser csvParser) throws BadArgException;

protected LocalDate parseDate(String date) {
List<String> formatStrings = List.of("yyyy", "M/d/yyyy");
for(String format: formatStrings) {
try {
return LocalDate.parse(date,
new DateTimeFormatterBuilder()
.appendPattern(format)
.parseDefaulting(ChronoField.MONTH_OF_YEAR, 1)
.parseDefaulting(ChronoField.DAY_OF_MONTH, 1)
.toFormatter());
} catch(DateTimeParseException ex) {
}
}
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.objectcomputing.checkins.services.reports;

import com.objectcomputing.checkins.services.memberprofile.MemberProfile;
import com.objectcomputing.checkins.services.memberprofile.MemberProfileRepository;
import com.objectcomputing.checkins.exceptions.BadArgException;

import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVRecord;

import lombok.AllArgsConstructor;
import lombok.Getter;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.Optional;
import java.util.stream.Collectors;

public class CompensationHistory extends CSVProcessor {

public record Compensation(
UUID memberId,
LocalDate startDate,
float amount
) {
}

private static final Logger LOG = LoggerFactory.getLogger(CompensationHistory.class);
private final List<Compensation> history = new ArrayList<>();

@Override
protected void loadImpl(MemberProfileRepository memberProfileRepository,
CSVParser csvParser) throws BadArgException {
history.clear();
for (CSVRecord csvRecord : csvParser) {
try {
String emailAddress = csvRecord.get("emailAddress");
Optional<MemberProfile> memberProfile =
memberProfileRepository.findByWorkEmail(emailAddress);
if (memberProfile.isPresent()) {
LocalDate date = parseDate(csvRecord.get("startDate"));
if (date == null) {
LOG.error("Unable to parse date: " + csvRecord.get("startDate"));
} else {
Compensation comp = new Compensation(
memberProfile.get().getId(),
date,
Float.parseFloat(csvRecord.get("compensation")
.replaceAll("[^\\d\\.,]", "")));
history.add(comp);
}
} else {
LOG.error("Unable to find a profile for " + emailAddress);
}
} catch(IllegalArgumentException ex) {
throw new BadArgException("Unable to parse the compensation history");
}
}
}

public List<Compensation> getHistory(UUID memberId) {
return history.stream()
.filter(entry -> entry.memberId().equals(memberId))
.toList();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.objectcomputing.checkins.services.reports;

import com.objectcomputing.checkins.exceptions.NotFoundException;
import com.objectcomputing.checkins.services.memberprofile.MemberProfile;
import com.objectcomputing.checkins.services.memberprofile.MemberProfileRepository;
import com.objectcomputing.checkins.exceptions.BadArgException;

import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVRecord;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.Optional;

public class CurrentInformation extends CSVProcessor {

public record Information(
UUID memberId,
float salary,
String range,
String nationalRange,
String biography,
String commitments
) {
}

private static final Logger LOG = LoggerFactory.getLogger(CurrentInformation.class);
private final List<Information> information = new ArrayList<>();

@Override
protected void loadImpl(MemberProfileRepository memberProfileRepository,
CSVParser csvParser) throws BadArgException {
information.clear();
for (CSVRecord csvRecord : csvParser) {
try {
String emailAddress = csvRecord.get("emailAddress");
Optional<MemberProfile> memberProfile =
memberProfileRepository.findByWorkEmail(emailAddress);
if (memberProfile.isPresent()) {
Information comp = new Information(
memberProfile.get().getId(),
Float.parseFloat(csvRecord.get("salary")
.replaceAll("[^\\d\\.,]", "")),
csvRecord.get("range"),
csvRecord.get("nationalRange"),
csvRecord.get("biography"),
csvRecord.get("commitments")
);
information.add(comp);
} else {
LOG.error("Unable to find a profile for " + emailAddress);
}
} catch(IllegalArgumentException ex) {
throw new BadArgException("Unable to parse the current information");
}
}
}

public Information getInformation(UUID memberId) {
// There should only be one entry per member.
return information.stream()
.filter(entry -> entry.memberId().equals(memberId))
.findFirst()
.orElseThrow(() -> new NotFoundException("Current Information not found for member: " + memberId));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.objectcomputing.checkins.services.reports;

import io.micronaut.core.annotation.Introspected;
import lombok.AllArgsConstructor;
import lombok.Getter;

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

@AllArgsConstructor
@Getter
@Introspected
class Feedback {
@AllArgsConstructor
@Getter
public static class Answer {
private final String memberName;
private final LocalDate submitted;
private final String question;
private final String answer;
private final String type;
private final int number;
}

private String name;
private List<Answer> answers;
}

Loading