mirror of
https://github.com/ae-utbm/sith.git
synced 2025-07-10 03:49:24 +00:00
replace drf by django-ninja
This commit is contained in:
@ -5,52 +5,79 @@
|
||||
{% trans %}UV Guide{% endtrans %}
|
||||
{% endblock %}
|
||||
|
||||
{% block additional_js %}
|
||||
<script src="{{ static('core/js/alpinejs.min.js') }}" defer></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
<meta name="viewport" content="width=device-width, initial-scale=0.6, maximum-scale=2">
|
||||
{% endblock head %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pedagogy">
|
||||
<form id="search_form" action="{{ url('pedagogy:guide') }}" method="get">
|
||||
{% if can_create_uv %}
|
||||
<div class="action-bar">
|
||||
<p>
|
||||
<a href="{{ url('pedagogy:uv_create') }}">{% trans %}Create UV{% endtrans %}</a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="{{ url('pedagogy:moderation') }}">{% trans %}Moderate comments{% endtrans %}</a>
|
||||
</p>
|
||||
</div>
|
||||
<br/>
|
||||
{% endif %}
|
||||
<div class="pedagogy" x-data="uv_search" x-cloak>
|
||||
<form id="search_form">
|
||||
<div class="search-form-container">
|
||||
{% if can_create_uv(user) %}
|
||||
<div class="action-bar">
|
||||
<p>
|
||||
<a href="{{ url('pedagogy:uv_create') }}">{% trans %}Create UV{% endtrans %}</a>
|
||||
</p>
|
||||
<p>
|
||||
<a href="{{ url('pedagogy:moderation') }}">{% trans %}Moderate comments{% endtrans %}</a>
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="search-bar">
|
||||
<input id="search_input" class="search-bar-input" type="text" name="search">
|
||||
<button class="search-bar-button">{% trans %}Search{% endtrans %}</button>
|
||||
<input
|
||||
id="search_input"
|
||||
class="search-bar-input"
|
||||
type="text"
|
||||
name="search"
|
||||
x-model.debounce.500ms="search"
|
||||
/>
|
||||
</div>
|
||||
<div class="radio-department">
|
||||
<div class="radio-guide">
|
||||
{% for (display_name, real_name) in [("EDIM", "EDIM"), ("ENERGIE", "EE"), ("IMSI", "IMSI"), ("INFO", "GI"), ("GMC", "MC"), ("HUMA", "HUMA"), ("TC", "TC")] %}
|
||||
<input type="radio" name="department" id="radio{{ real_name }}" value="{{ real_name }}"><label for="radio{{ real_name }}">{% trans %}{{ display_name }}{% endtrans %}</label>
|
||||
{% for (display_name, real_name) in [
|
||||
("EDIM", "EDIM"), ("ENERGIE", "EE"), ("IMSI", "IMSI"),
|
||||
("INFO", "GI"), ("GMC", "MC"), ("HUMA", "HUMA"), ("TC", "TC")
|
||||
] %}
|
||||
<input
|
||||
type="checkbox"
|
||||
name="department"
|
||||
id="radio{{ real_name }}"
|
||||
value="{{ real_name }}"
|
||||
x-model="department"
|
||||
/>
|
||||
<label for="radio{{ real_name }}">{% trans %}{{ display_name }}{% endtrans %}</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="radio-credit-type">
|
||||
<div class="radio-guide">
|
||||
{% for credit_type in ["CS", "TM", "EC", "QC", "OM"] %}
|
||||
<input type="radio" name="credit_type" id="radio{{ credit_type }}" value="{{ credit_type }}"><label for="radio{{ credit_type }}">{% trans %}{{ credit_type }}{% endtrans %}</label>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="credit_type"
|
||||
id="radio{{ credit_type }}"
|
||||
value="{{ credit_type }}"
|
||||
x-model="credit_type"
|
||||
/>
|
||||
<label for="radio{{ credit_type }}">{% trans %}{{ credit_type }}{% endtrans %}</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="radio-semester">
|
||||
<div class="radio-guide">
|
||||
<input type="checkbox" name="semester" id="radioAUTUMN" value="AUTUMN"><label for="radioAUTUMN"><i class="fa fa-leaf"></i></label>
|
||||
<input type="checkbox" name="semester" id="radioSPRING" value="SPRING"><label for="radioSPRING"><i class="fa fa-sun-o"></i></label>
|
||||
<span><input type="checkbox" name="semester" id="radioAP" value="AUTUMN_AND_SPRING"><label for="radioAP">AP</label></span>
|
||||
<input type="checkbox" name="semester" id="radioAUTUMN" value="AUTUMN" x-model="semester"/>
|
||||
<label for="radioAUTUMN"><i class="fa fa-sun-o"></i></label>
|
||||
<input type="checkbox" name="semester" id="radioSPRING" value="SPRING" x-model="semester"/>
|
||||
<label for="radioSPRING"><i class="fa fa-leaf"></i></label>
|
||||
</div>
|
||||
</div>
|
||||
<input type="text" name="json" hidden>
|
||||
</div>
|
||||
</form>
|
||||
<table id="dynamic_view">
|
||||
@ -62,185 +89,84 @@
|
||||
<td>{% trans %}Credit type{% endtrans %}</td>
|
||||
<td><i class="fa fa-leaf"></i></td>
|
||||
<td><i class="fa fa-sun-o"></i></td>
|
||||
{% if can_create_uv(user) %}
|
||||
<td>{% trans %}Edit{% endtrans %}</td>
|
||||
<td>{% trans %}Delete{% endtrans %}</td>
|
||||
{% if can_create_uv %}
|
||||
<td>{% trans %}Edit{% endtrans %}</td>
|
||||
<td>{% trans %}Delete{% endtrans %}</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="dynamic_view_content">
|
||||
{% for uv in object_list %}
|
||||
<tr onclick="window.location.href = `{{ url('pedagogy:uv_detail', uv_id=uv.id) }}`">
|
||||
<td><a href="{{ url('pedagogy:uv_detail', uv_id=uv.id) }}">{{ uv.code }}</a></td>
|
||||
<td>{{ uv.title }}</td>
|
||||
<td>{{ uv.department }}</td>
|
||||
<td>{{ uv.credit_type }}</td>
|
||||
<td>
|
||||
{% if uv.semester in ["AUTUMN", "AUTUMN_AND_SPRING"] %}
|
||||
<i class="fa fa-leaf"></i>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if uv.semester in ["SPRING", "AUTUMN_AND_SPRING"] %}
|
||||
<i class="fa fa-sun-o"></i>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% if user.is_owner(uv) -%}
|
||||
<td><a href="{{ url('pedagogy:uv_update', uv_id=uv.id) }}">{% trans %}Edit{% endtrans %}</a></td>
|
||||
<td><a href="{{ url('pedagogy:uv_delete', uv_id=uv.id) }}">{% trans %}Delete{% endtrans %}</a></td>
|
||||
{%- endif -%}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<template x-for="uv in uvs" :key="uv.id">
|
||||
<tr @click="window.location.href = `/pedagogy/uv/${uv.id}`">
|
||||
<td><a :href="`/pedagogy/uv/${uv.id}`" x-text="uv.code"></a></td>
|
||||
<td x-text="uv.title"></td>
|
||||
<td x-text="uv.department"></td>
|
||||
<td x-text="uv.credit_type"></td>
|
||||
<td><i :class="[uv.semester].includes('AUTUMN') && 'fa fa-leaf'"></i></td>
|
||||
<td><i :class="[uv.semester].includes('SPRING') && 'fa fa-sun-o'"></i></td>
|
||||
{% if can_create_uv -%}
|
||||
<td><a :href="`/pedagogy/uv/${uv.id}/update`">{% trans %}Edit{% endtrans %}</a></td>
|
||||
<td><a :href="`/pedagogy/uv/${uv.id}/delete`">{% trans %}Delete{% endtrans %}</a></td>
|
||||
{%- endif -%}
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<script>
|
||||
function autofillCheckboxRadio(name){
|
||||
if (urlParams.has(name)){ $("input[name='" + name + "']").each(function(){
|
||||
if ($(this).attr("value") == urlParams.get(name))
|
||||
$(this).prop("checked", true);
|
||||
});
|
||||
}
|
||||
}
|
||||
const initialUrlParams = new URLSearchParams(window.location.search);
|
||||
|
||||
function uvJSONToHTML(uv){
|
||||
var autumn = "";
|
||||
var spring = "";
|
||||
if (uv.semester == "AUTUMN" || uv.semester == "AUTUMN_AND_SPRING")
|
||||
autumn = "<i class='fa fa-leaf'></i>";
|
||||
if (uv.semester == "SPRING" || uv.semester == "AUTUMN_AND_SPRING")
|
||||
spring = "<i class='fa fa-sun-o'></i>";
|
||||
|
||||
var html = `
|
||||
<tr onclick="window.location.href = '${uv.absolute_url}';">
|
||||
<td><a href="${uv.absolute_url}">${uv.code}</a></td>
|
||||
<td>${uv.title}</td>
|
||||
<td>${uv.department}</td>
|
||||
<td>${uv.credit_type}</td>
|
||||
<td>${autumn}</td>
|
||||
<td>${spring}</td>
|
||||
`;
|
||||
{% if can_create_uv(user) %}
|
||||
html += `
|
||||
<td><a href="${uv.update_url}">{% trans %}Edit{% endtrans %}</a></td>
|
||||
<td><a href="${uv.delete_url}">{% trans %}Delete{% endtrans %}</a></td>
|
||||
`;
|
||||
{% endif %}
|
||||
return html + "</td>";
|
||||
}
|
||||
|
||||
var lastTypedLetter;
|
||||
$("#search_input").on("keyup", function(){
|
||||
// Auto submit when user pauses it's typing
|
||||
clearTimeout(lastTypedLetter);
|
||||
lastTypedLetter = setTimeout(function (){
|
||||
$("#search_form").submit();
|
||||
}, 300);
|
||||
});
|
||||
$("#search_input").on("change", function(e){
|
||||
// Don't send request when leaving the text area
|
||||
// It has already been send by the keypress event
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
// Auto fill from get arguments
|
||||
var urlParams = new URLSearchParams(window.location.search);
|
||||
if (urlParams.has("search"))
|
||||
$("input[name='search']").first().prop("value", urlParams.get("search"));
|
||||
autofillCheckboxRadio("department");
|
||||
autofillCheckboxRadio("credit_type");
|
||||
autofillCheckboxRadio("semester");
|
||||
|
||||
// Allow unchecking a radio button when we click on it
|
||||
// Keep a state of what is checked
|
||||
var formStates = {};
|
||||
function radioCheckToggle(e){
|
||||
if (formStates[this.name] == this.value){
|
||||
this.checked = false;
|
||||
formStates[this.name] = "";
|
||||
// Fire an update since the browser does not do it in this situation
|
||||
$("#search_form").submit();
|
||||
return;
|
||||
}
|
||||
formStates[this.name] = this.value;
|
||||
}
|
||||
|
||||
$("input[type='radio']").each(function() {
|
||||
$(this).on("click", radioCheckToggle);
|
||||
// Get current state
|
||||
if ($(this).prop("checked")){
|
||||
formStates[$(this).attr("name")] = $(this).attr("value");
|
||||
}
|
||||
});
|
||||
|
||||
var autumn_and_spring = $("input[value='AUTUMN_AND_SPRING']").first();
|
||||
var autumn = $("input[value='AUTUMN']").first();
|
||||
var spring = $("input[value='SPRING']").first();
|
||||
|
||||
// Make autumn and spring hidden if js is enabled
|
||||
autumn_and_spring.parent().hide();
|
||||
|
||||
// Fill json field if js is enabled
|
||||
$("input[name='json']").first().prop("value", "true");
|
||||
|
||||
// Set correctly state of what is checked
|
||||
if (autumn_and_spring.prop("checked")){
|
||||
autumn.prop("checked", true);
|
||||
spring.prop("checked", true);
|
||||
autumn_and_spring.prop("checked", false);
|
||||
function update_query_string(key, value) {
|
||||
const url = new URL(window.location.href);
|
||||
console.log(value)
|
||||
console.log(!!value)
|
||||
if (!value) {
|
||||
url.searchParams.delete(key)
|
||||
} else if (Array.isArray(value)) {
|
||||
url.searchParams.delete(key)
|
||||
value.forEach((v) => url.searchParams.append(key, v))
|
||||
} else {
|
||||
url.searchParams.set(key, value);
|
||||
}
|
||||
history.pushState(null, document.title, url.toString());
|
||||
}
|
||||
|
||||
// Handle submit here and modify autumn and spring here
|
||||
$("#search_form").submit(function(e) {
|
||||
e.preventDefault();
|
||||
if (autumn.prop("checked") && spring.prop("checked")){
|
||||
autumn_and_spring.prop("checked", true);
|
||||
autumn.prop("checked", false);
|
||||
spring.prop("checked", false);
|
||||
}
|
||||
{#
|
||||
How does this work :
|
||||
|
||||
// Do query
|
||||
var xhr = new XMLHttpRequest();
|
||||
$.ajax({
|
||||
type: "GET",
|
||||
url: "{{ url('pedagogy:guide') }}",
|
||||
data: $(this).serialize(),
|
||||
tryCount: 0,
|
||||
retryLimit: 10,
|
||||
xhr: function(){
|
||||
return xhr;
|
||||
},
|
||||
success: function(data){
|
||||
// Update URL
|
||||
history.pushState({}, null, xhr.responseURL.replace("&json=true", ""));
|
||||
// Update content
|
||||
$("#dynamic_view_content").html("");
|
||||
for (key in data){
|
||||
$("#dynamic_view_content").append(uvJSONToHTML(data[key]));
|
||||
}
|
||||
},
|
||||
error: function(){
|
||||
console.log(`try ${this.tryCount}`);
|
||||
if (this.tryCount++ <= this.retryLimit){
|
||||
$("dynamic_view_content").html("");
|
||||
$.ajax(this);
|
||||
return;
|
||||
}
|
||||
$("#dynamic_view_content").html("<tr><td></td><td>{% trans %}Error connecting to the server{% endtrans %}</td></tr>");
|
||||
}
|
||||
});
|
||||
The page contains two main elements : the form and the results.
|
||||
The form contains multiple inputs, allowing the user to apply the filter of its choice.
|
||||
Each modification of those filters will modify the GET parameters of the URL,
|
||||
then fetch the corresponding data from the API.
|
||||
This data will then be displayed on the result part of the page.
|
||||
#}
|
||||
document.addEventListener("alpine:init", () => {
|
||||
Alpine.data("uv_search", () => ({
|
||||
uvs: [],
|
||||
search: initialUrlParams.get("search") || "",
|
||||
department: initialUrlParams.getAll("department"),
|
||||
credit_type: initialUrlParams.getAll("credit_type"),
|
||||
{# The semester is easier to use on the backend as an enum (spring/autumn/both/none)
|
||||
and easier to use on the frontend as an array ([spring, autumn]).
|
||||
Thus there is some conversion involved when both communicate together #}
|
||||
semester: initialUrlParams.has("semester") ?
|
||||
initialUrlParams.get("semester").split("_AND_") : [],
|
||||
|
||||
// Restore autumn and spring for perfect illusion
|
||||
if (autumn_and_spring.prop("checked")){
|
||||
autumn_and_spring.prop("checked", false);
|
||||
autumn.prop("checked", true);
|
||||
spring.prop("checked", true);
|
||||
}
|
||||
});
|
||||
async init() {
|
||||
["search", "department", "credit_type", "semester"].forEach((param) => {
|
||||
this.$watch(param, async (value) => {
|
||||
update_query_string(param, value);
|
||||
await this.fetch_data(); {# reload data on form change #}
|
||||
});
|
||||
})
|
||||
await this.fetch_data(); {# load initial data #}
|
||||
},
|
||||
|
||||
// Auto send on change
|
||||
$("#search_form").on("change", function(e){
|
||||
$(this).submit();
|
||||
});
|
||||
async fetch_data() {
|
||||
const url = "{{ url("api:fetch_uvs") }}" + window.location.search;
|
||||
this.uvs = await (await fetch(url)).json();
|
||||
}
|
||||
}))
|
||||
})
|
||||
</script>
|
||||
{% endblock content %}
|
@ -51,29 +51,31 @@
|
||||
if (today.getMonth() < 7) { // student year starts in september
|
||||
year--
|
||||
}
|
||||
const url = "{{ url('api:uv_endpoint') }}?year=" + year + "&code=" + codeInput.value
|
||||
const url = `/api/uv/${year}/${codeInput.value}`;
|
||||
deleteQuickNotifs()
|
||||
|
||||
$.ajax({
|
||||
dataType: "json",
|
||||
url: url,
|
||||
success: function(data, _, xhr) {
|
||||
if (xhr.status != 200) {
|
||||
if (xhr.status !== 200) {
|
||||
createQuickNotif("{% trans %}Unknown UE code{% endtrans %}")
|
||||
return
|
||||
}
|
||||
for (let key in data) {
|
||||
if (data.hasOwnProperty(key)) {
|
||||
const el = document.querySelector('[name="' + key + '"]')
|
||||
if (el.tagName == 'TEXTAREA') {
|
||||
el.parentNode.querySelector('.CodeMirror').CodeMirror.setValue(data[key])
|
||||
Object.entries(data)
|
||||
.filter(([_, val]) => !!val) // skip entries with null or undefined value
|
||||
.map(([key, val]) => { // convert keys to DOM elements
|
||||
return [document.querySelector('[name="' + key + '"]'), val];
|
||||
})
|
||||
.filter(([elem, _]) => !!elem) // skip non-existing DOM elements
|
||||
.forEach(([elem, val]) => { // write the value in the form field
|
||||
if (elem.tagName === 'TEXTAREA') {
|
||||
// MD editor text input
|
||||
elem.parentNode.querySelector('.CodeMirror').CodeMirror.setValue(val);
|
||||
} else {
|
||||
el.value = data[key]
|
||||
elem.value = val;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
createQuickNotif('{% trans %}Successful autocomplete{% endtrans %}')
|
||||
},
|
||||
error: function(_, _, statusMessage) {
|
||||
|
Reference in New Issue
Block a user