diff --git a/core/src/main/java/se/su/dsv/scipro/notifications/dataobject/ProjectEvent.java b/core/src/main/java/se/su/dsv/scipro/notifications/dataobject/ProjectEvent.java index c8c3e51032..db38cb81e1 100755 --- a/core/src/main/java/se/su/dsv/scipro/notifications/dataobject/ProjectEvent.java +++ b/core/src/main/java/se/su/dsv/scipro/notifications/dataobject/ProjectEvent.java @@ -20,7 +20,7 @@ public class ProjectEvent extends NotificationEvent { ROUGH_DRAFT_APPROVAL_APPROVED, ROUGH_DRAFT_APPROVAL_REJECTED, REVIEWER_GRADING_REPORT_SUBMITTED, ONE_YEAR_PASSED_FROM_LATEST_ANNUAL_REVIEW, SUPERVISOR_GRADING_INITIAL_ASSESSMENT_DONE, EXPORTED_SUCCESS, REVIEWER_GRADING_INITIAL_ASSESSMENT_DONE, FIRST_MEETING, OPPOSITION_FAILED, PARTICIPATION_APPROVED, COMPLETED, - PARTICIPATION_FAILED + REFLECTION_IMPROVEMENTS_REQUESTED, PARTICIPATION_FAILED } @ManyToOne diff --git a/core/src/main/java/se/su/dsv/scipro/notifications/notifications.properties b/core/src/main/java/se/su/dsv/scipro/notifications/notifications.properties index a972eac30e..513d92cbd5 100755 --- a/core/src/main/java/se/su/dsv/scipro/notifications/notifications.properties +++ b/core/src/main/java/se/su/dsv/scipro/notifications/notifications.properties @@ -85,6 +85,10 @@ PROJECT.PARTICIPATION_APPROVED.body = Your active participation on {0} has been PROJECT.PARTICIPATION_FAILED.title = Your active participation on {1} did not meet the minimum requirements. PROJECT.PARTICIPATION_FAILED.body = Your active participation did not meet the minimum requirements set, and you will \ have to be an active participant on a different final seminar to pass this step. +PROJECT.REFLECTION_IMPROVEMENTS_REQUESTED.title = Reflection improvements requested +PROJECT.REFLECTION_IMPROVEMENTS_REQUESTED.body = The supervisor has deemed that the reflection submitted does not meet \ + the minimum requirements and has requested improvements. Please log into SciPro and submit a new reflection. \ + Their comments can be seen below:\n\n{0} FORUM.NEW_FORUM_POST.title = Forum post: {2} FORUM.NEW_FORUM_POST.body = New forum post submitted:

{0} diff --git a/core/src/main/java/se/su/dsv/scipro/project/Author.java b/core/src/main/java/se/su/dsv/scipro/project/Author.java index c3094e697c..e8f2ac5704 100644 --- a/core/src/main/java/se/su/dsv/scipro/project/Author.java +++ b/core/src/main/java/se/su/dsv/scipro/project/Author.java @@ -39,6 +39,15 @@ public class Author { @Basic(optional = true) private String reflection; + @Basic + @Enumerated(EnumType.STRING) + @Column(name = "reflection_status") + private ReflectionStatus reflectionStatus = ReflectionStatus.NOT_SUBMITTED; + + @Basic + @Column(name = "reflection_comment_by_supervisor") + private String reflectionSupervisorComment; + public Project getProject() { return project; } @@ -63,6 +72,22 @@ public class Author { this.reflection = reflection; } + public ReflectionStatus getReflectionStatus() { + return reflectionStatus; + } + + public void setReflectionStatus(ReflectionStatus reflectionStatus) { + this.reflectionStatus = reflectionStatus; + } + + public void setReflectionSupervisorComment(String reflectionSupervisorComment) { + this.reflectionSupervisorComment = reflectionSupervisorComment; + } + + public String getReflectionSupervisorComment() { + return reflectionSupervisorComment; + } + @Embeddable public static class AuthorPK implements Serializable { private Long projectId; diff --git a/core/src/main/java/se/su/dsv/scipro/project/ReflectionStatus.java b/core/src/main/java/se/su/dsv/scipro/project/ReflectionStatus.java new file mode 100644 index 0000000000..92b94cd7b6 --- /dev/null +++ b/core/src/main/java/se/su/dsv/scipro/project/ReflectionStatus.java @@ -0,0 +1,7 @@ +package se.su.dsv.scipro.project; + +public enum ReflectionStatus { + NOT_SUBMITTED, + SUBMITTED, + IMPROVEMENTS_NEEDED +} diff --git a/core/src/main/java/se/su/dsv/scipro/reflection/Reflection.java b/core/src/main/java/se/su/dsv/scipro/reflection/Reflection.java new file mode 100644 index 0000000000..898d961a2c --- /dev/null +++ b/core/src/main/java/se/su/dsv/scipro/reflection/Reflection.java @@ -0,0 +1,22 @@ +package se.su.dsv.scipro.reflection; + +public sealed interface Reflection { + boolean isSubmittable(); + + record NotSubmitted() implements Reflection { + @Override + public boolean isSubmittable() { return true; } + } + + record Submitted(String reflection) implements Reflection { + @Override + public boolean isSubmittable() { + return false; + } + } + + record ImprovementsNeeded(String oldReflection, String commentBySupervisor) implements Reflection { + @Override + public boolean isSubmittable() { return true; } + } +} diff --git a/core/src/main/java/se/su/dsv/scipro/reflection/ReflectionImprovementsRequestedEvent.java b/core/src/main/java/se/su/dsv/scipro/reflection/ReflectionImprovementsRequestedEvent.java new file mode 100644 index 0000000000..07d8811f99 --- /dev/null +++ b/core/src/main/java/se/su/dsv/scipro/reflection/ReflectionImprovementsRequestedEvent.java @@ -0,0 +1,7 @@ +package se.su.dsv.scipro.reflection; + +import se.su.dsv.scipro.project.Project; +import se.su.dsv.scipro.system.User; + +public record ReflectionImprovementsRequestedEvent(Project project, User author, String supervisorComment) { +} diff --git a/core/src/main/java/se/su/dsv/scipro/reflection/ReflectionService.java b/core/src/main/java/se/su/dsv/scipro/reflection/ReflectionService.java index b4a206bf76..139dc9a6d9 100644 --- a/core/src/main/java/se/su/dsv/scipro/reflection/ReflectionService.java +++ b/core/src/main/java/se/su/dsv/scipro/reflection/ReflectionService.java @@ -14,4 +14,15 @@ public interface ReflectionService { * @return the reflection, or {@code null} if none has been submitted */ String getSubmittedReflection(Project project, User author); + + /** + * Used by the supervisor when the currently submitted reflection does not meet the minimum requirements. + * This is done individually by author. + * + * @param author the author whose reflection does not meet the minimum requirements. + * @param supervisorComment feedback provided by the supervisor so the author knows what to improve. + */ + void requestNewReflection(Project project, User author, String supervisorComment); + + Reflection getReflection(Project project, User author); } diff --git a/core/src/main/java/se/su/dsv/scipro/reflection/ReflectionServiceImpl.java b/core/src/main/java/se/su/dsv/scipro/reflection/ReflectionServiceImpl.java index ccd23a6244..6b08ec3ea6 100644 --- a/core/src/main/java/se/su/dsv/scipro/reflection/ReflectionServiceImpl.java +++ b/core/src/main/java/se/su/dsv/scipro/reflection/ReflectionServiceImpl.java @@ -1,22 +1,32 @@ package se.su.dsv.scipro.reflection; +import com.google.common.eventbus.EventBus; import com.google.inject.persist.Transactional; import se.su.dsv.scipro.finalseminar.AuthorRepository; import se.su.dsv.scipro.finalseminar.FinalSeminarService; import se.su.dsv.scipro.project.Author; import se.su.dsv.scipro.project.Project; +import se.su.dsv.scipro.project.ReflectionStatus; import se.su.dsv.scipro.system.User; import jakarta.inject.Inject; +import java.util.Optional; + class ReflectionServiceImpl implements ReflectionService { private final AuthorRepository authorRepository; private final FinalSeminarService finalSeminarService; + private final EventBus eventBus; @Inject - ReflectionServiceImpl(AuthorRepository authorRepository, FinalSeminarService finalSeminarService) { + ReflectionServiceImpl( + AuthorRepository authorRepository, + FinalSeminarService finalSeminarService, + EventBus eventBus) + { this.authorRepository = authorRepository; this.finalSeminarService = finalSeminarService; + this.eventBus = eventBus; } @Override @@ -25,10 +35,13 @@ class ReflectionServiceImpl implements ReflectionService { } @Override - public boolean hasToFillInReflection(Project project, User author) { - boolean noReflectionSubmitted = authorRepository.findByProjectAndUser(project, author) - .map(Author::getReflection) - .isEmpty(); + public boolean hasToFillInReflection(Project project, User user) { + Optional optionalAuthor = authorRepository.findByProjectAndUser(project, user); + if (optionalAuthor.isEmpty()) { + return false; + } + Author author = optionalAuthor.get(); + boolean noReflectionSubmitted = author.getReflectionStatus() != ReflectionStatus.SUBMITTED; return hasReachedReflectionProcess(project) && noReflectionSubmitted; } @@ -36,7 +49,10 @@ class ReflectionServiceImpl implements ReflectionService { @Transactional public void submitReflection(Project project, User user, String reflection) { authorRepository.findByProjectAndUser(project, user) - .ifPresent(author -> author.setReflection(reflection)); + .ifPresent(author -> { + author.setReflection(reflection); + author.setReflectionStatus(ReflectionStatus.SUBMITTED); + }); } @Override @@ -45,4 +61,32 @@ class ReflectionServiceImpl implements ReflectionService { .map(Author::getReflection) .orElse(null); } + + @Override + @Transactional + public void requestNewReflection(Project project, User user, String supervisorComment) { + authorRepository.findByProjectAndUser(project, user) + .ifPresent(author -> { + author.setReflectionStatus(ReflectionStatus.IMPROVEMENTS_NEEDED); + author.setReflectionSupervisorComment(supervisorComment); + }); + eventBus.post(new ReflectionImprovementsRequestedEvent(project, user, supervisorComment)); + } + + @Override + public Reflection getReflection(Project project, User author) { + return authorRepository.findByProjectAndUser(project, author) + .map(this::toReflection) + .orElseGet(Reflection.NotSubmitted::new); + } + + private Reflection toReflection(Author author) { + return switch (author.getReflectionStatus()) { + case SUBMITTED -> new Reflection.Submitted(author.getReflection()); + case IMPROVEMENTS_NEEDED -> new Reflection.ImprovementsNeeded( + author.getReflection(), + author.getReflectionSupervisorComment()); + default -> new Reflection.NotSubmitted(); + }; + } } diff --git a/core/src/main/resources/db/migration/V389__reflection_resubmission.sql b/core/src/main/resources/db/migration/V389__reflection_resubmission.sql new file mode 100644 index 0000000000..d80e266c9d --- /dev/null +++ b/core/src/main/resources/db/migration/V389__reflection_resubmission.sql @@ -0,0 +1,4 @@ +ALTER TABLE `project_user` + ADD COLUMN `reflection_status` VARCHAR(32) NOT NULL DEFAULT 'NOT_SUBMITTED'; + +UPDATE `project_user` SET `reflection_status` = 'SUBMITTED' WHERE `reflection` IS NOT NULL; diff --git a/core/src/main/resources/db/migration/V390__reflection_comment_by_supervisor.sql b/core/src/main/resources/db/migration/V390__reflection_comment_by_supervisor.sql new file mode 100644 index 0000000000..f3cc5f2ce1 --- /dev/null +++ b/core/src/main/resources/db/migration/V390__reflection_comment_by_supervisor.sql @@ -0,0 +1 @@ +ALTER TABLE `project_user` ADD COLUMN `reflection_comment_by_supervisor` TEXT NULL; diff --git a/core/src/test/java/se/su/dsv/scipro/reflection/ReflectionServiceTest.java b/core/src/test/java/se/su/dsv/scipro/reflection/ReflectionServiceTest.java index 8d8d462e31..745def4bcd 100644 --- a/core/src/test/java/se/su/dsv/scipro/reflection/ReflectionServiceTest.java +++ b/core/src/test/java/se/su/dsv/scipro/reflection/ReflectionServiceTest.java @@ -101,6 +101,26 @@ public class ReflectionServiceTest extends IntegrationTest { assertTrue(reflectionService.hasReachedReflectionProcess(project)); } + @Test + public void request_resubmission() { + LocalDate seminarDate = scheduleSeminar(); + clock.setDate(seminarDate.plusDays(1)); + assertTrue(reflectionService.hasToFillInReflection(project, author), + "After the final seminar the author should be required to submit a reflection"); + + String myReflection = "my reflection"; + reflectionService.submitReflection(project, author, myReflection); + assertEquals(myReflection, reflectionService.getSubmittedReflection(project, author)); + assertFalse(reflectionService.hasToFillInReflection(project, author), + "After submitting the initial reflection it should no longer be required"); + + reflectionService.requestNewReflection(project, author, "Very bad reflection"); + assertTrue(reflectionService.hasToFillInReflection(project, author), + "After supervisor requests resubmission the author should now be required to submit a new reflection"); + assertEquals(myReflection, reflectionService.getSubmittedReflection(project, author), + "The old reflection should be saved to make it easier for the student to update it"); + } + private LocalDate scheduleSeminar() { project.setFinalSeminarRuleExempted(true); // to bypass rough draft approval FinalSeminarDetails details = new FinalSeminarDetails("Zoom", false, 1, 1, Language.SWEDISH, Language.ENGLISH, "zoom id 123"); diff --git a/view/src/main/java/se/su/dsv/scipro/crosscutting/CrosscuttingModule.java b/view/src/main/java/se/su/dsv/scipro/crosscutting/CrosscuttingModule.java index fc50e7ff37..745812b613 100644 --- a/view/src/main/java/se/su/dsv/scipro/crosscutting/CrosscuttingModule.java +++ b/view/src/main/java/se/su/dsv/scipro/crosscutting/CrosscuttingModule.java @@ -13,5 +13,6 @@ public class CrosscuttingModule extends AbstractModule { bind(ReviewerAssignedNotifications.class).asEagerSingleton(); bind(ReviewerAssignedDeadline.class).asEagerSingleton(); bind(ForwardPhase2Feedback.class).asEagerSingleton(); + bind(NotifyFailedReflection.class).asEagerSingleton(); } } diff --git a/view/src/main/java/se/su/dsv/scipro/crosscutting/NotifyFailedReflection.java b/view/src/main/java/se/su/dsv/scipro/crosscutting/NotifyFailedReflection.java new file mode 100644 index 0000000000..e626522bec --- /dev/null +++ b/view/src/main/java/se/su/dsv/scipro/crosscutting/NotifyFailedReflection.java @@ -0,0 +1,33 @@ +package se.su.dsv.scipro.crosscutting; + +import com.google.common.eventbus.EventBus; +import com.google.common.eventbus.Subscribe; +import jakarta.inject.Inject; +import se.su.dsv.scipro.data.dataobjects.Member; +import se.su.dsv.scipro.notifications.NotificationController; +import se.su.dsv.scipro.notifications.dataobject.NotificationSource; +import se.su.dsv.scipro.notifications.dataobject.ProjectEvent; +import se.su.dsv.scipro.reflection.ReflectionImprovementsRequestedEvent; + +import java.util.Set; + +public class NotifyFailedReflection { + private final NotificationController notificationController; + + @Inject + public NotifyFailedReflection(NotificationController notificationController, EventBus eventBus) { + this.notificationController = notificationController; + eventBus.register(this); + } + + @Subscribe + public void reflectionImprovementsRequested(ReflectionImprovementsRequestedEvent event) { + NotificationSource source = new NotificationSource(); + source.setMessage(event.supervisorComment()); + notificationController.notifyCustomProject( + event.project(), + ProjectEvent.Event.REFLECTION_IMPROVEMENTS_REQUESTED, + source, + Set.of(new Member(event.author(), Member.Type.AUTHOR))); + } +} diff --git a/view/src/main/java/se/su/dsv/scipro/grading/CriteriaPanel.java b/view/src/main/java/se/su/dsv/scipro/grading/CriteriaPanel.java index 2dcfa3420a..58373509aa 100644 --- a/view/src/main/java/se/su/dsv/scipro/grading/CriteriaPanel.java +++ b/view/src/main/java/se/su/dsv/scipro/grading/CriteriaPanel.java @@ -34,6 +34,7 @@ import se.su.dsv.scipro.reflection.ReflectionService; import se.su.dsv.scipro.report.AbstractGradingCriterion; import se.su.dsv.scipro.report.GradingCriterion; import se.su.dsv.scipro.report.GradingCriterionPoint; +import se.su.dsv.scipro.report.GradingReport; import se.su.dsv.scipro.report.SupervisorGradingReport; import se.su.dsv.scipro.system.Language; import se.su.dsv.scipro.system.User; @@ -271,7 +272,10 @@ public class CriteriaPanel extends GenericPanel { modal = new LargeModalWindow("modal"); modal.setTitle("Reflection"); - modal.setContent(id_ -> new MultiLineLabel(id_, new NullReplacementModel(reflection, "No reflection filled in."))); + modal.setContent(modalBodyId -> { + IModel projectModel = CriteriaPanel.this.getModel().map(GradingReport::getProject); + return new ReflectionModalBodyPanel(modalBodyId, projectModel, author); + }); add(modal); WebMarkupContainer showReflection = new WebMarkupContainer("showReflection") { diff --git a/view/src/main/java/se/su/dsv/scipro/grading/ReflectionModalBodyPanel.html b/view/src/main/java/se/su/dsv/scipro/grading/ReflectionModalBodyPanel.html new file mode 100644 index 0000000000..a7efa53954 --- /dev/null +++ b/view/src/main/java/se/su/dsv/scipro/grading/ReflectionModalBodyPanel.html @@ -0,0 +1,45 @@ + + + + + +
+

+ + You've requested improvements to the submitted version. + +

+ + [Supervisor feedback on needed improvements] + +
+
+ + + [Authors submitted reflection] + + +
+
+

+ + Please provide feedback on what improvements are needed in the submitted version. + +

+
+ + +
+ +
+
+ + \ No newline at end of file diff --git a/view/src/main/java/se/su/dsv/scipro/grading/ReflectionModalBodyPanel.java b/view/src/main/java/se/su/dsv/scipro/grading/ReflectionModalBodyPanel.java new file mode 100644 index 0000000000..d5f3ce9a43 --- /dev/null +++ b/view/src/main/java/se/su/dsv/scipro/grading/ReflectionModalBodyPanel.java @@ -0,0 +1,102 @@ +package se.su.dsv.scipro.grading; + +import jakarta.inject.Inject; +import org.apache.wicket.ajax.AjaxRequestTarget; +import org.apache.wicket.ajax.markup.html.form.AjaxSubmitLink; +import org.apache.wicket.markup.html.basic.Label; +import org.apache.wicket.markup.html.basic.MultiLineLabel; +import org.apache.wicket.markup.html.form.Form; +import org.apache.wicket.markup.html.form.TextArea; +import org.apache.wicket.markup.html.panel.Panel; +import org.apache.wicket.model.IModel; +import org.apache.wicket.model.Model; +import se.su.dsv.scipro.project.Project; +import se.su.dsv.scipro.reflection.Reflection; +import se.su.dsv.scipro.reflection.ReflectionService; +import se.su.dsv.scipro.system.User; + +/** + * This is not meant to be a re-usable panel and is made specifically to be used + * as the body of the modal dialog that opens when a supervisor views the + * author's reflection as they're doing their final individual assessment. + */ +class ReflectionModalBodyPanel extends Panel { + @Inject + private ReflectionService reflectionService; + + private final IModel projectModel; + private final IModel authorModel; + + ReflectionModalBodyPanel(String id, IModel projectModel, IModel authorModel) { + super(id); + this.projectModel = projectModel; + this.authorModel = authorModel; + + setOutputMarkupPlaceholderTag(true); // enable ajax refreshing of the entire body + + IModel reflectionModel = projectModel.combineWith(authorModel, reflectionService::getReflection); + + add(new MultiLineLabel("reflection_text", reflectionModel.map(this::getReflectionText))); + + add(new MultiLineLabel("improvements_needed_supervisor_feedback", reflectionModel + .as(Reflection.ImprovementsNeeded.class) + .map(Reflection.ImprovementsNeeded::commentBySupervisor)) { + @Override + protected void onConfigure() { + super.onConfigure(); + setVisible(!getDefaultModelObjectAsString().isBlank()); + } + }); + + add(new RequestImprovementsForm("request_improvements_form", reflectionModel)); + } + + private String getReflectionText(Reflection reflection) { + if (reflection instanceof Reflection.Submitted submitted) { + return submitted.reflection(); + } else if (reflection instanceof Reflection.ImprovementsNeeded improvementsNeeded) { + return improvementsNeeded.oldReflection(); + } else { + return getString("reflection_not_submitted"); + } + } + + @Override + protected void onDetach() { + this.projectModel.detach(); + this.authorModel.detach(); + super.onDetach(); + } + + private class RequestImprovementsForm extends Form { + public RequestImprovementsForm(String id, IModel reflectionModel) { + super(id, reflectionModel); + + IModel commentModel = new Model<>(); + + TextArea comment = new TextArea<>("comment", commentModel); + comment.setRequired(true); + add(comment); + + add(new AjaxSubmitLink("submit") { + @Override + protected void onSubmit(AjaxRequestTarget target) { + super.onSubmit(target); + + reflectionService.requestNewReflection( + projectModel.getObject(), + authorModel.getObject(), + commentModel.getObject()); + + target.add(ReflectionModalBodyPanel.this); + } + }); + } + + @Override + protected void onConfigure() { + super.onConfigure(); + setVisible(getModelObject() instanceof Reflection.Submitted); + } + } +} diff --git a/view/src/main/java/se/su/dsv/scipro/grading/ReflectionModalBodyPanel.utf8.properties b/view/src/main/java/se/su/dsv/scipro/grading/ReflectionModalBodyPanel.utf8.properties new file mode 100644 index 0000000000..9d29420810 --- /dev/null +++ b/view/src/main/java/se/su/dsv/scipro/grading/ReflectionModalBodyPanel.utf8.properties @@ -0,0 +1,7 @@ +improvements_requested=You've requested improvements to the submitted version. +request_improvements=Request improvements +comment=Comment +reflection_not_submitted=Reflection not submitted yet +request_improvements_text=If the submitted reflection does not meet the minimum requirements \ + you can request improvements from the student. The student will be notified and can submit a new reflection. \ + Use the comment field to provide feedback to the student. diff --git a/view/src/main/java/se/su/dsv/scipro/project/panels/FinalStepsPanel.html b/view/src/main/java/se/su/dsv/scipro/project/panels/FinalStepsPanel.html index a50d6d7828..6cc7d5661d 100644 --- a/view/src/main/java/se/su/dsv/scipro/project/panels/FinalStepsPanel.html +++ b/view/src/main/java/se/su/dsv/scipro/project/panels/FinalStepsPanel.html @@ -38,6 +38,30 @@
+ +
+

+ + Reflection improvements needed + +

+

+ + Your supervisor has requested that you improve and resubmit your reflection. + See their comments below about what changes are necessary. + +

+

+ [You need to reflect more on the methods you used.] +

+
+

+ + Your previous reflection + +

+

+