Allow changes to the reflection to be made after it's been submitted #13

Merged
ansv7779 merged 20 commits from 3412-supervisor-edit-reflection into develop 2024-11-21 19:20:48 +01:00
26 changed files with 628 additions and 25 deletions

View File

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

View File

@ -23,7 +23,8 @@ public class ProjectEvent extends NotificationEvent {
ROUGH_DRAFT_APPROVAL_APPROVED, ROUGH_DRAFT_APPROVAL_REJECTED, REVIEWER_GRADING_REPORT_SUBMITTED, ROUGH_DRAFT_APPROVAL_APPROVED, ROUGH_DRAFT_APPROVAL_REJECTED, REVIEWER_GRADING_REPORT_SUBMITTED,
ONE_YEAR_PASSED_FROM_LATEST_ANNUAL_REVIEW, SUPERVISOR_GRADING_INITIAL_ASSESSMENT_DONE, ONE_YEAR_PASSED_FROM_LATEST_ANNUAL_REVIEW, SUPERVISOR_GRADING_INITIAL_ASSESSMENT_DONE,
EXPORTED_SUCCESS, REVIEWER_GRADING_INITIAL_ASSESSMENT_DONE, FIRST_MEETING, OPPOSITION_FAILED, 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
Review

Move the Event enum to its own file, it has grown to be very big.
That way we can separate the enums and also add comments as explanation for the different events.

Suggesting this to be done in a separate issue marked as technical debt.

Move the Event enum to its own file, it has grown to be very big. That way we can separate the enums and also add comments as explanation for the different events. Suggesting this to be done in a separate issue marked as technical debt.
} }
@Basic @Basic

View File

@ -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.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 \ 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. 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.title = Forum post: {2}
FORUM.NEW_FORUM_POST.body = New forum post submitted:<br /><br />{0} FORUM.NEW_FORUM_POST.body = New forum post submitted:<br /><br />{0}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,4 +14,15 @@ public interface ReflectionService {
* @return the reflection, or {@code null} if none has been submitted * @return the reflection, or {@code null} if none has been submitted
*/ */
String getSubmittedReflection(Project project, User author); 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);
} }

View File

@ -1,22 +1,32 @@
package se.su.dsv.scipro.reflection; package se.su.dsv.scipro.reflection;
import com.google.common.eventbus.EventBus;
import jakarta.transaction.Transactional; import jakarta.transaction.Transactional;
import se.su.dsv.scipro.finalseminar.AuthorRepository; import se.su.dsv.scipro.finalseminar.AuthorRepository;
import se.su.dsv.scipro.finalseminar.FinalSeminarService; import se.su.dsv.scipro.finalseminar.FinalSeminarService;
import se.su.dsv.scipro.project.Author; import se.su.dsv.scipro.project.Author;
import se.su.dsv.scipro.project.Project; import se.su.dsv.scipro.project.Project;
import se.su.dsv.scipro.project.ReflectionStatus;
import se.su.dsv.scipro.system.User; import se.su.dsv.scipro.system.User;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import java.util.Optional;
public class ReflectionServiceImpl implements ReflectionService { public class ReflectionServiceImpl implements ReflectionService {
private final AuthorRepository authorRepository; private final AuthorRepository authorRepository;
private final FinalSeminarService finalSeminarService; private final FinalSeminarService finalSeminarService;
private final EventBus eventBus;
@Inject @Inject
public ReflectionServiceImpl(AuthorRepository authorRepository, FinalSeminarService finalSeminarService) { public ReflectionServiceImpl(
AuthorRepository authorRepository,
FinalSeminarService finalSeminarService,
EventBus eventBus)
{
this.authorRepository = authorRepository; this.authorRepository = authorRepository;
this.finalSeminarService = finalSeminarService; this.finalSeminarService = finalSeminarService;
this.eventBus = eventBus;
} }
@Override @Override
@ -25,10 +35,13 @@ public class ReflectionServiceImpl implements ReflectionService {
} }
@Override @Override
public boolean hasToFillInReflection(Project project, User author) { public boolean hasToFillInReflection(Project project, User user) {
boolean noReflectionSubmitted = authorRepository.findByProjectAndUser(project, author) Optional<Author> optionalAuthor = authorRepository.findByProjectAndUser(project, user);
.map(Author::getReflection) if (optionalAuthor.isEmpty()) {
.isEmpty(); return false;
}
Author author = optionalAuthor.get();
boolean noReflectionSubmitted = author.getReflectionStatus() != ReflectionStatus.SUBMITTED;
return hasReachedReflectionProcess(project) && noReflectionSubmitted; return hasReachedReflectionProcess(project) && noReflectionSubmitted;
} }
@ -36,7 +49,13 @@ public class ReflectionServiceImpl implements ReflectionService {
@Transactional @Transactional
public void submitReflection(Project project, User user, String reflection) { public void submitReflection(Project project, User user, String reflection) {
authorRepository.findByProjectAndUser(project, user) 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 @Override
@ -45,4 +64,32 @@ public class ReflectionServiceImpl implements ReflectionService {
.map(Author::getReflection) .map(Author::getReflection)
.orElse(null); .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();
};
}
} }

View File

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

View File

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

View File

@ -101,6 +101,26 @@ public class ReflectionServiceTest extends IntegrationTest {
assertTrue(reflectionService.hasReachedReflectionProcess(project)); 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() { private LocalDate scheduleSeminar() {
project.setFinalSeminarRuleExempted(true); // to bypass rough draft approval project.setFinalSeminarRuleExempted(true); // to bypass rough draft approval
FinalSeminarDetails details = new FinalSeminarDetails("Zoom", false, 1, 1, Language.SWEDISH, Language.ENGLISH, "zoom id 123"); FinalSeminarDetails details = new FinalSeminarDetails("Zoom", false, 1, 1, Language.SWEDISH, Language.ENGLISH, "zoom id 123");

View File

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

View File

@ -49,10 +49,16 @@
</div> </div>
</wicket:fragment> </wicket:fragment>
</div> </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> <a wicket:id="showReflection" href="#">View reflection</a>
<div wicket:id="modal"></div> <div wicket:id="modal"></div>
</wicket:container> </div>
</fieldset> </fieldset>
</wicket:panel> </wicket:panel>
</body> </body>

View File

@ -30,10 +30,12 @@ import se.su.dsv.scipro.finalseminar.FinalSeminar;
import se.su.dsv.scipro.finalseminar.FinalSeminarOpposition; import se.su.dsv.scipro.finalseminar.FinalSeminarOpposition;
import se.su.dsv.scipro.finalseminar.FinalSeminarService; import se.su.dsv.scipro.finalseminar.FinalSeminarService;
import se.su.dsv.scipro.project.Project; 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.reflection.ReflectionService;
import se.su.dsv.scipro.report.AbstractGradingCriterion; import se.su.dsv.scipro.report.AbstractGradingCriterion;
import se.su.dsv.scipro.report.GradingCriterion; import se.su.dsv.scipro.report.GradingCriterion;
import se.su.dsv.scipro.report.GradingCriterionPoint; 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.report.SupervisorGradingReport;
import se.su.dsv.scipro.system.Language; import se.su.dsv.scipro.system.Language;
import se.su.dsv.scipro.system.User; import se.su.dsv.scipro.system.User;
@ -264,14 +266,33 @@ public class CriteriaPanel extends GenericPanel<SupervisorGradingReport> {
super(id, author); super(id, author);
this.gradingCriterion = gradingCriterion; this.gradingCriterion = gradingCriterion;
IModel<String> reflection = LoadableDetachableModel.of(() -> { IModel<Reflection> reflection = LoadableDetachableModel.of(() -> {
Project project = CriteriaPanel.this.getModelObject().getProject(); 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 = new LargeModalWindow("modal");
modal.setTitle("Reflection"); 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); add(modal);
WebMarkupContainer showReflection = new WebMarkupContainer("showReflection") { WebMarkupContainer showReflection = new WebMarkupContainer("showReflection") {

View File

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

View File

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

View File

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

View File

@ -158,7 +158,7 @@ public class NotificationLandingPage extends WebPage {
setResponsePage(RoughDraftApprovalDecisionPage.class, reviewerParameters); setResponsePage(RoughDraftApprovalDecisionPage.class, reviewerParameters);
} }
break; 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)) { if (project.isSupervisor(currentUser)) {
setResponsePage(SupervisorGradingReportPage.class, pp); setResponsePage(SupervisorGradingReportPage.class, pp);
} }

View File

@ -38,6 +38,30 @@
<div wicket:id="current_thesis_file"></div> <div wicket:id="current_thesis_file"></div>
</div> </div>
</wicket:enclosure> </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"> <div class="mb-3">
<label class="form-label" wicket:for="reflection"> <label class="form-label" wicket:for="reflection">
<wicket:message key="reflection">[Reflection]</wicket:message> <wicket:message key="reflection">[Reflection]</wicket:message>

View File

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

View File

@ -9,3 +9,8 @@ current_final_thesis=Final thesis
publication_metadata_why=Please provide the following metadata. publication_metadata_why=Please provide the following metadata.
englishTitle=English title englishTitle=English title
swedishTitle=Swedish 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

View File

@ -65,6 +65,8 @@ ProjectEvent.FIRST_MEETING = First meeting created. (with date, time, place/room
ProjectEvent.OPPOSITION_FAILED = An author fails their opposition. ProjectEvent.OPPOSITION_FAILED = An author fails their opposition.
ProjectEvent.PARTICIPATION_APPROVED = An author's active participation is approved. ProjectEvent.PARTICIPATION_APPROVED = An author's active participation is approved.
ProjectEvent.PARTICIPATION_FAILED = An author fails their active participation. 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 = Forum thread created.
ProjectForumEvent.NEW_FORUM_POST_COMMENT = Comment posted in forum thread. ProjectForumEvent.NEW_FORUM_POST_COMMENT = Comment posted in forum thread.

View File

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

View File

@ -10,6 +10,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import se.su.dsv.scipro.SciProApplication; import se.su.dsv.scipro.SciProApplication;
import se.su.dsv.scipro.crosscutting.ForwardPhase2Feedback; 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.ReviewerAssignedNotifications;
import se.su.dsv.scipro.crosscutting.ReviewerSupportMailer; import se.su.dsv.scipro.crosscutting.ReviewerSupportMailer;
import se.su.dsv.scipro.crosscutting.ReviewingNotifications; import se.su.dsv.scipro.crosscutting.ReviewingNotifications;
@ -87,4 +88,13 @@ public class WicketConfiguration {
return new ReviewerAssignedNotifications(roughDraftApprovalService, return new ReviewerAssignedNotifications(roughDraftApprovalService,
finalSeminarApprovalService, notificationController, eventBus); 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);
}
} }