diff --git a/evap/contributor/templates/contributor_evaluation_form.html b/evap/contributor/templates/contributor_evaluation_form.html index 5f83768512..ecd2e22c51 100644 --- a/evap/contributor/templates/contributor_evaluation_form.html +++ b/evap/contributor/templates/contributor_evaluation_form.html @@ -127,8 +127,14 @@ {% include 'confirmation_modal.html' with modal_id='approveEvaluationModal' title=title question=question action_text=action_text btn_type='primary' %} diff --git a/evap/contributor/templates/contributor_index.html b/evap/contributor/templates/contributor_index.html index e3d8253eb9..e61132d2cc 100644 --- a/evap/contributor/templates/contributor_index.html +++ b/evap/contributor/templates/contributor_index.html @@ -228,14 +228,15 @@

if(type == "q"){ // open collapsed answer and scroll into center - const answer_div = $("#faq-"+id+"-a"); window.history.pushState('', id, '/faq#faq-' + id + '-q'); var answerCard = document.getElementById("faq-"+id+"-a"); var answerCardCollapse = bootstrap.Collapse.getOrCreateInstance(answerCard); diff --git a/evap/grades/templates/grades_course_view.html b/evap/grades/templates/grades_course_view.html index 3145cd87f2..7ab7b60fcb 100644 --- a/evap/grades/templates/grades_course_view.html +++ b/evap/grades/templates/grades_course_view.html @@ -58,13 +58,14 @@

{{ course.name }} ({{ semester.name }})

{% include 'confirmation_modal.html' with modal_id='deleteGradedocumentModal' title=title question=question action_text=action_text btn_type='danger' %} {% endblock %} diff --git a/evap/grades/templates/grades_semester_view.html b/evap/grades/templates/grades_semester_view.html index 2264725265..ae9d586115 100644 --- a/evap/grades/templates/grades_semester_view.html +++ b/evap/grades/templates/grades_semester_view.html @@ -108,13 +108,14 @@

{% include 'confirmation_modal.html' with modal_id='confirmNouploadModal' title=title question=question action_text=action_text btn_type='primary' %} {% trans 'Will final grades be uploaded?' as title %} diff --git a/evap/rewards/templates/rewards_reward_point_redemption_events.html b/evap/rewards/templates/rewards_reward_point_redemption_events.html index 105637763e..afda183798 100644 --- a/evap/rewards/templates/rewards_reward_point_redemption_events.html +++ b/evap/rewards/templates/rewards_reward_point_redemption_events.html @@ -33,15 +33,17 @@ {% trans 'Do you really want to delete the event ?' as question %} {% trans 'Delete event' as action_text %} {% include 'confirmation_modal.html' with modal_id='deleteEventModal' title=title question=question action_text=action_text btn_type='danger' %} + {% endblock %} diff --git a/evap/staff/templates/staff_email_preview_form.html b/evap/staff/templates/staff_email_preview_form.html index a1d78d89a2..09b4219048 100644 --- a/evap/staff/templates/staff_email_preview_form.html +++ b/evap/staff/templates/staff_email_preview_form.html @@ -3,7 +3,7 @@ {% if heading %}

{{ heading }}

{% endif %}
+ checked onclick="document.getElementById('email_form').classList.toggle('d-none');" />
diff --git a/evap/staff/templates/staff_evaluation_person_management.html b/evap/staff/templates/staff_evaluation_person_management.html index 3ba518e8cd..55ad0ab6d5 100644 --- a/evap/staff/templates/staff_evaluation_person_management.html +++ b/evap/staff/templates/staff_evaluation_person_management.html @@ -136,11 +136,14 @@
{% trans 'To CSV file' %}
{% include 'confirmation_modal.html' with modal_id='importParticipantsModal' title=title question=question action_text=action_text btn_type='primary' %} @@ -150,11 +153,14 @@
{% trans 'To CSV file' %}
{% include 'confirmation_modal.html' with modal_id='replaceParticipantsModal' title=title question=question action_text=action_text btn_type='danger' %} @@ -164,11 +170,14 @@
{% trans 'To CSV file' %}
{% include 'confirmation_modal.html' with modal_id='copyParticipantsModal' title=title question=question action_text=action_text btn_type='primary' %} @@ -178,11 +187,14 @@
{% trans 'To CSV file' %}
{% include 'confirmation_modal.html' with modal_id='copyReplaceParticipantsModal' title=title question=question action_text=action_text btn_type='danger' %} @@ -192,11 +204,14 @@
{% trans 'To CSV file' %}
{% include 'confirmation_modal.html' with modal_id='importContributorsModal' title=title question=question action_text=action_text btn_type='primary' %} @@ -206,11 +221,14 @@
{% trans 'To CSV file' %}
{% include 'confirmation_modal.html' with modal_id='replaceContributorsModal' title=title question=question action_text=action_text btn_type='danger' %} @@ -220,11 +238,14 @@
{% trans 'To CSV file' %}
{% include 'confirmation_modal.html' with modal_id='copyContributorsModal' title=title question=question action_text=action_text btn_type='primary' %} @@ -234,11 +255,14 @@
{% trans 'To CSV file' %}
{% include 'confirmation_modal.html' with modal_id='copyReplaceContributorsModal' title=title question=question action_text=action_text btn_type='danger' %} diff --git a/evap/staff/templates/staff_questionnaire_form.html b/evap/staff/templates/staff_questionnaire_form.html index c85c87fe61..21122e80d4 100644 --- a/evap/staff/templates/staff_questionnaire_form.html +++ b/evap/staff/templates/staff_questionnaire_form.html @@ -94,12 +94,12 @@
{% trans 'Questions' %}
makeFormSortable("question_table", "questions", rowChanged, rowAdded, "", true, true); const selectChangedHandler = function(e) { - const checkbox = $($(this).closest("td.question-type")).find("input"); + const checkbox = this.closest("td.question-type").querySelector("input[type=checkbox]"); if (e.currentTarget.value == 0 || e.currentTarget.value == 5) { // 0: Text question; 5: Heading - checkbox.prop("checked", false); - checkbox.prop("disabled", true); + checkbox.checked = false; + checkbox.disabled = true; } else { - checkbox.prop("disabled", false); + checkbox.disabled = false; } }; diff --git a/evap/staff/templates/staff_questionnaire_index.html b/evap/staff/templates/staff_questionnaire_index.html index df3da253d1..8a83c00656 100644 --- a/evap/staff/templates/staff_questionnaire_index.html +++ b/evap/staff/templates/staff_questionnaire_index.html @@ -53,13 +53,14 @@ {% include 'confirmation_modal.html' with modal_id='deleteQuestionnaireModal' title=title question=question action_text=action_text btn_type='danger' %} {% endblock %} @@ -80,33 +81,33 @@ {% endblock %} diff --git a/evap/staff/templates/staff_questionnaire_index_list.html b/evap/staff/templates/staff_questionnaire_index_list.html index b126a5bc69..c4f0331e3b 100644 --- a/evap/staff/templates/staff_questionnaire_index_list.html +++ b/evap/staff/templates/staff_questionnaire_index_list.html @@ -34,16 +34,16 @@ {% if type != 'contributor' %}
- - + +
{% endif %}
- - - + + +
diff --git a/evap/staff/templates/staff_semester_import.html b/evap/staff/templates/staff_semester_import.html index 3bd4c1e861..c1d3b4ed27 100644 --- a/evap/staff/templates/staff_semester_import.html +++ b/evap/staff/templates/staff_semester_import.html @@ -49,11 +49,14 @@

{% trans 'Import semester data' %}

{% include 'confirmation_modal.html' with modal_id='importSemesterModal' title=title question=question action_text=action_text btn_type='primary' %} diff --git a/evap/staff/templates/staff_semester_preparation_reminder.html b/evap/staff/templates/staff_semester_preparation_reminder.html index ace9e6b6b8..053ec93032 100644 --- a/evap/staff/templates/staff_semester_preparation_reminder.html +++ b/evap/staff/templates/staff_semester_preparation_reminder.html @@ -78,20 +78,17 @@ {% include 'confirmation_modal.html' with modal_id='remindAllModal' title=title question=question action_text=action_text btn_type='primary' %} {% endblock %} diff --git a/evap/staff/templates/staff_semester_view.html b/evap/staff/templates/staff_semester_view.html index 444ecfd470..f586e5f83f 100644 --- a/evap/staff/templates/staff_semester_view.html +++ b/evap/staff/templates/staff_semester_view.html @@ -305,11 +305,12 @@

{% if request.user.is_manager and not semester.participations_are_archived %}
@@ -449,16 +450,18 @@

{% include 'confirmation_modal.html' with modal_id='makeActiveSemesterModal' title=title question=question action_text=action_text btn_type='primary' %} {% trans 'Delete semester' as title %} @@ -467,16 +470,18 @@

{% include 'confirmation_text_modal.html' with modal_id='deleteSemesterModal' title=title question=question action_text=action_text btn_type='danger' %} {% trans 'Archive participations' as title %} @@ -485,13 +490,14 @@

{% include 'confirmation_modal.html' with modal_id='archiveParticipationsModal' title=title question=question action_text=action_text btn_type='danger' %} {% trans 'Delete grade documents' as title %} @@ -500,13 +506,14 @@

{% include 'confirmation_modal.html' with modal_id='deleteGradeDocumentsModal' title=title question=question action_text=action_text btn_type='danger' %} {% trans 'Archive results' as title %} @@ -515,13 +522,14 @@

{% include 'confirmation_modal.html' with modal_id='archiveResultsModal' title=title question=question action_text=action_text btn_type='danger' %} {% trans 'Delete evaluation' as title %} @@ -530,13 +538,14 @@

{% include 'confirmation_modal.html' with modal_id='deleteEvaluationModal' title=title question=question action_text=action_text btn_type='danger' %} {% trans 'Delete course' as title %} @@ -545,13 +554,14 @@

{% include 'confirmation_modal.html' with modal_id='deleteCourseModal' title=title question=question action_text=action_text btn_type='danger' %} {% trans 'Activate reward points' as title %} @@ -567,15 +577,6 @@

{% block additional_javascript %} {% if form.instance.id %} {% trans 'Send notification email' as title %} @@ -131,13 +132,14 @@

{% trans 'Export evaluation results' %}
{% include 'confirmation_modal.html' with modal_id='resendEmailModal' title=title question=question action_text=action_text btn_type='success' %} {% endif %} diff --git a/evap/staff/templates/staff_user_import.html b/evap/staff/templates/staff_user_import.html index 6e5d8d5783..b316052145 100644 --- a/evap/staff/templates/staff_user_import.html +++ b/evap/staff/templates/staff_user_import.html @@ -48,11 +48,14 @@

{% trans 'Import users' %}

{% include 'confirmation_modal.html' with modal_id='importUserModal' title=title question=question action_text=action_text btn_type='primary' %} diff --git a/evap/static/ts/src/csrf-utils.ts b/evap/static/ts/src/csrf-utils.ts index f95e803205..82c618c62b 100644 --- a/evap/static/ts/src/csrf-utils.ts +++ b/evap/static/ts/src/csrf-utils.ts @@ -14,22 +14,9 @@ function getCookie(name: string): string | null { const csrftoken = getCookie("csrftoken")!; export const CSRF_HEADERS = { "X-CSRFToken": csrftoken }; -function isMethodCsrfSafe(method: string): boolean { - // these HTTP methods do not require CSRF protection - return ["GET", "HEAD", "OPTIONS", "TRACE"].includes(method); -} - -// setup ajax sending csrf token -$.ajaxSetup({ - beforeSend: function (xhr: JQuery.jqXHR, settings: JQuery.AjaxSettings) { - const isMethodSafe = settings.method && isMethodCsrfSafe(settings.method); - if (!isMethodSafe && !this.crossDomain) { - xhr.setRequestHeader("X-CSRFToken", csrftoken); - } - }, -}); +// TODO +(globalThis as any).CSRF_HEADERS = CSRF_HEADERS; export const testable = { getCookie, - isMethodCsrfSafe, }; diff --git a/evap/static/ts/src/utils.ts b/evap/static/ts/src/utils.ts index 533d08d137..ecaaae5917 100644 --- a/evap/static/ts/src/utils.ts +++ b/evap/static/ts/src/utils.ts @@ -39,3 +39,15 @@ export const findPreviousElementSibling = (element: Element, selector: string): }; export const isVisible = (element: HTMLElement): boolean => element.offsetWidth !== 0 || element.offsetHeight !== 0; + +export const fadeOutThenRemove = (element: HTMLElement) => { + element.style.transition = "opacity 600ms"; + element.style.opacity = "0"; + setTimeout(() => { + element.remove(); + }, 600); +}; + +// TODO: How to handle exporting / importing +(globalThis as any).assert = assert; +(globalThis as any).fadeOutThenRemove = fadeOutThenRemove; diff --git a/evap/static/ts/tests/unit/csrf-utils.ts b/evap/static/ts/tests/unit/csrf-utils.ts index 6e5085c40f..7ed01095f3 100644 --- a/evap/static/ts/tests/unit/csrf-utils.ts +++ b/evap/static/ts/tests/unit/csrf-utils.ts @@ -10,12 +10,9 @@ Object.defineProperty(document, "cookie", { `baz=${encodeURIComponent("+{`")}`, }); -// @ts-ignore -window.$ = require("../../../js/jquery-2.1.3.min"); - import { testable } from "src/csrf-utils"; -const { getCookie, isMethodCsrfSafe } = testable; +const { getCookie } = testable; test("parse cookie", () => { expect(getCookie("foo")).toBe("F00"); @@ -24,24 +21,3 @@ test("parse cookie", () => { expect(getCookie("csrftoken")).toBe("token"); expect(getCookie("qux")).toBe(null); }); - -test.each(["GET", "HEAD", "OPTIONS", "TRACE"])("method %s is considered safe", method => { - expect(isMethodCsrfSafe(method)).toBe(true); -}); - -test.each(["POST", "PUT", "DELETE"])("method %s is considered unsafe", method => { - expect(isMethodCsrfSafe(method)).toBe(false); -}); - -test("send csrf token in request", () => { - const mock = { - open: jest.fn(), - send: jest.fn(), - setRequestHeader: jest.fn(), - }; - window.XMLHttpRequest = jest.fn(() => mock) as unknown as typeof window.XMLHttpRequest; - - $.post("/receiver"); - - expect(mock.setRequestHeader).toBeCalledWith("X-CSRFToken", "token"); -}); diff --git a/evap/student/templates/student_vote.html b/evap/student/templates/student_vote.html index 681bc8a458..e5afa86763 100644 --- a/evap/student/templates/student_vote.html +++ b/evap/student/templates/student_vote.html @@ -206,7 +206,7 @@

{{ evaluation.full_name }} ({{ evaluation.course.semester.name function updateLastSavedLabel() { const timeNow = new Date(); - const lastSavedLabel = $('#last-saved'); + const lastSavedLabel = document.getElementById('last-saved'); if (localStorage.getItem(lastSavedStorageKey) !== null) { const lastSavedDate = new Date(localStorage.getItem(lastSavedStorageKey)); const delta = Math.round((timeNow - lastSavedDate) / 1000); @@ -232,9 +232,9 @@

{{ evaluation.full_name }} ({{ evaluation.course.semester.name + padWithLeadingZeros(lastSavedDate.getHours()) + ":" + padWithLeadingZeros(lastSavedDate.getMinutes()); } - lastSavedLabel.text("{% trans 'Last saved locally' %}: " + timeStamp); + lastSavedLabel.innerText = "{% trans 'Last saved locally' %}: " + timeStamp; } else { - lastSavedLabel.text("{% trans 'Could not save your information locally' %}"); + lastSavedLabel.innerText = "{% trans 'Could not save your information locally' %}"; } } @@ -250,50 +250,41 @@

{{ evaluation.full_name }} ({{ evaluation.course.semester.name updateLastSavedLabel(); setInterval(updateLastSavedLabel, 1000); - initTextAnswerWarnings(document.querySelectorAll("#student-vote-form textarea"), JSON.parse($("#text-answer-warnings").text())); + initTextAnswerWarnings(document.querySelectorAll("#student-vote-form textarea"), JSON.parse(document.getElementById("text-answer-warnings").textContent)); - var form = $('#student-vote-form'); - - // Taken from http://stackoverflow.com/questions/9177252/detecting-a-redirect-in-jquery-ajax - // This hacks jQuery to give us this xhr object which is used below - var xhr; - var _orgAjax = jQuery.ajaxSettings.xhr; - jQuery.ajaxSettings.xhr = function () { - xhr = _orgAjax(); - return xhr; - }; - - form.submit(function (event) { + const form = document.getElementById('student-vote-form'); + const submitListener = (event) => { event.preventDefault(); // don't use the default submission - var submitButton = $('#vote-submit-btn'); - var originalText = submitButton.text(); - - submitButton.text("{% trans 'Submitting...' %}"); - submitButton.prop('disabled', true); - - $.ajax({ - type: form.attr('method'), - url: form.attr('action'), - data: form.serialize(), - success: function (data, s, req) { - if(data === "{{ success_magic_string }}") { - sisyphus.manuallyReleaseData(); - window.location.replace("{{ success_redirect_url }}"); - } else { - // resubmit without this handler to show the site with the form errors - form.unbind("submit"); - form.submit(); - } - }, - error: function(data) { - // show a warning if the post isn't successful - document.getElementById('submit-error-warning').style.display = 'block'; - submitButton.text(originalText); - submitButton.prop('disabled', false); + const submitButton = document.getElementById('vote-submit-btn'); + const originalText = submitButton.innerText; + + submitButton.innerText = "{% trans 'Submitting...' %}"; + submitButton.disabled = true; + + fetch(form.action, { + body: new FormData(form), + headers: CSRF_HEADERS, + method: form.method, + }).then(response => { + assert(response.ok); + return response.text(); + }).then(response_text => { + if(response_text === "{{ success_magic_string }}") { + sisyphus.manuallyReleaseData(); + window.location.replace("{{ success_redirect_url }}"); + } else { + // resubmit without this handler to show the site with the form errors + form.removeEventListener("submit", submitListener); + form.requestSubmit(); } + }).catch(error => { + // show a warning if the post isn't successful + document.getElementById('submit-error-warning').style.display = 'block'; + submitButton.innerText = originalText; + submitButton.disabled = false; }); - return false; - }); + }; + form.addEventListener("submit", submitListener); function clearChoiceError(voteButton) { voteButton.closest(".row").querySelectorAll(".choice-error").forEach(highlightedElement => {