Allows admins to manage grading report templates #14

Merged
niat8586 merged 41 commits from 3482-new-grading-criteria into develop 2024-10-30 10:05:23 +01:00
13 changed files with 283 additions and 6 deletions
Showing only changes of commit 1672171f46 - Show all commits

View File

@ -3,6 +3,7 @@ package se.su.dsv.scipro.grading;
import com.google.inject.Key; import com.google.inject.Key;
import com.google.inject.PrivateModule; import com.google.inject.PrivateModule;
import com.google.inject.name.Names; import com.google.inject.name.Names;
import se.su.dsv.scipro.report.GradingReportServiceImpl;
public class GradingModule extends PrivateModule { public class GradingModule extends PrivateModule {
@Override @Override
@ -29,5 +30,8 @@ public class GradingModule extends PrivateModule {
bind(NationalSubjectCategoryRepository.class).to(NationalSubjectCategoryRepositoryImpl.class); bind(NationalSubjectCategoryRepository.class).to(NationalSubjectCategoryRepositoryImpl.class);
bind(NationalSubjectCategoryService.class).to(NationalSubjectCategoryServiceImpl.class); bind(NationalSubjectCategoryService.class).to(NationalSubjectCategoryServiceImpl.class);
expose(NationalSubjectCategoryService.class); expose(NationalSubjectCategoryService.class);
bind(GradingReportTemplateService.class).to(GradingReportServiceImpl.class);
expose(GradingReportTemplateService.class);
} }
} }

View File

@ -0,0 +1,12 @@
package se.su.dsv.scipro.grading;
import se.su.dsv.scipro.report.GradingReportTemplate;
import se.su.dsv.scipro.system.ProjectType;
import java.util.List;
public interface GradingReportTemplateService {
List<ProjectType> getProjectTypes();
GradingReportTemplate getCurrentTemplate(ProjectType projectType);
}

View File

@ -4,18 +4,23 @@ import com.google.common.eventbus.EventBus;
import com.google.inject.persist.Transactional; import com.google.inject.persist.Transactional;
import se.su.dsv.scipro.finalseminar.FinalSeminarOpposition; import se.su.dsv.scipro.finalseminar.FinalSeminarOpposition;
import se.su.dsv.scipro.grading.GradingBasis; import se.su.dsv.scipro.grading.GradingBasis;
import se.su.dsv.scipro.grading.GradingReportTemplateService;
import se.su.dsv.scipro.grading.ThesisSubmissionHistoryService; import se.su.dsv.scipro.grading.ThesisSubmissionHistoryService;
import se.su.dsv.scipro.project.Project; import se.su.dsv.scipro.project.Project;
import se.su.dsv.scipro.system.Language; import se.su.dsv.scipro.system.Language;
import se.su.dsv.scipro.system.ProjectModule;
import se.su.dsv.scipro.system.ProjectType;
import se.su.dsv.scipro.system.ProjectTypeService;
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 jakarta.inject.Inject; import jakarta.inject.Inject;
import java.time.Clock; import java.time.Clock;
import java.time.Instant; import java.time.Instant;
import java.time.LocalDate;
import java.util.*; import java.util.*;
public class GradingReportServiceImpl implements GradingReportService { public class GradingReportServiceImpl implements GradingReportTemplateService, GradingReportService {
public static final String OPPOSITION_SWEDISH = "Ö1 Oppositionsrapport"; public static final String OPPOSITION_SWEDISH = "Ö1 Oppositionsrapport";
public static final String OPPOSITION_ENGLISH = "Ö1 Opposition report"; public static final String OPPOSITION_ENGLISH = "Ö1 Opposition report";
@ -24,6 +29,7 @@ public class GradingReportServiceImpl implements GradingReportService {
private final Clock clock; private final Clock clock;
private final SupervisorGradingReportRepository supervisorGradingReportRepository; private final SupervisorGradingReportRepository supervisorGradingReportRepository;
private final GradingReportTemplateRepo gradingReportTemplateRepo; private final GradingReportTemplateRepo gradingReportTemplateRepo;
private final ProjectTypeService projectTypeService;
@Inject @Inject
public GradingReportServiceImpl( public GradingReportServiceImpl(
@ -31,13 +37,15 @@ public class GradingReportServiceImpl implements GradingReportService {
ThesisSubmissionHistoryService thesisSubmissionHistoryService, ThesisSubmissionHistoryService thesisSubmissionHistoryService,
Clock clock, Clock clock,
SupervisorGradingReportRepository supervisorGradingReportRepository, SupervisorGradingReportRepository supervisorGradingReportRepository,
GradingReportTemplateRepo gradingReportTemplateRepo) GradingReportTemplateRepo gradingReportTemplateRepo,
ProjectTypeService projectTypeService)
{ {
this.eventBus = eventBus; this.eventBus = eventBus;
this.thesisSubmissionHistoryService = thesisSubmissionHistoryService; this.thesisSubmissionHistoryService = thesisSubmissionHistoryService;
this.clock = clock; this.clock = clock;
this.supervisorGradingReportRepository = supervisorGradingReportRepository; this.supervisorGradingReportRepository = supervisorGradingReportRepository;
this.gradingReportTemplateRepo = gradingReportTemplateRepo; this.gradingReportTemplateRepo = gradingReportTemplateRepo;
this.projectTypeService = projectTypeService;
} }
@Override @Override
@ -194,4 +202,14 @@ public class GradingReportServiceImpl implements GradingReportService {
private static boolean isBlank(final String str) { private static boolean isBlank(final String str) {
return str == null || str.isBlank(); return str == null || str.isBlank();
} }
@Override
public List<ProjectType> getProjectTypes() {
return projectTypeService.findWithModule(ProjectModule.GRADING);
}
@Override
public GradingReportTemplate getCurrentTemplate(ProjectType projectType) {
return gradingReportTemplateRepo.getCurrentTemplate(projectType, LocalDate.now(clock));
}
} }

View File

@ -76,6 +76,14 @@ public class GradingReportTemplate extends DomainObject {
return this.id; return this.id;
} }
public LocalDate getValidFrom() {
return validFrom;
}
public void setValidFrom(LocalDate validFrom) {
this.validFrom = validFrom;
}
@Override @Override
public boolean equals(final Object o) { public boolean equals(final Object o) {
if (o == this) return true; if (o == this) return true;

View File

@ -2,7 +2,12 @@ package se.su.dsv.scipro.report;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import se.su.dsv.scipro.project.Project; import se.su.dsv.scipro.project.Project;
import se.su.dsv.scipro.system.ProjectType;
import java.time.LocalDate;
public interface GradingReportTemplateRepo extends JpaRepository<GradingReportTemplate, Long> { public interface GradingReportTemplateRepo extends JpaRepository<GradingReportTemplate, Long> {
GradingReportTemplate getTemplate(Project project); GradingReportTemplate getTemplate(Project project);
GradingReportTemplate getCurrentTemplate(ProjectType projectType, LocalDate now);
} }

View File

@ -8,6 +8,7 @@ import se.su.dsv.scipro.system.GenericRepo;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import jakarta.inject.Provider; import jakarta.inject.Provider;
import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManager;
import se.su.dsv.scipro.system.ProjectType;
import java.time.LocalDate; import java.time.LocalDate;
@ -19,13 +20,18 @@ public class GradingReportTemplateRepoImpl extends GenericRepo<GradingReportTemp
@Override @Override
public GradingReportTemplate getTemplate(Project project) { public GradingReportTemplate getTemplate(Project project) {
return getCurrentTemplate(project.getProjectType(), project.getStartDate());
}
@Override
public GradingReportTemplate getCurrentTemplate(ProjectType projectType, LocalDate now) {
QGradingReportTemplate template = QGradingReportTemplate.gradingReportTemplate; QGradingReportTemplate template = QGradingReportTemplate.gradingReportTemplate;
// find the latest template that is valid for the project // find the latest template that is valid for the project
JPQLQuery<LocalDate> validFrom = JPAExpressions JPQLQuery<LocalDate> validFrom = JPAExpressions
.select(template.validFrom.max()) .select(template.validFrom.max())
.from(template) .from(template)
.where(template.projectType.eq(project.getProjectType()) .where(template.projectType.eq(projectType)
.and(template.validFrom.loe(project.getStartDate()))); .and(template.validFrom.loe(now)));
return findOne(template.projectType.eq(project.getProjectType()).and(template.validFrom.eq(validFrom))); return findOne(template.projectType.eq(projectType).and(template.validFrom.eq(validFrom)));
} }
} }

View File

@ -14,6 +14,8 @@ import org.apache.wicket.util.convert.converter.LocalDateConverter;
import org.apache.wicket.util.convert.converter.LocalDateTimeConverter; import org.apache.wicket.util.convert.converter.LocalDateTimeConverter;
import se.su.dsv.scipro.activityplan.*; import se.su.dsv.scipro.activityplan.*;
import se.su.dsv.scipro.admin.pages.*; import se.su.dsv.scipro.admin.pages.*;
import se.su.dsv.scipro.admin.pages.grading.AdminGradingTemplatePage;
import se.su.dsv.scipro.admin.pages.grading.AdminGradingTemplatesOverviewPage;
import se.su.dsv.scipro.admin.pages.settings.*; import se.su.dsv.scipro.admin.pages.settings.*;
import se.su.dsv.scipro.applicationperiod.AdminEditApplicationPeriodExemptionsPage; import se.su.dsv.scipro.applicationperiod.AdminEditApplicationPeriodExemptionsPage;
import se.su.dsv.scipro.applicationperiod.AdminEditApplicationPeriodPage; import se.su.dsv.scipro.applicationperiod.AdminEditApplicationPeriodPage;
@ -282,6 +284,8 @@ public class SciProApplication extends LifecycleManagedWebApplication {
mountPage("admin/project/survey", AdminSurveyPage.class); mountPage("admin/project/survey", AdminSurveyPage.class);
mountPage("admin/project/reviewer", AdminAssignReviewerPage.class); mountPage("admin/project/reviewer", AdminAssignReviewerPage.class);
mountPage("admin/project/reviewer/capacity", AdminReviewerCapacityManagementPage.class); mountPage("admin/project/reviewer/capacity", AdminReviewerCapacityManagementPage.class);
mountPage("admin/project/grading/template", AdminGradingTemplatesOverviewPage.class);
mountPage("admin/project/grading/template/view", AdminGradingTemplatePage.class);
mountPage("admin/edit", AdminEditProjectPage.class); mountPage("admin/edit", AdminEditProjectPage.class);
mountPage("admin/finalseminars", AdminFinalSeminarPage.class); mountPage("admin/finalseminars", AdminFinalSeminarPage.class);
mountPage("admin/finalseminars/exemptions", AdminFinalSeminarExemptionPage.class); mountPage("admin/finalseminars/exemptions", AdminFinalSeminarExemptionPage.class);

View File

@ -1,6 +1,8 @@
package se.su.dsv.scipro.admin.pages; package se.su.dsv.scipro.admin.pages;
import se.su.dsv.scipro.activityplan.AdminActivityPlanTemplatesPage; import se.su.dsv.scipro.activityplan.AdminActivityPlanTemplatesPage;
import se.su.dsv.scipro.admin.pages.grading.AdminGradingTemplatesOverviewPage;
import se.su.dsv.scipro.admin.pages.grading.MenuHighlightGradingTemplates;
import se.su.dsv.scipro.checklists.AdminChecklistPage; import se.su.dsv.scipro.checklists.AdminChecklistPage;
import se.su.dsv.scipro.components.AbstractMenuPanel; import se.su.dsv.scipro.components.AbstractMenuPanel;
import se.su.dsv.scipro.components.menuhighlighting.MenuHighlightAdminActivityPlanTemplates; import se.su.dsv.scipro.components.menuhighlighting.MenuHighlightAdminActivityPlanTemplates;
@ -28,7 +30,8 @@ public class AbstractAdminProjectPage extends AbstractAdminPage {
new MenuItem("Checklist templates", AdminChecklistPage.class, MenuHighlightAdminChecklist.class), new MenuItem("Checklist templates", AdminChecklistPage.class, MenuHighlightAdminChecklist.class),
new MenuItem("Non work days", NonWorkDaysPage.class), new MenuItem("Non work days", NonWorkDaysPage.class),
new MenuItem("Survey", AdminSurveyPage.class), new MenuItem("Survey", AdminSurveyPage.class),
new MenuItem("Reviewer targets", AdminReviewerCapacityManagementPage.class) new MenuItem("Reviewer targets", AdminReviewerCapacityManagementPage.class),
new MenuItem("Grading templates", AdminGradingTemplatesOverviewPage.class, MenuHighlightGradingTemplates.class)
); );
} }

View File

@ -0,0 +1,19 @@
package se.su.dsv.scipro.admin.pages.grading;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import se.su.dsv.scipro.admin.pages.AbstractAdminProjectPage;
import se.su.dsv.scipro.report.GradingReportTemplate;
public class AdminGradingTemplatePage extends AbstractAdminProjectPage implements MenuHighlightGradingTemplates {
private static final String TEMPLATE_ID_PARAMETER = "id";
public AdminGradingTemplatePage(PageParameters parameters) {
long templateId = parameters.get(TEMPLATE_ID_PARAMETER).toLong();
}
public static PageParameters getPageParameters(GradingReportTemplate gradingReportTemplate) {
PageParameters pageParameters = new PageParameters();
pageParameters.add(TEMPLATE_ID_PARAMETER, gradingReportTemplate.getId());
return pageParameters;
}
}

View File

@ -0,0 +1,125 @@
<?xml version="1.0" encoding="UTF-8"?>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:wicket="http://wicket.apache.org">
<head>
<wicket:remove>
<link rel="stylesheet" type="text/css" href="../../../../../../../../webapp/css/scipro_m.css">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
</wicket:remove>
</head>
<body>
<div class="container-fluid">
<wicket:extend>
<h1>Grading report template management</h1>
<p>
Here you can manage the grading templates that are used to grade projects.
</p>
<p>
A template consists of multiple criteria, each with a description and the maximum points that can be scored on it.
The point sum total conversion to grade letters is also defined in the template.
</p>
<p>
Each template has a period during which it is in use. The period is defined by a start date,
and it remains current until a new template takes over. There will only ever be one template
current for any given date.
The template that is selected to be used for grading a project is the one that was current the date
the project started.
</p>
<p>
Once a template has become current it is <strong><em>no longer possible</em></strong> to edit it.
</p>
<div class="btn-group mb-3" role="group">
<button type="button" class="btn btn-outline-primary">Create new template</button>
<div class="btn-group" role="group">
<button type="button" class="btn btn-outline-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
</button>
<ul class="dropdown-menu">
<li><button class="dropdown-item" href="#">Create new based on another template</button></li>
</ul>
</div>
</div>
<div class="mb-3" wicket:id="project_types">
<h2 wicket:id="name">Bachelor</h2>
<div class="hstack gap-3 flex-wrap align-items-start" wicket:id="templates">
<div class="card">
<div class="card-header bg-info-subtle">
Upcoming template
</div>
<div class="card-body">
<dl>
<dt>Valid from</dt>
<dd>2024-10-31</dd>
<dt>Note</dt>
<dd>Added a new criteria U14</dd>
</dl>
<span class="card-text">
<a href="#">View / Edit</a>
</span>
</div>
</div>
<div class="card">
<div class="card-header bg-success-subtle">
Current template
</div>
<div class="card-body">
<dl>
<dt>Valid from</dt>
<dd wicket:id="valid_from">2024-01-01</dd>
<wicket:enclosure>
<dt>Note</dt>
<dd wicket:id="note">Change ethics criteria to include AI</dd>
</wicket:enclosure>
</dl>
<span class="card-text">
<a href="#" wicket:id="view_template">View</a>
</span>
</div>
</div>
<div class="card">
<div class="card-header">
Older templates
</div>
<div class="card-body hstack gap-2">
<div class="mw-100">
<dl>
<dt>Valid</dt>
<dd>2023-01-01 to 2023-12-41</dd>
<dt>Note</dt>
<dd>Change ethics criteria to include AI</dd>
</dl>
<span class="card-text">
<a href="#">View</a>
</span>
</div>
<div class="vr"></div>
<div>
<dl>
<dt>Valid</dt>
<dd>2001-01-01 to 2022-12-31</dd>
<dt>Note</dt>
<dd>Change ethics criteria to include AI</dd>
</dl>
<span class="card-text">
<a href="#">View</a>
</span>
</div>
</div>
</div>
</div>
</div>
</wicket:extend>
</div>
</body>
</html>

View File

@ -0,0 +1,48 @@
package se.su.dsv.scipro.admin.pages.grading;
import jakarta.inject.Inject;
import org.apache.wicket.markup.html.GenericWebMarkupContainer;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.link.BookmarkablePageLink;
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.admin.pages.AbstractAdminProjectPage;
import se.su.dsv.scipro.components.NonEmptyLabel;
import se.su.dsv.scipro.grading.GradingReportTemplateService;
import se.su.dsv.scipro.report.GradingReportTemplate;
import se.su.dsv.scipro.system.ProjectType;
import java.util.List;
public class AdminGradingTemplatesOverviewPage extends AbstractAdminProjectPage {
@Inject
private GradingReportTemplateService gradingReportTemplateService;
public AdminGradingTemplatesOverviewPage() {
LoadableDetachableModel<List<ProjectType>> projectTypes =
LoadableDetachableModel.of(gradingReportTemplateService::getProjectTypes);
add(new ListView<>("project_types", projectTypes) {
@Override
protected void populateItem(ListItem<ProjectType> item) {
item.add(new Label("name", item.getModel().map(ProjectType::getName)));
item.add(new TemplatesPanel("templates", item.getModel()));
}
});
}
private class TemplatesPanel extends GenericWebMarkupContainer<ProjectType> {
public TemplatesPanel(String id, IModel<ProjectType> model) {
super(id, model);
IModel<GradingReportTemplate> currentTemplate = model.map(gradingReportTemplateService::getCurrentTemplate);
add(new Label("valid_from", currentTemplate.map(GradingReportTemplate::getValidFrom)));
add(new NonEmptyLabel("note", Model.of("")));
add(new BookmarkablePageLink<>("view_template", AdminGradingTemplatePage.class, AdminGradingTemplatePage.getPageParameters(currentTemplate.getObject())));
}
}
}

View File

@ -0,0 +1,6 @@
package se.su.dsv.scipro.admin.pages.grading;
import se.su.dsv.scipro.components.menuhighlighting.MenuHighlight;
public interface MenuHighlightGradingTemplates extends MenuHighlight {
}

View File

@ -0,0 +1,19 @@
package se.su.dsv.scipro.components;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.model.IModel;
/**
* A basic {@link Label} that hides itself if the rendered model object {@link String#isBlank() is blank}
*/
public class NonEmptyLabel extends Label {
public NonEmptyLabel(String id, IModel<?> model) {
super(id, model);
}
@Override
protected void onConfigure() {
super.onConfigure();
setVisible(!getDefaultModelObjectAsString().isBlank());
}
}