Enable creating an API using Spring Web #5
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
@ -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()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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() + ")";
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE `project_type_settings`
|
||||||
|
ADD COLUMN `review_process_information_url_for_supervisor` VARCHAR(255) NULL;
|
127
core/src/test/java/se/su/dsv/scipro/reviewing/ReviewerTest.java
Normal file
127
core/src/test/java/se/su/dsv/scipro/reviewing/ReviewerTest.java
Normal 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());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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() {
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
@ -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
|
||||||
|
@ -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">
|
||||||
|
@ -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));
|
||||||
|
@ -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
|
||||||
|
@ -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">
|
||||||
|
@ -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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user