Basic GUI for assigning reviewers

This commit is contained in:
Andreas Svanberg 2023-12-05 14:02:42 +01:00
parent a615f12238
commit 33e0e033b5
8 changed files with 166 additions and 25 deletions

@ -0,0 +1,7 @@
package se.su.dsv.scipro.reviewing;
import se.su.dsv.scipro.project.Project;
public interface ReviewerAssignmentService {
ReviewerCandidates getCandidatesToReview(Project project);
}

@ -0,0 +1,10 @@
package se.su.dsv.scipro.reviewing;
import se.su.dsv.scipro.system.User;
import java.util.List;
public record ReviewerCandidates(List<Candidate> goodCandidates) {
public record Candidate(User reviewer, int target, int assigned) {
}
}

@ -1,11 +1,13 @@
package se.su.dsv.scipro.reviewing;
import se.su.dsv.scipro.project.Project;
import se.su.dsv.scipro.system.User;
import javax.inject.Inject;
import java.time.LocalDate;
import java.util.List;
class ReviewerCapacityServiceImpl implements ReviewerCapacityService {
class ReviewerCapacityServiceImpl implements ReviewerCapacityService, ReviewerAssignmentService {
private final ReviewerTargetRepository reviewerTargetRepository;
@Inject
@ -31,4 +33,10 @@ class ReviewerCapacityServiceImpl implements ReviewerCapacityService {
.max()
.orElse(0);
}
@Override
public ReviewerCandidates getCandidatesToReview(Project project) {
return new ReviewerCandidates(
List.of(new ReviewerCandidates.Candidate(project.getHeadSupervisor(), 5, 2)));
}
}

@ -20,5 +20,6 @@ public class ReviewingModule extends AbstractModule {
bind(ReviewerDeadlineSettingsRepository.class).to(ReviewerDeadlineSettingsRepositoryImpl.class);
bind(ReviewerDeadlineFollowupService.class).to(ReviewerDeadlineFollowupServiceImpl.class);
bind(ReviewerCapacityService.class).to(ReviewerCapacityServiceImpl.class);
bind(ReviewerAssignmentService.class).to(ReviewerCapacityServiceImpl.class);
}
}

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

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:wicket="http://wicket.apache.org">
<body>
<wicket:extend>
<div class="row">
<div class="col-12 col-xl-4" wicket:id="project_details">
<dl>
<dt>Project</dt>
<dd wicket:id="title"></dd>
<dt>Supervisor</dt>
<dd wicket:id="supervisor"></dd>
<dt>Research area</dt>
<dd wicket:id="research_area"></dd>
<dt>Language</dt>
<dd wicket:id="language"></dd>
</dl>
</div>
<div class="col-12 col-xl-4 col-md-6" wicket:id="reviewers">
<div class="card bg-success text-white bg-opacity-50" wicket:id="good_candidates">
<div class="row">
<div class="col-auto">
<img class="img-fluid rounded-start" wicket:id="image">
</div>
<div class="col my-auto">
<div class="card-body p-0">
<h4 class="card-title text-white" wicket:id="user"></h4>
<span wicket:id="assigned"></span> / <span wicket:id="target"></span>
</div>
</div>
<div class="col-auto my-auto">
<div class="card-body py-0">
<button class="btn btn-success">Assign</button>
</div>
</div>
</div>
</div>
</div>
</div>
</wicket:extend>
</body>
</html>

@ -0,0 +1,85 @@
package se.su.dsv.scipro.admin.pages;
import org.apache.wicket.RestartResponseException;
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.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.request.mapper.parameter.PageParameters;
import org.apache.wicket.util.string.StringValueConversionException;
import se.su.dsv.scipro.data.DetachableServiceModel;
import se.su.dsv.scipro.profile.UserLabel;
import se.su.dsv.scipro.profile.UserLinkPanel;
import se.su.dsv.scipro.profile.UserProfileImage;
import se.su.dsv.scipro.project.Project;
import se.su.dsv.scipro.project.ProjectService;
import se.su.dsv.scipro.reviewing.ReviewerAssignmentService;
import se.su.dsv.scipro.reviewing.ReviewerCandidates;
import se.su.dsv.scipro.system.ResearchArea;
import javax.inject.Inject;
public class AdminAssignReviewerPage extends AbstractAdminProjectPage {
@Inject
private ProjectService projectService;
public AdminAssignReviewerPage(PageParameters pp) {
IModel<Project> projectModel = new DetachableServiceModel<>(projectService);
try {
long pid = pp.get("pid").toLong();
Project project = projectService.findOne(pid);
if (project == null) {
throw new RestartResponseException(ProjectManagementPage.class);
}
projectModel.setObject(project);
} catch (StringValueConversionException ignored) {
throw new RestartResponseException(ProjectManagementPage.class);
}
add(new ProjectDetailsPanel("project_details", projectModel));
add(new AvailableReviewersPanel("reviewers", projectModel));
}
public static PageParameters pageParametersFor(Project project) {
PageParameters pageParameters = new PageParameters();
pageParameters.set("pid", project.getId());
return pageParameters;
}
private static class ProjectDetailsPanel extends WebMarkupContainer {
public ProjectDetailsPanel(String id, IModel<Project> projectModel) {
super(id, projectModel);
add(new Label("title", projectModel.map(Project::getTitle)));
add(new Label("research_area", projectModel.map(Project::getResearchArea).map(ResearchArea::getTitle)));
add(new UserLinkPanel("supervisor", projectModel.map(Project::getHeadSupervisor)));
add(new EnumLabel<>("language", projectModel.map(Project::getLanguage)));
}
}
private static class AvailableReviewersPanel extends WebMarkupContainer {
@Inject
private ReviewerAssignmentService reviewerAssignmentService;
public AvailableReviewersPanel(String id, IModel<Project> projectModel) {
super(id, projectModel);
IModel<ReviewerCandidates> reviewerCandidates = LoadableDetachableModel.of(() ->
reviewerAssignmentService.getCandidatesToReview(projectModel.getObject()));
add(new ListView<>("good_candidates", reviewerCandidates.map(ReviewerCandidates::goodCandidates)) {
@Override
protected void populateItem(ListItem<ReviewerCandidates.Candidate> item) {
item.add(new UserProfileImage("image", item.getModel().map(ReviewerCandidates.Candidate::reviewer), UserProfileImage.Size.MEDIUM));
item.add(new UserLabel("user", item.getModel().map(ReviewerCandidates.Candidate::reviewer)));
item.add(new Label("target", item.getModel().map(ReviewerCandidates.Candidate::target)));
item.add(new Label("assigned", item.getModel().map(ReviewerCandidates.Candidate::assigned)));
}
});
}
}
}

@ -18,6 +18,7 @@ import org.apache.wicket.model.IModel;
import org.apache.wicket.model.LambdaModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import se.su.dsv.scipro.admin.pages.AdminAssignReviewerPage;
import se.su.dsv.scipro.admin.pages.AdminCreateProjectPage;
import se.su.dsv.scipro.admin.pages.AdminEditProjectPage;
import se.su.dsv.scipro.components.EmployeeAutoCompleteDivPanel;
@ -159,32 +160,15 @@ public class ProjectDataPanel extends Panel {
}
private AbstractColumn<Project, String> reviewerColumn() {
return new UserColumn<>(Model.of("Reviewer"), Project::getReviewer) {
return new LambdaColumn<>(Model.of("Reviewer"), Project::getReviewer) {
@Override
public void populateItem(final Item<ICellPopulator<Project>> cellItem, final String componentId, final IModel<Project> rowModel) {
cellItem.add(new EmployeeAutoCompleteDivPanel(componentId, new DetachableServiceModel<>(userService, rowModel.getObject().getReviewer())) {
@Override
public void onNewEmployeeSelection(final AjaxRequestTarget target, final User newSelection) {
Project project = rowModel.getObject();
if (project.getHeadSupervisor().equals(newSelection)) {
error(getString("supervisor.reviewer.error"));
} else if (!newSelection.getRoles().contains(Roles.REVIEWER)) {
error(getString("not.reviewer.error"));
} else {
for (User currentReviewer : project.getReviewers()) {
project.removeReviewer(currentReviewer);
}
project.addReviewer(newSelection);
notifyReviewersChanged(project);
eventBus.post(new ReviewerAssignedEvent(project, newSelection));
projectService.save(project);
info(getString("reviewer.updated"));
}
target.add(feedback);
target.add(exportableDataPanel);
}
});
cellItem.add(LinkWrapper.apply(componentId, id -> {
PageParameters pp = AdminAssignReviewerPage.pageParametersFor(rowModel.getObject());
final BookmarkablePageLink<Void> link = new BookmarkablePageLink<>(id, AdminAssignReviewerPage.class, pp);
link.setBody(Model.of("Assign reviewer"));
return link;
}));
}
};
}