Merge remote-tracking branch 'origin/2934-reviewer-grading' into 1584-abstract-keywords

# Conflicts:
#	core/src/main/java/se/su/dsv/scipro/notifications/Notifications.java
#	core/src/main/java/se/su/dsv/scipro/report/GradingReportService.java
#	core/src/main/java/se/su/dsv/scipro/report/GradingReportServiceImpl.java
#	core/src/test/java/se/su/dsv/scipro/milestones/service/MilestoneActivatorTest.java
#	view/src/main/java/se/su/dsv/scipro/grading/FillOutGradingReportPanel.utf8.properties
#	view/src/main/java/se/su/dsv/scipro/reviewer/RoughDraftApprovalDecisionPage.java
#	view/src/main/resources/se/su/dsv/scipro/reviewer/RoughDraftApprovalDecisionPage.html
#	view/src/test/java/se/su/dsv/scipro/SciProTest.java
#	view/src/test/java/se/su/dsv/scipro/reviewer/ApprovalReviewerPanelTest.java
#	view/src/test/java/se/su/dsv/scipro/reviewer/RoughDraftApprovalDecisionPageTest.java
This commit is contained in:
Andreas Svanberg 2023-09-16 11:26:56 +02:00
commit 5b3a64ea6e
8 changed files with 262 additions and 23 deletions

@ -9,5 +9,5 @@ public record Grade(
Type type,
@JsonProperty("letter") String letter)
{
enum Type {PASSING, CREDITED, FAILING}
public enum Type {PASSING, CREDITED, FAILING}
}

@ -0,0 +1,37 @@
package se.su.dsv.scipro.reviewer;
import org.apache.wicket.model.IDetachable;
import org.apache.wicket.model.IModel;
import se.su.dsv.scipro.grading.Examination;
import se.su.dsv.scipro.system.User;
import java.util.HashMap;
import java.util.Map;
public class AuthorExaminations implements IDetachable {
private transient Map<User, Examination> selected = new HashMap<>();
@Override
public void detach() {
selected.clear();
}
public IModel<Examination> forAuthor(IModel<User> author) {
return new IModel<>() {
@Override
public Examination getObject() {
return selected.get(author.getObject());
}
@Override
public void setObject(Examination object) {
selected.put(author.getObject(), object);
}
};
}
public Examination forAuthor(User author) {
return selected.get(author);
}
}

@ -1,24 +1,37 @@
package se.su.dsv.scipro.reviewer;
import org.apache.wicket.RestartResponseException;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.markup.html.AjaxLink;
import org.apache.wicket.feedback.FencedFeedbackPanel;
import org.apache.wicket.markup.head.IHeaderResponse;
import org.apache.wicket.markup.head.OnDomReadyHeaderItem;
import org.apache.wicket.markup.head.OnEventHeaderItem;
import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.basic.EnumLabel;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.form.Button;
import org.apache.wicket.markup.html.form.DropDownChoice;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.LambdaChoiceRenderer;
import org.apache.wicket.markup.html.form.TextArea;
import org.apache.wicket.markup.html.form.upload.FileUploadField;
import org.apache.wicket.markup.html.list.ListItem;
import org.apache.wicket.markup.html.list.ListView;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.LoadableDetachableModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.model.ResourceModel;
import org.apache.wicket.request.flow.RedirectToUrlException;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import se.su.dsv.scipro.components.InfoPanel;
import se.su.dsv.scipro.files.WicketFileUpload;
import se.su.dsv.scipro.forum.panels.threaded.SubmitForumReplyPanel;
import se.su.dsv.scipro.grading.Examination;
import se.su.dsv.scipro.grading.Grade;
import se.su.dsv.scipro.grading.GradingService;
import se.su.dsv.scipro.oauth.OAuth;
import se.su.dsv.scipro.oauth.OAuthService;
import se.su.dsv.scipro.profile.UserLabel;
import se.su.dsv.scipro.project.Project;
import se.su.dsv.scipro.project.ProjectService;
import se.su.dsv.scipro.project.panels.ProjectHeaderPanel;
@ -28,8 +41,13 @@ import se.su.dsv.scipro.reviewing.ReviewerDecisionService;
import se.su.dsv.scipro.reviewing.ReviewerInteractionService;
import se.su.dsv.scipro.reviewing.RoughDraftApprovalService;
import se.su.dsv.scipro.supervisor.panels.FinalSeminarApprovalProcessPanel;
import se.su.dsv.scipro.system.User;
import javax.inject.Inject;
import java.time.Instant;
import java.time.LocalDate;
import java.util.*;
import java.util.function.Predicate;
public class RoughDraftApprovalDecisionPage extends ReviewerPage {
public static final String PAGE_PARAMETER_ID = "id";
@ -42,6 +60,10 @@ public class RoughDraftApprovalDecisionPage extends ReviewerPage {
private RoughDraftApprovalService roughDraftApprovalService;
@Inject
private ProjectService projectService;
@Inject
private OAuthService oAuthService;
@Inject
private GradingService gradingService;
protected final IModel<ReviewerApproval> approval;
@ -69,18 +91,45 @@ public class RoughDraftApprovalDecisionPage extends ReviewerPage {
add(new TimelinePanel("timeline", project));
}
@Override
protected void onConfigure() {
super.onConfigure();
checkGradeServiceAuthentication();
}
private void checkGradeServiceAuthentication() {
final Instant expiration = getSession().getMetaData(OAuth.EXPIRATION);
final boolean validToken = expiration != null && expiration.isAfter(Instant.now());
if (!validToken) {
final PageParameters pp = RoughDraftApprovalDecisionPage.pageParametersFor(approval.getObject().getProject());
final String returnUrl = "/" + getRequestCycle().mapUrlFor(RoughDraftApprovalDecisionPage.class, pp).canonical();
getSession().setMetaData(OAuth.RETURN_URL, returnUrl);
final String href = oAuthService.authorizeUrl(
null,
Set.of("grade:read", "grade:write"));
throw new RedirectToUrlException(href);
}
}
private class DecisionForm extends Form<ReviewerApproval> {
private final TextArea<String> feedback;
private final FileUploadField attachment;
public DecisionForm(String id, IModel<ReviewerApproval> reviewerApproval) {
super(id, reviewerApproval);
add(new FencedFeedbackPanel("feedbackPanel", this));
TextArea<String> feedback = new TextArea<>("feedback", Model.of(""));
feedback = new TextArea<>("feedback", Model.of(""));
feedback.setRequired(true);
add(feedback);
FileUploadField attachment = new FileUploadField("attachment");
attachment = new FileUploadField("attachment");
add(attachment);
add(new Button("reject") {
WebMarkupContainer decisionButtons = new WebMarkupContainer("decision_buttons");
decisionButtons.setOutputMarkupPlaceholderTag(true);
add(decisionButtons);
decisionButtons.add(new Button("reject") {
@Override
public void onSubmit() {
reviewerDecisionService.reject(
@ -89,13 +138,21 @@ public class RoughDraftApprovalDecisionPage extends ReviewerPage {
WicketFileUpload.ofOptional(attachment.getFileUpload()));
}
});
add(new Button("approve") {
WebMarkupContainer gradeAndApprove = new ApproveAndGrade("approve_and_grade");
gradeAndApprove.setVisible(false);
gradeAndApprove.setOutputMarkupPlaceholderTag(true);
add(gradeAndApprove);
decisionButtons.add(new AjaxLink<>("approve") {
@Override
public void onSubmit() {
reviewerDecisionService.approve(
DecisionForm.this.getModelObject(),
feedback.getModelObject(),
WicketFileUpload.ofOptional(attachment.getFileUpload()));
public void onClick(AjaxRequestTarget target) {
checkGradeServiceAuthentication();
gradeAndApprove.setVisible(true);
target.add(gradeAndApprove);
target.appendJavaScript("$('#%s').hide().slideDown()".formatted(gradeAndApprove.getMarkupId()));
decisionButtons.setVisible(false);
target.add(decisionButtons);
}
});
}
@ -105,13 +162,91 @@ public class RoughDraftApprovalDecisionPage extends ReviewerPage {
super.onConfigure();
setVisible(!getModelObject().isDecided());
}
private class ApproveAndGrade extends WebMarkupContainer {
private AuthorExaminations selectedExaminations = new AuthorExaminations();
public ApproveAndGrade(String id) {
super(id);
IModel<List<User>> authors = approval.map(ReviewerApproval::getProject)
.map(Project::getProjectParticipants)
.map(ArrayList::new);
ListView<User> listView = new ListView<>("authors", authors) {
@Override
protected void populateItem(ListItem<User> item) {
item.add(new UserLabel("name", item.getModel()));
IModel<List<Examination>> examinations = LoadableDetachableModel.of(() ->
getPassFailExaminations(item.getModel()));
DropDownChoice<Examination> examination = new DropDownChoice<>(
"examination",
selectedExaminations.forAuthor(item.getModel()),
examinations,
new LambdaChoiceRenderer<>(e -> e.name().english(), Examination::id));
examination.setLabel(item.getModel().map(User::getFullName));
examination.setRequired(true);
item.add(examination);
}
private List<Examination> getPassFailExaminations(IModel<User> author) {
return gradingService.getExaminations(
getSession().getMetaData(OAuth.TOKEN),
approval.getObject().getProject().getIdentifier(),
author.getObject().getIdentifier())
.stream()
.filter(Predicate.not(Examination::hasManyPassingGrades))
.toList();
}
};
listView.setReuseItems(true);
add(listView);
add(new Button("approve") {
@Override
public void onSubmit() {
super.onSubmit();
Project project = approval.getObject().getProject();
for (User author : project.getProjectParticipants()) {
Examination examination = selectedExaminations.forAuthor(author);
Grade passingGrade = getPassingGrade(examination);
gradingService.reportGrade(
getSession().getMetaData(OAuth.TOKEN),
project.getIdentifier(),
author.getIdentifier(),
examination.id(),
passingGrade.letter(),
LocalDate.now());
}
reviewerDecisionService.approve(
approval.getObject(),
feedback.getModelObject(),
WicketFileUpload.ofOptional(attachment.getFileUpload()));
}
private Grade getPassingGrade(Examination examination) {
Optional<Grade> passingGrade = examination.grades()
.stream()
.filter(g -> g.type() == Grade.Type.PASSING)
.findFirst();
assert passingGrade.isPresent();
return passingGrade.get();
}
});
}
@Override
protected void onDetach() {
super.onDetach();
selectedExaminations.detach();
}
}
}
@Override
public void renderHead(IHeaderResponse response) {
super.renderHead(response);
response.render(OnEventHeaderItem.forMarkupId("communication_toggle", "click", "$(this).hide(); $('#communication').show();"));
response.render(OnDomReadyHeaderItem.forScript("if ($('#attach_gr').is(':checked')) { $('#radp_grading_report').show(); }"));
}
public static PageParameters pageParametersFor(Project project) {

@ -4,3 +4,6 @@ info= Private communication between Reviewer and Supervisor.
interact.with.supervisor= Interact with supervisor
approve=Approve
reject=Improvements are needed
examination.Required=Examination for ${label} must be set
report_halfway_explanation=When approving a project for phase two review you should also report the halfway \
examination. Please select the appropriate examination below for each author before approving.

@ -0,0 +1,9 @@
reply.link= New message
feedback.Required= Feedback is required
info= Private communication between Reviewer and Supervisor.
interact.with.supervisor= Interact with supervisor
approve=Approve
reject=Improvements are needed
examination.Required=Examination for ${label} must be set
report_halfway_explanation=When approving a project for phase two review you should also report the halfway \
examination. Please select the appropriate examination below for each author before approving.

@ -20,19 +20,35 @@
<form wicket:id="decision">
<div wicket:id="feedbackPanel"></div>
<div class="mb-3">
<label for="feedback" wicket:for="feedback">Feedback</label>
<textarea class="form-control" id="feedback" wicket:id="feedback"></textarea>
<label for="feedback" class="form-label" wicket:for="feedback">Feedback</label>
<textarea required class="form-control" id="feedback" wicket:id="feedback"></textarea>
</div>
<div class="mb-3">
<label for="attachment" wicket:for="attachment">Attachment</label>
<label for="attachment" class="form-label" wicket:for="attachment">Attachment</label>
<input type="file" id="attachment" class="form-control" wicket:id="attachment">
</div>
<button type="submit" class="btn btn-success btn-sm" wicket:id="approve">
<wicket:message key="approve">Approve</wicket:message>
</button>
<button type="submit" class="btn btn-danger btn-sm" wicket:id="reject">
<wicket:message key="reject">Improvements are needed</wicket:message>
</button>
<div class="mb-3" wicket:id="decision_buttons">
<button type="submit" class="btn btn-outline-success btn-sm" wicket:id="approve">
<wicket:message key="approve">Approve</wicket:message>
</button>
<button type="submit" class="btn btn-danger btn-sm" wicket:id="reject">
<wicket:message key="reject">Improvements are needed</wicket:message>
</button>
</div>
<div wicket:id="approve_and_grade">
<p class="mb-3"><wicket:message key="report_halfway_explanation">
[When approving a project for phase two review you should also report the halfway examination.
Please select the appropriate examination below for each author before approving.]
</wicket:message></p>
<div class="mb-3" wicket:id="authors">
<label class="form-label" wicket:id="name">[Author name]</label>
<select class="form-select" wicket:id="examination"></select>
</div>
<button type="submit" class="btn btn-success btn-sm" wicket:id="approve">
Approve
</button>
</div>
</form>
</div>
<div class="col-12 col-xl-6">

@ -4,14 +4,18 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import se.su.dsv.scipro.SciProTest;
import se.su.dsv.scipro.file.FileReference;
import se.su.dsv.scipro.oauth.OAuth;
import se.su.dsv.scipro.project.Project;
import se.su.dsv.scipro.reviewing.MyReviewService;
import se.su.dsv.scipro.reviewing.RoughDraftApproval;
import se.su.dsv.scipro.system.DegreeType;
import se.su.dsv.scipro.system.Page;
import se.su.dsv.scipro.system.ProjectType;
import se.su.dsv.scipro.system.User;
import se.su.dsv.scipro.test.DomainObjects;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.util.*;
@ -33,6 +37,8 @@ public class ApprovalReviewerPanelTest extends SciProTest {
when(myReviewService.findDecision(any())).thenReturn(approval.getCurrentDecision());
when(roughDraftApprovalService.findBy(project)).thenReturn(Optional.of(approval));
setLoggedInAs(project.getReviewer());
tester.getSession().setMetaData(OAuth.TOKEN, "abc");
tester.getSession().setMetaData(OAuth.EXPIRATION, Instant.now().plus(Duration.ofHours(1)));
tester.startComponentInPage(RoughDraftApprovalReviewerPanel.class);
}

@ -8,6 +8,10 @@ import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import se.su.dsv.scipro.SciProTest;
import se.su.dsv.scipro.file.FileReference;
import se.su.dsv.scipro.grading.Examination;
import se.su.dsv.scipro.grading.Grade;
import se.su.dsv.scipro.grading.Name;
import se.su.dsv.scipro.oauth.OAuth;
import se.su.dsv.scipro.project.Project;
import se.su.dsv.scipro.project.panels.ProjectHeaderPanel;
import se.su.dsv.scipro.reviewing.RoughDraftApproval;
@ -18,11 +22,18 @@ import se.su.dsv.scipro.system.User;
import se.su.dsv.scipro.test.DomainObjects;
import java.net.URISyntaxException;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.util.*;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anySet;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ -39,6 +50,11 @@ public class RoughDraftApprovalDecisionPageTest extends SciProTest {
DomainObjects.injectId(roughDraftApproval, 123L);
when(projectService.findOne(project.getId())).thenReturn(project);
when(roughDraftApprovalService.findBy(project)).thenReturn(Optional.of(roughDraftApproval));
tester.getSession().setMetaData(OAuth.TOKEN, "abc");
tester.getSession().setMetaData(OAuth.EXPIRATION, Instant.now().plus(Duration.ofHours(1)));
Examination problemAndMethod = new Examination(614, new Name("Problem", "Problem"), "KX1P", List.of(new Grade(Grade.Type.PASSING, "P")));
lenient().when(gradingService.getExaminations(anyString(), anyLong(), anyLong()))
.thenReturn(List.of(problemAndMethod));
startPage();
}
@ -60,7 +76,7 @@ public class RoughDraftApprovalDecisionPageTest extends SciProTest {
FormTester formTester = tester.newFormTester("decision");
formTester.setValue("feedback", feedback);
formTester.submit("reject");
formTester.submit(path("decision_buttons", "reject"));
verify(reviewerDecisionService).reject(roughDraftApproval, feedback, Optional.empty());
}
@ -71,10 +87,13 @@ public class RoughDraftApprovalDecisionPageTest extends SciProTest {
String feedback = "This is good enough";
File attachment = testFile();
tester.executeAjaxEvent(path("decision", "decision_buttons", "approve"), "click");
FormTester formTester = tester.newFormTester("decision");
formTester.setValue("feedback", feedback);
formTester.setFile("attachment", attachment, "application/pdf");
formTester.submit("approve");
formTester.setValue(path("approve_and_grade", "authors", 0, "examination"), "614");
formTester.submit(path("approve_and_grade", "approve"));
tester.assertNoErrorMessage();
ArgumentCaptor<Optional> captor = ArgumentCaptor.forClass(Optional.class);
verify(reviewerDecisionService).approve(eq(roughDraftApproval), eq(feedback), captor.capture());
@ -96,14 +115,28 @@ public class RoughDraftApprovalDecisionPageTest extends SciProTest {
tester.assertInvisible("decision");
}
@Test
public void authorizes_with_grading_service() {
String authorizationUrl = "https://authorize";
when(oAuthService.authorizeUrl(any(), anySet())).thenReturn(authorizationUrl);
tester.getSession().setMetaData(OAuth.EXPIRATION, null);
startPage();
tester.assertRedirectUrl(authorizationUrl);
}
private Project createProject() {
ProjectType bachelor = new ProjectType(DegreeType.BACHELOR, "Bachelor", "Bachelor");
User supervisor = User.builder().firstName("Kalle").lastName("Tester").emailAddress("kalle@dsv.su.se").build();
User reviewer = User.builder().firstName("John").lastName("Doe").emailAddress("john@example.com").build();
User stina = User.builder().firstName("Stina").lastName("Student").emailAddress("stina@example.com").build();
stina.setId(987L);
stina.setIdentifier(523);
reviewer.setId(34L);
Project build = Project.builder().title("My project").projectType(bachelor).startDate(LocalDate.now()).headSupervisor(supervisor).build();
build.addReviewer(reviewer);
build.setId(87L);
build.setIdentifier(8798);
build.addProjectParticipant(stina);
return build;
}