From ac69f9c6028d5b07945d4b36ba70ea8c51efee1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20H=C3=A4ttasch?= <benjamin.haettasch@fachschaft.informatik.tu-darmstadt.de> Date: Mon, 28 Nov 2022 00:36:50 +0100 Subject: [PATCH] Improve CV overview and reuse in scheduler Move general ajax setup call to external js file Move common functionality for CV loading to external js file Visualize existing violations in scheduler Add reloading function to scheduler --- .../static/AKScheduling/js/scheduling.js | 12 ++ .../AKScheduling/constraint_violations.html | 97 ++++------- .../admin/AKScheduling/scheduling.html | 158 ++++++++++++------ static_common/common/js/api.js | 31 ++++ 4 files changed, 183 insertions(+), 115 deletions(-) create mode 100644 AKScheduling/static/AKScheduling/js/scheduling.js create mode 100644 static_common/common/js/api.js diff --git a/AKScheduling/static/AKScheduling/js/scheduling.js b/AKScheduling/static/AKScheduling/js/scheduling.js new file mode 100644 index 00000000..1c15164a --- /dev/null +++ b/AKScheduling/static/AKScheduling/js/scheduling.js @@ -0,0 +1,12 @@ +function loadCVs(url, callback_success, callback_error) { + $.ajax({ + url: url, + type: 'GET', + success: callback_success, + error: callback_error + }); +} + +const default_cv_callback_error = function(response) { + alert("{% trans 'Cannot load current violations from server' %}"); +} diff --git a/AKScheduling/templates/admin/AKScheduling/constraint_violations.html b/AKScheduling/templates/admin/AKScheduling/constraint_violations.html index 19c76971..26ba0757 100644 --- a/AKScheduling/templates/admin/AKScheduling/constraint_violations.html +++ b/AKScheduling/templates/admin/AKScheduling/constraint_violations.html @@ -13,78 +13,45 @@ {% block extrahead %} {{ block.super }} + <script type="application/javascript" src="{% static "common/js/api.js" %}"></script> + <script type="application/javascript" src="{% static "AKScheduling/js/scheduling.js" %}"></script> + <script> document.addEventListener('DOMContentLoaded', function () { - // CSRF Protection/Authentication - function getCookie(name) { - let cookieValue = null; - if (document.cookie && document.cookie !== '') { - const cookies = document.cookie.split(';'); - for (let i = 0; i < cookies.length; i++) { - const cookie = cookies[i].trim(); - // Does this cookie string begin with the name we want? - if (cookie.substring(0, name.length + 1) === (name + '=')) { - cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); - break; - } - } - } - return cookieValue; + const url = "{% url "model:scheduling-constraint-violations-list" event_slug=event.slug %}"; + + const callback_success = function(response) { + let table_html = ''; + + if(response.length > 0) { + // Update violation count badge + $('#violationCountBadge').html(response.length).removeClass('badge-success').addClass('badge-warning'); + + // Update violations table + for(let i=0;i<response.length;i++) { + if(response[i].manually_resolved) + table_html += '<tr class="text-muted"><td class="nowrap">{% fa5_icon "check" "fas" %}</td>'; + else + table_html += '<tr><td></td>'; + table_html += "<td>" + response[i].level_display + "</td><td>" + response[i].type_display + "</td><td>" + response[i].details + "</td><td class='nowrap'>" + response[i].timestamp_display + "</td><td><a href='" + response[i].edit_url + "'><i class='btn btn-primary fa fa-pen'></i></a></td></tr>"; + } + } + else { + // Update violation count badge + $('#violationCountBadge').html(0).removeClass('badge-warning').addClass('badge-success'); + + // Update violations table + table_html ='<tr class="text-muted"><td colspan="5" class="text-center">{% trans "No violations" %}</td></tr>' + } + + // Show violation list (potentially empty) in violations table + $('#violationsTableBody').html(table_html); } - const csrftoken = getCookie('csrftoken'); - - function csrfSafeMethod(method) { - // these HTTP methods do not require CSRF protection - return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method)); - } - - $.ajaxSetup({ - beforeSend: function (xhr, settings) { - if (!csrfSafeMethod(settings.type) && !this.crossDomain) { - xhr.setRequestHeader("X-CSRFToken", csrftoken); - } - } - }); // (Re-)Load constraint violations using AJAX and visualize using violation count badge and violation table function reload() { - $.ajax({ - url: "{% url "model:scheduling-constraint-violations-list" event_slug=event.slug %}", - type: 'GET', - success: function (response) { - console.log(response); - - let table_html = ''; - - if(response.length > 0) { - // Update violation count badge - $('#violationCountBadge').html(response.length).removeClass('badge-success').addClass('badge-warning'); - - // Update violations table - for(let i=0;i<response.length;i++) { - if(response[i].manually_resolved) - table_html += '<tr class="text-muted"><td class="nowrap">{% fa5_icon "check" "fas" %}</td>'; - else - table_html += '<tr><td></td>'; - table_html += "<td>" + response[i].level_display + "</td><td>" + response[i].type_display + "</td><td>" + response[i].details + "</td><td class='nowrap'>" + response[i].timestamp_display + "</td><td><a href='" + response[i].edit_url + "'><i class='btn btn-primary fa fa-pen'></i></a></td></tr>"; - } - } - else { - // Update violation count badge - $('#violationCountBadge').html(0).removeClass('badge-warning').addClass('badge-success'); - - // Update violations table - table_html ='<tr class="text-muted"><td colspan="5" class="text-center">{% trans "No violations" %}</td></tr>' - } - - // Show violation list (potentially empty) in violations table - $('#violationsTableBody').html(table_html); - }, - error: function (response) { - alert("{% trans 'Cannot load current violations from server' %}"); - } - }); + loadCVs(url, callback_success, default_cv_callback_error) } reload(); diff --git a/AKScheduling/templates/admin/AKScheduling/scheduling.html b/AKScheduling/templates/admin/AKScheduling/scheduling.html index 465f422a..50ab859c 100644 --- a/AKScheduling/templates/admin/AKScheduling/scheduling.html +++ b/AKScheduling/templates/admin/AKScheduling/scheduling.html @@ -59,48 +59,17 @@ } </style> + <script type="application/javascript" src="{% static "common/js/api.js" %}"></script> + <script type="application/javascript" src="{% static "AKScheduling/js/scheduling.js" %}"></script> + <script> document.addEventListener('DOMContentLoaded', function () { - // CSRF Protection/Authentication - function getCookie(name) { - let cookieValue = null; - if (document.cookie && document.cookie !== '') { - const cookies = document.cookie.split(';'); - for (let i = 0; i < cookies.length; i++) { - const cookie = cookies[i].trim(); - // Does this cookie string begin with the name we want? - if (cookie.substring(0, name.length + 1) === (name + '=')) { - cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); - break; - } - } - } - return cookieValue; - } - - const csrftoken = getCookie('csrftoken'); - - function csrfSafeMethod(method) { - // these HTTP methods do not require CSRF protection - return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method)); - } - - $.ajaxSetup({ - beforeSend: function (xhr, settings) { - if (!csrfSafeMethod(settings.type) && !this.crossDomain) { - xhr.setRequestHeader("X-CSRFToken", csrftoken); - } - } - }); - - // Place slots by dropping placeholders on calendar var containerEl = document.getElementById('unscheduled-slots'); new FullCalendar.Draggable(containerEl, { itemSelector: '.unscheduled-slot', }); - // Calendar var planEl = document.getElementById('planCalendar'); @@ -211,6 +180,58 @@ $('.unscheduled-slot').each(function() { $(this).tooltip({title: $(this).first().attr('data-details'), trigger: 'hover'}); }); + + const cv_url = "{% url "model:scheduling-constraint-violations-list" event_slug=event.slug %}"; + + const cv_callback_success = function(response) { + let table_html = ''; + + if(response.length > 0) { + // Update violation count badge + $('#violationCountBadge').html(response.length).removeClass('badge-success').addClass('badge-warning'); + + // Update violations table + for(let i=0;i<response.length;i++) { + if(response[i].manually_resolved) + table_html += '<tr class="text-muted"><td class="nowrap">{% fa5_icon "check" "fas" %} '; + else + table_html += '<tr><td>'; + + if(response[i].level_display==='{% trans "Violation" %}') + table_html += '{% fa5_icon "exclamation-circle" "fas" %}'; + else + table_html += '{% fa5_icon "info-circle" "fas" %}'; + + table_html += "</td><td class='small'>" + response[i].type_display + "</td></tr>"; + table_html += "<tr><td colspan='2' class='small'>" + response[i].details + "</td></tr>" + } + } + else { + // Update violation count badge + $('#violationCountBadge').html(0).removeClass('badge-warning').addClass('badge-success'); + + // Update violations table + table_html ='<tr class="text-muted"><td colspan="2" class="text-center">{% trans "No violations" %}</td></tr>' + } + + // Show violation list (potentially empty) in violations table + $('#violationsTableBody').html(table_html); + } + + function reloadCVs() { + loadCVs(cv_url, cv_callback_success, default_cv_callback_error); + } + reloadCVs(); + + const reloadBtn = $('#reloadBtn'); + + function reload() { + plan.refetchEvents(); + reloadCVs(); + // TODO Reload unscheduled AKs + } + + reloadBtn.click(reload); }); </script> @@ -218,28 +239,65 @@ <body> <div class="box p-3"> <div class="row header pb-2"> - <div class="col-sm-10"> - <h2 class="d-inline">{% trans "Scheduling for" %} {{event}}</h2> <h5 class="d-inline ml-2"><a href="{% url 'admin:event_status' event.slug %}">{% trans "Event Status" %} {% fa5_icon "level-up-alt" "fas" %}</a></h5> + <div class="col"> + <h2 class="d-inline"> + <button class="btn btn-outline-warning" id="reloadBtn" style="vertical-align: text-bottom;"> + <span id="reloadBtnVisDefault">{% fa5_icon "redo" "fas" %}</span> + </button> + {% trans "Scheduling for" %} {{event}} + </h2> + <h5 class="d-inline ml-2"> + <a href="{% url 'admin:event_status' event.slug %}">{% trans "Event Status" %} {% fa5_icon "level-up-alt" "fas" %}</a> + </h5> </div> - <div class="col-sm-2"></div> </div> <div class="row content"> - <div class="col-md-10 col-lg-10"> + <div class="col-md-8 col-lg-9 col-xl-10"> <div id="planCalendar"></div> </div> - <div class="col-md-2 col-lg-2" id="unscheduled-slots"> - {% regroup slots_unscheduled by ak.track as slots_unscheduled_by_track_list %} - {% for track_slots in slots_unscheduled_by_track_list %} - {% if track_slots.grouper %} - <h5 class="mt-2">{{ track_slots.grouper }}</h5> - {% endif %} - {% for slot in track_slots.list %} - <div class="unscheduled-slot badge badge-primary" style='background-color: {{ slot.ak.category.color }}' - data-event='{ "title": "{{ slot.ak.short_name }}", "duration": {"hours": "{{ slot.duration|unlocalize }}"}, "constraint": "roomAvailable", "description": "{{ slot.ak.details | escapejs }}", "slotID": "{{ slot.pk }}", "backgroundColor": "{{ slot.ak.category.color }}"}' data-details="{{ slot.ak.details }}">{{ slot.ak.short_name }} - ({{ slot.duration }} h)<br>{{ slot.ak.owners_list }} - </div> + <div class="col-md-4 col-lg-3 col-xl-2" id="sidebar"> + <ul class="nav nav-tabs"> + <li class="nav-item"> + <a class="nav-link active" data-toggle="tab" href="#unscheduled-slots">{% trans "Unscheduled" %}</a> + </li> + <li class="nav-item"> + <a class="nav-link" data-toggle="tab" href="#violations"><span id="violationCountBadge" class="badge badge-success">0</span> {% trans "Violation(s)" %}</a> + </li> + </ul> + <div id="sidebarContent" class="tab-content"> + <div class="tab-pane fade show active" id="unscheduled-slots"> + {% regroup slots_unscheduled by ak.track as slots_unscheduled_by_track_list %} + {% for track_slots in slots_unscheduled_by_track_list %} + {% if track_slots.grouper %} + <h5 class="mt-2">{{ track_slots.grouper }}</h5> + {% endif %} + {% for slot in track_slots.list %} + <div class="unscheduled-slot badge badge-primary" style='background-color: {{ slot.ak.category.color }}' + data-event='{ "title": "{{ slot.ak.short_name }}", "duration": {"hours": "{{ slot.duration|unlocalize }}"}, "constraint": "roomAvailable", "description": "{{ slot.ak.details | escapejs }}", "slotID": "{{ slot.pk }}", "backgroundColor": "{{ slot.ak.category.color }}"}' data-details="{{ slot.ak.details }}">{{ slot.ak.short_name }} + ({{ slot.duration }} h)<br>{{ slot.ak.owners_list }} + </div> + {% endfor %} {% endfor %} - {% endfor %} + </div> + <div class="tab-pane fade" id="violations"> + <table class="table table-striped mt-4 mb-4"> + <thead> + <tr> + <th>{% trans "Level" %}</th> + <th>{% trans "Problem" %}</th> + </tr> + </thead> + <tbody id="violationsTableBody"> + <tr class="text-muted"> + <td colspan="2" class="text-center"> + {% trans "No violations" %} + </td> + </tr> + </tbody> + </table> + + </div> + </div> </div> </div> <div class="row footer"> diff --git a/static_common/common/js/api.js b/static_common/common/js/api.js new file mode 100644 index 00000000..ba966fb2 --- /dev/null +++ b/static_common/common/js/api.js @@ -0,0 +1,31 @@ +// CSRF Protection/Authentication +function getCookie(name) { + let cookieValue = null; + if (document.cookie && document.cookie !== '') { + const cookies = document.cookie.split(';'); + for (let i = 0; i < cookies.length; i++) { + const cookie = cookies[i].trim(); + // Does this cookie string begin with the name we want? + if (cookie.substring(0, name.length + 1) === (name + '=')) { + cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); + break; + } + } + } + return cookieValue; +} + +const csrftoken = getCookie('csrftoken'); + +function csrfSafeMethod(method) { + // these HTTP methods do not require CSRF protection + return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method)); +} + +$.ajaxSetup({ + beforeSend: function (xhr, settings) { + if (!csrfSafeMethod(settings.type) && !this.crossDomain) { + xhr.setRequestHeader("X-CSRFToken", csrftoken); + } + } +}); \ No newline at end of file -- GitLab