3204 Assign the reviewer

This commit is contained in:
Andreas Svanberg 2023-12-07 15:31:54 +01:00
parent 6d076b2b77
commit db612da854
7 changed files with 91 additions and 23 deletions

@ -19,6 +19,7 @@ import se.su.dsv.scipro.peer.PeerRequestExpiredEvent;
import se.su.dsv.scipro.project.ProjectActivatedEvent;
import se.su.dsv.scipro.project.ProjectCompletedEvent;
import se.su.dsv.scipro.project.ProjectDeactivatedEvent;
import se.su.dsv.scipro.project.ReviewerAssignedEvent;
import javax.inject.Inject;
import javax.inject.Singleton;
@ -120,4 +121,9 @@ public class Notifications {
source.setMessage(event.getFinalSeminar().getProjectTitle());
notificationController.notifyCustomProject(event.getProject(), ProjectEvent.Event.PARTICIPATION_FAILED, source, recipients);
}
@Subscribe
public void reviewersChanged(ReviewerAssignedEvent event) {
notificationController.notifyProject(event.getProject(), ProjectEvent.Event.REVIEWERS_CHANGED, new NotificationSource());
}
}

@ -1,9 +1,16 @@
package se.su.dsv.scipro.reviewing;
import se.su.dsv.scipro.project.Project;
import se.su.dsv.scipro.system.User;
import java.time.LocalDate;
public interface ReviewerAssignmentService {
ReviewerCandidates getCandidatesToReview(Project project, LocalDate date);
ReviewerAssignment assignReviewer(Project project, User reviewer);
enum ReviewerAssignment {
OK, ERROR_IS_SUPERVISOR, ERROR_IS_NOT_REVIEWER
}
}

@ -1,6 +1,10 @@
package se.su.dsv.scipro.reviewing;
import com.google.common.eventbus.EventBus;
import se.su.dsv.scipro.project.Project;
import se.su.dsv.scipro.project.ProjectService;
import se.su.dsv.scipro.project.ReviewerAssignedEvent;
import se.su.dsv.scipro.security.auth.roles.Roles;
import se.su.dsv.scipro.system.Unit;
import se.su.dsv.scipro.system.User;
import se.su.dsv.scipro.system.UserService;
@ -12,22 +16,27 @@ 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;
private final DecisionRepository decisionRepository;
private final UserService userService;
private final ProjectService projectService;
private final EventBus eventBus;
@Inject
ReviewerCapacityServiceImpl(
ReviewerTargetRepository reviewerTargetRepository,
DecisionRepository decisionRepository,
UserService userService)
UserService userService,
ProjectService projectService,
EventBus eventBus)
{
this.reviewerTargetRepository = reviewerTargetRepository;
this.decisionRepository = decisionRepository;
this.userService = userService;
this.projectService = projectService;
this.eventBus = eventBus;
}
@Override
@ -121,6 +130,23 @@ class ReviewerCapacityServiceImpl implements ReviewerCapacityService, ReviewerAs
return new ReviewerCandidates(good, mismatched, busy, unavailable);
}
@Override
public ReviewerAssignment assignReviewer(Project project, User reviewer) {
if (project.getHeadSupervisor().equals(reviewer)) {
return ReviewerAssignment.ERROR_IS_SUPERVISOR;
} else if (!reviewer.getRoles().contains(Roles.REVIEWER)) {
return ReviewerAssignment.ERROR_IS_NOT_REVIEWER;
} else {
for (User currentReviewer : project.getReviewers()) {
project.removeReviewer(currentReviewer);
}
project.addReviewer(reviewer);
projectService.save(project);
eventBus.post(new ReviewerAssignedEvent(project, reviewer));
return ReviewerAssignment.OK;
}
}
private int countAssignedReviews(ReviewerTarget reviewerTarget) {
return countAssignedReviews(reviewerTarget.getReviewer(), reviewerTarget.getFromDate(), reviewerTarget.getToDate());
}

@ -17,10 +17,14 @@
<dt>Language</dt>
<dd wicket:id="language"></dd>
<dt>Reviewer</dt>
<dd wicket:id="reviewer"></dd>
</dl>
</div>
<div class="col-12 col-xl-4 col-md-6" wicket:id="reviewers">
<div wicket:id="feedback"></div>
<wicket:enclosure child="good_candidates">
<h3>Good candidates</h3>
<p>
@ -40,7 +44,7 @@
</div>
<div class="col-auto my-auto">
<div class="card-body py-0">
<button class="btn btn-success">Assign</button>
<button class="btn btn-success" wicket:id="assign">Assign</button>
</div>
</div>
</div>
@ -81,7 +85,7 @@
</div>
<div class="col-auto my-auto">
<div class="card-body py-0">
<button class="btn btn-success">Assign</button>
<button class="btn btn-success" wicket:id="assign">Assign</button>
</div>
</div>
</div>
@ -106,7 +110,7 @@
</div>
<div class="col-auto my-auto">
<div class="card-body py-0">
<button class="btn btn-success">Assign</button>
<button class="btn btn-success" wicket:id="assign">Assign</button>
</div>
</div>
</div>
@ -130,7 +134,7 @@
</div>
<div class="col-auto my-auto">
<div class="card-body py-0">
<button class="btn btn-secondary">Assign</button>
<button class="btn btn-secondary" wicket:id="assign">Assign</button>
</div>
</div>
</div>

@ -1,11 +1,14 @@
package se.su.dsv.scipro.admin.pages;
import org.apache.wicket.RestartResponseException;
import org.apache.wicket.markup.html.GenericWebMarkupContainer;
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.link.Link;
import org.apache.wicket.markup.html.list.ListItem;
import org.apache.wicket.markup.html.list.ListView;
import org.apache.wicket.markup.html.panel.FeedbackPanel;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.LoadableDetachableModel;
import org.apache.wicket.request.mapper.parameter.PageParameters;
@ -64,19 +67,24 @@ public class AdminAssignReviewerPage extends AbstractAdminProjectPage {
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)));
add(new UserLinkPanel("reviewer", projectModel.map(Project::getReviewer)));
}
}
private static class AvailableReviewersPanel extends WebMarkupContainer {
private static class AvailableReviewersPanel extends GenericWebMarkupContainer<Project> {
@Inject
private ReviewerAssignmentService reviewerAssignmentService;
@Inject
private Clock clock;
private final IModel<ReviewerCandidates> reviewerCandidates;
public AvailableReviewersPanel(String id, IModel<Project> projectModel) {
super(id, projectModel);
IModel<ReviewerCandidates> reviewerCandidates = LoadableDetachableModel.of(() ->
add(new FeedbackPanel("feedback"));
reviewerCandidates = LoadableDetachableModel.of(() ->
reviewerAssignmentService.getCandidatesToReview(projectModel.getObject(), LocalDate.now(clock)));
add(new AutoHidingListView<>("good_candidates", reviewerCandidates.map(ReviewerCandidates::good)) {
@ -86,6 +94,7 @@ public class AdminAssignReviewerPage extends AbstractAdminProjectPage {
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)));
item.add(new AssignReviewerLink("assign", item.getModel().map(ReviewerCandidates.Candidate::reviewer)));
}
});
add(new AutoHidingListView<>("mismatched_candidates", reviewerCandidates.map(ReviewerCandidates::mismatched)) {
@ -96,6 +105,7 @@ public class AdminAssignReviewerPage extends AbstractAdminProjectPage {
item.add(new UserLabel("user", reviewerModel));
item.add(new Label("target", item.getModel().map(ReviewerCandidates.Candidate::target)));
item.add(new Label("assigned", item.getModel().map(ReviewerCandidates.Candidate::assigned)));
item.add(new AssignReviewerLink("assign", reviewerModel));
item.add(new ListView<>("research_areas", reviewerModel.map(User::getResearchAreas).map(ArrayList::new)) {
@Override
protected void populateItem(ListItem<ResearchArea> item) {
@ -117,6 +127,7 @@ public class AdminAssignReviewerPage extends AbstractAdminProjectPage {
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)));
item.add(new AssignReviewerLink("assign", item.getModel().map(ReviewerCandidates.Candidate::reviewer)));
}
});
add(new AutoHidingListView<>("unavailable_candidates", reviewerCandidates.map(ReviewerCandidates::unavailable)) {
@ -124,8 +135,34 @@ public class AdminAssignReviewerPage extends AbstractAdminProjectPage {
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)));
IModel<User> reviewer = item.getModel().map(ReviewerCandidates.Candidate::reviewer);
item.add(new AssignReviewerLink("assign", reviewer));
}
});
}
private class AssignReviewerLink extends Link<User> {
public AssignReviewerLink(String id, IModel<User> reviewer) {
super(id, reviewer);
}
@Override
public void onClick() {
ReviewerAssignmentService.ReviewerAssignment reviewerAssignment = reviewerAssignmentService.assignReviewer(
AvailableReviewersPanel.this.getModelObject(),
getModelObject());
switch (reviewerAssignment) {
case OK -> AvailableReviewersPanel.this.success(AvailableReviewersPanel.this.getString("reviewer_assigned"));
case ERROR_IS_SUPERVISOR -> AvailableReviewersPanel.this.error(AvailableReviewersPanel.this.getString("error_reviewer_is_supervisor"));
case ERROR_IS_NOT_REVIEWER -> AvailableReviewersPanel.this.error(AvailableReviewersPanel.this.getString("error_reviewer_is_not_reviewer"));
}
// have to detach the model since fetching the model object for this component will
// cause the model object for the ListItem which will cause the ListView to be loaded and
// that in turn will load the reviewer candidates. This means the candidates will be loaded
// before the re-assignment has taken place. Forcibly detach the model so that it will have
// to be loaded again before rendering.
AvailableReviewersPanel.this.reviewerCandidates.detach();
}
}
}
}

@ -0,0 +1,3 @@
reviewer_assigned=Reviewer assigned
error_reviewer_is_supervisor=The selected reviewer is the supervisor of the project
error_reviewer_is_not_reviewer=The selected reviewer does not have the reviewer role

@ -22,7 +22,6 @@ 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;
import se.su.dsv.scipro.components.EnumLambdaColumn;
import se.su.dsv.scipro.components.ExportableDataPanel;
import se.su.dsv.scipro.components.LinkWrapper;
@ -30,17 +29,13 @@ import se.su.dsv.scipro.components.ListAdapterModel;
import se.su.dsv.scipro.components.TemporalColumn;
import se.su.dsv.scipro.components.datatables.MultipleUsersColumn;
import se.su.dsv.scipro.components.datatables.UserColumn;
import se.su.dsv.scipro.data.DetachableServiceModel;
import se.su.dsv.scipro.dataproviders.FilteredDataProvider;
import se.su.dsv.scipro.datatables.AjaxCheckboxWrapper;
import se.su.dsv.scipro.notifications.NotificationController;
import se.su.dsv.scipro.notifications.dataobject.NotificationSource;
import se.su.dsv.scipro.notifications.dataobject.ProjectEvent;
import se.su.dsv.scipro.profile.UserLinkPanel;
import se.su.dsv.scipro.project.Project;
import se.su.dsv.scipro.project.ProjectService;
import se.su.dsv.scipro.project.ProjectStatus;
import se.su.dsv.scipro.project.ReviewerAssignedEvent;
import se.su.dsv.scipro.security.auth.roles.Roles;
import se.su.dsv.scipro.session.SciProSession;
import se.su.dsv.scipro.system.DegreeType;
@ -60,12 +55,6 @@ public class ProjectDataPanel extends Panel {
private ProjectTypeService projectTypeService;
@Inject
private ProjectService projectService;
@Inject
private EventBus eventBus;
@Inject
private NotificationController notificationController;
@Inject
private UserService userService;
private ExportableDataPanel<Project, String> exportableDataPanel;
private ProjectService.Filter filter;
@ -193,10 +182,6 @@ public class ProjectDataPanel extends Panel {
};
}
private void notifyReviewersChanged(Project project) {
notificationController.notifyProject(project, ProjectEvent.Event.REVIEWERS_CHANGED, new NotificationSource());
}
private void addFeedbackPanel() {
feedback = new FencedFeedbackPanel("feedback", this);
feedback.setOutputMarkupId(true);