Compare commits

..

2 Commits

Author SHA1 Message Date
09cf81d4f3 Do not add duplicate supervisors in Daisy
Daisy's API does not support the function "change supervisor" and only what comes down to SQL INSERT and DELETE on a specific table. If the removal of the previous supervisor(s) failed the new one was still added. This change makes it so that if the removal of any (there is no limit in the API) current supervisor fails it will not attempt to add the new supervisor.
2024-11-28 18:14:42 +01:00
8a657b21dd 3494 UI improvements to the administration page for grading templates ()
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: 
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>
2024-11-26 10:18:55 +01:00
8 changed files with 176 additions and 14 deletions

@ -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>

@ -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);
}
});

@ -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>

@ -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);
}

@ -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);
}
}
}
}

@ -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
}
}

@ -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);
}
}
}

@ -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.