From a23ccba901765c15c5e7dba9923f71284f1b2f3a Mon Sep 17 00:00:00 2001 From: Fabian Emilius Date: Thu, 22 Aug 2024 01:39:47 +0200 Subject: [PATCH 01/13] Implement email builder class --- .../application-accepted-no-advisor.html | 23 ++ ...eptance.html => application-accepted.html} | 0 .../application-created-chair.html | 2 +- .../application-created-student.html | 2 +- ...jection.html => application-rejected.html} | 0 .../thesis-assessment-added.html | 0 server/mail-templates/thesis-closed.html | 0 .../mail-templates/thesis-comment-posted.html | 0 server/mail-templates/thesis-created.html | 0 server/mail-templates/thesis-final-grade.html | 0 .../thesis-final-submission.html | 0 .../thesis-presentation-deleted.html | 0 .../thesis-presentation-scheduled.html | 0 .../thesis-proposal-accepted.html | 0 .../thesis-proposal-uploaded.html | 0 .../ls1/controller/ThesisController.java | 2 +- .../java/thesistrack/ls1/dto/ThesisDto.java | 12 +- .../java/thesistrack/ls1/entity/Thesis.java | 36 ++ .../ls1/service/ApplicationService.java | 7 +- .../ls1/service/MailingService.java | 351 ++++++------------ .../ls1/service/ThesisCommentService.java | 10 +- .../ls1/service/ThesisService.java | 41 +- .../ls1/utility/DataFormatter.java | 25 ++ .../thesistrack/ls1/utility/MailBuilder.java | 233 ++++++++++++ .../thesistrack/ls1/utility/MailConfig.java | 111 ++++++ 25 files changed, 597 insertions(+), 258 deletions(-) create mode 100644 server/mail-templates/application-accepted-no-advisor.html rename server/mail-templates/{application-acceptance.html => application-accepted.html} (100%) rename server/mail-templates/{application-rejection.html => application-rejected.html} (100%) create mode 100644 server/mail-templates/thesis-assessment-added.html create mode 100644 server/mail-templates/thesis-closed.html create mode 100644 server/mail-templates/thesis-comment-posted.html create mode 100644 server/mail-templates/thesis-created.html create mode 100644 server/mail-templates/thesis-final-grade.html create mode 100644 server/mail-templates/thesis-final-submission.html create mode 100644 server/mail-templates/thesis-presentation-deleted.html create mode 100644 server/mail-templates/thesis-presentation-scheduled.html create mode 100644 server/mail-templates/thesis-proposal-accepted.html create mode 100644 server/mail-templates/thesis-proposal-uploaded.html create mode 100644 server/src/main/java/thesistrack/ls1/utility/DataFormatter.java create mode 100644 server/src/main/java/thesistrack/ls1/utility/MailBuilder.java create mode 100644 server/src/main/java/thesistrack/ls1/utility/MailConfig.java diff --git a/server/mail-templates/application-accepted-no-advisor.html b/server/mail-templates/application-accepted-no-advisor.html new file mode 100644 index 00000000..378a2a9a --- /dev/null +++ b/server/mail-templates/application-accepted-no-advisor.html @@ -0,0 +1,23 @@ +

Dear {{student.firstName}},

+

+ I am delighted to inform you that I would like to take the next steps in supervising your thesis. This includes + writing a proposal and familiarizing yourself with the development environment independently. +

+

+ Please coordinate with the next steps with me using the following link: + {{config.workspaceUrl}} +

+

+ I would like to emphasize that in undertaking this thesis, you will be assuming the role of project manager for your + thesis project. This role requires proactive communication, high dedication, and a strong commitment to the successful + completion of the project. I have full confidence in your ability to rise to this challenge and produce exemplary + work. +

+

+ I am excited about the opportunity to work with you and am eager to see the contributions you will make to our field. + Please feel free to reach out if you have any questions or need further clarification on any aspect of the + project. +

+

Congratulations once again, and I look forward to this academic journey with you.

+ +{{config.signature}} \ No newline at end of file diff --git a/server/mail-templates/application-acceptance.html b/server/mail-templates/application-accepted.html similarity index 100% rename from server/mail-templates/application-acceptance.html rename to server/mail-templates/application-accepted.html diff --git a/server/mail-templates/application-created-chair.html b/server/mail-templates/application-created-chair.html index 8bc74900..91b45629 100644 --- a/server/mail-templates/application-created-chair.html +++ b/server/mail-templates/application-created-chair.html @@ -11,7 +11,7 @@

{{student.email}}


University ID: 

-

{{student.tumId}}

+

{{student.universityId}}


Matriculation Number: 

{{student.matriculationNumber}}

diff --git a/server/mail-templates/application-created-student.html b/server/mail-templates/application-created-student.html index 13fef8b1..857b42dd 100644 --- a/server/mail-templates/application-created-student.html +++ b/server/mail-templates/application-created-student.html @@ -11,7 +11,7 @@

{{student.email}}


University ID: 

-

{{student.tumId}}

+

{{student.universityId}}


Matriculation Number: 

{{student.matriculationNumber}}

diff --git a/server/mail-templates/application-rejection.html b/server/mail-templates/application-rejected.html similarity index 100% rename from server/mail-templates/application-rejection.html rename to server/mail-templates/application-rejected.html diff --git a/server/mail-templates/thesis-assessment-added.html b/server/mail-templates/thesis-assessment-added.html new file mode 100644 index 00000000..e69de29b diff --git a/server/mail-templates/thesis-closed.html b/server/mail-templates/thesis-closed.html new file mode 100644 index 00000000..e69de29b diff --git a/server/mail-templates/thesis-comment-posted.html b/server/mail-templates/thesis-comment-posted.html new file mode 100644 index 00000000..e69de29b diff --git a/server/mail-templates/thesis-created.html b/server/mail-templates/thesis-created.html new file mode 100644 index 00000000..e69de29b diff --git a/server/mail-templates/thesis-final-grade.html b/server/mail-templates/thesis-final-grade.html new file mode 100644 index 00000000..e69de29b diff --git a/server/mail-templates/thesis-final-submission.html b/server/mail-templates/thesis-final-submission.html new file mode 100644 index 00000000..e69de29b diff --git a/server/mail-templates/thesis-presentation-deleted.html b/server/mail-templates/thesis-presentation-deleted.html new file mode 100644 index 00000000..e69de29b diff --git a/server/mail-templates/thesis-presentation-scheduled.html b/server/mail-templates/thesis-presentation-scheduled.html new file mode 100644 index 00000000..e69de29b diff --git a/server/mail-templates/thesis-proposal-accepted.html b/server/mail-templates/thesis-proposal-accepted.html new file mode 100644 index 00000000..e69de29b diff --git a/server/mail-templates/thesis-proposal-uploaded.html b/server/mail-templates/thesis-proposal-uploaded.html new file mode 100644 index 00000000..e69de29b diff --git a/server/src/main/java/thesistrack/ls1/controller/ThesisController.java b/server/src/main/java/thesistrack/ls1/controller/ThesisController.java index 25296ef2..c1302700 100644 --- a/server/src/main/java/thesistrack/ls1/controller/ThesisController.java +++ b/server/src/main/java/thesistrack/ls1/controller/ThesisController.java @@ -381,7 +381,7 @@ public ResponseEntity deletePresentation( throw new AccessDeniedException("You are not allowed to delete this presentation"); } - Thesis thesis = thesisService.deletePresentation(presentation.getThesis(), presentationId); + Thesis thesis = thesisService.deletePresentation(presentation); return ResponseEntity.ok(ThesisDto.fromThesisEntity(thesis, thesis.hasAdvisorAccess(authenticatedUser))); } diff --git a/server/src/main/java/thesistrack/ls1/dto/ThesisDto.java b/server/src/main/java/thesistrack/ls1/dto/ThesisDto.java index af485c34..4f020232 100644 --- a/server/src/main/java/thesistrack/ls1/dto/ThesisDto.java +++ b/server/src/main/java/thesistrack/ls1/dto/ThesisDto.java @@ -38,7 +38,7 @@ public record ThesisDto ( List states ) { - record ThesisAssessmentDto( + public record ThesisAssessmentDto( String summary, String positives, String negatives, @@ -62,7 +62,7 @@ public static ThesisAssessmentDto fromAssessmentEntity(ThesisAssessment assessme } } - record ThesisProposalDto( + public record ThesisProposalDto( Instant createdAt, LightUserDto createdBy, Instant approvedAt, @@ -82,7 +82,7 @@ public static ThesisProposalDto fromProposalEntity(ThesisProposal proposal) { } } - record ThesisPresentationDto( + public record ThesisPresentationDto( UUID presentationId, ThesisPresentationType type, String location, @@ -108,18 +108,18 @@ public static ThesisPresentationDto fromPresentationEntity(ThesisPresentation pr } } - record ThesisFilesDto( + public record ThesisFilesDto( String thesis, String presentation, String proposal ) { } - record ThesisGradeDto( + public record ThesisGradeDto( String finalGrade, String feedback ) { } - record ThesisStateChangeDto( + public record ThesisStateChangeDto( ThesisState state, Instant startedAt, Instant endedAt diff --git a/server/src/main/java/thesistrack/ls1/entity/Thesis.java b/server/src/main/java/thesistrack/ls1/entity/Thesis.java index f2a9bd39..314f02a8 100644 --- a/server/src/main/java/thesistrack/ls1/entity/Thesis.java +++ b/server/src/main/java/thesistrack/ls1/entity/Thesis.java @@ -93,6 +93,42 @@ public class Thesis { @OneToMany(mappedBy = "thesis", fetch = FetchType.EAGER) private Set states = new HashSet<>(); + public List getStudents() { + List result = new ArrayList<>(); + + for (ThesisRole role : getRoles()) { + if (role.getId().getRole() == ThesisRoleName.STUDENT) { + result.add(role.getUser()); + } + } + + return result; + } + + public List getAdvisors() { + List result = new ArrayList<>(); + + for (ThesisRole role : getRoles()) { + if (role.getId().getRole() == ThesisRoleName.ADVISOR) { + result.add(role.getUser()); + } + } + + return result; + } + + public List getSupervisors() { + List result = new ArrayList<>(); + + for (ThesisRole role : getRoles()) { + if (role.getId().getRole() == ThesisRoleName.SUPERVISOR) { + result.add(role.getUser()); + } + } + + return result; + } + public boolean hasSupervisorAccess(User user) { if (user == null) { return false; diff --git a/server/src/main/java/thesistrack/ls1/service/ApplicationService.java b/server/src/main/java/thesistrack/ls1/service/ApplicationService.java index c1d3fd5d..58f1d5a9 100644 --- a/server/src/main/java/thesistrack/ls1/service/ApplicationService.java +++ b/server/src/main/java/thesistrack/ls1/service/ApplicationService.java @@ -125,8 +125,7 @@ public Application createLegacyApplication( application.setDesiredStartDate(payload.desiredStartDate()); application.setCreatedAt(currentTime); - mailingService.sendApplicationCreatedMailToChair(application); - mailingService.sendApplicationCreatedMailToStudent(application); + mailingService.sendApplicationCreatedEmail(application); return applicationRepository.save(application); } @@ -148,7 +147,7 @@ public Application accept( application.setReviewedAt(Instant.now()); application.setReviewedBy(reviewer); - thesisService.createThesis( + Thesis thesis = thesisService.createThesis( reviewer, thesisTitle, thesisType, @@ -167,7 +166,7 @@ public Application accept( } if (notifyUser) { - mailingService.sendApplicationAcceptanceEmail(application, userService.findById(advisorIds.iterator().next())); + mailingService.sendApplicationAcceptanceEmail(application, thesis); } return applicationRepository.save(application); diff --git a/server/src/main/java/thesistrack/ls1/service/MailingService.java b/server/src/main/java/thesistrack/ls1/service/MailingService.java index d8bd8964..91210c54 100644 --- a/server/src/main/java/thesistrack/ls1/service/MailingService.java +++ b/server/src/main/java/thesistrack/ls1/service/MailingService.java @@ -1,285 +1,166 @@ package thesistrack.ls1.service; -import jakarta.mail.Address; -import jakarta.mail.BodyPart; -import jakarta.mail.MessagingException; -import jakarta.mail.Multipart; import jakarta.mail.internet.*; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.stereotype.Service; -import thesistrack.ls1.entity.Application; -import thesistrack.ls1.entity.User; +import thesistrack.ls1.constants.ThesisCommentType; +import thesistrack.ls1.entity.*; import thesistrack.ls1.exception.MailingException; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.text.SimpleDateFormat; -import java.time.Instant; -import java.util.*; +import thesistrack.ls1.utility.MailBuilder; +import thesistrack.ls1.utility.MailConfig; @Service public class MailingService { private final JavaMailSender javaMailSender; private final UploadService uploadService; - - private final boolean enabled; - - private final String mailSignature; - private final String workspaceUrl; - - private final InternetAddress sender; - private final List chairMemberRecipientsList; - private final List bccRecipientsList; - - private final Path mailTemplateLocation; + private final MailConfig config; @Autowired public MailingService( JavaMailSender javaMailSender, UploadService uploadService, - @Value("${thesis-track.mail.enabled}") boolean enabled, - @Value("${thesis-track.mail.mail-template-location}") String mailTemplateLocation, - @Value("${thesis-track.mail.sender}") InternetAddress sender, - @Value("${thesis-track.mail.chair-member-recipients}") String chairMemberRecipientsList, - @Value("${thesis-track.mail.bcc-recipients}") String bccRecipientsList, - @Value("${thesis-track.mail.signature}") String mailSignature, - @Value("${thesis-track.mail.workspace-url}") String workspaceUrl + MailConfig config ) { this.javaMailSender = javaMailSender; this.uploadService = uploadService; - - this.enabled = enabled; - this.sender = sender; - this.workspaceUrl = workspaceUrl; - this.mailSignature = mailSignature; - this.mailTemplateLocation = Paths.get(mailTemplateLocation); - - if (chairMemberRecipientsList != null && !chairMemberRecipientsList.isEmpty()) { - List addresses = Arrays.asList(chairMemberRecipientsList.split(";")); - addresses.removeIf(String::isEmpty); - - this.chairMemberRecipientsList = addresses.stream().map(address -> { - try { - return new InternetAddress(address); - } catch (AddressException e) { - throw new IllegalArgumentException("Invalid email address", e); - } - }).toList(); - } else { - this.chairMemberRecipientsList = new ArrayList<>(); - } - - if (bccRecipientsList != null && !bccRecipientsList.isEmpty()) { - List addresses = Arrays.asList(bccRecipientsList.split(";")); - addresses.removeIf(String::isEmpty); - - this.bccRecipientsList = addresses.stream().map(address -> { - try { - return new InternetAddress(address); - } catch (AddressException e) { - throw new IllegalArgumentException("Invalid email address", e); - } - }).toList(); - } else { - this.bccRecipientsList = new ArrayList<>(); - } - } - - public void sendApplicationCreatedMailToChair(Application application) throws MailingException { - if (!enabled) { - return; - } - - if (chairMemberRecipientsList.isEmpty()) { - return; - } - - try { - MimeMessage message = createMailMessage(true); - - for (Address recipient : chairMemberRecipientsList) { - message.addRecipient(MimeMessage.RecipientType.TO, recipient); - } - - message.setSubject("New Thesis Application"); - - String template = getMailTemplate("application-created-chair"); - template = fillUserPlaceholders(template, "student", application.getUser()); - template = fillApplicationPlaceholders(template, application); - - message.setContent(createApplicationFilesMailContent(template, application)); - - javaMailSender.send(message); - } catch (MessagingException | IOException e) { - throw new MailingException("Failed to send email", e); - } + this.config = config; } - public void sendApplicationCreatedMailToStudent(Application application) throws MailingException { - if (!enabled) { - return; - } - - try { - MimeMessage message = createMailMessage(false); - - message.setSubject("Thesis Application Confirmation"); - - message.addRecipient(MimeMessage.RecipientType.TO, application.getUser().getEmail()); - - String template = getMailTemplate("application-created-student"); - template = fillUserPlaceholders(template, "student", application.getUser()); - template = fillApplicationPlaceholders(template, application); - - message.setContent(createApplicationFilesMailContent(template, application)); - - javaMailSender.send(message); - } catch (MessagingException | IOException e) { - throw new MailingException("Failed to send email", e); - } + public void sendApplicationCreatedEmail(Application application) throws MailingException { + MailBuilder chairMailBuilder = new MailBuilder(this.config, "New Thesis Application", "application-created-chair"); + chairMailBuilder + .addChairMemberRecipients() + .addAttachment(application.getUser().getCvFilename()) + .addAttachment(application.getUser().getExaminationFilename()) + .addAttachment(application.getUser().getDegreeFilename()) + .fillApplicationPlaceholders(application) + .send(javaMailSender, uploadService); + + MailBuilder studentMailBuilder = new MailBuilder(this.config, "Thesis Application Confirmation", "application-created-student"); + studentMailBuilder + .addRecipient(MimeMessage.RecipientType.TO, application.getUser().getEmail()) + .addAttachment(application.getUser().getCvFilename()) + .addAttachment(application.getUser().getExaminationFilename()) + .addAttachment(application.getUser().getDegreeFilename()) + .fillApplicationPlaceholders(application) + .send(javaMailSender, uploadService); } - public void sendApplicationAcceptanceEmail(Application application, User advisor) throws MailingException { - if (!enabled) { - return; - } - - try { - MimeMessage message = createMailMessage(true); - - message.setSubject("Thesis Application Acceptance"); - - message.addRecipient(MimeMessage.RecipientType.TO, application.getUser().getEmail()); - message.addRecipient(MimeMessage.RecipientType.CC, advisor.getEmail()); + public void sendApplicationAcceptanceEmail(Application application, Thesis thesis) throws MailingException { + User advisor = thesis.getAdvisors().getFirst(); + User supervisor = thesis.getSupervisors().getFirst(); - String template = getMailTemplate("application-acceptance"); - template = fillUserPlaceholders(template, "advisor", advisor); - template = fillUserPlaceholders(template, "student", application.getUser()); - template = fillApplicationPlaceholders(template, application); - message.setContent(template, "text/html; charset=utf-8"); + String template = advisor.getId().equals(supervisor.getId()) ? "application-accepted-no-advisor" : "application-accepted"; - javaMailSender.send(message); - } catch (MessagingException e) { - throw new MailingException("Failed to send email", e); - } + MailBuilder builder = new MailBuilder(this.config, "Thesis Application Acceptance", template); + builder + .addBccRecipients() + .addRecipient(MimeMessage.RecipientType.TO, application.getUser().getEmail()) + .addRecipient(MimeMessage.RecipientType.CC, advisor.getEmail()) + .fillUserPlaceholders(advisor, "advisor") + .fillApplicationPlaceholders(application) + .send(javaMailSender, uploadService); } public void sendApplicationRejectionEmail(Application application) throws MailingException { - if (!enabled) { - return; - } - - try { - MimeMessage message = createMailMessage(true); - - message.setSubject("Thesis Application Rejection"); - - message.addRecipient(MimeMessage.RecipientType.TO, application.getUser().getEmail()); - - String content = getMailTemplate("application-rejection"); - content = fillUserPlaceholders(content, "student", application.getUser()); - content = fillApplicationPlaceholders(content, application); - - message.setContent(content, "text/html; charset=utf-8"); - - javaMailSender.send(message); - } catch (MessagingException e) { - throw new MailingException("Failed to send email", e); - } + MailBuilder builder = new MailBuilder(this.config, "Thesis Application Rejection", "application-rejected"); + builder + .addBccRecipients() + .addRecipient(MimeMessage.RecipientType.TO, application.getUser().getEmail()) + .fillApplicationPlaceholders(application) + .send(javaMailSender, uploadService); } - private String getMailTemplate(String name) throws MailingException { - Path filePath = mailTemplateLocation.resolve(name + ".html"); - - try { - byte[] fileBytes = Files.readAllBytes(filePath); + public void sendThesisCreatedEmail(Thesis thesis) { + MailBuilder builder = new MailBuilder(this.config, "Thesis Created", "thesis-created"); + builder + .addBccRecipients() + .sendToThesisStudents(thesis) + .fillThesisPlaceholders(thesis) + .send(javaMailSender, uploadService); + } - String template = new String(fileBytes, StandardCharsets.UTF_8); + public void sendThesisClosedEmail(Thesis thesis) { + MailBuilder builder = new MailBuilder(this.config, "Thesis Closed", "thesis-closed"); + builder + .addBccRecipients() + .sendToThesisStudents(thesis) + .fillThesisPlaceholders(thesis) + .send(javaMailSender, uploadService); + } - return template - .replace("{{config.signature}}", Objects.requireNonNullElse(mailSignature, "")) - .replace("{{config.workspaceUrl}}", Objects.requireNonNullElse(workspaceUrl, "")); - } catch (IOException e) { - throw new MailingException("Mail template not found", e); - } + public void sendProposalUploadedEmail(ThesisProposal proposal) { + MailBuilder builder = new MailBuilder(this.config, "Thesis Proposal Added", "thesis-proposal-uploaded"); + builder + .sendToThesisAdvisors(proposal.getThesis()) + .fillThesisProposalPlaceholders(proposal) + .send(javaMailSender, uploadService); } - private MimeMessage createMailMessage(boolean includeBcc) throws MessagingException { - MimeMessage message = javaMailSender.createMimeMessage(); + public void sendProposalAcceptedEmail(Thesis thesis) { + MailBuilder builder = new MailBuilder(this.config, "Thesis Proposal Accepted", "thesis-proposal-accepted"); + builder + .sendToThesisStudents(thesis) + .fillThesisPlaceholders(thesis) + .send(javaMailSender, uploadService); + } - message.setSender(sender); + public void sendNewCommentEmail(ThesisComment comment) { + MailBuilder builder = new MailBuilder(this.config, "New Thesis Comment", "thesis-comment-posted"); - if (includeBcc) { - for (Address bccRecipient : bccRecipientsList) { - message.addRecipient(MimeMessage.RecipientType.BCC, bccRecipient); - } + if (comment.getType() == ThesisCommentType.ADVISOR) { + builder.sendToThesisAdvisors(comment.getThesis()); + } else { + builder.sendToThesisStudents(comment.getThesis()); } - return message; + builder + .fillThesisCommentPlaceholders(comment) + .send(javaMailSender, uploadService); } - private String fillUserPlaceholders(String template, String placeholder, User user) { - return template - .replace("{{" + placeholder + ".firstName}}", Objects.requireNonNullElse(user.getFirstName(), "")) - .replace("{{" + placeholder + ".lastName}}", Objects.requireNonNullElse(user.getLastName(), "")) - .replace("{{" + placeholder + ".email}}", Objects.requireNonNullElse(user.getEmail() != null ? user.getEmail().toString() : "", "")) - .replace("{{" + placeholder + ".tumId}}", Objects.requireNonNullElse(user.getUniversityId(), "")) - .replace("{{" + placeholder + ".matriculationNumber}}", Objects.requireNonNullElse(user.getMatriculationNumber(), "")) - .replace("{{" + placeholder + ".gender}}", Objects.requireNonNullElse(user.getGender(), "")) - .replace("{{" + placeholder + ".nationality}}", Objects.requireNonNullElse(user.getNationality(), "")) - .replace("{{" + placeholder + ".isExchangeStudent}}", Objects.requireNonNullElse(user.getIsExchangeStudent(), false).toString()); + public void sendNewScheduledPresentationEmail(ThesisPresentation presentation) { + MailBuilder builder = new MailBuilder(this.config, "New Presentation scheduled", "thesis-presentation-scheduled"); + builder + .addBccRecipients() + .sendToThesisStudents(presentation.getThesis()) + .fillThesisPresentationPlaceholders(presentation) + .send(javaMailSender, uploadService); } - private String fillApplicationPlaceholders(String template, Application application) { - String pattern = "dd. MMM yyyy"; - SimpleDateFormat simpleDateFormat = new SimpleDateFormat(pattern); - - User student = application.getUser(); - - return template.replace("{{application.studyProgram}}", Objects.requireNonNullElse(student.getStudyProgram(), "")) - .replace("{{application.studyDegree}}", Objects.requireNonNullElse(student.getStudyDegree(), "")) - .replace("{{application.enrolledAt}}", simpleDateFormat.format( - Date.from(Objects.requireNonNullElse(student.getEnrolledAt(), Instant.now()) - ))) - .replace("{{application.desiredThesisStart}}", simpleDateFormat.format( - Date.from(Objects.requireNonNullElse(application.getDesiredStartDate(), Instant.now()) - ))) - .replace("{{application.specialSkills}}", Objects.requireNonNullElse(student.getSpecialSkills(), "")) - .replace("{{application.motivation}}", Objects.requireNonNullElse(application.getMotivation(), "")) - .replace("{{application.interests}}", Objects.requireNonNullElse(student.getInterests(), "")) - .replace("{{application.projects}}", Objects.requireNonNullElse(student.getProjects(), "")) - .replace("{{application.specialSkills}}", Objects.requireNonNullElse(student.getSpecialSkills(), "")) - .replace("{{application.thesisTitle}}", Objects.requireNonNullElse(application.getThesisTitle(), "")) - .replace("{{application.researchAreas}}", String.join(", ", Objects.requireNonNullElse(student.getResearchAreas(), new HashSet<>()))) - .replace("{{application.focusTopics}}", String.join(", ", Objects.requireNonNullElse(student.getFocusTopics(), new HashSet<>()))); + public void sendPresentationDeletedEmail(ThesisPresentation presentation) { + MailBuilder builder = new MailBuilder(this.config, "Presentation deleted", "thesis-presentation-deleted"); + builder + .addBccRecipients() + .sendToThesisStudents(presentation.getThesis()) + .fillThesisPresentationPlaceholders(presentation) + .send(javaMailSender, uploadService); } - private Multipart createApplicationFilesMailContent(String text, Application application) throws MessagingException, IOException { - Multipart multipart = new MimeMultipart(); - - BodyPart messageBodyPart = new MimeBodyPart(); - messageBodyPart.setContent(text, "text/html; charset=utf-8"); - multipart.addBodyPart(messageBodyPart); - - addAttachment(multipart, application.getUser().getCvFilename()); - addAttachment(multipart, application.getUser().getExaminationFilename()); - addAttachment(multipart, application.getUser().getDegreeFilename()); - - return multipart; + public void sendFinalSubmissionEmail(Thesis thesis) { + MailBuilder builder = new MailBuilder(this.config, "Thesis Submitted", "thesis-final-submission"); + builder + .addBccRecipients() + .sendToThesisAdvisors(thesis) + .fillThesisPlaceholders(thesis) + .send(javaMailSender, uploadService); } - private void addAttachment(Multipart multipart, String filename) throws MessagingException, IOException { - if (filename == null || filename.isBlank()) { - return; - } + public void sendAssessmentAddedEmail(ThesisAssessment assessment) { + MailBuilder builder = new MailBuilder(this.config, "Assessment added", "thesis-assessment-added"); + builder + .sendToThesisAdvisors(assessment.getThesis()) + .fillThesisAssessmentPlaceholders(assessment) + .send(javaMailSender, uploadService); + } - MimeBodyPart attachment = new MimeBodyPart(); - attachment.attachFile(uploadService.load(filename).getFile()); - multipart.addBodyPart(attachment); + public void sendFinalGradeEmail(Thesis thesis) { + MailBuilder builder = new MailBuilder(this.config, "Final Grade available for Thesis", "thesis-final-grade"); + builder + .addBccRecipients() + .sendToThesisStudents(thesis) + .fillThesisPlaceholders(thesis) + .send(javaMailSender, uploadService); } } diff --git a/server/src/main/java/thesistrack/ls1/service/ThesisCommentService.java b/server/src/main/java/thesistrack/ls1/service/ThesisCommentService.java index 29df5e13..fc8605d4 100644 --- a/server/src/main/java/thesistrack/ls1/service/ThesisCommentService.java +++ b/server/src/main/java/thesistrack/ls1/service/ThesisCommentService.java @@ -20,10 +20,12 @@ public class ThesisCommentService { private final ThesisCommentRepository thesisCommentRepository; private final UploadService uploadService; + private final MailingService mailingService; - public ThesisCommentService(ThesisCommentRepository thesisCommentRepository, UploadService uploadService) { + public ThesisCommentService(ThesisCommentRepository thesisCommentRepository, UploadService uploadService, MailingService mailingService) { this.thesisCommentRepository = thesisCommentRepository; this.uploadService = uploadService; + this.mailingService = mailingService; } public Page getComments(Thesis thesis, ThesisCommentType commentType, Integer page, Integer limit) { @@ -47,7 +49,11 @@ public ThesisComment postComment(User creator, Thesis thesis, ThesisCommentType comment.setFilename(uploadService.store(file, 3 * 1024 * 1024)); } - return thesisCommentRepository.save(comment); + comment = thesisCommentRepository.save(comment); + + mailingService.sendNewCommentEmail(comment); + + return comment; } public Resource getCommentFile(ThesisComment comment) { diff --git a/server/src/main/java/thesistrack/ls1/service/ThesisService.java b/server/src/main/java/thesistrack/ls1/service/ThesisService.java index 401889d4..494c2274 100644 --- a/server/src/main/java/thesistrack/ls1/service/ThesisService.java +++ b/server/src/main/java/thesistrack/ls1/service/ThesisService.java @@ -19,7 +19,6 @@ import java.time.Instant; import java.util.*; -import java.util.stream.Collectors; @Service public class ThesisService { @@ -31,6 +30,7 @@ public class ThesisService { private final ThesisProposalRepository thesisProposalRepository; private final ThesisAssessmentRepository thesisAssessmentRepository; private final ThesisPresentationRepository thesisPresentationRepository; + private final MailingService mailingService; @Autowired public ThesisService( @@ -41,7 +41,7 @@ public ThesisService( ThesisProposalRepository thesisProposalRepository, ThesisAssessmentRepository thesisAssessmentRepository, UploadService uploadService, - ThesisPresentationRepository thesisPresentationRepository) { + ThesisPresentationRepository thesisPresentationRepository, MailingService mailingService) { this.thesisRoleRepository = thesisRoleRepository; this.thesisRepository = thesisRepository; this.thesisStateChangeRepository = thesisStateChangeRepository; @@ -50,6 +50,7 @@ public ThesisService( this.thesisProposalRepository = thesisProposalRepository; this.thesisAssessmentRepository = thesisAssessmentRepository; this.thesisPresentationRepository = thesisPresentationRepository; + this.mailingService = mailingService; } public Page getAll( @@ -102,7 +103,9 @@ public Thesis createThesis( assignThesisRoles(thesis, creator, supervisorIds, advisorIds, studentIds); saveStateChange(thesis, ThesisState.PROPOSAL, Instant.now()); - return findById(thesis.getId()); + mailingService.sendThesisCreatedEmail(thesis); + + return thesis; } @Transactional @@ -114,7 +117,11 @@ public Thesis closeThesis(Thesis thesis) { thesis.setState(ThesisState.DROPPED_OUT); saveStateChange(thesis, ThesisState.DROPPED_OUT, Instant.now()); - return thesisRepository.save(thesis); + thesis = thesisRepository.save(thesis); + + mailingService.sendThesisClosedEmail(thesis); + + return thesis; } @Transactional @@ -192,6 +199,8 @@ public Thesis uploadProposal(User uploader, Thesis thesis, MultipartFile proposa thesisProposalRepository.save(proposal); + mailingService.sendProposalUploadedEmail(proposal); + return thesisRepository.save(thesis); } @@ -214,6 +223,8 @@ public Thesis acceptProposal(User reviewer, Thesis thesis) { thesis.setState(ThesisState.WRITING); + mailingService.sendProposalAcceptedEmail(thesis); + return thesisRepository.save(thesis); } @@ -229,6 +240,8 @@ public Thesis submitThesis(Thesis thesis) { saveStateChange(thesis, ThesisState.SUBMITTED, Instant.now()); + mailingService.sendFinalSubmissionEmail(thesis); + return thesisRepository.save(thesis); } @@ -285,18 +298,26 @@ public Thesis createPresentation(User creator, Thesis thesis, ThesisPresentation presentations.sort(Comparator.comparing(ThesisPresentation::getScheduledAt)); thesis.setPresentations(presentations); - return thesisRepository.save(thesis); + thesis = thesisRepository.save(thesis); + + mailingService.sendNewScheduledPresentationEmail(presentation); + + return thesis; } - public Thesis deletePresentation(Thesis thesis, UUID presentationId) { - thesisPresentationRepository.deleteById(presentationId); + public Thesis deletePresentation(ThesisPresentation presentation) { + Thesis thesis = presentation.getThesis(); + + thesisPresentationRepository.deleteById(presentation.getId()); List presentations = new ArrayList<>(thesis.getPresentations().stream() - .filter(presentation -> !presentation.getId().equals(presentationId)) + .filter(x -> !presentation.getId().equals(x.getId())) .toList()); thesis.setPresentations(presentations); + mailingService.sendPresentationDeletedEmail(presentation); + return thesisRepository.save(thesis); } @@ -330,6 +351,8 @@ public Thesis submitAssessment( saveStateChange(thesis, ThesisState.ASSESSED, Instant.now()); + mailingService.sendAssessmentAddedEmail(assessment); + return thesisRepository.save(thesis); } @@ -342,6 +365,8 @@ public Thesis gradeThesis(Thesis thesis, String finalGrade, String finalFeedback saveStateChange(thesis, ThesisState.GRADED, Instant.now()); + mailingService.sendFinalGradeEmail(thesis); + return thesisRepository.save(thesis); } diff --git a/server/src/main/java/thesistrack/ls1/utility/DataFormatter.java b/server/src/main/java/thesistrack/ls1/utility/DataFormatter.java new file mode 100644 index 00000000..c145a6db --- /dev/null +++ b/server/src/main/java/thesistrack/ls1/utility/DataFormatter.java @@ -0,0 +1,25 @@ +package thesistrack.ls1.utility; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; + +public class DataFormatter { + public static String formatDate(Object time) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd.MM.yyyy") + .withZone(ZoneId.systemDefault()); + + return formatter.format((Instant) time); + } + + public static String formatDateTime(Object time) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss") + .withZone(ZoneId.systemDefault()); + + return formatter.format((Instant) time); + } + + public static String formatEnum(Object value) { + return ((Enum) value).name(); + } +} diff --git a/server/src/main/java/thesistrack/ls1/utility/MailBuilder.java b/server/src/main/java/thesistrack/ls1/utility/MailBuilder.java new file mode 100644 index 00000000..50eb8979 --- /dev/null +++ b/server/src/main/java/thesistrack/ls1/utility/MailBuilder.java @@ -0,0 +1,233 @@ +package thesistrack.ls1.utility; + +import jakarta.mail.*; +import jakarta.mail.internet.MimeBodyPart; +import jakarta.mail.internet.MimeMessage; +import jakarta.mail.internet.MimeMultipart; +import org.springframework.mail.javamail.JavaMailSender; +import thesistrack.ls1.constants.ThesisRoleName; +import thesistrack.ls1.dto.ApplicationDto; +import thesistrack.ls1.dto.ThesisCommentDto; +import thesistrack.ls1.dto.ThesisDto; +import thesistrack.ls1.dto.UserDto; +import thesistrack.ls1.entity.*; +import thesistrack.ls1.exception.MailingException; +import thesistrack.ls1.service.UploadService; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.time.Instant; +import java.util.*; +import java.util.function.Function; + +public class MailBuilder { + private record MailRecipient( + Message.RecipientType type, + Address address + ) {} + + private final MailConfig config; + private final List recipients; + private final List attachments; + private final String subject; + + private String content; + + public MailBuilder(MailConfig config, String subject, String template) { + this.config = config; + + this.attachments = new ArrayList<>(); + this.recipients = new ArrayList<>(); + + this.subject = subject; + this.content = config.getTemplate(template); + } + + public MailBuilder addAttachment(String filename) { + if (filename == null || filename.isBlank()) { + return this; + } + + attachments.add(filename); + + return this; + } + + public MailBuilder addBccRecipients() { + for (Address address : config.getBccRecipients()) { + recipients.add(new MailRecipient(MimeMessage.RecipientType.BCC, address)); + } + + return this; + } + + public MailBuilder addChairMemberRecipients() { + for (Address address : config.getChairMemberRecipients()) { + recipients.add(new MailRecipient(MimeMessage.RecipientType.CC, address)); + } + + return this; + } + + public MailBuilder sendToThesisAdvisors(Thesis thesis) { + for (ThesisRole role : thesis.getRoles()) { + if (role.getId().getRole() != ThesisRoleName.STUDENT) { + recipients.add(new MailRecipient(MimeMessage.RecipientType.TO, role.getUser().getEmail())); + } + } + + return this; + } + + public MailBuilder sendToThesisStudents(Thesis thesis) { + for (ThesisRole role : thesis.getRoles()) { + if (role.getId().getRole() == ThesisRoleName.STUDENT) { + recipients.add(new MailRecipient(MimeMessage.RecipientType.TO, role.getUser().getEmail())); + } else { + recipients.add(new MailRecipient(MimeMessage.RecipientType.CC, role.getUser().getEmail())); + } + } + + return this; + } + + public MailBuilder addRecipient(Message.RecipientType type, Address address) { + recipients.add(new MailRecipient(type, address)); + + return this; + } + + public MailBuilder fillUserPlaceholders(User user, String placeholder) { + replaceDtoPlaceholders(UserDto.fromUserEntity(user), placeholder, new HashMap<>()); + + return this; + } + + public MailBuilder fillApplicationPlaceholders(Application application) { + HashMap> formatters = new HashMap<>(); + + formatters.put("application.thesisTitle", (Object value) -> { + if (application.getTopic() != null) { + return application.getTopic().getTitle(); + } + + return Objects.requireNonNullElse(application.getThesisTitle(), ""); + }); + formatters.put("application.desiredThesisStart", DataFormatter::formatDate); + formatters.put("application.user.enrolledAt", DataFormatter::formatDate); + + replaceDtoPlaceholders(ApplicationDto.fromApplicationEntity(application, false), "application", formatters); + + return this; + } + + public MailBuilder fillThesisPlaceholders(Thesis thesis) { + HashMap> formatters = new HashMap<>(); + + formatters.put("thesis.startDate", DataFormatter::formatDate); + formatters.put("thesis.endDate", DataFormatter::formatDate); + + replaceDtoPlaceholders(ThesisDto.fromThesisEntity(thesis, false), "thesis", formatters); + + return this; + } + + public MailBuilder fillThesisCommentPlaceholders(ThesisComment comment) { + fillThesisPlaceholders(comment.getThesis()); + replaceDtoPlaceholders(ThesisCommentDto.fromCommentEntity(comment), "comment", new HashMap<>()); + + return this; + } + + public MailBuilder fillThesisPresentationPlaceholders(ThesisPresentation presentation) { + fillThesisPlaceholders(presentation.getThesis()); + replaceDtoPlaceholders(ThesisDto.ThesisPresentationDto.fromPresentationEntity(presentation), "presentation", new HashMap<>()); + + return this; + } + + public MailBuilder fillThesisProposalPlaceholders(ThesisProposal proposal) { + fillThesisPlaceholders(proposal.getThesis()); + replaceDtoPlaceholders(ThesisDto.ThesisProposalDto.fromProposalEntity(proposal), "proposal", new HashMap<>()); + + return this; + } + + public MailBuilder fillThesisAssessmentPlaceholders(ThesisAssessment assessment) { + fillThesisPlaceholders(assessment.getThesis()); + replaceDtoPlaceholders(ThesisDto.ThesisAssessmentDto.fromAssessmentEntity(assessment), "assessment", new HashMap<>()); + + return this; + } + + public void send(JavaMailSender mailSender, UploadService uploadService) { + if (!config.isEnabled()) { + return; + } + + if (recipients.isEmpty()) { + return; + } + + try { + MimeMessage message = mailSender.createMimeMessage(); + + message.setSubject(subject); + message.setSender(config.getSender()); + + for (MailRecipient recipient : recipients) { + message.addRecipient(recipient.type(), recipient.address()); + } + + Multipart multipart = new MimeMultipart(); + + BodyPart messageBody = new MimeBodyPart(); + messageBody.setContent(content, "text/html; charset=utf-8"); + multipart.addBodyPart(messageBody); + + for (String filename : attachments) { + MimeBodyPart attachment = new MimeBodyPart(); + attachment.attachFile(uploadService.load(filename).getFile()); + multipart.addBodyPart(attachment); + } + + message.setContent(multipart); + + mailSender.send(message); + } catch (MessagingException | IOException e) { + throw new MailingException("Failed to send email", e); + } + } + + private void replaceDtoPlaceholders(Object dto, String dtoPrefix, Map> formatters) { + Field[] fields = dto.getClass().getDeclaredFields(); + + for (Field field : fields) { + field.setAccessible(true); + + try { + Object value = field.get(dto); + String identifier = dtoPrefix + "." + field.getName(); + String placeholder = "{{" + identifier + "}}"; + + if (value != null) { + if (formatters.get(identifier) != null) { + content = content.replace(placeholder, formatters.get(identifier).apply(value)); + } else if (value.getClass().isRecord()) { + replaceDtoPlaceholders(value, dtoPrefix + "." + field.getName(), formatters); + } else if (value instanceof Instant) { + content = content.replace(placeholder, DataFormatter.formatDateTime((Instant) value)); + } else if (value.getClass().isEnum()) { + content = content.replace(placeholder, DataFormatter.formatEnum(value)); + } else { + content = content.replace(placeholder, value.toString().replace("{{", "").replace("}}", "")); + } + } else { + content = content.replace(placeholder, ""); + } + } catch (IllegalAccessException e) { + throw new RuntimeException("Failed to access field: " + field.getName(), e); + } + } + } +} diff --git a/server/src/main/java/thesistrack/ls1/utility/MailConfig.java b/server/src/main/java/thesistrack/ls1/utility/MailConfig.java new file mode 100644 index 00000000..f59189b9 --- /dev/null +++ b/server/src/main/java/thesistrack/ls1/utility/MailConfig.java @@ -0,0 +1,111 @@ +package thesistrack.ls1.utility; + +import jakarta.mail.internet.AddressException; +import jakarta.mail.internet.InternetAddress; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; +import lombok.Getter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.validation.annotation.Validated; +import thesistrack.ls1.exception.MailingException; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +@Data +@Validated +@Configuration +@ConfigurationProperties(prefix = "thesis-track.mail") +public class MailConfig { + @NotBlank + private Boolean enabled; + + @NotBlank + private String sender; + + @NotBlank + private String signature; + + @NotBlank + private String workspaceUrl; + + @NotBlank + private String chairMemberRecipients; + + @NotBlank + private String bccRecipients; + + @NotBlank + private String mailTemplateLocation; + + public boolean isEnabled() { + return enabled; + } + + public InternetAddress getSender() { + try { + return new InternetAddress(sender); + } catch (AddressException e) { + throw new MailingException("Invalid email for sender"); + } + } + + public List getChairMemberRecipients() { + if (chairMemberRecipients != null && !chairMemberRecipients.isEmpty()) { + List addresses = Arrays.asList(chairMemberRecipients.split(";")); + addresses.removeIf(String::isEmpty); + + return addresses.stream().map(address -> { + try { + return new InternetAddress(address); + } catch (AddressException e) { + throw new IllegalArgumentException("Invalid email address", e); + } + }).toList(); + } else { + return new ArrayList<>(); + } + } + + public List getBccRecipients() { + if (bccRecipients != null && !bccRecipients.isEmpty()) { + List addresses = Arrays.asList(bccRecipients.split(";")); + addresses.removeIf(String::isEmpty); + + return addresses.stream().map(address -> { + try { + return new InternetAddress(address); + } catch (AddressException e) { + throw new IllegalArgumentException("Invalid email address", e); + } + }).toList(); + } else { + return new ArrayList<>(); + } + } + + public String getTemplate(String name) { + Path folder = Paths.get(mailTemplateLocation); + Path filePath = folder.resolve(name + ".html"); + + try { + byte[] fileBytes = Files.readAllBytes(filePath); + + String template = new String(fileBytes, StandardCharsets.UTF_8); + + return template + .replace("{{config.signature}}", Objects.requireNonNullElse(signature, "")) + .replace("{{config.workspaceUrl}}", Objects.requireNonNullElse(workspaceUrl, "")); + } catch (IOException e) { + throw new MailingException("Mail template not found", e); + } + } +} From 628b16374842212113aa5ce8438513f2b2383335 Mon Sep 17 00:00:00 2001 From: Fabian Emilius Date: Thu, 22 Aug 2024 14:18:44 +0200 Subject: [PATCH 02/13] Improve mail builder and recipient name handling --- .../application-accepted-no-advisor.html | 2 +- .../mail-templates/application-accepted.html | 2 +- .../application-created-chair.html | 88 ++++++----- .../application-created-student.html | 86 ++++++----- .../mail-templates/application-rejected.html | 2 +- .../ls1/repository/UserRepository.java | 4 + .../ls1/service/MailingService.java | 73 ++++----- .../ls1/utility/DataFormatter.java | 12 ++ .../thesistrack/ls1/utility/MailBuilder.java | 141 +++++++++++------- .../thesistrack/ls1/utility/MailConfig.java | 97 ++++++------ server/src/main/resources/application.yml | 1 - .../test/java/thesistrack/ls1/MailTest.java | 15 ++ server/src/test/resources/application.yml | 67 +++++++++ 13 files changed, 366 insertions(+), 224 deletions(-) create mode 100644 server/src/test/java/thesistrack/ls1/MailTest.java create mode 100644 server/src/test/resources/application.yml diff --git a/server/mail-templates/application-accepted-no-advisor.html b/server/mail-templates/application-accepted-no-advisor.html index 378a2a9a..1d81699b 100644 --- a/server/mail-templates/application-accepted-no-advisor.html +++ b/server/mail-templates/application-accepted-no-advisor.html @@ -1,4 +1,4 @@ -

Dear {{student.firstName}},

+

Dear {{recipientName}},

I am delighted to inform you that I would like to take the next steps in supervising your thesis. This includes writing a proposal and familiarizing yourself with the development environment independently. diff --git a/server/mail-templates/application-accepted.html b/server/mail-templates/application-accepted.html index 20a0ec7d..e69fe942 100644 --- a/server/mail-templates/application-accepted.html +++ b/server/mail-templates/application-accepted.html @@ -1,4 +1,4 @@ -

Dear {{student.firstName}},

+

Dear {{recipientName}},

I am delighted to inform you that I would like to take the next steps in supervising your thesis. This includes writing a proposal and familiarizing yourself with the development environment diff --git a/server/mail-templates/application-created-chair.html b/server/mail-templates/application-created-chair.html index 91b45629..d4a562a2 100644 --- a/server/mail-templates/application-created-chair.html +++ b/server/mail-templates/application-created-chair.html @@ -1,46 +1,52 @@ -

Dear Chair Members,

+

Dear {{recipientName}},

there is a new thesis application submitted by {{student.firstName}} {{student.lastName}}.

We received the following thesis application details:

-
+
+ +

+ Name:
+ {{application.user.firstName}} {{application.user.lastName}} +

+

+ Email:
+ {{application.user.email}} +

+

+ University ID:
+ {{application.user.universityId}} +

+

+ Matriculation Number:
+ {{application.user.matriculationNumber}} +

+

+ Study program:
+ {{application.user.studyProgram}} {{application.user.studyDegree}} (Enrolled at {{application.user.enrolledAt}}) +

+

+ Thesis Title Suggestion:
+ {{application.thesisTitle}} +

+

+ Desired Thesis Start Date:
+ {{application.desiredThesisStart}} +

+

+ Motivation:
+ {{application.motivation}} +

+

+ Special Skills:
+ {{application.user.specialSkills}} +

+

+ Interests:
+ {{application.user.interests}} +

+

+ Projects:
+ {{application.user.projects}} +

-

Name: 

-

{{student.firstName}} {{student.lastName}}

-
-

Email: 

-

{{student.email}}

-
-

University ID: 

-

{{student.universityId}}

-
-

Matriculation Number: 

-

{{student.matriculationNumber}}

-
-

Study program: 

-

{{application.studyProgram}} {{application.studyDegree}} (Enrolled at {{application.enrolledAt}})

-
-

Desired Thesis Start Date: 

-

{{application.desiredThesisStart}}

-
-

Special Skills: 

-

{{application.specialSkills}}

-
-

Motivation: 

-

{{application.motivation}}

-
-

Interests: 

-

{{application.interests}}

-
-

Projects: 

-

{{application.projects}}

-
-

Thesis Title Suggestion: 

-

{{application.thesisTitle}}

-
-

Research Areas: 

-

{{application.researchAreas}}

-
-

Focus Topics: 

-

{{application.focusTopics}}

-

You can find the submitted files in the attachment part of this email.

diff --git a/server/mail-templates/application-created-student.html b/server/mail-templates/application-created-student.html index 857b42dd..22de7529 100644 --- a/server/mail-templates/application-created-student.html +++ b/server/mail-templates/application-created-student.html @@ -1,46 +1,52 @@ -

Dear {{student.firstName}},

+

Dear {{recipientName}},

With this email we confirm your successful thesis application submission.

We received the following thesis application details:


-

Name: 

-

{{student.firstName}} {{student.lastName}}

-
-

Email: 

-

{{student.email}}

-
-

University ID: 

-

{{student.universityId}}

-
-

Matriculation Number: 

-

{{student.matriculationNumber}}

-
-

Study program: 

-

{{application.studyProgram}} {{application.studyDegree}} (Enrolled at {{application.enrolledAt}})

-
-


Desired Thesis Start Date: 

-

{{application.desiredThesisStart}}

-
-

Special Skills: 

-

{{application.specialSkills}}

-
-

Motivation: 

-

{{application.motivation}}

-
-

Interests: 

-

{{application.interests}}

-
-

Projects: 

-

{{application.projects}}

-
-

Thesis Title Suggestion: 

-

{{application.thesisTitle}}

-
-

Research Areas: 

-

{{application.researchAreas}}

-
-

Focus Topics: 

-

{{application.focusTopics}}

-
+

+ Name:
+ {{application.user.firstName}} {{application.user.lastName}} +

+

+ Email:
+ {{application.user.email}} +

+

+ University ID:
+ {{application.user.universityId}} +

+

+ Matriculation Number:
+ {{application.user.matriculationNumber}} +

+

+ Study program:
+ {{application.user.studyProgram}} {{application.user.studyDegree}} (Enrolled at {{application.user.enrolledAt}}) +

+

+ Thesis Title Suggestion:
+ {{application.thesisTitle}} +

+

+ Desired Thesis Start Date:
+ {{application.desiredThesisStart}} +

+

+ Motivation:
+ {{application.motivation}} +

+

+ Special Skills:
+ {{application.user.specialSkills}} +

+

+ Interests:
+ {{application.user.interests}} +

+

+ Projects:
+ {{application.user.projects}} +

+

You can find the submitted files in the attachment part of this email.

diff --git a/server/mail-templates/application-rejected.html b/server/mail-templates/application-rejected.html index 9485d700..eaa2564c 100644 --- a/server/mail-templates/application-rejected.html +++ b/server/mail-templates/application-rejected.html @@ -1,4 +1,4 @@ -

Dear {{student.firstName}},

+

Dear {{recipientName}},

Thank you for your interest in pursuing your thesis under my supervision. I have carefully reviewed your application and supporting documents. diff --git a/server/src/main/java/thesistrack/ls1/repository/UserRepository.java b/server/src/main/java/thesistrack/ls1/repository/UserRepository.java index e2a7af5b..e329738e 100644 --- a/server/src/main/java/thesistrack/ls1/repository/UserRepository.java +++ b/server/src/main/java/thesistrack/ls1/repository/UserRepository.java @@ -8,6 +8,7 @@ import org.springframework.stereotype.Repository; import thesistrack.ls1.entity.User; +import java.util.List; import java.util.Optional; import java.util.Set; import java.util.UUID; @@ -24,4 +25,7 @@ public interface UserRepository extends JpaRepository { "LOWER(u.matriculationNumber) LIKE %:searchQuery% OR " + "LOWER(u.universityId) LIKE %:searchQuery%)") Page searchUsers(@Param("searchQuery") String searchQuery, @Param("groups") Set groups, Pageable page); + + @Query("SELECT DISTINCT u FROM User u LEFT JOIN UserGroup g ON (u.id = g.id.userId) WHERE g.id.group IN (\"supervisor\", \"advisor\", \"admin\")") + List getChairMembers(); } diff --git a/server/src/main/java/thesistrack/ls1/service/MailingService.java b/server/src/main/java/thesistrack/ls1/service/MailingService.java index 91210c54..83762d7d 100644 --- a/server/src/main/java/thesistrack/ls1/service/MailingService.java +++ b/server/src/main/java/thesistrack/ls1/service/MailingService.java @@ -1,6 +1,5 @@ package thesistrack.ls1.service; -import jakarta.mail.internet.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.stereotype.Service; @@ -28,21 +27,21 @@ public MailingService( } public void sendApplicationCreatedEmail(Application application) throws MailingException { - MailBuilder chairMailBuilder = new MailBuilder(this.config, "New Thesis Application", "application-created-chair"); + MailBuilder chairMailBuilder = new MailBuilder(config, "New Thesis Application", "application-created-chair"); chairMailBuilder - .addChairMemberRecipients() - .addAttachment(application.getUser().getCvFilename()) - .addAttachment(application.getUser().getExaminationFilename()) - .addAttachment(application.getUser().getDegreeFilename()) + .sendToChairMembers() + .addAttachmentFile(application.getUser().getCvFilename()) + .addAttachmentFile(application.getUser().getExaminationFilename()) + .addAttachmentFile(application.getUser().getDegreeFilename()) .fillApplicationPlaceholders(application) .send(javaMailSender, uploadService); - MailBuilder studentMailBuilder = new MailBuilder(this.config, "Thesis Application Confirmation", "application-created-student"); + MailBuilder studentMailBuilder = new MailBuilder(config, "Thesis Application Confirmation", "application-created-student"); studentMailBuilder - .addRecipient(MimeMessage.RecipientType.TO, application.getUser().getEmail()) - .addAttachment(application.getUser().getCvFilename()) - .addAttachment(application.getUser().getExaminationFilename()) - .addAttachment(application.getUser().getDegreeFilename()) + .addPrimaryRecipient(application.getUser()) + .addAttachmentFile(application.getUser().getCvFilename()) + .addAttachmentFile(application.getUser().getExaminationFilename()) + .addAttachmentFile(application.getUser().getDegreeFilename()) .fillApplicationPlaceholders(application) .send(javaMailSender, uploadService); } @@ -53,53 +52,54 @@ public void sendApplicationAcceptanceEmail(Application application, Thesis thesi String template = advisor.getId().equals(supervisor.getId()) ? "application-accepted-no-advisor" : "application-accepted"; - MailBuilder builder = new MailBuilder(this.config, "Thesis Application Acceptance", template); + MailBuilder builder = new MailBuilder(config, "Thesis Application Acceptance", template); builder - .addBccRecipients() - .addRecipient(MimeMessage.RecipientType.TO, application.getUser().getEmail()) - .addRecipient(MimeMessage.RecipientType.CC, advisor.getEmail()) + .addPrimaryRecipient(application.getUser()) + .addSecondaryRecipient(advisor.getEmail()) + .addDefaultBccRecipients() .fillUserPlaceholders(advisor, "advisor") .fillApplicationPlaceholders(application) .send(javaMailSender, uploadService); } public void sendApplicationRejectionEmail(Application application) throws MailingException { - MailBuilder builder = new MailBuilder(this.config, "Thesis Application Rejection", "application-rejected"); + MailBuilder builder = new MailBuilder(config, "Thesis Application Rejection", "application-rejected"); builder - .addBccRecipients() - .addRecipient(MimeMessage.RecipientType.TO, application.getUser().getEmail()) + .addPrimaryRecipient(application.getUser()) + .addDefaultBccRecipients() .fillApplicationPlaceholders(application) .send(javaMailSender, uploadService); } public void sendThesisCreatedEmail(Thesis thesis) { - MailBuilder builder = new MailBuilder(this.config, "Thesis Created", "thesis-created"); + MailBuilder builder = new MailBuilder(config, "Thesis Created", "thesis-created"); builder - .addBccRecipients() .sendToThesisStudents(thesis) + .addDefaultBccRecipients() .fillThesisPlaceholders(thesis) .send(javaMailSender, uploadService); } public void sendThesisClosedEmail(Thesis thesis) { - MailBuilder builder = new MailBuilder(this.config, "Thesis Closed", "thesis-closed"); + MailBuilder builder = new MailBuilder(config, "Thesis Closed", "thesis-closed"); builder - .addBccRecipients() .sendToThesisStudents(thesis) + .addDefaultBccRecipients() .fillThesisPlaceholders(thesis) .send(javaMailSender, uploadService); } public void sendProposalUploadedEmail(ThesisProposal proposal) { - MailBuilder builder = new MailBuilder(this.config, "Thesis Proposal Added", "thesis-proposal-uploaded"); + MailBuilder builder = new MailBuilder(config, "Thesis Proposal Added", "thesis-proposal-uploaded"); builder .sendToThesisAdvisors(proposal.getThesis()) .fillThesisProposalPlaceholders(proposal) + .addAttachmentFile(proposal.getProposalFilename()) .send(javaMailSender, uploadService); } public void sendProposalAcceptedEmail(Thesis thesis) { - MailBuilder builder = new MailBuilder(this.config, "Thesis Proposal Accepted", "thesis-proposal-accepted"); + MailBuilder builder = new MailBuilder(config, "Thesis Proposal Accepted", "thesis-proposal-accepted"); builder .sendToThesisStudents(thesis) .fillThesisPlaceholders(thesis) @@ -107,7 +107,7 @@ public void sendProposalAcceptedEmail(Thesis thesis) { } public void sendNewCommentEmail(ThesisComment comment) { - MailBuilder builder = new MailBuilder(this.config, "New Thesis Comment", "thesis-comment-posted"); + MailBuilder builder = new MailBuilder(config, "New Thesis Comment", "thesis-comment-posted"); if (comment.getType() == ThesisCommentType.ADVISOR) { builder.sendToThesisAdvisors(comment.getThesis()); @@ -117,49 +117,52 @@ public void sendNewCommentEmail(ThesisComment comment) { builder .fillThesisCommentPlaceholders(comment) + .addAttachmentFile(comment.getFilename()) .send(javaMailSender, uploadService); } public void sendNewScheduledPresentationEmail(ThesisPresentation presentation) { - MailBuilder builder = new MailBuilder(this.config, "New Presentation scheduled", "thesis-presentation-scheduled"); + MailBuilder builder = new MailBuilder(config, "New Presentation scheduled", "thesis-presentation-scheduled"); builder - .addBccRecipients() .sendToThesisStudents(presentation.getThesis()) + .addDefaultBccRecipients() .fillThesisPresentationPlaceholders(presentation) .send(javaMailSender, uploadService); } public void sendPresentationDeletedEmail(ThesisPresentation presentation) { - MailBuilder builder = new MailBuilder(this.config, "Presentation deleted", "thesis-presentation-deleted"); + MailBuilder builder = new MailBuilder(config, "Presentation deleted", "thesis-presentation-deleted"); builder - .addBccRecipients() .sendToThesisStudents(presentation.getThesis()) + .addDefaultBccRecipients() .fillThesisPresentationPlaceholders(presentation) .send(javaMailSender, uploadService); } public void sendFinalSubmissionEmail(Thesis thesis) { - MailBuilder builder = new MailBuilder(this.config, "Thesis Submitted", "thesis-final-submission"); + MailBuilder builder = new MailBuilder(config, "Thesis Submitted", "thesis-final-submission"); builder - .addBccRecipients() .sendToThesisAdvisors(thesis) + .addDefaultBccRecipients() .fillThesisPlaceholders(thesis) + .addAttachmentFile(thesis.getFinalThesisFilename()) + .addAttachmentFile(thesis.getFinalPresentationFilename()) .send(javaMailSender, uploadService); } public void sendAssessmentAddedEmail(ThesisAssessment assessment) { - MailBuilder builder = new MailBuilder(this.config, "Assessment added", "thesis-assessment-added"); + MailBuilder builder = new MailBuilder(config, "Assessment added", "thesis-assessment-added"); builder - .sendToThesisAdvisors(assessment.getThesis()) + .sendToThesisSupervisors(assessment.getThesis()) .fillThesisAssessmentPlaceholders(assessment) .send(javaMailSender, uploadService); } public void sendFinalGradeEmail(Thesis thesis) { - MailBuilder builder = new MailBuilder(this.config, "Final Grade available for Thesis", "thesis-final-grade"); + MailBuilder builder = new MailBuilder(config, "Final Grade available for Thesis", "thesis-final-grade"); builder - .addBccRecipients() .sendToThesisStudents(thesis) + .addDefaultBccRecipients() .fillThesisPlaceholders(thesis) .send(javaMailSender, uploadService); } diff --git a/server/src/main/java/thesistrack/ls1/utility/DataFormatter.java b/server/src/main/java/thesistrack/ls1/utility/DataFormatter.java index c145a6db..1c6d6a06 100644 --- a/server/src/main/java/thesistrack/ls1/utility/DataFormatter.java +++ b/server/src/main/java/thesistrack/ls1/utility/DataFormatter.java @@ -6,6 +6,10 @@ public class DataFormatter { public static String formatDate(Object time) { + if (!(time instanceof Instant)) { + return ""; + } + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd.MM.yyyy") .withZone(ZoneId.systemDefault()); @@ -13,6 +17,10 @@ public static String formatDate(Object time) { } public static String formatDateTime(Object time) { + if (!(time instanceof Instant)) { + return ""; + } + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss") .withZone(ZoneId.systemDefault()); @@ -20,6 +28,10 @@ public static String formatDateTime(Object time) { } public static String formatEnum(Object value) { + if (!value.getClass().isEnum()) { + return ""; + } + return ((Enum) value).name(); } } diff --git a/server/src/main/java/thesistrack/ls1/utility/MailBuilder.java b/server/src/main/java/thesistrack/ls1/utility/MailBuilder.java index 50eb8979..01408947 100644 --- a/server/src/main/java/thesistrack/ls1/utility/MailBuilder.java +++ b/server/src/main/java/thesistrack/ls1/utility/MailBuilder.java @@ -1,9 +1,11 @@ package thesistrack.ls1.utility; import jakarta.mail.*; +import jakarta.mail.internet.InternetAddress; import jakarta.mail.internet.MimeBodyPart; import jakarta.mail.internet.MimeMessage; import jakarta.mail.internet.MimeMultipart; +import lombok.Getter; import org.springframework.mail.javamail.JavaMailSender; import thesistrack.ls1.constants.ThesisRoleName; import thesistrack.ls1.dto.ApplicationDto; @@ -21,29 +23,34 @@ import java.util.function.Function; public class MailBuilder { - private record MailRecipient( - Message.RecipientType type, - Address address - ) {} - private final MailConfig config; - private final List recipients; - private final List attachments; + + private final List primaryRecipients; + private final List secondaryRecipients; + private final List bccRecipients; + + @Getter private final String subject; + @Getter private String content; + @Getter + private final List attachments; + public MailBuilder(MailConfig config, String subject, String template) { this.config = config; - this.attachments = new ArrayList<>(); - this.recipients = new ArrayList<>(); + this.primaryRecipients = new ArrayList<>(); + this.secondaryRecipients = new ArrayList<>(); + this.bccRecipients = new ArrayList<>(); this.subject = subject; this.content = config.getTemplate(template); + this.attachments = new ArrayList<>(); } - public MailBuilder addAttachment(String filename) { + public MailBuilder addAttachmentFile(String filename) { if (filename == null || filename.isBlank()) { return this; } @@ -53,17 +60,45 @@ public MailBuilder addAttachment(String filename) { return this; } - public MailBuilder addBccRecipients() { - for (Address address : config.getBccRecipients()) { - recipients.add(new MailRecipient(MimeMessage.RecipientType.BCC, address)); + public MailBuilder addDefaultBccRecipients() { + for (InternetAddress address : config.getDefaultBccRecipients()) { + addBccRecipient(address); } return this; } - public MailBuilder addChairMemberRecipients() { - for (Address address : config.getChairMemberRecipients()) { - recipients.add(new MailRecipient(MimeMessage.RecipientType.CC, address)); + public MailBuilder addPrimaryRecipient(User user) { + primaryRecipients.add(user); + + return this; + } + + public MailBuilder addSecondaryRecipient(InternetAddress address) { + secondaryRecipients.add(address); + + return this; + } + + public MailBuilder addBccRecipient(InternetAddress address) { + bccRecipients.add(address); + + return this; + } + + public MailBuilder sendToChairMembers() { + for (User user : config.getChairMembers()) { + addPrimaryRecipient(user); + } + + return this; + } + + public MailBuilder sendToThesisSupervisors(Thesis thesis) { + for (ThesisRole role : thesis.getRoles()) { + if (role.getId().getRole() == ThesisRoleName.SUPERVISOR) { + addPrimaryRecipient(role.getUser()); + } } return this; @@ -71,8 +106,10 @@ public MailBuilder addChairMemberRecipients() { public MailBuilder sendToThesisAdvisors(Thesis thesis) { for (ThesisRole role : thesis.getRoles()) { - if (role.getId().getRole() != ThesisRoleName.STUDENT) { - recipients.add(new MailRecipient(MimeMessage.RecipientType.TO, role.getUser().getEmail())); + if (role.getId().getRole() == ThesisRoleName.ADVISOR) { + addPrimaryRecipient(role.getUser()); + } else if (role.getId().getRole() == ThesisRoleName.SUPERVISOR) { + addSecondaryRecipient(role.getUser().getEmail()); } } @@ -82,23 +119,21 @@ public MailBuilder sendToThesisAdvisors(Thesis thesis) { public MailBuilder sendToThesisStudents(Thesis thesis) { for (ThesisRole role : thesis.getRoles()) { if (role.getId().getRole() == ThesisRoleName.STUDENT) { - recipients.add(new MailRecipient(MimeMessage.RecipientType.TO, role.getUser().getEmail())); + addPrimaryRecipient(role.getUser()); } else { - recipients.add(new MailRecipient(MimeMessage.RecipientType.CC, role.getUser().getEmail())); + addSecondaryRecipient(role.getUser().getEmail()); } } return this; } - public MailBuilder addRecipient(Message.RecipientType type, Address address) { - recipients.add(new MailRecipient(type, address)); + public MailBuilder fillUserPlaceholders(User user, String placeholder) { + HashMap> formatters = new HashMap<>(); - return this; - } + formatters.put(placeholder + ".enrolledAt", DataFormatter::formatDate); - public MailBuilder fillUserPlaceholders(User user, String placeholder) { - replaceDtoPlaceholders(UserDto.fromUserEntity(user), placeholder, new HashMap<>()); + replaceDtoPlaceholders(UserDto.fromUserEntity(user), placeholder, formatters); return this; } @@ -165,37 +200,43 @@ public void send(JavaMailSender mailSender, UploadService uploadService) { return; } - if (recipients.isEmpty()) { - return; - } + for (User recipient : primaryRecipients) { + try { + MimeMessage message = mailSender.createMimeMessage(); - try { - MimeMessage message = mailSender.createMimeMessage(); + message.setSubject(subject); + message.setSender(config.getSender()); + message.addRecipient(Message.RecipientType.TO, recipient.getEmail()); - message.setSubject(subject); - message.setSender(config.getSender()); + for (InternetAddress address : secondaryRecipients) { + message.addRecipient(Message.RecipientType.CC, address); + } - for (MailRecipient recipient : recipients) { - message.addRecipient(recipient.type(), recipient.address()); - } + for (InternetAddress address : bccRecipients) { + message.addRecipient(Message.RecipientType.BCC, address); + } - Multipart multipart = new MimeMultipart(); + Multipart multipart = new MimeMultipart(); - BodyPart messageBody = new MimeBodyPart(); - messageBody.setContent(content, "text/html; charset=utf-8"); - multipart.addBodyPart(messageBody); + BodyPart messageBody = new MimeBodyPart(); + messageBody.setContent( + content.replace("{{recipientName}}", Objects.requireNonNullElse(recipient.getFirstName(), "")), + "text/html; charset=utf-8" + ); + multipart.addBodyPart(messageBody); - for (String filename : attachments) { - MimeBodyPart attachment = new MimeBodyPart(); - attachment.attachFile(uploadService.load(filename).getFile()); - multipart.addBodyPart(attachment); - } + for (String filename : attachments) { + MimeBodyPart attachment = new MimeBodyPart(); + attachment.attachFile(uploadService.load(filename).getFile()); + multipart.addBodyPart(attachment); + } - message.setContent(multipart); + message.setContent(multipart); - mailSender.send(message); - } catch (MessagingException | IOException e) { - throw new MailingException("Failed to send email", e); + mailSender.send(message); + } catch (MessagingException | IOException e) { + throw new MailingException("Failed to send email", e); + } } } @@ -216,7 +257,7 @@ private void replaceDtoPlaceholders(Object dto, String dtoPrefix, Map defaultBccRecipients; - @NotBlank - private String mailTemplateLocation; + private final Path templateLocation; - public boolean isEnabled() { - return enabled; - } + @Autowired + public MailConfig( + @Value("${thesis-track.mail.enabled}") boolean enabled, + @Value("${thesis-track.mail.mail-template-location}") String mailTemplateLocation, + @Value("${thesis-track.mail.sender}") InternetAddress sender, + @Value("${thesis-track.mail.bcc-recipients}") String bccRecipientsList, + @Value("${thesis-track.mail.signature}") String mailSignature, + @Value("${thesis-track.mail.workspace-url}") String workspaceUrl, + UserRepository userRepository + ) { + this.enabled = enabled; + this.sender = sender; + this.workspaceUrl = workspaceUrl; + this.signature = mailSignature; - public InternetAddress getSender() { - try { - return new InternetAddress(sender); - } catch (AddressException e) { - throw new MailingException("Invalid email for sender"); - } - } + this.userRepository = userRepository; + this.templateLocation = Paths.get(mailTemplateLocation); - public List getChairMemberRecipients() { - if (chairMemberRecipients != null && !chairMemberRecipients.isEmpty()) { - List addresses = Arrays.asList(chairMemberRecipients.split(";")); + if (bccRecipientsList != null && !bccRecipientsList.isEmpty()) { + List addresses = Arrays.asList(bccRecipientsList.split(";")); addresses.removeIf(String::isEmpty); - return addresses.stream().map(address -> { + this.defaultBccRecipients = addresses.stream().map(address -> { try { return new InternetAddress(address); } catch (AddressException e) { @@ -71,30 +70,20 @@ public List getChairMemberRecipients() { } }).toList(); } else { - return new ArrayList<>(); + this.defaultBccRecipients = new ArrayList<>(); } } - public List getBccRecipients() { - if (bccRecipients != null && !bccRecipients.isEmpty()) { - List addresses = Arrays.asList(bccRecipients.split(";")); - addresses.removeIf(String::isEmpty); + public boolean isEnabled() { + return enabled; + } - return addresses.stream().map(address -> { - try { - return new InternetAddress(address); - } catch (AddressException e) { - throw new IllegalArgumentException("Invalid email address", e); - } - }).toList(); - } else { - return new ArrayList<>(); - } + public List getChairMembers() { + return userRepository.getChairMembers(); } public String getTemplate(String name) { - Path folder = Paths.get(mailTemplateLocation); - Path filePath = folder.resolve(name + ".html"); + Path filePath = templateLocation.resolve(name + ".html"); try { byte[] fileBytes = Files.readAllBytes(filePath); diff --git a/server/src/main/resources/application.yml b/server/src/main/resources/application.yml index 0dbde918..6604c6c4 100644 --- a/server/src/main/resources/application.yml +++ b/server/src/main/resources/application.yml @@ -57,7 +57,6 @@ thesis-track: sender: ${MAIL_SENDER:test@ios.ase.cit.tum.de} signature: ${MAIL_SIGNATURE:} workspace-url: ${MAIL_WORKSPACE_URL:https://slack.com} - chair-member-recipients: ${MAIL_CHAIR_RECIPIENTS:} bcc-recipients: ${MAIL_BCC_RECIPIENTS:} mail-template-location: ${MAIL_TEMPLATE_FOLDER:/default-mail-templates} storage: diff --git a/server/src/test/java/thesistrack/ls1/MailTest.java b/server/src/test/java/thesistrack/ls1/MailTest.java new file mode 100644 index 00000000..a5253ae0 --- /dev/null +++ b/server/src/test/java/thesistrack/ls1/MailTest.java @@ -0,0 +1,15 @@ +package thesistrack.ls1; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.ComponentScan; + +@ComponentScan(basePackages = "thesistrack.ls1") +class MailTest { + @Test + void testGreet() { + applicationRepository + + assertThat(result).isEqualTo("Hello, John!"); + } +} diff --git a/server/src/test/resources/application.yml b/server/src/test/resources/application.yml new file mode 100644 index 00000000..6604c6c4 --- /dev/null +++ b/server/src/test/resources/application.yml @@ -0,0 +1,67 @@ +logging: + level: + liquibase: INFO + +spring: + datasource: + url: ${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/thesis-track} + username: ${SPRING_DATASOURCE_USERNAME:thesis-track-postgres} + password: ${SPRING_DATASOURCE_PASSWORD:thesis-track-postgres} + driver-class-name: org.postgresql.Driver + liquibase: + change-log: db/changelog/db.changelog-master.xml + jpa: + hibernate: + ddl-auto: validate + show-sql: ${DEBUG_MODE:false} + properties: + hibernate: + dialect: org.hibernate.dialect.PostgreSQLDialect + format_sql: ${DEBUG_MODE:false} + security: + oauth2: + client: + registration: + keycloak: + client-id: ${KEYCLOAK_CLIENT_ID:thesis-track-app} + scope: openid + provider: + keycloak: + issuer-uri: ${KEYCLOAK_HOST:http://localhost:8081}/realms/${KEYCLOAK_REALM_NAME:thesis-track} + user-name-attribute: ${UNIVERSITY_ID_JWT_ATTRIBUTE:preferred_username} + resourceserver: + jwt: + jwk-set-uri: ${KEYCLOAK_HOST:http://localhost:8081}/realms/${KEYCLOAK_REALM_NAME:thesis-track}/protocol/openid-connect/certs + issuer-uri: ${KEYCLOAK_HOST:http://localhost:8081}/realms/${KEYCLOAK_REALM_NAME:thesis-track} + mail: + host: ${POSTFIX_HOST:localhost} + port: ${POSTFIX_PORT:25} + username: ${POSTFIX_USERNAME:} + password: ${POSTFIX_PASSWORD:} + properties: + mail: + transport: + protocol: smtp + smtp: + starttls: + enable: true + +thesis-track: + keycloak: + client-id: ${KEYCLOAK_CLIENT_ID:thesis-track-app} + university-id-jwt-attribute: ${UNIVERSITY_ID_JWT_ATTRIBUTE:preferred_username} + client: + host: ${CLIENT_HOST:http://localhost:3000} + mail: + enabled: ${MAIL_ENABLED:false} + sender: ${MAIL_SENDER:test@ios.ase.cit.tum.de} + signature: ${MAIL_SIGNATURE:} + workspace-url: ${MAIL_WORKSPACE_URL:https://slack.com} + bcc-recipients: ${MAIL_BCC_RECIPIENTS:} + mail-template-location: ${MAIL_TEMPLATE_FOLDER:/default-mail-templates} + storage: + upload-location: ${UPLOAD_FOLDER:uploads} + +server: + servlet: + context-path: /api From 57fbde825415d7535e29e79fcb270209e9859f35 Mon Sep 17 00:00:00 2001 From: Fabian Emilius Date: Thu, 22 Aug 2024 18:05:37 +0200 Subject: [PATCH 03/13] Add email content --- .../application-created-chair.html | 10 ++++++- .../application-created-student.html | 4 +++ .../thesis-assessment-added.html | 26 ++++++++++++++++++ server/mail-templates/thesis-closed.html | 7 +++++ .../mail-templates/thesis-comment-posted.html | 12 +++++++++ server/mail-templates/thesis-created.html | 15 +++++++++++ server/mail-templates/thesis-final-grade.html | 16 +++++++++++ .../thesis-final-submission.html | 9 +++++++ .../thesis-presentation-deleted.html | 7 +++++ .../thesis-presentation-scheduled.html | 27 +++++++++++++++++++ .../thesis-proposal-accepted.html | 10 +++++++ .../thesis-proposal-uploaded.html | 10 +++++++ .../thesistrack/ls1/utility/MailBuilder.java | 14 ++++++---- .../thesistrack/ls1/utility/MailConfig.java | 8 ++++-- 14 files changed, 167 insertions(+), 8 deletions(-) diff --git a/server/mail-templates/application-created-chair.html b/server/mail-templates/application-created-chair.html index d4a562a2..e2c06501 100644 --- a/server/mail-templates/application-created-chair.html +++ b/server/mail-templates/application-created-chair.html @@ -30,23 +30,31 @@

Desired Thesis Start Date:
- {{application.desiredThesisStart}} + {{application.desiredStartDate}}

Motivation:
{{application.motivation}}

+

Special Skills:
{{application.user.specialSkills}}

+

Interests:
{{application.user.interests}}

+

Projects:
{{application.user.projects}}

+
+ +

+ Full Details: {{applicationUrl}} +

You can find the submitted files in the attachment part of this email.

diff --git a/server/mail-templates/application-created-student.html b/server/mail-templates/application-created-student.html index 22de7529..a7cb6f78 100644 --- a/server/mail-templates/application-created-student.html +++ b/server/mail-templates/application-created-student.html @@ -36,17 +36,21 @@ Motivation:
{{application.motivation}}

+

Special Skills:
{{application.user.specialSkills}}

+

Interests:
{{application.user.interests}}

+

Projects:
{{application.user.projects}}

+

You can find the submitted files in the attachment part of this email.

diff --git a/server/mail-templates/thesis-assessment-added.html b/server/mail-templates/thesis-assessment-added.html index e69de29b..44f1f12e 100644 --- a/server/mail-templates/thesis-assessment-added.html +++ b/server/mail-templates/thesis-assessment-added.html @@ -0,0 +1,26 @@ +

Dear {{recipientName}},

+ +

An assessment has been added to thesis "{{thesis.thesisTitle}}"

+ +

+ Summary
+ {{assessment.summary}} +

+ +

+ Positives
+ {{assessment.positives}} +

+ +

+ Negatives
+ {{assessment.negatives}} +

+ +

+ Grade Suggestion: {{assessment.gradeSuggestion}} +

+ +

+ Full Details: {{thesisUrl}} +

diff --git a/server/mail-templates/thesis-closed.html b/server/mail-templates/thesis-closed.html index e69de29b..66e15e4c 100644 --- a/server/mail-templates/thesis-closed.html +++ b/server/mail-templates/thesis-closed.html @@ -0,0 +1,7 @@ +

Dear {{recipientName}},

+ +

The thesis "{{thesis.thesisTitle}}" has been closed.

+ +

+ Full Details: {{thesisUrl}} +

diff --git a/server/mail-templates/thesis-comment-posted.html b/server/mail-templates/thesis-comment-posted.html index e69de29b..aa9e31d6 100644 --- a/server/mail-templates/thesis-comment-posted.html +++ b/server/mail-templates/thesis-comment-posted.html @@ -0,0 +1,12 @@ +

Dear {{recipientName}},

+ +

A comment has been added to thesis "{{thesis.thesisTitle}}"

+ +

+ Message
+ {{comment.message}} +

+ +

+ Full Details: {{thesisUrl}} +

diff --git a/server/mail-templates/thesis-created.html b/server/mail-templates/thesis-created.html index e69de29b..8ad63107 100644 --- a/server/mail-templates/thesis-created.html +++ b/server/mail-templates/thesis-created.html @@ -0,0 +1,15 @@ +

Dear {{recipientName}},

+ +

+ {{thesisUrl}} +

+ +

+ A thesis has been created and assigned to you. + The next step is that you write a proposal and submit it to thesis track on the above URL. +

+ +

+ Title
+ {{thesis.thesisTitle}} +

diff --git a/server/mail-templates/thesis-final-grade.html b/server/mail-templates/thesis-final-grade.html index e69de29b..b588cf94 100644 --- a/server/mail-templates/thesis-final-grade.html +++ b/server/mail-templates/thesis-final-grade.html @@ -0,0 +1,16 @@ +

Dear {{recipientName}},

+ +

The final grade has been added to your thesis "{{thesis.thesisTitle}}"

+ +

+ Final Grade {{thesis.grade.finalGrade}} +

+ +

+ Positives
+ {{thesis.grade.feedback}} +

+ +

+ Full Details: {{thesisUrl}} +

diff --git a/server/mail-templates/thesis-final-submission.html b/server/mail-templates/thesis-final-submission.html index e69de29b..8987fad2 100644 --- a/server/mail-templates/thesis-final-submission.html +++ b/server/mail-templates/thesis-final-submission.html @@ -0,0 +1,9 @@ +

Dear {{recipientName}},

+ +

The thesis "{{thesis.thesisTitle}}" has been submitted and is ready for review.

+ +

The final thesis and presentation is attached to this email.

+ +

+ Full Details: {{thesisUrl}} +

diff --git a/server/mail-templates/thesis-presentation-deleted.html b/server/mail-templates/thesis-presentation-deleted.html index e69de29b..2a340c2a 100644 --- a/server/mail-templates/thesis-presentation-deleted.html +++ b/server/mail-templates/thesis-presentation-deleted.html @@ -0,0 +1,7 @@ +

Dear {{recipientName}},

+ +

A presentation scheduled at {{presentation.scheduledAt}}, has been cancelled for thesis "{{thesis.thesisTitle}}"

+ +

+ Full Details: {{thesisUrl}} +

diff --git a/server/mail-templates/thesis-presentation-scheduled.html b/server/mail-templates/thesis-presentation-scheduled.html index e69de29b..60e16c4f 100644 --- a/server/mail-templates/thesis-presentation-scheduled.html +++ b/server/mail-templates/thesis-presentation-scheduled.html @@ -0,0 +1,27 @@ +

Dear {{recipientName}},

+ +

A presentation has been scheduled for thesis "{{thesis.thesisTitle}}"

+ +

+ Type
+ {{presentation.type}} +

+ +

+ Location
+ {{presentation.location}} +

+ +

+ Stream URL
+ {{presentation.streamUrl}} +

+ +

+ Scheduled At
+ {{presentation.scheduledAt}} +

+ +

+ Full Details: {{thesisUrl}} +

diff --git a/server/mail-templates/thesis-proposal-accepted.html b/server/mail-templates/thesis-proposal-accepted.html index e69de29b..9993b4ca 100644 --- a/server/mail-templates/thesis-proposal-accepted.html +++ b/server/mail-templates/thesis-proposal-accepted.html @@ -0,0 +1,10 @@ +

Dear {{recipientName}},

+ +

+ Your proposal was accepted. The next step is to start writing your thesis. + You can see start and end date on the thesis page. +

+ +

+ Full Details: {{thesisUrl}} +

diff --git a/server/mail-templates/thesis-proposal-uploaded.html b/server/mail-templates/thesis-proposal-uploaded.html index e69de29b..dc73ffc5 100644 --- a/server/mail-templates/thesis-proposal-uploaded.html +++ b/server/mail-templates/thesis-proposal-uploaded.html @@ -0,0 +1,10 @@ +

Dear {{recipientName}},

+ +

+ An proposal has been uploaded to thesis "{{thesis.thesisTitle}}". + The proposal is attached to this email. +

+ +

+ Full Details: {{thesisUrl}} +

diff --git a/server/src/main/java/thesistrack/ls1/utility/MailBuilder.java b/server/src/main/java/thesistrack/ls1/utility/MailBuilder.java index 01408947..b93c227a 100644 --- a/server/src/main/java/thesistrack/ls1/utility/MailBuilder.java +++ b/server/src/main/java/thesistrack/ls1/utility/MailBuilder.java @@ -153,6 +153,8 @@ public MailBuilder fillApplicationPlaceholders(Application application) { replaceDtoPlaceholders(ApplicationDto.fromApplicationEntity(application, false), "application", formatters); + content = content.replace("{{applicationUrl}}", config.getClientHost() + "/applications/" + application.getId()); + return this; } @@ -164,6 +166,8 @@ public MailBuilder fillThesisPlaceholders(Thesis thesis) { replaceDtoPlaceholders(ThesisDto.fromThesisEntity(thesis, false), "thesis", formatters); + content = content.replace("{{thesisUrl}}", config.getClientHost() + "/theses/" + thesis.getId()); + return this; } @@ -195,7 +199,7 @@ public MailBuilder fillThesisAssessmentPlaceholders(ThesisAssessment assessment) return this; } - public void send(JavaMailSender mailSender, UploadService uploadService) { + public void send(JavaMailSender mailSender, UploadService uploadService) throws MailingException { if (!config.isEnabled()) { return; } @@ -216,22 +220,22 @@ public void send(JavaMailSender mailSender, UploadService uploadService) { message.addRecipient(Message.RecipientType.BCC, address); } - Multipart multipart = new MimeMultipart(); + Multipart messageContent = new MimeMultipart(); BodyPart messageBody = new MimeBodyPart(); messageBody.setContent( content.replace("{{recipientName}}", Objects.requireNonNullElse(recipient.getFirstName(), "")), "text/html; charset=utf-8" ); - multipart.addBodyPart(messageBody); + messageContent.addBodyPart(messageBody); for (String filename : attachments) { MimeBodyPart attachment = new MimeBodyPart(); attachment.attachFile(uploadService.load(filename).getFile()); - multipart.addBodyPart(attachment); + messageContent.addBodyPart(attachment); } - message.setContent(multipart); + message.setContent(messageContent); mailSender.send(message); } catch (MessagingException | IOException e) { diff --git a/server/src/main/java/thesistrack/ls1/utility/MailConfig.java b/server/src/main/java/thesistrack/ls1/utility/MailConfig.java index 44d2bf72..6b5d35c5 100644 --- a/server/src/main/java/thesistrack/ls1/utility/MailConfig.java +++ b/server/src/main/java/thesistrack/ls1/utility/MailConfig.java @@ -25,6 +25,10 @@ public class MailConfig { private final UserRepository userRepository; private final Boolean enabled; + private final Path templateLocation; + + @Getter + private final String clientHost; @Getter private final InternetAddress sender; @@ -38,8 +42,6 @@ public class MailConfig { @Getter private final List defaultBccRecipients; - private final Path templateLocation; - @Autowired public MailConfig( @Value("${thesis-track.mail.enabled}") boolean enabled, @@ -48,12 +50,14 @@ public MailConfig( @Value("${thesis-track.mail.bcc-recipients}") String bccRecipientsList, @Value("${thesis-track.mail.signature}") String mailSignature, @Value("${thesis-track.mail.workspace-url}") String workspaceUrl, + @Value("${thesis-track.client.host}") String clientHost, UserRepository userRepository ) { this.enabled = enabled; this.sender = sender; this.workspaceUrl = workspaceUrl; this.signature = mailSignature; + this.clientHost = clientHost; this.userRepository = userRepository; this.templateLocation = Paths.get(mailTemplateLocation); From 4f743a3a2ef7060daa03bc77bfceb47ea92ac0b1 Mon Sep 17 00:00:00 2001 From: Fabian Emilius Date: Thu, 22 Aug 2024 23:36:17 +0200 Subject: [PATCH 04/13] Fix email formatting --- .../application-created-chair.html | 2 +- .../application-created-student.html | 2 +- .../thesis-assessment-added.html | 2 +- server/mail-templates/thesis-closed.html | 2 +- .../mail-templates/thesis-comment-posted.html | 2 +- server/mail-templates/thesis-created.html | 2 +- server/mail-templates/thesis-final-grade.html | 4 +- .../thesis-final-submission.html | 2 +- .../thesis-presentation-deleted.html | 2 +- .../thesis-presentation-scheduled.html | 2 +- .../thesis-proposal-uploaded.html | 2 +- .../thesistrack/ls1/entity/TopicReviewer.java | 40 ----------- .../thesistrack/ls1/utility/MailBuilder.java | 2 +- .../test/java/thesistrack/ls1/MailTest.java | 15 ----- server/src/test/resources/application.yml | 67 ------------------- 15 files changed, 13 insertions(+), 135 deletions(-) delete mode 100644 server/src/main/java/thesistrack/ls1/entity/TopicReviewer.java delete mode 100644 server/src/test/java/thesistrack/ls1/MailTest.java delete mode 100644 server/src/test/resources/application.yml diff --git a/server/mail-templates/application-created-chair.html b/server/mail-templates/application-created-chair.html index e2c06501..412b3f27 100644 --- a/server/mail-templates/application-created-chair.html +++ b/server/mail-templates/application-created-chair.html @@ -1,5 +1,5 @@

Dear {{recipientName}},

-

there is a new thesis application submitted by {{student.firstName}} {{student.lastName}}.

+

there is a new thesis application submitted by {{application.user.firstName}} {{application.user.lastName}}.

We received the following thesis application details:


diff --git a/server/mail-templates/application-created-student.html b/server/mail-templates/application-created-student.html index a7cb6f78..3555b61e 100644 --- a/server/mail-templates/application-created-student.html +++ b/server/mail-templates/application-created-student.html @@ -30,7 +30,7 @@

Desired Thesis Start Date:
- {{application.desiredThesisStart}} + {{application.desiredStartDate}}

Motivation:
diff --git a/server/mail-templates/thesis-assessment-added.html b/server/mail-templates/thesis-assessment-added.html index 44f1f12e..1706d798 100644 --- a/server/mail-templates/thesis-assessment-added.html +++ b/server/mail-templates/thesis-assessment-added.html @@ -1,6 +1,6 @@

Dear {{recipientName}},

-

An assessment has been added to thesis "{{thesis.thesisTitle}}"

+

An assessment has been added to thesis "{{thesis.title}}"

Summary
diff --git a/server/mail-templates/thesis-closed.html b/server/mail-templates/thesis-closed.html index 66e15e4c..2eec293e 100644 --- a/server/mail-templates/thesis-closed.html +++ b/server/mail-templates/thesis-closed.html @@ -1,6 +1,6 @@

Dear {{recipientName}},

-

The thesis "{{thesis.thesisTitle}}" has been closed.

+

The thesis "{{thesis.title}}" has been closed.

Full Details: {{thesisUrl}} diff --git a/server/mail-templates/thesis-comment-posted.html b/server/mail-templates/thesis-comment-posted.html index aa9e31d6..9a00f50e 100644 --- a/server/mail-templates/thesis-comment-posted.html +++ b/server/mail-templates/thesis-comment-posted.html @@ -1,6 +1,6 @@

Dear {{recipientName}},

-

A comment has been added to thesis "{{thesis.thesisTitle}}"

+

A comment has been added to thesis "{{thesis.title}}"

Message
diff --git a/server/mail-templates/thesis-created.html b/server/mail-templates/thesis-created.html index 8ad63107..46aada29 100644 --- a/server/mail-templates/thesis-created.html +++ b/server/mail-templates/thesis-created.html @@ -11,5 +11,5 @@

Title
- {{thesis.thesisTitle}} + {{thesis.title}}

diff --git a/server/mail-templates/thesis-final-grade.html b/server/mail-templates/thesis-final-grade.html index b588cf94..2bd4b43f 100644 --- a/server/mail-templates/thesis-final-grade.html +++ b/server/mail-templates/thesis-final-grade.html @@ -1,13 +1,13 @@

Dear {{recipientName}},

-

The final grade has been added to your thesis "{{thesis.thesisTitle}}"

+

The final grade has been added to your thesis "{{thesis.title}}"

Final Grade {{thesis.grade.finalGrade}}

- Positives
+ Feedback
{{thesis.grade.feedback}}

diff --git a/server/mail-templates/thesis-final-submission.html b/server/mail-templates/thesis-final-submission.html index 8987fad2..4a55d517 100644 --- a/server/mail-templates/thesis-final-submission.html +++ b/server/mail-templates/thesis-final-submission.html @@ -1,6 +1,6 @@

Dear {{recipientName}},

-

The thesis "{{thesis.thesisTitle}}" has been submitted and is ready for review.

+

The thesis "{{thesis.title}}" has been submitted and is ready for review.

The final thesis and presentation is attached to this email.

diff --git a/server/mail-templates/thesis-presentation-deleted.html b/server/mail-templates/thesis-presentation-deleted.html index 2a340c2a..d72a4988 100644 --- a/server/mail-templates/thesis-presentation-deleted.html +++ b/server/mail-templates/thesis-presentation-deleted.html @@ -1,6 +1,6 @@

Dear {{recipientName}},

-

A presentation scheduled at {{presentation.scheduledAt}}, has been cancelled for thesis "{{thesis.thesisTitle}}"

+

A presentation scheduled at {{presentation.scheduledAt}}, has been cancelled for thesis "{{thesis.title}}"

Full Details: {{thesisUrl}} diff --git a/server/mail-templates/thesis-presentation-scheduled.html b/server/mail-templates/thesis-presentation-scheduled.html index 60e16c4f..e2ae5d4d 100644 --- a/server/mail-templates/thesis-presentation-scheduled.html +++ b/server/mail-templates/thesis-presentation-scheduled.html @@ -1,6 +1,6 @@

Dear {{recipientName}},

-

A presentation has been scheduled for thesis "{{thesis.thesisTitle}}"

+

A presentation has been scheduled for thesis "{{thesis.title}}"

Type
diff --git a/server/mail-templates/thesis-proposal-uploaded.html b/server/mail-templates/thesis-proposal-uploaded.html index dc73ffc5..2640f517 100644 --- a/server/mail-templates/thesis-proposal-uploaded.html +++ b/server/mail-templates/thesis-proposal-uploaded.html @@ -1,7 +1,7 @@

Dear {{recipientName}},

- An proposal has been uploaded to thesis "{{thesis.thesisTitle}}". + An proposal has been uploaded to thesis "{{thesis.title}}". The proposal is attached to this email.

diff --git a/server/src/main/java/thesistrack/ls1/entity/TopicReviewer.java b/server/src/main/java/thesistrack/ls1/entity/TopicReviewer.java deleted file mode 100644 index 54525334..00000000 --- a/server/src/main/java/thesistrack/ls1/entity/TopicReviewer.java +++ /dev/null @@ -1,40 +0,0 @@ -package thesistrack.ls1.entity; - -import jakarta.persistence.*; -import jakarta.validation.constraints.NotNull; -import lombok.Getter; -import lombok.Setter; -import org.hibernate.annotations.CreationTimestamp; -import thesistrack.ls1.entity.key.TopicReviewerId; - -import java.time.Instant; - -@Getter -@Setter -@Entity -@Table(name = "topic_reviewers") -public class TopicReviewer { - @EmbeddedId - private TopicReviewerId id; - - @MapsId("topicId") - @ManyToOne(fetch = FetchType.LAZY, optional = false) - @JoinColumn(name = "topic_id", nullable = false) - private Topic topic; - - @MapsId("userId") - @ManyToOne(fetch = FetchType.LAZY, optional = false) - @JoinColumn(name = "user_id", nullable = false) - private User user; - - @CreationTimestamp - @NotNull - @Column(name = "assigned_at", nullable = false) - private Instant assignedAt; - - @NotNull - @ManyToOne(fetch = FetchType.LAZY, optional = false) - @JoinColumn(name = "assigned_by", nullable = false) - private User assignedBy; - -} \ No newline at end of file diff --git a/server/src/main/java/thesistrack/ls1/utility/MailBuilder.java b/server/src/main/java/thesistrack/ls1/utility/MailBuilder.java index b93c227a..786a061c 100644 --- a/server/src/main/java/thesistrack/ls1/utility/MailBuilder.java +++ b/server/src/main/java/thesistrack/ls1/utility/MailBuilder.java @@ -148,7 +148,7 @@ public MailBuilder fillApplicationPlaceholders(Application application) { return Objects.requireNonNullElse(application.getThesisTitle(), ""); }); - formatters.put("application.desiredThesisStart", DataFormatter::formatDate); + formatters.put("application.desiredStartDate", DataFormatter::formatDate); formatters.put("application.user.enrolledAt", DataFormatter::formatDate); replaceDtoPlaceholders(ApplicationDto.fromApplicationEntity(application, false), "application", formatters); diff --git a/server/src/test/java/thesistrack/ls1/MailTest.java b/server/src/test/java/thesistrack/ls1/MailTest.java deleted file mode 100644 index a5253ae0..00000000 --- a/server/src/test/java/thesistrack/ls1/MailTest.java +++ /dev/null @@ -1,15 +0,0 @@ -package thesistrack.ls1; - -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.ComponentScan; - -@ComponentScan(basePackages = "thesistrack.ls1") -class MailTest { - @Test - void testGreet() { - applicationRepository - - assertThat(result).isEqualTo("Hello, John!"); - } -} diff --git a/server/src/test/resources/application.yml b/server/src/test/resources/application.yml deleted file mode 100644 index 6604c6c4..00000000 --- a/server/src/test/resources/application.yml +++ /dev/null @@ -1,67 +0,0 @@ -logging: - level: - liquibase: INFO - -spring: - datasource: - url: ${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/thesis-track} - username: ${SPRING_DATASOURCE_USERNAME:thesis-track-postgres} - password: ${SPRING_DATASOURCE_PASSWORD:thesis-track-postgres} - driver-class-name: org.postgresql.Driver - liquibase: - change-log: db/changelog/db.changelog-master.xml - jpa: - hibernate: - ddl-auto: validate - show-sql: ${DEBUG_MODE:false} - properties: - hibernate: - dialect: org.hibernate.dialect.PostgreSQLDialect - format_sql: ${DEBUG_MODE:false} - security: - oauth2: - client: - registration: - keycloak: - client-id: ${KEYCLOAK_CLIENT_ID:thesis-track-app} - scope: openid - provider: - keycloak: - issuer-uri: ${KEYCLOAK_HOST:http://localhost:8081}/realms/${KEYCLOAK_REALM_NAME:thesis-track} - user-name-attribute: ${UNIVERSITY_ID_JWT_ATTRIBUTE:preferred_username} - resourceserver: - jwt: - jwk-set-uri: ${KEYCLOAK_HOST:http://localhost:8081}/realms/${KEYCLOAK_REALM_NAME:thesis-track}/protocol/openid-connect/certs - issuer-uri: ${KEYCLOAK_HOST:http://localhost:8081}/realms/${KEYCLOAK_REALM_NAME:thesis-track} - mail: - host: ${POSTFIX_HOST:localhost} - port: ${POSTFIX_PORT:25} - username: ${POSTFIX_USERNAME:} - password: ${POSTFIX_PASSWORD:} - properties: - mail: - transport: - protocol: smtp - smtp: - starttls: - enable: true - -thesis-track: - keycloak: - client-id: ${KEYCLOAK_CLIENT_ID:thesis-track-app} - university-id-jwt-attribute: ${UNIVERSITY_ID_JWT_ATTRIBUTE:preferred_username} - client: - host: ${CLIENT_HOST:http://localhost:3000} - mail: - enabled: ${MAIL_ENABLED:false} - sender: ${MAIL_SENDER:test@ios.ase.cit.tum.de} - signature: ${MAIL_SIGNATURE:} - workspace-url: ${MAIL_WORKSPACE_URL:https://slack.com} - bcc-recipients: ${MAIL_BCC_RECIPIENTS:} - mail-template-location: ${MAIL_TEMPLATE_FOLDER:/default-mail-templates} - storage: - upload-location: ${UPLOAD_FOLDER:uploads} - -server: - servlet: - context-path: /api From 34f3f6d8d82eb9bad59fb9e396c0d40b62ae8d88 Mon Sep 17 00:00:00 2001 From: Fabian Emilius Date: Thu, 22 Aug 2024 23:54:41 +0200 Subject: [PATCH 05/13] Fix wording --- server/mail-templates/thesis-closed.html | 2 ++ server/mail-templates/thesis-created.html | 6 ++---- server/mail-templates/thesis-final-grade.html | 2 ++ server/mail-templates/thesis-proposal-uploaded.html | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/server/mail-templates/thesis-closed.html b/server/mail-templates/thesis-closed.html index 2eec293e..efd20dbd 100644 --- a/server/mail-templates/thesis-closed.html +++ b/server/mail-templates/thesis-closed.html @@ -5,3 +5,5 @@

Full Details: {{thesisUrl}}

+ +{{config.signature}} \ No newline at end of file diff --git a/server/mail-templates/thesis-created.html b/server/mail-templates/thesis-created.html index 46aada29..2d104b98 100644 --- a/server/mail-templates/thesis-created.html +++ b/server/mail-templates/thesis-created.html @@ -6,10 +6,8 @@

A thesis has been created and assigned to you. + The title is "{{thesis.title}}". The next step is that you write a proposal and submit it to thesis track on the above URL.

-

- Title
- {{thesis.title}} -

+{{config.signature}} diff --git a/server/mail-templates/thesis-final-grade.html b/server/mail-templates/thesis-final-grade.html index 2bd4b43f..1cf54c77 100644 --- a/server/mail-templates/thesis-final-grade.html +++ b/server/mail-templates/thesis-final-grade.html @@ -14,3 +14,5 @@

Full Details: {{thesisUrl}}

+ +{{config.signature}} \ No newline at end of file diff --git a/server/mail-templates/thesis-proposal-uploaded.html b/server/mail-templates/thesis-proposal-uploaded.html index 2640f517..94b792c8 100644 --- a/server/mail-templates/thesis-proposal-uploaded.html +++ b/server/mail-templates/thesis-proposal-uploaded.html @@ -1,7 +1,7 @@

Dear {{recipientName}},

- An proposal has been uploaded to thesis "{{thesis.title}}". + A proposal has been uploaded to thesis "{{thesis.title}}". The proposal is attached to this email.

From aa6b8dad67abe84a2fbef02dd05537e0fd4997df Mon Sep 17 00:00:00 2001 From: Fabian Emilius Date: Mon, 26 Aug 2024 13:47:47 +0200 Subject: [PATCH 06/13] use active wording for emails instead of passive wording --- .../application-created-chair.html | 1 + .../application-created-student.html | 1 + .../thesis-assessment-added.html | 4 +- server/mail-templates/thesis-closed.html | 5 ++- .../mail-templates/thesis-comment-posted.html | 4 +- server/mail-templates/thesis-created.html | 13 ++++-- server/mail-templates/thesis-final-grade.html | 6 ++- .../thesis-final-submission.html | 9 ++++- .../thesis-presentation-deleted.html | 4 +- .../thesis-presentation-scheduled.html | 4 +- .../thesis-proposal-accepted.html | 9 ++--- .../thesis-proposal-uploaded.html | 4 +- .../ls1/controller/ThesisController.java | 4 +- .../ls1/service/ApplicationService.java | 15 +++---- .../ls1/service/MailingService.java | 20 +++++++--- .../ls1/service/ThesisCommentService.java | 4 +- .../ls1/service/ThesisService.java | 40 +++++++++++-------- .../ls1/utility/DataFormatter.java | 20 ++++++++++ .../thesistrack/ls1/utility/MailBuilder.java | 17 ++++++++ 19 files changed, 128 insertions(+), 56 deletions(-) diff --git a/server/mail-templates/application-created-chair.html b/server/mail-templates/application-created-chair.html index 412b3f27..506ca82e 100644 --- a/server/mail-templates/application-created-chair.html +++ b/server/mail-templates/application-created-chair.html @@ -1,4 +1,5 @@

Dear {{recipientName}},

+

there is a new thesis application submitted by {{application.user.firstName}} {{application.user.lastName}}.

We received the following thesis application details:

diff --git a/server/mail-templates/application-created-student.html b/server/mail-templates/application-created-student.html index 3555b61e..0c099b11 100644 --- a/server/mail-templates/application-created-student.html +++ b/server/mail-templates/application-created-student.html @@ -1,4 +1,5 @@

Dear {{recipientName}},

+

With this email we confirm your successful thesis application submission.

We received the following thesis application details:

diff --git a/server/mail-templates/thesis-assessment-added.html b/server/mail-templates/thesis-assessment-added.html index 1706d798..da9321d3 100644 --- a/server/mail-templates/thesis-assessment-added.html +++ b/server/mail-templates/thesis-assessment-added.html @@ -1,6 +1,8 @@

Dear {{recipientName}},

-

An assessment has been added to thesis "{{thesis.title}}"

+

+ {{assessment.createdBy.firstName}} {{assessment.createdBy.lastName}} added an assessment to thesis "{{thesis.title}}" +

Summary
diff --git a/server/mail-templates/thesis-closed.html b/server/mail-templates/thesis-closed.html index efd20dbd..5e27d71d 100644 --- a/server/mail-templates/thesis-closed.html +++ b/server/mail-templates/thesis-closed.html @@ -1,6 +1,9 @@

Dear {{recipientName}},

-

The thesis "{{thesis.title}}" has been closed.

+

+ {{deletingUser.firstName}} {{deletingUser.lastName}} closed thesis "{{thesis.title}}". + Please contact your advisor or supervisor if you think that this was a mistake. +

Full Details: {{thesisUrl}} diff --git a/server/mail-templates/thesis-comment-posted.html b/server/mail-templates/thesis-comment-posted.html index 9a00f50e..14e8cb03 100644 --- a/server/mail-templates/thesis-comment-posted.html +++ b/server/mail-templates/thesis-comment-posted.html @@ -1,6 +1,8 @@

Dear {{recipientName}},

-

A comment has been added to thesis "{{thesis.title}}"

+

+ {{comment.createdBy.firstName}} {{comment.createdBy.lastName}} posted a comment on thesis "{{thesis.title}}" +

Message
diff --git a/server/mail-templates/thesis-created.html b/server/mail-templates/thesis-created.html index 2d104b98..b45aa82c 100644 --- a/server/mail-templates/thesis-created.html +++ b/server/mail-templates/thesis-created.html @@ -1,13 +1,18 @@

Dear {{recipientName}},

- {{thesisUrl}} + {{creatingUser.firstName}} {{creatingUser.lastName}} created and assigned a thesis to you: {{thesisUrl}}

- A thesis has been created and assigned to you. - The title is "{{thesis.title}}". - The next step is that you write a proposal and submit it to thesis track on the above URL. + Title: {{thesis.title}}
+ Supervisor: {{thesis.supervisors}}
+ Advisor: {{thesis.advisors}}
+ Student: {{thesis.students}}
+

+ +

+ The next step is that you write a proposal and submit it on {{thesisUrl}}

{{config.signature}} diff --git a/server/mail-templates/thesis-final-grade.html b/server/mail-templates/thesis-final-grade.html index 1cf54c77..abd26fe5 100644 --- a/server/mail-templates/thesis-final-grade.html +++ b/server/mail-templates/thesis-final-grade.html @@ -1,9 +1,11 @@

Dear {{recipientName}},

-

The final grade has been added to your thesis "{{thesis.title}}"

+

+ {{thesis.supervisors}} added the final grade to your thesis "{{thesis.title}}" +

- Final Grade {{thesis.grade.finalGrade}} + Final Grade: {{thesis.grade.finalGrade}}

diff --git a/server/mail-templates/thesis-final-submission.html b/server/mail-templates/thesis-final-submission.html index 4a55d517..5fd8322c 100644 --- a/server/mail-templates/thesis-final-submission.html +++ b/server/mail-templates/thesis-final-submission.html @@ -1,8 +1,13 @@

Dear {{recipientName}},

-

The thesis "{{thesis.title}}" has been submitted and is ready for review.

+

+ {{thesis.students}} submitted thesis "{{thesis.title}}". +

-

The final thesis and presentation is attached to this email.

+

+ You can find the submitted files in the attachment part of this email. + The next step is to write an assessment about the thesis. +

Full Details: {{thesisUrl}} diff --git a/server/mail-templates/thesis-presentation-deleted.html b/server/mail-templates/thesis-presentation-deleted.html index d72a4988..04af2b22 100644 --- a/server/mail-templates/thesis-presentation-deleted.html +++ b/server/mail-templates/thesis-presentation-deleted.html @@ -1,6 +1,8 @@

Dear {{recipientName}},

-

A presentation scheduled at {{presentation.scheduledAt}}, has been cancelled for thesis "{{thesis.title}}"

+

+ {{deletingUser.firstName}} {{deletingUser.lastName}} cancelled the presentation scheduled at {{presentation.scheduledAt}} for thesis "{{thesis.title}}" +

Full Details: {{thesisUrl}} diff --git a/server/mail-templates/thesis-presentation-scheduled.html b/server/mail-templates/thesis-presentation-scheduled.html index e2ae5d4d..71fad98f 100644 --- a/server/mail-templates/thesis-presentation-scheduled.html +++ b/server/mail-templates/thesis-presentation-scheduled.html @@ -1,6 +1,8 @@

Dear {{recipientName}},

-

A presentation has been scheduled for thesis "{{thesis.title}}"

+

+ {{presentation.createdBy.firstName}} {{presentation.createdBy.lastName}} scheduled a presentation for thesis "{{thesis.title}}" +

Type
diff --git a/server/mail-templates/thesis-proposal-accepted.html b/server/mail-templates/thesis-proposal-accepted.html index 9993b4ca..16267884 100644 --- a/server/mail-templates/thesis-proposal-accepted.html +++ b/server/mail-templates/thesis-proposal-accepted.html @@ -1,10 +1,7 @@

Dear {{recipientName}},

- Your proposal was accepted. The next step is to start writing your thesis. - You can see start and end date on the thesis page. -

- -

- Full Details: {{thesisUrl}} + {{proposal.approvedBy.firstName}} {{proposal.approvedBy.lastName}} approved the proposal of thesis "{{thesis.title}}". + The next step is to start writing the thesis. + You can see your submission deadline on {{thesisUrl}}.

diff --git a/server/mail-templates/thesis-proposal-uploaded.html b/server/mail-templates/thesis-proposal-uploaded.html index 94b792c8..ed7dfb5c 100644 --- a/server/mail-templates/thesis-proposal-uploaded.html +++ b/server/mail-templates/thesis-proposal-uploaded.html @@ -1,8 +1,8 @@

Dear {{recipientName}},

- A proposal has been uploaded to thesis "{{thesis.title}}". - The proposal is attached to this email. + {{proposal.createdBy.firstName}} {{proposal.createdBy.lastName}} uploaded a proposal to thesis "{{thesis.title}}". + You can find the submitted file in the attachment part of this email.

diff --git a/server/src/main/java/thesistrack/ls1/controller/ThesisController.java b/server/src/main/java/thesistrack/ls1/controller/ThesisController.java index c1302700..5bb4c292 100644 --- a/server/src/main/java/thesistrack/ls1/controller/ThesisController.java +++ b/server/src/main/java/thesistrack/ls1/controller/ThesisController.java @@ -169,7 +169,7 @@ public ResponseEntity closeThesis( throw new AccessDeniedException("You do not have the required permissions to view this thesis"); } - thesis = thesisService.closeThesis(thesis); + thesis = thesisService.closeThesis(authenticatedUser, thesis); return ResponseEntity.ok(ThesisDto.fromThesisEntity(thesis, thesis.hasAdvisorAccess(authenticatedUser))); } @@ -381,7 +381,7 @@ public ResponseEntity deletePresentation( throw new AccessDeniedException("You are not allowed to delete this presentation"); } - Thesis thesis = thesisService.deletePresentation(presentation); + Thesis thesis = thesisService.deletePresentation(authenticatedUser, presentation); return ResponseEntity.ok(ThesisDto.fromThesisEntity(thesis, thesis.hasAdvisorAccess(authenticatedUser))); } diff --git a/server/src/main/java/thesistrack/ls1/service/ApplicationService.java b/server/src/main/java/thesistrack/ls1/service/ApplicationService.java index 58f1d5a9..1cf1318e 100644 --- a/server/src/main/java/thesistrack/ls1/service/ApplicationService.java +++ b/server/src/main/java/thesistrack/ls1/service/ApplicationService.java @@ -27,7 +27,6 @@ public class ApplicationService { private final MailingService mailingService; private final TopicRepository topicRepository; private final ThesisService thesisService; - private final UserService userService; @Autowired public ApplicationService( @@ -36,8 +35,7 @@ public ApplicationService( UploadService storageService, MailingService mailingService, TopicRepository topicRepository, - ThesisService thesisService, - UserService userService + ThesisService thesisService ) { this.applicationRepository = applicationRepository; this.userRepository = userRepository; @@ -46,7 +44,6 @@ public ApplicationService( this.mailingService = mailingService; this.topicRepository = topicRepository; this.thesisService = thesisService; - this.userService = userService; } public Page getAll( @@ -132,7 +129,7 @@ public Application createLegacyApplication( @Transactional public Application accept( - User reviewer, + User reviewingUser, Application application, String thesisTitle, String thesisType, @@ -145,10 +142,10 @@ public Application accept( application.setState(ApplicationState.ACCEPTED); application.setComment(comment); application.setReviewedAt(Instant.now()); - application.setReviewedBy(reviewer); + application.setReviewedBy(reviewingUser); Thesis thesis = thesisService.createThesis( - reviewer, + reviewingUser, thesisTitle, thesisType, supervisorIds, @@ -173,11 +170,11 @@ public Application accept( } @Transactional - public Application reject(User reviewer, Application application, String comment, boolean notifyUser) { + public Application reject(User reviewingUser, Application application, String comment, boolean notifyUser) { application.setState(ApplicationState.REJECTED); application.setComment(comment); application.setReviewedAt(Instant.now()); - application.setReviewedBy(reviewer); + application.setReviewedBy(reviewingUser); if (notifyUser) { mailingService.sendApplicationRejectionEmail(application); diff --git a/server/src/main/java/thesistrack/ls1/service/MailingService.java b/server/src/main/java/thesistrack/ls1/service/MailingService.java index 83762d7d..07876b37 100644 --- a/server/src/main/java/thesistrack/ls1/service/MailingService.java +++ b/server/src/main/java/thesistrack/ls1/service/MailingService.java @@ -71,38 +71,42 @@ public void sendApplicationRejectionEmail(Application application) throws Mailin .send(javaMailSender, uploadService); } - public void sendThesisCreatedEmail(Thesis thesis) { + public void sendThesisCreatedEmail(User creatingUser, Thesis thesis) { MailBuilder builder = new MailBuilder(config, "Thesis Created", "thesis-created"); builder .sendToThesisStudents(thesis) .addDefaultBccRecipients() .fillThesisPlaceholders(thesis) + .fillUserPlaceholders(creatingUser, "creatingUser") .send(javaMailSender, uploadService); } - public void sendThesisClosedEmail(Thesis thesis) { + public void sendThesisClosedEmail(User deletingUser, Thesis thesis) { MailBuilder builder = new MailBuilder(config, "Thesis Closed", "thesis-closed"); builder .sendToThesisStudents(thesis) .addDefaultBccRecipients() .fillThesisPlaceholders(thesis) + .fillUserPlaceholders(deletingUser, "deletingUser") .send(javaMailSender, uploadService); } public void sendProposalUploadedEmail(ThesisProposal proposal) { MailBuilder builder = new MailBuilder(config, "Thesis Proposal Added", "thesis-proposal-uploaded"); builder + .addPrimarySender(proposal.getCreatedBy()) .sendToThesisAdvisors(proposal.getThesis()) .fillThesisProposalPlaceholders(proposal) .addAttachmentFile(proposal.getProposalFilename()) .send(javaMailSender, uploadService); } - public void sendProposalAcceptedEmail(Thesis thesis) { + public void sendProposalAcceptedEmail(ThesisProposal proposal) { MailBuilder builder = new MailBuilder(config, "Thesis Proposal Accepted", "thesis-proposal-accepted"); builder - .sendToThesisStudents(thesis) - .fillThesisPlaceholders(thesis) + .addPrimarySender(proposal.getApprovedBy()) + .sendToThesisStudents(proposal.getThesis()) + .fillThesisPlaceholders(proposal.getThesis()) .send(javaMailSender, uploadService); } @@ -116,6 +120,7 @@ public void sendNewCommentEmail(ThesisComment comment) { } builder + .addPrimarySender(comment.getCreatedBy()) .fillThesisCommentPlaceholders(comment) .addAttachmentFile(comment.getFilename()) .send(javaMailSender, uploadService); @@ -124,18 +129,20 @@ public void sendNewCommentEmail(ThesisComment comment) { public void sendNewScheduledPresentationEmail(ThesisPresentation presentation) { MailBuilder builder = new MailBuilder(config, "New Presentation scheduled", "thesis-presentation-scheduled"); builder + .addPrimarySender(presentation.getCreatedBy()) .sendToThesisStudents(presentation.getThesis()) .addDefaultBccRecipients() .fillThesisPresentationPlaceholders(presentation) .send(javaMailSender, uploadService); } - public void sendPresentationDeletedEmail(ThesisPresentation presentation) { + public void sendPresentationDeletedEmail(User deletingUser, ThesisPresentation presentation) { MailBuilder builder = new MailBuilder(config, "Presentation deleted", "thesis-presentation-deleted"); builder .sendToThesisStudents(presentation.getThesis()) .addDefaultBccRecipients() .fillThesisPresentationPlaceholders(presentation) + .fillUserPlaceholders(deletingUser, "deletingUser") .send(javaMailSender, uploadService); } @@ -153,6 +160,7 @@ public void sendFinalSubmissionEmail(Thesis thesis) { public void sendAssessmentAddedEmail(ThesisAssessment assessment) { MailBuilder builder = new MailBuilder(config, "Assessment added", "thesis-assessment-added"); builder + .addPrimarySender(assessment.getCreatedBy()) .sendToThesisSupervisors(assessment.getThesis()) .fillThesisAssessmentPlaceholders(assessment) .send(javaMailSender, uploadService); diff --git a/server/src/main/java/thesistrack/ls1/service/ThesisCommentService.java b/server/src/main/java/thesistrack/ls1/service/ThesisCommentService.java index fc8605d4..d7eded44 100644 --- a/server/src/main/java/thesistrack/ls1/service/ThesisCommentService.java +++ b/server/src/main/java/thesistrack/ls1/service/ThesisCommentService.java @@ -36,14 +36,14 @@ public Page getComments(Thesis thesis, ThesisCommentType commentT ); } - public ThesisComment postComment(User creator, Thesis thesis, ThesisCommentType commentType, String message, MultipartFile file) { + public ThesisComment postComment(User postingUser, Thesis thesis, ThesisCommentType commentType, String message, MultipartFile file) { ThesisComment comment = new ThesisComment(); comment.setType(commentType); comment.setThesis(thesis); comment.setMessage(message); comment.setCreatedAt(Instant.now()); - comment.setCreatedBy(creator); + comment.setCreatedBy(postingUser); if (file != null) { comment.setFilename(uploadService.store(file, 3 * 1024 * 1024)); diff --git a/server/src/main/java/thesistrack/ls1/service/ThesisService.java b/server/src/main/java/thesistrack/ls1/service/ThesisService.java index 494c2274..021fdd05 100644 --- a/server/src/main/java/thesistrack/ls1/service/ThesisService.java +++ b/server/src/main/java/thesistrack/ls1/service/ThesisService.java @@ -103,13 +103,13 @@ public Thesis createThesis( assignThesisRoles(thesis, creator, supervisorIds, advisorIds, studentIds); saveStateChange(thesis, ThesisState.PROPOSAL, Instant.now()); - mailingService.sendThesisCreatedEmail(thesis); + mailingService.sendThesisCreatedEmail(creator, thesis); return thesis; } @Transactional - public Thesis closeThesis(Thesis thesis) { + public Thesis closeThesis(User closingUser, Thesis thesis) { if (thesis.getState() == ThesisState.DROPPED_OUT || thesis.getState() == ThesisState.FINISHED) { throw new ResourceInvalidParametersException("Thesis is already completed"); } @@ -119,14 +119,14 @@ public Thesis closeThesis(Thesis thesis) { thesis = thesisRepository.save(thesis); - mailingService.sendThesisClosedEmail(thesis); + mailingService.sendThesisClosedEmail(closingUser, thesis); return thesis; } @Transactional public Thesis updateThesis( - User updater, + User updatingUser, Thesis thesis, String thesisTitle, String thesisType, @@ -149,7 +149,7 @@ public Thesis updateThesis( thesis.setStartDate(startDate); thesis.setEndDate(endDate); - assignThesisRoles(thesis, updater, supervisorIds, advisorIds, studentIds); + assignThesisRoles(thesis, updatingUser, supervisorIds, advisorIds, studentIds); for (ThesisStatePayload state : states) { saveStateChange(thesis, state.state(), state.changedAt()); @@ -183,13 +183,13 @@ public Resource getProposalFile(Thesis thesis) { } @Transactional - public Thesis uploadProposal(User uploader, Thesis thesis, MultipartFile proposalFile) { + public Thesis uploadProposal(User uploadingUser, Thesis thesis, MultipartFile proposalFile) { ThesisProposal proposal = new ThesisProposal(); proposal.setThesis(thesis); proposal.setProposalFilename(uploadService.store(proposalFile, 3 * 1024 * 1024)); proposal.setCreatedAt(Instant.now()); - proposal.setCreatedBy(uploader); + proposal.setCreatedBy(uploadingUser); List proposals = thesis.getProposals() == null ? new ArrayList<>() : thesis.getProposals(); proposals.addFirst(proposal); @@ -205,7 +205,7 @@ public Thesis uploadProposal(User uploader, Thesis thesis, MultipartFile proposa } @Transactional - public Thesis acceptProposal(User reviewer, Thesis thesis) { + public Thesis acceptProposal(User reviewingUser, Thesis thesis) { List proposals = thesis.getProposals(); if (proposals == null || proposals.isEmpty()) { @@ -215,7 +215,7 @@ public Thesis acceptProposal(User reviewer, Thesis thesis) { ThesisProposal proposal = proposals.getFirst(); proposal.setApprovedAt(Instant.now()); - proposal.setApprovedBy(reviewer); + proposal.setApprovedBy(reviewingUser); thesisProposalRepository.save(proposal); @@ -223,7 +223,7 @@ public Thesis acceptProposal(User reviewer, Thesis thesis) { thesis.setState(ThesisState.WRITING); - mailingService.sendProposalAcceptedEmail(thesis); + mailingService.sendProposalAcceptedEmail(proposal); return thesisRepository.save(thesis); } @@ -279,7 +279,15 @@ public Resource getThesisFile(Thesis thesis) { return uploadService.load(filename); } - public Thesis createPresentation(User creator, Thesis thesis, ThesisPresentationType type, ThesisPresentationVisibility visibility, String location, String streamUrl, Instant date) { + public Thesis createPresentation( + User creatingUser, + Thesis thesis, + ThesisPresentationType type, + ThesisPresentationVisibility visibility, + String location, + String streamUrl, + Instant date + ) { ThesisPresentation presentation = new ThesisPresentation(); presentation.setThesis(thesis); @@ -288,7 +296,7 @@ public Thesis createPresentation(User creator, Thesis thesis, ThesisPresentation presentation.setLocation(location); presentation.setStreamUrl(streamUrl); presentation.setScheduledAt(date); - presentation.setCreatedBy(creator); + presentation.setCreatedBy(creatingUser); presentation.setCreatedAt(Instant.now()); presentation = thesisPresentationRepository.save(presentation); @@ -305,7 +313,7 @@ public Thesis createPresentation(User creator, Thesis thesis, ThesisPresentation return thesis; } - public Thesis deletePresentation(ThesisPresentation presentation) { + public Thesis deletePresentation(User deletingUser, ThesisPresentation presentation) { Thesis thesis = presentation.getThesis(); thesisPresentationRepository.deleteById(presentation.getId()); @@ -316,7 +324,7 @@ public Thesis deletePresentation(ThesisPresentation presentation) { thesis.setPresentations(presentations); - mailingService.sendPresentationDeletedEmail(presentation); + mailingService.sendPresentationDeletedEmail(deletingUser, presentation); return thesisRepository.save(thesis); } @@ -324,7 +332,7 @@ public Thesis deletePresentation(ThesisPresentation presentation) { /* ASSESSMENT */ @Transactional public Thesis submitAssessment( - User creator, + User creatingUser, Thesis thesis, String summary, String positives, @@ -334,7 +342,7 @@ public Thesis submitAssessment( ThesisAssessment assessment = new ThesisAssessment(); assessment.setThesis(thesis); - assessment.setCreatedBy(creator); + assessment.setCreatedBy(creatingUser); assessment.setCreatedAt(Instant.now()); assessment.setSummary(summary); assessment.setPositives(positives); diff --git a/server/src/main/java/thesistrack/ls1/utility/DataFormatter.java b/server/src/main/java/thesistrack/ls1/utility/DataFormatter.java index 1c6d6a06..43bbd2db 100644 --- a/server/src/main/java/thesistrack/ls1/utility/DataFormatter.java +++ b/server/src/main/java/thesistrack/ls1/utility/DataFormatter.java @@ -1,8 +1,14 @@ package thesistrack.ls1.utility; +import thesistrack.ls1.dto.LightUserDto; +import thesistrack.ls1.entity.User; + import java.time.Instant; import java.time.ZoneId; import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; public class DataFormatter { public static String formatDate(Object time) { @@ -34,4 +40,18 @@ public static String formatEnum(Object value) { return ((Enum) value).name(); } + + public static String formatUsers(Object value) { + List users = new ArrayList<>(); + + if (value instanceof List) { + for (Object element : (List) value) { + if (element instanceof LightUserDto) { + users.add((LightUserDto) element); + } + } + } + + return String.join(" and ", users.stream().map(user -> user.firstName() + " " + user.lastName()).toList()); + } } diff --git a/server/src/main/java/thesistrack/ls1/utility/MailBuilder.java b/server/src/main/java/thesistrack/ls1/utility/MailBuilder.java index 786a061c..bbe974f5 100644 --- a/server/src/main/java/thesistrack/ls1/utility/MailBuilder.java +++ b/server/src/main/java/thesistrack/ls1/utility/MailBuilder.java @@ -25,7 +25,9 @@ public class MailBuilder { private final MailConfig config; + private final List primarySenders; private final List primaryRecipients; + private final List secondaryRecipients; private final List bccRecipients; @@ -41,6 +43,7 @@ public class MailBuilder { public MailBuilder(MailConfig config, String subject, String template) { this.config = config; + this.primarySenders = new ArrayList<>(); this.primaryRecipients = new ArrayList<>(); this.secondaryRecipients = new ArrayList<>(); this.bccRecipients = new ArrayList<>(); @@ -60,6 +63,12 @@ public MailBuilder addAttachmentFile(String filename) { return this; } + public MailBuilder addPrimarySender(User user) { + this.primarySenders.add(user); + + return this; + } + public MailBuilder addDefaultBccRecipients() { for (InternetAddress address : config.getDefaultBccRecipients()) { addBccRecipient(address); @@ -164,6 +173,10 @@ public MailBuilder fillThesisPlaceholders(Thesis thesis) { formatters.put("thesis.startDate", DataFormatter::formatDate); formatters.put("thesis.endDate", DataFormatter::formatDate); + formatters.put("thesis.students", DataFormatter::formatUsers); + formatters.put("thesis.advisors", DataFormatter::formatUsers); + formatters.put("thesis.supervisors", DataFormatter::formatUsers); + replaceDtoPlaceholders(ThesisDto.fromThesisEntity(thesis, false), "thesis", formatters); content = content.replace("{{thesisUrl}}", config.getClientHost() + "/theses/" + thesis.getId()); @@ -205,6 +218,10 @@ public void send(JavaMailSender mailSender, UploadService uploadService) throws } for (User recipient : primaryRecipients) { + if (primarySenders.contains(recipient)) { + continue; + } + try { MimeMessage message = mailSender.createMimeMessage(); From 736b359b0be47a93e4221c3816700b9ed9c7e9fa Mon Sep 17 00:00:00 2001 From: Fabian Emilius Date: Mon, 26 Aug 2024 14:15:14 +0200 Subject: [PATCH 07/13] Fix merge conflicts --- client/package-lock.json | 1 + .../main/java/thesistrack/ls1/service/ApplicationService.java | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 0f40915d..60524b70 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -53,6 +53,7 @@ "globals": "15.9.0", "html-webpack-plugin": "5.6.0", "mini-css-extract-plugin": "2.9.0", + "postcss": "8.4.41", "postcss-loader": "8.1.1", "postcss-preset-mantine": "1.17.0", "prettier": "3.3.3", diff --git a/server/src/main/java/thesistrack/ls1/service/ApplicationService.java b/server/src/main/java/thesistrack/ls1/service/ApplicationService.java index 41b98661..6829b7cf 100644 --- a/server/src/main/java/thesistrack/ls1/service/ApplicationService.java +++ b/server/src/main/java/thesistrack/ls1/service/ApplicationService.java @@ -142,8 +142,7 @@ public Application createApplication(User user, UUID topicId, String thesisTitle application.setDesiredStartDate(desiredStartDate); application.setCreatedAt(Instant.now()); - mailingService.sendApplicationCreatedMailToChair(application); - mailingService.sendApplicationCreatedMailToStudent(application); + mailingService.sendApplicationCreatedEmail(application); return applicationRepository.save(application); } From 22656c4b998ecf481ad0c8c938ee3a8c9a6486fd Mon Sep 17 00:00:00 2001 From: Fabian Emilius Date: Mon, 26 Aug 2024 15:56:44 +0200 Subject: [PATCH 08/13] Update server/mail-templates/application-created-student.html Co-authored-by: Stephan Krusche --- server/mail-templates/application-created-student.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/mail-templates/application-created-student.html b/server/mail-templates/application-created-student.html index 0c099b11..b8c8542a 100644 --- a/server/mail-templates/application-created-student.html +++ b/server/mail-templates/application-created-student.html @@ -1,6 +1,6 @@

Dear {{recipientName}},

-

With this email we confirm your successful thesis application submission.

+

With this email, we confirm your thesis application.

We received the following thesis application details:


From 0b7acd2ddb330dcfb2ebaf9e3dcdd554f87b341f Mon Sep 17 00:00:00 2001 From: Fabian Emilius Date: Mon, 26 Aug 2024 15:56:50 +0200 Subject: [PATCH 09/13] Update server/mail-templates/application-created-student.html Co-authored-by: Stephan Krusche --- server/mail-templates/application-created-student.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/mail-templates/application-created-student.html b/server/mail-templates/application-created-student.html index b8c8542a..7bcc5d89 100644 --- a/server/mail-templates/application-created-student.html +++ b/server/mail-templates/application-created-student.html @@ -1,7 +1,7 @@

Dear {{recipientName}},

With this email, we confirm your thesis application.

-

We received the following thesis application details:

+

We received the following details:


From a8733ad873b1f0ed85584708a88e90c58a9baff9 Mon Sep 17 00:00:00 2001 From: Fabian Emilius Date: Mon, 26 Aug 2024 15:57:08 +0200 Subject: [PATCH 10/13] Add documentation about emails --- README.md | 66 ++++--------------------------------------- docs/CONFIGURATION.md | 32 +++++++++++++++++++++ docs/MAILS.md | 24 ++++++++++++++++ docs/SETUP.md | 60 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 122 insertions(+), 60 deletions(-) create mode 100644 docs/CONFIGURATION.md create mode 100644 docs/MAILS.md create mode 100644 docs/SETUP.md diff --git a/README.md b/README.md index ded6661e..d1becc17 100644 --- a/README.md +++ b/README.md @@ -1,63 +1,9 @@ -# thesis-tracker -Web Application for trackage of theses applied for and supervised at the chair +# Thesis Track -## Client - React Web Application - -### Local development - -#### Preconditions -* Server running at http://localhost:8080 -* Keycloak realm `thesis-track` is available under http://localhost:8081 (See [Keycloak Setup](#keycloak-setup)) - -To start the client application for local development, navigate to /client folder and execute the following command from the terminal: -``` -yarn install -yarn run dev -``` - -Client is served at http://localhost:3000.
- -The following **routes** are available:
-`/management/thesis-applications` - Role-protected console for management of thesis applications
-`/applications/thesis` - Form for thesis application submission - -## Server - Java Spring Boot Application - -### Local development - -#### Preconditions -* Database available at `jdbc:postgresql://db:5432/thesis-track` -* Keycloak realm `thesis-track` is available under http://localhost:8081 (See [Keycloak Setup](#keycloak-setup)) - -Server is served at http://localhost:8080. - -## Keycloak Setup - -For local development start a keycloak container by following the steps below: -1. From the project root execute: -``` -docker compose up keycloak -d -``` -2. Open http://localhost:8081 and sign in with admin credentials - * Username: `admin` - * Password: `admin` -3. Import the [keycloak-realm-config-example-json](/keycloak-realm-config-example.json) or create a new real `thesis-track` manually. - -## PostgreSQL Database - -For local development start a database container by executing the following command from the project root: -``` -docker compose up db -d -``` - -### Liquibase - -Project employs liquibase technology for database migrations. Upon a database schema change, follow the steps: -1. Create a new changeset by adding a new script in the [changelog folder](/src/main/resources/db/changelog/changes) -2. Include the new changeset script into the [master changelog file](/src/main/resources/db/changelog/db.changelog-master.xml) - -## Postfix - -Notice: local development currently does not support mailing functionality, i.e. mail send attempts will fail. However, in spite of the errors the initial requests are executed normally and completely, thus, not limiting local development of other features. +Web Application that represents the complete thesis lifecycle of theses applied for and supervised at the chair. +## Documentation +1. [Setup Guide](docs/SETUP.md) +2. [Configuration](docs/CONFIGURATION.md) +3. [Customizing E-Mails](docs/MAILS.md) \ No newline at end of file diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md new file mode 100644 index 00000000..f55ddc92 --- /dev/null +++ b/docs/CONFIGURATION.md @@ -0,0 +1,32 @@ +# Configuration + +## Environment variables + +| Variable Name | Services | Default Value | Description | +|-----------------------------|----------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------| +| SPRING_DATASOURCE_URL | server | jdbc:postgresql://localhost:5432/thesis-track | Postgres connection url | +| SPRING_DATASOURCE_USERNAME | server | thesis-track-postgres | Postgres username | +| SPRING_DATASOURCE_PASSWORD | server | thesis-track-postgres | Postgres password | +| KEYCLOAK_HOST | server, client | http://localhost:8081 | Keycloak hostname | +| KEYCLOAK_REALM_NAME | server, client | thesis-track | Keycloak realm name | +| KEYCLOAK_CLIENT_ID | server, client | thesis-track-app | Keycloak client id | +| UNIVERSITY_ID_JWT_ATTRIBUTE | server, client | preferred_username | Attribute name in keycloak JWT that represents university id. Each user is identified by its university id | +| POSTFIX_HOST | server | localhost | Postfix host to send emails. Only required if emails are enabled. | +| POSTFIX_PORT | server | 25 | Postfix port | +| POSTFIX_USERNAME | server | | Postfix username | +| POSTFIX_PASSWORD | server | | Postfix password | +| CLIENT_HOST | server, client | http://localhost:3000 | Hosting url of client | +| SERVER_HOST | client | http://localhost:8080 | Hosting url of server | +| MAIL_ENABLED | server | false | If set to true, the application will try to send emails via Postfix | +| MAIL_SENDER | server | test@ios.ase.cit.tum.de | Sender email address | +| MAIL_SIGNATURE | server | | Signature of the chair's supervisor / of the chair in general | +| MAIL_WORKSPACE_URL | server | https://slack.com | URL to the workspace where students can connect with advisors and supervisors | +| MAIL_BCC_RECIPIENTS | server | | Default BCC recipients for important emails | +| MAIL_TEMPLATE_FOLDER | server | /default-mail-templates | Folder where mail templates are stored. If not set, it will use the default emails of the repository | +| UPLOAD_FOLDER | server | uploads | Folder where uploaded files will be stored | +| APPLICATION_TITLE | client | Thesis Track | HTML title of the client | +| GENDERS | client | `{"MALE":"Male","FEMALE":"Female","OTHER":"Other","PREFER_NOT_TO_SAY":"Prefer not to say"}` | Available genders that a user can configure | +| STUDY_DEGREES | client | `{"BACHELOR":"Bachelor","MASTER":"Master"}` | Available study degrees | +| STUDY_PROGRAMS | client | `{"COMPUTER_SCIENCE":"Computer Science","INFORMATION_SYSTEMS":"Information Systems","GAMES_ENGINEERING":"Games Engineering","MANAGEMENT_AND_TECHNOLOGY":"Management and Technology","OTHER":"Other"}` | Available study programs | +| THESIS_TYPES | client | `{"BACHELOR":"Bachelor Thesis","MASTER":"Master Thesis","INTERDISCIPLINARY_PROJECT":"Interdisciplinary Project","GUIDED_RESEARCH":"Guided Research"}` | Available thesis types | +| DEFAULT_SUPERVISOR_UUID | client | | The user UUID from the database if a default supervisor should be selected when creating topics or theses | \ No newline at end of file diff --git a/docs/MAILS.md b/docs/MAILS.md new file mode 100644 index 00000000..493a137c --- /dev/null +++ b/docs/MAILS.md @@ -0,0 +1,24 @@ +# Mails + +Mails can be customized if you upload own templates to `MAIL_TEMPLATE_FOLDER`. +By default, mail templates from the repository are used. + +## Templates + +| Template | TO | CC | BCC | Description | +|-------------------------------------------------------------------------------------------------------|---------------------|-----------------------|-----------------------|------------------------------------------------------------------------------| +| [application-accepted.html](../server/mail-templates/application-accepted.html) | Application Student | Supervisor, Advisor | `MAIL_BCC_RECIPIENTS` | Application was accepted with different advisor and supervisor | +| [application-accepted-no-advisor.html](../server/mail-templates/application-accepted-no-advisor.html) | Application Student | Supervisor, Advisor | `MAIL_BCC_RECIPIENTS` | Application was accepted with same advisor and supervisor | +| [application-created-chair.html](../server/mail-templates/application-created-chair.html) | Chair Members | | | All supervisors and advisors get a summary about a new application | +| [application-created-student.html](../server/mail-templates/application-created-student.html) | Application User | | | Confirmation email to the applying student when application was submitted | +| [application-rejected.html](../server/mail-templates/application-rejected.html) | Application User | | `MAIL_BCC_RECIPIENTS` | Application was rejected | +| [thesis-assessment-added.html](../server/mail-templates/thesis-assessment-added.html) | Supervisors | | | Assessment was added to a submitted thesis | +| [thesis-closed.html](../server/mail-templates/thesis-closed.html) | Students | Supervisors, Advisors | `MAIL_BCC_RECIPIENTS` | Thesis was closed before completion | +| [thesis-comment-posted.html](../server/mail-templates/thesis-comment-posted.html) | Students / Advisors | Supervisors, Advisors | | New comment on a thesis. TO depends whether its a student or advisor comment | +| [thesis-created.html](../server/mail-templates/thesis-created.html) | Students | Supervisors, Advisors | `MAIL_BCC_RECIPIENTS` | New thesis was created and assigned to a student | +| [thesis-final-grade.html](../server/mail-templates/thesis-final-grade.html) | Students | Supervisors, Advisors | `MAIL_BCC_RECIPIENTS` | Final grade was added to a thesis | +| [thesis-final-submission.html](../server/mail-templates/thesis-final-submission.html) | Advisors | Supervisors | `MAIL_BCC_RECIPIENTS` | Student submitted final thesis | +| [thesis-presentation-deleted.html](../server/mail-templates/thesis-presentation-deleted.html) | Students | Supervisors, Advisors | `MAIL_BCC_RECIPIENTS` | Scheduled presentation was deleted | +| [thesis-presentation-scheduled.html](../server/mail-templates/thesis-presentation-scheduled.html) | Students | Supervisors, Advisors | `MAIL_BCC_RECIPIENTS` | New presentation was scheduled | +| [thesis-proposal-accepted.html](../server/mail-templates/thesis-proposal-accepted.html) | Students | Supervisors, Advisors | | Proposal was accepted | +| [thesis-proposal-uploaded.html](../server/mail-templates/thesis-proposal-uploaded.html) | Advisors | Supervisors | | Student uploaded new proposal | \ No newline at end of file diff --git a/docs/SETUP.md b/docs/SETUP.md new file mode 100644 index 00000000..69e49c6f --- /dev/null +++ b/docs/SETUP.md @@ -0,0 +1,60 @@ +# Setup + +## Client - React Web Application + +### Local development + +#### Preconditions +* Server running at http://localhost:8080 +* Keycloak realm `thesis-track` is available under http://localhost:8081 (See [Keycloak Setup](#keycloak-setup)) + +To start the client application for local development, navigate to /client folder and execute the following command from the terminal: +``` +yarn install +yarn run dev +``` + +Client is served at http://localhost:3000.
+ +The following **routes** are available:
+`/management/thesis-applications` - Role-protected console for management of thesis applications
+`/applications/thesis` - Form for thesis application submission + +## Server - Java Spring Boot Application + +### Local development + +#### Preconditions +* Database available at `jdbc:postgresql://db:5432/thesis-track` +* Keycloak realm `thesis-track` is available under http://localhost:8081 (See [Keycloak Setup](#keycloak-setup)) + +Server is served at http://localhost:8080. + +## Keycloak Setup + +For local development start a keycloak container by following the steps below: +1. From the project root execute: +``` +docker compose up keycloak -d +``` +2. Open http://localhost:8081 and sign in with admin credentials + * Username: `admin` + * Password: `admin` +3. Import the [keycloak-realm-config-example-json](/keycloak-realm-config-example.json) or create a new real `thesis-track` manually. + +## PostgreSQL Database + +For local development start a database container by executing the following command from the project root: +``` +docker compose up db -d +``` + +### Liquibase + +Project employs liquibase technology for database migrations. Upon a database schema change, follow the steps: +1. Create a new changeset by adding a new script in the [changelog folder](/src/main/resources/db/changelog/changes) +2. Include the new changeset script into the [master changelog file](/src/main/resources/db/changelog/db.changelog-master.xml) + +## Postfix + +Notice: local development currently does not support mailing functionality, i.e. mail send attempts will fail. However, in spite of the errors the initial requests are executed normally and completely, thus, not limiting local development of other features. From 560003a31c78b77bc6d4a5d1c3e5c8521d30d2e3 Mon Sep 17 00:00:00 2001 From: Fabian Emilius Date: Mon, 26 Aug 2024 22:44:47 +0200 Subject: [PATCH 11/13] Improve file size handling --- client/package-lock.json | 212 +++++++----------- client/package.json | 4 +- .../src/components/UploadArea/UploadArea.tsx | 10 +- .../UploadFileModal/UploadFileModal.tsx | 5 +- .../UserInformationForm.tsx | 15 +- .../LegacySubmitApplicationPage.tsx | 15 +- .../ThesisCommentsForm/ThesisCommentsForm.tsx | 1 + .../ThesisProposalSection.tsx | 1 + .../ThesisWritingSection.tsx | 2 + .../ls1/service/ApplicationService.java | 11 +- .../ls1/service/ThesisService.java | 6 +- server/src/main/resources/application.yml | 4 + 12 files changed, 126 insertions(+), 160 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 93ef677f..b8be9fd7 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -38,8 +38,8 @@ "@eslint/js": "9.9.0", "@types/react": "18.3.3", "@types/react-dom": "18.3.0", - "@typescript-eslint/eslint-plugin": "8.2.0", - "@typescript-eslint/parser": "8.2.0", + "@typescript-eslint/eslint-plugin": "8.3.0", + "@typescript-eslint/parser": "8.3.0", "clean-webpack-plugin": "4.0.0", "compression-webpack-plugin": "11.1.0", "copy-webpack-plugin": "12.0.2", @@ -1625,16 +1625,16 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.2.0.tgz", - "integrity": "sha512-02tJIs655em7fvt9gps/+4k4OsKULYGtLBPJfOsmOq1+3cdClYiF0+d6mHu6qDnTcg88wJBkcPLpQhq7FyDz0A==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.3.0.tgz", + "integrity": "sha512-FLAIn63G5KH+adZosDYiutqkOkYEx0nvcwNNfJAf+c7Ae/H35qWwTYvPZUKFj5AS+WfHG/WJJfWnDnyNUlp8UA==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.2.0", - "@typescript-eslint/type-utils": "8.2.0", - "@typescript-eslint/utils": "8.2.0", - "@typescript-eslint/visitor-keys": "8.2.0", + "@typescript-eslint/scope-manager": "8.3.0", + "@typescript-eslint/type-utils": "8.3.0", + "@typescript-eslint/utils": "8.3.0", + "@typescript-eslint/visitor-keys": "8.3.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -1658,15 +1658,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.2.0.tgz", - "integrity": "sha512-j3Di+o0lHgPrb7FxL3fdEy6LJ/j2NE8u+AP/5cQ9SKb+JLH6V6UHDqJ+e0hXBkHP1wn1YDFjYCS9LBQsZDlDEg==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.3.0.tgz", + "integrity": "sha512-h53RhVyLu6AtpUzVCYLPhZGL5jzTD9fZL+SYf/+hYOx2bDkyQXztXSc4tbvKYHzfMXExMLiL9CWqJmVz6+78IQ==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.2.0", - "@typescript-eslint/types": "8.2.0", - "@typescript-eslint/typescript-estree": "8.2.0", - "@typescript-eslint/visitor-keys": "8.2.0", + "@typescript-eslint/scope-manager": "8.3.0", + "@typescript-eslint/types": "8.3.0", + "@typescript-eslint/typescript-estree": "8.3.0", + "@typescript-eslint/visitor-keys": "8.3.0", "debug": "^4.3.4" }, "engines": { @@ -1686,13 +1686,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.2.0.tgz", - "integrity": "sha512-OFn80B38yD6WwpoHU2Tz/fTz7CgFqInllBoC3WP+/jLbTb4gGPTy9HBSTsbDWkMdN55XlVU0mMDYAtgvlUspGw==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.3.0.tgz", + "integrity": "sha512-mz2X8WcN2nVu5Hodku+IR8GgCOl4C0G/Z1ruaWN4dgec64kDBabuXyPAr+/RgJtumv8EEkqIzf3X2U5DUKB2eg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.2.0", - "@typescript-eslint/visitor-keys": "8.2.0" + "@typescript-eslint/types": "8.3.0", + "@typescript-eslint/visitor-keys": "8.3.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1703,13 +1703,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.2.0.tgz", - "integrity": "sha512-g1CfXGFMQdT5S+0PSO0fvGXUaiSkl73U1n9LTK5aRAFnPlJ8dLKkXr4AaLFvPedW8lVDoMgLLE3JN98ZZfsj0w==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.3.0.tgz", + "integrity": "sha512-wrV6qh//nLbfXZQoj32EXKmwHf4b7L+xXLrP3FZ0GOUU72gSvLjeWUl5J5Ue5IwRxIV1TfF73j/eaBapxx99Lg==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "8.2.0", - "@typescript-eslint/utils": "8.2.0", + "@typescript-eslint/typescript-estree": "8.3.0", + "@typescript-eslint/utils": "8.3.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -1727,9 +1727,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.2.0.tgz", - "integrity": "sha512-6a9QSK396YqmiBKPkJtxsgZZZVjYQ6wQ/TlI0C65z7vInaETuC6HAHD98AGLC8DyIPqHytvNuS8bBVvNLKyqvQ==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.3.0.tgz", + "integrity": "sha512-y6sSEeK+facMaAyixM36dQ5NVXTnKWunfD1Ft4xraYqxP0lC0POJmIaL/mw72CUMqjY9qfyVfXafMeaUj0noWw==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1740,15 +1740,15 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.2.0.tgz", - "integrity": "sha512-kiG4EDUT4dImplOsbh47B1QnNmXSoUqOjWDvCJw/o8LgfD0yr7k2uy54D5Wm0j4t71Ge1NkynGhpWdS0dEIAUA==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.3.0.tgz", + "integrity": "sha512-Mq7FTHl0R36EmWlCJWojIC1qn/ZWo2YiWYc1XVtasJ7FIgjo0MVv9rZWXEE7IK2CGrtwe1dVOxWwqXUdNgfRCA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.2.0", - "@typescript-eslint/visitor-keys": "8.2.0", + "@typescript-eslint/types": "8.3.0", + "@typescript-eslint/visitor-keys": "8.3.0", "debug": "^4.3.4", - "globby": "^11.1.0", + "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", @@ -1792,15 +1792,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.2.0.tgz", - "integrity": "sha512-O46eaYKDlV3TvAVDNcoDzd5N550ckSe8G4phko++OCSC1dYIb9LTc3HDGYdWqWIAT5qDUKphO6sd9RrpIJJPfg==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.3.0.tgz", + "integrity": "sha512-F77WwqxIi/qGkIGOGXNBLV7nykwfjLsdauRB/DOFPdv6LTF3BHHkBpq81/b5iMPSF055oO2BiivDJV4ChvNtXA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.2.0", - "@typescript-eslint/types": "8.2.0", - "@typescript-eslint/typescript-estree": "8.2.0" + "@typescript-eslint/scope-manager": "8.3.0", + "@typescript-eslint/types": "8.3.0", + "@typescript-eslint/typescript-estree": "8.3.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1814,12 +1814,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.2.0.tgz", - "integrity": "sha512-sbgsPMW9yLvS7IhCi8IpuK1oBmtbWUNP+hBdwl/I9nzqVsszGnNGti5r9dUtF5RLivHUFFIdRvLiTsPhzSyJ3Q==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.3.0.tgz", + "integrity": "sha512-RmZwrTbQ9QveF15m/Cl28n0LXD6ea2CjkhH5rQ55ewz3H24w+AMCJHPVYaZ8/0HoG8Z3cLLFFycRXxeO2tz9FA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.2.0", + "@typescript-eslint/types": "8.3.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -2281,12 +2281,15 @@ } }, "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==", "dev": true, + "dependencies": { + "array-uniq": "^1.0.1" + }, "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, "node_modules/array-uniq": { @@ -3001,50 +3004,6 @@ "webpack": "^5.1.0" } }, - "node_modules/copy-webpack-plugin/node_modules/globby": { - "version": "14.0.2", - "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.2.tgz", - "integrity": "sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==", - "dev": true, - "dependencies": { - "@sindresorhus/merge-streams": "^2.1.0", - "fast-glob": "^3.3.2", - "ignore": "^5.2.4", - "path-type": "^5.0.0", - "slash": "^5.1.0", - "unicorn-magic": "^0.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/copy-webpack-plugin/node_modules/path-type": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz", - "integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/copy-webpack-plugin/node_modules/slash": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", - "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", - "dev": true, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -3559,18 +3518,6 @@ "node": ">=6" } }, - "node_modules/del/node_modules/array-union": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==", - "dev": true, - "dependencies": { - "array-uniq": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/del/node_modules/globby": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", @@ -3640,18 +3587,6 @@ "node": ">=0.3.1" } }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/dns-packet": { "version": "5.6.1", "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", @@ -4966,20 +4901,32 @@ } }, "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.2.tgz", + "integrity": "sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==", "dev": true, "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.2", + "ignore": "^5.2.4", + "path-type": "^5.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.1.0" }, "engines": { - "node": ">=10" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby/node_modules/path-type": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz", + "integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==", + "dev": true, + "engines": { + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -9338,12 +9285,15 @@ } }, "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", "dev": true, "engines": { - "node": ">=8" + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/sockjs": { diff --git a/client/package.json b/client/package.json index e32701db..2ba95932 100644 --- a/client/package.json +++ b/client/package.json @@ -43,8 +43,8 @@ "@eslint/js": "9.9.0", "@types/react": "18.3.3", "@types/react-dom": "18.3.0", - "@typescript-eslint/eslint-plugin": "8.2.0", - "@typescript-eslint/parser": "8.2.0", + "@typescript-eslint/eslint-plugin": "8.3.0", + "@typescript-eslint/parser": "8.3.0", "clean-webpack-plugin": "4.0.0", "compression-webpack-plugin": "11.1.0", "copy-webpack-plugin": "12.0.2", diff --git a/client/src/components/UploadArea/UploadArea.tsx b/client/src/components/UploadArea/UploadArea.tsx index f7698a1a..da486d75 100644 --- a/client/src/components/UploadArea/UploadArea.tsx +++ b/client/src/components/UploadArea/UploadArea.tsx @@ -17,13 +17,13 @@ import { useMemo } from 'react' interface IUploadAreaProps { value: File | undefined onChange: (file: File | undefined) => unknown + maxSize: number label?: string required?: boolean - maxSize?: number } const UploadArea = (props: IUploadAreaProps) => { - const { label, required, value, onChange, maxSize = 1024 } = props + const { label, required, value, onChange, maxSize } = props const theme = useMantineTheme() @@ -56,9 +56,9 @@ const UploadArea = (props: IUploadAreaProps) => { onChange(files[0]) }} onReject={() => { - showSimpleError(`Failed upload file. Max file size is ${Math.floor(maxSize / 1024)}MB`) + showSimpleError(`Failed upload file. Max file size is ${Math.floor(maxSize / 1024 / 1024)}MB`) }} - maxSize={maxSize * 1024} + maxSize={maxSize} accept={PDF_MIME_TYPE} > @@ -76,7 +76,7 @@ const UploadArea = (props: IUploadAreaProps) => { Drag the file here or click to select file - The file should not exceed {Math.floor(maxSize / 1024)}MB + The file should not exceed {Math.floor(maxSize / 1024 / 1024)}MB diff --git a/client/src/components/UploadFileModal/UploadFileModal.tsx b/client/src/components/UploadFileModal/UploadFileModal.tsx index e0a77d37..afe8f215 100644 --- a/client/src/components/UploadFileModal/UploadFileModal.tsx +++ b/client/src/components/UploadFileModal/UploadFileModal.tsx @@ -7,10 +7,11 @@ interface IUploadFileModalProps { opened: boolean onClose: () => unknown onUpload: (file: File) => unknown + maxSize: number } const UploadFileModal = (props: IUploadFileModalProps) => { - const { title, opened, onClose, onUpload } = props + const { title, opened, onClose, onUpload, maxSize } = props const [submitting, setSubmitting] = useState(false) @@ -23,7 +24,7 @@ const UploadFileModal = (props: IUploadFileModalProps) => { return ( - +