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