diff --git a/faqs/__init__.py b/faqs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/faqs/forms.py b/faqs/forms.py new file mode 100644 index 0000000..3c8aa41 --- /dev/null +++ b/faqs/forms.py @@ -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"}), + } diff --git a/faqs/migrations/0001_initial.py b/faqs/migrations/0001_initial.py new file mode 100644 index 0000000..a326379 --- /dev/null +++ b/faqs/migrations/0001_initial.py @@ -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" + ), + ), + ] diff --git a/faqs/migrations/__init__.py b/faqs/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/faqs/mixins.py b/faqs/mixins.py new file mode 100644 index 0000000..8a09157 --- /dev/null +++ b/faqs/mixins.py @@ -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) diff --git a/faqs/models.py b/faqs/models.py new file mode 100644 index 0000000..f972c61 --- /dev/null +++ b/faqs/models.py @@ -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") + ] diff --git a/faqs/templates/faqs/faq.html b/faqs/templates/faqs/faq.html new file mode 100644 index 0000000..79d5c5c --- /dev/null +++ b/faqs/templates/faqs/faq.html @@ -0,0 +1,43 @@ +{% extends "base.html" %} +{% load i18n markdown %} + + +{% block content %} + +
+ {# Titre de la FAQ #} +
+

{{ faq.title }}

+
+ +
+ {# Date de dernière modification #} +
+ {% blocktrans with maj=faq.last_modified|date:'d/m/Y' %}Mis à jour le {{ maj }}{% endblocktrans %} +
+ + {# Lien vers la page d'édition #} + {% if faq.author == user %} + + {% endif %} +
+
+
+ +{# Description #} +
+
{{ faq.description|markdown|safe }}
+
+ +{# Contenu #} +
+ {{ faq.content|markdown|safe }} +
+ +{% endblock %} diff --git a/faqs/templates/faqs/faq_create.html b/faqs/templates/faqs/faq_create.html new file mode 100644 index 0000000..c1c52b4 --- /dev/null +++ b/faqs/templates/faqs/faq_create.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} +{% load i18n %} + + +{% block content %} + +{% for error in form.non_field_errors %} +
+ {{ error }} +
+{% endfor %} + +

{% trans "Nouvelle FAQ" %}

+
+ +{% url 'faq.list' as r_url %} +{% include "forms/common-form.html" with c_size="is-9" errors=False %} + +{% endblock %} + diff --git a/faqs/templates/faqs/faq_edit.html b/faqs/templates/faqs/faq_edit.html new file mode 100644 index 0000000..4b31145 --- /dev/null +++ b/faqs/templates/faqs/faq_edit.html @@ -0,0 +1,20 @@ +{% extends "base.html" %} +{% load i18n %} + + +{% block content %} + +{% for error in form.non_field_errors %} +
+ {{ error }} +
+{% endfor %} + +

{% trans "Modification de la FAQ" %}

+
+ +{% url 'faq.view' faq.anchor as r_url %} +{% include "forms/common-form.html" with c_size="is-9" errors=False %} + +{% endblock %} + diff --git a/faqs/templates/faqs/faq_list.html b/faqs/templates/faqs/faq_list.html new file mode 100644 index 0000000..5eefaf0 --- /dev/null +++ b/faqs/templates/faqs/faq_list.html @@ -0,0 +1,48 @@ +{% extends "base.html" %} +{% load i18n markdown %} + + +{% block content %} + +
+
+
+

{% trans "Liste des FAQ" %}

+
+
+ + {% if perms.faqs.is_author %} +
+ +
+ {% endif %} +
+
+ +{% for f in faq_list %} +
+
+ {{ f.title }} +
+ + {% if f.description %} +
+
+ {{ f.description|markdown|safe }} +
+
+ {% endif %} +
+{% if not forloop.last %} +
+{% endif %} +{% endfor %} + +{% endblock %} diff --git a/faqs/urls.py b/faqs/urls.py new file mode 100644 index 0000000..e89febf --- /dev/null +++ b/faqs/urls.py @@ -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/", views.FaqEditView.as_view(), name="faq.edit"), + # Public views + path("", views.FaqListView.as_view(), name="faq.list"), + path("view/", views.FaqView.as_view(), name="faq.view"), +] diff --git a/faqs/views.py b/faqs/views.py new file mode 100644 index 0000000..7857e12 --- /dev/null +++ b/faqs/views.py @@ -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" diff --git a/kadenios/settings/common.py b/kadenios/settings/common.py index bbbb328..6143d80 100644 --- a/kadenios/settings/common.py +++ b/kadenios/settings/common.py @@ -55,6 +55,7 @@ INSTALLED_APPS = [ "kadenios.apps.IgnoreSrcStaticFilesConfig", "shared", "elections", + "faqs", "authens", ] diff --git a/kadenios/urls.py b/kadenios/urls.py index 4c81a39..e8ace9b 100644 --- a/kadenios/urls.py +++ b/kadenios/urls.py @@ -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")), diff --git a/shared/templates/base.html b/shared/templates/base.html index ed33571..1a6e446 100644 --- a/shared/templates/base.html +++ b/shared/templates/base.html @@ -112,16 +112,22 @@