diff --git a/client/e2e/application-review-workflow.spec.ts b/client/e2e/application-review-workflow.spec.ts index 26d5b2c98..40fa76757 100644 --- a/client/e2e/application-review-workflow.spec.ts +++ b/client/e2e/application-review-workflow.spec.ts @@ -6,6 +6,7 @@ import { getBody, getToAddresses, assertSentFromApp, + assertEmailFooter, findBySubject, } from './mailpit' @@ -62,6 +63,7 @@ test.describe('Application Review Workflow', () => { const rejectionEmail = findBySubject(newEmails, 'Thesis Application Rejection') expect(rejectionEmail, 'Rejection email with correct subject should be sent').toBeDefined() assertSentFromApp(rejectionEmail!) + assertEmailFooter(rejectionEmail!) expect(getToAddresses(rejectionEmail!)).toContain('student4@test.local') // Body should greet the student by first name and reference the topic title @@ -131,6 +133,7 @@ test.describe('Application Review Workflow', () => { const acceptanceEmail = findBySubject(newEmails, 'Thesis Application Acceptance') expect(acceptanceEmail, 'Acceptance email should be sent').toBeDefined() assertSentFromApp(acceptanceEmail!) + assertEmailFooter(acceptanceEmail!) expect(getToAddresses(acceptanceEmail!)).toContain('student5@test.local') // Body should greet student, mention the supervisor, and include a thesis link @@ -143,6 +146,7 @@ test.describe('Application Review Workflow', () => { const thesisEmail = findBySubject(newEmails, 'Thesis Created') expect(thesisEmail, 'Thesis creation email should be sent').toBeDefined() assertSentFromApp(thesisEmail!) + assertEmailFooter(thesisEmail!) expect(getToAddresses(thesisEmail!)).toContain('student5@test.local') // Body should contain the thesis title and a link to the thesis diff --git a/client/e2e/application-workflow.spec.ts b/client/e2e/application-workflow.spec.ts index 53f115177..9ee423c6e 100644 --- a/client/e2e/application-workflow.spec.ts +++ b/client/e2e/application-workflow.spec.ts @@ -12,6 +12,7 @@ import { getBody, getToAddresses, assertSentFromApp, + assertEmailFooter, findBySubject, } from './mailpit' @@ -131,6 +132,7 @@ test.describe('Application Workflow - Student submits application', () => { const studentEmail = findBySubject(newStudentEmails, 'Thesis Application Confirmation') expect(studentEmail, 'Student confirmation email should be sent').toBeDefined() assertSentFromApp(studentEmail!) + assertEmailFooter(studentEmail!) expect(getToAddresses(studentEmail!)).toContain('student@test.local') const studentBody = getBody(studentEmail!) @@ -151,6 +153,7 @@ test.describe('Application Workflow - Student submits application', () => { 'Examiner (examiner@test.local) should receive "New Thesis Application" email', ).toBeDefined() assertSentFromApp(examinerChairEmail!) + assertEmailFooter(examinerChairEmail!) const examinerBody = getBody(examinerChairEmail!) expect(examinerBody, 'Examiner email should mention the applicant').toContain('Student') diff --git a/client/e2e/mailpit.ts b/client/e2e/mailpit.ts index 4a77abd22..050d45369 100644 --- a/client/e2e/mailpit.ts +++ b/client/e2e/mailpit.ts @@ -137,6 +137,19 @@ export function assertBodyContains(message: MailpitMessage, expectedText: string expect(body, `Expected email body to contain "${expectedText}"`).toContain(expectedText) } +/** + * Assert that the email contains the standard footer with notification settings link. + */ +export function assertEmailFooter(message: MailpitMessage): void { + const body = getBody(message) + expect(body, 'Expected email to contain notification settings link').toContain( + 'notification settings in Thesis Management', + ) + expect(body, 'Expected email to contain /settings/notifications link').toContain( + '/settings/notifications', + ) +} + /** * Assert that the email was sent from the application sender. * Verifies the From header matches the expected format: "ThesisManagement <...@...>" diff --git a/client/e2e/presentation-workflow.spec.ts b/client/e2e/presentation-workflow.spec.ts index 154e731f6..43d48d248 100644 --- a/client/e2e/presentation-workflow.spec.ts +++ b/client/e2e/presentation-workflow.spec.ts @@ -6,6 +6,7 @@ import { getBody, getToAddresses, assertSentFromApp, + assertEmailFooter, } from './mailpit' // Thesis d000-0003 is in ASSESSED state, assigned to student3, has abstract text set @@ -170,6 +171,7 @@ test.describe.serial('Presentation Workflow', () => { 'New Presentation scheduled', ) assertSentFromApp(privateEmail) + assertEmailFooter(privateEmail) expect(getToAddresses(privateEmail)).toContain('student3@test.local') const body = getBody(privateEmail) diff --git a/client/e2e/proposal-feedback-workflow.spec.ts b/client/e2e/proposal-feedback-workflow.spec.ts index e8841471c..a4227526b 100644 --- a/client/e2e/proposal-feedback-workflow.spec.ts +++ b/client/e2e/proposal-feedback-workflow.spec.ts @@ -7,6 +7,7 @@ import { getBody, getToAddresses, assertSentFromApp, + assertEmailFooter, hasAttachment, } from './mailpit' @@ -58,6 +59,7 @@ test.describe('Proposal Upload - Student uploads proposal', () => { const proposalEmail = newEmails.find((e) => getSubject(e) === 'Thesis Proposal Added') expect(proposalEmail, 'Proposal upload email with correct subject should be sent').toBeDefined() assertSentFromApp(proposalEmail!) + assertEmailFooter(proposalEmail!) expect(getToAddresses(proposalEmail!)).toContain('supervisor@test.local') // Body should reference the thesis title, the uploader name, and include a link @@ -126,6 +128,7 @@ test.describe('Proposal Feedback - Supervisor requests changes', () => { 'Change request email with correct subject should be sent', ).toBeDefined() assertSentFromApp(changeRequestEmail!) + assertEmailFooter(changeRequestEmail!) expect(getToAddresses(changeRequestEmail!)).toContain('student2@test.local') // Body should greet the student, reference the thesis title, mention the reviewer, diff --git a/client/e2e/thesis-grading-workflow.spec.ts b/client/e2e/thesis-grading-workflow.spec.ts index 3928d4115..10db435ca 100644 --- a/client/e2e/thesis-grading-workflow.spec.ts +++ b/client/e2e/thesis-grading-workflow.spec.ts @@ -7,6 +7,7 @@ import { getBody, getToAddresses, assertSentFromApp, + assertEmailFooter, } from './mailpit' // Thesis d000-0003: ASSESSED state, student3, supervisor2, examiner2 (DSA group) @@ -181,6 +182,7 @@ test.describe.serial('Thesis Grading Workflow', () => { const gradeEmail = newEmails.find((e) => getSubject(e) === 'Final Grade available for Thesis') expect(gradeEmail, 'Final grade email should be sent').toBeDefined() assertSentFromApp(gradeEmail!) + assertEmailFooter(gradeEmail!) expect(getToAddresses(gradeEmail!)).toContain('student3@test.local') // Body should contain: greeting, thesis title, examiner name, final grade, diff --git a/client/e2e/thesis-workflow.spec.ts b/client/e2e/thesis-workflow.spec.ts index 2d378db67..6241ff5e8 100644 --- a/client/e2e/thesis-workflow.spec.ts +++ b/client/e2e/thesis-workflow.spec.ts @@ -7,6 +7,7 @@ import { getBody, getToAddresses, assertSentFromApp, + assertEmailFooter, } from './mailpit' test.describe('Thesis Workflow - Examiner creates a thesis', () => { @@ -66,6 +67,7 @@ test.describe('Thesis Workflow - Examiner creates a thesis', () => { const creationEmail = newEmails.find((e) => getSubject(e) === 'Thesis Created') expect(creationEmail, 'Thesis creation email should be sent').toBeDefined() assertSentFromApp(creationEmail!) + assertEmailFooter(creationEmail!) expect(getToAddresses(creationEmail!)).toContain('student4@test.local') // Body should contain: greeting, thesis title, examiner/supervisor/student names, and link diff --git a/client/src/requests/responses/researchGroupSettings.ts b/client/src/requests/responses/researchGroupSettings.ts index 15333b28b..07d9412f9 100644 --- a/client/src/requests/responses/researchGroupSettings.ts +++ b/client/src/requests/responses/researchGroupSettings.ts @@ -23,6 +23,7 @@ export interface IResearchGroupSettingsPhase { export interface IResearchGroupSettingsEmail { applicationNotificationEmail?: string | null + emailSignature?: string | null } export interface IResearchGroupSettingsWritingGuide { diff --git a/server/src/main/java/de/tum/cit/aet/thesis/controller/ResearchGroupSettingsController.java b/server/src/main/java/de/tum/cit/aet/thesis/controller/ResearchGroupSettingsController.java index e8100bb11..8e678648a 100644 --- a/server/src/main/java/de/tum/cit/aet/thesis/controller/ResearchGroupSettingsController.java +++ b/server/src/main/java/de/tum/cit/aet/thesis/controller/ResearchGroupSettingsController.java @@ -82,6 +82,8 @@ public ResponseEntity createOrUpdateRejectSettings(@Pa String validatedEmail = RequestValidator.validateEmailAllowNull( newSettings.emailSettings().applicationNotificationEmail() == null ? null : newSettings.emailSettings().applicationNotificationEmail().trim()); toSave.setApplicationNotificationEmail(validatedEmail); + String signature = newSettings.emailSettings().emailSignature(); + toSave.setEmailSignature(signature != null && !signature.trim().isEmpty() ? signature.trim() : null); } if (newSettings.writingGuideSettings() != null) { String link = newSettings.writingGuideSettings().scientificWritingGuideLink(); diff --git a/server/src/main/java/de/tum/cit/aet/thesis/dto/ResearchGroupSettingsEmailDTO.java b/server/src/main/java/de/tum/cit/aet/thesis/dto/ResearchGroupSettingsEmailDTO.java index 525b99f5f..a20147ab3 100644 --- a/server/src/main/java/de/tum/cit/aet/thesis/dto/ResearchGroupSettingsEmailDTO.java +++ b/server/src/main/java/de/tum/cit/aet/thesis/dto/ResearchGroupSettingsEmailDTO.java @@ -1,11 +1,14 @@ package de.tum.cit.aet.thesis.dto; +import com.fasterxml.jackson.annotation.JsonInclude; import de.tum.cit.aet.thesis.entity.ResearchGroupSettings; +@JsonInclude(JsonInclude.Include.NON_EMPTY) public record ResearchGroupSettingsEmailDTO( - String applicationNotificationEmail + String applicationNotificationEmail, + String emailSignature ) { public static ResearchGroupSettingsEmailDTO fromEntity(ResearchGroupSettings settings) { - return new ResearchGroupSettingsEmailDTO(settings.getApplicationNotificationEmail()); + return new ResearchGroupSettingsEmailDTO(settings.getApplicationNotificationEmail(), settings.getEmailSignature()); } } diff --git a/server/src/main/java/de/tum/cit/aet/thesis/entity/ResearchGroupSettings.java b/server/src/main/java/de/tum/cit/aet/thesis/entity/ResearchGroupSettings.java index 5d4f3074f..496fa9749 100644 --- a/server/src/main/java/de/tum/cit/aet/thesis/entity/ResearchGroupSettings.java +++ b/server/src/main/java/de/tum/cit/aet/thesis/entity/ResearchGroupSettings.java @@ -39,4 +39,7 @@ public class ResearchGroupSettings { @Column(name = "include_application_data_in_email", nullable = false) private boolean includeApplicationDataInEmail = false; + + @Column(name = "email_signature") + private String emailSignature; } diff --git a/server/src/main/java/de/tum/cit/aet/thesis/service/MailingService.java b/server/src/main/java/de/tum/cit/aet/thesis/service/MailingService.java index 56edae860..b8eb89b0c 100644 --- a/server/src/main/java/de/tum/cit/aet/thesis/service/MailingService.java +++ b/server/src/main/java/de/tum/cit/aet/thesis/service/MailingService.java @@ -107,6 +107,7 @@ public void sendApplicationCreatedEmail(Application application) { "en"); MailBuilder studentMailBuilder = new MailBuilder(config, studentEmailTemplate.getSubject(), studentEmailTemplate.getBodyHtml()); + applyGroupSignature(studentMailBuilder, application.getResearchGroup()); studentMailBuilder .addPrimaryRecipient(application.getUser()) .addStoredAttachment(application.getUser().getCvFilename(), @@ -123,7 +124,7 @@ public void sendApplicationCreatedEmail(Application application) { } private MailBuilder prepareApplicationCreatedMailBuilder(Application application, EmailTemplate template) { - return new MailBuilder(config, template.getSubject(), template.getBodyHtml()) + MailBuilder builder = new MailBuilder(config, template.getSubject(), template.getBodyHtml()) .addStoredAttachment(application.getUser().getCvFilename(), getUserFilename(application.getUser(), "CV", application.getUser().getCvFilename())) @@ -134,6 +135,8 @@ private MailBuilder prepareApplicationCreatedMailBuilder(Application application getUserFilename(application.getUser(), "Degree Report", application.getUser().getDegreeFilename())) .fillApplicationPlaceholders(application); + applyGroupSignature(builder, application.getResearchGroup()); + return builder; } private MailBuilder prepareMinimalApplicationMailBuilder(Application application) { @@ -165,9 +168,15 @@ private MailBuilder prepareMinimalApplicationMailBuilder(Application application + "

" + "

You can view the full application details here: " + "" - + applicationUrl + "

"; - - return new MailBuilder(config, subject, body); + + applicationUrl + "

" + // Match the rest of the templates (which all end with the + // emailSignature placeholder so the per-research-group + // signature gets injected by Thymeleaf). + + "
"; + + MailBuilder builder = new MailBuilder(config, subject, body); + applyGroupSignature(builder, application.getResearchGroup()); + return builder; } /** @@ -228,6 +237,7 @@ public void sendApplicationAcceptanceEmail(Application application, Thesis thesi templateCase, "en"); MailBuilder mailBuilder = new MailBuilder(config, emailTemplate.getSubject(), emailTemplate.getBodyHtml()); + applyGroupSignature(mailBuilder, application.getResearchGroup()); mailBuilder .addPrimaryRecipient(application.getUser()) .addSecondaryRecipient(advisor) @@ -250,6 +260,7 @@ public void sendApplicationRejectionEmail(Application application, ApplicationRe reason.getTemplateCase(), "en"); MailBuilder mailBuilder = new MailBuilder(config, emailTemplate.getSubject(), emailTemplate.getBodyHtml()); + applyGroupSignature(mailBuilder, application.getResearchGroup()); mailBuilder .addPrimaryRecipient(application.getUser()) .addDefaultBccRecipients(application.getResearchGroup().getHead().getEmail()) @@ -269,6 +280,7 @@ public void sendApplicationReminderEmail(User user, long unreviewedApplications) "APPLICATION_REMINDER", "en"); MailBuilder mailBuilder = new MailBuilder(config, emailTemplate.getSubject(), emailTemplate.getBodyHtml()); + applyGroupSignature(mailBuilder, user.getResearchGroup()); mailBuilder .addPrimaryRecipient(user) .addNotificationName("unreviewed-application-reminder") @@ -294,6 +306,7 @@ public void sendApplicationAutomaticRejectReminderEmail(User user, List(); this.variables.put("config", config.getConfigDto()); + this.variables.put("emailSignature", "

Regards,
The Thesis Management Team.

"); this.fileAttachments = new ArrayList<>(); this.rawAttachments = new ArrayList<>(); @@ -369,6 +371,19 @@ public MailBuilder fillPlaceholder(String placeholder, Object value) { return this; } + /** + * Overrides the default email signature with a custom one. + * + * @param signature the custom HTML signature + * @return this builder + */ + public MailBuilder withSignature(String signature) { + if (signature != null && !signature.isBlank()) { + this.variables.put("emailSignature", signature); + } + return this; + } + /** * Fills user placeholders under the given placeholder key. * @@ -573,7 +588,16 @@ public void send(JavaMailSender mailSender, UploadService uploadService) { Multipart messageContent = new MimeMultipart(); BodyPart messageBody = new MimeBodyPart(); - messageBody.setContent(config.getTemplateEngine().process(templateHtml, templateContext), "text/html; charset=utf-8"); + String renderedHtml = config.getTemplateEngine().process(templateHtml, templateContext); + // HTML-escape the client host before interpolating it into an + // href so a misconfigured CHAIR_URL can't break the attribute + // or open an HTML-injection vector in outgoing mail. + String settingsUrl = HtmlUtils.htmlEscape(config.getClientHost() + "/settings/notifications"); + renderedHtml += "
" + + "You can (un)subscribe to similar emails in your " + + "" + + "notification settings in Thesis Management.
"; + messageBody.setContent(renderedHtml, "text/html; charset=utf-8"); messageContent.addBodyPart(messageBody); for (StoredAttachment data : fileAttachments) { diff --git a/server/src/main/resources/db/changelog/changes/40_consistent_email_footer.sql b/server/src/main/resources/db/changelog/changes/40_consistent_email_footer.sql new file mode 100644 index 000000000..8c200c1f9 --- /dev/null +++ b/server/src/main/resources/db/changelog/changes/40_consistent_email_footer.sql @@ -0,0 +1,44 @@ +--liquibase formatted sql +--changeset consistent-email-footer:40-add-email-signature-column +ALTER TABLE research_group_settings ADD COLUMN email_signature TEXT; + +--changeset consistent-email-footer:40-replace-best-regards-with-signature-variable +-- Replace hardcoded "Best regards" sign-offs with the emailSignature template variable +UPDATE email_templates SET body_html = REPLACE(body_html, + '

Best regards,
' || chr(10) || 'The Thesis Coordination Team

', + '
') +WHERE body_html LIKE '%Best regards%Thesis Coordination Team%'; + +--changeset consistent-email-footer:40-replace-config-signature-with-email-signature +-- Replace AET ${config.signature} with ${emailSignature} +UPDATE email_templates SET body_html = REPLACE(body_html, + '
', + '
') +WHERE body_html LIKE '%config.signature%'; + +--changeset consistent-email-footer:40-remove-notification-links-multiline +-- Remove multiline notification settings link blocks +UPDATE email_templates SET body_html = REPLACE(body_html, + '
' || chr(10) || '
' || chr(10) || ' Manage your notification settings here' || chr(10) || '
' || chr(10) || '

', + '') +WHERE body_html LIKE '%Manage your notification settings%'; + +--changeset consistent-email-footer:40-remove-notification-links-inline +-- Remove inline notification settings link blocks (presentation invitation templates) +UPDATE email_templates SET body_html = REPLACE(body_html, + '


' || chr(10) || '
Manage your notification settings here
', + '') +WHERE body_html LIKE '%Manage your notification settings%'; + +--changeset consistent-email-footer:40-remove-trailing-br +-- Remove trailing

from DATA_EXPORT_READY template +UPDATE email_templates SET body_html = REPLACE(body_html, + '

', + '') +WHERE template_case = 'DATA_EXPORT_READY' AND body_html LIKE '%

%'; + +--changeset consistent-email-footer:40-add-signature-to-templates-without +-- Add emailSignature variable to templates that don't have any sign-off +UPDATE email_templates SET body_html = body_html || chr(10) || '
' +WHERE body_html NOT LIKE '%emailSignature%' + AND body_html NOT LIKE '%Best regards%'; diff --git a/server/src/main/resources/db/changelog/db.changelog-master.xml b/server/src/main/resources/db/changelog/db.changelog-master.xml index a8cfa9ee2..ecd79dcb6 100644 --- a/server/src/main/resources/db/changelog/db.changelog-master.xml +++ b/server/src/main/resources/db/changelog/db.changelog-master.xml @@ -43,6 +43,7 @@ + diff --git a/server/src/main/resources/db/changelog/manual/insert_tum_aet_email_templates.sql b/server/src/main/resources/db/changelog/manual/insert_tum_aet_email_templates.sql index 580ff3279..42bbde16c 100644 --- a/server/src/main/resources/db/changelog/manual/insert_tum_aet_email_templates.sql +++ b/server/src/main/resources/db/changelog/manual/insert_tum_aet_email_templates.sql @@ -64,7 +64,7 @@ WITH defaults You can view your thesis details and tasks on: [[${thesisUrl}]]

-
+
', 'Application was accepted with different supervisor and examiner'), ('APPLICATION_ACCEPTED_NO_SUPERVISOR', 'Thesis Application Acceptance', '

Dear [[${recipient.firstName}]],

@@ -93,7 +93,7 @@ project. You can view your thesis details and tasks on: [[${thesisUrl}]]

-
', 'Application was accepted with same supervisor and examiner'), +
', 'Application was accepted with same supervisor and examiner'), ('APPLICATION_CREATED_CHAIR', 'New Thesis Application', '

Dear [[${recipient.firstName}]],

there is a new thesis application by [[${application.applicantFirstName}]] [[${application.applicantLastName}]].

@@ -158,11 +158,7 @@ Full Details: You can find the submitted files in the attachment part of this email.

-
-
-

', +
', 'All examiners and supervisors get a summary about a new application'), ('APPLICATION_CREATED_STUDENT', 'Thesis Application Confirmation', '

Dear [[${recipient.firstName}]],

@@ -229,7 +225,9 @@ and research commitments of our group may result in a response time of up to fou We appreciate your patience and understanding during this period.

-

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

', +

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

+ +
', 'Confirmation email to the applying student when application was submitted'), ('APPLICATION_REJECTED', 'Thesis Application Rejection', '

Dear [[${recipient.firstName}]],

@@ -244,7 +242,7 @@ The volume of applications received this year was exceptionally high, and I have I recommend reaching out to other faculty members who may align more closely with your qualifications and area of interest.

-
', 'Application was rejected'), +
', 'Application was rejected'), ('APPLICATION_REJECTED_TOPIC_REQUIREMENTS', 'Thesis Application Rejection', '

Dear [[${recipient.firstName}]],

@@ -256,7 +254,7 @@ After reviewing your application and supporting documents, I regret to inform yo I recommend considering other thesis topics or reaching out to faculty members whose research focus may better align with your qualifications.

-
', 'Application was rejected because topic requirements were not met'), +
', 'Application was rejected because topic requirements were not met'), ('APPLICATION_REJECTED_STUDENT_REQUIREMENTS', 'Thesis Application Rejection', '

Dear [[${recipient.firstName}]],

@@ -269,7 +267,7 @@ Unfortunately, I must inform you that you do not currently meet the necessary re I recommend reaching out to other faculty members who may align more closely with your qualifications and area of interest.

-
', +
', 'Application was rejected because student does not fulfil chair''s requirements'), ('APPLICATION_REJECTED_TITLE_NOT_INTERESTING', 'Thesis Application Rejection', '

Dear [[${recipient.firstName}]],

@@ -283,7 +281,7 @@ However, the suggested topic does not align with the current research interests I encourage you to explore other faculty members whose research is more closely related to your proposed area of study.

-
', +
', 'Application was rejected because the suggested thesis title is not interesting'), ('APPLICATION_REJECTED_TOPIC_FILLED', 'Thesis Application Rejection', '

Dear [[${recipient.firstName}]],

@@ -300,7 +298,7 @@ We found a student for the specific topic you applied for. You can explore other topics or suggest a topic yourself in your area of interest.

-
', 'Application was rejected because topic was closed'), +
', 'Application was rejected because topic was closed'), ('APPLICATION_REJECTED_TOPIC_OUTDATED', 'Thesis Application Rejection', '

Dear [[${recipient.firstName}]],

@@ -316,7 +314,7 @@ I wanted to inform you that the topic you applied for is no longer available. You can explore other topics or suggest a topic yourself in your area of interest.

-
', 'Application was rejected because topic is outdated'), +
', 'Application was rejected because topic is outdated'), ('APPLICATION_REMINDER', 'Unreviewed Thesis Applications', '

Dear [[${recipient.firstName}]],

@@ -327,11 +325,7 @@ There are currently [[${unreviewedApplications}]] unreviewed th Review Applications: [[${reviewApplicationsLink}]]

-
-
- Manage your notification settings here -
-

', +
', 'Weekly email if there are more than 10 unreviewed applications'), ('THESIS_ASSESSMENT_ADDED', 'Assessment added', '

Dear [[${recipient.firstName}]],

@@ -362,11 +356,7 @@ Review Applications:

-
-
- Manage your notification settings here -
-

', 'Assessment was added to a submitted thesis'), +
', 'Assessment was added to a submitted thesis'), ('THESIS_CLOSED', 'Thesis Closed', '

Dear [[${recipient.firstName}]],

@@ -378,11 +368,7 @@ Please contact your supervisor or examiner if you think that this was a mistake. Full Details:

-
-
- Manage your notification settings here -
-

', 'Thesis was closed before completion'), +
', 'Thesis was closed before completion'), ('THESIS_COMMENT_POSTED', 'A Comment was posted', '

Dear [[${recipient.firstName}]],

@@ -398,11 +384,7 @@ Please contact your supervisor or examiner if you think that this was a mistake. Full Details:

-
-
- Manage your notification settings here -
-

', +
', 'New comment on a thesis. TO depends whether it''s a student or supervisor comment'), ('THESIS_CREATED', 'Thesis Created', '

Dear [[${recipient.firstName}]],

@@ -419,7 +401,9 @@ Please contact your supervisor or examiner if you think that this was a mistake.

The next step is that you write a proposal and submit it on [[${thesisUrl}]] -

', 'New thesis was created and assigned to a student'), +

+ +
', 'New thesis was created and assigned to a student'), ('THESIS_FINAL_GRADE', 'Final Grade available for Thesis', '

Dear [[${recipient.firstName}]],

@@ -439,11 +423,7 @@ The next step is that you write a proposal and submit it on

-
-
- Manage your notification settings here -
-

', 'Final grade was added to a thesis'), +
', 'Final grade was added to a thesis'), ('THESIS_FINAL_SUBMISSION', 'Thesis Submitted', '

Dear [[${recipient.firstName}]],

@@ -458,11 +438,7 @@ The next step is to write an assessment about the thesis. Full Details:

-
-
- Manage your notification settings here -
-

', 'Student submitted final thesis'), +
', 'Student submitted final thesis'), ('THESIS_PRESENTATION_DELETED', 'Presentation deleted', '

Dear [[${recipient.firstName}]],

@@ -473,11 +449,7 @@ The next step is to write an assessment about the thesis. Full Details:

-
-
- Manage your notification settings here -
-

', 'Scheduled presentation was deleted'), +
', 'Scheduled presentation was deleted'), ('THESIS_PRESENTATION_SCHEDULED', 'New Presentation scheduled', '

Dear [[${recipient.firstName}]],

@@ -513,11 +485,7 @@ The next step is to write an assessment about the thesis. Full Details:

-
-
- Manage your notification settings here -
-

', 'New presentation was scheduled'), +
', 'New presentation was scheduled'), ('THESIS_PRESENTATION_UPDATED', 'Presentation updated', '

Dear [[${recipient.firstName}]],

@@ -553,11 +521,7 @@ The next step is to write an assessment about the thesis. Full Details:

-
-
- Manage your notification settings here -
-

', 'Presentation was updated'), +
', 'Presentation was updated'), ('THESIS_PRESENTATION_INVITATION', 'Thesis Presentation Invitation', '

INVITATION

As part of their [[${thesis.type}]]''s thesis
@@ -591,11 +555,7 @@ The presentation will be in [[${presentation.language}]]. Everybody is cordially Full Details: [[${presentationUrl}]]

-
-
- Manage your notification settings here -
-

', 'Public Presentation Invitation'), +
', 'Public Presentation Invitation'), ('THESIS_PRESENTATION_INVITATION_CANCELLED', 'Thesis Presentation Cancelled', '

Dear [[${recipient.firstName}]],

@@ -604,11 +564,7 @@ The [[${thesis.type}]] thesis presentation of [[${presentation.scheduledAt}]] was cancelled.

-
-
- Manage your notification settings here -
-

', 'Public Presentation was deleted'), +
', 'Public Presentation was deleted'), ('THESIS_PRESENTATION_INVITATION_UPDATED', 'Thesis Presentation Updated', '

INVITATION

As part of their [[${thesis.type}]]''s thesis
@@ -642,11 +598,7 @@ The presentation will be in [[${presentation.language}]]. Everybody is cordially Full Details: [[${presentationUrl}]]

-
-
- Manage your notification settings here -
-

', 'Public Presentation was updated'), +
', 'Public Presentation was updated'), ('THESIS_PROPOSAL_ACCEPTED', 'Thesis Proposal Accepted', '

Dear [[${recipient.firstName}]],

@@ -655,11 +607,7 @@ The next step is to start with the project work and with writing the thesis. You can see your submission deadline on [[${thesisUrl}]].

-
-
- Manage your notification settings here -
-

', 'Proposal was accepted'), +
', 'Proposal was accepted'), ('THESIS_PROPOSAL_REJECTED', 'Changes were requested for Proposal', '

Dear [[${recipient.firstName}]],

@@ -677,11 +625,7 @@ The following changes were requested:
Full Details:

-
-
- Manage your notification settings here -
-

', 'Changes were requested for proposal'), +
', 'Changes were requested for proposal'), ('THESIS_PROPOSAL_UPLOADED', 'Thesis Proposal Added', '

Dear [[${recipient.firstName}]],

@@ -693,11 +637,7 @@ You can find the submitted file in the attachment part of this email. Full Details:

-
-
- Manage your notification settings here -
-

', +
', 'Student uploaded new proposal')) AS v(template_case, subject, body_html, description)) INSERT INTO email_templates (email_template_id,