Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -317,27 +317,32 @@ private void confirmApplicationToProfessor(Application application) {
}

/**
* Withdraws an application by setting its state to WITHDRAWN.
*
* @param applicationId the UUID of the application to withdraw
* Reverts a submitted application back to {@link ApplicationState#SAVED} so
* the applicant can edit and resubmit it. Only applications currently in
* {@link ApplicationState#SENT} can be unsubmitted, and only while the job's
* deadline has not yet passed.
*
* @param applicationId the UUID of the application to unsubmit
* @throws OperationNotAllowedException if the application is not in SENT state
* or the job deadline has already passed
*/
public void withdrawApplication(UUID applicationId) {
Application application = assertCanManageApplication(applicationId);
User user = application.getApplicant().getUser();
Job job = application.getJob();

application.setState(ApplicationState.WITHDRAWN);
application = applicationRepository.save(application);
if (application.getState() != ApplicationState.SENT) {
throw new OperationNotAllowedException(
"Application " + applicationId + " cannot be unsubmitted from state " + application.getState()
);
}

Email email = Email.builder()
.to(user)
.language(Language.fromCode(user.getSelectedLanguage()))
.emailType(EmailType.APPLICATION_WITHDRAWN)
.content(application)
.researchGroup(job.getResearchGroup())
.build();
LocalDate endDate = job.getEndDate();
if (endDate != null && endDate.isBefore(LocalDate.now())) {
throw new OperationNotAllowedException("Application " + applicationId + " cannot be unsubmitted after the job deadline");
}

sender.sendAsync(email);
application.setState(ApplicationState.SAVED);
applicationRepository.save(application);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,10 @@
*/
submittedReferenceLetters = computed(() =>
this.references()
.filter(reference => !!reference.documentId)

Check warning on line 96 in src/main/webapp/app/application/application-detail-for-applicant/application-detail-for-applicant.component.ts

View workflow job for this annotation

GitHub Actions / Client Quality & Tests

Unexpected nullable string value in conditional. Please handle the nullish/empty cases explicitly
.map(reference => ({
documentId: reference.documentId,
refereeName: [reference.firstName, reference.lastName].filter(part => !!part).join(' '),

Check warning on line 99 in src/main/webapp/app/application/application-detail-for-applicant/application-detail-for-applicant.component.ts

View workflow job for this annotation

GitHub Actions / Client Quality & Tests

Unexpected nullable string value in conditional. Please handle the nullish/empty cases explicitly
viewerInput: {
id: reference.documentId as string,
name: `${reference.firstName ?? ''} ${reference.lastName ?? ''}`.trim(),
Expand Down Expand Up @@ -142,8 +142,9 @@
});
}

// Add Withdraw button for SENT/IN_REVIEW states
if (['SENT', 'IN_REVIEW'].includes(app.applicationState)) {
// Unsubmit: only while the application is still in SENT (i.e. not yet
// picked up for review). Server also enforces the job-deadline guard.
if (app.applicationState === 'SENT') {
items.push({
label: 'button.withdraw',
icon: 'withdraw',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,15 +141,9 @@ export default class ApplicationOverviewForApplicantComponent {
});
}

// Withdraw action - for SENT or IN_REVIEW applications
if (
(
[
ApplicationOverviewDTOApplicationStateEnum.Sent,
ApplicationOverviewDTOApplicationStateEnum.InReview,
] as ApplicationOverviewDTOApplicationStateEnum[]
).includes(application.applicationState as ApplicationOverviewDTOApplicationStateEnum)
) {
// Unsubmit: only while the application is still in SENT (i.e. not yet
// picked up for review). Server also enforces the job-deadline guard.
if (application.applicationState === ApplicationOverviewDTOApplicationStateEnum.Sent) {
items.push({
label: 'button.withdraw',
icon: 'withdraw',
Expand Down
3 changes: 2 additions & 1 deletion src/main/webapp/i18n/de/button.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"save": "Speichern",
"send": "Senden",
"view": "Anzeigen",
"withdraw": "Zurückziehen",
"update": "Aktualisieren",
"withdraw": "Einreichung zurücknehmen",
"back": "Zurück",
"next": "Weiter",
"apply": "Bewerben",
Expand Down
20 changes: 10 additions & 10 deletions src/main/webapp/i18n/de/global.json
Original file line number Diff line number Diff line change
Expand Up @@ -445,8 +445,8 @@
"created": "Erstellt"
},
"dialogs": {
"withdrawHeader": "Zurückziehen bestätigen",
"withdrawMessage": "Bist du sicher, dass du diese Bewerbung zurückziehen möchtest? Sobald sie zurückgezogen wurde, ist sie für den Professor nicht mehr sichtbar. Diese Aktion kann NICHT rückgängig gemacht werden!",
"withdrawHeader": "Einreichung zurücknehmen?",
"withdrawMessage": "Deine Bewerbung wird zurück in den Entwurf-Status gesetzt. Der Professor sieht sie nicht mehr und du kannst sie vor Ablauf der Frist bearbeiten und erneut einreichen.",
"deleteHeader": "Löschvorgang bestätigen",
"deleteMessage": "Bist du sicher, dass du diesen Entwurf dauerhaft löschen möchtest? Diese Aktion kann NICHT rückgängig gemacht werden!"
},
Expand Down Expand Up @@ -536,12 +536,12 @@
"detail": "Die Bewerbung konnte nicht gelöscht werden. Bitte versuche es erneut, oder kontaktiere uns, wenn das Problem weiterhin besteht."
},
"applicationWithdrawn": {
"summary": "Bewerbung zurückgezogen",
"detail": "Deine Bewerbung wurde erfolgreich zurückgezogen."
"summary": "Bewerbung als Entwurf gespeichert",
"detail": "Deine Bewerbung ist wieder im Entwurf. Bearbeite und reiche sie vor der Frist erneut ein."
},
"errorWithdrawingApplication": {
"summary": "Zurückziehen fehlgeschlagen",
"detail": "Die Bewerbung konnte nicht zurückgezogen werden. Bitte versuche es erneut, oder kontaktiere uns, wenn das Problem weiterhin besteht."
"summary": "Zurücknahme fehlgeschlagen",
"detail": "Die Bewerbung konnte nicht in den Entwurf zurückgesetzt werden. Möglicherweise ist die Frist abgelaufen, bitte versuche es erneut."
},
"jobIdNotAvailable": {
"summary": "Fehler",
Expand Down Expand Up @@ -581,12 +581,12 @@
},
"withdraw": {
"success": {
"summary": "Bewerbung zurückgezogen",
"detail": "Deine Bewerbung wurde erfolgreich zurückgezogen."
"summary": "Bewerbung als Entwurf gespeichert",
"detail": "Deine Bewerbung ist wieder im Entwurf. Bearbeite und reiche sie vor der Frist erneut ein."
},
"error": {
"summary": "Zurückziehen fehlgeschlagen",
"detail": "Die Bewerbung konnte nicht zurückgezogen werden. Bitte versuche es erneut."
"summary": "Zurücknahme fehlgeschlagen",
"detail": "Die Bewerbung konnte nicht in den Entwurf zurückgesetzt werden. Möglicherweise ist die Frist abgelaufen."
}
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/main/webapp/i18n/en/button.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"save": "Save",
"send": "Send",
"view": "View",
"withdraw": "Withdraw",
"update": "Update",
Comment thread
az108 marked this conversation as resolved.
Outdated
"withdraw": "Unsubmit",
"back": "Back",
"next": "Next",
"apply": "Apply",
Expand Down
20 changes: 10 additions & 10 deletions src/main/webapp/i18n/en/global.json
Original file line number Diff line number Diff line change
Expand Up @@ -445,8 +445,8 @@
"created": "Created"
},
"dialogs": {
"withdrawHeader": "Confirm Withdrawal",
"withdrawMessage": "Are you sure you want to withdraw this application? Once withdrawn, it will no longer be visible to the professor. This action can NOT be undone!",
"withdrawHeader": "Unsubmit application?",
"withdrawMessage": "This will move your application back to draft. The professor will no longer see it, and you can edit and resubmit it before the job's deadline.",
"deleteHeader": "Confirm Delete Operation",
"deleteMessage": "Are you sure you want to permanently delete this draft? This action can NOT be undone!"
},
Expand Down Expand Up @@ -536,12 +536,12 @@
"detail": "The application could not be deleted. Please try again, or contact us if the problem persists."
},
"applicationWithdrawn": {
"summary": "Application withdrawn",
"detail": "Your application was withdrawn successfully."
"summary": "Application moved back to draft",
"detail": "Your application is back in draft. Edit and resubmit it before the deadline."
},
"errorWithdrawingApplication": {
"summary": "Withdrawal failed",
"detail": "The application could not be withdrawn. Please try again, or contact us if the problem persists."
"summary": "Couldn't move back to draft",
"detail": "The application could not be moved back to draft. The deadline may have passed, or please try again."
},
"jobIdNotAvailable": {
"summary": "Error",
Expand Down Expand Up @@ -581,12 +581,12 @@
},
"withdraw": {
"success": {
"summary": "Application withdrawn",
"detail": "Your application was withdrawn successfully."
"summary": "Application moved back to draft",
"detail": "Your application is back in draft. Edit and resubmit it before the deadline."
},
"error": {
"summary": "Withdrawal failed",
"detail": "The application could not be withdrawn. Please try again."
"summary": "Couldn't move back to draft",
"detail": "The application could not be moved back to draft. The deadline may have passed."
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -435,21 +435,49 @@ void deleteApplicationWithoutAuthReturnsForbidden() {
}
}

// ===== WITHDRAW APPLICATION =====
// ===== UNSUBMIT APPLICATION =====
@Nested
class WithdrawApplicationTests {

@Test
void withdrawApplicationMarksAsWithdrawn() {
void withdrawApplicationRevertsToDraft() {
Application application = ApplicationTestData.savedSent(applicationRepository, publishedJob, applicant);
assertThat(application.getState()).isEqualTo(ApplicationState.SENT);

api
.with(JwtPostProcessors.jwtUser(applicant.getUserId(), "ROLE_APPLICANT"))
.putAndRead("/api/applications/withdraw/" + application.getApplicationId(), null, Void.class, 200);

Application withdrawn = applicationRepository.findById(application.getApplicationId()).orElseThrow();
assertThat(withdrawn.getState()).isEqualTo(ApplicationState.WITHDRAWN);
Application reverted = applicationRepository.findById(application.getApplicationId()).orElseThrow();
assertThat(reverted.getState()).isEqualTo(ApplicationState.SAVED);
}

@Test
void withdrawApplicationFailsAfterDeadline() {
publishedJob.setEndDate(LocalDate.now().minusDays(1));
jobRepository.saveAndFlush(publishedJob);
Application application = ApplicationTestData.savedSent(applicationRepository, publishedJob, applicant);

api
.with(JwtPostProcessors.jwtUser(applicant.getUserId(), "ROLE_APPLICANT"))
.putAndRead("/api/applications/withdraw/" + application.getApplicationId(), null, Void.class, 400);

Application unchanged = applicationRepository.findById(application.getApplicationId()).orElseThrow();
assertThat(unchanged.getState()).isEqualTo(ApplicationState.SENT);
}

@Test
void withdrawApplicationFailsWhenNotInSentState() {
Application application = ApplicationTestData.savedSent(applicationRepository, publishedJob, applicant);
application.setState(ApplicationState.IN_REVIEW);
applicationRepository.saveAndFlush(application);

api
.with(JwtPostProcessors.jwtUser(applicant.getUserId(), "ROLE_APPLICANT"))
.putAndRead("/api/applications/withdraw/" + application.getApplicationId(), null, Void.class, 400);

Application unchanged = applicationRepository.findById(application.getApplicationId()).orElseThrow();
assertThat(unchanged.getState()).isEqualTo(ApplicationState.IN_REVIEW);
}

@Test
Expand Down
Loading