Skip to content

Commit

Permalink
Rewrite confirmation modals (#1985)
Browse files Browse the repository at this point in the history
This removes our Django template `confirmation_modal.html` and instead provides the `confirmation-modal` custom element. The new modal is used like a submit button for HTML forms. Alternatively, if a modal does not have an associated form, it will dispatch a custom "confirmed" event on itself.

Additionally, I added "custom-success" forms. By specifying the `custom-success` tag on an HTML form, the form will not use the default behavior when a submit is requested, but instead submit the form data with `fetch` and dispatch a custom event afterwards. One common use case is making a form reload the current site on success - this way, we don't need the views to know what sites usually request it. I added the `reload-on-success` tag for this special case.

I changed all confirmation modals, except for the delegation modal on the contributor index site. This modal was already a special case before and we can port it later.
  • Loading branch information
niklasmohrin authored Feb 26, 2024
1 parent 8145c09 commit 04015c9
Show file tree
Hide file tree
Showing 30 changed files with 846 additions and 665 deletions.
31 changes: 12 additions & 19 deletions evap/contributor/templates/contributor_evaluation_form.html
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,18 @@ <h5 class="card-title me-auto">{% trans 'Evaluation data' %}</h5>
{% if editable %}
<button name="operation" value="preview" type="submit" class="btn btn-light">{% trans 'Preview' %}</button>
<button name="operation" value="save" type="submit" class="btn btn-primary">{% trans 'Save' %}</button>
{# webtest does not allow submission with value "approve" if no such button exists #}
<button style="display: none" name="operation" value="approve" type="submit"></button>
<button type="button" onclick="approveEvaluationModalShow(0, '');" class="btn btn-success">{% trans 'Save and approve' %}</button>

<confirmation-modal type="submit" name="operation" value="approve">
<span slot="title">{% trans 'Approve evaluation' %}</span>
<span slot="action-text">{% trans 'Approve evaluation' %}</span>
<span slot="question">
{% blocktrans trimmed %}
Do you want to approve this evaluation? This will allow the evaluation team to proceed with the preparation, but you won't be able to make any further changes.
{% endblocktrans %}
</span>

<button slot="show-button" type="button" class="btn btn-success">{% trans 'Save and approve' %}</button>
</confirmation-modal>
{% endif %}
<a href="{% url 'contributor:index' %}" class="btn btn-light">{% if edit %}{% trans 'Cancel' %}{% else %}{% trans 'Back' %}{% endif %}</a>
</div>
Expand Down Expand Up @@ -123,22 +132,6 @@ <h5 class="modal-title" id="previewModalLabel">{% trans 'Preview' %}</h5>

{% block modals %}
{{ block.super }}
{% trans 'Approve evaluation' as title %}
{% blocktrans asvar question%}Do you want to approve this evaluation? This will allow the evaluation team to proceed with the preparation, but you won't be able to make any further changes.{% endblocktrans %}
{% trans 'Approve evaluation' as action_text %}
{% include 'confirmation_modal.html' with modal_id='approveEvaluationModal' title=title question=question action_text=action_text btn_type='primary' %}
<script type="text/javascript">
function approveEvaluationModalAction(dataId) {
const input = document.createElement("input");
input.type = "hidden";
input.name = "operation";
input.value = "approve";

const form = document.getElementById("evaluation-form");
form.appendChild(input);
form.requestSubmit();
};
</script>

{% blocktrans asvar title with evaluation_name=evaluation.full_name %}Request account creation for {{ evaluation_name }}{% endblocktrans %}
{% trans 'Please tell us which new account we should create. We need the name and email for all new accounts.' as teaser %}
Expand Down
37 changes: 21 additions & 16 deletions evap/contributor/templates/contributor_index.html
Original file line number Diff line number Diff line change
Expand Up @@ -157,12 +157,13 @@
<span class="fas fa-pencil"></span>
</a>
{% if not evaluation|has_nonresponsible_editor %}
<a href="#" class="btn btn-sm btn-dark" data-bs-toggle="tooltip"
<button class="btn btn-sm btn-dark" data-bs-toggle="tooltip"
data-bs-placement="top" title="{% trans 'Delegate preparation' %}"
onclick="delegateSelectionModalShow(`{{ evaluation.full_name }}`, `{% url 'contributor:evaluation_direct_delegation' evaluation.id %}`);return false;"
data-evaluation-name="{{ evaluation.full_name }}"
data-delegation-url="{% url 'contributor:evaluation_direct_delegation' evaluation.id %}"
>
<span class="fas fa-hand-point-left"></span>
</a>
</button>
{% endif %}
{% elif evaluation.state == evaluation.State.EDITOR_APPROVED or evaluation.state == evaluation.State.APPROVED %}
<a href="{% url 'contributor:evaluation_view' evaluation.id %}" class="btn btn-sm btn-light"
Expand Down Expand Up @@ -226,22 +227,26 @@ <h5 class="modal-title" id="{{ modal_id }}Label">{% trans 'Delegate preparation'
</div>
</div>

<script type="text/javascript">
function {{ modal_id }}Show(evaluationName, action) {
const modal = document.getElementById("{{ modal_id }}");
// set form's action location
modal.querySelectorAll("form").forEach(form => form.action = action);
<script type="module">
document.querySelectorAll("[data-evaluation-name][data-delegation-url]").forEach(showButton => {
showButton.addEventListener("click", event => {
event.stopPropagation();

// put the correct evaluation name in the modal
modal.querySelectorAll('[data-label=""]').forEach(el => el.innerText = evaluationName);
const modal = document.getElementById("{{ modal_id }}");
// set form's action location
modal.querySelectorAll("form").forEach(form => form.action = showButton.dataset.delegationUrl);

// unselect any previously selected options in the modal
modal.querySelectorAll("select").forEach(select => select.tomselect.clear());
// put the correct evaluation name in the modal
modal.querySelectorAll('[data-label=""]').forEach(el => el.innerText = showButton.dataset.evaluationName);

// show modal
var {{ modal_id }} = new bootstrap.Modal(document.getElementById('{{ modal_id }}'));
{{ modal_id }}.show();
}
// unselect any previously selected options in the modal
modal.querySelectorAll("select").forEach(select => select.tomselect.clear());

// show modal
var {{ modal_id }} = new bootstrap.Modal(document.getElementById('{{ modal_id }}'));
{{ modal_id }}.show();
});
});
</script>
{% endwith %}
{% endblock %}
9 changes: 7 additions & 2 deletions evap/contributor/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@
from model_bakery import baker

from evap.evaluation.models import Contribution, Course, Evaluation, Questionnaire, UserProfile
from evap.evaluation.tests.tools import WebTestWith200Check, create_evaluation_with_responsible_and_editor, render_pages
from evap.evaluation.tests.tools import (
WebTestWith200Check,
create_evaluation_with_responsible_and_editor,
render_pages,
submit_with_modal,
)


class TestContributorDirectDelegationView(WebTest):
Expand Down Expand Up @@ -197,7 +202,7 @@ def test_contributor_evaluation_edit(self):
self.evaluation = Evaluation.objects.get(pk=self.evaluation.pk)
self.assertEqual(self.evaluation.state, Evaluation.State.PREPARED)

form.submit(name="operation", value="approve")
submit_with_modal(page, form, name="operation", value="approve")
self.evaluation = Evaluation.objects.get(pk=self.evaluation.pk)
self.assertEqual(self.evaluation.state, Evaluation.State.EDITOR_APPROVED)

Expand Down
8 changes: 8 additions & 0 deletions evap/evaluation/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
<body>
<script type="text/javascript" src="{% static 'bootstrap/dist/js/bootstrap.bundle.min.js' %}"></script>

{% include "custom_elements.html" %}

{% block modals %}
{% if user.is_authenticated %}
{% trans 'Feedback' as title %}
Expand Down Expand Up @@ -199,6 +201,12 @@

</script>

<script type="module">
import { setupForms } from "{% static 'js/custom-success-form.js' %}";

setupForms();
</script>

{% block additional_javascript %}{% endblock %}
</body>
</html>
33 changes: 0 additions & 33 deletions evap/evaluation/templates/confirmation_modal.html

This file was deleted.

33 changes: 33 additions & 0 deletions evap/evaluation/templates/confirmation_modal_template.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{% load static %}

<template id="confirmation-modal-template">
<link rel="stylesheet" href="{% static 'css/evap.css' %}" />

<dialog class="evap-modal-dialog">
<form method="dialog">
<div class="evap-modal-container">
<header>
<h5 class="mb-0"><slot name="title"></slot></h5>
<button class="btn-close"></button>
</header>
<section class="question-area">
<slot name="question"></slot>
<slot name="extra-inputs"></slot>
</section>
<section class="button-area">
<button class="btn btn-light" autofocus>{% trans 'Cancel' %}</button>
<slot name="submit-group">
<button class="btn ms-2" data-event-type="confirm">
<slot name="action-text"></slot>
</button>
</slot>
</section>
</div>
</form>
</dialog>

<slot name="show-button"></slot>

{# All children without the "slot" attribute go into this unnamed slot #}
<slot></slot>
</template>
9 changes: 9 additions & 0 deletions evap/evaluation/templates/custom_elements.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{% load static %}

{% include "confirmation_modal_template.html" %}

<script type="module">
import { ConfirmationModal } from "{% static 'js/confirmation-modal.js' %}";

customElements.define("confirmation-modal", ConfirmationModal);
</script>
6 changes: 3 additions & 3 deletions evap/evaluation/tests/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from model_bakery import baker

from evap.evaluation.models import CourseType, Degree, Semester, UserProfile
from evap.evaluation.tests.tools import make_manager
from evap.evaluation.tests.tools import make_manager, submit_with_modal
from evap.staff.tests.utils import WebTestStaffMode


Expand All @@ -36,7 +36,7 @@ def test_sample_semester_file(self):
form = page.forms["semester-import-form"]
form["vote_start_datetime"] = "2015-01-01 11:11:11"
form["vote_end_date"] = "2099-01-01"
form.submit(name="operation", value="import")
submit_with_modal(page, form, name="operation", value="import")

self.assertEqual(UserProfile.objects.count(), original_user_count + 4)

Expand All @@ -50,7 +50,7 @@ def test_sample_user_file(self):
page = form.submit(name="operation", value="test")

form = page.forms["user-import-form"]
form.submit(name="operation", value="import")
submit_with_modal(page, form, name="operation", value="import")

self.assertEqual(UserProfile.objects.count(), original_user_count + 2)

Expand Down
9 changes: 9 additions & 0 deletions evap/evaluation/tests/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from contextlib import contextmanager
from datetime import timedelta

import webtest
from django.conf import settings
from django.contrib.auth.models import Group
from django.db import DEFAULT_DB_ALIAS, connections
Expand Down Expand Up @@ -125,6 +126,14 @@ def test_check_response_code_200(self):
self.app.get(self.url, user=user, status=200)


def submit_with_modal(page: webtest.TestResponse, form: webtest.Form, *, name: str, value: str) -> webtest.TestResponse:
# Like form.submit, but looks for a modal instead of a submit button.
assert page.forms[form.id] == form
assert page.html.select_one(f"confirmation-modal[type=submit][name={name}][value={value}]")
params = form.submit_fields() + [(name, value)]
return form.response.goto(form.action, method=form.method, params=params)


def get_form_data_from_instance(form_cls, instance, **kwargs):
assert form_cls._meta.model == type(instance)
form = form_cls(instance=instance, **kwargs)
Expand Down
48 changes: 24 additions & 24 deletions evap/grades/templates/grades_course_view.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ <h3 class="mb-3">{{ course.name }} ({{ semester.name }})</h3>
</div>
<div class="card-body table-responsive">
{% if grade_documents %}
<form id="grade-document-deletion-form" custom-success method="POST" action="{% url 'grades:delete_grades' %}">
{% csrf_token %}
</form>

<table class="table table-striped table-vertically-aligned">
<thead>
<tr>
Expand All @@ -22,23 +26,39 @@ <h3 class="mb-3">{{ course.name }} ({{ semester.name }})</h3>
</thead>
<tbody>
{% for grade_document in grade_documents %}
<tr id="grade_document-row-{{ grade_document.id }}">
<tr id="grade-document-row-{{ grade_document.id }}">
<td>{{ grade_document.description }}</td>
<td>{{ grade_document.get_type_display }}</td>
<td>{{ grade_document.last_modified_time }}, {% trans 'by' %} {{ grade_document.last_modified_user }}</td>
<td>
<a href="{% url 'grades:download_grades' grade_document.id %}" class="btn btn-sm btn-light" data-bs-toggle="tooltip" data-bs-placement="top" title="{% trans 'Download' %}"><span class="fas fa-download"></span></a>
{% if user.is_grade_publisher %}
<a href="{% url 'grades:edit_grades' grade_document.id %}" class="btn btn-sm btn-light" data-bs-toggle="tooltip" data-bs-placement="top" title="{% trans 'Edit' %}"><span class="fas fa-pencil"></span></a>
<button type="button" onclick="deleteGradedocumentModalShow({{ grade_document.id }}, '{{ grade_document.description|escapejs }}');" class="btn btn-sm btn-danger" data-bs-toggle="tooltip" data-bs-placement="top" title="{% trans 'Delete' %}">
<span class="fas fa-trash"></span>
</button>
<confirmation-modal type="submit" form="grade-document-deletion-form" name="grade_document_id" value="{{ grade_document.id }}" confirm-button-class="btn-danger">
<span slot="title">{% trans 'Delete grade document' %}</span>
<span slot="action-text">{% trans 'Delete grade document' %}</span>
<span slot="question">
{% blocktrans trimmed with description=grade_document.description %}
Do you really want to delete the grade document <strong>{{ description }}</strong>?
{% endblocktrans %}
</span>

<button slot="show-button" type="button" class="btn btn-sm btn-danger" data-bs-toggle="tooltip" data-bs-placement="top" title="{% trans 'Delete' %}">
<span class="fas fa-trash"></span>
</button>
</confirmation-modal>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>

<script type="module">
document.getElementById("grade-document-deletion-form").addEventListener("submit-success", event => {
fadeOutThenRemove(document.getElementById(`grade-document-row-${event.detail.body.get("grade_document_id")}`));
});
</script>
{% else %}
<span class="fst-italic">{% trans 'No grade documents have been uploaded yet' %}</span>
{% endif %}
Expand All @@ -49,23 +69,3 @@ <h3 class="mb-3">{{ course.name }} ({{ semester.name }})</h3>
<a href="{% url 'grades:upload_grades' course.id %}?final=true" class="btn btn-dark">{% trans 'Upload new final grades' %}</a>
{% endif %}
{% endblock %}

{% block modals %}
{{ block.super }}
{% trans 'Delete grade document' as title %}
{% trans 'Do you really want to delete the grade document <strong data-label=""></strong>?' as question %}
{% trans 'Delete grade document' as action_text %}
{% include 'confirmation_modal.html' with modal_id='deleteGradedocumentModal' title=title question=question action_text=action_text btn_type='danger' %}
<script type="text/javascript">
function deleteGradedocumentModalAction(dataId) {
fetch("{% url 'grades:delete_grades' %}", {
body: new URLSearchParams({grade_document_id: dataId}),
headers: CSRF_HEADERS,
method: "POST",
}).then(response => {
assert(response.ok);
fadeOutThenRemove(document.getElementById('grade_document-row-'+dataId));
}).catch(error => {window.alert("{% trans 'The server is not responding.' %}");});
};
</script>
{% endblock %}
Loading

0 comments on commit 04015c9

Please sign in to comment.