diff --git a/core/src/main/java/se/su/dsv/scipro/grading/GradingReportTemplateService.java b/core/src/main/java/se/su/dsv/scipro/grading/GradingReportTemplateService.java index bc678a0455..42c03c8a55 100644 --- a/core/src/main/java/se/su/dsv/scipro/grading/GradingReportTemplateService.java +++ b/core/src/main/java/se/su/dsv/scipro/grading/GradingReportTemplateService.java @@ -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; } diff --git a/core/src/main/java/se/su/dsv/scipro/grading/GradingReportTemplateUpdate.java b/core/src/main/java/se/su/dsv/scipro/grading/GradingReportTemplateUpdate.java new file mode 100644 index 0000000000..357ea9a04e --- /dev/null +++ b/core/src/main/java/se/su/dsv/scipro/grading/GradingReportTemplateUpdate.java @@ -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"); + } + } + } +} diff --git a/core/src/main/java/se/su/dsv/scipro/grading/LocalizedString.java b/core/src/main/java/se/su/dsv/scipro/grading/LocalizedString.java new file mode 100644 index 0000000000..cb777b5c65 --- /dev/null +++ b/core/src/main/java/se/su/dsv/scipro/grading/LocalizedString.java @@ -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"); + } +} diff --git a/core/src/main/java/se/su/dsv/scipro/report/DuplicateDateException.java b/core/src/main/java/se/su/dsv/scipro/report/DuplicateDateException.java new file mode 100644 index 0000000000..6a1959586d --- /dev/null +++ b/core/src/main/java/se/su/dsv/scipro/report/DuplicateDateException.java @@ -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 { + 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; + } +} diff --git a/core/src/main/java/se/su/dsv/scipro/report/GradingReportServiceImpl.java b/core/src/main/java/se/su/dsv/scipro/report/GradingReportServiceImpl.java index 83f61b9362..7c06a09631 100644 --- a/core/src/main/java/se/su/dsv/scipro/report/GradingReportServiceImpl.java +++ b/core/src/main/java/se/su/dsv/scipro/report/GradingReportServiceImpl.java @@ -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); + } } diff --git a/core/src/main/java/se/su/dsv/scipro/report/GradingReportTemplate.java b/core/src/main/java/se/su/dsv/scipro/report/GradingReportTemplate.java index 9816ddacdb..0bd1da6bfb 100644 --- a/core/src/main/java/se/su/dsv/scipro/report/GradingReportTemplate.java +++ b/core/src/main/java/se/su/dsv/scipro/report/GradingReportTemplate.java @@ -67,7 +67,7 @@ public class GradingReportTemplate extends DomainObject { return gradingCriterionTemplate; } - public Iterable<GradingCriterionTemplate> getCriteria() { + public Collection<GradingCriterionTemplate> getCriteria() { return criteria; } diff --git a/core/src/main/java/se/su/dsv/scipro/report/GradingReportTemplateRepo.java b/core/src/main/java/se/su/dsv/scipro/report/GradingReportTemplateRepo.java index 0f5a8f2332..320f8150e4 100644 --- a/core/src/main/java/se/su/dsv/scipro/report/GradingReportTemplateRepo.java +++ b/core/src/main/java/se/su/dsv/scipro/report/GradingReportTemplateRepo.java @@ -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); } diff --git a/core/src/main/java/se/su/dsv/scipro/report/GradingReportTemplateRepoImpl.java b/core/src/main/java/se/su/dsv/scipro/report/GradingReportTemplateRepoImpl.java index 4fd2548fa2..6da82c20d6 100644 --- a/core/src/main/java/se/su/dsv/scipro/report/GradingReportTemplateRepoImpl.java +++ b/core/src/main/java/se/su/dsv/scipro/report/GradingReportTemplateRepoImpl.java @@ -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; + } } \ No newline at end of file diff --git a/core/src/main/java/se/su/dsv/scipro/report/NoSuchTemplateException.java b/core/src/main/java/se/su/dsv/scipro/report/NoSuchTemplateException.java new file mode 100644 index 0000000000..0013f826d2 --- /dev/null +++ b/core/src/main/java/se/su/dsv/scipro/report/NoSuchTemplateException.java @@ -0,0 +1,4 @@ +package se.su.dsv.scipro.report; + +public class NoSuchTemplateException extends Exception { +} diff --git a/core/src/main/java/se/su/dsv/scipro/report/TemplateLockedException.java b/core/src/main/java/se/su/dsv/scipro/report/TemplateLockedException.java new file mode 100644 index 0000000000..217cdff1da --- /dev/null +++ b/core/src/main/java/se/su/dsv/scipro/report/TemplateLockedException.java @@ -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; + } +} diff --git a/core/src/main/java/se/su/dsv/scipro/report/ValidDateMustBeInTheFutureException.java b/core/src/main/java/se/su/dsv/scipro/report/ValidDateMustBeInTheFutureException.java new file mode 100644 index 0000000000..dcaee0092b --- /dev/null +++ b/core/src/main/java/se/su/dsv/scipro/report/ValidDateMustBeInTheFutureException.java @@ -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; + } +} diff --git a/view/src/main/java/se/su/dsv/scipro/admin/pages/grading/AdminGradingTemplateEditPage.html b/view/src/main/java/se/su/dsv/scipro/admin/pages/grading/AdminGradingTemplateEditPage.html index c61838afca..9e28766880 100644 --- a/view/src/main/java/se/su/dsv/scipro/admin/pages/grading/AdminGradingTemplateEditPage.html +++ b/view/src/main/java/se/su/dsv/scipro/admin/pages/grading/AdminGradingTemplateEditPage.html @@ -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"> diff --git a/view/src/main/java/se/su/dsv/scipro/admin/pages/grading/AdminGradingTemplateEditPage.java b/view/src/main/java/se/su/dsv/scipro/admin/pages/grading/AdminGradingTemplateEditPage.java index eb61e470a1..d9ab2a9ab7 100644 --- a/view/src/main/java/se/su/dsv/scipro/admin/pages/grading/AdminGradingTemplateEditPage.java +++ b/view/src/main/java/se/su/dsv/scipro/admin/pages/grading/AdminGradingTemplateEditPage.java @@ -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()); diff --git a/view/src/main/java/se/su/dsv/scipro/admin/pages/grading/AdminGradingTemplateEditPage.utf8.properties b/view/src/main/java/se/su/dsv/scipro/admin/pages/grading/AdminGradingTemplateEditPage.utf8.properties new file mode 100644 index 0000000000..a9420cf6bd --- /dev/null +++ b/view/src/main/java/se/su/dsv/scipro/admin/pages/grading/AdminGradingTemplateEditPage.utf8.properties @@ -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}.