From c6352091eaa6726b7601c76afcdb3625188fc8f8 Mon Sep 17 00:00:00 2001 From: Jean-Marie Favreau Date: Sat, 27 Apr 2024 18:32:08 +0200 Subject: [PATCH] Ajout de la gestion des lieux --- src/agenda_culturel/forms.py | 51 ++- .../locale/fr/LC_MESSAGES/django.po | 380 ++++++++++-------- .../migrations/0057_alter_place_aliases.py | 19 + .../0058_place_city_alter_place_address.py | 24 ++ .../migrations/0059_auto_20240427_1829.py | 23 ++ src/agenda_culturel/models.py | 19 +- src/agenda_culturel/static/style.scss | 12 + .../agenda_culturel/event-location-inc.html | 14 + .../templates/agenda_culturel/filter-inc.html | 3 + .../templates/agenda_culturel/page.html | 3 + .../agenda_culturel/place_confirm_delete.html | 10 +- .../agenda_culturel/place_detail.html | 9 +- .../templates/agenda_culturel/place_form.html | 9 +- .../templates/agenda_culturel/place_list.html | 12 +- .../agenda_culturel/place_unknown_form.html | 49 +++ .../agenda_culturel/place_unknown_list.html | 49 +++ .../templates/agenda_culturel/side-nav.html | 3 + .../single-event/event-in-flat-list-inc.html | 2 +- .../event-in-list-by-day-inc.html | 6 +- .../single-event/event-in-list-inc.html | 6 +- .../event-in-unknown-place-list-inc.html | 46 +++ .../single-event/event-modal-inc.html | 2 +- .../single-event/event-single-inc.html | 2 +- .../templates/location_field/map_widget.html | 13 +- .../templatetags/event_extra.py | 11 +- src/agenda_culturel/urls.py | 5 + src/agenda_culturel/views.py | 142 ++++++- 27 files changed, 713 insertions(+), 211 deletions(-) create mode 100644 src/agenda_culturel/migrations/0057_alter_place_aliases.py create mode 100644 src/agenda_culturel/migrations/0058_place_city_alter_place_address.py create mode 100644 src/agenda_culturel/migrations/0059_auto_20240427_1829.py create mode 100644 src/agenda_culturel/templates/agenda_culturel/event-location-inc.html create mode 100644 src/agenda_culturel/templates/agenda_culturel/place_unknown_form.html create mode 100644 src/agenda_culturel/templates/agenda_culturel/place_unknown_list.html create mode 100644 src/agenda_culturel/templates/agenda_culturel/single-event/event-in-unknown-place-list-inc.html diff --git a/src/agenda_culturel/forms.py b/src/agenda_culturel/forms.py index 6bb1249..d26aba3 100644 --- a/src/agenda_culturel/forms.py +++ b/src/agenda_culturel/forms.py @@ -1,8 +1,8 @@ -from django.forms import ModelForm, ValidationError, TextInput, Form, URLField, MultipleHiddenInput, Textarea, CharField, ChoiceField, RadioSelect, MultipleChoiceField, BooleanField, HiddenInput +from django.forms import ModelForm, ValidationError, TextInput, Form, URLField, MultipleHiddenInput, Textarea, CharField, ChoiceField, RadioSelect, MultipleChoiceField, BooleanField, HiddenInput, ModelChoiceField from datetime import date from django_better_admin_arrayfield.forms.widgets import DynamicArrayWidget -from .models import Event, BatchImportation, RecurrentImport, CategorisationRule, ModerationAnswer, ModerationQuestion +from .models import Event, BatchImportation, RecurrentImport, CategorisationRule, ModerationAnswer, ModerationQuestion, Place from django.utils.translation import gettext_lazy as _ from string import ascii_uppercase as auc from .templatetags.utils_extra import int_to_abc @@ -283,8 +283,6 @@ class CategorisationForm(Form): if '_' not in f: if f + '_cat' in args[0]: events.append((Event.objects.get(pk=int(f)), args[0][f + '_cat'])) - - # TODO super().__init__(*args, **kwargs) for e, c in events: @@ -294,3 +292,48 @@ class CategorisationForm(Form): def get_validated(self): return [(e, self.cleaned_data.get(e + '_cat')) for e in self.fields if '_' not in e and self.cleaned_data.get(e)] + +class EventAddPlaceForm(Form): + + place = ModelChoiceField(label=_("Place"), queryset=Place.objects.all(), empty_label=_("Create a missing place"), required=False) + add_alias = BooleanField(initial=True, required=False) + + def __init__(self, *args, **kwargs): + self.instance = kwargs.pop('instance', False) + super().__init__(*args, **kwargs) + if self.instance.location: + self.fields["add_alias"].label = _("Add \"{}\" to the aliases of the place").format(self.instance.location) + else: + self.fields.pop("add_alias") + + def modified_event(self): + return self.cleaned_data.get('place') + + def save(self): + if self.cleaned_data.get("place"): + place = self.cleaned_data.get("place") + self.instance.exact_location = place + self.instance.save() + if self.cleaned_data.get('add_alias'): + place.aliases.append(self.instance.location) + place.save() + + return self.instance + +class PlaceForm(ModelForm): + apply_to_all = BooleanField(initial=True, label=_('On saving, use aliases to detect all matching events with missing place'), required=False) + + class Meta: + model = Place + fields = '__all__' + widgets = { + 'location': TextInput() + } + + def as_grid(self): + return mark_safe('
' + super().as_p() + '
' + + '
') + + + def apply(self): + return self.cleaned_data.get("apply_to_all") diff --git a/src/agenda_culturel/locale/fr/LC_MESSAGES/django.po b/src/agenda_culturel/locale/fr/LC_MESSAGES/django.po index 785f14b..f2eaabf 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-04-26 14:59+0000\n" +"POT-Creation-Date: 2024-04-27 16:25+0000\n" "PO-Revision-Date: 2023-10-29 14:16+0000\n" "Last-Translator: Jean-Marie Favreau \n" "Language-Team: Jean-Marie Favreau \n" @@ -29,19 +29,35 @@ msgstr "L'heure de fin ne peut pas être avant l'heure de début." msgid "JSON in the format expected for the import." msgstr "JSON dans le format attendu pour l'import" -#: agenda_culturel/forms.py:291 -#, fuzzy -#| msgid "Category of the event" +#: agenda_culturel/forms.py:289 msgid "Apply category {} to the event {}" -msgstr "Catégorie de l'événement" +msgstr "Appliquer la catégorie {} à l'événement {}" + +#: agenda_culturel/forms.py:298 agenda_culturel/models.py:170 +msgid "Place" +msgstr "Lieu" + +#: agenda_culturel/forms.py:298 +msgid "Create a missing place" +msgstr "Créer un lieu manquant" + +#: agenda_culturel/forms.py:305 +msgid "Add \"{}\" to the aliases of the place" +msgstr "Ajouter « {} » aux alias du lieu" + +#: agenda_culturel/forms.py:324 +msgid "On saving, use aliases to detect all matching events with missing place" +msgstr "" +"Lors de l'enregistrement, utiliser des alias pour détecter tous les " +"événements correspondants dont la place est manquante." #: agenda_culturel/import_tasks/generic_extractors.py:151 msgid "Cannot extract event from url {}" msgstr "Impossible d'extraire l'événement depuis l'url {}" #: agenda_culturel/models.py:39 agenda_culturel/models.py:68 -#: agenda_culturel/models.py:162 agenda_culturel/models.py:753 -#: agenda_culturel/models.py:792 +#: agenda_culturel/models.py:162 agenda_culturel/models.py:770 +#: agenda_culturel/models.py:809 msgid "Name" msgstr "Nom" @@ -89,8 +105,8 @@ msgstr "Couleur" msgid "Color used as background for the category" msgstr "Couleur utilisée comme fond de la catégorie" -#: agenda_culturel/models.py:115 agenda_culturel/models.py:195 -#: agenda_culturel/models.py:804 agenda_culturel/models.py:855 +#: agenda_culturel/models.py:115 agenda_culturel/models.py:205 +#: agenda_culturel/models.py:821 agenda_culturel/models.py:872 msgid "Category" msgstr "Catégorie" @@ -107,14 +123,22 @@ msgid "Address" msgstr "Adresse" #: agenda_culturel/models.py:163 -msgid "Address of this place" -msgstr "Adresse de ce lieu" +msgid "Address of this place (without city name)" +msgstr "Adresse de ce lieu (sans le nom de la ville)" -#: agenda_culturel/models.py:166 +#: agenda_culturel/models.py:164 +msgid "City" +msgstr "Ville" + +#: agenda_culturel/models.py:164 +msgid "City name" +msgstr "Nom de la ville" + +#: agenda_culturel/models.py:167 msgid "Alternative names" msgstr "Noms alternatifs" -#: agenda_culturel/models.py:166 +#: agenda_culturel/models.py:167 msgid "" "Alternative names or addresses used to match a place with the free-form " "location of an event." @@ -122,81 +146,77 @@ msgstr "" "Noms et adresses alternatives qui seront utilisées pour associer une adresse " "avec la localisation en forme libre d'un événement" -#: agenda_culturel/models.py:169 -msgid "Place" -msgstr "Lieu" - -#: agenda_culturel/models.py:170 +#: agenda_culturel/models.py:171 msgid "Places" msgstr "Lieux" -#: agenda_culturel/models.py:179 agenda_culturel/models.py:802 +#: agenda_culturel/models.py:189 agenda_culturel/models.py:819 msgid "Published" msgstr "Publié" -#: agenda_culturel/models.py:180 +#: agenda_culturel/models.py:190 msgid "Draft" msgstr "Brouillon" -#: agenda_culturel/models.py:181 +#: agenda_culturel/models.py:191 msgid "Trash" msgstr "Corbeille" -#: agenda_culturel/models.py:191 +#: agenda_culturel/models.py:201 msgid "Title" msgstr "Titre" -#: agenda_culturel/models.py:191 +#: agenda_culturel/models.py:201 msgid "Short title" msgstr "Titre court" -#: agenda_culturel/models.py:193 agenda_culturel/models.py:839 +#: agenda_culturel/models.py:203 agenda_culturel/models.py:856 msgid "Status" msgstr "Status" -#: agenda_culturel/models.py:195 +#: agenda_culturel/models.py:205 msgid "Category of the event" msgstr "Catégorie de l'événement" -#: agenda_culturel/models.py:197 +#: agenda_culturel/models.py:207 msgid "Day of the event" msgstr "Date de l'événement" -#: agenda_culturel/models.py:198 +#: agenda_culturel/models.py:208 msgid "Starting time" msgstr "Heure de début" -#: agenda_culturel/models.py:200 +#: agenda_culturel/models.py:210 msgid "End day of the event" msgstr "Fin de l'événement" -#: agenda_culturel/models.py:200 +#: agenda_culturel/models.py:210 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:201 +#: agenda_culturel/models.py:211 msgid "Final time" msgstr "Heure de fin" -#: agenda_culturel/models.py:203 +#: agenda_culturel/models.py:213 msgid "Recurrence" msgstr "Récurrence" -#: agenda_culturel/models.py:205 agenda_culturel/models.py:803 +#: agenda_culturel/models.py:215 agenda_culturel/models.py:820 msgid "Location" msgstr "Localisation" -#: agenda_culturel/models.py:205 +#: agenda_culturel/models.py:215 msgid "Address of the event" msgstr "Adresse de l'événement" -#: agenda_culturel/models.py:206 +#: agenda_culturel/models.py:216 msgid "Location (free form)" msgstr "Localisation (forme libre)" -#: agenda_culturel/models.py:206 +#: agenda_culturel/models.py:216 msgid "" "Address of the event in case its not available in the already known places " "(free form)" @@ -204,189 +224,187 @@ msgstr "" "Addresse d'un événement si elle n'est pas déjà présente dans la liste des " "lieux disponible (forme libre)" -#: agenda_culturel/models.py:208 +#: agenda_culturel/models.py:218 msgid "Description" msgstr "Description" -#: agenda_culturel/models.py:208 +#: agenda_culturel/models.py:218 msgid "General description of the event" msgstr "Description générale de l'événement" -#: agenda_culturel/models.py:210 +#: agenda_culturel/models.py:220 msgid "Illustration (local image)" msgstr "Illustration (image locale)" -#: agenda_culturel/models.py:210 +#: agenda_culturel/models.py:220 msgid "Illustration image stored in the agenda server" msgstr "Image d'illustration stockée sur le serveur de l'agenda" -#: agenda_culturel/models.py:212 +#: agenda_culturel/models.py:222 msgid "Illustration" msgstr "Illustration" -#: agenda_culturel/models.py:212 +#: agenda_culturel/models.py:222 msgid "URL of the illustration image" msgstr "URL de l'image illustrative" -#: agenda_culturel/models.py:213 +#: agenda_culturel/models.py:223 msgid "Illustration description" msgstr "Description de l'illustration" -#: agenda_culturel/models.py:213 +#: agenda_culturel/models.py:223 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:215 +#: agenda_culturel/models.py:225 msgid "Importation source" msgstr "Source d'importation" -#: agenda_culturel/models.py:215 +#: agenda_culturel/models.py:225 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:216 +#: agenda_culturel/models.py:226 msgid "UUIDs" msgstr "UUIDs" -#: agenda_culturel/models.py:216 +#: agenda_culturel/models.py:226 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:217 +#: agenda_culturel/models.py:227 msgid "URLs" msgstr "URLs" -#: agenda_culturel/models.py:217 +#: agenda_culturel/models.py:227 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:219 +#: agenda_culturel/models.py:229 msgid "Tags" msgstr "Étiquettes" -#: agenda_culturel/models.py:219 +#: agenda_culturel/models.py:229 msgid "A list of tags that describe the event." msgstr "Une liste d'étiquettes décrivant l'événement" -#: agenda_culturel/models.py:221 +#: agenda_culturel/models.py:231 msgid "Possibly duplicated" msgstr "Possibles doublons" -#: agenda_culturel/models.py:262 +#: agenda_culturel/models.py:272 msgid "Event" msgstr "Événement" -#: agenda_culturel/models.py:263 +#: agenda_culturel/models.py:273 msgid "Events" msgstr "Événements" -#: agenda_culturel/models.py:749 +#: agenda_culturel/models.py:766 msgid "Contact message" msgstr "Message de contact" -#: agenda_culturel/models.py:750 -#, fuzzy -#| msgid "Your message" +#: agenda_culturel/models.py:767 msgid "Contact messages" msgstr "Messages de contact" -#: agenda_culturel/models.py:752 +#: agenda_culturel/models.py:769 msgid "Subject" msgstr "Sujet" -#: agenda_culturel/models.py:752 +#: agenda_culturel/models.py:769 msgid "The subject of your message" msgstr "Sujet de votre message" -#: agenda_culturel/models.py:753 +#: agenda_culturel/models.py:770 msgid "Your name" msgstr "Votre nom" -#: agenda_culturel/models.py:754 +#: agenda_culturel/models.py:771 msgid "Email address" msgstr "Adresse email" -#: agenda_culturel/models.py:754 +#: agenda_culturel/models.py:771 msgid "Your email address" msgstr "Votre adresse email" -#: agenda_culturel/models.py:755 +#: agenda_culturel/models.py:772 msgid "Message" msgstr "Message" -#: agenda_culturel/models.py:755 +#: agenda_culturel/models.py:772 msgid "Your message" msgstr "Votre message" -#: agenda_culturel/models.py:759 agenda_culturel/views.py:462 +#: agenda_culturel/models.py:776 agenda_culturel/views.py:470 msgid "Closed" msgstr "Fermé" -#: agenda_culturel/models.py:759 +#: agenda_culturel/models.py:776 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:760 +#: agenda_culturel/models.py:777 msgid "Comments" msgstr "Commentaires" -#: agenda_culturel/models.py:760 +#: agenda_culturel/models.py:777 msgid "Comments on the message from the moderation team" msgstr "Commentaires sur ce message par l'équipe de modération" -#: agenda_culturel/models.py:770 agenda_culturel/models.py:837 +#: agenda_culturel/models.py:787 agenda_culturel/models.py:854 msgid "Recurrent import" msgstr "Import récurrent" -#: agenda_culturel/models.py:771 +#: agenda_culturel/models.py:788 msgid "Recurrent imports" msgstr "Imports récurrents" -#: agenda_culturel/models.py:775 +#: agenda_culturel/models.py:792 msgid "ical" msgstr "ical" -#: agenda_culturel/models.py:776 +#: agenda_culturel/models.py:793 msgid "ical no busy" msgstr "ical sans busy" -#: agenda_culturel/models.py:777 +#: agenda_culturel/models.py:794 msgid "ical no VC" msgstr "ical sans VC" -#: agenda_culturel/models.py:778 +#: agenda_culturel/models.py:795 msgid "lacoope.org" msgstr "lacoope.org" -#: agenda_culturel/models.py:779 +#: agenda_culturel/models.py:796 msgid "la comédie" msgstr "la comédie" -#: agenda_culturel/models.py:780 +#: agenda_culturel/models.py:797 msgid "le fotomat" msgstr "le fotomat" -#: agenda_culturel/models.py:781 +#: agenda_culturel/models.py:798 msgid "la puce à loreille" msgstr "la puce à loreille" -#: agenda_culturel/models.py:784 +#: agenda_culturel/models.py:801 msgid "simple" msgstr "simple" -#: agenda_culturel/models.py:785 +#: agenda_culturel/models.py:802 msgid "Headless Chromium" msgstr "chromium sans interface" -#: agenda_culturel/models.py:789 +#: agenda_culturel/models.py:806 msgid "daily" msgstr "chaque jour" -#: agenda_culturel/models.py:790 +#: agenda_culturel/models.py:807 msgid "weekly" msgstr "chaque semaine" -#: agenda_culturel/models.py:792 +#: agenda_culturel/models.py:809 msgid "" "Recurrent import name. Be careful to choose a name that is easy to " "understand, as it will be public and displayed on the sites About page." @@ -394,127 +412,127 @@ msgstr "" "Nom de l'import récurrent. Attention à choisir un nom compréhensible, car il " "sera public, et affiché sur la page à propos du site." -#: agenda_culturel/models.py:793 +#: agenda_culturel/models.py:810 msgid "Processor" msgstr "Processeur" -#: agenda_culturel/models.py:794 +#: agenda_culturel/models.py:811 msgid "Downloader" msgstr "Téléchargeur" -#: agenda_culturel/models.py:796 +#: agenda_culturel/models.py:813 msgid "Import recurrence" msgstr "Récurrence d'import" -#: agenda_culturel/models.py:799 +#: agenda_culturel/models.py:816 msgid "Source" msgstr "Source" -#: agenda_culturel/models.py:799 +#: agenda_culturel/models.py:816 msgid "URL of the source document" msgstr "URL du document source" -#: agenda_culturel/models.py:800 +#: agenda_culturel/models.py:817 msgid "Browsable url" msgstr "URL navigable" -#: agenda_culturel/models.py:800 +#: agenda_culturel/models.py:817 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:802 +#: agenda_culturel/models.py:819 msgid "Status of each imported event (published or draft)" msgstr "Status de chaque événement importé (publié ou brouillon)" -#: agenda_culturel/models.py:803 +#: agenda_culturel/models.py:820 msgid "Address for each imported event" msgstr "Adresse de chaque événement importé" -#: agenda_culturel/models.py:804 +#: agenda_culturel/models.py:821 msgid "Category of each imported event" msgstr "Catégorie de chaque événement importé" -#: agenda_culturel/models.py:805 +#: agenda_culturel/models.py:822 msgid "Tags for each imported event" msgstr "Étiquettes de chaque événement importé" -#: agenda_culturel/models.py:805 +#: agenda_culturel/models.py:822 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:824 +#: agenda_culturel/models.py:841 msgid "Running" msgstr "En cours" -#: agenda_culturel/models.py:825 +#: agenda_culturel/models.py:842 msgid "Canceled" msgstr "Annulé" -#: agenda_culturel/models.py:826 +#: agenda_culturel/models.py:843 msgid "Success" msgstr "Succès" -#: agenda_culturel/models.py:827 +#: agenda_culturel/models.py:844 msgid "Failed" msgstr "Erreur" -#: agenda_culturel/models.py:830 +#: agenda_culturel/models.py:847 msgid "Batch importation" msgstr "Importation par lot" -#: agenda_culturel/models.py:831 +#: agenda_culturel/models.py:848 msgid "Batch importations" msgstr "Importations par lot" -#: agenda_culturel/models.py:837 +#: agenda_culturel/models.py:854 msgid "Reference to the recurrent import processing" msgstr "Référence du processus d'import récurrent" -#: agenda_culturel/models.py:841 +#: agenda_culturel/models.py:858 msgid "Error message" msgstr "Votre message" -#: agenda_culturel/models.py:843 +#: agenda_culturel/models.py:860 msgid "Number of collected events" msgstr "Nombre d'événements collectés" -#: agenda_culturel/models.py:844 +#: agenda_culturel/models.py:861 msgid "Number of imported events" msgstr "Nombre d'événements importés" -#: agenda_culturel/models.py:845 +#: agenda_culturel/models.py:862 msgid "Number of updated events" msgstr "Nombre d'événements mis à jour" -#: agenda_culturel/models.py:846 +#: agenda_culturel/models.py:863 msgid "Number of removed events" msgstr "Nombre d'événements supprimés" -#: agenda_culturel/models.py:853 +#: agenda_culturel/models.py:870 msgid "Weight" msgstr "Poids" -#: agenda_culturel/models.py:853 +#: agenda_culturel/models.py:870 msgid "The lower is the weight, the earlier the filter is applied" msgstr "Plus le poids est léger, plus le filtre sera appliqué tôt" -#: agenda_culturel/models.py:855 +#: agenda_culturel/models.py:872 msgid "Category applied to the event" msgstr "Catégorie appliquée à l'événement" -#: agenda_culturel/models.py:857 +#: agenda_culturel/models.py:874 msgid "Contained in the title" msgstr "Contenu dans le titre" -#: agenda_culturel/models.py:857 +#: agenda_culturel/models.py:874 msgid "Text contained in the event title" msgstr "Texte contenu dans le titre de l'événement" -#: agenda_culturel/models.py:858 +#: agenda_culturel/models.py:875 msgid "Exact title extract" msgstr "Extrait exact du titre" -#: agenda_culturel/models.py:858 +#: agenda_culturel/models.py:875 msgid "" "If checked, the extract will be searched for in the title using the exact " "form (capitals, accents)." @@ -522,19 +540,19 @@ msgstr "" "Si coché, l'extrait sera recherché dans le titre en utilisant la forme " "exacte (majuscules, accents)" -#: agenda_culturel/models.py:860 +#: agenda_culturel/models.py:877 msgid "Contained in the description" msgstr "Contenu dans la description" -#: agenda_culturel/models.py:860 +#: agenda_culturel/models.py:877 msgid "Text contained in the description" msgstr "Texte contenu dans la description" -#: agenda_culturel/models.py:861 +#: agenda_culturel/models.py:878 msgid "Exact description extract" msgstr "Extrait exact de description" -#: agenda_culturel/models.py:861 +#: agenda_culturel/models.py:878 msgid "" "If checked, the extract will be searched for in the description using the " "exact form (capitals, accents)." @@ -542,19 +560,19 @@ msgstr "" "Si coché, l'extrait sera recherché dans la description en utilisant la forme " "exacte (majuscules, accents)" -#: agenda_culturel/models.py:863 +#: agenda_culturel/models.py:880 msgid "Contained in the location" msgstr "Contenu dans la localisation" -#: agenda_culturel/models.py:863 +#: agenda_culturel/models.py:880 msgid "Text contained in the event location" msgstr "Texte contenu dans la localisation de l'événement" -#: agenda_culturel/models.py:864 +#: agenda_culturel/models.py:881 msgid "Exact location extract" msgstr "Extrait exact de localisation" -#: agenda_culturel/models.py:864 +#: agenda_culturel/models.py:881 msgid "" "If checked, the extract will be searched for in the location using the exact " "form (capitals, accents)." @@ -562,52 +580,52 @@ msgstr "" "Si coché, l'extrait sera recherché dans la localisation en utilisant la " "forme exacte (majuscules, accents)" -#: agenda_culturel/models.py:867 +#: agenda_culturel/models.py:884 msgid "Categorisation rule" msgstr "Règle de catégorisation" -#: agenda_culturel/models.py:868 +#: agenda_culturel/models.py:885 msgid "Categorisation rules" msgstr "Règles de catégorisation" -#: agenda_culturel/models.py:924 agenda_culturel/models.py:945 +#: agenda_culturel/models.py:941 agenda_culturel/models.py:962 msgid "Question" msgstr "Question" -#: agenda_culturel/models.py:924 agenda_culturel/models.py:947 +#: agenda_culturel/models.py:941 agenda_culturel/models.py:964 msgid "Text that will be shown to moderators" msgstr "Text tel que présenté aux modérateurices" -#: agenda_culturel/models.py:927 +#: agenda_culturel/models.py:944 msgid "Moderation question" msgstr "Question de modération" -#: agenda_culturel/models.py:928 +#: agenda_culturel/models.py:945 msgid "Moderation questions" msgstr "Questions de modération" -#: agenda_culturel/models.py:945 +#: agenda_culturel/models.py:962 msgid "Associated question from moderation" msgstr "Question associée pour la modération" -#: agenda_culturel/models.py:947 +#: agenda_culturel/models.py:964 msgid "Answer" msgstr "Réponse" -#: agenda_culturel/models.py:949 +#: agenda_culturel/models.py:966 msgid "Adds tags" msgstr "Ajoute les étiquettes" -#: agenda_culturel/models.py:949 +#: agenda_culturel/models.py:966 msgid "A list of tags that will be added if you choose this answer." msgstr "" "Une liste d'étiquettes qui seront ajoutées si vous choisissez cette réponse." -#: agenda_culturel/models.py:950 +#: agenda_culturel/models.py:967 msgid "Removes tags" msgstr "Retire les étiquettes" -#: agenda_culturel/models.py:950 +#: agenda_culturel/models.py:967 msgid "A list of tags that will be removed if you choose this answer." msgstr "" "Une liste d'étiquettes qui seront retirées si vous choisissez cette réponse." @@ -620,27 +638,27 @@ msgstr "anglais" msgid "French" msgstr "français" -#: agenda_culturel/views.py:261 +#: agenda_culturel/views.py:269 msgid "The static content has been successfully updated." msgstr "Le contenu statique a été modifié avec succès." -#: agenda_culturel/views.py:268 agenda_culturel/views.py:303 +#: agenda_culturel/views.py:276 agenda_culturel/views.py:311 msgid "The event has been successfully modified." msgstr "L'événement a été modifié avec succès." -#: agenda_culturel/views.py:280 +#: agenda_culturel/views.py:288 msgid "The event has been successfully deleted." msgstr "L'événement a été supprimé avec succès" -#: agenda_culturel/views.py:329 +#: agenda_culturel/views.py:337 msgid "The status has been successfully modified." msgstr "Le status a été modifié avec succès." -#: agenda_culturel/views.py:351 +#: agenda_culturel/views.py:359 msgid "The event is saved." msgstr "L'événement est enregistré." -#: agenda_culturel/views.py:354 +#: agenda_culturel/views.py:362 msgid "" "The event has been submitted and will be published as soon as it has been " "validated by the moderation team." @@ -648,7 +666,7 @@ msgstr "" "L'événement a été soumis et sera publié dès qu'il aura été validé par " "l'équipe de modération." -#: agenda_culturel/views.py:391 +#: agenda_culturel/views.py:399 msgid "" "The event has been successfully extracted, and you can now submit it after " "modifying it if necessary." @@ -656,7 +674,7 @@ msgstr "" "L'événement a été extrait avec succès, vous pouvez maintenant le soumettre " "après l'avoir modifié au besoin." -#: agenda_culturel/views.py:395 +#: agenda_culturel/views.py:403 msgid "" "Unable to extract an event from the proposed URL. Please use the form below " "to submit the event." @@ -664,16 +682,16 @@ msgstr "" "Impossible d'extraire un événement depuis l'URL proposée. Veuillez utiliser " "le formulaire ci-dessous pour soumettre l'événement." -#: agenda_culturel/views.py:404 +#: agenda_culturel/views.py:412 msgid "This URL has already been submitted, and you can find the event below." msgstr "" "Cette URL a déjà été soumise, et vous trouverez l'événement ci-dessous." -#: agenda_culturel/views.py:408 +#: agenda_culturel/views.py:416 msgid "This URL has already been submitted and is awaiting moderation." msgstr "Cette URL a déjà été soumise, et est en attente de modération" -#: agenda_culturel/views.py:410 +#: agenda_culturel/views.py:418 msgid "" "This URL has already been submitted, but has not been selected for " "publication by the moderation team." @@ -681,51 +699,51 @@ msgstr "" "Cette URL a déjà été soumise, mais n'a pas été retenue par l'équipe de " "modération pour la publication." -#: agenda_culturel/views.py:432 +#: agenda_culturel/views.py:440 msgid "Your message has been sent successfully." msgstr "Votre message a été envoyé avec succès." -#: agenda_culturel/views.py:447 +#: agenda_culturel/views.py:455 msgid "The contact message properties has been successfully modified." msgstr "Les propriétés du message de contact ont été modifié avec succès." -#: agenda_culturel/views.py:462 +#: agenda_culturel/views.py:470 msgid "Open" msgstr "Ouvert" -#: agenda_culturel/views.py:504 +#: agenda_culturel/views.py:512 msgid "Search" msgstr "Rechercher" -#: agenda_culturel/views.py:639 +#: agenda_culturel/views.py:647 msgid "The import has been run successfully." msgstr "L'import a été lancé avec succès" -#: agenda_culturel/views.py:656 +#: agenda_culturel/views.py:664 msgid "The import has been canceled." msgstr "L'import a été annulé" -#: agenda_culturel/views.py:693 +#: agenda_culturel/views.py:701 msgid "The recurrent import has been successfully modified." msgstr "L'import récurrent a été modifié avec succès." -#: agenda_culturel/views.py:700 +#: agenda_culturel/views.py:708 msgid "The recurrent import has been successfully deleted." msgstr "L'import récurrent a été supprimé avec succès" -#: agenda_culturel/views.py:731 +#: agenda_culturel/views.py:739 msgid "The import has been launched." msgstr "L'import a été lancé" -#: agenda_culturel/views.py:791 +#: agenda_culturel/views.py:799 msgid "The merge has been successfully completed." msgstr "La fusion a été réalisée avec succès." -#: agenda_culturel/views.py:821 +#: agenda_culturel/views.py:829 msgid "Events have been marked as unduplicated." msgstr "Les événements ont été marqués comme non dupliqués." -#: agenda_culturel/views.py:838 +#: agenda_culturel/views.py:846 msgid "" "The selected event has been retained, while the other has been moved to the " "recycle bin." @@ -733,7 +751,7 @@ msgstr "" "L'événement sélectionné a été conservé, l'autre a été déplacé dans la " "corbeille." -#: agenda_culturel/views.py:840 +#: agenda_culturel/views.py:848 msgid "" "The selected event has been retained, while the others have been moved to " "the recycle bin." @@ -741,15 +759,15 @@ msgstr "" "L'événement sélectionné a été conservé, les autres ont été déplacés dans la " "corbeille." -#: agenda_culturel/views.py:846 +#: agenda_culturel/views.py:854 msgid "The event has been withdrawn from the group and made independent." msgstr "L'événement a été retiré du groupe et rendu indépendant." -#: agenda_culturel/views.py:893 +#: agenda_culturel/views.py:901 msgid "The event was successfully duplicated." msgstr "L'événement a été marqué dupliqué avec succès." -#: agenda_culturel/views.py:896 +#: agenda_culturel/views.py:904 msgid "" "The event has been successfully flagged as a duplicate. The moderation team " "will deal with your suggestion shortly." @@ -757,35 +775,63 @@ 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:935 +#: agenda_culturel/views.py:943 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:942 +#: agenda_culturel/views.py:950 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:962 agenda_culturel/views.py:991 +#: agenda_culturel/views.py:970 agenda_culturel/views.py:999 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:964 agenda_culturel/views.py:993 +#: agenda_culturel/views.py:972 agenda_culturel/views.py:1001 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:966 agenda_culturel/views.py:995 +#: agenda_culturel/views.py:974 agenda_culturel/views.py:1003 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é" -#: agenda_culturel/views.py:1023 +#: agenda_culturel/views.py:1031 msgid "The moderation question has been created with success." msgstr "La question de modération a été créée avec succès." +#: agenda_culturel/views.py:1123 agenda_culturel/views.py:1178 +msgid "{} events have been updated." +msgstr "{} événements ont été mis à jour." + +#: agenda_culturel/views.py:1125 agenda_culturel/views.py:1180 +msgid "1 event has been updated." +msgstr "1 événement a été mis à jour" + +#: agenda_culturel/views.py:1127 agenda_culturel/views.py:1182 +msgid "No events have been modified." +msgstr "Aucun événement n'a été modifié." + +#: agenda_culturel/views.py:1135 +msgid "The place has been successfully updated." +msgstr "Le lieu a été modifié avec succès." + +#: agenda_culturel/views.py:1142 +msgid "The place has been successfully created." +msgstr "Le lieu a été créé avec succès." + +#: agenda_culturel/views.py:1201 +msgid "The selected place has been assigned to the event. " +msgstr "Le lieu sélectionné a été assigné à l'événement. " + +#: agenda_culturel/views.py:1203 +msgid "A new alias has been added to the selected place. " +msgstr "Un nouvel alias a été créé pour le lieu sélectionné. " + msgid "Recurrent import name" msgstr "Nome de l'import récurrent" diff --git a/src/agenda_culturel/migrations/0057_alter_place_aliases.py b/src/agenda_culturel/migrations/0057_alter_place_aliases.py new file mode 100644 index 0000000..8bc7a40 --- /dev/null +++ b/src/agenda_culturel/migrations/0057_alter_place_aliases.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.7 on 2024-04-27 07:44 + +from django.db import migrations, models +import django_better_admin_arrayfield.models.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('agenda_culturel', '0056_place_alter_event_location_event_exact_location'), + ] + + operations = [ + migrations.AlterField( + model_name='place', + name='aliases', + field=django_better_admin_arrayfield.models.fields.ArrayField(base_field=models.CharField(max_length=512), blank=True, help_text='Alternative names or addresses used to match a place with the free-form location of an event.', null=True, size=None, verbose_name='Alternative names'), + ), + ] diff --git a/src/agenda_culturel/migrations/0058_place_city_alter_place_address.py b/src/agenda_culturel/migrations/0058_place_city_alter_place_address.py new file mode 100644 index 0000000..6b3373b --- /dev/null +++ b/src/agenda_culturel/migrations/0058_place_city_alter_place_address.py @@ -0,0 +1,24 @@ +# Generated by Django 4.2.7 on 2024-04-27 15:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('agenda_culturel', '0057_alter_place_aliases'), + ] + + operations = [ + migrations.AddField( + model_name='place', + name='city', + field=models.CharField(default='', help_text='City name', verbose_name='City'), + preserve_default=False, + ), + migrations.AlterField( + model_name='place', + name='address', + field=models.CharField(help_text='Address of this place (without city name)', verbose_name='Address'), + ), + ] diff --git a/src/agenda_culturel/migrations/0059_auto_20240427_1829.py b/src/agenda_culturel/migrations/0059_auto_20240427_1829.py new file mode 100644 index 0000000..771bbf1 --- /dev/null +++ b/src/agenda_culturel/migrations/0059_auto_20240427_1829.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.7 on 2024-04-27 16:29 + +from django.db import migrations +from django.contrib.auth.management import create_permissions +from django.contrib.auth.models import Group, Permission + +def update_groups_permissions(apps, schema_editor): + + all_perms = Permission.objects.all() + + # set permissions for moderators + moderator_perms = [i for i in all_perms if i.content_type.app_label == 'agenda_culturel' and i.content_type.model in ['place']] + Group.objects.get(name="Moderator").permissions.add(*moderator_perms) + +class Migration(migrations.Migration): + + dependencies = [ + ('agenda_culturel', '0058_place_city_alter_place_address'), + ] + + operations = [ + migrations.RunPython(update_groups_permissions), + ] diff --git a/src/agenda_culturel/models.py b/src/agenda_culturel/models.py index 21debb5..b2790e6 100644 --- a/src/agenda_culturel/models.py +++ b/src/agenda_culturel/models.py @@ -160,8 +160,9 @@ class DuplicatedEvents(models.Model): class Place(models.Model): name = models.CharField(verbose_name=_('Name'), help_text=_('Name of the place')) - address = models.CharField(verbose_name=_('Address'), help_text=_('Address of this place')) - location = PlainLocationField(based_fields=['name', 'address'], zoom=12) + address = models.CharField(verbose_name=_('Address'), help_text=_('Address of this place (without city name)')) + city = models.CharField(verbose_name=_('City'), help_text=_('City name')) + location = PlainLocationField(based_fields=['name', 'address', 'city'], zoom=12) aliases = ArrayField(models.CharField(max_length=512), verbose_name=_('Alternative names'), help_text=_("Alternative names or addresses used to match a place with the free-form location of an event."), blank=True, null=True) @@ -175,6 +176,11 @@ class Place(models.Model): def get_absolute_url(self): return reverse("view_place", kwargs={"pk": self.pk}) + def nb_events(self): + return Event.objects.filter(exact_location=self).count() + + def match(self, event): + return event.location in self.aliases class Event(models.Model): @@ -396,9 +402,16 @@ 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(): + # try to detect category CategorisationRule.apply_rules(self) + # try to detect location + if not self.exact_location: + for p in Place.objects.all(): + if p.match(self): + logger.warning("Found a place for an imported event: " + p.name) + self.exact_location = p + break def save(self, *args, **kwargs): diff --git a/src/agenda_culturel/static/style.scss b/src/agenda_culturel/static/style.scss index 620f553..abbb21a 100644 --- a/src/agenda_culturel/static/style.scss +++ b/src/agenda_culturel/static/style.scss @@ -308,6 +308,10 @@ header .title { } } +article form label { + display: inline-block; +} + article#filters { .buttons-filter { float: right; @@ -838,4 +842,12 @@ table .buttons { border-top-right-radius: 0; } } +} + +.missing-data { + color: red; +} + +#filters #id_city { + columns: 4; } \ No newline at end of file diff --git a/src/agenda_culturel/templates/agenda_culturel/event-location-inc.html b/src/agenda_culturel/templates/agenda_culturel/event-location-inc.html new file mode 100644 index 0000000..7f51fab --- /dev/null +++ b/src/agenda_culturel/templates/agenda_culturel/event-location-inc.html @@ -0,0 +1,14 @@ + +{% if event.exact_location %} + {% if nolink %} + {{ event.exact_location.name }}, {{ event.exact_location.city }} + {% else %} + {{ event.exact_location.name }}, {{ event.exact_location.city }} + {% endif %} +{% else %} + {% if perms.agenda_culturel.change_event and perms.agenda_culturel.change_place %} + {{ event.location }} + {% else %} + {{ event.location }} + {% endif %} +{% endif %} \ No newline at end of file diff --git a/src/agenda_culturel/templates/agenda_culturel/filter-inc.html b/src/agenda_culturel/templates/agenda_culturel/filter-inc.html index 4bcfb65..dac6805 100644 --- a/src/agenda_culturel/templates/agenda_culturel/filter-inc.html +++ b/src/agenda_culturel/templates/agenda_culturel/filter-inc.html @@ -18,6 +18,9 @@ {% for s in filter.get_status_names %} {{ s }} {% endfor %} + {% for c in filter.get_cities %} + {{ c }} + {% endfor %} {{ filter.get_recurrence_filtering }} {% else %} Filtrer diff --git a/src/agenda_culturel/templates/agenda_culturel/page.html b/src/agenda_culturel/templates/agenda_culturel/page.html index 4a4876f..aefbaed 100644 --- a/src/agenda_culturel/templates/agenda_culturel/page.html +++ b/src/agenda_culturel/templates/agenda_culturel/page.html @@ -52,6 +52,9 @@ {% if perms.agenda_culturel.change_duplicatedevents %} {% show_badge_duplicated "bottom" %} {% endif %} + {% if perms.agenda_culturel.change_place and perms.agenda_culturel.change_event %} + {% show_badge_unknown_places "bottom" %} + {% endif %} {% if perms.agenda_culturel.view_contactmessage %} {% show_badge_contactmessages "bottom" %} {% endif %} diff --git a/src/agenda_culturel/templates/agenda_culturel/place_confirm_delete.html b/src/agenda_culturel/templates/agenda_culturel/place_confirm_delete.html index d066473..f2b6f51 100644 --- a/src/agenda_culturel/templates/agenda_culturel/place_confirm_delete.html +++ b/src/agenda_culturel/templates/agenda_culturel/place_confirm_delete.html @@ -10,8 +10,16 @@

Suppression du lieu {{ object.name }}

+ {% with nb_events=object.nb_events %} + {% if nb_events > 0 %} +

Attention : {{ nb_events }} événement{{ nb_events|pluralize }} + {% if nb_events > 1 %}sont enregistrés{% else %}est enregistré{% endif %} dans ce lieu.

+ {% endif %} + {% endwith %} +

Êtes-vous sûr·e de vouloir supprimer le lieu « {{ object.name }} ({{ object.pk }}) » situé à l'adresse {{ object.address }}, et de coordonnées {{ object.location }} ?

+
{% csrf_token %} -

Êtes-vous sûr·e de vouloir supprimer le lieu « {{ object.name }} ({{ object.pk }}) » situé à l'adresse {{ object.address }}, et de coordonnées {{ object.location }} ?

+ {{ form }}
Annuler diff --git a/src/agenda_culturel/templates/agenda_culturel/place_detail.html b/src/agenda_culturel/templates/agenda_culturel/place_detail.html index 671dddb..3d6f88d 100644 --- a/src/agenda_culturel/templates/agenda_culturel/place_detail.html +++ b/src/agenda_culturel/templates/agenda_culturel/place_detail.html @@ -25,8 +25,8 @@ {% if perms.agenda_culturel.change_place %} < Tous les lieux {% endif %} -
{% if perms.agenda_culturel.change_place %} + @@ -36,8 +36,13 @@
    -
  • Adresse : {{ object.address }}
  • +
  • Adresse : {{ object.address }}, {{ object.city }}
  • Coordonnée GPS : {{ object.location }}
  • + {% if object.nb_events > 0 %} +
  • Nombre d'événements : {{ object.nb_events }}
  • + {% else %} +
  • Aucun événement n'est enregistré dans ce lieu
  • + {% endif %}
{% if user.is_authenticated %} {% if object.aliases|length > 0 %} diff --git a/src/agenda_culturel/templates/agenda_culturel/place_form.html b/src/agenda_culturel/templates/agenda_culturel/place_form.html index 345c71c..b20866e 100644 --- a/src/agenda_culturel/templates/agenda_culturel/place_form.html +++ b/src/agenda_culturel/templates/agenda_culturel/place_form.html @@ -18,13 +18,20 @@

{% if form.instance.pk %}Modification{% else %}Création{% endif %} d'un lieu

+ {% if event %} +

Création d'un lieu depuis l'événement « {{ event }} » (voir en bas de page le détail de l'événement).

+ {% endif %} {% csrf_token %} - {{ form.as_p }} + {{ form.as_grid }} + {% if event %} +

Description du lieu :

+ {% include "agenda_culturel/single-event/event-single-inc.html" with event=event noedit=1 %} + {% endif %}
{% endblock %} \ No newline at end of file diff --git a/src/agenda_culturel/templates/agenda_culturel/place_list.html b/src/agenda_culturel/templates/agenda_culturel/place_list.html index acea469..6c89972 100644 --- a/src/agenda_culturel/templates/agenda_culturel/place_list.html +++ b/src/agenda_culturel/templates/agenda_culturel/place_list.html @@ -12,7 +12,10 @@

Lieu #{{ place.pk }} : {{ place.name }}

    -
  • Adresse : {{ place.address }}
  • +
  • Adresse : {{ place.address }}, {{ place.city }}
  • Coordonnée GPS : {{ place.location }}
{% if place.aliases|length > 0 %} @@ -34,6 +37,11 @@ {% else %}

Ce lieu n'a pas d'alias défini

{% endif %} + {% if place.nb_events > 0 %} +

Nombre d'événements : {{ place.nb_events }}

+ {% else %} +

Aucun événement n'est enregistré dans ce lieu

+ {% endif %} diff --git a/src/agenda_culturel/templates/agenda_culturel/place_unknown_form.html b/src/agenda_culturel/templates/agenda_culturel/place_unknown_form.html new file mode 100644 index 0000000..c102c34 --- /dev/null +++ b/src/agenda_culturel/templates/agenda_culturel/place_unknown_form.html @@ -0,0 +1,49 @@ +{% extends "agenda_culturel/page.html" %} +{% load static %} + + +{% block title %}Ajouter un lieu à {{ object.title }}{% endblock %} + +{% block entete_header %} + + + + + + + + + + + + + + + +{% endblock %} + +{% block fluid %}{% endblock %} + +{% block content %} + +{% load static_content_extra %} + +
+
+

Ajouter un lieu à {{ object.title }} ({{ object.start_day }})

+
+ +

Si le lieu est disponible dans la liste, veuillez le sélectionner.

+
{% csrf_token %} + {{ form.media }} + {{ form.as_p }} +
+ Annuler + +
+
+

Description du lieu :

+ {% include "agenda_culturel/single-event/event-single-inc.html" with event=object noedit=1 %} +
+ +{% endblock %} \ No newline at end of file diff --git a/src/agenda_culturel/templates/agenda_culturel/place_unknown_list.html b/src/agenda_culturel/templates/agenda_culturel/place_unknown_list.html new file mode 100644 index 0000000..d1be020 --- /dev/null +++ b/src/agenda_culturel/templates/agenda_culturel/place_unknown_list.html @@ -0,0 +1,49 @@ +{% extends "agenda_culturel/page.html" %} +{% load utils_extra %} + + +{% block title %}Événements sans lieu{% endblock %} + +{% load cat_extra %} +{% block entete_header %} + {% css_categories %} +{% endblock %} + +{% block content %} +
+
+
+ +

Événements sans lieu

+
+ +
+{% for obj in object_list %} +
{% include "agenda_culturel/single-event/event-in-unknown-place-list-inc.html" with event=obj %}
+{% endfor %} +
+
+ + {% if page_obj.has_previous %} + « premier + précédent + {% endif %} + + + Page {{ page_obj.number }} sur {{ page_obj.paginator.num_pages }} + + + {% if page_obj.has_next %} + suivant + dernier » + {% endif %} + +
+
+ +{% include "agenda_culturel/side-nav.html" with current="unknown_places" %} +
+ +{% 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 d5ae150..f9a0260 100644 --- a/src/agenda_culturel/templates/agenda_culturel/side-nav.html +++ b/src/agenda_culturel/templates/agenda_culturel/side-nav.html @@ -24,6 +24,9 @@ {% if perms.agenda_culturel.change_place %}
  • Liste des lieux
  • {% endif %} + {% if perms.agenda_culturel.change_place and perms.agenda_culturel.change_event %} +
  • Événements sans lieu{% show_badge_unknown_places "left" %}
  • + {% endif %} {% if perms.agenda_culturel.view_batchimportation or perms.agenda_culturel.view_recurrentimport or perms.agenda_culturel.view_categorisationrule%} diff --git a/src/agenda_culturel/templates/agenda_culturel/single-event/event-in-flat-list-inc.html b/src/agenda_culturel/templates/agenda_culturel/single-event/event-in-flat-list-inc.html index f6a078a..4706714 100644 --- a/src/agenda_culturel/templates/agenda_culturel/single-event/event-in-flat-list-inc.html +++ b/src/agenda_culturel/templates/agenda_culturel/single-event/event-in-flat-list-inc.html @@ -12,7 +12,7 @@ {% picto_from_name "map-pin" %} - {% if event.location_hl %}{{ event.location_hl | safe }}{% else %}{{ event.location }}{% endif %}

    + {% if event.location_hl %}{{ event.location_hl | safe }}{% else %}{% include "agenda_culturel/event-location-inc.html" with event=event %}{% endif %}

    {% if event.has_recurrences %}
    {% endif %} + {% if event.location or event.exact_location %}
    {% endif %}

    {{ event|picto_status }} {{ event.title }}

    - {% if event.location %} + {% if event.location or event.exact_location %}

    {% picto_from_name "map-pin" %} - {{ event.location }} + {% include "agenda_culturel/event-location-inc.html" with event=event %}

    {% endif %} diff --git a/src/agenda_culturel/templates/agenda_culturel/single-event/event-in-list-inc.html b/src/agenda_culturel/templates/agenda_culturel/single-event/event-in-list-inc.html index ce8bb1f..89b4666 100644 --- a/src/agenda_culturel/templates/agenda_culturel/single-event/event-in-list-inc.html +++ b/src/agenda_culturel/templates/agenda_culturel/single-event/event-in-list-inc.html @@ -8,16 +8,16 @@ {% include "agenda_culturel/ephemeris-inc.html" with event=event filter=filter %} {{ event.category | small_cat_recurrent:event.has_recurrences }} - {% if event.location %}
    {% endif %} + {% if event.location or event.exact_location %}
    {% endif %}

    {{ event|picto_status }} {{ event.title }}

    - {% if event.location %} + {% if event.location or event.exact_location %}

    {% picto_from_name "map-pin" %} - {{ event.location }} + {% include "agenda_culturel/event-location-inc.html" with event=event %}

    {% endif %} diff --git a/src/agenda_culturel/templates/agenda_culturel/single-event/event-in-unknown-place-list-inc.html b/src/agenda_culturel/templates/agenda_culturel/single-event/event-in-unknown-place-list-inc.html new file mode 100644 index 0000000..852a1b4 --- /dev/null +++ b/src/agenda_culturel/templates/agenda_culturel/single-event/event-in-unknown-place-list-inc.html @@ -0,0 +1,46 @@ +{% load utils_extra %} +{% load cat_extra %} +{% load event_extra %} + +
    + + + + {{ event.category | small_cat_recurrent:event.has_recurrences }} + {{ event|picto_status }} + + {{ event.title }}

    + {% picto_from_name "map-pin" %} + {% include "agenda_culturel/event-location-inc.html" with event=event %} +
    +

    {% picto_from_name "calendar" %} + {% if event.end_day and event.end_day != event.start_day %}du{% else %}le{% endif %} + {% include "agenda_culturel/date-times-inc.html" with event=event %} + {% if event.has_recurrences %} +

    + {% picto_from_name "repeat" %} + + {% for r in event.recurrences.rrules %} + {{ r.to_text }}{% if not forloop.first %}, {% endif %}{% endfor %}, depuis le + {% if event.recurrences.dtstart.date %} + {{ event.recurrences.dtstart.date }} + {% else %} + {{ event.start_day }} + {% endif %} +

    + {% endif %} +

    + {% picto_from_name "tag" %} + {% for tag in event.tags %} + {{ tag }} + {% if not forloop.last %}, {% endif %} + {% endfor %} +

    +
    + {% if event.description %}{{ event.description |truncatewords:60 }}{% else %}pas de description{% endif %} + +
    diff --git a/src/agenda_culturel/templates/agenda_culturel/single-event/event-modal-inc.html b/src/agenda_culturel/templates/agenda_culturel/single-event/event-modal-inc.html index 57f1261..5eeac81 100644 --- a/src/agenda_culturel/templates/agenda_culturel/single-event/event-modal-inc.html +++ b/src/agenda_culturel/templates/agenda_culturel/single-event/event-modal-inc.html @@ -15,7 +15,7 @@

    {% picto_from_name "map-pin" %} - {{ event.location }} + {% include "agenda_culturel/event-location-inc.html" with event=event nolink=1 %}

    {% picto_from_name "calendar" %} diff --git a/src/agenda_culturel/templates/agenda_culturel/single-event/event-single-inc.html b/src/agenda_culturel/templates/agenda_culturel/single-event/event-single-inc.html index d8b8702..fe179b6 100644 --- a/src/agenda_culturel/templates/agenda_culturel/single-event/event-single-inc.html +++ b/src/agenda_culturel/templates/agenda_culturel/single-event/event-single-inc.html @@ -16,7 +16,7 @@

    {% picto_from_name "map-pin" %} - {{ event.location }} + {% include "agenda_culturel/event-location-inc.html" with event=event %}

    diff --git a/src/agenda_culturel/templates/location_field/map_widget.html b/src/agenda_culturel/templates/location_field/map_widget.html index b5ffadc..1ab9dc2 100644 --- a/src/agenda_culturel/templates/location_field/map_widget.html +++ b/src/agenda_culturel/templates/location_field/map_widget.html @@ -1,12 +1 @@ -
    -
    - {{field_input}} -
    -
    -
    - - -
    -
    -
    -
    +{{field_input}} diff --git a/src/agenda_culturel/templatetags/event_extra.py b/src/agenda_culturel/templatetags/event_extra.py index fc07f1e..d4ff22c 100644 --- a/src/agenda_culturel/templatetags/event_extra.py +++ b/src/agenda_culturel/templatetags/event_extra.py @@ -50,7 +50,16 @@ def show_badges_events(placement="top"): return mark_safe('' + picto_from_name("calendar") + " " + str(nb_drafts) + '') else: return "" - + +@register.simple_tag +def show_badge_unknown_places(placement="top"): + nb_unknown = Event.objects.filter(exact_location__isnull=True).count() + if nb_unknown != 0: + return mark_safe('' + picto_from_name("map-pin") + " " + str(nb_unknown) + '') + else: + return "" + + @register.simple_tag def event_field_verbose_name(the_field): return Event._meta.get_field(the_field).verbose_name diff --git a/src/agenda_culturel/urls.py b/src/agenda_culturel/urls.py index 75d3537..b4ed79f 100644 --- a/src/agenda_culturel/urls.py +++ b/src/agenda_culturel/urls.py @@ -72,6 +72,11 @@ urlpatterns = [ path("place//delete", PlaceDeleteView.as_view(), name="delete_place"), path("places/", PlaceListView.as_view(), name="view_places"), path("places/add", PlaceCreateView.as_view(), name="add_place"), + path("places/add/", PlaceFromEventCreateView.as_view(), name="add_place_from_event"), + path("events/unknown-places", UnknownPlacesListView.as_view(), name="view_unknown_places"), + path("events/unknown-places/fix", fix_unknown_places, name="fix_unknown_places"), + path("event//addplace", UnknownPlaceAddView.as_view(), name="add_place_to_event"), + ] if settings.DEBUG: diff --git a/src/agenda_culturel/views.py b/src/agenda_culturel/views.py index 259600f..d268888 100644 --- a/src/agenda_culturel/views.py +++ b/src/agenda_culturel/views.py @@ -12,7 +12,7 @@ from django.urls import reverse import urllib from collections import Counter -from .forms import EventSubmissionForm, EventForm, BatchImportationForm, FixDuplicates, SelectEventInList, MergeDuplicates, RecurrentImportForm, CategorisationRuleImportForm, ModerationQuestionForm, ModerationAnswerForm, ModerateForm, CategorisationForm +from .forms import EventSubmissionForm, EventForm, BatchImportationForm, FixDuplicates, SelectEventInList, MergeDuplicates, RecurrentImportForm, CategorisationRuleImportForm, ModerationQuestionForm, ModerationAnswerForm, ModerateForm, CategorisationForm, EventAddPlaceForm, PlaceForm from .models import Event, Category, StaticContent, ContactMessage, BatchImportation, DuplicatedEvents, RecurrentImport, CategorisationRule, remove_accents, ModerationQuestion, ModerationAnswer, Place from django.utils import timezone @@ -94,6 +94,11 @@ class EventFilter(django_filters.FilterSet): queryset=Category.objects.all(), widget=CategoryCheckboxSelectMultiple) + city = django_filters.MultipleChoiceFilter(label="Filtrer par ville", + field_name='exact_location__city', + choices=[(p["city"], p["city"]) for p in Place.objects.values("city").distinct().order_by("city")], + widget=forms.CheckboxSelectMultiple) + status = django_filters.MultipleChoiceFilter(label="Filtrer par status", choices=Event.STATUS.choices, field_name="status", @@ -102,7 +107,7 @@ class EventFilter(django_filters.FilterSet): class Meta: model = Event - fields = ["category", "tags", "exclude_tags", "status", "recurrences"] + fields = ["category", "city", "tags", "exclude_tags", "status", "recurrences"] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -136,6 +141,9 @@ class EventFilter(django_filters.FilterSet): def get_status(self): return self.form.cleaned_data["status"] + def get_cities(self): + return self.form.cleaned_data["city"] + def get_status_names(self): if "status" in self.form.cleaned_data: return [dict(Event.STATUS.choices)[s] for s in self.form.cleaned_data["status"]] @@ -158,7 +166,7 @@ class EventFilter(django_filters.FilterSet): else: if "status" in self.form.cleaned_data and len(self.form.cleaned_data["status"]) != 0: return True - return len(self.form.cleaned_data["category"]) != 0 or len(self.form.cleaned_data["tags"]) != 0 or len(self.form.cleaned_data["exclude_tags"]) != 0 or len(self.form.cleaned_data["recurrences"]) != 0 + return len(self.form.cleaned_data["category"]) != 0 or len(self.form.cleaned_data["tags"]) != 0 or len(self.form.cleaned_data["exclude_tags"]) != 0 or len(self.form.cleaned_data["recurrences"]) != 0 or len(self.form.cleaned_data["city"]) != 0 def is_selected(self, cat): return cat in self.form.cleaned_data["category"] @@ -1081,22 +1089,138 @@ class ModerationAnswerDeleteView(PermissionRequiredMixin, DeleteView): class PlaceListView(ListView): model = Place + paginate_by = 10 + ordering = ['-pk'] -class PlaceDetailView(PermissionRequiredMixin, DetailView): - model = Place - permission_required = ("agenda_culturel.view_place") -class PlaceUpdateView(PermissionRequiredMixin, UpdateView): +class PlaceDetailView(DetailView): + model = Place + +class UpdatePlaces: + def form_valid(self, form): + result = super().form_valid(form) + p = form.instance + + if not hasattr(self, "nb_applied"): + self.nb_applied = 0 + + # if required, find all matching events + if form.apply(): + u_events = Event.objects.filter(exact_location__isnull=True) + + to_be_updated = [] + # try to find matches + for ue in u_events: + if p.match(ue): + ue.exact_location = p + to_be_updated.append(ue) + continue + # update events with a location + Event.objects.bulk_update(to_be_updated, fields=["exact_location"]) + self.nb_applied += len(to_be_updated) + + if self.nb_applied > 1: + messages.success(self.request, _("{} events have been updated.").format(self.nb_applied)) + elif self.nb_applied == 1: + messages.success(self.request, _("1 event has been updated.")) + else: + messages.info(self.request, _("No events have been modified.")) + return result + + + +class PlaceUpdateView(UpdatePlaces, PermissionRequiredMixin, SuccessMessageMixin, UpdateView): model = Place - fields = '__all__' permission_required = ("agenda_culturel.change_place") + success_message = _('The place has been successfully updated.') + form_class = PlaceForm -class PlaceCreateView(PermissionRequiredMixin, SuccessMessageMixin, CreateView): + +class PlaceCreateView(UpdatePlaces, PermissionRequiredMixin, SuccessMessageMixin, CreateView): model = Place permission_required = ("agenda_culturel.add_place") + success_message = _('The place has been successfully created.') + form_class = PlaceForm class PlaceDeleteView(PermissionRequiredMixin, DeleteView): model = Place permission_required = ("agenda_culturel.delete_place") + success_url = reverse_lazy('view_places') + +class UnknownPlacesListView(PermissionRequiredMixin, ListView): + model = Event + permission_required = ("agenda_culturel.add_place") + paginate_by = 10 + template_name = 'agenda_culturel/place_unknown_list.html' + queryset = Event.objects.filter(exact_location__isnull=True) + +def fix_unknown_places(request): + # get all places + places = Place.objects.all() + # get all events without exact location + u_events = Event.objects.filter(exact_location__isnull=True) + + to_be_updated = [] + # try to find matches + for ue in u_events: + for p in places: + if p.match(ue): + ue.exact_location = p + to_be_updated.append(ue) + continue + # update events with a location + Event.objects.bulk_update(to_be_updated, fields=["exact_location"]) + + # create a success message + nb = len(to_be_updated) + if nb > 1: + messages.success(request, _("{} events have been updated.").format(nb)) + elif nb == 1: + messages.success(request, _("1 event has been updated.")) + else: + messages.info(request, _("No events have been modified.")) + + # come back to the list of places + return HttpResponseRedirect(reverse_lazy("view_unknown_places")) + + +class UnknownPlaceAddView(PermissionRequiredMixin, SuccessMessageMixin, UpdateView): + model = Event + permission_required = ("agenda_culturel.change_place", "agenda_culturel.change_event") + form_class = EventAddPlaceForm + template_name = 'agenda_culturel/place_unknown_form.html' + + def form_valid(self, form): + self.modified_event = form.modified_event() + return super().form_valid(form) + + def get_success_message(self, cleaned_data): + msg = "" + if cleaned_data.get("place"): + msg += _('The selected place has been assigned to the event. ') + if cleaned_data.get('add_alias'): + msg += _('A new alias has been added to the selected place. ') + return msg + + def get_success_url(self): + if self.modified_event: + return reverse_lazy('view_unknown_places') + else: + return reverse_lazy('add_place_from_event', args=[self.object.pk]) + + +class PlaceFromEventCreateView(PlaceCreateView): + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["event"] = self.event + return context + + def get_initial(self, *args, **kwargs): + initial = super().get_initial(**kwargs) + self.event = get_object_or_404(Event, pk=self.kwargs['pk']) + if self.event.location: + initial['aliases'] = [self.event.location] + return initial