Merge branch 'main' of ssh://forge.chapril.org:222/jmtrivial/agenda_culturel

This commit is contained in:
Jean-Marie Favreau 2024-02-17 13:03:30 +01:00
commit 07395da9e0
12 changed files with 501 additions and 123 deletions

View File

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

View File

@ -143,7 +143,6 @@ class ICALNoVCExtractor(ICALExtractor):
return text return text
else: else:
result = self.parser.format(text) result = self.parser.format(text)
logger.warning(result)
return 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): 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 "" msgstr ""
"Project-Id-Version: agenda_culturel\n" "Project-Id-Version: agenda_culturel\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: 2023-10-29 14:16+0000\n"
"Last-Translator: Jean-Marie Favreau <jeanmarie.favreau@free.fr>\n" "Last-Translator: Jean-Marie Favreau <jeanmarie.favreau@free.fr>\n"
"Language-Team: 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-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\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." msgid "The end date must be after the start date."
msgstr "La date de fin doit être après la date de début." 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." 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." 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." msgid "JSON in the format expected for the import."
msgstr "JSON dans le format attendu pour l'import" msgstr "JSON dans le format attendu pour l'import"
#: agenda_culturel/models.py:32 agenda_culturel/models.py:61 #: agenda_culturel/models.py:33 agenda_culturel/models.py:62
#: agenda_culturel/models.py:702 #: agenda_culturel/models.py:703
msgid "Name" msgid "Name"
msgstr "Nom" 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" msgid "Category name"
msgstr "Nom de la catégorie" msgstr "Nom de la catégorie"
#: agenda_culturel/models.py:33 #: agenda_culturel/models.py:34
msgid "Content" msgid "Content"
msgstr "Contenu" msgstr "Contenu"
#: agenda_culturel/models.py:33 #: agenda_culturel/models.py:34
msgid "Text as shown to the visitors" msgid "Text as shown to the visitors"
msgstr "Text tel que présenté aux visiteureuses" msgstr "Text tel que présenté aux visiteureuses"
#: agenda_culturel/models.py:34 #: agenda_culturel/models.py:35
msgid "URL path" msgid "URL path"
msgstr "" msgstr ""
#: agenda_culturel/models.py:34 #: agenda_culturel/models.py:35
msgid "URL path where the content is included." msgid "URL path where the content is included."
msgstr "" msgstr ""
#: agenda_culturel/models.py:62 #: agenda_culturel/models.py:63
msgid "Alternative Name" msgid "Alternative Name"
msgstr "Nom alternatif" msgstr "Nom alternatif"
#: agenda_culturel/models.py:62 #: agenda_culturel/models.py:63
msgid "Alternative name used with a time period" msgid "Alternative name used with a time period"
msgstr "Nom alternatif utilisé avec une période de temps" msgstr "Nom alternatif utilisé avec une période de temps"
#: agenda_culturel/models.py:63 #: agenda_culturel/models.py:64
msgid "Short name" msgid "Short name"
msgstr "Nom court" msgstr "Nom court"
#: agenda_culturel/models.py:63 #: agenda_culturel/models.py:64
msgid "Short name of the category" msgid "Short name of the category"
msgstr "Nom court de la catégorie" msgstr "Nom court de la catégorie"
#: agenda_culturel/models.py:64 #: agenda_culturel/models.py:65
msgid "Color" msgid "Color"
msgstr "Couleur" msgstr "Couleur"
#: agenda_culturel/models.py:64 #: agenda_culturel/models.py:65
msgid "Color used as background for the category" msgid "Color used as background for the category"
msgstr "Couleur utilisée comme fond de la catégorie" msgstr "Couleur utilisée comme fond de la catégorie"
#: agenda_culturel/models.py:101 agenda_culturel/models.py:168 #: agenda_culturel/models.py:102 agenda_culturel/models.py:169
#: agenda_culturel/models.py:741 #: agenda_culturel/models.py:743 agenda_culturel/models.py:782
msgid "Category" msgid "Category"
msgstr "Catégorie" msgstr "Catégorie"
#: agenda_culturel/models.py:102 #: agenda_culturel/models.py:103
msgid "Categories" msgid "Categories"
msgstr "Catégories" 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" msgid "Published"
msgstr "Publié" msgstr "Publié"
#: agenda_culturel/models.py:154 #: agenda_culturel/models.py:155
msgid "Draft" msgid "Draft"
msgstr "Brouillon" msgstr "Brouillon"
#: agenda_culturel/models.py:155 #: agenda_culturel/models.py:156
msgid "Trash" msgid "Trash"
msgstr "Corbeille" msgstr "Corbeille"
#: agenda_culturel/models.py:164 #: agenda_culturel/models.py:165
msgid "Title" msgid "Title"
msgstr "Titre" msgstr "Titre"
#: agenda_culturel/models.py:164 #: agenda_culturel/models.py:165
msgid "Short title" msgid "Short title"
msgstr "Titre court" 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" msgid "Status"
msgstr "Status" msgstr "Status"
#: agenda_culturel/models.py:168 #: agenda_culturel/models.py:169
msgid "Category of the event" msgid "Category of the event"
msgstr "Catégorie de l'événement" msgstr "Catégorie de l'événement"
#: agenda_culturel/models.py:170 #: agenda_culturel/models.py:171
msgid "Day of the event" msgid "Day of the event"
msgstr "Date de l'événement" msgstr "Date de l'événement"
#: agenda_culturel/models.py:171 #: agenda_culturel/models.py:172
msgid "Starting time" msgid "Starting time"
msgstr "Heure de début" msgstr "Heure de début"
#: agenda_culturel/models.py:173 #: agenda_culturel/models.py:174
msgid "End day of the event" msgid "End day of the event"
msgstr "Fin de l'événement" 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." msgid "End day of the event, only required if different from the start day."
msgstr "" msgstr ""
"Date de fin de l'événement, uniquement nécessaire s'il est différent du " "Date de fin de l'événement, uniquement nécessaire s'il est différent du "
"premier jour de l'événement" "premier jour de l'événement"
#: agenda_culturel/models.py:174 #: agenda_culturel/models.py:175
msgid "Final time" msgid "Final time"
msgstr "Heure de fin" msgstr "Heure de fin"
#: agenda_culturel/models.py:176 #: agenda_culturel/models.py:177
msgid "Recurrence" msgid "Recurrence"
msgstr "Récurrence" 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" msgid "Location"
msgstr "Localisation" msgstr "Localisation"
#: agenda_culturel/models.py:178 #: agenda_culturel/models.py:179
msgid "Address of the event" msgid "Address of the event"
msgstr "Adresse de l'événement" msgstr "Adresse de l'événement"
#: agenda_culturel/models.py:180 #: agenda_culturel/models.py:181
msgid "Description" msgid "Description"
msgstr "Description" msgstr "Description"
#: agenda_culturel/models.py:180 #: agenda_culturel/models.py:181
msgid "General description of the event" msgid "General description of the event"
msgstr "Description générale de l'événement" msgstr "Description générale de l'événement"
#: agenda_culturel/models.py:182 #: agenda_culturel/models.py:183
msgid "Illustration (local image)" msgid "Illustration (local image)"
msgstr "Illustration (image locale)" msgstr "Illustration (image locale)"
#: agenda_culturel/models.py:182 #: agenda_culturel/models.py:183
msgid "Illustration image stored in the agenda server" msgid "Illustration image stored in the agenda server"
msgstr "Image d'illustration stockée sur le serveur de l'agenda" msgstr "Image d'illustration stockée sur le serveur de l'agenda"
#: agenda_culturel/models.py:184 #: agenda_culturel/models.py:185
msgid "Illustration" msgid "Illustration"
msgstr "Illustration" msgstr "Illustration"
#: agenda_culturel/models.py:184 #: agenda_culturel/models.py:185
msgid "URL of the illustration image" msgid "URL of the illustration image"
msgstr "URL de l'image illustrative" msgstr "URL de l'image illustrative"
#: agenda_culturel/models.py:185 #: agenda_culturel/models.py:186
msgid "Illustration description" msgid "Illustration description"
msgstr "Description de l'illustration" 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" msgid "Alternative text used by screen readers for the image"
msgstr "Texte alternatif utiliser par les lecteurs d'écrans pour l'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" msgid "Importation source"
msgstr "Source d'importation" msgstr "Source d'importation"
#: agenda_culturel/models.py:187 #: agenda_culturel/models.py:188
msgid "Importation source used to detect removed entries." msgid "Importation source used to detect removed entries."
msgstr "Source d'importation utilisée pour détecter les éléments supprimés/" 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" msgid "UUIDs"
msgstr "UUIDs" msgstr "UUIDs"
#: agenda_culturel/models.py:188 #: agenda_culturel/models.py:189
msgid "UUIDs from import to detect duplicated entries." msgid "UUIDs from import to detect duplicated entries."
msgstr "UUIDs utilisés pendant l'import pour détecter les entrées dupliquées" 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" msgid "URLs"
msgstr "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." 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é." 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" msgid "Tags"
msgstr "Étiquettes" msgstr "Étiquettes"
#: agenda_culturel/models.py:191 #: agenda_culturel/models.py:192
msgid "A list of tags that describe the event." msgid "A list of tags that describe the event."
msgstr "Une liste d'étiquettes décrivant l'événement" msgstr "Une liste d'étiquettes décrivant l'événement"
#: agenda_culturel/models.py:193 #: agenda_culturel/models.py:194
msgid "Possibly duplicated" msgid "Possibly duplicated"
msgstr "Possibles doublons" msgstr "Possibles doublons"
#: agenda_culturel/models.py:234 #: agenda_culturel/models.py:235
msgid "Event" msgid "Event"
msgstr "Événement" msgstr "Événement"
#: agenda_culturel/models.py:235 #: agenda_culturel/models.py:236
msgid "Events" msgid "Events"
msgstr "Événements" msgstr "Événements"
#: agenda_culturel/models.py:701 #: agenda_culturel/models.py:702
msgid "Subject" msgid "Subject"
msgstr "Sujet" msgstr "Sujet"
#: agenda_culturel/models.py:701 #: agenda_culturel/models.py:702
msgid "The subject of your message" msgid "The subject of your message"
msgstr "Sujet de votre message" msgstr "Sujet de votre message"
#: agenda_culturel/models.py:702 #: agenda_culturel/models.py:703
msgid "Your name" msgid "Your name"
msgstr "Votre nom" msgstr "Votre nom"
#: agenda_culturel/models.py:703 #: agenda_culturel/models.py:704
msgid "Email address" msgid "Email address"
msgstr "Adresse email" msgstr "Adresse email"
#: agenda_culturel/models.py:703 #: agenda_culturel/models.py:704
msgid "Your email address" msgid "Your email address"
msgstr "Votre adresse email" msgstr "Votre adresse email"
#: agenda_culturel/models.py:704 #: agenda_culturel/models.py:705
msgid "Message" msgid "Message"
msgstr "Message" msgstr "Message"
#: agenda_culturel/models.py:704 #: agenda_culturel/models.py:705
msgid "Your message" msgid "Your message"
msgstr "Votre 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" msgid "Closed"
msgstr "Fermé" 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" 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" 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" msgid "Comments"
msgstr "Commentaires" msgstr "Commentaires"
#: agenda_culturel/models.py:709 #: agenda_culturel/models.py:710
msgid "Comments on the message from the moderation team" msgid "Comments on the message from the moderation team"
msgstr "Commentaires sur ce message par l'équipe de modération" msgstr "Commentaires sur ce message par l'équipe de modération"
#: agenda_culturel/models.py:718 #: agenda_culturel/models.py:719
msgid "ical" msgid "ical"
msgstr "ical" msgstr "ical"
#: agenda_culturel/models.py:719 #: agenda_culturel/models.py:720
msgid "ical no busy" msgid "ical no busy"
msgstr "ical sans 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" msgid "simple"
msgstr "simple" msgstr "simple"
#: agenda_culturel/models.py:723 #: agenda_culturel/models.py:725
msgid "Headless Chromium" msgid "Headless Chromium"
msgstr "chromium sans interface" msgstr "chromium sans interface"
#: agenda_culturel/models.py:727 #: agenda_culturel/models.py:729
msgid "daily" msgid "daily"
msgstr "chaque jour" msgstr "chaque jour"
#: agenda_culturel/models.py:728 #: agenda_culturel/models.py:730
msgid "weekly" msgid "weekly"
msgstr "chaque semaine" msgstr "chaque semaine"
#: agenda_culturel/models.py:730 #: agenda_culturel/models.py:732
msgid "Processor" msgid "Processor"
msgstr "Processeur" msgstr "Processeur"
#: agenda_culturel/models.py:731 #: agenda_culturel/models.py:733
msgid "Downloader" msgid "Downloader"
msgstr "Téléchargeur" msgstr "Téléchargeur"
#: agenda_culturel/models.py:733 #: agenda_culturel/models.py:735
msgid "Import recurrence" msgid "Import recurrence"
msgstr "Récurrence d'import" msgstr "Récurrence d'import"
#: agenda_culturel/models.py:736 #: agenda_culturel/models.py:738
msgid "Source" msgid "Source"
msgstr "Source" msgstr "Source"
#: agenda_culturel/models.py:736 #: agenda_culturel/models.py:738
msgid "URL of the source document" msgid "URL of the source document"
msgstr "URL du document source" msgstr "URL du document source"
#: agenda_culturel/models.py:737 #: agenda_culturel/models.py:739
msgid "Browsable url" msgid "Browsable url"
msgstr "URL navigable" 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." msgid "URL of the corresponding document that will be shown to visitors."
msgstr "URL correspondant au document et qui sera montrée aux visiteurs" 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)" msgid "Status of each imported event (published or draft)"
msgstr "Status de chaque événement importé (publié ou brouillon)" 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" msgid "Address for each imported event"
msgstr "Adresse de chaque événement importé" msgstr "Adresse de chaque événement importé"
#: agenda_culturel/models.py:741 #: agenda_culturel/models.py:743
msgid "Category of each imported event" msgid "Category of each imported event"
msgstr "Catégorie de chaque événement importé" msgstr "Catégorie de chaque événement importé"
#: agenda_culturel/models.py:742 #: agenda_culturel/models.py:744
msgid "Tags for each imported event" msgid "Tags for each imported event"
msgstr "Étiquettes de chaque événement importé" 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." msgid "A list of tags that describe each imported event."
msgstr "Une liste d'étiquettes décrivant chaque événement importé" msgstr "Une liste d'étiquettes décrivant chaque événement importé"
#: agenda_culturel/models.py:754 #: agenda_culturel/models.py:756
msgid "Running" msgid "Running"
msgstr "En cours" msgstr "En cours"
#: agenda_culturel/models.py:755 #: agenda_culturel/models.py:757
msgid "Canceled" msgid "Canceled"
msgstr "Annulé" msgstr "Annulé"
#: agenda_culturel/models.py:756 #: agenda_culturel/models.py:758
msgid "Success" msgid "Success"
msgstr "Succès" msgstr "Succès"
#: agenda_culturel/models.py:757 #: agenda_culturel/models.py:759
msgid "Failed" msgid "Failed"
msgstr "Erreur" msgstr "Erreur"
#: agenda_culturel/models.py:762 #: agenda_culturel/models.py:764
msgid "Recurrent import" msgid "Recurrent import"
msgstr "Import récurrent" msgstr "Import récurrent"
#: agenda_culturel/models.py:762 #: agenda_culturel/models.py:764
msgid "Reference to the recurrent import processing" msgid "Reference to the recurrent import processing"
msgstr "Référence du processus d'import récurrent" msgstr "Référence du processus d'import récurrent"
#: agenda_culturel/models.py:766 #: agenda_culturel/models.py:768
msgid "Error message" msgid "Error message"
msgstr "Votre message" msgstr "Votre message"
#: agenda_culturel/models.py:768 #: agenda_culturel/models.py:770
msgid "Number of collected events" msgid "Number of collected events"
msgstr "Nombre d'événements collectés" msgstr "Nombre d'événements collectés"
#: agenda_culturel/models.py:769 #: agenda_culturel/models.py:771
msgid "Number of imported events" msgid "Number of imported events"
msgstr "Nombre d'événements importés" msgstr "Nombre d'événements importés"
#: agenda_culturel/models.py:770 #: agenda_culturel/models.py:772
msgid "Number of updated events" msgid "Number of updated events"
msgstr "Nombre d'événements mis à jour" msgstr "Nombre d'événements mis à jour"
#: agenda_culturel/models.py:771 #: agenda_culturel/models.py:773
msgid "Number of removed events" msgid "Number of removed events"
msgstr "Nombre d'événements supprimés" 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 #: agenda_culturel/settings/base.py:135
msgid "English" msgid "English"
msgstr "anglais" msgstr "anglais"
@ -530,6 +586,31 @@ msgstr ""
"L'événement a été signalé comme dupliqué avec succès. Votre suggestion sera " "L'événement a été signalé comme dupliqué avec succès. Votre suggestion sera "
"prochainement prise en charge par l'équipe de modération." "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" msgid "Add another"
msgstr "Ajouter un autre" msgstr "Ajouter un autre"

View File

@ -0,0 +1,24 @@
# Generated by Django 4.2.7 on 2024-02-17 08:45
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('agenda_culturel', '0031_recurrentimport_defaultpublished'),
]
operations = [
migrations.AlterField(
model_name='recurrentimport',
name='defaultCategory',
field=models.ForeignKey(default=1, help_text='Category of each imported event', on_delete=django.db.models.deletion.SET_DEFAULT, to='agenda_culturel.category', verbose_name='Category'),
),
migrations.AlterField(
model_name='recurrentimport',
name='processor',
field=models.CharField(choices=[('ical', 'ical'), ('icalnobusy', 'ical no busy'), ('icalnovc', 'ical no VC')], default='ical', max_length=20, verbose_name='Processor'),
),
]

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.fields
import recurrence import recurrence
import copy import copy
import unicodedata
from django.template.defaultfilters import date as _date from django.template.defaultfilters import date as _date
from datetime import time, timedelta, date from datetime import time, timedelta, date
@ -26,6 +27,9 @@ from .calendar import CalendarList, CalendarDay
import logging import logging
logger = logging.getLogger(__name__) 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): class StaticContent(models.Model):
@ -363,6 +367,10 @@ class Event(models.Model):
if self.image and not self.local_image: if self.image and not self.local_image:
self.download_image() self.download_image()
# try to detect category
if self.is_in_importation_process():
CategorisationRule.apply_rules(self)
def save(self, *args, **kwargs): 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) nb_removed = models.PositiveIntegerField(verbose_name=_('Number of removed events'), default=0)
celery_id = models.CharField(max_length=128, default="") 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

@ -4,41 +4,31 @@
<aside> <aside>
<article> <article>
<header> <header>
<h2>Consulter</h2> <h2>Administrer</h2>
</header>
<nav>
<ul>
<li><a {% if current == "tags" %}class="selected" {% endif %}href="{% url 'view_all_tags' %}">Toutes les étiquettes</a></li>
</ul>
</nav>
</article>
<article>
<header>
<h2>Administrer</h2>
</header> </header>
<h3>Événements</h3>
<nav> <nav>
<ul> <ul>
<li><a {% if current == "moderation" %}class="selected" {% endif %}href="{% url 'moderation' %}">Derniers événements soumis</a>{% show_badges_events "left" %}</li> <li><a {% if current == "moderation" %}class="selected" {% endif %}href="{% url 'moderation' %}">Derniers événements soumis</a>{% show_badges_events "left" %}</li>
<li><a {% if current == "contactmessages" %}class="selected" {% endif %}href="{% url 'contactmessages' %}">Messages de contact</a>{% show_badge_contactmessages "left" %}</li>
<li><a {% if current == "duplicates" %}class="selected" {% endif %}href="{% url 'duplicates' %}">Gestion des doublons</a>{% show_badge_duplicated "left" %}</li> <li><a {% if current == "duplicates" %}class="selected" {% endif %}href="{% url 'duplicates' %}">Gestion des doublons</a>{% show_badge_duplicated "left" %}</li>
<li><a {% if current == "tags" %}class="selected" {% endif %}href="{% url 'view_all_tags' %}">Consulter les étiquettes</a></li>
</ul> </ul>
</nav> </nav>
</article> <h3>Traitements automatiques</h3>
<article>
<header>
<h2>Importations</h2>
</header>
<nav> <nav>
<ul> <ul>
<li><a {% if current == "imports" %}class="selected" {% endif %}href="{% url 'imports' %}">Historiques des importations</a></li> <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 == "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> </ul>
</nav> </nav>
</article> <h3>Messages</h3>
<article> <nav>
<header> <ul>
<h2>Configurer</h2> <li><a {% if current == "contactmessages" %}class="selected" {% endif %}href="{% url 'contactmessages' %}">Messages de contact</a>{% show_badge_contactmessages "left" %}</li>
</header> </ul>
</nav>
<h3>Configuration interne</h3>
<nav> <nav>
<ul> <ul>
<li><a href="{% url 'admin:index' %}">Administration de django</a></li> <li><a href="{% url 'admin:index' %}">Administration de django</a></li>

View File

@ -46,6 +46,11 @@ urlpatterns = [
path("rimports/<int:pk>/edit", RecurrentImportUpdateView.as_view(), name="edit_rimport"), 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>/delete", RecurrentImportDeleteView.as_view(), name="delete_rimport"),
path("rimports/<int:pk>/run", run_rimport, name="run_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/", duplicates, name="duplicates"),
path("duplicates/<int:pk>", DuplicatedEventsDetailView.as_view(), name="view_duplicate"), path("duplicates/<int:pk>", DuplicatedEventsDetailView.as_view(), name="view_duplicate"),
path("duplicates/<int:pk>/fix", fix_duplicate, name="fix_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 from django.urls import reverse
import urllib 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 django.utils import timezone
from enum import StrEnum from enum import StrEnum
from datetime import date, timedelta from datetime import date, timedelta
@ -181,9 +181,6 @@ def view_tag(request, t):
def tag_list(request): 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() tags = Event.get_all_tags()
context = {"tags": sorted(tags, key=lambda x: remove_accents(x).lower())} 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}) 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"))