Use AJAX to edit options and questions

This commit is contained in:
Tom Hubrecht 2021-08-21 22:58:01 +02:00
parent e14ceca91a
commit f2b4e9bcfe
10 changed files with 474 additions and 349 deletions

View file

@ -0,0 +1,40 @@
{% load i18n markdown %}
<div class="panel-block" id="o_{{ o.pk }}">
{% if o.question.election.start_date > current_time %}
<span class="tags has-addons mb-0">
<a class="tag is-danger is-light is-outlined has-tooltip-primary mb-0 del" data-tooltip="{% trans "Supprimer" %}" data-url="{% url 'election.del-option' o.pk %}" data-target="o_{{ o.pk }}">
<span class="icon">
<i class="fas fa-times"></i>
</span>
</a>
<a class="tag is-info is-light is-outlined has-tooltip-primary mb-0 modal-button" data-tooltip="{% trans "Modifier" %}" data-post_url="{% url 'election.mod-option' o.pk %}" data-target="modal-option" data-o_en="{{ o.text_en }}" data-o_fr="{{ o.text_fr }}" data-abbr="{{ o.abbreviation }}" data-title="{% trans "Modifier l'option" %}" data-type="option" data-parent="o_{{ o.pk }}">
<span class="icon">
<i class="fas fa-edit"></i>
</span>
</a>
</span>
{% elif o.question.election.tallied %}
<span class="tag {% if o.winner %}is-success{% else %}is-primary{% endif %}">
<span class="icon-text">
{% if q.vote_type == "select" %}
<span class="icon">
<i class="fas fa-vote-yea"></i>
</span>
<span>{{ o.nb_votes }}</span>
{% elif q.vote_type == "rank" %}
<span class="icon">
<i class="fas fa-layer-group"></i>
</span>
<span>{% if o.abbreviation %}{{ o.abbreviation }}{% else %}{{ forloop.counter }}{% endif %}</span>
{% endif %}
</span>
{% endif %}
</span>
<span class="ml-2">{{ o }}</span>
</div>

View file

@ -0,0 +1,71 @@
{% load i18n markdown %}
<div class="panel" id="q_{{ q.pk }}">
<div class="panel-heading is-size-6">
<div class="level">
<div class="level-left is-flex-shrink-1">
<div class="level-item is-flex-shrink-1">
<span class="icon-text">
<span class="icon">
<i class="fas fa-poll-h"></i>
</span>
<span>{{ q }}</span>
</span>
</div>
{% if q.election.start_date > current_time %}
<div class="level-item">
<a class="tag is-outlined is-light is-danger del" data-url="{% url 'election.del-question' q.pk %}" data-target="q_{{ q.pk }}">
<span class="icon-text">
<span class="icon">
<i class="fas fa-times"></i>
</span>
<span>{% trans "Supprimer" %}</span>
</span>
</a>
<a class="tag is-outlined is-light is-info ml-1 modal-button" data-post_url="{% url 'election.mod-question' q.pk %}" data-target="modal-question" data-q_type="{{ q.type }}" data-q_en="{{ q.text_en }}" data-q_fr="{{ q.text_fr }}" data-title="{% trans "Modifier la question" %}" data-type="question" data-parent="q_{{ q.pk }}">
<span class="icon-text">
<span class="icon">
<i class="fas fa-edit"></i>
</span>
<span>{% trans "Modifier" %}</span>
</span>
</a>
</div>
{% endif %}
</div>
<div class="level-right">
<span class="tag is-outlined is-primary is-light">{{ q.get_type_display }}</span>
</div>
</div>
</div>
{# Liste des options possibles #}
<div id="options_{{ q.pk }}">
{% for o in q.options.all %}
{% include 'elections/admin/option.html' %}
{% endfor %}
</div>
{# Permet d'afficher une ligne #}
<div class="panel-block py-0"></div>
{# Affiche plus d'informations sur le résultat #}
{% if q.election.tallied %}
{{ q.get_results_data }}
{% endif %}
{# Rajout d'une option #}
{% if q.election.start_date > current_time %}
<div class="panel-block">
<button class="button modal-button is-primary is-outlined is-fullwidth option" data-post_url="{% url 'election.add-option' q.pk %}" data-target="modal-option" data-title="{% trans "Rajouter une option" %}" data-type="option" data-next="options_{{ q.pk }}">
<span class="icon">
<i class="fas fa-plus"></i>
</span>
<span>{% trans "Rajouter une option" %}</span>
</button>
</div>
{% endif %}
</div>

View file

@ -2,12 +2,14 @@
{% load i18n markdown %} {% load i18n markdown %}
{% block extra_head %} {% block custom_js %}
<script> <script>
document.addEventListener('DOMContentLoaded', () => { const _fm = b => {
_$('.modal-button').forEach(b => {
b.addEventListener('click', () => { b.addEventListener('click', () => {
const f = _$('form', _id(b.dataset.target), false); const f = _$('form', _id(b.dataset.target), false);
f.dataset.next = b.dataset.next;
f.dataset.modal = b.dataset.target;
f.dataset.origin = b.dataset.parent
if (b.dataset.type == 'question') { if (b.dataset.type == 'question') {
_$('[name="text_fr"]', f, false).value = b.dataset.q_fr || ''; _$('[name="text_fr"]', f, false).value = b.dataset.q_fr || '';
@ -19,6 +21,52 @@
_$('[name="abbreviation"]', f, false).value = b.dataset.abbr || ''; _$('[name="abbreviation"]', f, false).value = b.dataset.abbr || '';
} }
}); });
}
_$('.modal-button').forEach(_fm);
const _del = d => {
d.addEventListener('click', () => {
_get(d.dataset.url, r => {
if (r.success && r.action == 'delete') {
_id(d.dataset.target).remove()
}
if (r.message) {
_notif(r.message.content, r.message.class);
}
});
});
}
_$('.del').forEach(_del);
_$('form').forEach(f => {
f.addEventListener('submit', event => {
event.preventDefault();
_post(f.action, f, r => {
if (r.success) {
const e = document.createElement('div');
e.innerHTML = r.html;
// On initialise les boutons
_$('.modal-button', e).forEach(b => {
_om(b);
_fm(b);
});
_$('.del', e).forEach(_del);
if (r.action == 'create') {
_id(f.dataset.next).appendChild(e.childNodes[1]);
} else if (r.action == 'update') {
const n = _id(f.dataset.origin);
n.parentNode.replaceChild(e.childNodes[1], n);
}
// On ferme le modal
document.documentElement.classList.remove('is-clipped');
_id(f.dataset.modal).classList.remove('is-active');
}
});
}); });
}); });
@ -213,109 +261,11 @@
{% endif %} {% endif %}
{# Liste des questions #} {# Liste des questions #}
<div id="questions" class="block">
{% for q in election.questions.all %} {% for q in election.questions.all %}
<div class="panel" id="q_{{ q.pk }}"> {% include 'elections/admin/question.html' %}
<div class="panel-heading is-size-6">
<div class="level">
<div class="level-left is-flex-shrink-1">
<div class="level-item is-flex-shrink-1">
<span class="icon-text">
<span class="icon">
<i class="fas fa-poll-h"></i>
</span>
<span>{{ q }}</span>
</span>
</div>
{% if election.start_date > current_time %}
<div class="level-item">
<a class="tag is-outlined is-light is-danger" href="{% url 'election.del-question' q.pk %}">
<span class="icon-text">
<span class="icon">
<i class="fas fa-times"></i>
</span>
<span>{% trans "Supprimer" %}</span>
</span>
</a>
<a class="tag is-outlined is-light is-info ml-1 modal-button" data-post_url="{% url 'election.mod-question' q.pk %}" data-target="modal-question" data-q_type="{{ q.type }}" data-q_en="{{ q.text_en }}" data-q_fr="{{ q.text_fr }}" data-title="{% trans "Modifier la question" %}" data-type="question">
<span class="icon-text">
<span class="icon">
<i class="fas fa-edit"></i>
</span>
<span>{% trans "Modifier" %}</span>
</span>
</a>
</div>
{% endif %}
</div>
<div class="level-right">
<span class="tag is-outlined is-primary is-light">{{ q.get_type_display }}</span>
</div>
</div>
</div>
{# Liste des options possibles #}
{% for o in q.options.all %}
<div class="panel-block" id="o_{{ o.pk }}">
{% if election.start_date > current_time %}
<span class="tags has-addons mb-0">
<a class="tag is-danger is-light is-outlined has-tooltip-primary mb-0" data-tooltip="{% trans "Supprimer" %}" href="{% url 'election.del-option' o.pk %}">
<span class="icon">
<i class="fas fa-times"></i>
</span>
</a>
<a class="tag is-info is-light is-outlined has-tooltip-primary mb-0 modal-button" data-tooltip="{% trans "Modifier" %}" data-post_url="{% url 'election.mod-option' o.pk %}" data-target="modal-option" data-o_en="{{ o.text_en }}" data-o_fr="{{ o.text_fr }}" data-abbr="{{ o.abbreviation }}" data-title="{% trans "Modifier l'option" %}" data-type="option">
<span class="icon">
<i class="fas fa-edit"></i>
</span>
</a>
</span>
{% elif election.tallied %}
<span class="tag {% if o.winner %}is-success{% else %}is-primary{% endif %}">
<span class="icon-text">
{% if q.vote_type == "select" %}
<span class="icon">
<i class="fas fa-vote-yea"></i>
</span>
<span>{{ o.nb_votes }}</span>
{% elif q.vote_type == "rank" %}
<span class="icon">
<i class="fas fa-layer-group"></i>
</span>
<span>{% if o.abbreviation %}{{ o.abbreviation }}{% else %}{{ forloop.counter }}{% endif %}</span>
{% endif %}
</span>
{% endif %}
</span>
<span class="ml-2">{{ o }}</span>
</div>
{% endfor %} {% endfor %}
{# Affiche plus d'informations sur le résultat #}
{% if election.tallied %}
{{ q.get_results_data }}
{% endif %}
{# Rajout d'une option #}
{% if election.start_date > current_time %}
<div class="panel-block">
<button class="button modal-button is-primary is-outlined is-fullwidth option" data-post_url="{% url 'election.add-option' q.pk %}" data-target="modal-option" data-title="{% trans "Rajouter une option" %}">
<span class="icon">
<i class="fas fa-plus"></i>
</span>
<span>{% trans "Rajouter une option" %}</span>
</button>
</div> </div>
{% endif %}
</div>
{% endfor %}
{# Rajout d'une question #} {# Rajout d'une question #}
{% if election.start_date > current_time %} {% if election.start_date > current_time %}
@ -330,7 +280,7 @@
<div class="columns is-centered" id="q_add"> <div class="columns is-centered" id="q_add">
<div class="column is-two-thirds"> <div class="column is-two-thirds">
<button class="button modal-button is-primary is-outlined is-fullwidth question" data-post_url="{% url 'election.add-question' election.pk %}" data-target="modal-question" data-title="{% trans "Rajouter une question" %}"> <button class="button modal-button is-primary is-outlined is-fullwidth question" data-post_url="{% url 'election.add-question' election.pk %}" data-target="modal-question" data-title="{% trans "Rajouter une question" %}" data-next="questions" data-type="question">
<span class="icon"> <span class="icon">
<i class="fas fa-question"></i> <i class="fas fa-question"></i>
</span> </span>

View file

@ -49,33 +49,33 @@ urlpatterns = [
# Question views # Question views
path( path(
"add-question/<int:pk>", "add-question/<int:pk>",
views.AddQuestionView.as_view(), views.CreateQuestionView.as_view(),
name="election.add-question", name="election.add-question",
), ),
path( path(
"mod-question/<int:pk>", "mod-question/<int:pk>",
views.ModQuestionView.as_view(), views.UpdateQuestionView.as_view(),
name="election.mod-question", name="election.mod-question",
), ),
path( path(
"del-question/<int:pk>", "del-question/<int:pk>",
views.DelQuestionView.as_view(), views.DeleteQuestionView.as_view(),
name="election.del-question", name="election.del-question",
), ),
# Option views # Option views
path( path(
"add-option/<int:pk>", "add-option/<int:pk>",
views.AddOptionView.as_view(), views.CreateOptionView.as_view(),
name="election.add-option", name="election.add-option",
), ),
path( path(
"mod-option/<int:pk>", "mod-option/<int:pk>",
views.ModOptionView.as_view(), views.UpdateOptionView.as_view(),
name="election.mod-option", name="election.mod-option",
), ),
path( path(
"del-option/<int:pk>", "del-option/<int:pk>",
views.DelOptionView.as_view(), views.DeleteOptionView.as_view(),
name="election.del-option", name="election.del-option",
), ),
# Common views # Common views

View file

@ -8,10 +8,8 @@ from django.db import transaction
from django.http import Http404, HttpResponse, HttpResponseRedirect from django.http import Http404, HttpResponse, HttpResponseRedirect
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from django.utils.decorators import method_decorator
from django.utils.text import slugify from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.decorators.http import require_POST
from django.views.generic import ( from django.views.generic import (
CreateView, CreateView,
DetailView, DetailView,
@ -21,7 +19,8 @@ from django.views.generic import (
View, View,
) )
from shared.views import BackgroundUpdateView from shared.json.views import JsonCreateView, JsonDeleteView, JsonUpdateView
from shared.views import BackgroundUpdateView, TimeMixin
from .forms import ( from .forms import (
DeleteVoteForm, DeleteVoteForm,
@ -72,7 +71,7 @@ class ElectionCreateView(AdminOnlyMixin, SuccessMessageMixin, CreateView):
return super().form_valid(form) return super().form_valid(form)
class ElectionAdminView(CreatorOnlyMixin, DetailView): class ElectionAdminView(CreatorOnlyMixin, TimeMixin, DetailView):
model = Election model = Election
template_name = "elections/election_admin.html" template_name = "elections/election_admin.html"
@ -82,7 +81,6 @@ class ElectionAdminView(CreatorOnlyMixin, DetailView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
kwargs.update( kwargs.update(
{ {
"current_time": timezone.now(),
"question_types": QUESTION_TYPES, "question_types": QUESTION_TYPES,
"o_form": OptionForm, "o_form": OptionForm,
"q_form": QuestionForm, "q_form": QuestionForm,
@ -352,46 +350,27 @@ class ElectionArchiveView(ClosedElectionMixin, BackgroundUpdateView):
# ############################################################################# # #############################################################################
@method_decorator(require_POST, name="dispatch") class CreateQuestionView(CreatorOnlyEditMixin, TimeMixin, JsonCreateView):
class AddQuestionView(CreatorOnlyEditMixin, CreateView):
model = Election model = Election
form_class = QuestionForm form_class = QuestionForm
context_object_name = "q"
def get_success_url(self): template_name = "elections/admin/question.html"
return reverse("election.admin", args=[self.election.pk]) + "#q_add"
def form_valid(self, form): def form_valid(self, form):
self.election = self.get_object() form.instance.election = self.get_object()
# On ajoute l'élection voulue à la question créée
form.instance.election = self.election
return super().form_valid(form) return super().form_valid(form)
class ModQuestionView(CreatorOnlyEditMixin, SuccessMessageMixin, UpdateView): class UpdateQuestionView(CreatorOnlyEditMixin, TimeMixin, JsonUpdateView):
model = Question model = Question
form_class = QuestionForm form_class = QuestionForm
success_message = _("Question modifiée avec succès !") context_object_name = "q"
template_name = "elections/question_update.html" template_name = "elections/admin/question.html"
def get_success_url(self):
return (
reverse("election.admin", args=[self.object.election.pk])
+ f"#q_{self.object.pk}"
)
class DelQuestionView(CreatorOnlyEditMixin, BackgroundUpdateView): class DeleteQuestionView(CreatorOnlyEditMixin, JsonDeleteView):
model = Question model = Question
success_message = _("Question supprimée !") message = _("Question supprimée !")
def get_redirect_url(self, *args, **kwargs):
return reverse("election.admin", args=[self.election.pk]) + "#q_add"
def get(self, request, *args, **kwargs):
question = self.get_object()
self.election = question.election
question.delete()
return super().get(request, *args, **kwargs)
# ############################################################################# # #############################################################################
@ -399,49 +378,27 @@ class DelQuestionView(CreatorOnlyEditMixin, BackgroundUpdateView):
# ############################################################################# # #############################################################################
@method_decorator(require_POST, name="dispatch") class CreateOptionView(CreatorOnlyEditMixin, TimeMixin, JsonCreateView):
class AddOptionView(CreatorOnlyEditMixin, CreateView):
model = Question model = Question
form_class = OptionForm form_class = OptionForm
context_object_name = "o"
def get_success_url(self): template_name = "elections/admin/option.html"
return (
reverse("election.admin", args=[self.question.election.pk])
+ f"#q_{self.question.pk}"
)
def form_valid(self, form): def form_valid(self, form):
self.question = self.get_object() form.instance.question = self.get_object()
# On ajoute l'élection voulue à la question créée
form.instance.question = self.question
return super().form_valid(form) return super().form_valid(form)
class ModOptionView(CreatorOnlyEditMixin, SuccessMessageMixin, UpdateView): class UpdateOptionView(CreatorOnlyEditMixin, TimeMixin, JsonUpdateView):
model = Option model = Option
form_class = OptionForm form_class = OptionForm
success_message = _("Option modifiée avec succès !") context_object_name = "o"
template_name = "elections/option_update.html" template_name = "elections/admin/option.html"
def get_success_url(self):
return (
reverse("election.admin", args=[self.object.question.election.pk])
+ f"#o_{self.object.pk}"
)
class DelOptionView(CreatorOnlyEditMixin, BackgroundUpdateView): class DeleteOptionView(CreatorOnlyEditMixin, JsonDeleteView):
model = Option model = Option
success_message = _("Option supprimée !") message = _("Option supprimée !")
def get_redirect_url(self, *args, **kwargs):
return reverse("election.admin", args=[self.election.pk]) + "#q_add"
def get(self, request, *args, **kwargs):
option = self.get_object()
self.election = option.question.election
option.delete()
return super().get(request, *args, **kwargs)
# ############################################################################# # #############################################################################

93
shared/json/views.py Normal file
View file

@ -0,0 +1,93 @@
from django.http import JsonResponse
from django.utils.decorators import method_decorator
from django.views.decorators.http import require_POST
from django.views.generic.base import TemplateResponseMixin, View
from django.views.generic.detail import SingleObjectMixin
from django.views.generic.edit import FormMixin, ModelFormMixin, ProcessFormView
# #############################################################################
# Views for use with AJAX
# #############################################################################
class JsonMixin:
success = True
errors = {}
def get_data(self, **kwargs):
data = {"success": self.success, "errors": self.errors}
data.update(kwargs)
return data
def render_to_json(self, **kwargs):
return JsonResponse(self.get_data(**kwargs))
class JsonFormMixin(JsonMixin, FormMixin):
def form_valid(self, form):
"""If the form is valid, return success"""
return self.render_to_json()
def form_invalid(self, form):
"""If the form is invalid, return the errors and no success"""
return self.render_to_json(success=False, errors=form.errors)
class JsonModelFormMixin(JsonFormMixin, ModelFormMixin):
def form_valid(self, form):
"""Override form_valid to return a JSON response"""
self.object = form.save()
return super().form_valid(form)
class JsonMessageMixin:
message = ""
def get_message(self):
return {"content": self.message, "class": "success"}
def get_data(self, **kwargs):
kwargs.update(message=self.get_message())
return super().get_data(**kwargs)
class JsonDetailView(JsonMixin, SingleObjectMixin, TemplateResponseMixin, View):
def get(self, request, *args, **kwargs):
self.object = self.get_object()
context = self.get_context_data(object=self.object)
return self.render_to_json(html=self.render_to_response(context))
class JsonDeleteView(JsonMessageMixin, JsonDetailView):
def get(self, request, *args, **kwargs):
obj = self.get_object()
obj.delete()
return self.render_to_json(action="delete")
@method_decorator(require_POST, name="dispatch")
class JsonCreateView(
JsonMessageMixin, JsonModelFormMixin, TemplateResponseMixin, ProcessFormView
):
def render_to_json(self, **kwargs):
context = self.get_context_data(object=self.object)
kwargs.update(
html=self.render_to_response(context).rendered_content, action="create"
)
return super().render_to_json(**kwargs)
@method_decorator(require_POST, name="dispatch")
class JsonUpdateView(
JsonMessageMixin, JsonModelFormMixin, TemplateResponseMixin, ProcessFormView
):
def post(self, request, *args, **kwargs):
self.object = self.get_object()
return super().post(request, *args, **kwargs)
def render_to_json(self, **kwargs):
context = self.get_context_data(object=self.object)
kwargs.update(
html=self.render_to_response(context).rendered_content, action="update"
)
return super().render_to_json(**kwargs)

167
shared/static/js/main.js Normal file
View file

@ -0,0 +1,167 @@
const _$ = (s, e = document, a = true) => {
const r = e.querySelectorAll(s) || [];
if (!a) {
return r.item(0);
}
return r;
};
const _id = s => document.getElementById(s);
const _get = (u, f) => {
const xhr = new XMLHttpRequest();
xhr.responseType = 'json';
xhr.addEventListener('load', () => {
f(xhr.response);
});
xhr.open('GET', u);
xhr.send();
};
const _post = (u, d, f) => {
const xhr = new XMLHttpRequest();
const fd = new FormData(d);
xhr.responseType = 'json';
xhr.addEventListener('load', () => {
f(xhr.response);
});
xhr.open('POST', u);
xhr.send(fd);
};
const _notif = (m, c) => {
const n = document.createElement('div');
n.classList.add('notification', 'is-light');
if (c !== undefined) {
n.classList.add(`is-${c}`);
}
n.innerHTML = `${m}<button class="delete"></button>`;
_id('notifications').insertBefore(n, _id('content'))
_$('.delete', n, false).addEventListener('click', () => {
n.remove();
});
}
const _om = b => {
b.addEventListener('click', () => {
const m = _id(b.dataset.target);
if ('post_url' in b.dataset) {
_$('form', m, false).action = b.dataset.post_url;
};
if ('title' in b.dataset) {
_$('.modal-card-title', m, false).innerHTML = b.dataset.title;
};
document.documentElement.classList.add('is-clipped');
m.classList.add('is-active');
});
}
const _cm = b => {
b.addEventListener('click', () => {
document.documentElement.classList.remove('is-clipped');
_id(b.dataset.closes).classList.remove('is-active')
});
}
const _sm = '.modal';
const _smb = '.modal-button';
const _smc = '.modal-background, .modal-close, .modal-card-head .delete, .modal-card-foot .button-close';
document.addEventListener('DOMContentLoaded', () => {
// Delete notifications
_$('.notification .delete').forEach(d => {
const n = d.parentNode;
d.addEventListener('click', () => {
n.remove();
});
});
// Interact with dropdowns
const ds = _$('.dropdown:not(.is-hoverable)');
ds.forEach(d => {
d.addEventListener('click', e => {
e.stopPropagation();
d.classList.toggle('is-active');
});
});
document.addEventListener('click', () => {
ds.forEach(d => {
d.classList.remove('is-active');
});
});
// Interact with modals
const ms = _$(_sm);
const mbs = _$(_smb);
const mcs = _$(_smc);
mbs.forEach(_om);
mcs.forEach(_cm);
document.addEventListener('keydown', ev => {
const e = ev || window.event;
if (e.keyCode === 27) {
ds.forEach(d => {
d.classList.remove('is-active');
});
document.documentElement.classList.remove('is-clipped');
ms.forEach(m => {
m.classList.remove('is-active');
});
}
});
// Language selection
_$('.dropdown-item.lang-selector').forEach(l => {
l.addEventListener('click', () => {
_id('lang-input').value = l.dataset.lang;
_id('lang-form').submit();
});
});
// Disable button after form submission
_$('form').forEach(f => {
f.addEventListener('submit', () => {
_$('button[type=submit]', f).forEach(b => {
b.classList.add('is-loading');
setTimeout(() => {
b.classList.remove('is-loading');
}, 1000);
});
});
});
// Scroll to top button
const up = _id('scroll-button');
if (document.documentElement.scrollTop >= 100) {
up.classList.remove('is-hidden');
}
window.onscroll = () => {
if (document.documentElement.scrollTop >= 100) {
up.classList.remove('is-hidden');
} else {
up.classList.add('is-hidden');
}
}
up.addEventListener('click', () => {
window.scrollTo({
top: 0,
behavior: 'smooth',
});
});
});

View file

@ -17,167 +17,7 @@
<link rel="stylesheet" href="{% static 'vendor/font-awesome/css/font-awesome.min.css' %}"> <link rel="stylesheet" href="{% static 'vendor/font-awesome/css/font-awesome.min.css' %}">
<link rel="stylesheet" href="{% static 'vendor/font-awesome/css/solid.min.css' %}"> <link rel="stylesheet" href="{% static 'vendor/font-awesome/css/solid.min.css' %}">
<script> <script src="{% static 'js/main.js' %}"></script>
const _$ = (s, e = document, a = true) => {
const r = e.querySelectorAll(s) || [];
if (!a) {
return r.item(0);
}
return r;
};
const _id = s => document.getElementById(s);
const _get = (u, f) => {
const xhr = new XMLHttpRequest();
xhr.responseType = 'json';
xhr.addEventListener('load', () => {
f(xhr.response);
});
xhr.open('GET', u);
xhr.send();
};
const _post = (u, d, f) => {
const xhr = new XMLHttpRequest();
const fd = new FormData(d);
xhr.responseType = 'json';
xhr.addEventListener('load', () => {
f(xhr.response);
});
xhr.open('POST', u);
xhr.send(fd);
};
const _notif = (m, c = 'success') => {
const n = document.createElement('div');
n.classList.add('notification', `is-${c}`, 'is-light');
n.innerHTML = `${m}<button class="delete"></button>`;
_id('notifications').insertBefore(n, _id('content'))
_$('.delete', n, false).addEventListener('click', () => {
n.remove();
});
}
document.addEventListener('DOMContentLoaded', () => {
// Delete notifications
_$('.notification .delete').forEach(d => {
const n = d.parentNode;
d.addEventListener('click', () => {
n.remove();
});
});
// Interact with dropdowns
const ds = _$('.dropdown:not(.is-hoverable)');
ds.forEach(d => {
d.addEventListener('click', e => {
e.stopPropagation();
d.classList.toggle('is-active');
});
});
document.addEventListener('click', () => {
ds.forEach(d => {
d.classList.remove('is-active');
});
});
// Interact with modals
const ms = _$('.modal');
const mbs = _$('.modal-button');
const mcs = _$('.modal-background, .modal-close, .modal-card-head .delete, .modal-card-foot .button-close');
mbs.forEach(b => {
b.addEventListener('click', () => {
const m = _id(b.dataset.target);
if ('post_url' in b.dataset) {
_$('form', m, false).action = b.dataset.post_url;
};
if ('title' in b.dataset) {
_$('.modal-card-title', m, false).innerHTML = b.dataset.title;
};
document.documentElement.classList.add('is-clipped');
m.classList.add('is-active');
});
});
mcs.forEach(d => {
d.addEventListener('click', () => {
document.documentElement.classList.remove('is-clipped');
ms.forEach(m => {
m.classList.remove('is-active');
});
});
});
document.addEventListener('keydown', ev => {
const e = ev || window.event;
if (e.keyCode === 27) {
ds.forEach(d => {
d.classList.remove('is-active');
});
document.documentElement.classList.remove('is-clipped');
ms.forEach(m => {
m.classList.remove('is-active');
});
}
});
// Language selection
_$('.dropdown-item.lang-selector').forEach(l => {
l.addEventListener('click', () => {
_id('lang-input').value = l.dataset.lang;
_id('lang-form').submit();
});
});
// Disable button after form submission
_$('form').forEach(f => {
f.addEventListener('submit', () => {
_$('button[type=submit]', f).forEach(b => {
b.classList.add('is-loading');
setTimeout(() => {
b.classList.remove('is-loading');
}, 1000);
});
});
});
// Scroll to top button
const up = _id('scroll-button');
if (document.documentElement.scrollTop >= 100) {
up.classList.remove('is-hidden');
}
window.onscroll = () => {
if (document.documentElement.scrollTop >= 100) {
up.classList.remove('is-hidden');
} else {
up.classList.add('is-hidden');
}
}
up.addEventListener('click', () => {
window.scrollTo({
top: 0,
behavior: 'smooth',
});
});
});
</script>
{% block extra_head %}{% endblock extra_head %} {% block extra_head %}{% endblock extra_head %}

View file

@ -1,14 +1,14 @@
{% load i18n %} {% load i18n %}
<div class="modal" id="modal-{{ modal_id }}"> <div class="modal" id="modal-{{ modal_id }}">
<div class="modal-background"></div> <div class="modal-background" data-closes="modal-{{ modal_id }}"></div>
<div class="modal-card"> <div class="modal-card">
<form method="post" action="{{ post_url }}"> <form method="post" action="{{ post_url }}">
{% csrf_token %} {% csrf_token %}
<header class="modal-card-head"> <header class="modal-card-head">
<p class="modal-card-title">{{ modal_title }}</p> <p class="modal-card-title">{{ modal_title }}</p>
<a class="delete" aria-label="close"></a> <a class="delete" aria-label="close" data-closes="modal-{{ modal_id }}"></a>
</header> </header>
<section class="modal-card-body"> <section class="modal-card-body">
@ -23,7 +23,7 @@
<span>{% trans "Enregistrer" %}</span> <span>{% trans "Enregistrer" %}</span>
</button> </button>
<a class="button is-primary button-close"> <a class="button is-primary button-close" data-closes="modal-{{ modal_id }}">
<span class="icon"> <span class="icon">
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
</span> </span>

View file

@ -1,4 +1,5 @@
from django.contrib import messages from django.contrib import messages
from django.utils import timezone
from django.views.generic import RedirectView from django.views.generic import RedirectView
# ############################################################################# # #############################################################################
@ -17,3 +18,9 @@ class BackgroundUpdateView(RedirectView):
if success_message: if success_message:
messages.success(self.request, success_message) messages.success(self.request, success_message)
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
class TimeMixin:
def get_context_data(self, **kwargs):
kwargs.update(current_time=timezone.now())
return super().get_context_data(**kwargs)