Allow authors to re-submit their reflection on supervisor's request #12

Closed
ansv7779 wants to merge 8 commits from 3213-reflection-resubmission into develop
21 changed files with 420 additions and 17 deletions

View File

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

View File

@ -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:<br /><br />{0}

View File

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

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

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

View File

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

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

View File

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

View File

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

View File

@ -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<SupervisorGradingReport> {
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);
});
add(modal);
WebMarkupContainer showReflection = new WebMarkupContainer("showReflection") {

View File

@ -0,0 +1,45 @@
<?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 class="mb-0" 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>
<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>
</form>
</wicket:panel>
</body>
</html>

View File

@ -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<Project> projectModel;
private final IModel<User> authorModel;
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)));
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<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());
target.add(ReflectionModalBodyPanel.this);
}
});
}
@Override
protected void onConfigure() {
super.onConfigure();
setVisible(getModelObject() instanceof Reflection.Submitted);
}
}
}

View File

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

View File

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

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.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,15 +71,42 @@ 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> i = this.currentReflection.as(Reflection.ImprovementsNeeded.class);
IModel<String> oldReflection = i.map(Reflection.ImprovementsNeeded::oldReflection);
add(new MultiLineLabel("old_reflection", oldReflection) {
@Override
protected void onConfigure() {
super.onConfigure();
setVisibilityAllowed(!getDefaultModelObjectAsString().isBlank());
}
});
add(new MultiLineLabel("supervisor_comment", i.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);
@ -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

View File

@ -9,3 +9,7 @@ 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

View File

@ -65,6 +65,7 @@ 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.
ProjectForumEvent.NEW_FORUM_POST = Forum thread created.
ProjectForumEvent.NEW_FORUM_POST_COMMENT = Comment posted in forum thread.