From aabb2e9d10ff1379e78d273444c7431108a69f62 Mon Sep 17 00:00:00 2001
From: Andreas Svanberg <andreass@dsv.su.se>
Date: Mon, 25 Nov 2024 11:24:08 +0100
Subject: [PATCH 1/2] Fix saving the textboxes students fill in when submitting
 ideas (#25)

Background, literature, problem, method, and interests were not being saved due to explicitly having been marked as "do not save".

Multiple OTRS tickets have been filed for this problem; [391725](https://otrs.dsv.su.se/otrs/index.pl?Action=AgentTicketZoom;TicketID=391725#1422495), [391732](https://otrs.dsv.su.se/otrs/index.pl?Action=AgentTicketZoom;TicketID=391732#1422521), [391738](https://otrs.dsv.su.se/otrs/index.pl?Action=AgentTicketZoom;TicketID=391738#1422537), and [391757](https://otrs.dsv.su.se/otrs/index.pl?Action=AgentTicketZoom;TicketID=391757#1422590).

## How to test
1. Create an open application period under "Admin" / "Match" / "Application periods" (open meaning start is before today, end is after)
2. Log in as author
3. Submit an idea in the newly created period
4. See that neither of background, literature, problem, method, or interests textboxes are saved
5. Switch to this branch
6. Submit/update the idea
7. See that everything is saved

Reviewed-on: https://gitea.dsv.su.se/DMC/scipro/pulls/25
Reviewed-by: Nico Athanassiadis <nico@dsv.su.se>
Co-authored-by: Andreas Svanberg <andreass@dsv.su.se>
Co-committed-by: Andreas Svanberg <andreass@dsv.su.se>
---
 core/src/main/java/se/su/dsv/scipro/match/Idea.java | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/core/src/main/java/se/su/dsv/scipro/match/Idea.java b/core/src/main/java/se/su/dsv/scipro/match/Idea.java
index e2772b539b..1ab583880a 100755
--- a/core/src/main/java/se/su/dsv/scipro/match/Idea.java
+++ b/core/src/main/java/se/su/dsv/scipro/match/Idea.java
@@ -105,11 +105,11 @@ public class Idea extends DomainObject {
 
     @Embedded
     @AttributeOverrides({
-            @AttributeOverride(name = "literature", column = @Column(name  = "literature", insertable = false, updatable = false)),
-            @AttributeOverride(name = "background", column = @Column(name  = "background", insertable = false, updatable = false)),
-            @AttributeOverride(name = "problem", column = @Column(name  = "problem", insertable = false, updatable = false)),
-            @AttributeOverride(name = "method", column = @Column(name  = "method", insertable = false, updatable = false)),
-            @AttributeOverride(name = "interests", column = @Column(name  = "interests", insertable = false, updatable = false))
+            @AttributeOverride(name = "literature", column = @Column(name  = "literature")),
+            @AttributeOverride(name = "background", column = @Column(name  = "background")),
+            @AttributeOverride(name = "problem", column = @Column(name  = "problem")),
+            @AttributeOverride(name = "method", column = @Column(name  = "method")),
+            @AttributeOverride(name = "interests", column = @Column(name  = "interests"))
     })
     private TholanderBox tholanderBox = new TholanderBox();
 

From 8a657b21dd8344b80ea0892b2246cf4aae6216e4 Mon Sep 17 00:00:00 2001
From: Nico Athanassiadis <nico@dsv.su.se>
Date: Tue, 26 Nov 2024 10:18:55 +0100
Subject: [PATCH 2/2] 3494 UI improvements to the administration page for
 grading templates (#21)

Changes made to the grading template UI

Some of the improvements include:
- When editing a template, and the user makes a change the user will be informed/alerted that the template has changed.
- When editing a template the user will now see a project title that shows what project type that template has.
- Default value of points required to pass has been changed from 0 -> 1
- Requirements to points will be added by default when adding a new criteria.
- The create button is now hidden until the user has chosen a grading template project type from the drop down menu.
- Max available points are now updated as point requirements are added to the criterion.

Reviewed-on: https://gitea.dsv.su.se/DMC/scipro/pulls/21
Reviewed-by: Andreas Svanberg <andreass@dsv.su.se>
Co-authored-by: Nico Athanassiadis <nico@dsv.su.se>
Co-committed-by: Nico Athanassiadis <nico@dsv.su.se>
---
 .../AdminGradingTemplateCreationPage.html     |  2 +-
 .../AdminGradingTemplateCreationPage.java     | 10 ++-
 .../grading/AdminGradingTemplateEditPage.html | 10 ++-
 .../grading/AdminGradingTemplateEditPage.java | 34 ++++++++-
 .../pages/grading/EditingGradingTemplate.java | 74 ++++++++++++++++++-
 .../EditingGradingTemplateComponentPanel.java | 32 +++++++-
 .../admin/pages/grading/GradeLimits.java      | 26 +++++++
 .../grading/wicket-package.utf8.properties    |  2 +
 8 files changed, 176 insertions(+), 14 deletions(-)

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.