diff --git a/src/agenda_culturel/forms.py b/src/agenda_culturel/forms.py index f98ea50..59d87c2 100644 --- a/src/agenda_culturel/forms.py +++ b/src/agenda_culturel/forms.py @@ -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: diff --git a/src/agenda_culturel/import_tasks/extractor_ical.py b/src/agenda_culturel/import_tasks/extractor_ical.py index 8f825dd..63ad97a 100644 --- a/src/agenda_culturel/import_tasks/extractor_ical.py +++ b/src/agenda_culturel/import_tasks/extractor_ical.py @@ -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): diff --git a/src/agenda_culturel/locale/fr/LC_MESSAGES/django.po b/src/agenda_culturel/locale/fr/LC_MESSAGES/django.po index 8c6f1c0..5c95fe2 100644 --- a/src/agenda_culturel/locale/fr/LC_MESSAGES/django.po +++ b/src/agenda_culturel/locale/fr/LC_MESSAGES/django.po @@ -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 \n" "Language-Team: Jean-Marie Favreau \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" diff --git a/src/agenda_culturel/migrations/0033_categorisationrule.py b/src/agenda_culturel/migrations/0033_categorisationrule.py new file mode 100644 index 0000000..5f46ba5 --- /dev/null +++ b/src/agenda_culturel/migrations/0033_categorisationrule.py @@ -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')), + ], + ), + ] diff --git a/src/agenda_culturel/models.py b/src/agenda_culturel/models.py index ec48de1..103ac8a 100644 --- a/src/agenda_culturel/models.py +++ b/src/agenda_culturel/models.py @@ -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 diff --git a/src/agenda_culturel/templates/agenda_culturel/categorisation_rules.html b/src/agenda_culturel/templates/agenda_culturel/categorisation_rules.html new file mode 100644 index 0000000..1bbcf5d --- /dev/null +++ b/src/agenda_culturel/templates/agenda_culturel/categorisation_rules.html @@ -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 %} +
+
+
+ +

Règles de catégorisation

+

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.

+

Une règle est satisfaite si au moins une des conditions est satisfaite.

+

Les règles sont appliquées à l'import sur tous les événements, et à la demande sur les événements sans catégorie.

+
+ + + + + + + + + + + + + {% for obj in paginator_filter %} + + + + + + + + {% endfor %} + +
IdentifiantCatégorieConditionsPoidsActions
{{ obj.pk }}{{ obj.category }} +
    + {% if obj.title_contains %} +
  • Le titre contient {% if obj.title_exact %}exactement {% endif %} « {{ obj.title_contains }} »
  • + {% endif %} + {% if obj.description_contains %} +
  • La description contient {% if obj.desc_exact %}exactement {% endif %} « {{ obj.description_contains }} »
  • + {% endif %} +
+
{{ obj.weight }} + +
+ +
+ + {% if paginator_filter.has_previous %} + « premier + précédent + {% endif %} + + + Page {{ paginator_filter.number }} sur {{ paginator_filter.paginator.num_pages }} + + + {% if paginator_filter.has_next %} + suivant + dernier » + {% endif %} + +
+
+ +{% include "agenda_culturel/side-nav.html" with current="catrules" %} +
+ +{% endblock %} \ No newline at end of file diff --git a/src/agenda_culturel/templates/agenda_culturel/categorisationrule_confirm_delete.html b/src/agenda_culturel/templates/agenda_culturel/categorisationrule_confirm_delete.html new file mode 100644 index 0000000..a3e7cc1 --- /dev/null +++ b/src/agenda_culturel/templates/agenda_culturel/categorisationrule_confirm_delete.html @@ -0,0 +1,30 @@ +{% extends "agenda_culturel/page.html" %} + +{% block title %}Supprimer la règle de catégorisation {{ object.pk }}{% endblock %} + + +{% block content %} + +

Suppression de la règle de catégorisation {{ object.pk }}

+
{% csrf_token %} +

Êtes-vous sûr·e de vouloir supprimer la règle de catégorisation « {{ object.pk }} »  + par la catégorie {{ object.category }}, + de poids {{ object.weight }} et + correspondant aux filtres ci-dessous ?

+
    + {% if object.title_contains %} +
  • Le titre contient {% if object.title_exact %}exactement {% endif %} « {{ object.title_contains }} »
  • + {% endif %} + {% if object.description_contains %} +
  • La description contient {% if object.desc_exact %}exactement {% endif %} « {{ object.description_contains }} »
  • + {% endif %} +
+ + {{ form }} +
+ Annuler + +
+
+ +{% endblock %} \ No newline at end of file diff --git a/src/agenda_culturel/templates/agenda_culturel/categorisationrule_form.html b/src/agenda_culturel/templates/agenda_culturel/categorisationrule_form.html new file mode 100644 index 0000000..dcd19ac --- /dev/null +++ b/src/agenda_culturel/templates/agenda_culturel/categorisationrule_form.html @@ -0,0 +1,27 @@ +{% extends "agenda_culturel/page.html" %} +{% load static %} + +{% block title %}Règle de catéorisation{% endblock %} + +{% block entete_header %} + + + + +{% endblock %} + +{% block content %} + +

Règle de catéorisation

+ +
+
{% csrf_token %} + {{ form.as_p }} +
+ Annuler + +
+
+
+ +{% endblock %} \ No newline at end of file diff --git a/src/agenda_culturel/templates/agenda_culturel/side-nav.html b/src/agenda_culturel/templates/agenda_culturel/side-nav.html index 10a324f..feec84c 100644 --- a/src/agenda_culturel/templates/agenda_culturel/side-nav.html +++ b/src/agenda_culturel/templates/agenda_culturel/side-nav.html @@ -14,11 +14,12 @@
  • Consulter les étiquettes
  • -

    Importations

    +

    Traitements automatiques

    Messages

    diff --git a/src/agenda_culturel/urls.py b/src/agenda_culturel/urls.py index d5338e7..9c6a239 100644 --- a/src/agenda_culturel/urls.py +++ b/src/agenda_culturel/urls.py @@ -46,6 +46,11 @@ urlpatterns = [ path("rimports//edit", RecurrentImportUpdateView.as_view(), name="edit_rimport"), path("rimports//delete", RecurrentImportDeleteView.as_view(), name="delete_rimport"), path("rimports//run", run_rimport, name="run_rimport"), + path("catrules/", categorisation_rules, name="categorisation_rules"), + path("catrules/add", CategorisationRuleCreateView.as_view(), name="add_catrule"), + path("catrules//edit", CategorisationRuleUpdateView.as_view(), name="edit_catrule"), + path("catrules//delete", CategorisationRuleDeleteView.as_view(), name="delete_catrule"), + path("catrules/apply", apply_categorisation_rules, name="apply_catrules"), path("duplicates/", duplicates, name="duplicates"), path("duplicates/", DuplicatedEventsDetailView.as_view(), name="view_duplicate"), path("duplicates//fix", fix_duplicate, name="fix_duplicate"), diff --git a/src/agenda_culturel/views.py b/src/agenda_culturel/views.py index 14a28c3..5732b5e 100644 --- a/src/agenda_culturel/views.py +++ b/src/agenda_culturel/views.py @@ -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")) + \ No newline at end of file