3204 Start of managing reviewer capacity

This commit is contained in:
Andreas Svanberg 2023-12-06 12:51:37 +01:00
parent 7f999022c8
commit a123fd1932
8 changed files with 207 additions and 1 deletions

@ -1,9 +1,17 @@
package se.su.dsv.scipro.reviewing;
import se.su.dsv.scipro.system.Unit;
import se.su.dsv.scipro.system.User;
import java.time.LocalDate;
import java.util.List;
public interface ReviewerCapacityService {
void assignTarget(User reviewer, DateRange dateRange, int target);
List<Unit> getUnitsWithReviewers();
List<User> getAllActiveReviewers();
List<User> getActiveReviewersOnUnit(Unit unit);
}

@ -1,6 +1,7 @@
package se.su.dsv.scipro.reviewing;
import se.su.dsv.scipro.project.Project;
import se.su.dsv.scipro.system.Unit;
import se.su.dsv.scipro.system.User;
import se.su.dsv.scipro.system.UserService;
@ -9,7 +10,9 @@ import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
class ReviewerCapacityServiceImpl implements ReviewerCapacityService, ReviewerAssignmentService {
private final ReviewerTargetRepository reviewerTargetRepository;
@ -37,6 +40,29 @@ class ReviewerCapacityServiceImpl implements ReviewerCapacityService, ReviewerAs
reviewerTargetRepository.save(reviewerTarget);
}
@Override
public List<Unit> getUnitsWithReviewers() {
return userService.findActiveReviewers()
.stream()
.map(User::getUnit)
.distinct()
.sorted(Comparator.comparing(Unit::getTitle))
.toList();
}
@Override
public List<User> getAllActiveReviewers() {
return userService.findActiveReviewers();
}
@Override
public List<User> getActiveReviewersOnUnit(Unit unit) {
return userService.findActiveReviewers()
.stream()
.filter(reviewer -> Objects.equals(reviewer.getUnit(), unit))
.toList();
}
private Optional<ReviewerTarget> getTarget(User reviewer, LocalDate date) {
return reviewerTargetRepository.getReviewerTargets(reviewer, date)
.stream()

@ -274,6 +274,7 @@ public class SciProApplication extends LifecycleManagedWebApplication {
mountPage("admin/project/create", AdminCreateProjectPage.class);
mountPage("admin/project/survey", AdminSurveyPage.class);
mountPage("admin/project/reviewer", AdminAssignReviewerPage.class);
mountPage("admin/project/reviewer/capacity", AdminReviewerCapacityManagementPage.class);
mountPage("admin/edit", AdminEditProjectPage.class);
mountPage("admin/finalseminars", AdminFinalSeminarPage.class);
mountPage("admin/finalseminars/exemptions", AdminFinalSeminarExemptionPage.class);

@ -27,7 +27,8 @@ public class AbstractAdminProjectPage extends AbstractAdminPage {
new MenuItem("Activity plan templates", AdminActivityPlanTemplatesPage.class, MenuHighlightAdminActivityPlanTemplates.class),
new MenuItem("Checklist templates", AdminChecklistPage.class, MenuHighlightAdminChecklist.class),
new MenuItem("Non work days", NonWorkDaysPage.class),
new MenuItem("Survey", AdminSurveyPage.class)
new MenuItem("Survey", AdminSurveyPage.class),
new MenuItem("Reviewer targets", AdminReviewerCapacityManagementPage.class)
);
}

@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:wicket="http://wicket.apache.org">
<body>
<wicket:extend>
<h2>Reviewer targets</h2>
<div class="row">
<div class="col-12 col-xl-4">
<p>
On this page you can set the number of reviews (target) that each reviewer should do in any
given period. The number is not a strict cap but acts as a very strong hint to the
administrators during the assignment process.
</p>
<p>
You can use the below "default period" to quickly set the number of reviews for all
reviewers within a single period. If you want to set the number of reviews for a single
reviewer, you can expand the reviewer by clicking on their box and add individual targets
for each period.
</p>
<p>
You also have the ability to mark reviewers as completely unavailable for a given period.
It can be useful if a reviewer is on vacation, sick, busy with course work, or otherwise
unavailable for a period. This is intended to prevent the reviewer from being assigned and
holding up the thesis process since they are unable to perform the review.
</p>
<h3>Filter</h3>
<form>
<div class="mb-3">
<label class="form-label">Unit</label>
<select class="form-select" wicket:id="units">
<option>All</option>
<optgroup label="Units">
<option>ACT</option>
<option>IDEAL</option>
<option>IS</option>
<option>SAS</option>
</optgroup>
</select>
<small class="form-text text-muted">Only show reviewers on the selected unit</small>
</div>
</form>
<h3>Default period</h3>
<div class="mb-3 row">
<div class="col">
<div class="input-group">
<input type="text" class="form-control" wicket:id="default_from_date">
</div>
</div>
<div class="col-auto align-self-center">
&mdash;
</div>
<div class="col">
<div class="input-group">
<input type="text" class="form-control" wicket:id="default_to_date">
</div>
</div>
</div>
</div>
<div class="col-12 col-xl-6" wicket:id="reviewers_container">
<div class="card mb-3" wicket:id="reviewers">
<div class="row g-0">
<div class="col-auto">
<img class="img-fluid rounded-start profile-picture-md" wicket:id="profile_image">
</div>
<div class="col">
<div class="card-body">
<h4 class="card-title" wicket:id="name">[John Doe]</h4>
</div>
</div>
<div class="col-auto w-64-px align-self-center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22"><defs><clipPath><path fill="#00f" fill-opacity=".514" d="m-7 1024.36h34v34h-34z"/></clipPath><clipPath><path fill="#aade87" fill-opacity=".472" d="m-6 1028.36h32v32h-32z"/></clipPath></defs><path d="m345.44 248.29l-194.29 194.28c-12.359 12.365-32.397 12.365-44.75 0-12.354-12.354-12.354-32.391 0-44.744l171.91-171.91-171.91-171.9c-12.354-12.359-12.354-32.394 0-44.748 12.354-12.359 32.391-12.359 44.75 0l194.29 194.28c6.177 6.18 9.262 14.271 9.262 22.366 0 8.099-3.091 16.196-9.267 22.373" transform="matrix(.03541-.00013.00013.03541 2.98 3.02)" fill="#4d4d4d"/></svg>
</div>
</div>
</div>
</div>
</div>
</wicket:extend>
</body>
</html>

@ -0,0 +1,86 @@
package se.su.dsv.scipro.admin.pages;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.ajax.form.AjaxFormComponentUpdatingBehavior;
import org.apache.wicket.markup.html.WebMarkupContainer;
import org.apache.wicket.markup.html.form.DropDownChoice;
import org.apache.wicket.markup.html.form.FormComponent;
import org.apache.wicket.markup.html.form.LambdaChoiceRenderer;
import org.apache.wicket.markup.html.form.TextField;
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 se.su.dsv.scipro.components.BootstrapDatePicker;
import se.su.dsv.scipro.data.DetachableServiceModel;
import se.su.dsv.scipro.profile.UserLabel;
import se.su.dsv.scipro.profile.UserProfileImage;
import se.su.dsv.scipro.reviewing.ReviewerCapacityService;
import se.su.dsv.scipro.springdata.services.UnitService;
import se.su.dsv.scipro.system.Unit;
import se.su.dsv.scipro.system.User;
import javax.inject.Inject;
import java.time.LocalDate;
import java.util.List;
public class AdminReviewerCapacityManagementPage extends AbstractAdminProjectPage {
@Inject
ReviewerCapacityService reviewerCapacityService;
@Inject
UnitService unitService;
private final WebMarkupContainer reviewerList;
private IModel<Unit> selectedUnit;
private IModel<LocalDate> fromDate = new Model<>();
private IModel<LocalDate> toDate = new Model<>();
public AdminReviewerCapacityManagementPage() {
IModel<List<Unit>> units = LoadableDetachableModel.of(() ->
reviewerCapacityService.getUnitsWithReviewers());
selectedUnit = new DetachableServiceModel<>(unitService);
DropDownChoice<Unit> unitDropDownChoice = new DropDownChoice<>(
"units",
selectedUnit,
units,
new LambdaChoiceRenderer<>(Unit::getTitle, Unit::getId));
unitDropDownChoice.setNullValid(true);
unitDropDownChoice.add(new AjaxFormComponentUpdatingBehavior("change") {
@Override
protected void onUpdate(AjaxRequestTarget target) {
target.add(reviewerList);
}
});
add(unitDropDownChoice);
FormComponent<LocalDate> fromDateField = new TextField<>("default_from_date", fromDate, LocalDate.class);
fromDateField.add(new BootstrapDatePicker());
add(fromDateField);
FormComponent<LocalDate> toDateField = new TextField<>("default_to_date", toDate, LocalDate.class);
toDateField.add(new BootstrapDatePicker());
add(toDateField);
IModel<List<User>> reviewers = LoadableDetachableModel.of(() -> {
if (selectedUnit.getObject() == null) {
return reviewerCapacityService.getAllActiveReviewers();
}
else {
return reviewerCapacityService.getActiveReviewersOnUnit(selectedUnit.getObject());
}
});
reviewerList = new WebMarkupContainer("reviewers_container");
reviewerList.add(new ListView<>("reviewers", reviewers) {
@Override
protected void populateItem(ListItem<User> item) {
item.add(new UserProfileImage("profile_image", item.getModel(), UserProfileImage.Size.MEDIUM));
item.add(new UserLabel("name", item.getModel()));
}
});
reviewerList.setOutputMarkupId(true);
add(reviewerList);
}
}

@ -538,6 +538,9 @@ th.wicket_orderUp, th.sorting_asc {
.w-200-p {
width: 200px;
}
.w-64-px {
width: 64px;
}
.read-state {
text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black;