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,12 +2,14 @@
|
|||
{% load i18n markdown %}
|
||||
|
||||
|
||||
{% block extra_head %}
|
||||
{% block custom_js %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
_$('.modal-button').forEach(b => {
|
||||
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 || '';
|
||||
|
@ -19,6 +21,52 @@
|
|||
_$('[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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
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/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 %}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue