Création de l'application de faq
This commit is contained in:
parent
f63dc2db55
commit
a4cadca34c
15 changed files with 348 additions and 3 deletions
0
faqs/__init__.py
Normal file
0
faqs/__init__.py
Normal file
28
faqs/forms.py
Normal file
28
faqs/forms.py
Normal file
|
@ -0,0 +1,28 @@
|
|||
from translated_fields import language_code_formfield_callback
|
||||
|
||||
from django import forms
|
||||
|
||||
from .models import Faq
|
||||
|
||||
|
||||
class FaqForm(forms.ModelForm):
|
||||
formfield_callback = language_code_formfield_callback
|
||||
|
||||
class Meta:
|
||||
model = Faq
|
||||
fields = [
|
||||
*Faq.title.fields,
|
||||
"anchor",
|
||||
*Faq.description.fields,
|
||||
*Faq.content.fields,
|
||||
]
|
||||
widgets = {
|
||||
"description_en": forms.Textarea(
|
||||
attrs={"rows": 4, "class": "is-family-monospace"}
|
||||
),
|
||||
"description_fr": forms.Textarea(
|
||||
attrs={"rows": 4, "class": "is-family-monospace"}
|
||||
),
|
||||
"content_en": forms.Textarea(attrs={"class": "is-family-monospace"}),
|
||||
"content_fr": forms.Textarea(attrs={"class": "is-family-monospace"}),
|
||||
}
|
66
faqs/migrations/0001_initial.py
Normal file
66
faqs/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,66 @@
|
|||
# Generated by Django 3.2.4 on 2021-06-15 08:34
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="Faq",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("title_fr", models.CharField(max_length=255, verbose_name="titre")),
|
||||
(
|
||||
"title_en",
|
||||
models.CharField(blank=True, max_length=255, verbose_name="titre"),
|
||||
),
|
||||
("description_fr", models.TextField(verbose_name="description")),
|
||||
(
|
||||
"description_en",
|
||||
models.TextField(blank=True, verbose_name="description"),
|
||||
),
|
||||
("content_fr", models.TextField(blank=True, verbose_name="contenu")),
|
||||
("content_en", models.TextField(blank=True, verbose_name="contenu")),
|
||||
(
|
||||
"last_modified",
|
||||
models.DateField(auto_now=True, verbose_name="mise à jour"),
|
||||
),
|
||||
("anchor", models.CharField(max_length=20, verbose_name="ancre")),
|
||||
(
|
||||
"author",
|
||||
models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="faqs",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"permissions": [("is_author", "Can create faqs")],
|
||||
},
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name="faq",
|
||||
constraint=models.UniqueConstraint(
|
||||
fields=("anchor",), name="unique_faq_anchor"
|
||||
),
|
||||
),
|
||||
]
|
0
faqs/migrations/__init__.py
Normal file
0
faqs/migrations/__init__.py
Normal file
14
faqs/mixins.py
Normal file
14
faqs/mixins.py
Normal file
|
@ -0,0 +1,14 @@
|
|||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||
|
||||
|
||||
class AdminOnlyMixin(PermissionRequiredMixin):
|
||||
"""Restreint l'accès aux admins"""
|
||||
|
||||
permission_required = "faqs.is_author"
|
||||
|
||||
|
||||
class CreatorOnlyMixin(AdminOnlyMixin):
|
||||
"""Restreint l'accès à l'auteur"""
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().filter(author=self.request.user)
|
32
faqs/models.py
Normal file
32
faqs/models.py
Normal file
|
@ -0,0 +1,32 @@
|
|||
from translated_fields import TranslatedFieldWithFallback
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class Faq(models.Model):
|
||||
title = TranslatedFieldWithFallback(
|
||||
models.CharField(_("titre"), blank=False, max_length=255)
|
||||
)
|
||||
description = TranslatedFieldWithFallback(
|
||||
models.TextField(_("description"), blank=False)
|
||||
)
|
||||
content = TranslatedFieldWithFallback(models.TextField(_("contenu"), blank=True))
|
||||
|
||||
author = models.ForeignKey(
|
||||
User, related_name="faqs", null=True, on_delete=models.SET_NULL
|
||||
)
|
||||
last_modified = models.DateField(_("mise à jour"), auto_now=True)
|
||||
|
||||
anchor = models.CharField(_("ancre"), max_length=20)
|
||||
|
||||
class Meta:
|
||||
permissions = [
|
||||
("is_author", "Can create faqs"),
|
||||
]
|
||||
constraints = [
|
||||
models.UniqueConstraint(fields=["anchor"], name="unique_faq_anchor")
|
||||
]
|
43
faqs/templates/faqs/faq.html
Normal file
43
faqs/templates/faqs/faq.html
Normal file
|
@ -0,0 +1,43 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n markdown %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="level">
|
||||
{# Titre de la FAQ #}
|
||||
<div class="level-left is-flex-shrink-1">
|
||||
<h1 class="title">{{ faq.title }}</h1>
|
||||
</div>
|
||||
|
||||
<div class="level-right">
|
||||
{# Date de dernière modification #}
|
||||
<div class="level-item">
|
||||
<span class="tag is-primary is-light is-outlined">{% blocktrans with maj=faq.last_modified|date:'d/m/Y' %}Mis à jour le {{ maj }}{% endblocktrans %}</span>
|
||||
</div>
|
||||
|
||||
{# Lien vers la page d'édition #}
|
||||
{% if faq.author == user %}
|
||||
<div class="level-item">
|
||||
<a class="button has-tooltip-primary" href="{% url 'faq.edit' faq.anchor %}" data-tooltip="{% trans "Modifier" %}">
|
||||
<span class="icon">
|
||||
<i class="fas fa-cog"></i>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
{# Description #}
|
||||
<div class="message is-primary">
|
||||
<div class="message-body content">{{ faq.description|markdown|safe }}</div>
|
||||
</div>
|
||||
|
||||
{# Contenu #}
|
||||
<div class="content">
|
||||
{{ faq.content|markdown|safe }}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
20
faqs/templates/faqs/faq_create.html
Normal file
20
faqs/templates/faqs/faq_create.html
Normal file
|
@ -0,0 +1,20 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% for error in form.non_field_errors %}
|
||||
<div class="notification is-danger">
|
||||
{{ error }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<h1 class="title">{% trans "Nouvelle FAQ" %}</h1>
|
||||
<hr>
|
||||
|
||||
{% url 'faq.list' as r_url %}
|
||||
{% include "forms/common-form.html" with c_size="is-9" errors=False %}
|
||||
|
||||
{% endblock %}
|
||||
|
20
faqs/templates/faqs/faq_edit.html
Normal file
20
faqs/templates/faqs/faq_edit.html
Normal file
|
@ -0,0 +1,20 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% for error in form.non_field_errors %}
|
||||
<div class="notification is-danger">
|
||||
{{ error }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<h1 class="title">{% trans "Modification de la FAQ" %}</h1>
|
||||
<hr>
|
||||
|
||||
{% url 'faq.view' faq.anchor as r_url %}
|
||||
{% include "forms/common-form.html" with c_size="is-9" errors=False %}
|
||||
|
||||
{% endblock %}
|
||||
|
48
faqs/templates/faqs/faq_list.html
Normal file
48
faqs/templates/faqs/faq_list.html
Normal file
|
@ -0,0 +1,48 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n markdown %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="level">
|
||||
<div class="level-left">
|
||||
<div class="level-item">
|
||||
<h1 class="title">{% trans "Liste des FAQ" %}</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if perms.faqs.is_author %}
|
||||
<div class="level-right">
|
||||
<div class="level-item">
|
||||
<a class="button is-light is-outlined is-primary" href={% url 'faq.create' %}>
|
||||
<span class="icon">
|
||||
<i class="fas fa-plus"></i>
|
||||
</span>
|
||||
<span>{% trans "Créer une FAQ" %}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
{% for f in faq_list %}
|
||||
<div class="panel is-primary is-radiusless">
|
||||
<div class="panel-heading is-size-6 is-radiusless">
|
||||
<a class="has-text-primary-light" href="{% url 'faq.view' f.anchor %}"><u>{{ f.title }}</u></a>
|
||||
</div>
|
||||
|
||||
{% if f.description %}
|
||||
<div class="panel-block">
|
||||
<div class="content is-flex-grow-1">
|
||||
{{ f.description|markdown|safe }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if not forloop.last %}
|
||||
<br>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% endblock %}
|
12
faqs/urls.py
Normal file
12
faqs/urls.py
Normal file
|
@ -0,0 +1,12 @@
|
|||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
# Admin views
|
||||
path("create", views.FaqCreateView.as_view(), name="faq.create"),
|
||||
path("edit/<slug:slug>", views.FaqEditView.as_view(), name="faq.edit"),
|
||||
# Public views
|
||||
path("", views.FaqListView.as_view(), name="faq.list"),
|
||||
path("view/<slug:slug>", views.FaqView.as_view(), name="faq.view"),
|
||||
]
|
54
faqs/views.py
Normal file
54
faqs/views.py
Normal file
|
@ -0,0 +1,54 @@
|
|||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import CreateView, DetailView, ListView, UpdateView
|
||||
|
||||
from .forms import FaqForm
|
||||
from .mixins import AdminOnlyMixin, CreatorOnlyMixin
|
||||
from .models import Faq
|
||||
|
||||
# #############################################################################
|
||||
# Administration Views
|
||||
# #############################################################################
|
||||
|
||||
|
||||
class FaqCreateView(AdminOnlyMixin, SuccessMessageMixin, CreateView):
|
||||
model = Faq
|
||||
form_class = FaqForm
|
||||
success_message = _("Faq créée avec succès !")
|
||||
template_name = "faqs/faq_create.html"
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse("faq.view", args=[self.object.anchor])
|
||||
|
||||
def form_valid(self, form):
|
||||
form.instance.author = self.request.user
|
||||
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class FaqEditView(CreatorOnlyMixin, SuccessMessageMixin, UpdateView):
|
||||
model = Faq
|
||||
form_class = FaqForm
|
||||
slug_field = "anchor"
|
||||
success_message = _("Faq modifiée avec succès !")
|
||||
template_name = "faqs/faq_edit.html"
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse("faq.view", args=[self.object.anchor])
|
||||
|
||||
|
||||
# #############################################################################
|
||||
# Public Views
|
||||
# #############################################################################
|
||||
|
||||
|
||||
class FaqListView(ListView):
|
||||
model = Faq
|
||||
template_name = "faqs/faq_list.html"
|
||||
|
||||
|
||||
class FaqView(DetailView):
|
||||
model = Faq
|
||||
template_name = "faqs/faq.html"
|
||||
slug_field = "anchor"
|
|
@ -55,6 +55,7 @@ INSTALLED_APPS = [
|
|||
"kadenios.apps.IgnoreSrcStaticFilesConfig",
|
||||
"shared",
|
||||
"elections",
|
||||
"faqs",
|
||||
"authens",
|
||||
]
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ urlpatterns = [
|
|||
path("", HomeView.as_view(), name="kadenios"),
|
||||
path("admin/", admin.site.urls),
|
||||
path("elections/", include("elections.urls")),
|
||||
path("faqs/", include("faqs.urls")),
|
||||
path("auth/", include("shared.auth.urls")),
|
||||
path("authens/", include("authens.urls")),
|
||||
path("i18n/", include("django.conf.urls.i18n")),
|
||||
|
|
|
@ -112,16 +112,22 @@
|
|||
<nav class="level has-background-primary">
|
||||
<div class="level-left px-4">
|
||||
<div class="level-item">
|
||||
<a href="{% url "kadenios" %}">
|
||||
<a href="{% url 'kadenios' %}">
|
||||
<h1 class="has-text-primary-light is-size-1 is-family-secondary">Kadenios</h1>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="level-item px-4">
|
||||
<a href="{% url "election.list" %}">
|
||||
<div class="level-item pl-4">
|
||||
<a href="{% url 'election.list' %}">
|
||||
<h3 class="has-text-primary-light has-text-weight-semibold is-size-3">{% trans "Élections" %}</h3>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="level-item pl-4">
|
||||
<a href="{% url 'faq.list' %}">
|
||||
<h3 class="has-text-primary-light has-text-weight-semibold is-size-3">{% trans "FAQ" %}</h3>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="level-right px-5">
|
||||
|
|
Loading…
Reference in a new issue