Ajout de règles de catégorisation

Fix #19
This commit is contained in:
Jean-Marie Favreau 2024-02-17 12:59:21 +01:00
parent bd6edfd0e9
commit 8d23f83057
11 changed files with 467 additions and 102 deletions

View File

@ -2,7 +2,7 @@ from django.forms import ModelForm, ValidationError, TextInput, Form, URLField,
from datetime import date
from django_better_admin_arrayfield.forms.widgets import DynamicArrayWidget
from .models import Event, BatchImportation, RecurrentImport
from .models import Event, BatchImportation, RecurrentImport, CategorisationRule
from django.utils.translation import gettext_lazy as _
from string import ascii_uppercase as auc
from .templatetags.utils_extra import int_to_abc
@ -33,6 +33,11 @@ class RecurrentImportForm(ModelForm):
'defaultTags': DynamicArrayWidgetTags(),
}
class CategorisationRuleImportForm(ModelForm):
class Meta:
model = CategorisationRule
fields = '__all__'
class EventForm(ModelForm):
class Meta:

View File

@ -143,7 +143,6 @@ class ICALNoVCExtractor(ICALExtractor):
return text
else:
result = self.parser.format(text)
logger.warning(result)
return result
def add_event(self, title, category, start_day, location, description, tags, uuid, recurrences=None, url_human=None, start_time=None, end_day=None, end_time=None, last_modified=None, published=False, image=None, image_alt=None):

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: agenda_culturel\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-02-16 16:06+0000\n"
"POT-Creation-Date: 2024-02-17 11:31+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"
@ -17,374 +17,430 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: agenda_culturel/forms.py:62
#: agenda_culturel/forms.py:70
msgid "The end date must be after the start date."
msgstr "La date de fin doit être après la date de début."
#: agenda_culturel/forms.py:77
#: agenda_culturel/forms.py:85
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/forms.py:84
#: agenda_culturel/forms.py:92
msgid "JSON in the format expected for the import."
msgstr "JSON dans le format attendu pour l'import"
#: agenda_culturel/models.py:32 agenda_culturel/models.py:61
#: agenda_culturel/models.py:702
#: agenda_culturel/models.py:33 agenda_culturel/models.py:62
#: agenda_culturel/models.py:703
msgid "Name"
msgstr "Nom"
#: agenda_culturel/models.py:32 agenda_culturel/models.py:61
#: agenda_culturel/models.py:33 agenda_culturel/models.py:62
msgid "Category name"
msgstr "Nom de la catégorie"
#: agenda_culturel/models.py:33
#: agenda_culturel/models.py:34
msgid "Content"
msgstr "Contenu"
#: agenda_culturel/models.py:33
#: agenda_culturel/models.py:34
msgid "Text as shown to the visitors"
msgstr "Text tel que présenté aux visiteureuses"
#: agenda_culturel/models.py:34
#: agenda_culturel/models.py:35
msgid "URL path"
msgstr ""
#: agenda_culturel/models.py:34
#: agenda_culturel/models.py:35
msgid "URL path where the content is included."
msgstr ""
#: agenda_culturel/models.py:62
#: agenda_culturel/models.py:63
msgid "Alternative Name"
msgstr "Nom alternatif"
#: agenda_culturel/models.py:62
#: agenda_culturel/models.py:63
msgid "Alternative name used with a time period"
msgstr "Nom alternatif utilisé avec une période de temps"
#: agenda_culturel/models.py:63
#: agenda_culturel/models.py:64
msgid "Short name"
msgstr "Nom court"
#: agenda_culturel/models.py:63
#: agenda_culturel/models.py:64
msgid "Short name of the category"
msgstr "Nom court de la catégorie"
#: agenda_culturel/models.py:64
#: agenda_culturel/models.py:65
msgid "Color"
msgstr "Couleur"
#: agenda_culturel/models.py:64
#: agenda_culturel/models.py:65
msgid "Color used as background for the category"
msgstr "Couleur utilisée comme fond de la catégorie"
#: agenda_culturel/models.py:101 agenda_culturel/models.py:168
#: agenda_culturel/models.py:741
#: agenda_culturel/models.py:102 agenda_culturel/models.py:169
#: agenda_culturel/models.py:743 agenda_culturel/models.py:782
msgid "Category"
msgstr "Catégorie"
#: agenda_culturel/models.py:102
#: agenda_culturel/models.py:103
msgid "Categories"
msgstr "Catégories"
#: agenda_culturel/models.py:153 agenda_culturel/models.py:739
#: agenda_culturel/models.py:154 agenda_culturel/models.py:741
msgid "Published"
msgstr "Publié"
#: agenda_culturel/models.py:154
#: agenda_culturel/models.py:155
msgid "Draft"
msgstr "Brouillon"
#: agenda_culturel/models.py:155
#: agenda_culturel/models.py:156
msgid "Trash"
msgstr "Corbeille"
#: agenda_culturel/models.py:164
#: agenda_culturel/models.py:165
msgid "Title"
msgstr "Titre"
#: agenda_culturel/models.py:164
#: agenda_culturel/models.py:165
msgid "Short title"
msgstr "Titre court"
#: agenda_culturel/models.py:166 agenda_culturel/models.py:764
#: agenda_culturel/models.py:167 agenda_culturel/models.py:766
msgid "Status"
msgstr "Status"
#: agenda_culturel/models.py:168
#: agenda_culturel/models.py:169
msgid "Category of the event"
msgstr "Catégorie de l'événement"
#: agenda_culturel/models.py:170
#: agenda_culturel/models.py:171
msgid "Day of the event"
msgstr "Date de l'événement"
#: agenda_culturel/models.py:171
#: agenda_culturel/models.py:172
msgid "Starting time"
msgstr "Heure de début"
#: agenda_culturel/models.py:173
#: agenda_culturel/models.py:174
msgid "End day of the event"
msgstr "Fin de l'événement"
#: agenda_culturel/models.py:173
#: agenda_culturel/models.py:174
msgid "End day of the event, only required if different from the start day."
msgstr ""
"Date de fin de l'événement, uniquement nécessaire s'il est différent du "
"premier jour de l'événement"
#: agenda_culturel/models.py:174
#: agenda_culturel/models.py:175
msgid "Final time"
msgstr "Heure de fin"
#: agenda_culturel/models.py:176
#: agenda_culturel/models.py:177
msgid "Recurrence"
msgstr "Récurrence"
#: agenda_culturel/models.py:178 agenda_culturel/models.py:740
#: agenda_culturel/models.py:179 agenda_culturel/models.py:742
msgid "Location"
msgstr "Localisation"
#: agenda_culturel/models.py:178
#: agenda_culturel/models.py:179
msgid "Address of the event"
msgstr "Adresse de l'événement"
#: agenda_culturel/models.py:180
#: agenda_culturel/models.py:181
msgid "Description"
msgstr "Description"
#: agenda_culturel/models.py:180
#: agenda_culturel/models.py:181
msgid "General description of the event"
msgstr "Description générale de l'événement"
#: agenda_culturel/models.py:182
#: agenda_culturel/models.py:183
msgid "Illustration (local image)"
msgstr "Illustration (image locale)"
#: agenda_culturel/models.py:182
#: agenda_culturel/models.py:183
msgid "Illustration image stored in the agenda server"
msgstr "Image d'illustration stockée sur le serveur de l'agenda"
#: agenda_culturel/models.py:184
#: agenda_culturel/models.py:185
msgid "Illustration"
msgstr "Illustration"
#: agenda_culturel/models.py:184
#: agenda_culturel/models.py:185
msgid "URL of the illustration image"
msgstr "URL de l'image illustrative"
#: agenda_culturel/models.py:185
#: agenda_culturel/models.py:186
msgid "Illustration description"
msgstr "Description de l'illustration"
#: agenda_culturel/models.py:185
#: agenda_culturel/models.py:186
msgid "Alternative text used by screen readers for the image"
msgstr "Texte alternatif utiliser par les lecteurs d'écrans pour l'image"
#: agenda_culturel/models.py:187
#: agenda_culturel/models.py:188
msgid "Importation source"
msgstr "Source d'importation"
#: agenda_culturel/models.py:187
#: agenda_culturel/models.py:188
msgid "Importation source used to detect removed entries."
msgstr "Source d'importation utilisée pour détecter les éléments supprimés/"
#: agenda_culturel/models.py:188
#: agenda_culturel/models.py:189
msgid "UUIDs"
msgstr "UUIDs"
#: agenda_culturel/models.py:188
#: agenda_culturel/models.py:189
msgid "UUIDs from import to detect duplicated entries."
msgstr "UUIDs utilisés pendant l'import pour détecter les entrées dupliquées"
#: agenda_culturel/models.py:189
#: agenda_culturel/models.py:190
msgid "URLs"
msgstr "URLs"
#: agenda_culturel/models.py:189
#: agenda_culturel/models.py:190
msgid "List of all the urls where this event can be found."
msgstr "Liste de toutes les urls où l'événement peut être trouvé."
#: agenda_culturel/models.py:191
#: agenda_culturel/models.py:192
msgid "Tags"
msgstr "Étiquettes"
#: agenda_culturel/models.py:191
#: agenda_culturel/models.py:192
msgid "A list of tags that describe the event."
msgstr "Une liste d'étiquettes décrivant l'événement"
#: agenda_culturel/models.py:193
#: agenda_culturel/models.py:194
msgid "Possibly duplicated"
msgstr "Possibles doublons"
#: agenda_culturel/models.py:234
#: agenda_culturel/models.py:235
msgid "Event"
msgstr "Événement"
#: agenda_culturel/models.py:235
#: agenda_culturel/models.py:236
msgid "Events"
msgstr "Événements"
#: agenda_culturel/models.py:701
#: agenda_culturel/models.py:702
msgid "Subject"
msgstr "Sujet"
#: agenda_culturel/models.py:701
#: agenda_culturel/models.py:702
msgid "The subject of your message"
msgstr "Sujet de votre message"
#: agenda_culturel/models.py:702
#: agenda_culturel/models.py:703
msgid "Your name"
msgstr "Votre nom"
#: agenda_culturel/models.py:703
#: agenda_culturel/models.py:704
msgid "Email address"
msgstr "Adresse email"
#: agenda_culturel/models.py:703
#: agenda_culturel/models.py:704
msgid "Your email address"
msgstr "Votre adresse email"
#: agenda_culturel/models.py:704
#: agenda_culturel/models.py:705
msgid "Message"
msgstr "Message"
#: agenda_culturel/models.py:704
#: agenda_culturel/models.py:705
msgid "Your message"
msgstr "Votre message"
#: agenda_culturel/models.py:708 agenda_culturel/views.py:376
#: agenda_culturel/models.py:709 agenda_culturel/views.py:376
msgid "Closed"
msgstr "Fermé"
#: agenda_culturel/models.py:708
#: agenda_culturel/models.py:709
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:709
#: agenda_culturel/models.py:710
msgid "Comments"
msgstr "Commentaires"
#: agenda_culturel/models.py:709
#: agenda_culturel/models.py:710
msgid "Comments on the message from the moderation team"
msgstr "Commentaires sur ce message par l'équipe de modération"
#: agenda_culturel/models.py:718
#: agenda_culturel/models.py:719
msgid "ical"
msgstr "ical"
#: agenda_culturel/models.py:719
#: agenda_culturel/models.py:720
msgid "ical no busy"
msgstr "ical sans busy"
#: agenda_culturel/models.py:722
#: agenda_culturel/models.py:721
msgid "ical no VC"
msgstr "ical sans VC"
#: agenda_culturel/models.py:724
msgid "simple"
msgstr "simple"
#: agenda_culturel/models.py:723
#: agenda_culturel/models.py:725
msgid "Headless Chromium"
msgstr "chromium sans interface"
#: agenda_culturel/models.py:727
#: agenda_culturel/models.py:729
msgid "daily"
msgstr "chaque jour"
#: agenda_culturel/models.py:728
#: agenda_culturel/models.py:730
msgid "weekly"
msgstr "chaque semaine"
#: agenda_culturel/models.py:730
#: agenda_culturel/models.py:732
msgid "Processor"
msgstr "Processeur"
#: agenda_culturel/models.py:731
#: agenda_culturel/models.py:733
msgid "Downloader"
msgstr "Téléchargeur"
#: agenda_culturel/models.py:733
#: agenda_culturel/models.py:735
msgid "Import recurrence"
msgstr "Récurrence d'import"
#: agenda_culturel/models.py:736
#: agenda_culturel/models.py:738
msgid "Source"
msgstr "Source"
#: agenda_culturel/models.py:736
#: agenda_culturel/models.py:738
msgid "URL of the source document"
msgstr "URL du document source"
#: agenda_culturel/models.py:737
#: agenda_culturel/models.py:739
msgid "Browsable url"
msgstr "URL navigable"
#: agenda_culturel/models.py:737
#: agenda_culturel/models.py:739
msgid "URL of the corresponding document that will be shown to visitors."
msgstr "URL correspondant au document et qui sera montrée aux visiteurs"
#: agenda_culturel/models.py:739
#: agenda_culturel/models.py:741
msgid "Status of each imported event (published or draft)"
msgstr "Status de chaque événement importé (publié ou brouillon)"
#: agenda_culturel/models.py:740
#: agenda_culturel/models.py:742
msgid "Address for each imported event"
msgstr "Adresse de chaque événement importé"
#: agenda_culturel/models.py:741
#: agenda_culturel/models.py:743
msgid "Category of each imported event"
msgstr "Catégorie de chaque événement importé"
#: agenda_culturel/models.py:742
#: agenda_culturel/models.py:744
msgid "Tags for each imported event"
msgstr "Étiquettes de chaque événement importé"
#: agenda_culturel/models.py:742
#: agenda_culturel/models.py:744
msgid "A list of tags that describe each imported event."
msgstr "Une liste d'étiquettes décrivant chaque événement importé"
#: agenda_culturel/models.py:754
#: agenda_culturel/models.py:756
msgid "Running"
msgstr "En cours"
#: agenda_culturel/models.py:755
#: agenda_culturel/models.py:757
msgid "Canceled"
msgstr "Annulé"
#: agenda_culturel/models.py:756
#: agenda_culturel/models.py:758
msgid "Success"
msgstr "Succès"
#: agenda_culturel/models.py:757
#: agenda_culturel/models.py:759
msgid "Failed"
msgstr "Erreur"
#: agenda_culturel/models.py:762
#: agenda_culturel/models.py:764
msgid "Recurrent import"
msgstr "Import récurrent"
#: agenda_culturel/models.py:762
#: agenda_culturel/models.py:764
msgid "Reference to the recurrent import processing"
msgstr "Référence du processus d'import récurrent"
#: agenda_culturel/models.py:766
#: agenda_culturel/models.py:768
msgid "Error message"
msgstr "Votre message"
#: agenda_culturel/models.py:768
#: agenda_culturel/models.py:770
msgid "Number of collected events"
msgstr "Nombre d'événements collectés"
#: agenda_culturel/models.py:769
#: agenda_culturel/models.py:771
msgid "Number of imported events"
msgstr "Nombre d'événements importés"
#: agenda_culturel/models.py:770
#: agenda_culturel/models.py:772
msgid "Number of updated events"
msgstr "Nombre d'événements mis à jour"
#: agenda_culturel/models.py:771
#: agenda_culturel/models.py:773
msgid "Number of removed events"
msgstr "Nombre d'événements supprimés"
#: agenda_culturel/models.py:780
msgid "Weight"
msgstr ""
#: agenda_culturel/models.py:780
msgid "The lower is the weight, the earlier the filter is applied"
msgstr ""
#: agenda_culturel/models.py:782
msgid "Category applied to the event"
msgstr "Catégorie appliquée à l'événement"
#: agenda_culturel/models.py:784
msgid "Contained in the description"
msgstr "Contenu dans la description"
#: agenda_culturel/models.py:784
msgid "Text contained in the description"
msgstr "Texte contenu dans la description"
#: agenda_culturel/models.py:785
msgid "Exact description extract"
msgstr "Extrait exact de description"
#: agenda_culturel/models.py:785
msgid ""
"If checked, the extract will be searched for in the description using the "
"exact form (capitals, accents)."
msgstr ""
"Si coché, l'extrait sera recherché dans la description en utilisant la forme "
"exacte (majuscules, accents)"
#: agenda_culturel/models.py:787
msgid "Contained in the title"
msgstr "Contenu dans le titre"
#: agenda_culturel/models.py:787
msgid "Text contained in the event title"
msgstr "Texte contenu dans le titre de l'événement"
#: agenda_culturel/models.py:788
msgid "Exact title extract"
msgstr "Extrait exact du titre"
#: agenda_culturel/models.py:788
msgid ""
"If checked, the extract will be searched for in the title using the exact "
"form (capitals, accents)."
msgstr ""
"Si coché, l'extrait sera recherché dans le titre en utilisant la forme "
"exacte (majuscules, accents)"
#: agenda_culturel/settings/base.py:135
msgid "English"
msgstr "anglais"
@ -530,6 +586,31 @@ msgstr ""
"L'événement a été signalé comme dupliqué avec succès. Votre suggestion sera "
"prochainement prise en charge par l'équipe de modération."
#: agenda_culturel/views.py:814
msgid "The categorisation rule has been successfully modified."
msgstr "La règle de catégorisation a été modifiée avec succès."
#: agenda_culturel/views.py:820
msgid "The categorisation rule has been successfully deleted."
msgstr "La règle de catégorisation a été supprimée avec succès"
#: agenda_culturel/views.py:831
msgid "The rules were successfully applied and 1 event was categorised."
msgstr ""
"Les règles ont été appliquées avec succès et 1 événement a été catégorisé"
#: agenda_culturel/views.py:833
msgid "The rules were successfully applied and {} events were categorised."
msgstr ""
"Les règles ont été appliquées avec succès et {} événements ont été "
"catégorisés"
#: agenda_culturel/views.py:835
msgid "The rules were successfully applied and no events were categorised."
msgstr ""
"Les règles ont été appliquées avec succès et aucun événement n'a été "
"catégorisé"
msgid "Add another"
msgstr "Ajouter un autre"

View File

@ -0,0 +1,26 @@
# Generated by Django 4.2.7 on 2024-02-17 10:40
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('agenda_culturel', '0032_alter_recurrentimport_defaultcategory_and_more'),
]
operations = [
migrations.CreateModel(
name='CategorisationRule',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('weight', models.IntegerField(default=0, help_text='The lower is the weight, the earlier the filter is applied', verbose_name='Weight')),
('description_contains', models.CharField(blank=True, help_text='Text contained in the description', max_length=512, null=True, verbose_name='Contained in the description')),
('desc_exact', models.BooleanField(default=False, help_text='If checked, the extract will be searched for in the description using the exact form (capitals, accents).', verbose_name='Exact description extract')),
('title_contains', models.CharField(blank=True, help_text='Text contained in the event title', max_length=512, null=True, verbose_name='Contained in the title')),
('title_exact', models.BooleanField(default=False, help_text='If checked, the extract will be searched for in the title using the exact form (capitals, accents).', verbose_name='Exact title extract')),
('category', models.ForeignKey(help_text='Category applied to the event', on_delete=django.db.models.deletion.CASCADE, to='agenda_culturel.category', verbose_name='Category')),
],
),
]

View File

@ -15,6 +15,7 @@ from django.db.models import Q
import recurrence.fields
import recurrence
import copy
import unicodedata
from django.template.defaultfilters import date as _date
from datetime import time, timedelta, date
@ -26,6 +27,9 @@ from .calendar import CalendarList, CalendarDay
import logging
logger = logging.getLogger(__name__)
def remove_accents(input_str):
nfkd_form = unicodedata.normalize('NFKD', input_str)
return u"".join([c for c in nfkd_form if not unicodedata.combining(c)])
class StaticContent(models.Model):
@ -363,6 +367,10 @@ class Event(models.Model):
if self.image and not self.local_image:
self.download_image()
# try to detect category
if self.is_in_importation_process():
CategorisationRule.apply_rules(self)
def save(self, *args, **kwargs):
@ -772,3 +780,46 @@ class BatchImportation(models.Model):
nb_removed = models.PositiveIntegerField(verbose_name=_('Number of removed events'), default=0)
celery_id = models.CharField(max_length=128, default="")
class CategorisationRule(models.Model):
weight = models.IntegerField(verbose_name=_('Weight'), help_text=_("The lower is the weight, the earlier the filter is applied"), default=0)
category = models.ForeignKey(Category, verbose_name=_('Category'), help_text=_('Category applied to the event'), on_delete=models.CASCADE)
description_contains = models.CharField(verbose_name=_('Contained in the description'), help_text=_('Text contained in the description'), max_length=512, blank=True, null=True)
desc_exact = models.BooleanField(verbose_name=_('Exact description extract'), help_text=_("If checked, the extract will be searched for in the description using the exact form (capitals, accents)."), default=False)
title_contains = models.CharField(verbose_name=_('Contained in the title'), help_text=_('Text contained in the event title'), max_length=512, blank=True, null=True)
title_exact = models.BooleanField(verbose_name=_('Exact title extract'), help_text=_("If checked, the extract will be searched for in the title using the exact form (capitals, accents)."), default=False)
# on applique toutes les règles, de la première à la dernière
def apply_rules(event):
rules = CategorisationRule.objects.all().order_by("weight", "pk")
for rule in rules:
if rule.match(event):
event.category = rule.category
return 1
return 0
def match(self, event):
if self.description_contains:
if self.desc_exact:
if self.description_contains in event.description:
return True
else:
if remove_accents(self.description_contains).lower() in remove_accents(event.description).lower():
return True
if self.title_contains:
if self.title_exact:
if self.title_contains in event.title:
return True
else:
if remove_accents(self.title_contains).lower() in remove_accents(event.title).lower():
return True
return False

View File

@ -0,0 +1,84 @@
{% extends "agenda_culturel/page.html" %}
{% block title %}Règles de catégorisation{% endblock %}
{% load utils_extra %}
{% load cat_extra %}
{% block entete_header %}
{% css_categories %}
{% endblock %}
{% block content %}
<div class="grid two-columns">
<article>
<header>
<div class="slide-buttons">
<a href="{% url 'apply_catrules'%}" role="button" data-tooltip="Appliquer à tous les événements sans catégorie">Appliquer {% picto_from_name "arrow-down-circle" %}</a>
<a href="{% url 'add_catrule'%}" role="button">Ajouter {% picto_from_name "plus-circle" %}</a>
</div>
<h1>Règles de catégorisation</h1>
<p>Chaque règle est considérée dans l'ordre croissant des poids. La première règle satisfaite est appliquée par un changement de catégorie, et on les suivantes ne sont pas appliquées.</p>
<p>Une règle est satisfaite si au moins une des conditions est satisfaite.</p>
<p>Les règles sont appliquées à l'import sur tous les événements, et à la demande sur les événements sans catégorie.</p>
</header>
<table role="grid">
<thead>
<tr>
<th>Identifiant</th>
<th>Catégorie</th>
<th>Conditions</th>
<th>Poids</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for obj in paginator_filter %}
<tr>
<td>{{ obj.pk }}</a></td>
<td>{{ obj.category }}</td>
<td>
<ul>
{% if obj.title_contains %}
<li>Le titre contient {% if obj.title_exact %}exactement {% endif %} «&nbsp;{{ obj.title_contains }}&nbsp;»</li>
{% endif %}
{% if obj.description_contains %}
<li>La description contient {% if obj.desc_exact %}exactement {% endif %} «&nbsp;{{ obj.description_contains }}&nbsp;»</li>
{% endif %}
</ul>
</td>
<td>{{ obj.weight }}</td>
<td>
<div class="buttons">
<a href="{% url 'edit_catrule' obj.pk %}" role="button">Modifier</a>
<a href="{% url 'delete_catrule' obj.pk %}" role="button">Supprimer</a>
</div>
</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="catrules" %}
</div>
{% endblock %}

View File

@ -0,0 +1,30 @@
{% extends "agenda_culturel/page.html" %}
{% block title %}Supprimer la règle de catégorisation {{ object.pk }}{% endblock %}
{% block content %}
<h1>Suppression de la règle de catégorisation {{ object.pk }}</h1>
<form method="post">{% csrf_token %}
<p>Êtes-vous sûr·e de vouloir supprimer la règle de catégorisation «&nbsp;{{ object.pk }}&nbsp;»&nbsp;
par la catégorie {{ object.category }},
de poids {{ object.weight }} et
correspondant aux filtres ci-dessous&nbsp;?</p>
<ul>
{% if object.title_contains %}
<li>Le titre contient {% if object.title_exact %}exactement {% endif %} «&nbsp;{{ object.title_contains }}&nbsp;»</li>
{% endif %}
{% if object.description_contains %}
<li>La description contient {% if object.desc_exact %}exactement {% endif %} «&nbsp;{{ object.description_contains }}&nbsp;»</li>
{% endif %}
</ul>
{{ form }}
<div class="grid">
<a href="{% if request.META.HTTP_REFERER %}{{ request.META.HTTP_REFERER }}{% else %}{% url 'categorisation_rules' %}{% endif %}" role="button" class="secondary">Annuler</a>
<input type="submit" value="Confirmer">
</div>
</form>
{% endblock %}

View File

@ -0,0 +1,27 @@
{% extends "agenda_culturel/page.html" %}
{% load static %}
{% block title %}Règle de catéorisation{% endblock %}
{% block entete_header %}
<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>
{% endblock %}
{% block content %}
<h1>Règle de catéorisation</h1>
<article>
<form method="post">{% csrf_token %}
{{ form.as_p }}
<div class="grid">
<a href="{% if request.META.HTTP_REFERER %}{{ request.META.HTTP_REFERER }}{% else %}{% url 'categorisation_rules' %}{% endif %}" role="button" class="secondary">Annuler</a>
<input type="submit" value="Envoyer">
</div>
</form>
</article>
{% endblock %}

View File

@ -14,11 +14,12 @@
<li><a {% if current == "tags" %}class="selected" {% endif %}href="{% url 'view_all_tags' %}">Consulter les étiquettes</a></li>
</ul>
</nav>
<h2>Importations</h2>
<h3>Traitements automatiques</h3>
<nav>
<ul>
<li><a {% if current == "imports" %}class="selected" {% endif %}href="{% url 'imports' %}">Historiques des importations</a></li>
<li><a {% if current == "rimports" %}class="selected" {% endif %}href="{% url 'recurrent_imports' %}">Importations récurrentes</a></li>
<li><a {% if current == "catrules" %}class="selected" {% endif %}href="{% url 'categorisation_rules' %}">Règles de catégorisation</a></li>
</ul>
</nav>
<h3>Messages</h3>

View File

@ -46,6 +46,11 @@ urlpatterns = [
path("rimports/<int:pk>/edit", RecurrentImportUpdateView.as_view(), name="edit_rimport"),
path("rimports/<int:pk>/delete", RecurrentImportDeleteView.as_view(), name="delete_rimport"),
path("rimports/<int:pk>/run", run_rimport, name="run_rimport"),
path("catrules/", categorisation_rules, name="categorisation_rules"),
path("catrules/add", CategorisationRuleCreateView.as_view(), name="add_catrule"),
path("catrules/<int:pk>/edit", CategorisationRuleUpdateView.as_view(), name="edit_catrule"),
path("catrules/<int:pk>/delete", CategorisationRuleDeleteView.as_view(), name="delete_catrule"),
path("catrules/apply", apply_categorisation_rules, name="apply_catrules"),
path("duplicates/", duplicates, name="duplicates"),
path("duplicates/<int:pk>", DuplicatedEventsDetailView.as_view(), name="view_duplicate"),
path("duplicates/<int:pk>/fix", fix_duplicate, name="fix_duplicate"),

View File

@ -11,9 +11,9 @@ from django.http import HttpResponseRedirect
from django.urls import reverse
import urllib
from .forms import EventSubmissionForm, EventForm, BatchImportationForm, FixDuplicates, SelectEventInList, MergeDuplicates, RecurrentImportForm
from .forms import EventSubmissionForm, EventForm, BatchImportationForm, FixDuplicates, SelectEventInList, MergeDuplicates, RecurrentImportForm, CategorisationRuleImportForm
from .models import Event, Category, StaticContent, ContactMessage, BatchImportation, DuplicatedEvents, RecurrentImport
from .models import Event, Category, StaticContent, ContactMessage, BatchImportation, DuplicatedEvents, RecurrentImport, CategorisationRule, remove_accents
from django.utils import timezone
from enum import StrEnum
from datetime import date, timedelta
@ -181,9 +181,6 @@ def view_tag(request, t):
def tag_list(request):
def remove_accents(input_str):
nfkd_form = unicodedata.normalize('NFKD', input_str)
return u"".join([c for c in nfkd_form if not unicodedata.combining(c)])
tags = Event.get_all_tags()
context = {"tags": sorted(tags, key=lambda x: remove_accents(x).lower())}
@ -782,3 +779,62 @@ def set_duplicate(request, year, month, day, pk):
return render(request, 'agenda_culturel/set_duplicate.html', context={'form': form, 'event': event})
#########################
## categorisation rules
#########################
@login_required(login_url="/accounts/login/")
def categorisation_rules(request):
paginator = Paginator(CategorisationRule.objects.all().order_by("weight", "pk"), 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/categorisation_rules.html', {'paginator_filter': response} )
class CategorisationRuleCreateView(LoginRequiredMixin, CreateView):
model = CategorisationRule
success_url = reverse_lazy('categorisation_rules')
form_class = CategorisationRuleImportForm
class CategorisationRuleUpdateView(SuccessMessageMixin, LoginRequiredMixin, UpdateView):
model = CategorisationRule
form_class = CategorisationRuleImportForm
success_url = reverse_lazy('categorisation_rules')
success_message = _('The categorisation rule has been successfully modified.')
class CategorisationRuleDeleteView(SuccessMessageMixin, LoginRequiredMixin, DeleteView):
model = CategorisationRule
success_url = reverse_lazy('categorisation_rules')
success_message = _('The categorisation rule has been successfully deleted.')
@login_required(login_url="/accounts/login/")
def apply_categorisation_rules(request):
nb = 0
for e in Event.objects.filter(category=Category.get_default_category_id()):
success = CategorisationRule.apply_rules(e)
if success:
nb += 1
e.save()
if nb != 0:
if nb == 1:
messages.success(request, _("The rules were successfully applied and 1 event was categorised."))
else:
messages.success(request, _("The rules were successfully applied and {} events were categorised.").format(nb))
else:
messages.info(request, _("The rules were successfully applied and no events were categorised."))
return HttpResponseRedirect(reverse_lazy("categorisation_rules"))