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,21 +2,69 @@
{% load i18n markdown %}
{% block extra_head %}
{% block custom_js %}
<script>
document.addEventListener('DOMContentLoaded', () => {
_$('.modal-button').forEach(b => {
b.addEventListener('click', () => {
const f = _$('form', _id(b.dataset.target), false);
const _fm = b => {
b.addEventListener('click', () => {
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') {
_$('[name="text_fr"]', f, false).value = b.dataset.q_fr || '';
_$('[name="text_en"]', f, false).value = b.dataset.q_en || '';
_$('[name="type"]', f, false).value = b.dataset.q_type || 'assentiment';
} else if (b.dataset.type == 'option') {
_$('[name="text_fr"]', f, false).value = b.dataset.o_fr || '';
_$('[name="text_en"]', f, false).value = b.dataset.o_en || '';
_$('[name="abbreviation"]', f, false).value = b.dataset.abbr || '';
if (b.dataset.type == 'question') {
_$('[name="text_fr"]', f, false).value = b.dataset.q_fr || '';
_$('[name="text_en"]', f, false).value = b.dataset.q_en || '';
_$('[name="type"]', f, false).value = b.dataset.q_type || 'assentiment';
} else if (b.dataset.type == 'option') {
_$('[name="text_fr"]', f, false).value = b.dataset.o_fr || '';
_$('[name="text_en"]', f, false).value = b.dataset.o_en || '';
_$('[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 %}
{# Liste des questions #}
{% for q in election.questions.all %}
<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 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>
<div id="questions" class="block">
{% for q in election.questions.all %}
{% include 'elections/admin/question.html' %}
{% 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>
{% endif %}
</div>
{% endfor %}
{# Rajout d'une question #}
{% if election.start_date > current_time %}
@ -330,7 +280,7 @@
<div class="columns is-centered" id="q_add">
<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">
<i class="fas fa-question"></i>
</span>

View file

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

View file

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

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/solid.min.css' %}">
<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>
<script src="{% static 'js/main.js' %}"></script>
{% block extra_head %}{% endblock extra_head %}

View file

@ -1,14 +1,14 @@
{% load i18n %}
<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">
<form method="post" action="{{ post_url }}">
{% csrf_token %}
<header class="modal-card-head">
<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>
<section class="modal-card-body">
@ -23,7 +23,7 @@
<span>{% trans "Enregistrer" %}</span>
</button>
<a class="button is-primary button-close">
<a class="button is-primary button-close" data-closes="modal-{{ modal_id }}">
<span class="icon">
<i class="fas fa-times"></i>
</span>

View file

@ -1,4 +1,5 @@
from django.contrib import messages
from django.utils import timezone
from django.views.generic import RedirectView
# #############################################################################
@ -17,3 +18,9 @@ class BackgroundUpdateView(RedirectView):
if success_message:
messages.success(self.request, success_message)
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)