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
14 changed files with 280 additions and 1 deletions
Showing only changes of commit f9af18a49c - Show all commits

View File

@ -1,6 +1,10 @@
package se.su.dsv.scipro.grading;
import se.su.dsv.scipro.report.DuplicateDateException;
import se.su.dsv.scipro.report.GradingReportTemplate;
import se.su.dsv.scipro.report.ValidDateMustBeInTheFutureException;
import se.su.dsv.scipro.report.NoSuchTemplateException;
import se.su.dsv.scipro.report.TemplateLockedException;
import se.su.dsv.scipro.system.ProjectType;
import java.time.LocalDate;
@ -22,4 +26,11 @@ public interface GradingReportTemplateService {
LocalDate getEndDate(GradingReportTemplate gradingReportTemplate);
List<GradingReportTemplate> getUpcomingTemplates(ProjectType projectType);
GradingReportTemplate update(long templateId, GradingReportTemplateUpdate update)
throws
ValidDateMustBeInTheFutureException,
NoSuchTemplateException,
DuplicateDateException,
TemplateLockedException;
}

View File

@ -0,0 +1,54 @@
package se.su.dsv.scipro.grading;
import jakarta.annotation.Nullable;
import java.time.LocalDate;
import java.util.List;
import java.util.Objects;
public record GradingReportTemplateUpdate(
LocalDate validFrom,
@Nullable String note,
String defaultGrade,
List<Grade> grades,
List<Criteria> criteria)
{
public GradingReportTemplateUpdate {
Objects.requireNonNull(validFrom, "Valid from must not be null");
Objects.requireNonNull(defaultGrade, "Default grade must not be null");
Objects.requireNonNull(grades, "Grades must not be null");
Objects.requireNonNull(criteria, "Criteria must not be null");
for (Grade grade1 : grades) {
for (Grade grade2 : grades) {
if (grade1 != grade2 && grade1.minimumPoints() == grade2.minimumPoints()) {
throw new IllegalArgumentException("Duplicate minimum points on grades: %s and %s".formatted(
grade1.grade(),
grade2.grade()));
}
}
}
}
public record Grade(String grade, int minimumPoints) {
public Grade {
Objects.requireNonNull(grade, "Grade must not be null");
}
}
public record Criteria(LocalizedString title, @Nullable Flag flag, List<Requirement> requirements)
{
public enum Flag {OPPOSITION, REFLECTION}
public Criteria {
Objects.requireNonNull(title, "Title must not be null");
Objects.requireNonNull(requirements, "Requirements must not be null");
}
public record Requirement(int points, LocalizedString description) {
public Requirement {
Objects.requireNonNull(description, "Description must not be null");
}
}
}
}

View File

@ -0,0 +1,10 @@
package se.su.dsv.scipro.grading;
import java.util.Objects;
public record LocalizedString(String english, String swedish) {
public LocalizedString {
Objects.requireNonNull(english, "English must not be null");
Objects.requireNonNull(swedish, "Swedish must not be null");
}
}

View File

@ -0,0 +1,23 @@
package se.su.dsv.scipro.report;
import se.su.dsv.scipro.system.ProjectType;
import java.time.LocalDate;
public class DuplicateDateException extends Throwable {
ansv7779 marked this conversation as resolved Outdated

Should extend Exception instead of Throwable

Should extend Exception instead of Throwable
private final LocalDate validFrom;
private final ProjectType projectType;
public DuplicateDateException(LocalDate validFrom, ProjectType projectType) {
this.validFrom = validFrom;
this.projectType = projectType;
}
public LocalDate validFrom() {
return validFrom;
}
public ProjectType projectType() {
return projectType;
}
}

View File

@ -5,6 +5,7 @@ import com.google.inject.persist.Transactional;
import se.su.dsv.scipro.finalseminar.FinalSeminarOpposition;
import se.su.dsv.scipro.grading.GradingBasis;
import se.su.dsv.scipro.grading.GradingReportTemplateService;
import se.su.dsv.scipro.grading.GradingReportTemplateUpdate;
import se.su.dsv.scipro.grading.ThesisSubmissionHistoryService;
import se.su.dsv.scipro.project.Project;
import se.su.dsv.scipro.system.Language;
@ -237,4 +238,37 @@ public class GradingReportServiceImpl implements GradingReportTemplateService, G
return gradingReportTemplateRepo.getTemplatesValidAfter(projectType, current.getValidFrom());
}
}
@Override
public GradingReportTemplate update(long templateId, GradingReportTemplateUpdate update)
throws ValidDateMustBeInTheFutureException,
NoSuchTemplateException,
DuplicateDateException,
TemplateLockedException
{
LocalDate today = LocalDate.now(clock);
if (!update.validFrom().isAfter(today)) {
throw new ValidDateMustBeInTheFutureException(update.validFrom(), today.plusDays(1));
}
GradingReportTemplate template = gradingReportTemplateRepo.getTemplateById(templateId);
if (template == null) {
throw new NoSuchTemplateException();
}
GradingReportTemplate currentTemplate = gradingReportTemplateRepo.getCurrentTemplate(
template.getProjectType(),
update.validFrom());
if (currentTemplate.getId() != templateId &&
Objects.equals(currentTemplate.getValidFrom(), update.validFrom()))
{
throw new DuplicateDateException(update.validFrom(), template.getProjectType());
}
if (!template.getValidFrom().isAfter(today)) {
throw new TemplateLockedException(template.getValidFrom());
}
return gradingReportTemplateRepo.updateTemplate(templateId, update);
}
}

View File

@ -67,7 +67,7 @@ public class GradingReportTemplate extends DomainObject {
return gradingCriterionTemplate;
}
public Iterable<GradingCriterionTemplate> getCriteria() {
public Collection<GradingCriterionTemplate> getCriteria() {
return criteria;
}

View File

@ -1,6 +1,7 @@
package se.su.dsv.scipro.report;
import org.springframework.data.jpa.repository.JpaRepository;
import se.su.dsv.scipro.grading.GradingReportTemplateUpdate;
import se.su.dsv.scipro.project.Project;
import se.su.dsv.scipro.system.ProjectType;
@ -15,4 +16,8 @@ public interface GradingReportTemplateRepo extends JpaRepository<GradingReportTe
GradingReportTemplate getNextTemplate(GradingReportTemplate gradingReportTemplate);
List<GradingReportTemplate> getTemplatesValidAfter(ProjectType projectType, LocalDate date);
GradingReportTemplate getTemplateById(long templateId);
GradingReportTemplate updateTemplate(long templateId, GradingReportTemplateUpdate update);
}

View File

@ -1,7 +1,9 @@
package se.su.dsv.scipro.report;
import com.google.inject.persist.Transactional;
import com.querydsl.jpa.JPAExpressions;
import com.querydsl.jpa.JPQLQuery;
import se.su.dsv.scipro.grading.GradingReportTemplateUpdate;
import se.su.dsv.scipro.project.Project;
import se.su.dsv.scipro.system.GenericRepo;
@ -53,4 +55,16 @@ public class GradingReportTemplateRepoImpl extends GenericRepo<GradingReportTemp
return findAll(QGradingReportTemplate.gradingReportTemplate.projectType.eq(projectType)
.and(QGradingReportTemplate.gradingReportTemplate.validFrom.gt(date)));
}
@Override
public GradingReportTemplate getTemplateById(long templateId) {
return findOne(templateId);
}
@Override
@Transactional
public GradingReportTemplate updateTemplate(long templateId, GradingReportTemplateUpdate update) {
GradingReportTemplate gradingReportTemplate = findOne(templateId);
return gradingReportTemplate;
}
}

View File

@ -0,0 +1,4 @@
package se.su.dsv.scipro.report;
public class NoSuchTemplateException extends Exception {
}

View File

@ -0,0 +1,15 @@
package se.su.dsv.scipro.report;
import java.time.LocalDate;
public class TemplateLockedException extends Exception {
private final LocalDate becameValidAt;
public TemplateLockedException(LocalDate becameValidAt) {
this.becameValidAt = becameValidAt;
}
public LocalDate becameValidAt() {
return becameValidAt;
}
}

View File

@ -0,0 +1,21 @@
package se.su.dsv.scipro.report;
import java.time.LocalDate;
public class ValidDateMustBeInTheFutureException extends Exception {
private final LocalDate validFrom;
private final LocalDate earliestAllowedValidFrom;
public ValidDateMustBeInTheFutureException(LocalDate validFrom, LocalDate earliestAllowedValidFrom) {
this.validFrom = validFrom;
this.earliestAllowedValidFrom = earliestAllowedValidFrom;
}
public LocalDate validFrom() {
return validFrom;
}
public LocalDate earliestAllowedValidFrom() {
return earliestAllowedValidFrom;
}
}

View File

@ -2,6 +2,7 @@
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:wicket="http://wicket.apache.org">
<body>
<wicket:extend>
<div wicket:id="feedback"></div>
<form wicket:id="form">
<div wicket:id="editing" class="mb-3"></div>
<div class="position-sticky bottom-0 bg-white p-3 border line-length-limit">

View File

@ -1,15 +1,29 @@
package se.su.dsv.scipro.admin.pages.grading;
import jakarta.inject.Inject;
import org.apache.wicket.RestartResponseException;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.markup.html.panel.FeedbackPanel;
import org.apache.wicket.model.Model;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import se.su.dsv.scipro.admin.pages.AbstractAdminProjectPage;
import se.su.dsv.scipro.grading.GradingReportTemplateService;
import se.su.dsv.scipro.grading.GradingReportTemplateUpdate;
import se.su.dsv.scipro.grading.LocalizedString;
import se.su.dsv.scipro.report.DuplicateDateException;
import se.su.dsv.scipro.report.GradingReportTemplate;
import se.su.dsv.scipro.report.ValidDateMustBeInTheFutureException;
import se.su.dsv.scipro.report.NoSuchTemplateException;
import se.su.dsv.scipro.report.TemplateLockedException;
import java.util.ArrayList;
import java.util.List;
public class AdminGradingTemplateEditPage extends AbstractAdminProjectPage implements MenuHighlightGradingTemplates {
private static final String GRADING_REPORT_TEMPLATE_ID_PARAMETER = "id";
public static final GradingReportTemplateUpdate.Criteria.Requirement ZERO_REQUIREMENT = new GradingReportTemplateUpdate.Criteria.Requirement(
0,
new LocalizedString("", ""));
@Inject
GradingReportTemplateService gradingReportTemplateService;
@ -23,10 +37,27 @@ public class AdminGradingTemplateEditPage extends AbstractAdminProjectPage imple
editingGradingTemplate = new EditingGradingTemplate(template);
add(new FeedbackPanel("feedback"));
Form<EditingGradingTemplate> form = new Form<>("form") {
@Override
protected void onSubmit() {
super.onSubmit();
try {
GradingReportTemplateUpdate update = toUpdate(
editingGradingTemplate);
gradingReportTemplateService.update(id, update);
success(getString("template_updated"));
} catch (ValidDateMustBeInTheFutureException e) {
error(getString("valid_from_must_be_in_the_future", () -> e));
} catch (NoSuchTemplateException e) {
throw new RestartResponseException(AdminGradingTemplatesOverviewPage.class);
} catch (DuplicateDateException e) {
error(getString("another_template_exists_for_the_given_date_date", () -> e));
} catch (TemplateLockedException e) {
error(getString("template_is_locked", () -> e));
}
System.out.println(editingGradingTemplate);
}
};
@ -35,6 +66,58 @@ public class AdminGradingTemplateEditPage extends AbstractAdminProjectPage imple
add(form);
}
private GradingReportTemplateUpdate toUpdate(EditingGradingTemplate editingGradingTemplate) {
List<GradingReportTemplateUpdate.Grade> grades = editingGradingTemplate
.getGradeLimits()
.getGradeLimits()
.stream()
.map(this::toGrade)
.toList();
List<GradingReportTemplateUpdate.Criteria> criteria = editingGradingTemplate
.getCriteria()
.stream()
.map(this::toCriteria)
.toList();
return new GradingReportTemplateUpdate(
editingGradingTemplate.getValidFrom(),
editingGradingTemplate.getNote(),
editingGradingTemplate.getGradeLimits().getDefaultGrade(),
grades,
criteria);
}
private GradingReportTemplateUpdate.Criteria toCriteria(EditingGradingTemplate.Criteria criteria) {
ArrayList<GradingReportTemplateUpdate.Criteria.Requirement> requirements = new ArrayList<>();
requirements.add(ZERO_REQUIREMENT);
for (int i = 0; i < criteria.getPoints().size(); i++) {
EditingGradingTemplate.Criteria.Point point = criteria.getPoints().get(i);
requirements.add(new GradingReportTemplateUpdate.Criteria.Requirement(
i + 1,
new LocalizedString(point.getRequirementEn(), point.getRequirementSv())));
}
return new GradingReportTemplateUpdate.Criteria(
new LocalizedString(criteria.getTitleEn(), criteria.getTitleSv()),
getFlag(criteria),
requirements);
}
private static GradingReportTemplateUpdate.Criteria.Flag getFlag(EditingGradingTemplate.Criteria criteria) {
if (criteria.getFlag() == null) {
return null;
}
return switch (criteria.getFlag()) {
case OPPOSITION -> GradingReportTemplateUpdate.Criteria.Flag.OPPOSITION;
case REFLECTION -> GradingReportTemplateUpdate.Criteria.Flag.REFLECTION;
//case null -> null; sigh old versions of Java
};
}
private GradingReportTemplateUpdate.Grade toGrade(GradeLimits.GradeLimit gradeLimit) {
return new GradingReportTemplateUpdate.Grade(
gradeLimit.getGrade(),
gradeLimit.getLowerLimit());
}
public static PageParameters getPageParameters(GradingReportTemplate gradingReportTemplate) {
PageParameters pageParameters = new PageParameters();
pageParameters.add(GRADING_REPORT_TEMPLATE_ID_PARAMETER, gradingReportTemplate.getId());

View File

@ -0,0 +1,4 @@
template_updated=Template updated
valid_from_must_be_in_the_future=The templates valid date must be in the future. The given date was ${validFrom} but it must be at least ${earliestAllowedValidFrom}.
another_template_exists_for_the_given_date_date=There is already another ${projectType.name} template that becomes valid at ${validFrom}, please pick another date.
template_is_locked=You can not edit templates that have become current. The template you are trying to edit became current at ${validFrom}.