On ajoute une page contact, et une modération pour parcourir les messages de contact

Fix #50
Fix #56
This commit is contained in:
Jean-Marie Favreau 2023-12-16 21:14:19 +01:00
parent 3e8f28422f
commit 92e1330fc4
15 changed files with 351 additions and 79 deletions

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: agenda_culturel\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-12-16 18:21+0000\n"
"POT-Creation-Date: 2023-12-16 20:13+0000\n"
"PO-Revision-Date: 2023-10-29 14:16+0000\n"
"Last-Translator: Jean-Marie Favreau <jeanmarie.favreau@free.fr>\n"
"Language-Team: Jean-Marie Favreau <jeanmarie.favreau@free.fr>\n"
@ -34,7 +34,7 @@ msgid "The end time cannot be earlier than the start time."
msgstr "L'heure de fin ne peut pas être avant l'heure de début."
#: agenda_culturel/models.py:19 agenda_culturel/models.py:48
#: agenda_culturel/models.py:179
#: agenda_culturel/models.py:183
msgid "Name"
msgstr "Nom"
@ -204,47 +204,47 @@ msgstr "Événement"
msgid "Events"
msgstr "Événements"
#: agenda_culturel/models.py:178
#: agenda_culturel/models.py:182
msgid "Subject"
msgstr "Sujet"
#: agenda_culturel/models.py:178
#: agenda_culturel/models.py:182
msgid "The subject of your message"
msgstr "Sujet de votre message"
#: agenda_culturel/models.py:179
#: agenda_culturel/models.py:183
msgid "Your name"
msgstr "Votre nom"
#: agenda_culturel/models.py:180
#: agenda_culturel/models.py:184
msgid "Email address"
msgstr "Adresse email"
#: agenda_culturel/models.py:180
#: agenda_culturel/models.py:184
msgid "Your email address"
msgstr "Votre adresse email"
#: agenda_culturel/models.py:181
#: agenda_culturel/models.py:185
msgid "Message"
msgstr "Message"
#: agenda_culturel/models.py:181
#: agenda_culturel/models.py:185
msgid "Your message"
msgstr "Votre message"
#: agenda_culturel/models.py:185
#: agenda_culturel/models.py:189 agenda_culturel/views.py:334
msgid "Closed"
msgstr "Fermé"
#: agenda_culturel/models.py:185
#: agenda_culturel/models.py:189
msgid "this message has been processed and no longer needs to be handled"
msgstr "Ce message a été traité et ne nécessite plus d'être pris en charge"
#: agenda_culturel/models.py:186
#: agenda_culturel/models.py:190
msgid "Comments"
msgstr "Commentaires"
#: agenda_culturel/models.py:186
#: agenda_culturel/models.py:190
msgid "Comments on the message from the moderation team"
msgstr "Commentaires sur ce message par l'équipe de modération"
@ -260,23 +260,19 @@ msgstr "français"
msgid "The static content has been successfully updated."
msgstr "Le contenu statique a été modifié avec succès."
#: agenda_culturel/views.py:191
msgid "Your message has been sent successfully."
msgstr "L'événement a été supprimé avec succès"
#: agenda_culturel/views.py:197
#: agenda_culturel/views.py:188
msgid "The event has been successfully modified."
msgstr "L'événement a été modifié avec succès."
#: agenda_culturel/views.py:208
#: agenda_culturel/views.py:199
msgid "The event has been successfully deleted."
msgstr "L'événement a été supprimé avec succès"
#: agenda_culturel/views.py:246
#: agenda_culturel/views.py:237
msgid "The event is saved."
msgstr "L'événement est enregistré."
#: agenda_culturel/views.py:249
#: agenda_culturel/views.py:240
msgid ""
"The event has been submitted and will be published as soon as it has been "
"validated by the moderation team."
@ -284,7 +280,7 @@ msgstr ""
"L'événement a été soumis et sera publié dès qu'il aura été validé par "
"l'équipe de modération."
#: agenda_culturel/views.py:279
#: agenda_culturel/views.py:270
msgid ""
"The event has been successfully extracted, and you can now submit it after "
"modifying it if necessary."
@ -292,7 +288,7 @@ msgstr ""
"L'événement a été extrait avec succès, vous pouvez maintenant le soumettre "
"après l'avoir modifié au besoin."
#: agenda_culturel/views.py:283
#: agenda_culturel/views.py:274
msgid ""
"Unable to extract an event from the proposed URL. Please use the form below "
"to submit the event."
@ -300,11 +296,12 @@ msgstr ""
"Impossible d'extraire un événement depuis l'URL proposée. Veuillez utiliser "
"le formulaire ci-dessous pour soumettre l'événement."
#: agenda_culturel/views.py:292
#: agenda_culturel/views.py:283
msgid "This URL has already been submitted, and you can find the event below."
msgstr "Cette URL a déjà été soumise, et vous trouverez l'événement ci-dessous."
msgstr ""
"Cette URL a déjà été soumise, et vous trouverez l'événement ci-dessous."
#: agenda_culturel/views.py:296
#: agenda_culturel/views.py:287
msgid ""
"This URL has already been submitted, but has not been selected for "
"publication by the moderation team."
@ -312,10 +309,22 @@ msgstr ""
"Cette URL a déjà été soumise, mais n'a pas été retenue par l'équipe de "
"modération pour la publication."
#: agenda_culturel/views.py:298
#: agenda_culturel/views.py:289
msgid "This URL has already been submitted and is awaiting moderation."
msgstr "Cette URL a déjà été soumise, et est en attente de modération"
#: agenda_culturel/views.py:329
#: agenda_culturel/views.py:311
msgid "Your message has been sent successfully."
msgstr "L'événement a été supprimé avec succès"
#: agenda_culturel/views.py:319
msgid "The contact message properties has been successfully modified."
msgstr "Les propriétés du message de contact ont été modifié avec succès."
#: agenda_culturel/views.py:334
msgid "Open"
msgstr "Ouvert"
#: agenda_culturel/views.py:374
msgid "Search"
msgstr "Rechercher"

View File

@ -0,0 +1,19 @@
# Generated by Django 4.2.7 on 2023-12-16 19:54
import ckeditor.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('agenda_culturel', '0009_contactmessage_comments_alter_contactmessage_message'),
]
operations = [
migrations.AlterField(
model_name='contactmessage',
name='comments',
field=ckeditor.fields.RichTextField(blank=True, default='', help_text='Comments on the message from the moderation team', null=True, verbose_name='Comments'),
),
]

View File

@ -172,6 +172,10 @@ class Event(models.Model):
def modified(self):
return abs((self.modified_date - self.created_date).total_seconds()) > 1
def nb_draft_events():
return Event.objects.filter(status=Event.STATUS.DRAFT).count()
class ContactMessage(models.Model):
@ -183,5 +187,7 @@ class ContactMessage(models.Model):
date = models.DateTimeField(auto_now_add=True)
closed = models.BooleanField(verbose_name=_('Closed'), help_text=_('this message has been processed and no longer needs to be handled'), default=False)
comments = RichTextField(verbose_name=_('Comments'), help_text=_('Comments on the message from the moderation team'), default="")
comments = RichTextField(verbose_name=_('Comments'), help_text=_('Comments on the message from the moderation team'), default="", blank=True, null=True)
def nb_open_contactmessages():
return ContactMessage.objects.filter(closed=False).count()

View File

@ -599,8 +599,10 @@ nav .badge {
@extend [role="button"];
font-size: 70%;
padding: 0.2em .8em;
border-radius: 40%;
border-radius: .5em;
svg {
vertical-align: -0.125em;
}
}
form [role="button"] {
@ -617,3 +619,12 @@ form [role="button"] {
grid-template-columns: auto 25%;
}
}
aside nav a.selected {
background: var(--primary-focus);
}
aside nav a.badge {
float: right;
z-index: 1;
margin-top: -2em;
}

View File

@ -0,0 +1,35 @@
{% extends "agenda_culturel/page.html" %}
{% load static %}
{% block title %}Contact{% endblock %}
{% block entete_header %}
<script>window.CKEDITOR_BASEPATH = '/static/ckeditor/ckeditor/';</script>
<script src="/static/admin/js/vendor/jquery/jquery.js"></script>
<script src="/static/admin/js/jquery.init.js"></script>
<link href="{% static 'css/django_better_admin_arrayfield.min.css' %}" type="text/css" media="all" rel="stylesheet">
<script src="{% static 'js/django_better_admin_arrayfield.min.js' %}"></script>
<script src="{% static 'js/adjust_datetimes.js' %}"></script>
{% endblock %}
{% block content %}
<h1>Contact</h1>
<article>
{% url 'contact' as local_url %}
{% include "agenda_culturel/static_content.html" with name="contact" url_path=local_url %}
</article>
<article>
<head>
<p class="message warning"><strong>Attention&nbsp:</strong> n'utilisez pas le formulaire ci-dessous pour proposer un événement, il sera ignoré. Utilisez plutôt la page <a href="{% url 'add_event' %}">ajouter un événement</a>.</p>
</head>
<form method="post">{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Envoyer">
</form>
</article>
{% endblock %}

View File

@ -0,0 +1,43 @@
{% extends "agenda_culturel/page.html" %}
{% load static %}
{% block title %}Message de contact : {{ obj.subject }}{% endblock %}
{% block entete_header %}
<script>window.CKEDITOR_BASEPATH = '/static/ckeditor/ckeditor/';</script>
<script src="/static/admin/js/vendor/jquery/jquery.js"></script>
<script src="/static/admin/js/jquery.init.js"></script>
<link href="{% static 'css/django_better_admin_arrayfield.min.css' %}" type="text/css" media="all" rel="stylesheet">
<script src="{% static 'js/django_better_admin_arrayfield.min.js' %}"></script>
<script src="{% static 'js/adjust_datetimes.js' %}"></script>
{% endblock %}
{% block content %}
<div class="grid two-columns">
<div>
<article>
<header>
<h1>Modération du message «&nbsp;{{ object.subject }}&nbsp;»</h1>
<ul>
<li>Date&nbsp;: {{ object.date }}</li>
<li>Auteur&nbsp;: {{ object.name }} <a href="mailto:{{ object.email }}">{{ object.email }}</a></li>
</ul>
</header>
<div>
{{ object.message }}
</div>
</article>
<article>
<form method="post">{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Enregistrer">
</form>
</article>
</div>
{% include "agenda_culturel/side-nav.html" with current="contactmessages" %}
</div>
{% endblock %}

View File

@ -0,0 +1,63 @@
{% extends "agenda_culturel/page.html" %}
{% block title %}Derniers messages de contact reçus{% endblock %}
{% load utils_extra %}
{% load cat_extra %}
{% block entete_header %}
{% css_categories %}
{% endblock %}
{% block content %}
<div class="grid two-columns">
<article>
<header>
<h1>Derniers messages de contact reçus</h1>
<form method="get" class="form django-form recent">
{{ filter.form }}<br />
<button type="submit">Filtrer</button><br />
</header>
<table role="grid">
<thead>
<tr>
<th>Date</th>
<th>Sujet</th>
<th>Auteur</th>
<th>Fermé</th>
</tr>
</thead>
<tbody>
{% for obj in paginator_filter %}
<tr>
<td>{{ obj.date }}</td>
<td><a href="{% url 'contactmessage' obj.pk %}">{{ obj.subject }}</a></td>
<td>{{ obj.name }}</td>
<td>{% if obj.closed %}{% picto_from_name "check-square" "fermé" %}{% else %}{% picto_from_name "square" "ouvert" %}{% endif %}</td>
</tr>
{% endfor %}
</tbody>
</table>
<footer>
<span>
{% if paginator_filter.has_previous %}
<a href="?page=1" role="button">&laquo; premier</a>
<a href="?page={{ paginator_filter.previous_page_number }}" role="button">précédent</a>
{% endif %}
<span>
Page {{ paginator_filter.number }} sur {{ paginator_filter.paginator.num_pages }}
</span>
{% if paginator_filter.has_next %}
<a href="?page={{ paginator_filter.next_page_number }}" role="button">suivant</a>
<a href="?page={{ paginator_filter.paginator.num_pages }}" role="button">dernier &raquo;</a>
{% endif %}
</span>
</footer>
</article>
{% include "agenda_culturel/side-nav.html" with current="contactmessages" %}
</div>
{% endblock %}

View File

@ -43,22 +43,7 @@
</footer>
</article>
<aside>
<article>
<h2>Consulter</h2>
<ul>
<li><a href="{% url 'view_all_tags' %}">Toutes les étiquettes</a></li>
</ul>
<h2>Configurer</h2>
<ul>
<li><a href="{% url 'admin:index' %}">Administration de django</a></li>
</ul>
</article>
<article>
<p>Vous êtes connecté(e) en tant que {{ user }}.</p>
<a role="button" href="{% url 'logout' %}?next={% url 'home' %}">Déconnexion</a>
</article>
</aside>
{% include "agenda_culturel/side-nav.html" with current="moderation" %}
</div>
{% endblock %}

View File

@ -23,6 +23,7 @@
{% endblock %}
</head>
{% load event_extra %}
{% load contactmessages_extra %}
{% load utils_extra %}
<body>
<div id="main-nav">
@ -50,6 +51,9 @@
<li>
{% show_badges_events %}
</li>
<li>
{% show_badge_contactmessages %}
</li>
{% endif %}
</ul>
</nav>

View File

@ -0,0 +1,30 @@
{% load event_extra %}
{% load contactmessages_extra %}
<aside>
<article>
<head>
<h2>Consulter</h2>
</head>
<nav>
<ul>
<li><a {% if current == "moderation" %}class="selected" {% endif %}href="{% url 'moderation' %}">Derniers événements soumis</a>{% show_badges_events %}</li>
<li><a {% if current == "tags" %}class="selected" {% endif %}href="{% url 'view_all_tags' %}">Toutes les étiquettes</a></li>
<li><a {% if current == "contactmessages" %}class="selected" {% endif %}href="{% url 'contactmessages' %}">Messages de contact</a>{% show_badge_contactmessages %}</li>
</ul>
</nav>
</article>
<article>
<head>
<h2>Configurer</h2>
</head>
<nav>
<ul>
<li><a href="{% url 'admin:index' %}">Administration de django</a></li>
</ul>
</nav>
</article>
<article>
<p>Vous êtes connecté(e) en tant que {{ user }}.</p>
<a role="button" href="{% url 'logout' %}?next={% url 'home' %}">Déconnexion</a>
</article>
</aside>

View File

@ -5,6 +5,7 @@
{% load cat_extra %}
{% block content %}
<div class="grid two-columns">
<article>
<header>
<h1>Toutes les étiquettes</h1>
@ -20,4 +21,7 @@
<p><em>Il n'y a pour l'instant aucun événement renseigné par une étiquette.</em></p>
{% endif %}
</article>
{% include "agenda_culturel/side-nav.html" with current="tags" %}
</div>
{% endblock %}

View File

@ -0,0 +1,20 @@
from django import template
from django.utils.safestring import mark_safe
from django.urls import reverse_lazy
from django.template.defaultfilters import pluralize
from agenda_culturel.models import ContactMessage
from .utils_extra import picto_from_name
register = template.Library()
@register.simple_tag
def show_badge_contactmessages():
nb_open = ContactMessage.nb_open_contactmessages()
if nb_open != 0:
return mark_safe('<a href="' + reverse_lazy("contactmessages") + '?closed=false" class="badge" data-tooltip="' + str(nb_open) + ' message' + pluralize(nb_open) + ' à traiter">' + picto_from_name("mail") + " " + str(nb_open) + '</a>')
else:
return ""

View File

@ -15,10 +15,6 @@ register = template.Library()
def in_date(event, date):
return event.filter((Q(start_day__lte=date) & Q(end_day__gte=date)) | (Q(end_day=None) & Q(start_day=date)))
@register.simple_tag
def nb_draft_events():
return Event.objects.filter(status=Event.STATUS.DRAFT).count()
@register.filter
def can_show_start_time(event):
return event.start_time and (not event.end_day or event.end_day == event.start_day)
@ -42,8 +38,8 @@ def picto_status(event):
@register.simple_tag
def show_badges_events():
# TODO: seulement ceux dans le futur ?
nb_drafts = nb_draft_events()
nb_drafts = Event.nb_draft_events()
if nb_drafts != 0:
return mark_safe('<a href="' + reverse_lazy("moderation") + '?status=draft" class="badge" data-tooltip="' + str(nb_drafts) + ' brouillon' + pluralize(nb_drafts) + ' à valider">' + str(nb_drafts) + '</a>')
return mark_safe('<a href="' + reverse_lazy("moderation") + '?status=draft" class="badge" data-tooltip="' + str(nb_drafts) + ' brouillon' + pluralize(nb_drafts) + ' à valider">' + picto_from_name("calendar") + " " + str(nb_drafts) + '</a>')
else:
return ""

View File

@ -31,7 +31,9 @@ urlpatterns = [
path('rechercher', event_search, name='event_search'),
path('rechercher/complet/', event_search_full, name='event_search_full'),
path('mentions-legales', mentions_legales, name='mentions_legales'),
path('contact', ContactMessageCreateView.as_view(), name='contact')
path('contact', ContactMessageCreateView.as_view(), name='contact'),
path('contactmessages', contactmessages, name='contactmessages'),
path('contactmessage/<int:pk>', ContactMessageUpdateView.as_view(), name='contactmessage'),
]
if settings.DEBUG:

View File

@ -182,15 +182,6 @@ class StaticContentUpdateView(SuccessMessageMixin, LoginRequiredMixin, UpdateVie
success_message = _('The static content has been successfully updated.')
class ContactMessageCreateView(SuccessMessageMixin, CreateView):
model = ContactMessage
template_name = "agenda_culturel/contactmessage_create_form.html"
fields = ['subject', 'name', 'email', 'message']
success_url = reverse_lazy('home')
success_message = _('Your message has been sent successfully.')
class EventUpdateView(SuccessMessageMixin, LoginRequiredMixin, UpdateView):
model = Event
form_class = EventForm
@ -309,6 +300,44 @@ class EventFilterAdmin(django_filters.FilterSet):
fields = ['status']
class ContactMessageCreateView(SuccessMessageMixin, CreateView):
model = ContactMessage
template_name = "agenda_culturel/contactmessage_create_form.html"
fields = ['subject', 'name', 'email', 'message']
success_url = reverse_lazy('home')
success_message = _('Your message has been sent successfully.')
class ContactMessageUpdateView(SuccessMessageMixin, LoginRequiredMixin, UpdateView):
model = ContactMessage
template_name = "agenda_culturel/contactmessage_moderation_form.html"
fields = ('closed', 'comments')
success_message = _('The contact message properties has been successfully modified.')
success_url = reverse_lazy('contactmessages')
def get_form_kwargs(self):
"""Return the keyword arguments for instantiating the form."""
kwargs = super().get_form_kwargs()
if hasattr(self, 'object'):
kwargs.update({'instance': self.object})
return kwargs
class ContactMessagesFilterAdmin(django_filters.FilterSet):
closed = django_filters.MultipleChoiceFilter(label="Status", choices=((True, _("Closed")), (False, _("Open"))), widget=forms.CheckboxSelectMultiple)
class Meta:
model = ContactMessage
fields = ['closed']
@login_required(login_url="/accounts/login/")
def moderation(request):
filter = EventFilterAdmin(request.GET, queryset=Event.objects.all().order_by("-created_date"))
@ -324,6 +353,22 @@ def moderation(request):
return render(request, 'agenda_culturel/moderation.html', {'filter': filter, 'paginator_filter': response} )
@login_required(login_url="/accounts/login/")
def contactmessages(request):
filter = ContactMessagesFilterAdmin(request.GET, queryset=ContactMessage.objects.all().order_by("-date"))
paginator = Paginator(filter.qs, 10)
page = request.GET.get('page')
try:
response = paginator.page(page)
except PageNotAnInteger:
response = paginator.page(1)
except EmptyPage:
response = paginator.page(paginator.num_pages)
return render(request, 'agenda_culturel/contactmessages.html', {'filter': filter, 'paginator_filter': response} )
class SimpleSearchEventFilter(django_filters.FilterSet):
q = django_filters.CharFilter(method='custom_filter', label=_("Search"))