Ajout de la gestion des lieux

This commit is contained in:
Jean-Marie Favreau 2024-04-27 18:32:08 +02:00
parent 2df11abda3
commit c6352091ea
27 changed files with 713 additions and 211 deletions

View File

@ -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('<div class="grid"><div>' + super().as_p() + '</div><div><div class="map-widget">' +
'<div id="map_location" style="width: 100%; aspect-ratio: 16/9"></div></div></div></div>')
def apply(self):
return self.cleaned_data.get("apply_to_all")

View File

@ -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 <jeanmarie.favreau@free.fr>\n"
"Language-Team: Jean-Marie Favreau <jeanmarie.favreau@free.fr>\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"

View File

@ -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'),
),
]

View File

@ -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'),
),
]

View File

@ -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),
]

View File

@ -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):

View File

@ -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;
}

View File

@ -0,0 +1,14 @@
{% if event.exact_location %}
{% if nolink %}
{{ event.exact_location.name }}, {{ event.exact_location.city }}
{% else %}
<a href="{% url 'view_place' event.exact_location.pk %}">{{ event.exact_location.name }}, {{ event.exact_location.city }}</a>
{% endif %}
{% else %}
{% if perms.agenda_culturel.change_event and perms.agenda_culturel.change_place %}
<a href="{% url 'add_place_to_event' event.pk %}" class="missing-data">{{ event.location }}</a>
{% else %}
{{ event.location }}
{% endif %}
{% endif %}

View File

@ -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

View File

@ -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 %}

View File

@ -10,8 +10,16 @@
<header>
<h1>Suppression du lieu {{ object.name }}</h1>
</header>
{% with nb_events=object.nb_events %}
{% if nb_events > 0 %}
<p class="message warning"><strong>Attention&nbsp;:</strong> {{ nb_events }} événement{{ nb_events|pluralize }}
{% if nb_events > 1 %}sont enregistrés{% else %}est enregistré{% endif %} dans ce lieu.</p>
{% endif %}
{% endwith %}
<p>Êtes-vous sûr·e de vouloir supprimer le lieu «&nbsp;{{ object.name }} ({{ object.pk }})&nbsp;» situé à l'adresse {{ object.address }}, et de coordonnées {{ object.location }}&nbsp;?</p>
<form method="post">{% csrf_token %}
<p>Êtes-vous sûr·e de vouloir supprimer le lieu «&nbsp;{{ object.name }} ({{ object.pk }})&nbsp;» situé à l'adresse {{ object.address }}, et de coordonnées {{ object.location }}&nbsp;?</p>
{{ form }}
<div class="grid buttons">
<a href="{% if request.META.HTTP_REFERER %}{{ request.META.HTTP_REFERER }}{% else %}{{ object.get_absolute_url }}{% endif %}" role="button" class="secondary">Annuler</a>

View File

@ -25,8 +25,8 @@
{% if perms.agenda_culturel.change_place %}
<a href="{% url 'view_places' %}" role="button">&lt; Tous les lieux</a>
{% endif %}
<div class="slide-buttons">
{% if perms.agenda_culturel.change_place %}
<div class="slide-buttons">
<a href="{% url 'edit_place' place.pk %}" role="button">Modifier {% picto_from_name "edit-3" %}</a>
<a href="{% url 'delete_place' place.pk %}" role="button">Supprimer {% picto_from_name "trash-2" %}</a>
</div>
@ -36,8 +36,13 @@
<div class="grid">
<div>
<ul>
<li><strong>Adresse&nbsp;:</strong> {{ object.address }}</li>
<li><strong>Adresse&nbsp;:</strong> {{ object.address }}, {{ object.city }}</li>
<li><strong>Coordonnée GPS&nbsp;:</strong> <a href="geo:{{ object.location }}">{{ object.location }}</a></li>
{% if object.nb_events > 0 %}
<li><strong>Nombre d'événements&nbsp;:</strong> {{ object.nb_events }}</li>
{% else %}
<li><em>Aucun événement n'est enregistré dans ce lieu</em></li>
{% endif %}
</ul>
{% if user.is_authenticated %}
{% if object.aliases|length > 0 %}

View File

@ -18,13 +18,20 @@
<h1>{% if form.instance.pk %}Modification{% else %}Création{% endif %} d'un lieu</h1>
<article>
{% if event %}
<p>Création d'un lieu depuis l'événement « {{ event }} » (voir en bas de page le détail de l'événement).</p>
{% endif %}
<form method="post">{% csrf_token %}
{{ form.as_p }}
{{ form.as_grid }}
<div class="grid buttons">
<a href="{% if request.META.HTTP_REFERER %}{{ request.META.HTTP_REFERER }}{% else %}{{ object.get_absolute_url }}{% endif %}" role="button" class="secondary">Annuler</a>
<input type="submit" value="Envoyer">
</div>
</form>
{% if event %}
<h2>Description du lieu&nbsp;:</h2>
{% include "agenda_culturel/single-event/event-single-inc.html" with event=event noedit=1 %}
{% endif %}
</article>
{% endblock %}

View File

@ -12,7 +12,10 @@
<div class="grid two-columns">
<article>
<header>
<a class="slide-buttons" href="{% url 'add_place' %}" role="button">Ajouter {% picto_from_name "plus-circle" %}</a>
<div class="slide-buttons">
<a href="{% url 'fix_unknown_places' %}" role="button" data-tooltip="Utiliser les alias des lieux pour les associer aux événements sans lieu">Détecter par alias {% picto_from_name "link" %}</a>
<a href="{% url 'add_place' %}" role="button">Ajouter {% picto_from_name "plus-circle" %}</a>
</div>
<h1>Lieux</h1>
</header>
{% if object_list %}
@ -26,7 +29,7 @@
</div>
<h2>Lieu #{{ place.pk }}&nbsp;: {{ place.name }}</h2>
<ul>
<li><strong>Adresse&nbsp;:</strong> {{ place.address }}</li>
<li><strong>Adresse&nbsp;:</strong> {{ place.address }}, {{ place.city }}</li>
<li><strong>Coordonnée GPS&nbsp;:</strong> {{ place.location }}</li>
</ul>
{% if place.aliases|length > 0 %}
@ -34,6 +37,11 @@
{% else %}
<p><em>Ce lieu n'a pas d'alias défini</em></p>
{% endif %}
{% if place.nb_events > 0 %}
<p><strong>Nombre d'événements&nbsp;:</strong> {{ place.nb_events }}</p>
{% else %}
<p><em>Aucun événement n'est enregistré dans ce lieu</em></p>
{% endif %}
</header>
</article>

View File

@ -0,0 +1,49 @@
{% extends "agenda_culturel/page.html" %}
{% load static %}
{% block title %}Ajouter un lieu à {{ object.title }}{% endblock %}
{% block entete_header %}
<script src="{% url 'jsi18n' %}"></script>
<script src="/static/admin/js/vendor/jquery/jquery.js"></script>
<script src="/static/admin/js/jquery.init.js"></script>
<script src="/static/admin/js/admin/DateTimeShortcuts.js"></script>
<script src="{% static 'recurrence/js/recurrence.js' %}"></script>
<script src="{% static 'recurrence/js/recurrence.js' %}"></script>
<script src="/static/admin/js/admin/RelatedObjectLookups.js"></script>
<script>window.CKEDITOR_BASEPATH = '/static/ckeditor/ckeditor/';</script>
<script src="/static/admin/js/vendor/jquery/jquery.js"></script>
<script src="/static/admin/js/jquery.init.js"></script>
<link href="{% static 'css/django_better_admin_arrayfield.min.css' %}" type="text/css" media="all" rel="stylesheet">
<script src="{% static 'js/django_better_admin_arrayfield.min.js' %}"></script>
<script src="{% static 'js/adjust_datetimes.js' %}"></script>
{% endblock %}
{% block fluid %}{% endblock %}
{% block content %}
{% load static_content_extra %}
<article>
<header>
<h1>Ajouter un lieu à {{ object.title }} ({{ object.start_day }})</h1>
</header>
<p>Si le lieu est disponible dans la liste, veuillez le sélectionner.</p>
<form method="post">{% csrf_token %}
{{ form.media }}
{{ form.as_p }}
<div class="grid buttons">
<a href="{% if request.META.HTTP_REFERER %}{{ request.META.HTTP_REFERER }}{% else %}{{ object.get_absolute_url }}{% endif %}" role="button" class="secondary">Annuler</a>
<input type="submit" value="Valider">
</div>
</form>
<h2>Description du lieu&nbsp;:</h2>
{% include "agenda_culturel/single-event/event-single-inc.html" with event=object noedit=1 %}
</article>
{% endblock %}

View File

@ -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 %}
<div class="grid two-columns">
<article>
<header>
<div class="slide-buttons">
<a href="{% url 'fix_unknown_places' %}" role="button" data-tooltip="Utiliser les alias des lieux pour les associer aux événements sans lieu">Détecter par alias {% picto_from_name "link" %}</a>
</div>
<h1>Événements sans lieu</h1>
</header>
<div>
{% for obj in object_list %}
<article>{% include "agenda_culturel/single-event/event-in-unknown-place-list-inc.html" with event=obj %}</article>
{% endfor %}
</div>
<footer>
<span>
{% if page_obj.has_previous %}
<a href="?page=1" role="button">&laquo; premier</a>
<a href="?page={{ page_obj.previous_page_number }}" role="button">précédent</a>
{% endif %}
<span>
Page {{ page_obj.number }} sur {{ page_obj.paginator.num_pages }}
</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}" role="button">suivant</a>
<a href="?page={{ page_obj.paginator.num_pages }}" role="button">dernier &raquo;</a>
{% endif %}
</span>
</footer>
</article>
{% include "agenda_culturel/side-nav.html" with current="unknown_places" %}
</div>
{% endblock %}

View File

@ -24,6 +24,9 @@
{% if perms.agenda_culturel.change_place %}
<li><a {% if current == "places" %}class="selected" {% endif %}href="{% url 'view_places' %}">Liste des lieux</a></li>
{% endif %}
{% if perms.agenda_culturel.change_place and perms.agenda_culturel.change_event %}
<li><a {% if current == "unknown_places" %}class="selected" {% endif %}href="{% url 'view_unknown_places' %}">Événements sans lieu</a>{% show_badge_unknown_places "left" %}</li>
{% endif %}
</ul>
</nav>
{% if perms.agenda_culturel.view_batchimportation or perms.agenda_culturel.view_recurrentimport or perms.agenda_culturel.view_categorisationrule%}

View File

@ -12,7 +12,7 @@
</header>
<p class="subentry-search"></p>
{% picto_from_name "map-pin" %}
{% if event.location_hl %}{{ event.location_hl | safe }}{% else %}{{ event.location }}{% endif %}</p>
{% if event.location_hl %}{{ event.location_hl | safe }}{% else %}{% include "agenda_culturel/event-location-inc.html" with event=event %}{% endif %}</p>
{% if event.has_recurrences %}
<p class="subentry-search">
{% picto_from_name "repeat" %}

View File

@ -21,14 +21,14 @@
{% endif %}
{{ event.category | small_cat_recurrent:event.has_recurrences }}
{% if event.location %}<hgroup>{% endif %}
{% if event.location or event.exact_location %}<hgroup>{% endif %}
<h3>
{{ event|picto_status }}
<a href="{{ event.get_absolute_url }}">{{ event.title }}</a></h3>
{% if event.location %}
{% if event.location or event.exact_location %}
<h4>
{% picto_from_name "map-pin" %}
{{ event.location }}
{% include "agenda_culturel/event-location-inc.html" with event=event %}
</h4>
</hgroup>
{% endif %}

View File

@ -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 %}<hgroup>{% endif %}
{% if event.location or event.exact_location %}<hgroup>{% endif %}
<h2>
{{ event|picto_status }}
<a href="{{ event.get_absolute_url }}">{{ event.title }}</a>
</h2>
{% if event.location %}
{% if event.location or event.exact_location %}
<h3>
{% picto_from_name "map-pin" %}
{{ event.location }}
{% include "agenda_culturel/event-location-inc.html" with event=event %}
</h3>
</hgroup>
{% endif %}

View File

@ -0,0 +1,46 @@
{% load utils_extra %}
{% load cat_extra %}
{% load event_extra %}
<header>
<div class="slide-buttons">
<div class="buttons">
<a href="{% url 'add_place_to_event' event.pk %}" role="button">Ajouter le lieu {% picto_from_name "map-pin" %}</a>
</div>
</div>
{{ event.category | small_cat_recurrent:event.has_recurrences }}
{{ event|picto_status }}
<a href="{{ event.get_absolute_url }}">
{{ event.title }}</a></p>
{% picto_from_name "map-pin" %}
{% include "agenda_culturel/event-location-inc.html" with event=event %}
</header>
<p>{% 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 %}
<p>
{% picto_from_name "repeat" %}
<!-- TODO: see https://forge.chapril.org/jmtrivial/agenda_culturel/issues/65 -->
{% 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 %}
</p>
{% endif %}
<p>
{% picto_from_name "tag" %}
{% for tag in event.tags %}
<a href="{% url 'view_tag' tag %}">{{ tag }}</a>
{% if not forloop.last %}, {% endif %}
{% endfor %}
</p>
<div class="description">
{% if event.description %}{{ event.description |truncatewords:60 }}{% else %}<em>pas de description</em>{% endif %}
</div>

View File

@ -15,7 +15,7 @@
<p>
{% picto_from_name "map-pin" %}
{{ event.location }}
{% include "agenda_culturel/event-location-inc.html" with event=event nolink=1 %}
</p>
<p>
{% picto_from_name "calendar" %}

View File

@ -16,7 +16,7 @@
</p>
<p>
{% picto_from_name "map-pin" %}
{{ event.location }}
{% include "agenda_culturel/event-location-inc.html" with event=event %}
</p>
</header>

View File

@ -1,12 +1 @@
<div class="grid">
<div>
{{field_input}}
</div>
<div>
<div class="map-widget" style="margin-top: 4px">
<label></label>
<div id="map_{{field_name}}" style="width: 100%; aspect-ratio: 16/9"></div>
</div>
</div>
</div>
{{field_input}}

View File

@ -50,7 +50,16 @@ def show_badges_events(placement="top"):
return mark_safe('<a href="' + reverse_lazy("moderation") + '?status=draft" class="badge" data-placement="' + placement + '" data-tooltip="' + str(nb_drafts) + ' brouillon' + pluralize(nb_drafts) + ' à valider">' + picto_from_name("calendar") + " " + str(nb_drafts) + '</a>')
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('<a href="' + reverse_lazy("view_unknown_places") + '" class="badge" data-placement="' + placement + '" data-tooltip="' + str(nb_unknown) + ' événement' + pluralize(nb_unknown) + ' sans lieu défini">' + picto_from_name("map-pin") + " " + str(nb_unknown) + '</a>')
else:
return ""
@register.simple_tag
def event_field_verbose_name(the_field):
return Event._meta.get_field(the_field).verbose_name

View File

@ -72,6 +72,11 @@ urlpatterns = [
path("place/<int:pk>/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/<int:pk>", 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/<int:pk>/addplace", UnknownPlaceAddView.as_view(), name="add_place_to_event"),
]
if settings.DEBUG:

View File

@ -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