Enable creating an API using Spring Web #5

Merged
niat8586 merged 39 commits from spring into develop 2024-11-06 11:23:29 +01:00
21 changed files with 371 additions and 24 deletions
Showing only changes of commit 89c83ccbd6 - Show all commits

View File

@ -7,7 +7,7 @@ import java.time.LocalDate;
import java.util.List; import java.util.List;
public interface DecisionRepository { public interface DecisionRepository {
int countDecisions(User reviewer, LocalDate fromDate, LocalDate toDate); int countUniqueProjectsWithDecision(User reviewer, LocalDate fromDate, LocalDate toDate);
List<Decision> findBy(Project project); List<Decision> findBy(Project project);
} }

View File

@ -17,11 +17,13 @@ public class DecisionRepositoryImpl extends AbstractRepository implements Decisi
} }
@Override @Override
public int countDecisions(User reviewer, LocalDate fromDate, LocalDate toDate) { public int countUniqueProjectsWithDecision(User reviewer, LocalDate fromDate, LocalDate toDate) {
return (int) from(QDecision.decision) return (int) from(QDecision.decision)
.select(QDecision.decision.reviewerApproval.id)
.where(QDecision.decision.assignedReviewer.eq(reviewer) .where(QDecision.decision.assignedReviewer.eq(reviewer)
.and(QDecision.decision.reviewerAssignedAt.goe(fromDate)) .and(QDecision.decision.reviewerAssignedAt.goe(fromDate))
.and(QDecision.decision.reviewerAssignedAt.loe(toDate))) .and(QDecision.decision.reviewerAssignedAt.loe(toDate)))
.distinct()
.fetchCount(); .fetchCount();
} }

View File

@ -1,4 +1,4 @@
package se.su.dsv.scipro.crosscutting; package se.su.dsv.scipro.reviewing;
import com.google.common.eventbus.EventBus; import com.google.common.eventbus.EventBus;
import com.google.common.eventbus.Subscribe; import com.google.common.eventbus.Subscribe;

View File

@ -105,10 +105,8 @@ public class ReviewerCapacityServiceImpl implements ReviewerCapacityService, Rev
.limit(3) // get three years .limit(3) // get three years
.map(historicYear -> { .map(historicYear -> {
Optional<ReviewerTarget> reviewerTarget = reviewerTargetRepository.getReviewerTarget(reviewer, historicYear); Optional<ReviewerTarget> reviewerTarget = reviewerTargetRepository.getReviewerTarget(reviewer, historicYear);
int completedInSpring = decisionRepository.countDecisions(reviewer, int completedInSpring = countSpringReviews(reviewer, historicYear);
startOfSpring(historicYear.getValue()), endOfSpring(historicYear.getValue())); int completedInAutumn = countAutumnReviews(reviewer, historicYear);
int completedInAutumn = decisionRepository.countDecisions(reviewer,
startOfAutumn(historicYear.getValue()), endOfAutumn(historicYear.getValue()));
return new TargetHistory( return new TargetHistory(
historicYear, historicYear,
reviewerTarget.map(ReviewerTarget::getSpring).orElse(0), reviewerTarget.map(ReviewerTarget::getSpring).orElse(0),
@ -123,11 +121,23 @@ public class ReviewerCapacityServiceImpl implements ReviewerCapacityService, Rev
@Override @Override
public RemainingTargets getRemainingTargets(User reviewer, Year year) { public RemainingTargets getRemainingTargets(User reviewer, Year year) {
Target target = getTarget(reviewer, year); Target target = getTarget(reviewer, year);
int springDecisions = decisionRepository.countDecisions(reviewer, startOfSpring(year.getValue()), endOfSpring(year.getValue())); int springDecisions = countSpringReviews(reviewer, year);
int autumnReviews = decisionRepository.countDecisions(reviewer, startOfAutumn(year.getValue()), endOfAutumn(year.getValue())); int autumnReviews = countAutumnReviews(reviewer, year);
return new RemainingTargets(target.spring() - springDecisions, target.autumn() - autumnReviews); return new RemainingTargets(target.spring() - springDecisions, target.autumn() - autumnReviews);
} }
private int countSpringReviews(User reviewer, Year year) {
return countReviews(reviewer, startOfSpring(year.getValue()), endOfSpring(year.getValue()));
}
private int countAutumnReviews(User reviewer, Year year) {
return countReviews(reviewer, startOfAutumn(year.getValue()), endOfAutumn(year.getValue()));
}
private int countReviews(User reviewer, LocalDate fromDate, LocalDate toDate) {
return decisionRepository.countUniqueProjectsWithDecision(reviewer, fromDate, toDate);
}
private Optional<ReviewerTarget> getTarget(User reviewer, LocalDate date) { private Optional<ReviewerTarget> getTarget(User reviewer, LocalDate date) {
return reviewerTargetRepository.getReviewerTarget(reviewer, Year.from(date)); return reviewerTargetRepository.getReviewerTarget(reviewer, Year.from(date));
} }
@ -242,12 +252,10 @@ public class ReviewerCapacityServiceImpl implements ReviewerCapacityService, Rev
private int countAssignedReviews(User reviewer, LocalDate fromDate) { private int countAssignedReviews(User reviewer, LocalDate fromDate) {
if (fromDate.getMonthValue() <= Month.JUNE.getValue()) { if (fromDate.getMonthValue() <= Month.JUNE.getValue()) {
return decisionRepository.countDecisions(reviewer, return countReviews(reviewer, startOfSpring(fromDate.getYear()), endOfSpring(fromDate.getYear()));
startOfSpring(fromDate.getYear()), endOfSpring(fromDate.getYear()));
} }
else { else {
return decisionRepository.countDecisions(reviewer, return countReviews(reviewer, startOfAutumn(fromDate.getYear()), endOfAutumn(fromDate.getYear()));
startOfAutumn(fromDate.getYear()), endOfAutumn(fromDate.getYear()));
} }
} }

View File

@ -59,6 +59,10 @@ public class ProjectTypeSettings extends DomainObject {
@Basic(optional = false) @Basic(optional = false)
private int numDaysBeforePeerGetsCancelled = DEFAULT_NUM_DAYS_BEFORE_CANCELLED_PEERS; private int numDaysBeforePeerGetsCancelled = DEFAULT_NUM_DAYS_BEFORE_CANCELLED_PEERS;
@Basic
@Column(name = "review_process_information_url_for_supervisor")
private String reviewProcessInformationUrl;
@Override @Override
public Long getId() { public Long getId() {
return this.id; return this.id;
@ -164,6 +168,14 @@ public class ProjectTypeSettings extends DomainObject {
this.minimumActiveParticipationsToBeGraded = minimumActiveParticipationsToBeGraded; this.minimumActiveParticipationsToBeGraded = minimumActiveParticipationsToBeGraded;
} }
public String getReviewProcessInformationUrl() {
return reviewProcessInformationUrl;
}
public void setReviewProcessInformationUrl(String reviewProcessInformationUrl) {
this.reviewProcessInformationUrl = reviewProcessInformationUrl;
}
@Override @Override
public String toString() { public String toString() {
return "ProjectTypeSettings(id=" + this.getId() + ", projectType=" + this.getProjectType() + ", minAuthors=" + this.getMinAuthors() + ", maxAuthors=" + this.getMaxAuthors() + ", maxFinalSeminarActiveParticipation=" + this.getMaxFinalSeminarActiveParticipation() + ", maxOpponentsOnFinalSeminar=" + this.getMaxOpponentsOnFinalSeminar() + ", minFinalSeminarActiveParticipation=" + this.getMinFinalSeminarActiveParticipation() + ", minOpponentsOnFinalSeminar=" + this.getMinOpponentsOnFinalSeminar() + ", numDaysBetweenPeerReviewsOnSameProject=" + this.getNumDaysBetweenPeerReviewsOnSameProject() + ", numDaysToSubmitPeerReview=" + this.getNumDaysToSubmitPeerReview() + ", numDaysBeforePeerGetsCancelled=" + this.getNumDaysBeforePeerGetsCancelled() + ")"; return "ProjectTypeSettings(id=" + this.getId() + ", projectType=" + this.getProjectType() + ", minAuthors=" + this.getMinAuthors() + ", maxAuthors=" + this.getMaxAuthors() + ", maxFinalSeminarActiveParticipation=" + this.getMaxFinalSeminarActiveParticipation() + ", maxOpponentsOnFinalSeminar=" + this.getMaxOpponentsOnFinalSeminar() + ", minFinalSeminarActiveParticipation=" + this.getMinFinalSeminarActiveParticipation() + ", minOpponentsOnFinalSeminar=" + this.getMinOpponentsOnFinalSeminar() + ", numDaysBetweenPeerReviewsOnSameProject=" + this.getNumDaysBetweenPeerReviewsOnSameProject() + ", numDaysToSubmitPeerReview=" + this.getNumDaysToSubmitPeerReview() + ", numDaysBeforePeerGetsCancelled=" + this.getNumDaysBeforePeerGetsCancelled() + ")";

View File

@ -0,0 +1,2 @@
ALTER TABLE `project_type_settings`
ADD COLUMN `review_process_information_url_for_supervisor` VARCHAR(255) NULL;

View File

@ -0,0 +1,127 @@
package se.su.dsv.scipro.reviewing;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;
import se.su.dsv.scipro.file.FileUpload;
import se.su.dsv.scipro.project.Project;
import se.su.dsv.scipro.reviewing.ReviewerAssignmentService.ReviewerAssignment;
import se.su.dsv.scipro.security.auth.roles.Roles;
import se.su.dsv.scipro.system.DegreeType;
import se.su.dsv.scipro.system.ProjectType;
import se.su.dsv.scipro.system.User;
import se.su.dsv.scipro.test.MutableFixedClock;
import se.su.dsv.scipro.util.Either;
import java.io.InputStream;
import java.time.LocalDate;
import java.time.Month;
import java.time.Year;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class ReviewerTest extends ReviewingModuleTest {
@Inject
MutableFixedClock clock;
@Inject
ReviewerAssignmentService reviewerAssignmentService;
@Inject
ReviewerCapacityService reviewerCapacityService;
@Inject
RoughDraftApprovalService roughDraftApprovalService;
@Inject
ReviewerDecisionService reviewerDecisionService;
@Test
public void an_assigned_reviewer_should_only_consume_one_target_per_project() {
// Given
clock.setDate(LocalDate.of(2024, Month.MAY, 22)); // some date in spring
Project project = createProject();
User reviewer = createUser();
var target = new ReviewerCapacityService.Target(Year.now(clock), 2, 0, "Can review at any time");
// When
reviewerCapacityService.assignTarget(reviewer, target);
Either<AlreadyRequested, RoughDraftApproval> firstReviewRequest = roughDraftApprovalService.requestApproval(
project,
dummyFile(),
"Some comment");
assertTrue(firstReviewRequest.isRight());
ReviewerAssignment assignment = reviewerAssignmentService.assignReviewer(project,reviewer);
assertEquals(ReviewerAssignment.OK, assignment);
reviewerDecisionService.reject(firstReviewRequest.right(), "Not good enough", Optional.empty());
Either<AlreadyRequested, RoughDraftApproval> secondReviewRequest = roughDraftApprovalService.requestApproval(
project,
dummyFile(),
"Some new comment");
assertTrue(secondReviewRequest.isRight());
// Then
ReviewerCapacityService.RemainingTargets remainingTargets =
reviewerCapacityService.getRemainingTargets(reviewer, target.year());
assertEquals(1, remainingTargets.spring());
}
private User createUser() {
User user = User.builder()
.firstName("Edward")
.lastName("Employee")
.emailAddress("stuart@example.com")
.roles(Set.of(Roles.REVIEWER, Roles.SUPERVISOR))
.build();
return save(user);
}
private Project createProject() {
ProjectType bachelor = new ProjectType(DegreeType.BACHELOR, "Bachelor", "Bachelor");
save(bachelor);
User supervisor = createUser();
Project project = Project.builder()
.title("A project")
.projectType(bachelor)
.startDate(LocalDate.now(clock).minusMonths(1))
.headSupervisor(supervisor)
.build();
return save(project);
}
private FileUpload dummyFile() {
return new FileUpload() {
@Override
public String getFileName() {
return "dummy.tmp";
}
@Override
public String getContentType() {
return "text/plain";
}
@Override
public User getUploader() {
return null;
}
@Override
public long getSize() {
return 0;
}
@Override
public <T> T handleData(Function<InputStream, T> handler) {
return handler.apply(InputStream.nullInputStream());
}
};
}
}

View File

@ -1,7 +1,18 @@
package se.su.dsv.scipro.reviewing; package se.su.dsv.scipro.reviewing;
import com.google.inject.AbstractModule;
import com.google.inject.Module;
import se.su.dsv.scipro.test.IntegrationTest; import se.su.dsv.scipro.test.IntegrationTest;
public abstract class ReviewingModuleTest extends IntegrationTest { public abstract class ReviewingModuleTest extends IntegrationTest {
@Override
protected Module moduleUnderTest() {
return new AbstractModule() {
@Override
protected void configure() {
install(ReviewingModuleTest.super.moduleUnderTest());
bind(ReviewerAssignedDeadline.class).asEagerSingleton();
}
};
}
} }

View File

@ -17,7 +17,12 @@ import org.springframework.context.annotation.FilterType;
import se.su.dsv.scipro.forum.AbstractThreadRepositoryImpl; import se.su.dsv.scipro.forum.AbstractThreadRepositoryImpl;
import se.su.dsv.scipro.grading.GradingHistory; import se.su.dsv.scipro.grading.GradingHistory;
import se.su.dsv.scipro.grading.GradingHistoryEventRepository; import se.su.dsv.scipro.grading.GradingHistoryEventRepository;
import se.su.dsv.scipro.misc.DaysService;
import se.su.dsv.scipro.oauth.OAuthSettings; import se.su.dsv.scipro.oauth.OAuthSettings;
import se.su.dsv.scipro.reviewing.FinalSeminarApprovalService;
import se.su.dsv.scipro.reviewing.ReviewerAssignedDeadline;
import se.su.dsv.scipro.reviewing.ReviewerDeadlineSettingsService;
import se.su.dsv.scipro.reviewing.RoughDraftApprovalService;
import se.su.dsv.scipro.security.auth.LocalAuthentication; import se.su.dsv.scipro.security.auth.LocalAuthentication;
import se.su.dsv.scipro.sukat.Sukat; import se.su.dsv.scipro.sukat.Sukat;
import se.su.dsv.scipro.system.CurrentUser; import se.su.dsv.scipro.system.CurrentUser;
@ -127,5 +132,17 @@ public abstract class SpringTest {
public LocalAuthentication localAuthentication(UserService userService, PasswordService passwordService) { public LocalAuthentication localAuthentication(UserService userService, PasswordService passwordService) {
return new LocalAuthentication(userService, passwordService); return new LocalAuthentication(userService, passwordService);
} }
@Bean
public ReviewerAssignedDeadline reviewerAssignedDeadline(
RoughDraftApprovalService roughDraftApprovalService,
FinalSeminarApprovalService finalSeminarApprovalService,
ReviewerDeadlineSettingsService reviewerDeadlineSettingsService,
DaysService daysService,
EventBus eventBus, Clock clock)
{
return new ReviewerAssignedDeadline(roughDraftApprovalService, finalSeminarApprovalService,
reviewerDeadlineSettingsService, daysService, eventBus, clock);
}
} }
} }

View File

@ -22,6 +22,18 @@
<label wicket:for="description">Description</label> <label wicket:for="description">Description</label>
<textarea class="form-control" wicket:id="description" cols="60" rows="5"></textarea> <textarea class="form-control" wicket:id="description" cols="60" rows="5"></textarea>
</div> </div>
<div class="mb-3">
<label wicket:for="minimum_authors">
Minimum number of authors per student idea
</label>
<input class="form-control" type="number" wicket:id="minimum_authors">
</div>
<div class="mb-3">
<label wicket:for="minimum_authors">
Maximum number of authors per student idea
</label>
<input class="form-control" type="number" wicket:id="maximum_authors">
</div>
<div class="mb-3"> <div class="mb-3">
<label wicket:for="minimum_oppositions_to_be_graded"> <label wicket:for="minimum_oppositions_to_be_graded">
Minimum approved final seminar oppositions to be graded Minimum approved final seminar oppositions to be graded
@ -34,6 +46,15 @@
</label> </label>
<input class="form-control" type="number" wicket:id="minimum_active_participations_to_be_graded"> <input class="form-control" type="number" wicket:id="minimum_active_participations_to_be_graded">
</div> </div>
<div class="mb-3">
<label wicket:for="review_process_information_url">
URL with information about the review process
</label>
<input class="form-control" type="url" wicket:id="review_process_information_url">
<small class="text-muted">
This link is display for supervisors when they are in the review process.
</small>
</div>
<input class="btn btn-sm btn-success" wicket:id="createButton" type="submit" value="Save" /> <input class="btn btn-sm btn-success" wicket:id="createButton" type="submit" value="Save" />
<input class="btn btn-sm btn-danger" wicket:id="deleteButton" type="submit" value="Inactivate" /> <input class="btn btn-sm btn-danger" wicket:id="deleteButton" type="submit" value="Inactivate" />
</form> </form>

View File

@ -68,6 +68,30 @@ public class AdminProjectTypePanel extends Panel {
minimumActiveParticipationsToBeGraded.setMinimum(0); minimumActiveParticipationsToBeGraded.setMinimum(0);
add(minimumActiveParticipationsToBeGraded); add(minimumActiveParticipationsToBeGraded);
NumberTextField<Integer> minimumAuthors = new NumberTextField<>(
"minimum_authors",
LambdaModel.of(settings, ProjectTypeSettings::getMinAuthors, ProjectTypeSettings::setMinAuthors),
Integer.class);
minimumAuthors.setMinimum(1);
minimumAuthors.setRequired(true);
add(minimumAuthors);
NumberTextField<Integer> maximumAuthors = new NumberTextField<>(
"maximum_authors",
LambdaModel.of(settings, ProjectTypeSettings::getMaxAuthors, ProjectTypeSettings::setMaxAuthors),
Integer.class);
maximumAuthors.setMinimum(1);
maximumAuthors.setRequired(true);
add(maximumAuthors);
TextField<String> reviewProcessInformationUrl = new UrlTextField(
"review_process_information_url",
LambdaModel.of(
settings,
ProjectTypeSettings::getReviewProcessInformationUrl,
ProjectTypeSettings::setReviewProcessInformationUrl));
add(reviewProcessInformationUrl);
Button createButton = new Button("createButton") { Button createButton = new Button("createButton") {
@Override @Override
public void onSubmit() { public void onSubmit() {

View File

@ -27,7 +27,7 @@ public class SmarterLinkMultiLineLabel extends SmartLinkMultiLineLabel {
* with the addition of ";" to the accepted characters in the URL. * with the addition of ";" to the accepted characters in the URL.
* This enables proper linking of escaped URLs. * This enables proper linking of escaped URLs.
*/ */
private static final String urlPattern = "([a-zA-Z]+://[\\w\\.\\-\\:\\/~]+)[\\w\\.:\\-/?&=%;]*"; private static final String urlPattern = "([a-zA-Z]+://[\\w\\.\\-\\:\\/~]+)[\\w\\.:\\-/?!&=%;]*";
private EscapedLinkParser() { private EscapedLinkParser() {
addLinkRenderStrategy(emailPattern, DefaultLinkParser.EMAIL_RENDER_STRATEGY); addLinkRenderStrategy(emailPattern, DefaultLinkParser.EMAIL_RENDER_STRATEGY);

View File

@ -100,9 +100,7 @@ public class SendToExaminer extends GenericPanel<Project> {
add(form); add(form);
WebMarkupContainer sendButton = new WebMarkupContainer("send"); WebMarkupContainer sendButton = new WebMarkupContainer("send");
if (confirmationMessage.getObject() != null) {
sendButton.add(new JavascriptEventConfirmation("click", confirmationMessage)); sendButton.add(new JavascriptEventConfirmation("click", confirmationMessage));
}
form.add(sendButton); form.add(sendButton);
TextField<LocalDate> examinationDateField = new TextField<>("examinationDate", examinationDate, LocalDate.class); TextField<LocalDate> examinationDateField = new TextField<>("examinationDate", examinationDate, LocalDate.class);

View File

@ -11,6 +11,7 @@ import org.apache.wicket.markup.html.form.Button;
import org.apache.wicket.markup.html.form.Form; import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.form.TextArea; import org.apache.wicket.markup.html.form.TextArea;
import org.apache.wicket.markup.html.form.upload.FileUploadField; import org.apache.wicket.markup.html.form.upload.FileUploadField;
import org.apache.wicket.markup.html.link.ExternalLink;
import org.apache.wicket.markup.html.list.ListItem; import org.apache.wicket.markup.html.list.ListItem;
import org.apache.wicket.markup.html.list.ListView; import org.apache.wicket.markup.html.list.ListView;
import org.apache.wicket.model.IModel; import org.apache.wicket.model.IModel;
@ -36,6 +37,8 @@ import se.su.dsv.scipro.reviewing.ReviewerDecisionService;
import se.su.dsv.scipro.reviewing.ReviewerInteractionService; import se.su.dsv.scipro.reviewing.ReviewerInteractionService;
import se.su.dsv.scipro.reviewing.RoughDraftApprovalService; import se.su.dsv.scipro.reviewing.RoughDraftApprovalService;
import se.su.dsv.scipro.supervisor.panels.FinalSeminarApprovalProcessPanel; import se.su.dsv.scipro.supervisor.panels.FinalSeminarApprovalProcessPanel;
import se.su.dsv.scipro.system.ProjectType;
import se.su.dsv.scipro.system.ProjectTypeSettings;
import se.su.dsv.scipro.system.User; import se.su.dsv.scipro.system.User;
import se.su.dsv.scipro.util.Either; import se.su.dsv.scipro.util.Either;
@ -92,6 +95,17 @@ public class RoughDraftApprovalDecisionPage extends ReviewerPage {
add(new EnumLabel<>("title", approval.map(ReviewerApproval::getStep))); add(new EnumLabel<>("title", approval.map(ReviewerApproval::getStep)));
add(new TimelinePanel("timeline", project)); add(new TimelinePanel("timeline", project));
add(new FencedFeedbackPanel("feedback", this)); add(new FencedFeedbackPanel("feedback", this));
IModel<String> moreInformationUrl = project
.map(Project::getProjectType)
.map(ProjectType::getProjectTypeSettings)
.map(ProjectTypeSettings::getReviewProcessInformationUrl);
add(new ExternalLink("review_process_information", moreInformationUrl) {
@Override
protected void onConfigure() {
super.onConfigure();
setVisible(moreInformationUrl.getObject() != null);
}
});
} }
@Override @Override

View File

@ -10,7 +10,16 @@
<div class="col-12 col-xl-3 col-lg-6"> <div class="col-12 col-xl-3 col-lg-6">
<div class="card bg-light"> <div class="card bg-light">
<h4 class="card-header">Rough draft approval</h4> <h4 class="card-header">Rough draft approval</h4>
<div class="card-body" wicket:id="roughDraftApproval"></div> <div class="card-body">
<wicket:enclosure>
<p>
<a href="https://example.com" wicket:id="review_process_information">
See more information about the review process and any important dates.
</a>
</p>
</wicket:enclosure>
<div wicket:id="roughDraftApproval"></div>
</div>
</div> </div>
</div> </div>
<div class="col-12 col-xl-6 col-lg-6"> <div class="col-12 col-xl-6 col-lg-6">

View File

@ -3,6 +3,8 @@ package se.su.dsv.scipro.supervisor.pages;
import org.apache.wicket.markup.head.IHeaderResponse; import org.apache.wicket.markup.head.IHeaderResponse;
import org.apache.wicket.markup.head.OnEventHeaderItem; import org.apache.wicket.markup.head.OnEventHeaderItem;
import org.apache.wicket.markup.html.WebMarkupContainer; import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.link.ExternalLink;
import org.apache.wicket.model.IModel;
import org.apache.wicket.request.mapper.parameter.PageParameters; import org.apache.wicket.request.mapper.parameter.PageParameters;
import se.su.dsv.scipro.components.menuhighlighting.MenuHighlightSupervisorMyProjects; import se.su.dsv.scipro.components.menuhighlighting.MenuHighlightSupervisorMyProjects;
import se.su.dsv.scipro.file.FileService; import se.su.dsv.scipro.file.FileService;
@ -18,6 +20,8 @@ import se.su.dsv.scipro.security.auth.roles.Roles;
import se.su.dsv.scipro.supervisor.panels.RoughDraftApprovalPanel; import se.su.dsv.scipro.supervisor.panels.RoughDraftApprovalPanel;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import se.su.dsv.scipro.system.ProjectType;
import se.su.dsv.scipro.system.ProjectTypeSettings;
@Authorization(authorizedRoles = Roles.SUPERVISOR) @Authorization(authorizedRoles = Roles.SUPERVISOR)
public class SupervisorInteractWithReviewerPage extends AbstractSupervisorProjectDetailsPage implements MenuHighlightSupervisorMyProjects { public class SupervisorInteractWithReviewerPage extends AbstractSupervisorProjectDetailsPage implements MenuHighlightSupervisorMyProjects {
@ -41,6 +45,17 @@ public class SupervisorInteractWithReviewerPage extends AbstractSupervisorProjec
} }
}); });
IModel<String> moreInformationUrl = projectModel
.map(Project::getProjectType)
.map(ProjectType::getProjectTypeSettings)
.map(ProjectTypeSettings::getReviewProcessInformationUrl);
add(new ExternalLink("review_process_information", moreInformationUrl) {
@Override
protected void onConfigure() {
super.onConfigure();
setVisible(moreInformationUrl.getObject() != null);
}
});
ForumThread<Project> reviewerThread = new ReviewerInteractionForumThread(reviewerInteractionService); ForumThread<Project> reviewerThread = new ReviewerInteractionForumThread(reviewerInteractionService);
add(new RoughDraftApprovalPanel("roughDraftApproval", projectModel)); add(new RoughDraftApprovalPanel("roughDraftApproval", projectModel));
add(new SubmitForumReplyPanel<>("communication", projectModel, reviewerThread)); add(new SubmitForumReplyPanel<>("communication", projectModel, reviewerThread));

View File

@ -37,18 +37,20 @@ public class JavascriptEventConfirmation extends Behavior {
public void renderHead(Component component, IHeaderResponse response) { public void renderHead(Component component, IHeaderResponse response) {
super.renderHead(component, response); super.renderHead(component, response);
final String confirmationMsg = getConfirmationMsg(component); final String confirmationMsg = getConfirmationMsg(component);
if (confirmationMsg == null) return;
String confirmScript = "var conf = confirm('" + confirmationMsg.replaceAll("'", "\\\\'") + "'); " + String confirmScript = "var conf = confirm('" + confirmationMsg.replaceAll("'", "\\\\'") + "'); " +
"if (!conf) event.preventDefault();"; "if (!conf) event.preventDefault();";
response.render(OnEventHeaderItem.forComponent(component, event, confirmScript)); response.render(OnEventHeaderItem.forComponent(component, event, confirmScript));
} }
private String getConfirmationMsg(Component component) { private String getConfirmationMsg(Component component) {
IModel<?> model = msgModel; IModel<String> model = msgModel;
if (model instanceof IComponentAssignedModel) if (model instanceof IComponentAssignedModel<String> icam)
{ {
model = ((IComponentAssignedModel<?>)model).wrapOnAssignment(component); model = icam.wrapOnAssignment(component);
} }
return String.valueOf(model.getObject()); return model.getObject();
} }
@Override @Override

View File

@ -16,6 +16,13 @@
<div class="row"> <div class="row">
<div class="col-12 col-xl-6 mb-3"> <div class="col-12 col-xl-6 mb-3">
<h4><span wicket:id="title"></span></h4> <h4><span wicket:id="title"></span></h4>
<wicket:enclosure>
<p>
<a href="https://example.com" wicket:id="review_process_information">
See more information about the review process and any important dates.
</a>
</p>
</wicket:enclosure>
<div wicket:id="details"></div> <div wicket:id="details"></div>
<div wicket:id="feedback"></div> <div wicket:id="feedback"></div>
<form wicket:id="decision"> <form wicket:id="decision">

View File

@ -40,4 +40,32 @@ public class AdminProjectTypePanelTest extends SciProTest {
assertEquals(DegreeType.values()[index], captor.getValue().getDegreeType()); assertEquals(DegreeType.values()[index], captor.getValue().getDegreeType());
} }
@Test
public void set_min_max_authors() {
FormTester formTester = tester.newFormTester(path(panel, "projectTypeForm"));
formTester.setValue("name", "bachelor");
formTester.setValue("minimum_authors", "17");
formTester.setValue("maximum_authors", "29");
formTester.submit("createButton");
ArgumentCaptor<ProjectType> captor = ArgumentCaptor.forClass(ProjectType.class);
verify(projectTypeService).save(captor.capture());
assertEquals(17, captor.getValue().getProjectTypeSettings().getMinAuthors());
assertEquals(29, captor.getValue().getProjectTypeSettings().getMaxAuthors());
}
@Test
public void set_review_process_information_url() {
FormTester formTester = tester.newFormTester(path(panel, "projectTypeForm"));
formTester.setValue("name", "bachelor");
formTester.setValue("review_process_information_url", "https://example.com");
formTester.submit("createButton");
ArgumentCaptor<ProjectType> captor = ArgumentCaptor.forClass(ProjectType.class);
verify(projectTypeService).save(captor.capture());
assertEquals("https://example.com", captor.getValue().getProjectTypeSettings().getReviewProcessInformationUrl());
}
} }

View File

@ -18,6 +18,7 @@ import se.su.dsv.scipro.system.DegreeType;
import se.su.dsv.scipro.system.ProjectType; import se.su.dsv.scipro.system.ProjectType;
import se.su.dsv.scipro.system.User; import se.su.dsv.scipro.system.User;
import se.su.dsv.scipro.util.Either; import se.su.dsv.scipro.util.Either;
import se.su.dsv.scipro.util.JavascriptEventConfirmation;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
@ -152,6 +153,55 @@ public class SendToExaminerTest extends SciProTest {
verify(gradingService).reportGrade(TOKEN, project.getIdentifier(), biden.getIdentifier(), gw.id(), grade.name(), finalThesis.getUploadDate()); verify(gradingService).reportGrade(TOKEN, project.getIdentifier(), biden.getIdentifier(), gw.id(), grade.name(), finalThesis.getUploadDate());
} }
@Test
public void author_without_active_participations_done_should_generate_confirmation_on_sending_to_examiner() {
ProjectType bachelor = new ProjectType(DegreeType.BACHELOR, "Bachelor", "Bachelor");
User obama = User.builder()
.firstName("Barack")
.lastName("Obama")
.emailAddress("obama@example.com")
.identifier(44)
.build();
User biden = User.builder()
.firstName("Joe")
.lastName("Biden")
.emailAddress("joe@example.com")
.identifier(46)
.build();
biden.setId(46L);
Project project = Project.builder()
.title("My project")
.projectType(bachelor)
.startDate(LocalDate.of(2023, Month.JANUARY, 6))
.headSupervisor(obama)
.projectParticipants(Set.of(biden))
.identifier(1888)
.build();
project.setId(8L);
Examination gw = new Examination(
2,
new Name("Examensarbete", "Graduation work"),
"KX1E",
BigDecimal.valueOf(9),
List.of(
new Grade(Grade.Type.PASSING, "A"),
new Grade(Grade.Type.PASSING, "B"),
new Grade(Grade.Type.PASSING, "C"),
new Grade(Grade.Type.PASSING, "D"),
new Grade(Grade.Type.PASSING, "E"),
new Grade(Grade.Type.FAILING, "F")));
when(gradingService.getExaminations(any(), anyLong(), anyLong()))
.thenReturn(List.of(gw));
when(gradingService.getResult(any(), anyLong(), anyLong(), anyLong()))
.thenReturn(Either.right(Optional.empty()));
tester.startComponentInPage(new SendToExaminer("id", () -> project, () -> biden, () -> "please check active participations"));
tester.assertBehavior(path("id", "form", "send"), JavascriptEventConfirmation.class);
}
private static Thesis daisyThesis() { private static Thesis daisyThesis() {
Unit unit = new Unit(); Unit unit = new Unit();
unit.setId(1); unit.setId(1);

View File

@ -21,7 +21,6 @@ import se.su.dsv.scipro.checklist.ChecklistServiceImpl;
import se.su.dsv.scipro.checklist.ChecklistTemplateService; import se.su.dsv.scipro.checklist.ChecklistTemplateService;
import se.su.dsv.scipro.checklist.ChecklistTemplateServiceImpl; import se.su.dsv.scipro.checklist.ChecklistTemplateServiceImpl;
import se.su.dsv.scipro.crosscutting.ForwardPhase2Feedback; import se.su.dsv.scipro.crosscutting.ForwardPhase2Feedback;
import se.su.dsv.scipro.crosscutting.ReviewerAssignedDeadline;
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;
@ -161,6 +160,7 @@ import se.su.dsv.scipro.reviewing.DecisionRepository;
import se.su.dsv.scipro.reviewing.FinalSeminarApprovalService; import se.su.dsv.scipro.reviewing.FinalSeminarApprovalService;
import se.su.dsv.scipro.reviewing.FinalSeminarApprovalServiceImpl; import se.su.dsv.scipro.reviewing.FinalSeminarApprovalServiceImpl;
import se.su.dsv.scipro.reviewing.ProjectFinalSeminarStatisticsServiceImpl; import se.su.dsv.scipro.reviewing.ProjectFinalSeminarStatisticsServiceImpl;
import se.su.dsv.scipro.reviewing.ReviewerAssignedDeadline;
import se.su.dsv.scipro.reviewing.ReviewerCapacityServiceImpl; import se.su.dsv.scipro.reviewing.ReviewerCapacityServiceImpl;
import se.su.dsv.scipro.reviewing.ReviewerDeadlineFollowupServiceImpl; import se.su.dsv.scipro.reviewing.ReviewerDeadlineFollowupServiceImpl;
import se.su.dsv.scipro.reviewing.ReviewerDeadlineSettingsRepository; import se.su.dsv.scipro.reviewing.ReviewerDeadlineSettingsRepository;