diff --git a/view/src/main/java/se/su/dsv/scipro/admin/pages/grading/AdminGradingTemplateCreationPage.html b/view/src/main/java/se/su/dsv/scipro/admin/pages/grading/AdminGradingTemplateCreationPage.html index 8fd44e66ab..d300cde333 100644 --- a/view/src/main/java/se/su/dsv/scipro/admin/pages/grading/AdminGradingTemplateCreationPage.html +++ b/view/src/main/java/se/su/dsv/scipro/admin/pages/grading/AdminGradingTemplateCreationPage.html @@ -17,7 +17,7 @@ <div wicket:id="grading_template_component_panel"></div> - <div class="position-sticky bottom-0 bg-white p-3 border line-length-limit"> + <div wicket:id="button_container" class="position-sticky bottom-0 bg-white p-3 border line-length-limit"> <button type="submit" class="btn btn-primary">Create</button> </div> </form> diff --git a/view/src/main/java/se/su/dsv/scipro/admin/pages/grading/AdminGradingTemplateCreationPage.java b/view/src/main/java/se/su/dsv/scipro/admin/pages/grading/AdminGradingTemplateCreationPage.java index c14d4925db..0338617189 100644 --- a/view/src/main/java/se/su/dsv/scipro/admin/pages/grading/AdminGradingTemplateCreationPage.java +++ b/view/src/main/java/se/su/dsv/scipro/admin/pages/grading/AdminGradingTemplateCreationPage.java @@ -3,6 +3,7 @@ package se.su.dsv.scipro.admin.pages.grading; import jakarta.inject.Inject; import org.apache.wicket.RestartResponseException; import org.apache.wicket.ajax.AjaxRequestTarget; +import org.apache.wicket.markup.html.WebMarkupContainer; import org.apache.wicket.markup.html.form.Form; import org.apache.wicket.markup.html.form.LambdaChoiceRenderer; import org.apache.wicket.markup.html.panel.FeedbackPanel; @@ -29,6 +30,7 @@ public class AdminGradingTemplateCreationPage extends AbstractAdminProjectPage i private final IModel<ProjectType> projectTypeModel; private EditingGradingTemplate editingGradingTemplateModel; + private final WebMarkupContainer buttonContainer; public AdminGradingTemplateCreationPage() { projectTypeModel = new DetachableServiceModel<>(projectTypeService); @@ -57,6 +59,11 @@ public class AdminGradingTemplateCreationPage extends AbstractAdminProjectPage i form.setOutputMarkupId(true); add(form); + buttonContainer = new WebMarkupContainer("button_container"); + buttonContainer.setOutputMarkupPlaceholderTag(true); + buttonContainer.setVisible(false); + form.add(buttonContainer); + form.add(new AjaxDropDownChoice<>( "project_type", projectTypeModel, @@ -64,7 +71,8 @@ public class AdminGradingTemplateCreationPage extends AbstractAdminProjectPage i new LambdaChoiceRenderer<>(ProjectType::getName, ProjectType::getId)) { @Override public void onNewSelection(AjaxRequestTarget target, ProjectType objectSelected) { - target.add(form); + buttonContainer.setVisible(true); + target.add(form, buttonContainer); } }); 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 9e28766880..cd7a505d25 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 @@ -3,10 +3,18 @@ <body> <wicket:extend> <div wicket:id="feedback"></div> + <div class="mb-3 lead"> + <wicket:message key="project_type_name_editing"> + <span wicket:id="project_type_name"></span> + </wicket:message> + </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"> + <div class="position-sticky bottom-0 bg-white p-3 border line-length-limit hstack"> <button type="submit" class="btn btn-primary">Save</button> + <span wicket:id="unsaved_changes_alert" class="text-danger flex-grow-1 text-center" role="alert"> + <wicket:message key="unsaved_changes"/> + </span> </div> </form> </wicket:extend> 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 da4a0b42a7..d2ad546e0f 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 @@ -2,9 +2,13 @@ package se.su.dsv.scipro.admin.pages.grading; import jakarta.inject.Inject; import org.apache.wicket.RestartResponseException; +import org.apache.wicket.ajax.AjaxRequestTarget; +import org.apache.wicket.markup.html.WebMarkupContainer; +import org.apache.wicket.markup.html.basic.Label; 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.model.IModel; +import org.apache.wicket.model.LoadableDetachableModel; import org.apache.wicket.request.mapper.parameter.PageParameters; import se.su.dsv.scipro.admin.pages.AbstractAdminProjectPage; import se.su.dsv.scipro.grading.GradingReportTemplateService; @@ -12,9 +16,10 @@ 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 se.su.dsv.scipro.report.ValidDateMustBeInTheFutureException; +import se.su.dsv.scipro.system.ProjectType; import java.util.ArrayList; import java.util.List; @@ -25,6 +30,8 @@ public class AdminGradingTemplateEditPage extends AbstractAdminProjectPage imple 0, new LocalizedString("", "")); + private final WebMarkupContainer unsavedChangesAlert; + @Inject GradingReportTemplateService gradingReportTemplateService; @@ -38,6 +45,8 @@ public class AdminGradingTemplateEditPage extends AbstractAdminProjectPage imple editingGradingTemplate = new EditingGradingTemplate(template); add(new FeedbackPanel("feedback")); + IModel<GradingReportTemplate> model = LoadableDetachableModel.of(() -> gradingReportTemplateService.getTemplate(id)); + add(new Label("project_type_name", model.map(GradingReportTemplate::getProjectType).map(ProjectType::getName))); Form<EditingGradingTemplate> form = new Form<>("form") { @Override @@ -47,7 +56,8 @@ public class AdminGradingTemplateEditPage extends AbstractAdminProjectPage imple GradingReportTemplateUpdate update = toUpdate( editingGradingTemplate); - gradingReportTemplateService.update(id, update); + GradingReportTemplate newTemplate = gradingReportTemplateService.update(id, update); + editingGradingTemplate = new EditingGradingTemplate(newTemplate); success(getString("template_updated")); } catch (ValidDateMustBeInTheFutureException e) { error(getString("valid_from_must_be_in_the_future", () -> e)); @@ -61,7 +71,23 @@ public class AdminGradingTemplateEditPage extends AbstractAdminProjectPage imple } }; - form.add(new EditingGradingTemplateComponentPanel("editing", Model.of(editingGradingTemplate))); + unsavedChangesAlert = new WebMarkupContainer("unsaved_changes_alert") { + @Override + protected void onConfigure() { + super.onConfigure(); + setVisible(editingGradingTemplate.hasChanges()); + } + }; + form.add(unsavedChangesAlert); + unsavedChangesAlert.setOutputMarkupPlaceholderTag(true); + + form.add(new EditingGradingTemplateComponentPanel("editing", () -> editingGradingTemplate) { + @Override + protected void onTemplateChanged(AjaxRequestTarget target) { + super.onTemplateChanged(target); + target.add(unsavedChangesAlert); + } + }); add(form); } diff --git a/view/src/main/java/se/su/dsv/scipro/admin/pages/grading/EditingGradingTemplate.java b/view/src/main/java/se/su/dsv/scipro/admin/pages/grading/EditingGradingTemplate.java index 7e4e935ad5..a56af9d4eb 100644 --- a/view/src/main/java/se/su/dsv/scipro/admin/pages/grading/EditingGradingTemplate.java +++ b/view/src/main/java/se/su/dsv/scipro/admin/pages/grading/EditingGradingTemplate.java @@ -8,12 +8,15 @@ import java.io.Serializable; import java.time.LocalDate; import java.util.ArrayList; import java.util.List; +import java.util.Objects; class EditingGradingTemplate implements Serializable { + private EditingGradingTemplate original; private String note; private LocalDate validFrom; private List<Criteria> criteria; private GradeLimits gradeLimits; + private String projectType; public EditingGradingTemplate() { this.gradeLimits = new GradeLimits(); @@ -21,6 +24,16 @@ class EditingGradingTemplate implements Serializable { } EditingGradingTemplate(GradingReportTemplate template) { + this(template, null); + this.original = new EditingGradingTemplate(template, null); + } + + /** + * Private constructor for creating a new instance of EditingGradingTemplate + * to be able to track changes made. + * @param doNotCreateOriginal Only exists to differentiate the signature from the public constructor + */ + private EditingGradingTemplate(GradingReportTemplate template, @SuppressWarnings("unused") Void doNotCreateOriginal) { this.note = template.getNote(); this.validFrom = template.getValidFrom(); this.gradeLimits = new GradeLimits(template); @@ -29,6 +42,7 @@ class EditingGradingTemplate implements Serializable { Criteria editingCriteria = new Criteria(criteria); this.criteria.add(editingCriteria); } + this.projectType = template.getProjectType().getName(); } public String getNote() { @@ -61,8 +75,34 @@ class EditingGradingTemplate implements Serializable { .sum(); } + public Boolean hasChanges() { + return !Objects.equals(original, this); + } + public void addCriteria() { - this.criteria.add(new Criteria()); + Criteria newCriteria = new Criteria(); + newCriteria.points.add(newCriteria.new Point()); + this.criteria.add(newCriteria); + + } + + public String getProjectType() { + return projectType; + } + + @Override + public boolean equals(Object o) { + return o instanceof EditingGradingTemplate that + && Objects.equals(note, that.note) + && Objects.equals(validFrom, that.validFrom) + && Objects.equals(criteria, that.criteria) + && Objects.equals(gradeLimits, that.gradeLimits) + && Objects.equals(projectType, that.projectType); + } + + @Override + public int hashCode() { + return Objects.hash(note, validFrom, criteria, gradeLimits); } class Criteria implements Serializable { @@ -79,14 +119,14 @@ class EditingGradingTemplate implements Serializable { private List<Point> points = new ArrayList<>(); private Flag flag; private Type type = Type.PROJECT; - private int pointsRequiredToPass; + private int pointsRequiredToPass = 1; Criteria(GradingCriterionTemplate criteria) { this.titleSv = criteria.getTitle(); this.titleEn = criteria.getTitleEn(); this.pointsRequiredToPass = criteria.getPointsRequiredToPass(); for (var point : criteria.getGradingCriterionPointTemplates()) { - if (point.getPoint() == 0) continue; + if (point.getPoint() == 0) continue; // This is to hide zero point requirements that never have any text Point editingPoint = new Point(point); this.points.add(editingPoint); } @@ -150,6 +190,22 @@ class EditingGradingTemplate implements Serializable { this.pointsRequiredToPass = pointsRequiredToPass; } + @Override + public boolean equals(Object o) { + return o instanceof Criteria criterion + && pointsRequiredToPass == criterion.pointsRequiredToPass + && Objects.equals(titleSv, criterion.titleSv) + && Objects.equals(titleEn, criterion.titleEn) + && Objects.equals(points, criterion.points) + && flag == criterion.flag + && type == criterion.type; + } + + @Override + public int hashCode() { + return Objects.hash(titleSv, titleEn, points, flag, type, pointsRequiredToPass); + } + class Point implements Serializable { private String requirementEn; private String requirementSv; @@ -179,6 +235,18 @@ class EditingGradingTemplate implements Serializable { public void setRequirementSv(String requirementSv) { this.requirementSv = requirementSv; } + + @Override + public boolean equals(Object o) { + return o instanceof Point point + && Objects.equals(requirementEn, point.requirementEn) + && Objects.equals(requirementSv, point.requirementSv); + } + + @Override + public int hashCode() { + return Objects.hash(requirementEn, requirementSv); + } } } } diff --git a/view/src/main/java/se/su/dsv/scipro/admin/pages/grading/EditingGradingTemplateComponentPanel.java b/view/src/main/java/se/su/dsv/scipro/admin/pages/grading/EditingGradingTemplateComponentPanel.java index fb6b0af006..6a59cfa2f5 100644 --- a/view/src/main/java/se/su/dsv/scipro/admin/pages/grading/EditingGradingTemplateComponentPanel.java +++ b/view/src/main/java/se/su/dsv/scipro/admin/pages/grading/EditingGradingTemplateComponentPanel.java @@ -22,6 +22,9 @@ import java.time.LocalDate; import java.util.List; class EditingGradingTemplateComponentPanel extends GenericPanel<EditingGradingTemplate> { + + private final Label maxPointsAvailable; + EditingGradingTemplateComponentPanel( String id, IModel<EditingGradingTemplate> editingGradingTemplateModel) @@ -36,7 +39,7 @@ class EditingGradingTemplateComponentPanel extends GenericPanel<EditingGradingTe EditingGradingTemplate::setValidFrom), LocalDate.class); validFromField.add(new BootstrapDatePicker()); - validFromField.add(new AutoSave()); + validFromField.add(new AutoSave("changeDate")); add(validFromField); add(new TextArea<>("note", LambdaModel.of( @@ -50,7 +53,10 @@ class EditingGradingTemplateComponentPanel extends GenericPanel<EditingGradingTe add(new GradeLimitsPanel("grade_limits", editingGradingTemplateModel.map(EditingGradingTemplate::getGradeLimits))); - add(new Label("max_points_available", editingGradingTemplateModel.map(EditingGradingTemplate::getMaxPointsAvailable))); + maxPointsAvailable = new Label("max_points_available", editingGradingTemplateModel.map(EditingGradingTemplate::getMaxPointsAvailable)); + maxPointsAvailable.setOutputMarkupId(true); + add(maxPointsAvailable); + add(new ListView<>("criteria", editingGradingTemplateModel.map(EditingGradingTemplate::getCriteria)) { { @@ -64,6 +70,7 @@ class EditingGradingTemplateComponentPanel extends GenericPanel<EditingGradingTe public void onClick(AjaxRequestTarget target) { editingGradingTemplateModel.getObject().getCriteria().remove(item.getModelObject()); target.add(EditingGradingTemplateComponentPanel.this); + onTemplateChanged(target); } }); item.add(new CriteriaEditingPanel("criteria", item.getModel())); @@ -75,6 +82,7 @@ class EditingGradingTemplateComponentPanel extends GenericPanel<EditingGradingTe public void onClick(AjaxRequestTarget target) { editingGradingTemplateModel.getObject().addCriteria(); target.add(EditingGradingTemplateComponentPanel.this); + onTemplateChanged(target); } }); } @@ -117,6 +125,7 @@ class EditingGradingTemplateComponentPanel extends GenericPanel<EditingGradingTe EditingGradingTemplate.Criteria.Type objectSelected) { // auto save + onTemplateChanged(target); } }; typeChoice.setRequired(true); @@ -149,6 +158,7 @@ class EditingGradingTemplateComponentPanel extends GenericPanel<EditingGradingTe EditingGradingTemplate.Criteria.Flag objectSelected) { // auto save + onTemplateChanged(target); } }; flagChoice.setNullValid(true); @@ -176,6 +186,8 @@ class EditingGradingTemplateComponentPanel extends GenericPanel<EditingGradingTe EditingGradingTemplate.Criteria.Point newPoint = criteria.new Point(); criteria.getPoints().add(newPoint); target.add(CriteriaEditingPanel.this); + target.add(maxPointsAvailable); + onTemplateChanged(target); } }); } @@ -210,13 +222,14 @@ class EditingGradingTemplateComponentPanel extends GenericPanel<EditingGradingTe EditingGradingTemplate.Criteria criteria = CriteriaEditingPanel.this.getModelObject(); criteria.getPoints().remove(model.getObject()); target.add(CriteriaEditingPanel.this); + onTemplateChanged(target); } }); } } } - private static class GradeLimitsPanel extends GenericWebMarkupContainer<GradeLimits> { + private class GradeLimitsPanel extends GenericWebMarkupContainer<GradeLimits> { public GradeLimitsPanel(String id, IModel<GradeLimits> model) { super(id, model); @@ -248,6 +261,7 @@ class EditingGradingTemplateComponentPanel extends GenericPanel<EditingGradingTe GradeLimits gradeLimits = GradeLimitsPanel.this.getModelObject(); gradeLimits.addNewLimit(); target.add(GradeLimitsPanel.this); + onTemplateChanged(target); } }); } @@ -278,20 +292,30 @@ class EditingGradingTemplateComponentPanel extends GenericPanel<EditingGradingTe GradeLimits gradeLimits = GradeLimitsPanel.this.getModelObject(); gradeLimits.getGradeLimits().remove(model.getObject()); target.add(GradeLimitsPanel.this); + onTemplateChanged(target); } }); } } } - private static class AutoSave extends AjaxFormComponentUpdatingBehavior { + private class AutoSave extends AjaxFormComponentUpdatingBehavior { public AutoSave() { super("input"); } + public AutoSave(String event) { + super(event); + } + @Override protected void onUpdate(AjaxRequestTarget target) { // just trigger the ajax call is enough to update the model object + onTemplateChanged(target); } } + + protected void onTemplateChanged(AjaxRequestTarget target) { + // do nothing + } } diff --git a/view/src/main/java/se/su/dsv/scipro/admin/pages/grading/GradeLimits.java b/view/src/main/java/se/su/dsv/scipro/admin/pages/grading/GradeLimits.java index 29bd5877d2..dae5812f7a 100644 --- a/view/src/main/java/se/su/dsv/scipro/admin/pages/grading/GradeLimits.java +++ b/view/src/main/java/se/su/dsv/scipro/admin/pages/grading/GradeLimits.java @@ -5,6 +5,7 @@ import se.su.dsv.scipro.report.GradingReportTemplate; import java.io.Serializable; import java.util.ArrayList; import java.util.List; +import java.util.Objects; class GradeLimits implements Serializable { private List<GradeLimit> gradeLimits; @@ -41,6 +42,19 @@ class GradeLimits implements Serializable { return gradeLimits; } + @Override + public boolean equals(Object o) { + + return o instanceof GradeLimits that + && Objects.equals(gradeLimits, that.gradeLimits) + && Objects.equals(failingGrade, that.failingGrade); + } + + @Override + public int hashCode() { + return Objects.hash(gradeLimits, failingGrade); + } + class GradeLimit implements Serializable { private String grade; private int lowerLimit; @@ -60,5 +74,17 @@ class GradeLimits implements Serializable { public void setLowerLimit(int lowerLimit) { this.lowerLimit = lowerLimit; } + + @Override + public boolean equals(Object o) { + return o instanceof GradeLimit that + && lowerLimit == that.lowerLimit + && Objects.equals(grade, that.grade); + } + + @Override + public int hashCode() { + return Objects.hash(grade, lowerLimit); + } } } diff --git a/view/src/main/java/se/su/dsv/scipro/admin/pages/grading/wicket-package.utf8.properties b/view/src/main/java/se/su/dsv/scipro/admin/pages/grading/wicket-package.utf8.properties index f761f20e4e..defb43b968 100644 --- a/view/src/main/java/se/su/dsv/scipro/admin/pages/grading/wicket-package.utf8.properties +++ b/view/src/main/java/se/su/dsv/scipro/admin/pages/grading/wicket-package.utf8.properties @@ -3,3 +3,5 @@ valid_from_must_be_in_the_future=The templates valid date must be in the future. 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}. template_created=New template for ${name} created. +unsaved_changes=The grading template has been changed, unsaved changes will be lost if you do not save. +project_type_name_editing=You are editing a ${project_type_name} grading template.