Allow changes to the reflection to be made after it's been submitted ()

Replaces 

Fixes card 3213 and 3412

There are minimum requirements for the reflection document submitted by authors at the end of the thesis process. Before now there was no way to handle the case when the reflection did not meet these minimum requirements.

This change makes it possible in two ways;
 1. The supervisor can request improvements to be made requiring the author to re-submit a new reflection inside SciPro
 2. The supervisor can directly edit the reflection themselves if it has been submitted out-of-band or for any other reason

Co-authored-by: Nico Athanassiadis <nico@dsv.su.se>
Reviewed-on: 
Reviewed-by: Nico Athanassiadis <nico@dsv.su.se>
This commit is contained in:
Andreas Svanberg 2024-11-21 19:20:47 +01:00
parent 73307096c3
commit ff4c5b58b4
26 changed files with 628 additions and 25 deletions

@ -744,9 +744,10 @@ public class CoreConfig {
@Bean
public ReflectionServiceImpl reflectionService(
AuthorRepository authorRepository,
FinalSeminarServiceImpl finalSeminarService)
FinalSeminarServiceImpl finalSeminarService,
EventBus eventBus)
{
return new ReflectionServiceImpl(authorRepository, finalSeminarService);
return new ReflectionServiceImpl(authorRepository, finalSeminarService, eventBus);
}
@Bean

@ -23,7 +23,8 @@ 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
PARTICIPATION_APPROVED, COMPLETED, PARTICIPATION_FAILED, REFLECTION_IMPROVEMENTS_REQUESTED,
REFLECTION_IMPROVEMENTS_SUBMITTED
}
@Basic

@ -85,6 +85,13 @@ 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}
PROJECT.REFLECTION_IMPROVEMENTS_SUBMITTED.title = Reflection improvements submitted
PROJECT.REFLECTION_IMPROVEMENTS_SUBMITTED.body = The reflection improvements have been submitted. \
\n\n{0}
FORUM.NEW_FORUM_POST.title = Forum post: {2}
FORUM.NEW_FORUM_POST.body = New forum post submitted:<br /><br />{0}

@ -5,6 +5,8 @@ import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import jakarta.persistence.EmbeddedId;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.MapsId;
@ -30,6 +32,15 @@ public class Author {
@Column(name = "reflection")
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;
/**
* If this author wants to be notified when a final seminar created
* as long as they have not yet completed both an opposition and
@ -85,6 +96,22 @@ public class Author {
return user;
}
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;
}
// ----------------------------------------------------------------------------------
// Nested class
// ----------------------------------------------------------------------------------

@ -0,0 +1,7 @@
package se.su.dsv.scipro.project;
public enum ReflectionStatus {
NOT_SUBMITTED,
SUBMITTED,
IMPROVEMENTS_NEEDED
}

@ -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; }
}
}

@ -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) {
}

@ -0,0 +1,10 @@
package se.su.dsv.scipro.reflection;
import se.su.dsv.scipro.project.Project;
import se.su.dsv.scipro.system.User;
/**
* This event may be triggered by the supervisor if they edit the reflection after requesting improvements.
*/
public record ReflectionImprovementsSubmittedEvent(Project project, User author, String reflection) {
}

@ -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);
}

@ -1,22 +1,32 @@
package se.su.dsv.scipro.reflection;
import com.google.common.eventbus.EventBus;
import jakarta.transaction.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;
public class ReflectionServiceImpl implements ReflectionService {
private final AuthorRepository authorRepository;
private final FinalSeminarService finalSeminarService;
private final EventBus eventBus;
@Inject
public ReflectionServiceImpl(AuthorRepository authorRepository, FinalSeminarService finalSeminarService) {
public ReflectionServiceImpl(
AuthorRepository authorRepository,
FinalSeminarService finalSeminarService,
EventBus eventBus)
{
this.authorRepository = authorRepository;
this.finalSeminarService = finalSeminarService;
this.eventBus = eventBus;
}
@Override
@ -25,10 +35,13 @@ public 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<Author> 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,13 @@ public 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 -> {
if (author.getReflectionStatus() == ReflectionStatus.IMPROVEMENTS_NEEDED) {
eventBus.post(new ReflectionImprovementsSubmittedEvent(project, user, reflection));
}
author.setReflection(reflection);
author.setReflectionStatus(ReflectionStatus.SUBMITTED);
});
}
@Override
@ -45,4 +64,32 @@ public 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();
};
}
}

@ -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;

@ -0,0 +1 @@
ALTER TABLE `project_user` ADD COLUMN `reflection_comment_by_supervisor` TEXT NULL;

@ -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");

@ -0,0 +1,46 @@
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 se.su.dsv.scipro.reflection.ReflectionImprovementsSubmittedEvent;
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)));
}
@Subscribe
public void reflectionImprovementsSubmittted(ReflectionImprovementsSubmittedEvent event) {
NotificationSource source = new NotificationSource();
source.setMessage(event.reflection());
source.setAdditionalMessage(event.author().getFullName());
notificationController.notifyCustomProject(
event.project(),
ProjectEvent.Event.REFLECTION_IMPROVEMENTS_SUBMITTED,
source,
Set.of(new Member(event.project().getHeadSupervisor(), Member.Type.SUPERVISOR)));
}
}

@ -49,10 +49,16 @@
</div>
</wicket:fragment>
</div>
<wicket:container wicket:id="reflection">
<div wicket:id="reflection">
<wicket:enclosure>
<div class="alert alert-danger">
<h4 class="alert-heading">Improvements requested</h4>
<wicket:container wicket:id="improvementsNeeded"/>
</div>
</wicket:enclosure>
<a wicket:id="showReflection" href="#">View reflection</a>
<div wicket:id="modal"></div>
</wicket:container>
</div>
</fieldset>
</wicket:panel>
</body>

@ -30,10 +30,12 @@ import se.su.dsv.scipro.finalseminar.FinalSeminar;
import se.su.dsv.scipro.finalseminar.FinalSeminarOpposition;
import se.su.dsv.scipro.finalseminar.FinalSeminarService;
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.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;
@ -264,14 +266,33 @@ public class CriteriaPanel extends GenericPanel<SupervisorGradingReport> {
super(id, author);
this.gradingCriterion = gradingCriterion;
IModel<String> reflection = LoadableDetachableModel.of(() -> {
IModel<Reflection> reflection = LoadableDetachableModel.of(() -> {
Project project = CriteriaPanel.this.getModelObject().getProject();
return reflectionService.getSubmittedReflection(project, author.getObject());
return reflectionService.getReflection(project, author.getObject());
});
IModel<String> improvementsNeeded = reflection
.as(Reflection.ImprovementsNeeded.class)
.map(Reflection.ImprovementsNeeded::commentBySupervisor);
add(new MultiLineLabel("improvementsNeeded", improvementsNeeded) {
@Override
protected void onConfigure() {
super.onConfigure();
setVisible(!getDefaultModelObjectAsString().isBlank());
}
});
modal = new LargeModalWindow("modal");
modal.setTitle("Reflection");
modal.setContent(id_ -> new MultiLineLabel(id_, new NullReplacementModel(reflection, "No reflection filled in.")));
modal.setContent(modalBodyId -> {
IModel<Project> projectModel = CriteriaPanel.this.getModel().map(GradingReport::getProject);
return new ReflectionModalBodyPanel(modalBodyId, projectModel, author);
});
this.setOutputMarkupId(true);
this.setOutputMarkupPlaceholderTag(true);
modal.onClose(target -> {
target.add(ReflectionFeedbackPanel.this);
});
add(modal);
WebMarkupContainer showReflection = new WebMarkupContainer("showReflection") {

@ -0,0 +1,82 @@
<?xml version="1.0" encoding="UTF-8"?>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:wicket="http://wicket.apache.org">
<body>
<wicket:panel>
<wicket:enclosure>
<div class="alert alert-info">
<h4 class="alert-heading">
<wicket:message key="improvements_requested">
You've requested improvements to the submitted version.
</wicket:message>
</h4>
<wicket:container wicket:id="improvements_needed_supervisor_feedback">
[Supervisor feedback on needed improvements]
</wicket:container>
</div>
</wicket:enclosure>
<wicket:container wicket:id="reflection_text">
[Authors submitted reflection]
</wicket:container>
<button class="btn btn-outline-secondary" wicket:id="show_edit_reflection_form">
<wicket:message key="edit_reflection">
Edit reflection
</wicket:message>
</button>
<button class="btn btn-outline-secondary" wicket:id="show_request_improvements_form">
<wicket:message key="request_improvements">
Request improvements
</wicket:message>
</button>
<form wicket:id="request_improvements_form">
<hr>
<p>
<wicket:message key="request_improvements_text">
Please provide feedback on what improvements are needed in the submitted version.
</wicket:message>
</p>
<div class="mb-3">
<label class="form-label" wicket:for="comment">
<wicket:message key="comment">
Comment
</wicket:message>
</label>
<textarea class="form-control" wicket:id="comment" rows="5"></textarea>
</div>
<button class="btn btn-primary" wicket:id="submit">
<wicket:message key="request_improvements">
Request improvements
</wicket:message>
</button>
<button class="btn btn-link" wicket:id="cancel">
<wicket:message key="cancel">
Cancel
</wicket:message>
</button>
</form>
<form wicket:id="edit_reflection_form">
<div class="mb-3">
<label class="form-label">
<wicket:message key="reflection">
Reflection
</wicket:message>
</label>
<textarea class="form-control" wicket:id="reflection" rows="10"></textarea>
</div>
<button class="btn btn-primary" wicket:id="submit">
<wicket:message key="save">
Save
</wicket:message>
</button>
<button class="btn btn-link" wicket:id="cancel">
<wicket:message key="cancel">
Cancel
</wicket:message>
</button>
</form>
</wicket:panel>
</body>
</html>

@ -0,0 +1,191 @@
package se.su.dsv.scipro.grading;
import jakarta.inject.Inject;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.markup.html.AjaxLink;
import org.apache.wicket.ajax.markup.html.form.AjaxSubmitLink;
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<Project> projectModel;
private final IModel<User> authorModel;
private enum State { VIEWING, REQUESTING_IMPROVEMENTS, EDITING }
private State state = State.VIEWING;
ReflectionModalBodyPanel(String id, IModel<Project> projectModel, IModel<User> authorModel) {
super(id);
this.projectModel = projectModel;
this.authorModel = authorModel;
setOutputMarkupPlaceholderTag(true); // enable ajax refreshing of the entire body
IModel<Reflection> reflectionModel = projectModel.combineWith(authorModel, reflectionService::getReflection);
add(new MultiLineLabel("reflection_text", reflectionModel.map(this::getReflectionText)) {
@Override
protected void onConfigure() {
super.onConfigure();
setVisible(state != State.EDITING);
}
});
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));
add(new SupervisorEditReflectionForm("edit_reflection_form", reflectionModel));
add(new AjaxLink<>("show_request_improvements_form", reflectionModel) {
@Override
public void onClick(AjaxRequestTarget target) {
ReflectionModalBodyPanel.this.state = State.REQUESTING_IMPROVEMENTS;
target.add(ReflectionModalBodyPanel.this);
}
@Override
protected void onConfigure() {
super.onConfigure();
Reflection reflection = getModelObject();
boolean canRequestImprovements = reflection instanceof Reflection.Submitted;
setVisible(state == State.VIEWING && canRequestImprovements && isEnabledInHierarchy());
}
});
add(new AjaxLink<>("show_edit_reflection_form", reflectionModel) {
@Override
public void onClick(AjaxRequestTarget target) {
ReflectionModalBodyPanel.this.state = State.EDITING;
target.add(ReflectionModalBodyPanel.this);
}
@Override
protected void onConfigure() {
super.onConfigure();
Reflection reflection = getModelObject();
boolean canEditReflection = reflection instanceof Reflection.Submitted || reflection instanceof Reflection.ImprovementsNeeded;
setVisible(state == State.VIEWING && canEditReflection && isEnabledInHierarchy());
}
});
}
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<Reflection> {
public RequestImprovementsForm(String id, IModel<Reflection> reflectionModel) {
super(id, reflectionModel);
IModel<String> commentModel = new Model<>();
TextArea<String> 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());
ReflectionModalBodyPanel.this.state = State.VIEWING;
target.add(ReflectionModalBodyPanel.this);
}
});
add(new AjaxLink<>("cancel") {
@Override
public void onClick(AjaxRequestTarget target) {
ReflectionModalBodyPanel.this.state = State.VIEWING;
target.add(ReflectionModalBodyPanel.this);
}
});
}
@Override
protected void onConfigure() {
super.onConfigure();
setVisible(state == State.REQUESTING_IMPROVEMENTS && getModelObject() instanceof Reflection.Submitted);
}
}
private class SupervisorEditReflectionForm extends Form<Reflection> {
public SupervisorEditReflectionForm(String id, IModel<Reflection> reflectionModel) {
super(id, reflectionModel);
IModel<String> reflectionTextModel = new Model<>(getReflectionText(reflectionModel.getObject()));
TextArea<String> reflectionTextArea = new TextArea<>("reflection", reflectionTextModel);
reflectionTextArea.setRequired(true);
add(reflectionTextArea);
add(new AjaxSubmitLink("submit") {
@Override
protected void onSubmit(AjaxRequestTarget target) {
reflectionService.submitReflection(
projectModel.getObject(),
authorModel.getObject(),
reflectionTextModel.getObject());
ReflectionModalBodyPanel.this.state = State.VIEWING;
target.add(ReflectionModalBodyPanel.this);
}
});
add(new AjaxLink<>("cancel") {
@Override
public void onClick(AjaxRequestTarget target) {
ReflectionModalBodyPanel.this.state = State.VIEWING;
target.add(ReflectionModalBodyPanel.this);
}
});
}
@Override
protected void onConfigure() {
super.onConfigure();
setVisible(state == State.EDITING);
}
}
}

@ -0,0 +1,10 @@
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.
edit_reflection=Edit reflection
reflection=Reflection
cancel=Cancel

@ -158,7 +158,7 @@ public class NotificationLandingPage extends WebPage {
setResponsePage(RoughDraftApprovalDecisionPage.class, reviewerParameters);
}
break;
case REVIEWER_GRADING_INITIAL_ASSESSMENT_DONE, REVIEWER_GRADING_REPORT_SUBMITTED:
case REVIEWER_GRADING_INITIAL_ASSESSMENT_DONE, REVIEWER_GRADING_REPORT_SUBMITTED, REFLECTION_IMPROVEMENTS_SUBMITTED:
if (project.isSupervisor(currentUser)) {
setResponsePage(SupervisorGradingReportPage.class, pp);
}

@ -38,6 +38,30 @@
<div wicket:id="current_thesis_file"></div>
</div>
</wicket:enclosure>
<wicket:enclosure child="old_reflection">
<div class="alert alert-info">
<h4 class="alert-heading">
<wicket:message key="reflection_improvements_needed_heading">
Reflection improvements needed
</wicket:message>
</h4>
<p>
<wicket:message key="reflection_improvements_needed">
Your supervisor has requested that you improve and resubmit your reflection.
See their comments below about what changes are necessary.
</wicket:message>
</p>
<p wicket:id="supervisor_comment">
[You need to reflect more on the methods you used.]
</p>
</div>
<h4>
<wicket:message key="old_reflection">
Your previous reflection
</wicket:message>
</h4>
<p wicket:id="old_reflection"></p>
</wicket:enclosure>
<div class="mb-3">
<label class="form-label" wicket:for="reflection">
<wicket:message key="reflection">[Reflection]</wicket:message>

@ -24,6 +24,7 @@ import se.su.dsv.scipro.grading.PublicationMetadata;
import se.su.dsv.scipro.grading.PublicationMetadataFormComponentPanel;
import se.su.dsv.scipro.grading.PublicationMetadataService;
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.session.SciProSession;
@ -45,17 +46,20 @@ public class FinalStepsPanel extends GenericPanel<Project> {
add(new FencedFeedbackPanel("feedback", this));
IModel<String> reflection = LoadableDetachableModel.of(() ->
reflectionService.getSubmittedReflection(projectModel.getObject(), SciProSession.get().getUser()));
add(new MultiLineLabel("reflection", reflection) {
IModel<Reflection> currentReflection = LoadableDetachableModel.of(() ->
reflectionService.getReflection(projectModel.getObject(), SciProSession.get().getUser()));
IModel<String> reflectionText = currentReflection
.as(Reflection.Submitted.class)
.map(Reflection.Submitted::reflection);
add(new MultiLineLabel("reflection", reflectionText) {
@Override
protected void onConfigure() {
super.onConfigure();
setVisible(getDefaultModelObject() != null);
setVisible(!getDefaultModelObjectAsString().isBlank());
}
});
add(new FinalStepsForm("submit_reflection", projectModel));
add(new FinalStepsForm("submit_reflection", projectModel, currentReflection));
}
@Override
@ -67,22 +71,49 @@ public class FinalStepsPanel extends GenericPanel<Project> {
private class FinalStepsForm extends Form<Project> {
private final FinalThesisUploadComponent thesisFileUpload;
private final IModel<PublicationMetadata> publicationMetadataModel;
private final IModel<Reflection> currentReflection;
private IModel<String> reflectionModel;
private IModel<PublishingConsentService.Level> levelModel;
public FinalStepsForm(String id, IModel<Project> projectModel) {
public FinalStepsForm(String id, IModel<Project> projectModel, IModel<Reflection> currentReflection) {
super(id, projectModel);
this.currentReflection = currentReflection;
IModel<Reflection.ImprovementsNeeded> improvementsNeeded = this.currentReflection.as(Reflection.ImprovementsNeeded.class);
IModel<String> oldReflection = improvementsNeeded.map(Reflection.ImprovementsNeeded::oldReflection);
add(new MultiLineLabel("old_reflection", oldReflection) {
@Override
protected void onConfigure() {
super.onConfigure();
setVisibilityAllowed(!getDefaultModelObjectAsString().isBlank());
}
});
add(new MultiLineLabel("supervisor_comment", improvementsNeeded.map(Reflection.ImprovementsNeeded::commentBySupervisor)) {
@Override
protected void onConfigure() {
super.onConfigure();
setVisibilityAllowed(!getDefaultModelObjectAsString().isBlank());
}
});
reflectionModel = new Model<>();
TextArea<String> reflectionTextArea = new TextArea<>("reflection", reflectionModel);
TextArea<String> reflectionTextArea = new TextArea<>("reflection", reflectionModel) {
@Override
protected void onConfigure() {
super.onConfigure();
setVisible(FinalStepsForm.this.currentReflection.getObject().isSubmittable());
}
};
reflectionTextArea.setRequired(true);
add(reflectionTextArea);
IModel<PublishingConsentService.PublishingConsent> publishingConsent = LoadableDetachableModel.of(() ->
publishingConsentService.getPublishingConsent(getModelObject(), SciProSession.get().getUser()));
levelModel = new Model<>();
levelModel = new Model<>(publishingConsent.getObject().selected());
FormComponent<PublishingConsentService.Level> publishingConsentLevel = new BootstrapRadioChoice<>(
"publishingConsentLevel",
levelModel,
@ -111,7 +142,13 @@ public class FinalStepsPanel extends GenericPanel<Project> {
currentThesisFile.add(new OppositeVisibility(thesisFileUpload));
add(currentThesisFile);
publicationMetadataModel = LoadableDetachableModel.of(() -> publicationMetadataService.getByProject(getModelObject()));
WebMarkupContainer publicationMetadata = new WebMarkupContainer("publication_metadata");
WebMarkupContainer publicationMetadata = new WebMarkupContainer("publication_metadata") {
@Override
protected void onConfigure() {
super.onConfigure();
setEnabled(finalThesisService.isUploadAllowed(FinalStepsPanel.FinalStepsForm.this.getModelObject()));
}
};
add(publicationMetadata);
publicationMetadata.add(new PublicationMetadataFormComponentPanel("publication_metadata_components", publicationMetadataModel));
}
@ -119,7 +156,7 @@ public class FinalStepsPanel extends GenericPanel<Project> {
@Override
protected void onConfigure() {
super.onConfigure();
setVisibilityAllowed(reflectionService.hasToFillInReflection(getModelObject(), SciProSession.get().getUser()));
setVisibilityAllowed(currentReflection.getObject().isSubmittable());
}
@Override
@ -131,6 +168,7 @@ public class FinalStepsPanel extends GenericPanel<Project> {
new WicketProjectFileUpload(finalThesisUpload.fileUpload(), FinalStepsPanel.this.getModelObject()),
finalThesisUpload.englishTitle(),
finalThesisUpload.swedishTitle());
success(getString("final_thesis_uploaded"));
}
reflectionService.submitReflection(getModelObject(), SciProSession.get().getUser(), reflectionModel.getObject());
if (levelModel.getObject() != null) {

@ -9,3 +9,8 @@ current_final_thesis=Final thesis
publication_metadata_why=Please provide the following metadata.
englishTitle=English title
swedishTitle=Swedish title
reflection_improvements_needed_heading=Reflection improvements needed
reflection_improvements_needed=Your supervisor has requested that you improve and resubmit your reflection. \
See their comments below about what changes are necessary.
old_reflection=Your previous reflection
final_thesis_uploaded=Final thesis uploaded

@ -65,6 +65,8 @@ ProjectEvent.FIRST_MEETING = First meeting created. (with date, time, place/room
ProjectEvent.OPPOSITION_FAILED = An author fails their opposition.
ProjectEvent.PARTICIPATION_APPROVED = An author's active participation is approved.
ProjectEvent.PARTICIPATION_FAILED = An author fails their active participation.
ProjectEvent.REFLECTION_IMPROVEMENTS_REQUESTED = Reflection improvements requested.
ProjectEvent.REFLECTION_IMPROVEMENTS_SUBMITTED = Reflection improvements submitted.
ProjectForumEvent.NEW_FORUM_POST = Forum thread created.
ProjectForumEvent.NEW_FORUM_POST_COMMENT = Comment posted in forum thread.

@ -139,6 +139,7 @@ import java.util.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public abstract class SciProTest {
@ -390,6 +391,8 @@ public abstract class SciProTest {
publicationMetadata.setProject(answer.getArgument(0));
return publicationMetadata;
});
lenient().when(publishingConsentService.getPublishingConsent(any(), any()))
.thenReturn(new PublishingConsentService.PublishingConsent(null, List.of()));
}
@BeforeEach

@ -10,6 +10,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import se.su.dsv.scipro.SciProApplication;
import se.su.dsv.scipro.crosscutting.ForwardPhase2Feedback;
import se.su.dsv.scipro.crosscutting.NotifyFailedReflection;
import se.su.dsv.scipro.crosscutting.ReviewerAssignedNotifications;
import se.su.dsv.scipro.crosscutting.ReviewerSupportMailer;
import se.su.dsv.scipro.crosscutting.ReviewingNotifications;
@ -87,4 +88,13 @@ public class WicketConfiguration {
return new ReviewerAssignedNotifications(roughDraftApprovalService,
finalSeminarApprovalService, notificationController, eventBus);
}
// Not sure why this dependency lives in the view module
@Bean
public NotifyFailedReflection notifyFailedReflection(
EventBus eventBus,
NotificationController notificationController)
{
return new NotifyFailedReflection(notificationController, eventBus);
}
}