Use AJAX to edit options and questions
This commit is contained in:
parent
e14ceca91a
commit
f2b4e9bcfe
10 changed files with 474 additions and 349 deletions
40
elections/templates/elections/admin/option.html
Normal file
40
elections/templates/elections/admin/option.html
Normal 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>
|
71
elections/templates/elections/admin/question.html
Normal file
71
elections/templates/elections/admin/question.html
Normal 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>
|
|
@ -2,21 +2,69 @@
|
||||||
{% 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 || '';
|
||||||
_$('[name="text_en"]', f, false).value = b.dataset.q_en || '';
|
_$('[name="text_en"]', f, false).value = b.dataset.q_en || '';
|
||||||
_$('[name="type"]', f, false).value = b.dataset.q_type || 'assentiment';
|
_$('[name="type"]', f, false).value = b.dataset.q_type || 'assentiment';
|
||||||
} else if (b.dataset.type == 'option') {
|
} else if (b.dataset.type == 'option') {
|
||||||
_$('[name="text_fr"]', f, false).value = b.dataset.o_fr || '';
|
_$('[name="text_fr"]', f, false).value = b.dataset.o_fr || '';
|
||||||
_$('[name="text_en"]', f, false).value = b.dataset.o_en || '';
|
_$('[name="text_en"]', f, false).value = b.dataset.o_en || '';
|
||||||
_$('[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 #}
|
||||||
{% for q in election.questions.all %}
|
<div id="questions" class="block">
|
||||||
<div class="panel" id="q_{{ q.pk }}">
|
{% for q in election.questions.all %}
|
||||||
<div class="panel-heading is-size-6">
|
{% include 'elections/admin/question.html' %}
|
||||||
<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>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
93
shared/json/views.py
Normal 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
167
shared/static/js/main.js
Normal 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',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
|
@ -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 %}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue