Compare commits

...

119 Commits

Author SHA1 Message Date
Jean-Marie Favreau
77941ed0ee Fix bug (encore) 2025-01-02 20:51:37 +01:00
Jean-Marie Favreau
5240f426c1 Fix bug 2025-01-02 20:46:01 +01:00
Jean-Marie Favreau
001a0c1552 Fix première semaine de l'année 2024-12-30 02:21:36 +01:00
Jean-Marie Favreau
0300fd3979 Amélioration des balises d'entête 2024-12-29 17:26:05 +01:00
Jean-Marie Favreau
7f9ad5dd1b Raffinement de la gestion des messages 2024-12-29 17:16:20 +01:00
Jean-Marie Favreau
fc86738ee3 Suppression d'une trace inutile 2024-12-29 17:15:59 +01:00
Jean-Marie Favreau
c9df18c822 Amélioration de la gestion des messages
Fix #209
2024-12-29 13:07:08 +01:00
Jean-Marie Favreau
49a8f4b306 Fix #262 2024-12-29 10:35:25 +01:00
Jean-Marie Favreau
eef4f5639c On envoie une notification à la personne qui a proposé l'événement en cas de modération
Fix #209
2024-12-29 01:19:59 +01:00
Jean-Marie Favreau
4b55830419 Ajout de la possibilité d'ajouter des messages quand on soumet un événement
See #209
2024-12-28 18:47:03 +01:00
Jean-Marie Favreau
c3d10f01db Amélioration apparence menu admin 2024-12-28 17:40:11 +01:00
Jean-Marie Favreau
d4b364c567 On sauve le message entier 2024-12-28 17:13:45 +01:00
Jean-Marie Favreau
273287a250 Fix bug quand un événement n'a pas tous les champs définis 2024-12-28 17:13:06 +01:00
Jean-Marie Favreau
045cf06e06 Ajustement boutons administration 2024-12-28 12:03:46 +01:00
Jean-Marie Favreau
9cf1f437ef Modification description 2024-12-28 11:24:56 +01:00
Jean-Marie Favreau
789a53d2e5 Amélioration rendu écrans intermédiaires
Fix #113
2024-12-27 16:42:15 +01:00
Jean-Marie Favreau
2f0c0c6f0b Merge branch 'main' of ssh://forge.chapril.org:222/jmtrivial/agenda_culturel 2024-12-27 16:24:43 +01:00
Jean-Marie Favreau
19a51bc403 Fix error 505 in robots.txt 2024-12-27 16:24:28 +01:00
e99dc06bd9 Merge pull request 'Actualiser src/agenda_culturel/import_tasks/custom_extractors/lacomedie.py' (#261) from correction-script-import-comedie into main
Reviewed-on: #261
2024-12-27 16:04:14 +01:00
Jean-Marie Favreau
49c7bd5300 On utilise les classes là où les ids sont multiples (html pas valide) 2024-12-27 16:03:22 +01:00
cc0ae8b582 Merge branch 'main' into correction-script-import-comedie 2024-12-27 15:42:19 +01:00
3367876053 Actualiser src/agenda_culturel/import_tasks/custom_extractors/lacomedie.py
ajout de types de compléments à importer
2024-12-27 13:27:48 +01:00
Jean-Marie Favreau
84ce4b0d7d Amélioration de la gestion des images:
- téléchargement des images si elles sont manquantes
- utilisation d'un nom de fichier local pour éviter les collisions
- mise à jour des images lors de la mise à jour d'un événement
2024-12-27 00:09:17 +01:00
Jean-Marie Favreau
0d62660c2b Fix import 2024-12-26 16:16:35 +01:00
Jean-Marie Favreau
6b79949002 On ne compare pas les organisateurs quand on n'en a pas besoin
Fix #255
2024-12-26 11:53:00 +01:00
Jean-Marie Favreau
ad48b667be Amélioration du formulaire de déclaration de dupliqué 2024-12-26 11:13:00 +01:00
Jean-Marie Favreau
7c9d930e6f quand on créé une copie locale, elle est modérée en même temps
Fix #252
2024-12-22 16:08:58 +01:00
Jean-Marie Favreau
359451b9f8 AJout d'un message en cas d'événement trop long
Fix #259
2024-12-22 15:53:59 +01:00
Jean-Marie Favreau
ca6c7889a5 Ajout d'un fichier manquant 2024-12-22 15:53:52 +01:00
Jean-Marie Favreau
933e73de5c On peut forcer la localisation
Fix #248
2024-12-22 15:32:21 +01:00
Jean-Marie Favreau
d844fb7ccf Ajout de liens en évidence vers la vue jour
Fix #258
2024-12-22 14:09:00 +01:00
Jean-Marie Favreau
5343ad2cfa Amélioration mise en forme image 2024-12-22 12:37:08 +01:00
Jean-Marie Favreau
4a174d30ef Aout d'une image sur le modal
Fix #257
2024-12-22 12:15:29 +01:00
Jean-Marie Favreau
430c7b47a2 On ajoute une fonction de suppression de cache
Fix #136
2024-12-15 19:00:52 +01:00
Jean-Marie Favreau
63d3cb76ea on supprime le cache d'un événement quand on le modifie
Fix #254
2024-12-15 18:55:14 +01:00
Jean-Marie Favreau
d770cf23f0 Ajout d'un contenu fixe en première page de modération 2024-12-14 13:13:56 +01:00
Jean-Marie Favreau
cc0c798f5a Passer au suivant fonctionne
Fix #249
Fix #251
2024-12-14 12:53:55 +01:00
Jean-Marie Favreau
6ceec954d8 Le lien modérer "aujourd'hui" ne renvoie que sur les événements à venir 2024-12-14 12:50:59 +01:00
Jean-Marie Favreau
2c22d62302 Renommage du bouton 2024-12-14 12:40:23 +01:00
Jean-Marie Favreau
f79b1f0f89 Highlight boutons 2024-12-13 16:24:00 +01:00
Jean-Marie Favreau
3c1d51dda1 Encore du rendu 2024-12-13 16:19:13 +01:00
Jean-Marie Favreau
141949991c On améliore le rendu du tableau 2024-12-13 16:17:14 +01:00
Jean-Marie Favreau
290faf0b8f tentative d'amélioration des requêtes 2024-12-13 16:14:05 +01:00
Jean-Marie Favreau
f9f690cac7 mise en forme 2024-12-13 15:49:30 +01:00
Jean-Marie Favreau
5e8d9766ee Ajout de boutons pour modérer le jour souhaité 2024-12-13 12:28:40 +01:00
Jean-Marie Favreau
6589c1b0c3 On évite un possible bug 2024-12-13 05:44:32 +01:00
Jean-Marie Favreau
1055a36084 Fix problème site_id 2024-12-13 05:40:57 +01:00
Jean-Marie Favreau
2cca0322d1 Ajout d'une visualisation des modérations à venir
Fix #247
2024-12-13 00:06:36 +01:00
Jean-Marie Favreau
e5c075656c Fix collision de noms 2024-12-12 22:13:30 +01:00
Jean-Marie Favreau
5ef9358b28 Fix navigation semaines au changement d'année 2024-12-12 00:41:44 +01:00
Jean-Marie Favreau
0526854d6b Ajout de la possibilité de commenter les événements 2024-12-12 00:24:53 +01:00
Jean-Marie Favreau
bc06b6205d On affiche une chronologie en pied des événements 2024-12-11 23:27:38 +01:00
Jean-Marie Favreau
504198b14f Améliorations des performances 2024-12-11 20:16:08 +01:00
Jean-Marie Favreau
526b83ec20 Ajout de cache pour la page d'un jour 2024-12-11 18:42:35 +01:00
Jean-Marie Favreau
08e66918ab Possibles améliorations de performances 2024-12-11 18:40:49 +01:00
Jean-Marie Favreau
c34fb666b2 les événemnts notés spam ne sont pas comptés dans les messages ouverts 2024-12-11 18:16:10 +01:00
Jean-Marie Favreau
a94b9a53f2 Les personnes connextées ont un formulaire simplifié 2024-12-11 13:50:46 +01:00
Jean-Marie Favreau
c1f7bfd8c4 On renomme la classe ContactMessage en Message 2024-12-11 11:36:40 +01:00
Jean-Marie Favreau
b1e5414519 renomme "à venir" en "maintenant" 2024-12-09 23:20:13 +01:00
Jean-Marie Favreau
3da9a5239a AJout de la ligne "now" sur la vue "à venir" 2024-12-09 23:19:05 +01:00
Jean-Marie Favreau
c91cdf0c99 On ajoute la ligne now à la fin 2024-12-09 22:59:34 +01:00
Jean-Marie Favreau
6e8f00ccbe Amélioration url des organisations 2024-12-08 23:03:16 +01:00
Jean-Marie Favreau
a1984f60f5 Ajout de cache sur le sitemap 2024-12-08 22:52:50 +01:00
Jean-Marie Favreau
ce95fe6504 Ajout d'un sitemap
Fix #246
2024-12-08 22:46:49 +01:00
Jean-Marie Favreau
dd0c037929 Description de l'ical 2024-12-08 22:35:50 +01:00
Jean-Marie Favreau
41d6b39988 Fix événements sans image dans l'ical 2024-12-08 17:36:57 +01:00
Jean-Marie Favreau
3316d28e09 Amélioration export ical:
- ajout des images
- ajout de cache
2024-12-08 17:32:46 +01:00
Jean-Marie Favreau
f7f8d9cb0c On consolide la recherche (erreur 500 des moteurs de recherche) 2024-12-08 16:34:41 +01:00
Jean-Marie Favreau
ced15d5113 On assure que les dumps contiennent les utilisateurs 2024-12-08 15:53:28 +01:00
Jean-Marie Favreau
70ae92854f Consolidate migration 2024-12-08 15:08:26 +01:00
Jean-Marie Favreau
02448cf4d4 Fix export ical 2024-12-08 09:25:19 +01:00
Jean-Marie Favreau
14e25b660c Ajustement position ligne rouge 2024-12-08 09:21:30 +01:00
Jean-Marie Favreau
92da6585c6 Correction après modification de USE_TZ=False
Fix #245
2024-12-08 09:07:16 +01:00
Jean-Marie Favreau
cd52ae0286 Ajout d'une ligne "maintenant"
Fix #235
2024-12-07 11:15:56 +01:00
Jean-Marie Favreau
e050ce5eda On désactive la sortie d'erreurs 2024-12-07 10:11:36 +01:00
Jean-Marie Favreau
b0b828392a Traduction 2024-12-06 23:24:57 +01:00
Jean-Marie Favreau
c34abe9158 Restructuration des champs du formulaire de lieu 2024-12-06 23:24:31 +01:00
Jean-Marie Favreau
f52caf9855 Ajout d'une entrée code postal 2024-12-06 23:24:08 +01:00
Jean-Marie Favreau
bd1330cd2f Correction du nom par défaut 2024-12-06 23:22:38 +01:00
Jean-Marie Favreau
a31bcc2764 On modifie l'outil de localisation pour ajouter le lock
Fix #124
2024-12-06 19:48:32 +01:00
Jean-Marie Favreau
91907be984 Suggestions pour les champs d'un nouveau lieu
Voir #231
2024-12-06 18:10:11 +01:00
Jean-Marie Favreau
27ceac1e46 Ajout d'un espace manquant 2024-12-06 14:18:32 +01:00
Jean-Marie Favreau
b3cba9293c On ajoute l'url problématique 2024-12-06 11:28:20 +01:00
Jean-Marie Favreau
c857294345 Fix bug fusion manuelle 2024-12-05 21:32:35 +01:00
Jean-Marie Favreau
5a7cc080c7 Amélioration du mécanisme de modération
Fix #236
2024-12-05 20:52:50 +01:00
Jean-Marie Favreau
37ed7c45db Mise à jour des traductions 2024-12-05 20:52:43 +01:00
Jean-Marie Favreau
bda14c6ccb Ajout (temporaire) d'exports pour traquer les problèmes d'import des pages
Voir #244
2024-12-05 18:58:53 +01:00
Jean-Marie Favreau
3d70de9c1b On corrige la détection des users anonymes 2024-12-05 18:44:55 +01:00
Jean-Marie Favreau
874c1779f8 Correction soumission anonyme
Fix #239
2024-12-05 18:16:31 +01:00
Jean-Marie Favreau
084b3dfb25 Fix adresses image et url og 2024-11-29 21:43:22 +01:00
Jean-Marie Favreau
ec707bf272 On fait une capture par jour, pour l'aperçu moteurs de recherche
Fix #225
2024-11-29 21:13:21 +01:00
Jean-Marie Favreau
21b42e4fee Ajout d'un antispam
Fix #227
2024-11-29 20:09:48 +01:00
Jean-Marie Favreau
d55d029fc7 Fix formulaire (again) 2024-11-29 20:09:40 +01:00
Jean-Marie Favreau
1d9251946c Fix erreur 500 contact form 2024-11-29 20:02:12 +01:00
Jean-Marie Favreau
e875ae626b Amélioration mise en page 2024-11-29 19:49:47 +01:00
Jean-Marie Favreau
63aad60260 On supprime une méthode qui n'est plus utilisée depuis longtemps 2024-11-29 19:37:47 +01:00
Jean-Marie Favreau
27bce22670 On ne montre pas la pin s'il n'y a pas de lieu 2024-11-29 19:37:37 +01:00
Jean-Marie Favreau
1fc1fc13e1 Fix d'un bug possible quand on créé un groupe dupliqué 2024-11-29 19:37:21 +01:00
Jean-Marie Favreau
252fb8c27b Ajout d'informations lorsqu'un import est échoué pour éviter une détection en doublon 2024-11-29 19:37:00 +01:00
Jean-Marie Favreau
d70eca6493 Ajout d'une étape manquante 2024-11-29 19:36:42 +01:00
Jean-Marie Favreau
7f1bbabebf On enregistre l'auteur d'une modification
Fix #228
2024-11-29 19:35:45 +01:00
Jean-Marie Favreau
c55ed5c4dc Mise en forme recherche lieu 2024-11-29 15:44:20 +01:00
Jean-Marie Favreau
ac3d6796cf Ajout de l'import Rio
Fix #187
2024-11-29 14:57:29 +01:00
Jean-Marie Favreau
bf773686f9 L'image a une url absolue 2024-11-29 12:43:20 +01:00
Jean-Marie Favreau
1256adcb8a On ajoute un parse de plus pour les dates 2024-11-29 12:16:02 +01:00
Jean-Marie Favreau
7120da3e28 On défini une valeur par défaut 2024-11-29 11:44:40 +01:00
Jean-Marie Favreau
4e9ac573ac Consolidation en cas d'appel avec simple downloader 2024-11-29 11:42:29 +01:00
Jean-Marie Favreau
42fb85af48 Ajout d'informations complémentaires
Cf #216
2024-11-29 11:01:26 +01:00
Jean-Marie Favreau
256fed1e2e les paramètres de récurrence ne sont affichés que s'ils existent
Cf #224
2024-11-29 00:01:48 +01:00
Jean-Marie Favreau
d46ebeae3b Suppression des emoji sur les pages avec plusieurs événements
Fix #226
2024-11-28 23:29:47 +01:00
Jean-Marie Favreau
3be7d901c8 Fix couleur des liens accès rapide par lieu
Fix #220
2024-11-28 19:19:50 +01:00
Jean-Marie Favreau
5549d2172c Un utilisateur peut signaler un événement
Fix #15
2024-11-28 00:33:41 +01:00
Jean-Marie Favreau
674bba4a98 Ajout éditeur avancé pour contact 2024-11-27 23:08:34 +01:00
Jean-Marie Favreau
34008625d2 Ajout traduction formulaire 2024-11-27 22:45:38 +01:00
Jean-Marie Favreau
65430a2a8f Ajout de suggestions de filtres par ville 2024-11-27 19:57:39 +01:00
Jean-Marie Favreau
8ef620c8e1 Si un import se passe mal, on créé tout de même un événement pour pouvoir le gérer à la main
Fix #219
2024-11-27 18:25:10 +01:00
Jean-Marie Favreau
d119f1fa45 Merge branch 'filter-import-comedie' 2024-11-27 16:33:43 +01:00
Jean-Marie Favreau
41f6dbc352 Amélioration relation imports récurrents / étiquettes
- les imports récurrents sont mis à jour quand on renomme ou supprime une étiquette
- ajout de liens pour naviguer entre deux de ces objets
2024-11-27 16:25:59 +01:00
Jean-Marie Favreau
c9275c5ea0 on propose les tags uniquement dans la liste des existants:
Fix #217
2024-11-27 12:25:59 +01:00
87 changed files with 3656 additions and 845 deletions

View File

@ -36,7 +36,7 @@ Pour ajouter une nouvelle source custom:
### Récupérer un dump du prod sur un serveur dev
* sur le serveur de dev:
* ```docker exec -i agenda_culturel-backend python3 manage.py dumpdata --format=json --exclude=admin.logentry --exclude=auth.group --exclude=auth.permission --exclude=auth.user --exclude=contenttypes --indent=2 > fixtures/postgres-backup-20241101.json``` (à noter qu'ici on oublie les comptes, qu'il faudra recréer)
* ```docker exec -i agenda_culturel-backend python3 manage.py dumpdata --natural-foreign --natural-primary --format=json --exclude=admin.logentry --indent=2 > fixtures/postgres-backup-20241101.json``` (à noter qu'ici on oublie les comptes, qu'il faudra recréer)
* sur le serveur de prod:
* On récupère le dump json ```scp $SERVEUR:$PATH/fixtures/postgres-backup-20241101.json src/fixtures/```
* ```scripts/reset-database.sh FIXTURE COMMIT``` où ```FIXTURE``` est le timestamp dans le nom de la fixture, et ```COMMIT``` est l'ID du commit git correspondant à celle en prod sur le serveur au moment de la création de la fixture

View File

@ -5,10 +5,11 @@ WORKDIR /usr/src/app
RUN --mount=type=cache,target=/var/cache/apt \
apt-get update && \
apt-get install --no-install-recommends -y build-essential libpq-dev gettext chromium-driver gdal-bin \
apt-get install --no-install-recommends -y build-essential libpq-dev gettext chromium-driver gdal-bin fonts-symbola \
&& rm -rf /var/lib/apt/lists/*
COPY src/requirements.txt ./requirements.txt
RUN --mount=type=cache,target=/root/.cache/pip \

43
experimentations/get_le_rio.py Executable file
View File

@ -0,0 +1,43 @@
#!/usr/bin/python3
# coding: utf-8
import os
import json
import sys
# getting the name of the directory
# where the this file is present.
current = os.path.dirname(os.path.realpath(__file__))
# Getting the parent directory name
# where the current directory is present.
parent = os.path.dirname(current)
# adding the parent directory to
# the sys.path.
sys.path.append(parent)
from src.agenda_culturel.import_tasks.downloader import *
from src.agenda_culturel.import_tasks.extractor import *
from src.agenda_culturel.import_tasks.importer import *
from src.agenda_culturel.import_tasks.custom_extractors import *
if __name__ == "__main__":
u2e = URL2Events(SimpleDownloader(), lerio.CExtractor())
url = "https://www.cinemalerio.com/evenements/"
url_human = "https://www.cinemalerio.com/evenements/"
try:
events = u2e.process(url, url_human, cache = "cache-le-rio.html", default_values = {"location": "Cinéma le Rio", "category": "Cinéma"}, published = True)
exportfile = "events-le-roi.json"
print("Saving events to file {}".format(exportfile))
with open(exportfile, "w") as f:
json.dump(events, f, indent=4, default=str)
except Exception as e:
print("Exception: " + str(e))

View File

@ -73,6 +73,10 @@ git checkout $COMMIT
echobold "Setup database stucture according to the selected commit"
docker exec -i agenda_culturel-backend python3 manage.py migrate agenda_culturel
# remove all elements in database
echobold "Flush database"
docker exec -i agenda_culturel-backend python3 manage.py flush --no-input
# import data
echobold "Import data"
docker exec -i agenda_culturel-backend python3 manage.py loaddata --format=json $FFILE
@ -85,7 +89,4 @@ git checkout main
echobold "Update database"
docker exec -i agenda_culturel-backend python3 manage.py migrate agenda_culturel
# create superuser
echobold "Create superuser"
docker exec -ti agenda_culturel-backend python3 manage.py createsuperuser

View File

@ -9,7 +9,7 @@ from .models import (
BatchImportation,
RecurrentImport,
Place,
ContactMessage,
Message,
ReferenceLocation,
Organisation
)
@ -25,7 +25,7 @@ admin.site.register(DuplicatedEvents)
admin.site.register(BatchImportation)
admin.site.register(RecurrentImport)
admin.site.register(Place)
admin.site.register(ContactMessage)
admin.site.register(Message)
admin.site.register(ReferenceLocation)
admin.site.register(Organisation)

View File

@ -117,6 +117,23 @@ class DayInCalendar:
if e.start_time is None
else e.start_time
)
self.today_night = False
if self.is_today():
self.today_night = True
now = timezone.now()
nday = now.date()
ntime = now.time()
found = False
for idx,e in enumerate(self.events):
if (nday < e.start_day) or (nday == e.start_day and e.start_time and ntime <= e.start_time):
self.events[idx].is_first_after_now = True
found = True
break
if found:
self.today_night = False
def is_today_after_events(self):
return self.is_today() and self.today_night
def events_by_category_ordered(self):
from .models import Category
@ -175,12 +192,13 @@ class IntervalInDay(DayInCalendar):
self.id = self.id + '-' + str(id)
class CalendarList:
def __init__(self, firstdate, lastdate, filter=None, exact=False, ignore_dup=None):
def __init__(self, firstdate, lastdate, filter=None, exact=False, ignore_dup=None, qs=None):
self.firstdate = firstdate
self.lastdate = lastdate
self.now = date.today()
self.filter = filter
self.ignore_dup = ignore_dup
self.qs = qs
if exact:
self.c_firstdate = self.firstdate
@ -219,9 +237,12 @@ class CalendarList:
def fill_calendar_days(self):
if self.filter is None:
from .models import Event
if self.qs is None:
from .models import Event
qs = Event.objects.all()
qs = Event.objects.all()
else:
qs = self.qs
else:
qs = self.filter.qs
@ -229,7 +250,7 @@ class CalendarList:
qs = qs.exclude(other_versions=self.ignore_dup)
startdatetime = timezone.make_aware(datetime.combine(self.c_firstdate, time.min), timezone.get_default_timezone())
lastdatetime = timezone.make_aware(datetime.combine(self.c_lastdate, time.max), timezone.get_default_timezone())
self.events = qs.filter(
qs = qs.filter(
(Q(recurrence_dtend__isnull=True) & Q(recurrence_dtstart__lte=lastdatetime))
| (
Q(recurrence_dtend__isnull=False)
@ -243,7 +264,10 @@ class CalendarList:
Q(other_versions__isnull=True) |
Q(other_versions__representative=F('pk')) |
Q(other_versions__representative__isnull=True)
).order_by("start_time", "title__unaccent__lower").select_related("exact_location").select_related("category").select_related("other_versions").select_related("other_versions__representative")
).order_by("start_time", "title__unaccent__lower")
qs = qs.select_related("exact_location").select_related("category").select_related("other_versions").select_related("other_versions__representative")
self.events = qs
firstdate = datetime.fromordinal(self.c_firstdate.toordinal())
if firstdate.tzinfo is None or firstdate.tzinfo.utcoffset(firstdate) is None:
@ -291,14 +315,14 @@ class CalendarList:
def time_intervals_list_first(self):
return self.time_intervals_list(True)
def export_to_ics(self):
def export_to_ics(self, request):
from .models import Event
events = [event for day in self.get_calendar_days().values() for event in day.events]
return Event.export_to_ics(events)
return Event.export_to_ics(events, request)
class CalendarMonth(CalendarList):
def __init__(self, year, month, filter):
def __init__(self, year, month, filter, qs=None):
self.year = year
self.month = month
r = calendar.monthrange(year, month)
@ -306,7 +330,7 @@ class CalendarMonth(CalendarList):
first = date(year, month, 1)
last = date(year, month, r[1])
super().__init__(first, last, filter)
super().__init__(first, last, filter, qs)
def get_month_name(self):
return self.firstdate.strftime("%B")
@ -319,14 +343,14 @@ class CalendarMonth(CalendarList):
class CalendarWeek(CalendarList):
def __init__(self, year, week, filter):
def __init__(self, year, week, filter, qs=None):
self.year = year
self.week = week
first = date.fromisocalendar(self.year, self.week, 1)
last = date.fromisocalendar(self.year, self.week, 7)
super().__init__(first, last, filter)
super().__init__(first, last, filter, qs)
def next_week(self):
return self.firstdate + timedelta(days=7)
@ -336,8 +360,8 @@ class CalendarWeek(CalendarList):
class CalendarDay(CalendarList):
def __init__(self, date, filter=None):
super().__init__(date, date, filter, exact=True)
def __init__(self, date, filter=None, qs=None):
super().__init__(date, date, filter=filter, qs=qs, exact=True)
def get_events(self):
return self.calendar_days_list()[0].events

View File

@ -6,7 +6,8 @@ from celery.schedules import crontab
from celery.utils.log import get_task_logger
from celery.exceptions import MaxRetriesExceededError
import time as time_
from django.conf import settings
from celery.signals import worker_ready
from contextlib import contextmanager
@ -147,6 +148,8 @@ def run_recurrent_import_internal(rimport, downloader, req_id):
extractor = c3c.CExtractor()
elif rimport.processor == RecurrentImport.PROCESSOR.ARACHNEE:
extractor = arachnee.CExtractor()
elif rimport.processor == RecurrentImport.PROCESSOR.LERIO:
extractor = lerio.CExtractor()
else:
extractor = None
@ -173,6 +176,11 @@ def run_recurrent_import_internal(rimport, downloader, req_id):
published=published,
)
# force location if required
if rimport.forceLocation and location:
for i, e in enumerate(events['events']):
events['events'][i]["location"] = location
# convert it to json
json_events = json.dumps(events, default=str)
@ -248,6 +256,23 @@ def daily_imports(self):
run_recurrent_imports_from_list([imp.pk for imp in imports])
SCREENSHOT_FILE = settings.MEDIA_ROOT + '/screenshot.png'
@app.task(bind=True)
def screenshot(self):
downloader = ChromiumHeadlessDownloader(noimage=False)
downloader.screenshot("https://pommesdelune.fr", SCREENSHOT_FILE)
@worker_ready.connect
def at_start(sender, **k):
if not os.path.isfile(SCREENSHOT_FILE):
logger.info("Init screenshot file")
with sender.app.connection() as conn:
sender.app.send_task('agenda_culturel.celery.screenshot', None, connection=conn)
else:
logger.info("Screenshot file already exists")
@app.task(bind=True)
def run_all_recurrent_imports(self):
from agenda_culturel.models import RecurrentImport
@ -289,7 +314,7 @@ def weekly_imports(self):
run_recurrent_imports_from_list([imp.pk for imp in imports])
@app.task(base=ChromiumTask, bind=True)
def import_events_from_url(self, url, cat, tags, force=False):
def import_events_from_url(self, url, cat, tags, force=False, user_id=None, email=None, comments=None):
from .db_importer import DBImporterEvents
from agenda_culturel.models import RecurrentImport, BatchImportation
from agenda_culturel.models import Event, Category
@ -298,7 +323,7 @@ def import_events_from_url(self, url, cat, tags, force=False):
if acquired:
logger.info("URL import: {}".format(self.request.id))
logger.info("URL import: {}".format(self.request.id) + " force " + str(force))
# clean url
@ -323,7 +348,13 @@ def import_events_from_url(self, url, cat, tags, force=False):
# set default values
values = {}
if cat is not None:
values = {"category": cat, "tags": tags}
values["category"] = cat
if tags is not None:
values["tags"] = tags
if email is not None:
values["email"] = email
if comments is not None:
values["comments"] = comments
# get event
events = u2e.process(
@ -335,7 +366,7 @@ def import_events_from_url(self, url, cat, tags, force=False):
json_events = json.dumps(events, default=str)
# import events (from json)
success, error_message = importer.import_events(json_events)
success, error_message = importer.import_events(json_events, user_id)
# finally, close task
close_import_task(self.request.id, success, error_message, importer)
@ -352,14 +383,14 @@ def import_events_from_url(self, url, cat, tags, force=False):
@app.task(base=ChromiumTask, bind=True)
def import_events_from_urls(self, urls_cat_tags):
def import_events_from_urls(self, urls_cat_tags, user_id=None, email=None, comments=None):
for ucat in urls_cat_tags:
if ucat is not None:
url = ucat[0]
cat = ucat[1]
tags = ucat[2]
import_events_from_url.delay(url, cat, tags)
import_events_from_url.delay(url, cat, tags, user_id=user_id, email=email, comments=comments)
app.conf.beat_schedule = {
@ -368,6 +399,10 @@ app.conf.beat_schedule = {
# Daily imports at 3:14 a.m.
"schedule": crontab(hour=3, minute=14),
},
"daily_screenshot": {
"task": "agenda_culturel.celery.screenshot",
"schedule": crontab(hour=3, minute=3),
},
"weekly_imports": {
"task": "agenda_culturel.celery.weekly_imports",
# Daily imports on Mondays at 2:22 a.m.

View File

@ -11,6 +11,7 @@ class DBImporterEvents:
def __init__(self, celery_id):
self.celery_id = celery_id
self.error_message = ""
self.user_id = None
self.init_result_properties()
self.today = timezone.now().date().isoformat()
@ -34,9 +35,10 @@ class DBImporterEvents:
def get_nb_removed_events(self):
return self.nb_removed
def import_events(self, json_structure):
def import_events(self, json_structure, user_id=None):
print(json_structure)
self.init_result_properties()
self.user_id = user_id
try:
structure = json.loads(json_structure)
@ -95,7 +97,7 @@ class DBImporterEvents:
def save_imported(self):
self.db_event_objects, self.nb_updated, self.nb_removed = Event.import_events(
self.event_objects, remove_missing_from_source=self.url
self.event_objects, remove_missing_from_source=self.url, user_id=self.user_id
)
def is_valid_event_structure(self, event):

View File

@ -44,7 +44,7 @@ from .models import (
Tag,
Event,
Category,
ContactMessage,
Message,
DuplicatedEvents
)
@ -137,7 +137,11 @@ class EventFilter(django_filters.FilterSet):
if self.get_cleaned_data("position") is None or self.get_cleaned_data("radius") is None:
return parent
d = self.get_cleaned_data("radius")
p = self.get_cleaned_data("position").location
p = self.get_cleaned_data("position")
if not isinstance(d, str) or not isinstance(p, ReferenceLocation):
return parent
p = p.location
return parent.exclude(exact_location=False).filter(exact_location__location__distance_lt=(p, D(km=d)))
def get_url(self):
@ -188,6 +192,7 @@ class EventFilter(django_filters.FilterSet):
def get_cleaned_data(self, name):
try:
return self.form.cleaned_data[name]
except AttributeError:
@ -309,6 +314,13 @@ class EventFilter(django_filters.FilterSet):
else:
return str(self.get_cleaned_data("position")) + ' (' + str(self.get_cleaned_data("radius")) + ' km)'
def is_filtered_by_position_radius(self):
return not self.get_cleaned_data("position") is None and not self.get_cleaned_data("radius") is None
def get_url_add_suggested_position(self, location):
result = self.request.get_full_path()
return result + ('&' if '?' in result else '?') + 'position=' + str(location.pk) + "&radius=" + str(location.suggested_distance)
class EventFilterAdmin(django_filters.FilterSet):
status = django_filters.MultipleChoiceFilter(
@ -349,21 +361,27 @@ class EventFilterAdmin(django_filters.FilterSet):
fields = ["status"]
class ContactMessagesFilterAdmin(django_filters.FilterSet):
class MessagesFilterAdmin(django_filters.FilterSet):
closed = django_filters.MultipleChoiceFilter(
label="Status",
label=_("Status"),
choices=((True, _("Closed")), (False, _("Open"))),
widget=forms.CheckboxSelectMultiple,
)
spam = django_filters.MultipleChoiceFilter(
label="Spam",
label=_("Spam"),
choices=((True, _("Spam")), (False, _("Non spam"))),
widget=forms.CheckboxSelectMultiple,
)
message_type = django_filters.MultipleChoiceFilter(
label=_("Type"),
choices=Message.TYPE.choices,
widget=forms.CheckboxSelectMultiple,
)
class Meta:
model = ContactMessage
fields = ["closed", "spam"]
model = Message
fields = ["closed", "spam", "message_type"]
class SimpleSearchEventFilter(django_filters.FilterSet):

View File

@ -13,16 +13,21 @@ from django.forms import (
BooleanField,
HiddenInput,
ModelChoiceField,
EmailField
)
from django.forms import formset_factory
from django_better_admin_arrayfield.forms.widgets import DynamicArrayWidget
from .utils import PlaceGuesser
from .models import (
Event,
RecurrentImport,
CategorisationRule,
Place,
Category,
Tag
Tag,
Message
)
from django.conf import settings
from django.core.files import File
@ -31,7 +36,6 @@ from django.utils.translation import gettext_lazy as _
from string import ascii_uppercase as auc
from .templatetags.utils_extra import int_to_abc
from django.utils.safestring import mark_safe
from django.utils.timezone import localtime
from django.utils.formats import localize
from .templatetags.event_extra import event_field_verbose_name, field_to_html
import os
@ -73,7 +77,7 @@ class GroupFormMixin:
return [f for f in self.visible_fields() if not hasattr(f.field, "toggle_group") and (not hasattr(f.field, "group_id") or f.field.group_id == None)]
def fields_by_group(self):
return [(g, self.get_fields_in_group(g)) for g in self.groups] + [(None, self.get_no_group_fields())]
return [(g, self.get_fields_in_group(g)) for g in self.groups] + [(GroupFormMixin.FieldGroup("other", _("Other")), self.get_no_group_fields())]
def clean(self):
result = super().clean()
@ -124,7 +128,42 @@ class TagRenameForm(Form):
def is_force(self):
return "force" in self.fields and self.cleaned_data["force"] == True
class URLSubmissionForm(Form):
class SimpleContactForm(GroupFormMixin, Form):
email = EmailField(
label=_("Your email"),
help_text=_("Your email address"),
max_length=254,
required=False
)
comments = CharField(
label=_("Comments"),
help_text=_("Your message for the moderation team (comments, clarifications, requests...)"),
widget=Textarea,
max_length=2048,
required=False
)
def __init__(self, *args, **kwargs):
is_authenticated = "is_authenticated" in kwargs and kwargs["is_authenticated"]
super().__init__(*args, **kwargs)
if not is_authenticated:
self.add_group('communication',
_('Receive notification of publication or leave a message for moderation'),
maskable=True,
default_masked=True)
self.fields["email"].group_id = 'communication'
self.fields["comments"].group_id = 'communication'
else:
del self.fields["email"]
del self.fields["comments"]
class URLSubmissionForm(GroupFormMixin, Form):
required_css_class = 'required'
url = URLField(max_length=512)
@ -142,11 +181,20 @@ class URLSubmissionForm(Form):
)
def __init__(self, *args, **kwargs):
is_authenticated = kwargs.pop("is_authenticated", False)
super().__init__(*args, **kwargs)
self.fields["tags"].choices = Tag.get_tag_groups(all=True)
self.add_group('event', _('Event'))
self.fields["url"].group_id = 'event'
self.fields["category"].group_id = 'event'
self.fields["tags"].group_id = 'event'
class URLSubmissionFormWithContact(SimpleContactForm, URLSubmissionForm):
pass
URLSubmissionFormSet = formset_factory(URLSubmissionForm, extra=9, min_num=1)
class DynamicArrayWidgetURLs(DynamicArrayWidget):
template_name = "agenda_culturel/widgets/widget-urls.html"
@ -159,12 +207,20 @@ class DynamicArrayWidgetTags(DynamicArrayWidget):
class RecurrentImportForm(ModelForm):
required_css_class = 'required'
defaultTags = MultipleChoiceField(
label=_("Tags"),
initial=None,
choices=[],
required=False
)
class Meta:
model = RecurrentImport
fields = "__all__"
widgets = {
"defaultTags": DynamicArrayWidgetTags(),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["defaultTags"].choices = Tag.get_tag_groups(all=True)
class CategorisationRuleImportForm(ModelForm):
@ -180,6 +236,7 @@ class EventForm(GroupFormMixin, ModelForm):
old_local_image = CharField(widget=HiddenInput(), required=False)
simple_cloning = CharField(widget=HiddenInput(), required=False)
cloning = CharField(widget=HiddenInput(), required=False)
tags = MultipleChoiceField(
label=_("Tags"),
@ -196,7 +253,11 @@ class EventForm(GroupFormMixin, ModelForm):
"modified_date",
"moderated_date",
"import_sources",
"image"
"image",
"moderated_by_user",
"modified_by_user",
"created_by_user",
"imported_by_user"
]
widgets = {
"start_day": TextInput(
@ -245,7 +306,14 @@ class EventForm(GroupFormMixin, ModelForm):
self.fields['end_day'].group_id = 'end'
self.fields['end_time'].group_id = 'end'
self.add_group('recurrences', _('This is a recurring event'), maskable=True, default_masked=True)
self.add_group('recurrences',
_('This is a recurring event'),
maskable=True,
default_masked=not (self.instance and
self.instance.recurrences and
self.instance.recurrences.rrules and
len(self.instance.recurrences.rrules) > 0))
self.fields['recurrences'].group_id = 'recurrences'
self.add_group('details', _('Details'))
@ -261,6 +329,8 @@ class EventForm(GroupFormMixin, ModelForm):
self.fields['local_image'].group_id = 'illustration'
self.fields['image_alt'].group_id = 'illustration'
self.add_group('urls', _('URLs'))
self.fields["reference_urls"].group_id = 'urls'
if is_authenticated:
self.add_group('meta-admin', _('Meta information'))
@ -317,6 +387,9 @@ class EventForm(GroupFormMixin, ModelForm):
self.cleaned_data['local_image'] = File(name=basename, file=open(old, "rb"))
class EventFormWithContact(SimpleContactForm, EventForm):
pass
class MultipleChoiceFieldAcceptAll(MultipleChoiceField):
def validate(self, value):
pass
@ -347,6 +420,9 @@ class EventModerateForm(ModelForm):
"exact_location",
"tags"
]
widgets = {
"status": RadioSelect
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@ -470,14 +546,14 @@ class FixDuplicates(Form):
class SelectEventInList(Form):
required_css_class = 'required'
event = ChoiceField()
event = ChoiceField(label=_('Event'))
def __init__(self, *args, **kwargs):
events = kwargs.pop("events", None)
super().__init__(*args, **kwargs)
self.fields["event"].choices = [
(e.pk, str(e.start_day) + " " + e.title + ((", " + e.location) if e.location else "")) for e in events
(e.pk, (e.start_time.strftime('%H:%M') + " : " if e.start_time else "") + e.title + ((", " + e.location) if e.location else "")) for e in events
]
@ -525,17 +601,17 @@ class MergeDuplicates(Form):
'<li><a href="' + e.get_absolute_url() + '">' + e.title + "</a></li>"
)
result += (
"<li>Création&nbsp;: " + localize(localtime(e.created_date)) + "</li>"
"<li>Création&nbsp;: " + localize(e.created_date) + "</li>"
)
result += (
"<li>Dernière modification&nbsp;: "
+ localize(localtime(e.modified_date))
+ localize(e.modified_date)
+ "</li>"
)
if e.imported_date:
result += (
"<li>Dernière importation&nbsp;: "
+ localize(localtime(e.imported_date))
+ localize(e.imported_date)
+ "</li>"
)
result += "</ul>"
@ -586,7 +662,7 @@ class MergeDuplicates(Form):
result += '<input id="' + id + '" name="' + key + '"'
if key in MergeDuplicates.checkboxes_fields:
result += ' type="checkbox"'
if value in checked:
if checked and value in checked:
result += " checked"
else:
result += ' type="radio"'
@ -618,7 +694,7 @@ class MergeDuplicates(Form):
result = []
for s in selected:
for e in self.duplicates.get_duplicated():
if e.pk == selected:
if e.pk == s:
result.append(e)
break
return result
@ -702,7 +778,7 @@ class EventAddPlaceForm(Form):
return self.instance
class PlaceForm(ModelForm):
class PlaceForm(GroupFormMixin, ModelForm):
required_css_class = 'required'
apply_to_all = BooleanField(
@ -718,13 +794,70 @@ class PlaceForm(ModelForm):
fields = "__all__"
widgets = {"location": TextInput()}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.add_group('header', _('Header'))
self.fields['name'].group_id = 'header'
self.add_group('address', _('Address'))
self.fields['address'].group_id = 'address'
self.fields['postcode'].group_id = 'address'
self.fields['city'].group_id = 'address'
self.fields['location'].group_id = 'address'
self.add_group('meta', _('Meta'))
self.fields['aliases'].group_id = 'meta'
self.add_group('information', _('Information'))
self.fields['description'].group_id = 'information'
def as_grid(self):
return mark_safe(
'<div class="grid"><div>'
result = ('<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><p>Cliquez pour ajuster la position GPS</p></div></div></div>'
)
+ '''</div><div><div class="map-widget">
<div id="map_location" style="width: 100%; aspect-ratio: 16/9"></div>
<p>Cliquez pour ajuster la position GPS</p></div>
<input type="checkbox" role="switch" id="lock_position">Verrouiller la position</lock>
<script>
document.getElementById("lock_position").onclick = function() {
const field = document.getElementById("id_location");
if (this.checked)
field.setAttribute("readonly", true);
else
field.removeAttribute("readonly");
}
</script>
</div></div>''')
return mark_safe(result)
def apply(self):
return self.cleaned_data.get("apply_to_all")
class MessageForm(ModelForm):
class Meta:
model = Message
fields = ["subject", "name", "email", "message", "related_event"]
widgets = {"related_event": HiddenInput(), "user": HiddenInput() }
def __init__(self, *args, **kwargs):
self.event = kwargs.pop("event", False)
self.internal = kwargs.pop("internal", False)
super().__init__(*args, **kwargs)
self.fields['related_event'].required = False
if self.internal:
self.fields.pop("name")
self.fields.pop("email")
class MessageEventForm(ModelForm):
class Meta:
model = Message
fields = ["message"]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["message"].label = _("Add a comment")

View File

@ -3,6 +3,12 @@ from ..extractor_facebook import FacebookEvent
import json5
from bs4 import BeautifulSoup
import json
import os
from datetime import datetime
import logging
logger = logging.getLogger(__name__)
# A class dedicated to get events from a facebook events page
@ -13,10 +19,27 @@ class CExtractor(TwoStepsExtractor):
def build_event_url_list(self, content):
soup = BeautifulSoup(content, "html.parser")
debug = False
found = False
links = soup.find_all("a")
for link in links:
if link.get("href").startswith('https://www.facebook.com/events/'):
self.add_event_url(link.get('href').split('?')[0])
found = True
if not found and debug:
directory = "errors/"
if not os.path.exists(directory):
os.makedirs(directory)
now = datetime.now()
filename = directory + now.strftime("%Y%m%d_%H%M%S") + ".html"
logger.warning("cannot find any event link in events page. Save content page in " + filename)
with open(filename, "w") as text_file:
text_file.write("<!-- " + self.url + " -->\n\n")
text_file.write(content)
def add_event_from_content(
@ -42,4 +65,7 @@ class CExtractor(TwoStepsExtractor):
event["published"] = published
self.add_event(default_values, **event)
else:
logger.warning("cannot find any event in page")

View File

@ -106,6 +106,16 @@ class CExtractor(TwoStepsExtractor):
description = soup.select("#descspec")
if description and len(description) > 0:
description = description[0].get_text().replace("Lire plus...", "")
# on ajoute éventuellement les informations complémentaires
d_suite = ""
for d in ["#typespec", "#dureespec", "#lieuspec", ".lkuncontdroitespec"]:
comp_desc = soup.select(d)
if comp_desc and len(comp_desc) > 0:
for desc in comp_desc:
d_suite += "\n\n" + desc.get_text()
if d_suite != "":
description += "\n\n> Informations complémentaires:" + d_suite
else:
description = None

View File

@ -0,0 +1,91 @@
from ..generic_extractors import *
from bs4 import BeautifulSoup
from datetime import datetime
# A class dedicated to get events from Cinéma Le Rio (Clermont-Ferrand)
# URL: https://www.cinemalerio.com/evenements/
class CExtractor(TwoStepsExtractorNoPause):
def __init__(self):
super().__init__()
self.possible_dates = {}
self.theater = None
def build_event_url_list(self, content, infuture_days=180):
soup = BeautifulSoup(content, "html.parser")
links = soup.select("td.seance_link a")
if links:
for l in links:
print(l["href"])
self.add_event_url(l["href"])
def to_text_select_one(soup, filter):
e = soup.select_one(filter)
if e is None:
return None
else:
return e.text
def add_event_from_content(
self,
event_content,
event_url,
url_human=None,
default_values=None,
published=False,
):
soup = BeautifulSoup(event_content, "html.parser")
title = soup.select_one("h1").text
alerte_date = CExtractor.to_text_select_one(soup, ".alerte_date")
if alerte_date is None:
return
dh = alerte_date.split("à")
# if date is not found, we skip
if len(dh) != 2:
return
date = Extractor.parse_french_date(dh[0], default_year=datetime.now().year)
time = Extractor.parse_french_time(dh[1])
synopsis = CExtractor.to_text_select_one(soup, ".synopsis_bloc")
special_titre = CExtractor.to_text_select_one(soup, ".alerte_titre")
special = CExtractor.to_text_select_one(soup, ".alerte_text")
# it's not a specific event: we skip it
special_lines = None if special is None else special.split('\n')
if special is None or len(special_lines) == 0 or \
(len(special_lines) == 1 and special_lines[0].strip().startswith('En partenariat')):
return
description = "\n\n".join([x for x in [synopsis, special_titre, special] if not x is None])
image = soup.select_one(".col1 img")
image_alt = None
if not image is None:
image_alt = image["alt"]
image = image["src"]
self.add_event_with_props(
default_values,
event_url,
title,
None,
date,
None,
description,
[],
recurrences=None,
uuids=[event_url],
url_human=event_url,
start_time=time,
end_day=None,
end_time=None,
published=published,
image=image,
image_alt=image_alt
)

View File

@ -66,7 +66,7 @@ class SimpleDownloader(Downloader):
class ChromiumHeadlessDownloader(Downloader):
def __init__(self, pause=True):
def __init__(self, pause=True, noimage=True):
super().__init__()
self.pause = pause
self.options = Options()
@ -78,17 +78,31 @@ class ChromiumHeadlessDownloader(Downloader):
self.options.add_argument("--disable-dev-shm-usage")
self.options.add_argument("--disable-browser-side-navigation")
self.options.add_argument("--disable-gpu")
self.options.add_experimental_option(
"prefs", {
# block image loading
"profile.managed_default_content_settings.images": 2,
}
)
if noimage:
self.options.add_experimental_option(
"prefs", {
# block image loading
"profile.managed_default_content_settings.images": 2,
}
)
self.service = Service("/usr/bin/chromedriver")
self.driver = webdriver.Chrome(service=self.service, options=self.options)
def screenshot(self, url, path_image):
print("Screenshot {}".format(url))
try:
self.driver.get(url)
if self.pause:
time.sleep(2)
self.driver.save_screenshot(path_image)
except:
print(f">> Exception: {URL}")
return False
return True
def download(self, url, referer=None, post=None):
if post:
raise Exception("POST method with Chromium headless not yet implemented")

View File

@ -2,7 +2,7 @@ from abc import ABC, abstractmethod
from datetime import datetime, time, date, timedelta
import re
import unicodedata
from django.utils import timezone
@ -49,7 +49,7 @@ class Extractor(ABC):
return i + 1
return None
def parse_french_date(text):
def parse_french_date(text, default_year=None):
# format NomJour Numero Mois Année
m = re.search(
"[a-zA-ZéÉûÛ:.]+[ ]*([0-9]+)[er]*[ ]*([a-zA-ZéÉûÛ:.]+)[ ]*([0-9]+)", text
@ -73,8 +73,15 @@ class Extractor(ABC):
month = int(m.group(2))
year = m.group(3)
else:
# TODO: consolider les cas non satisfaits
return None
# format Numero Mois Annee
m = re.search("([0-9]+)[er]*[ ]*([a-zA-ZéÉûÛ:.]+)", text)
if m:
day = m.group(1)
month = Extractor.guess_month(m.group(2))
year = default_year
else:
# TODO: consolider les cas non satisfaits
return None
if month is None:
return None
@ -193,6 +200,8 @@ class Extractor(ABC):
"published": published,
"image": image,
"image_alt": image_alt,
"email": self.default_value_if_exists(default_values, "email"),
"comments": self.default_value_if_exists(default_values, "comments"),
}
# TODO: pourquoi url_human et non reference_url
if url_human is not None:
@ -240,6 +249,28 @@ class Extractor(ABC):
from .extractor_ggcal_link import GoogleCalendarLinkEventExtractor
if single_event:
return [FacebookEventExtractor(), GoogleCalendarLinkEventExtractor()]
return [FacebookEventExtractor(), GoogleCalendarLinkEventExtractor(), EventNotFoundExtractor()]
else:
return [ICALExtractor(), FacebookEventExtractor(), GoogleCalendarLinkEventExtractor()]
return [ICALExtractor(), FacebookEventExtractor(), GoogleCalendarLinkEventExtractor(), EventNotFoundExtractor()]
# A class that only produce a not found event
class EventNotFoundExtractor(Extractor):
def extract(
self, content, url, url_human=None, default_values=None, published=False
):
self.set_header(url)
self.clear_events()
self.add_event(default_values, "événement sans titre depuis " + url,
None, timezone.now().date(), None,
"l'import a échoué, la saisie doit se faire manuellement à partir de l'url source " + url,
[], [url], published=False, url_human=url)
return self.get_structure()
def clean_url(url):
return url

View File

@ -239,7 +239,7 @@ class FacebookEventExtractor(Extractor):
result = "https://www.facebook.com" + u.path
# remove name in the url
match = re.match(r"(.*/events)/s/([a-zA-Z-][a-zA-Z-0-9]+)/([0-9/]*)", result)
match = re.match(r"(.*/events)/s/([a-zA-Z-][a-zA-Z-0-9-]+)/([0-9/]*)", result)
if match:
result = match[1] + "/" + match[3]

View File

@ -264,9 +264,13 @@ class TwoStepsExtractorNoPause(TwoStepsExtractor):
only_future=True,
ignore_404=True
):
pause = self.downloader.pause
if hasattr(self.downloader, "pause"):
pause = self.downloader.pause
else:
pause = False
self.downloader.pause = False
result = super().extract(content, url, url_human, default_values, published, only_future, ignore_404)
self.downloader.pause = pause
return result
return result

View File

@ -1,6 +1,11 @@
from .downloader import *
from .extractor import *
import logging
logger = logging.getLogger(__name__)
class URL2Events:
def __init__(
@ -29,8 +34,9 @@ class URL2Events:
else:
# if the extractor is not defined, use a list of default extractors
for e in Extractor.get_default_extractors(self.single_event):
logger.warning('Extractor::' + type(e).__name__)
e.set_downloader(self.downloader)
events = e.extract(content, url, url_human, default_values, published)
if events is not None:
if events is not None and len(events) > 0:
return events
return None

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,11 @@
# Generated by Django 4.2.9 on 2024-10-10 20:35
from django.db import migrations
from agenda_culturel.models import Place
from django.contrib.gis.geos import Point
def change_coord_format(apps, schema_editor):
places = Place.objects.all()
Place = apps.get_model("agenda_culturel", "Place")
places = Place.objects.values("location", "location_pt").all()
for p in places:
l = p.location.split(',')
@ -13,14 +13,15 @@ def change_coord_format(apps, schema_editor):
p.location_pt = Point(float(l[1]), float(l[0]))
else:
p.location_pt = Point(3.08333, 45.783329)
p.save()
p.save(update_fields=["location_pt"])
def reverse_coord_format(apps, schema_editor):
places = Place.objects.all()
Place = apps.get_model("agenda_culturel", "Place")
places = Place.objects.values("location", "location_pt").all()
for p in places:
p.location = ','.join([p.location_pt[1], p.location_pt[0]])
p.save()
p.save(update_fields=["location"])

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.9 on 2024-11-27 18:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agenda_culturel', '0119_alter_tag_options_alter_event_category_and_more'),
]
operations = [
migrations.AddField(
model_name='referencelocation',
name='suggested_distance',
field=models.IntegerField(default=None, help_text='If this distance is given, this location is part of the suggested filters.', null=True, verbose_name='Suggested distance (km)'),
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 4.2.9 on 2024-11-27 22:13
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('agenda_culturel', '0120_referencelocation_suggested_distance'),
]
operations = [
migrations.AddField(
model_name='contactmessage',
name='related_event',
field=models.ForeignKey(default=None, help_text='The message is associated with this event.', null=True, on_delete=django.db.models.deletion.SET_DEFAULT, to='agenda_culturel.event', verbose_name='Related event'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.9 on 2024-11-29 13:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agenda_culturel', '0121_contactmessage_related_event'),
]
operations = [
migrations.AlterField(
model_name='recurrentimport',
name='processor',
field=models.CharField(choices=[('ical', 'ical'), ('icalnobusy', 'ical no busy'), ('icalnovc', 'ical no VC'), ('lacoope', 'lacoope.org'), ('lacomedie', 'la comédie'), ('lefotomat', 'le fotomat'), ('lapucealoreille', "la puce à l'oreille"), ('Plugin wordpress MEC', 'Plugin wordpress MEC'), ('Facebook events', "Événements d'une page FB"), ('cour3coquins', 'la cour des 3 coquins'), ('arachnee', 'Arachnée concert'), ('rio', 'Le Rio')], default='ical', max_length=20, verbose_name='Processor'),
),
]

View File

@ -0,0 +1,36 @@
# Generated by Django 4.2.9 on 2024-11-29 18:18
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('agenda_culturel', '0122_alter_recurrentimport_processor'),
]
operations = [
migrations.AddField(
model_name='event',
name='created_by_user',
field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='created_events', to=settings.AUTH_USER_MODEL, verbose_name='Author of the event creation'),
),
migrations.AddField(
model_name='event',
name='imported_by_user',
field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='imported_events', to=settings.AUTH_USER_MODEL, verbose_name='Author of the last importation'),
),
migrations.AddField(
model_name='event',
name='moderated_by_user',
field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='moderated_events', to=settings.AUTH_USER_MODEL, verbose_name='Author of the last moderation'),
),
migrations.AddField(
model_name='event',
name='modified_by_user',
field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, related_name='modified_events', to=settings.AUTH_USER_MODEL, verbose_name='Author of the last modification'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.9 on 2024-12-06 21:37
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agenda_culturel', '0123_event_created_by_user_event_imported_by_user_and_more'),
]
operations = [
migrations.AddField(
model_name='place',
name='postcode',
field=models.CharField(blank=True, help_text='The post code is not displayed, but makes it easier to find an address when you enter it.', null=True, verbose_name='Postcode'),
),
]

View File

@ -0,0 +1,21 @@
# Generated by Django 4.2.9 on 2024-12-11 11:33
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('agenda_culturel', '0124_place_postcode'),
]
operations = [
migrations.RenameModel(
old_name='ContactMessage',
new_name='Message',
),
migrations.AlterModelOptions(
name='message',
options={'verbose_name': 'Message', 'verbose_name_plural': 'Messages'},
),
]

View File

@ -0,0 +1,21 @@
# Generated by Django 4.2.9 on 2024-12-11 11:56
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('agenda_culturel', '0125_rename_contactmessage_message_alter_message_options'),
]
operations = [
migrations.AddField(
model_name='message',
name='user',
field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, to=settings.AUTH_USER_MODEL, verbose_name='Author of the message'),
),
]

View File

@ -0,0 +1,25 @@
# Generated by Django 4.2.9 on 2024-12-11 19:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agenda_culturel', '0126_message_user'),
]
operations = [
migrations.AddIndex(
model_name='event',
index=models.Index(fields=['end_day', 'end_time'], name='agenda_cult_end_day_4660a5_idx'),
),
migrations.AddIndex(
model_name='event',
index=models.Index(fields=['status'], name='agenda_cult_status_893243_idx'),
),
migrations.AddIndex(
model_name='event',
index=models.Index(fields=['recurrence_dtstart', 'recurrence_dtend'], name='agenda_cult_recurre_a8911c_idx'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.9 on 2024-12-11 19:12
from django.db import migrations, models
import django.db.models.functions.text
class Migration(migrations.Migration):
dependencies = [
('agenda_culturel', '0127_event_agenda_cult_end_day_4660a5_idx_and_more'),
]
operations = [
migrations.AddIndex(
model_name='event',
index=models.Index(models.F('start_time'), models.F('start_day'), models.F('end_day'), models.F('end_time'), django.db.models.functions.text.Lower('title'), name='datetimes title'),
),
]

View File

@ -0,0 +1,57 @@
# Generated by Django 4.2.9 on 2024-12-11 19:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agenda_culturel', '0128_event_datetimes_title'),
]
operations = [
migrations.AddIndex(
model_name='batchimportation',
index=models.Index(fields=['created_date'], name='agenda_cult_created_a23990_idx'),
),
migrations.AddIndex(
model_name='batchimportation',
index=models.Index(fields=['status'], name='agenda_cult_status_54b205_idx'),
),
migrations.AddIndex(
model_name='batchimportation',
index=models.Index(fields=['created_date', 'recurrentImport'], name='agenda_cult_created_0296e4_idx'),
),
migrations.AddIndex(
model_name='duplicatedevents',
index=models.Index(fields=['representative'], name='agenda_cult_represe_9a4fa2_idx'),
),
migrations.AddIndex(
model_name='message',
index=models.Index(fields=['related_event'], name='agenda_cult_related_79de3c_idx'),
),
migrations.AddIndex(
model_name='message',
index=models.Index(fields=['user'], name='agenda_cult_user_id_42dc88_idx'),
),
migrations.AddIndex(
model_name='message',
index=models.Index(fields=['date'], name='agenda_cult_date_049c71_idx'),
),
migrations.AddIndex(
model_name='message',
index=models.Index(fields=['spam', 'closed'], name='agenda_cult_spam_22f9b3_idx'),
),
migrations.AddIndex(
model_name='place',
index=models.Index(fields=['name'], name='agenda_cult_name_222846_idx'),
),
migrations.AddIndex(
model_name='place',
index=models.Index(fields=['city'], name='agenda_cult_city_156dc7_idx'),
),
migrations.AddIndex(
model_name='place',
index=models.Index(fields=['location'], name='agenda_cult_locatio_6f3c05_idx'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.9 on 2024-12-22 15:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agenda_culturel', '0129_batchimportation_agenda_cult_created_a23990_idx_and_more'),
]
operations = [
migrations.AddField(
model_name='recurrentimport',
name='forceLocation',
field=models.BooleanField(default=False, help_text='force location even if another is detected.', verbose_name='Force location'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.9 on 2024-12-29 11:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agenda_culturel', '0130_recurrentimport_forcelocation'),
]
operations = [
migrations.AddField(
model_name='message',
name='message_type',
field=models.CharField(choices=[('from_contributor', 'From contributor'), ('import_process', 'Import process'), ('contact_form', 'Contact form'), ('event_report', 'Event report')], default=None, max_length=20, null=True, verbose_name='Type'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.9 on 2024-12-29 16:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agenda_culturel', '0131_message_message_type'),
]
operations = [
migrations.AlterField(
model_name='message',
name='message_type',
field=models.CharField(choices=[('from_contributor', 'From contributor'), ('import_process', 'Import process'), ('contact_form', 'Contact form'), ('event_report', 'Event report'), ('from_contrib_no_msg', 'From contributor (without message)')], default=None, max_length=20, null=True, verbose_name='Type'),
),
]

View File

@ -10,7 +10,16 @@ from colorfield.fields import ColorField
from django_ckeditor_5.fields import CKEditor5Field
from urllib.parse import urlparse
from django.core.cache import cache
from django.core.cache.utils import make_template_fragment_key
from django.contrib.auth.models import User, AnonymousUser
import emoji
from django.core.files.storage import default_storage
from django.contrib.sites.models import Site
from django.core.mail import send_mail
from django.template.loader import render_to_string
import uuid
import hashlib
import urllib.request
@ -20,6 +29,7 @@ from django.utils import timezone
from django.contrib.postgres.search import TrigramSimilarity
from django.db.models import Q, Count, F, Subquery, OuterRef, Func
from django.db.models.functions import Lower
from django.contrib.postgres.lookups import Unaccent
import recurrence.fields
import recurrence
import copy
@ -284,6 +294,10 @@ class DuplicatedEvents(models.Model):
class Meta:
verbose_name = _("Duplicated events")
verbose_name_plural = _("Duplicated events")
indexes = [
models.Index(fields=['representative']),
]
def __init__(self, *args, **kwargs):
self.events = None
@ -398,6 +412,7 @@ class DuplicatedEvents(models.Model):
class ReferenceLocation(models.Model):
name = models.CharField(verbose_name=_("Name"), help_text=_("Name of the location"), unique=True, null=False)
location = LocationField(based_fields=["name"], zoom=12, default=Point(3.08333, 45.783329), srid=4326)
main = models.IntegerField(
@ -405,6 +420,12 @@ class ReferenceLocation(models.Model):
help_text=_("This location is one of the main locations (shown first higher values)."),
default=0,
)
suggested_distance = models.IntegerField(
verbose_name=_("Suggested distance (km)"),
help_text=_("If this distance is given, this location is part of the suggested filters."),
null=True,
default=None
)
class Meta:
verbose_name = _("Reference location")
@ -427,8 +448,9 @@ class Place(models.Model):
blank=True,
null=True,
)
postcode = models.CharField(verbose_name=_("Postcode"), help_text=_("The post code is not displayed, but makes it easier to find an address when you enter it."), blank=True, null=True)
city = models.CharField(verbose_name=_("City"), help_text=_("City name"))
location = LocationField(based_fields=["name", "address", "city"], zoom=12, default=Point(3.08333, 45.783329))
location = LocationField(based_fields=["name", "address", "postcode", "city"], zoom=12, default=Point(3.08333, 45.783329))
description = CKEditor5Field(
verbose_name=_("Description"),
@ -451,6 +473,11 @@ class Place(models.Model):
verbose_name = _("Place")
verbose_name_plural = _("Places")
ordering = ["name"]
indexes = [
models.Index(fields=['name']),
models.Index(fields=['city']),
models.Index(fields=['location']),
]
def __str__(self):
if self.address:
@ -536,7 +563,7 @@ class Organisation(models.Model):
return self.name
def get_absolute_url(self):
return reverse("view_organisation", kwargs={'pk': self.pk})
return reverse("view_organisation", kwargs={'pk': self.pk, "extra": self.name})
@ -551,6 +578,39 @@ class Event(models.Model):
modified_date = models.DateTimeField(blank=True, null=True)
moderated_date = models.DateTimeField(blank=True, null=True)
created_by_user = models.ForeignKey(
User,
verbose_name=_("Author of the event creation"),
null=True,
default=None,
on_delete=models.SET_DEFAULT,
related_name="created_events"
)
imported_by_user = models.ForeignKey(
User,
verbose_name=_("Author of the last importation"),
null=True,
default=None,
on_delete=models.SET_DEFAULT,
related_name="imported_events"
)
modified_by_user = models.ForeignKey(
User,
verbose_name=_("Author of the last modification"),
null=True,
default=None,
on_delete=models.SET_DEFAULT,
related_name="modified_events"
)
moderated_by_user = models.ForeignKey(
User,
verbose_name=_("Author of the last moderation"),
null=True,
default=None,
on_delete=models.SET_DEFAULT,
related_name="moderated_events"
)
recurrence_dtstart = models.DateTimeField(editable=False, blank=True, null=True)
recurrence_dtend = models.DateTimeField(editable=False, blank=True, null=True)
@ -685,6 +745,10 @@ class Event(models.Model):
blank=True,
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.processing_user = None
def get_consolidated_end_day(self, intuitive=True):
if intuitive:
end_day = self.get_consolidated_end_day(False)
@ -702,15 +766,6 @@ class Event(models.Model):
last = self.get_consolidated_end_day()
return [first + timedelta(n) for n in range(int((last - first).days) + 1)]
def get_nb_events_same_dates(self, remove_same_dup=True):
first = self.start_day
last = self.get_consolidated_end_day()
ignore_dup = None
if remove_same_dup:
ignore_dup = self.other_versions
calendar = CalendarList(first, last, exact=True, ignore_dup=ignore_dup)
return [(len(d.events), d.date) for dstr, d in calendar.get_calendar_days().items()]
def is_single_day(self, intuitive=True):
return self.start_day == self.get_consolidated_end_day(intuitive)
@ -720,6 +775,15 @@ class Event(models.Model):
end_date = parse_date(end_date)
return parse_date(self.start_day) + timedelta(days=min_days) < end_date
def set_message(self, msg):
self._message = msg
def get_message(self):
return self._message
def has_message(self):
return hasattr(self, '_message')
def contains_date(self, d, intuitive=True):
return d >= self.start_day and d <= self.get_consolidated_end_day(intuitive)
@ -757,9 +821,36 @@ class Event(models.Model):
permissions = [("set_duplicated_event", "Can set an event as duplicated")]
indexes = [
models.Index(fields=["start_day", "start_time"]),
models.Index("start_time", Lower("title"), name="start_time title")
models.Index(fields=["end_day", "end_time"]),
models.Index(fields=["status"]),
models.Index(fields=["recurrence_dtstart", "recurrence_dtend"]),
models.Index("start_time", Lower("title"), name="start_time title"),
models.Index("start_time", "start_day", "end_day", "end_time", Lower("title"), name="datetimes title")
]
def chronology(self):
c = []
if self.modified_date:
c.append({ "timestamp": self.modified_date, "data": "modified_date", "user": self.modified_by_user, "is_date": True })
if self.moderated_date:
c.append({ "timestamp": self.moderated_date, "data": "moderated_date", "user" : self.moderated_by_user, "is_date": True})
if self.imported_date:
c.append({ "timestamp": self.imported_date, "data": "imported_date", "user": self.imported_by_user, "is_date": True })
if self.created_date:
c.append({ "timestamp": self.created_date + timedelta(milliseconds=-1), "data": "created_date", "user": self.created_by_user, "is_date": True})
c += [{ "timestamp": m.date, "data": m, "user": m.user, "is_date": False} for m in self.message_set.filter(spam=False)]
if self.other_versions:
for o in self.other_versions.get_duplicated():
if o != self:
c += [{ "timestamp": m.date, "data": m, "user": m.user, "is_date": False} for m in o.message_set.filter(spam=False)]
c.sort(key=lambda x: x["timestamp"])
return c
def sorted_tags(self):
if self.tags is None:
return []
@ -851,19 +942,27 @@ class Event(models.Model):
def is_representative(self):
return self.other_versions is None or self.other_versions.representative == self
def download_missing_image(self):
if self.local_image and not default_storage.exists(self.local_image.name):
self.download_image()
self.save(update_fields=["local_image"])
def download_image(self):
# first download file
a = urlparse(self.image)
basename = os.path.basename(a.path)
ext = basename.split('.')[-1]
filename = "%s.%s" % (uuid.uuid4(), ext)
try:
tmpfile, _ = urllib.request.urlretrieve(self.image)
except:
return None
# if the download is ok, then create the corresponding file object
self.local_image = File(name=basename, file=open(tmpfile, "rb"))
self.local_image = File(name=filename, file=open(tmpfile, "rb"))
def add_pending_organisers(self, organisers):
self.pending_organisers = organisers
@ -889,6 +988,12 @@ class Event(models.Model):
def set_no_modification_date_changed(self):
self.no_modification_date_changed = True
def set_processing_user(self, user):
if user is None or user.is_anonymous:
self.processing_user = None
else:
self.processing_user = user
def set_in_moderation_process(self):
self.in_moderation_process = True
@ -899,12 +1004,16 @@ class Event(models.Model):
now = timezone.now()
if not self.id:
self.created_date = now
self.created_by_user = self.processing_user
if self.is_in_importation_process():
self.imported_date = now
self.imported_by_user = self.processing_user
if self.modified_date is None or not self.is_no_modification_date_changed():
self.modified_date = now
self.modified_by_user = self.processing_user
if self.is_in_moderation_process():
self.moderated_date = now
self.moderated_by_user = self.processing_user
def get_recurrence_at_date(self, year, month, day):
dtstart = timezone.make_aware(
@ -916,10 +1025,13 @@ class Event(models.Model):
else:
return recurrences[0]
def get_image_url(self):
def get_image_url(self, request=None):
if self.local_image and hasattr(self.local_image, "url"):
try:
return self.local_image.url
if request:
return request.build_absolute_uri(self.local_image.url)
else:
return self.local_image.url
except:
pass
if self.image:
@ -1017,7 +1129,7 @@ class Event(models.Model):
self.update_recurrence_dtstartend()
# if the image is defined but not locally downloaded
if self.image and not self.local_image:
if self.image and (not self.local_image or not default_storage.exists(self.local_image.name)):
self.download_image()
# remove "/" from tags
@ -1036,6 +1148,44 @@ class Event(models.Model):
if not self.category or self.category.name == Category.default_name:
CategorisationRule.apply_rules(self)
def get_contributor_message(self):
types = [Message.TYPE.FROM_CONTRIBUTOR, Message.TYPE.FROM_CONTRIBUTOR_NO_MSG]
if self.other_versions is None or self.other_versions.representative is None:
logger.warning("cas single")
return Message.objects.filter(related_event=self.pk, message_type__in=types, closed=False)
else:
logger.warning("cas multiple")
return Message.objects.filter(related_event__in=self.other_versions.get_duplicated(), message_type__in=types, closed=False).first()
def notify_if_required(self, request):
notif = False
if self.status != Event.STATUS.DRAFT:
messages = self.get_contributor_message()
logger.warning("messages: ")
logger.warning(messages)
if messages:
for message in messages:
if message and not message.closed and message.email:
# send email
context = {"sitename": Site.objects.get_current(request).name, 'event_title': self.title }
if self.status == Event.STATUS.PUBLISHED:
context["url"] = request.build_absolute_uri(self.get_absolute_url())
subject = _('Your event has been published')
body = render_to_string("agenda_culturel/emails/published.txt", context)
else:
subject = _('Your message has not been retained')
body = render_to_string("agenda_culturel/emails/retained.txt", context)
send_mail(subject, body, None, [message.email])
message.closed = True
message.save()
notif = True
return notif
def save(self, *args, **kwargs):
self.prepare_save()
@ -1069,6 +1219,21 @@ class Event(models.Model):
# first save the current object
super().save(*args, **kwargs)
# notify only if required (and request is known)
if "request" in kwargs:
self.notify_if_required(kwargs.get("request"))
# clear cache
for is_auth in [False, True]:
key = make_template_fragment_key("event_body", [is_auth, self])
cache.delete(key)
# save message if required
if self.has_message():
msg = self.get_message()
msg.related_event = self
msg.save()
# then if its a clone, update the representative
if clone:
self.other_versions.representative = self
@ -1085,6 +1250,8 @@ class Event(models.Model):
def from_structure(event_structure, import_source=None):
# organisers is a manytomany relation thus cannot be initialised before creation of the event
organisers = event_structure.pop('organisers', None)
email = event_structure.pop('email', None)
comments = event_structure.pop('comments', None)
if "category" in event_structure and event_structure["category"] is not None:
try:
@ -1114,8 +1281,6 @@ class Event(models.Model):
and event_structure["last_modified"] is not None
):
d = datetime.fromisoformat(event_structure["last_modified"])
if d.year == 2024 and d.month > 2:
logger.warning("last modified {}".format(d))
if d.tzinfo is None or d.tzinfo.utcoffset(d) is None:
d = timezone.make_aware(d, timezone.get_default_timezone())
event_structure["modified_date"] = d
@ -1163,7 +1328,13 @@ class Event(models.Model):
result = Event(**event_structure)
result.add_pending_organisers(organisers)
if email or comments:
has_comments = not comments in ["", None]
result.set_message(Message(subject=_('during import process'),
email=email,
message=comments,
closed=False,
message_type=Message.TYPE.FROM_CONTRIBUTOR if has_comments else Message.TYPE.FROM_CONTRIBUTOR_NO_MSG))
return result
@ -1315,9 +1486,10 @@ class Event(models.Model):
# otherwise merge existing groups
group = DuplicatedEvents.merge_groups(groups)
group.save()
if force_non_fixed:
group.representative = None
group.save()
# set the possibly duplicated group for the current object
self.other_versions = group
@ -1338,6 +1510,8 @@ class Event(models.Model):
"category",
"tags",
]
if not no_m2m:
result += ["organisers"]
result += [
"title",
@ -1349,8 +1523,6 @@ class Event(models.Model):
"description",
"image",
]
if not no_m2m:
result += ["organisers"]
if all and local_img:
result += ["local_image"]
if all and exact_location:
@ -1397,12 +1569,19 @@ class Event(models.Model):
self.uuids.append(uuid)
# add possible missing sources
for source in other.import_sources:
if source not in self.import_sources:
self.import_sources.append(source)
if other.import_sources:
if not self.import_sources:
self.import_sources = []
for source in other.import_sources:
if source not in self.import_sources:
self.import_sources.append(source)
# Limitation: the given events should not be considered similar one to another...
def import_events(events, remove_missing_from_source=None):
def import_events(events, remove_missing_from_source=None, user_id=None):
user = None
if user_id:
user = User.objects.filter(pk=user_id).first()
to_import = []
to_update = []
@ -1429,6 +1608,7 @@ class Event(models.Model):
# imported events should be updated
event.set_in_importation_process()
event.set_processing_user(user)
event.prepare_save()
# check if the event has already be imported (using uuid)
@ -1455,9 +1635,14 @@ class Event(models.Model):
same_imported.other_versions.representative = None
same_imported.other_versions.save()
# we only update local information if it's a pure import and has no moderated_date
new_image = same_imported.image != event.image
same_imported.update(event, pure and same_imported.moderated_date is None)
same_imported.set_in_importation_process()
same_imported.prepare_save()
# fix missing or updated files
if same_imported.local_image and (not default_storage.exists(same_imported.local_image.name) or new_image):
same_imported.download_image()
same_imported.save(update_fields=["local_image"])
to_update.append(same_imported)
else:
# otherwise, the new event possibly a duplication of the remaining others.
@ -1485,13 +1670,23 @@ class Event(models.Model):
for e in to_import:
if e.is_event_long_duration():
e.status = Event.STATUS.DRAFT
e.set_message(
Message(subject=_('Import'),
name=_('import process'),
message=_("The duration of the event is a little too long for direct publication. Moderators can choose to publish it or not."),
message_type=Message.TYPE.IMPORT_PROCESS)
)
# then import all the new events
imported = Event.objects.bulk_create(to_import)
# update organisers (m2m relation)
for i, ti in zip(imported, to_import):
if ti.has_pending_organisers():
if ti.has_pending_organisers() and ti.pending_organisers is not None:
i.organisers.set(ti.pending_organisers)
if ti.has_message():
msg = ti.get_message()
msg.related_event = i
msg.save()
nb_updated = Event.objects.bulk_update(
to_update,
@ -1565,13 +1760,12 @@ class Event(models.Model):
def get_concurrent_events(self, remove_same_dup=True):
day = self.current_date if hasattr(self, "current_date") else self.start_day
day_events = CalendarDay(self.start_day).get_events()
day_events = CalendarDay(day, qs = Event.objects.filter(status=Event.STATUS.PUBLISHED)).get_events()
return [
e
for e in day_events
if e != self
and self.is_concurrent_event(e, day)
and e.status == Event.STATUS.PUBLISHED
and (e.other_versions is None or e.other_versions != self.other_versions)
]
@ -1581,10 +1775,10 @@ class Event(models.Model):
return (dtstart <= e_dtstart <= dtend) or (e_dtstart <= dtstart <= e_dtend)
def export_to_ics(events):
def export_to_ics(events, request):
cal = icalCal()
# Some properties are required to be compliant
cal.add("prodid", "-//My calendar product//example.com//")
cal.add("prodid", "-//Pommes de lune//pommesdelune.fr//")
cal.add("version", "2.0")
for event in events:
@ -1635,9 +1829,12 @@ class Event(models.Model):
eventIcal.add("summary", event.title)
eventIcal.add("name", event.title)
url = ("\n" + event.reference_urls[0]) if event.reference_urls and len(event.reference_urls) > 0 else ""
description = event.description if event.description else ""
eventIcal.add(
"description", event.description + url
"description", description + url
)
if not event.local_image is None and event.local_image != "":
eventIcal.add('image', request.build_absolute_uri(event.local_image), parameters={'VALUE': 'URI'})
eventIcal.add("location", event.exact_location or event.location)
cal.add_component(eventIcal)
@ -1673,16 +1870,50 @@ class Event(models.Model):
return [Event.get_count_modification(w) for w in when_list]
class ContactMessage(models.Model):
class Message(models.Model):
class TYPE(models.TextChoices):
FROM_CONTRIBUTOR = "from_contributor", _("From contributor")
IMPORT_PROCESS = "import_process", _("Import process")
CONTACT_FORM = "contact_form", _("Contact form")
EVENT_REPORT = "event_report", _("Event report")
FROM_CONTRIBUTOR_NO_MSG = "from_contrib_no_msg", _("From contributor (without message)")
class Meta:
verbose_name = _("Contact message")
verbose_name_plural = _("Contact messages")
verbose_name = _("Message")
verbose_name_plural = _("Messages")
indexes = [
models.Index(fields=['related_event']),
models.Index(fields=['user']),
models.Index(fields=['date']),
models.Index(fields=['spam', 'closed']),
]
subject = models.CharField(
verbose_name=_("Subject"),
help_text=_("The subject of your message"),
max_length=512,
)
related_event = models.ForeignKey(
Event,
verbose_name=_("Related event"),
help_text=_("The message is associated with this event."),
null=True,
default=None,
on_delete=models.SET_DEFAULT,
)
user = models.ForeignKey(
User,
verbose_name=_("Author of the message"),
null=True,
default=None,
on_delete=models.SET_DEFAULT,
)
name = models.CharField(
verbose_name=_("Name"),
help_text=_("Your name"),
@ -1722,8 +1953,18 @@ class ContactMessage(models.Model):
null=True,
)
def nb_open_contactmessages():
return ContactMessage.objects.filter(closed=False).count()
message_type = models.CharField(
verbose_name=_("Type"),
max_length=20,
choices=TYPE.choices,
default=None, null=True
)
def nb_open_messages():
return Message.objects.filter(Q(closed=False)&Q(spam=False)&Q(message_type__in=[Message.TYPE.CONTACT_FORM, Message.TYPE.EVENT_REPORT, Message.TYPE.FROM_CONTRIBUTOR])).count()
def get_absolute_url(self):
return reverse("message", kwargs={"pk": self.pk})
class RecurrentImport(models.Model):
@ -1744,6 +1985,7 @@ class RecurrentImport(models.Model):
FBEVENTS = "Facebook events", _("Événements d'une page FB")
C3C = "cour3coquins", _("la cour des 3 coquins")
ARACHNEE = "arachnee", _("Arachnée concert")
LERIO = "rio", _('Le Rio')
class DOWNLOADER(models.TextChoices):
SIMPLE = "simple", _("simple")
@ -1811,6 +2053,12 @@ class RecurrentImport(models.Model):
blank=True,
)
forceLocation = models.BooleanField(
verbose_name=_("Force location"),
help_text=_("force location even if another is detected."),
default=False
)
defaultOrganiser = models.ForeignKey(
Organisation,
verbose_name=_("Organiser"),
@ -1871,6 +2119,11 @@ class BatchImportation(models.Model):
verbose_name = _("Batch importation")
verbose_name_plural = _("Batch importations")
permissions = [("run_batchimportation", "Can run a batch importation")]
indexes = [
models.Index(fields=['created_date']),
models.Index(fields=['status']),
models.Index(fields=['created_date', 'recurrentImport']),
]
created_date = models.DateTimeField(auto_now_add=True)

View File

@ -56,9 +56,10 @@ INSTALLED_APPS = [
"robots",
"debug_toolbar",
"cache_cleaner",
"honeypot",
]
SITE_ID = 1
HONEYPOT_FIELD_NAME = "alias_name"
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
@ -72,6 +73,7 @@ MIDDLEWARE = [
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"debug_toolbar.middleware.DebugToolbarMiddleware",
'django.contrib.sites.middleware.CurrentSiteMiddleware',
# "django.middleware.cache.UpdateCacheMiddleware",
# "django.middleware.common.CommonMiddleware",
# "django.middleware.cache.FetchFromCacheMiddleware",
@ -145,7 +147,7 @@ TIME_ZONE = "Europe/Paris"
USE_I18N = True
USE_TZ = True
USE_TZ = False
LANGUAGES = (
("fr", _("French")),
@ -248,6 +250,7 @@ LOCATION_FIELD = {
# stop robots
ROBOTS_USE_SITEMAP = False
ROBOTS_SITE_BY_REQUEST = 'cached-sitemap'
# debug
if DEBUG:

View File

@ -0,0 +1,13 @@
from django.contrib import sitemaps
from django.urls import reverse
class StaticViewSitemap(sitemaps.Sitemap):
priority = 0.5
changefreq = "daily"
def items(self):
return ["home", "cette_semaine", "ce_mois_ci", "aujourdhui", "a_venir", "about", "contact"]
def location(self, item):
return reverse(item)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 237 KiB

View File

@ -34,11 +34,17 @@ const openModal = (modal, back=true) => {
}
setTimeout(function() {
visibleModal = modal;
}, 500);
console.log("ici");
const mask = visibleModal.querySelector(".h-mask");
mask.classList.add("visible");
}, 350);
};
const hideModal = (modal) => {
if (modal != null) {
const mask = visibleModal.querySelector(".h-mask");
mask.classList.remove("visible");
visibleModal = null;
document.documentElement.style.removeProperty("--scrollbar-width");
modal.removeAttribute("open");

View File

@ -0,0 +1,673 @@
var SequentialLoader = function() {
var SL = {
loadJS: function(src, onload) {
//console.log(src);
// add to pending list
this._load_pending.push({'src': src, 'onload': onload});
// check if not already loading
if ( ! this._loading) {
this._loading = true;
// load first
this.loadNextJS();
}
},
loadNextJS: function() {
// get next
var next = this._load_pending.shift();
if (next == undefined) {
// nothing to load
this._loading = false;
return;
}
// check not loaded
if (this._load_cache[next.src] != undefined) {
next.onload();
this.loadNextJS();
return; // already loaded
}
else {
this._load_cache[next.src] = 1;
}
// load
var el = document.createElement('script');
el.type = 'application/javascript';
el.src = next.src;
// onload callback
var self = this;
el.onload = function(){
//console.log('Loaded: ' + next.src);
// trigger onload
next.onload();
// try to load next
self.loadNextJS();
};
document.body.appendChild(el);
},
_loading: false,
_load_pending: [],
_load_cache: {}
};
return {
loadJS: SL.loadJS.bind(SL)
}
};
!function($){
var LocationFieldCache = {
load: [],
onload: {},
isLoading: false
};
var LocationFieldResourceLoader;
$.locationField = function(options) {
var LocationField = {
options: $.extend({
provider: 'google',
providerOptions: {
google: {
api: '//maps.google.com/maps/api/js',
mapType: 'ROADMAP'
}
},
searchProvider: 'google',
id: 'map',
latLng: '0,0',
mapOptions: {
zoom: 9
},
basedFields: $(),
inputField: $(),
suffix: '',
path: '',
fixMarker: true
}, options),
providers: /google|openstreetmap|mapbox/,
searchProviders: /google|yandex|nominatim|addok/,
render: function() {
this.$id = $('#' + this.options.id);
if ( ! this.providers.test(this.options.provider)) {
this.error('render failed, invalid map provider: ' + this.options.provider);
return;
}
if ( ! this.searchProviders.test(this.options.searchProvider)) {
this.error('render failed, invalid search provider: ' + this.options.searchProvider);
return;
}
var self = this;
this.loadAll(function(){
var mapOptions = self._getMapOptions(),
map = self._getMap(mapOptions);
var marker = self._getMarker(map, mapOptions.center);
// fix issue w/ marker not appearing
if (self.options.provider == 'google' && self.options.fixMarker)
self.__fixMarker();
// watch based fields
self._watchBasedFields(map, marker);
});
},
fill: function(latLng) {
this.options.inputField.val(latLng.lat + ',' + latLng.lng);
},
search: function(map, marker, address) {
if (this.options.searchProvider === 'google') {
var provider = new GeoSearch.GoogleProvider({ apiKey: this.options.providerOptions.google.apiKey });
provider.search({query: address}).then(data => {
if (data.length > 0) {
var result = data[0],
latLng = new L.LatLng(result.y, result.x);
marker.setLatLng(latLng);
map.panTo(latLng);
}
});
}
else if (this.options.searchProvider === 'yandex') {
// https://yandex.com/dev/maps/geocoder/doc/desc/concepts/input_params.html
var url = 'https://geocode-maps.yandex.ru/1.x/?format=json&geocode=' + address;
if (typeof this.options.providerOptions.yandex.apiKey !== 'undefined') {
url += '&apikey=' + this.options.providerOptions.yandex.apiKey;
}
var request = new XMLHttpRequest();
request.open('GET', url, true);
request.onload = function () {
if (request.status >= 200 && request.status < 400) {
var data = JSON.parse(request.responseText);
var pos = data.response.GeoObjectCollection.featureMember[0].GeoObject.Point.pos.split(' ');
var latLng = new L.LatLng(pos[1], pos[0]);
marker.setLatLng(latLng);
map.panTo(latLng);
} else {
console.error('Yandex geocoder error response');
}
};
request.onerror = function () {
console.error('Check connection to Yandex geocoder');
};
request.send();
}
else if (this.options.searchProvider === 'addok') {
var url = 'https://api-adresse.data.gouv.fr/search/?limit=1&q=' + address;
var request = new XMLHttpRequest();
request.open('GET', url, true);
request.onload = function () {
if (request.status >= 200 && request.status < 400) {
var data = JSON.parse(request.responseText);
var pos = data.features[0].geometry.coordinates;
var latLng = new L.LatLng(pos[1], pos[0]);
marker.setLatLng(latLng);
map.panTo(latLng);
} else {
console.error('Addok geocoder error response');
}
};
request.onerror = function () {
console.error('Check connection to Addok geocoder');
};
request.send();
}
else if (this.options.searchProvider === 'nominatim') {
var url = '//nominatim.openstreetmap.org/search?format=json&q=' + address;
var request = new XMLHttpRequest();
request.open('GET', url, true);
request.onload = function () {
if (request.status >= 200 && request.status < 400) {
var data = JSON.parse(request.responseText);
if (data.length > 0) {
var pos = data[0];
var latLng = new L.LatLng(pos.lat, pos.lon);
marker.setLatLng(latLng);
map.panTo(latLng);
} else {
console.error(address + ': not found via Nominatim');
}
} else {
console.error('Nominatim geocoder error response');
}
};
request.onerror = function () {
console.error('Check connection to Nominatim geocoder');
};
request.send();
}
},
loadAll: function(onload) {
this.$id.html('Loading...');
// resource loader
if (LocationFieldResourceLoader == undefined)
LocationFieldResourceLoader = SequentialLoader();
this.load.loader = LocationFieldResourceLoader;
this.load.path = this.options.path;
var self = this;
this.load.common(function(){
var mapProvider = self.options.provider,
onLoadMapProvider = function() {
var searchProvider = self.options.searchProvider + 'SearchProvider',
onLoadSearchProvider = function() {
self.$id.html('');
onload();
};
if (self.load[searchProvider] != undefined) {
self.load[searchProvider](self.options.providerOptions[self.options.searchProvider] || {}, onLoadSearchProvider);
}
else {
onLoadSearchProvider();
}
};
if (self.load[mapProvider] != undefined) {
self.load[mapProvider](self.options.providerOptions[mapProvider] || {}, onLoadMapProvider);
}
else {
onLoadMapProvider();
}
});
},
load: {
google: function(options, onload) {
var js = [
this.path + '/@googlemaps/js-api-loader/index.min.js',
this.path + '/Leaflet.GoogleMutant.js',
];
this._loadJSList(js, function(){
const loader = new google.maps.plugins.loader.Loader({
apiKey: options.apiKey,
version: "weekly",
});
loader.load().then(() => onload());
});
},
googleSearchProvider: function(options, onload) {
onload();
//var url = options.api;
//if (typeof options.apiKey !== 'undefined') {
// url += url.indexOf('?') === -1 ? '?' : '&';
// url += 'key=' + options.apiKey;
//}
//var js = [
// url,
// this.path + '/l.geosearch.provider.google.js'
// ];
//this._loadJSList(js, function(){
// // https://github.com/smeijer/L.GeoSearch/issues/57#issuecomment-148393974
// L.GeoSearch.Provider.Google.Geocoder = new google.maps.Geocoder();
// onload();
//});
},
yandexSearchProvider: function (options, onload) {
onload();
},
mapbox: function(options, onload) {
onload();
},
openstreetmap: function(options, onload) {
onload();
},
common: function(onload) {
var self = this,
js = [
// map providers
this.path + '/leaflet/leaflet.js',
// search providers
this.path + '/leaflet-geosearch/geosearch.umd.js',
],
css = [
// map providers
this.path + '/leaflet/leaflet.css'
];
// Leaflet docs note:
// Include Leaflet JavaScript file *after* Leaflets CSS
// https://leafletjs.com/examples/quick-start/
this._loadCSSList(css, function(){
self._loadJSList(js, onload);
});
},
_loadJS: function(src, onload) {
this.loader.loadJS(src, onload);
},
_loadJSList: function(srclist, onload) {
this.__loadList(this._loadJS, srclist, onload);
},
_loadCSS: function(src, onload) {
if (LocationFieldCache.onload[src] != undefined) {
onload();
}
else {
LocationFieldCache.onload[src] = 1;
onloadCSS(loadCSS(src), onload);
}
},
_loadCSSList: function(srclist, onload) {
this.__loadList(this._loadCSS, srclist, onload);
},
__loadList: function(fn, srclist, onload) {
if (srclist.length > 1) {
for (var i = 0; i < srclist.length-1; ++i) {
fn.call(this, srclist[i], function(){});
}
}
fn.call(this, srclist[srclist.length-1], onload);
}
},
error: function(message) {
console.log(message);
this.$id.html(message);
},
_getMap: function(mapOptions) {
var map = new L.Map(this.options.id, mapOptions), layer;
if (this.options.provider == 'google') {
layer = new L.gridLayer.googleMutant({
type: this.options.providerOptions.google.mapType.toLowerCase(),
});
}
else if (this.options.provider == 'openstreetmap') {
layer = new L.tileLayer(
'//{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 18
});
}
else if (this.options.provider == 'mapbox') {
layer = new L.tileLayer(
'https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token={accessToken}', {
maxZoom: 18,
accessToken: this.options.providerOptions.mapbox.access_token,
id: 'mapbox/streets-v11'
});
}
map.addLayer(layer);
return map;
},
_getMapOptions: function() {
return $.extend(this.options.mapOptions, {
center: this._getLatLng()
});
},
_getLatLng: function() {
var l = this.options.latLng.split(',').map(parseFloat);
return new L.LatLng(l[0], l[1]);
},
_getMarker: function(map, center) {
var self = this,
markerOptions = {
draggable: true
};
var marker = L.marker(center, markerOptions).addTo(map);
marker.on('dragstart', function(){
if (self.options.inputField.is('[readonly]'))
marker.dragging.disable();
else
marker.dragging.enable();
});
// fill input on dragend
marker.on('dragend move', function(){
if (!self.options.inputField.is('[readonly]'))
self.fill(this.getLatLng());
});
// place marker on map click
map.on('click', function(e){
if (!self.options.inputField.is('[readonly]')) {
marker.setLatLng(e.latlng);
marker.dragging.enable();
}
});
return marker;
},
_watchBasedFields: function(map, marker) {
var self = this,
basedFields = this.options.basedFields,
onchangeTimer,
onchange = function() {
if (!self.options.inputField.is('[readonly]')) {
var values = basedFields.map(function() {
var value = $(this).val();
return value === '' ? null : value;
});
var address = values.toArray().join(', ');
clearTimeout(onchangeTimer);
onchangeTimer = setTimeout(function(){
self.search(map, marker, address);
}, 300);
}
};
basedFields.each(function(){
var el = $(this);
if (el.is('select'))
el.change(onchange);
else
el.keyup(onchange);
});
if (this.options.inputField.val() === '') {
var values = basedFields.map(function() {
var value = $(this).val();
return value === '' ? null : value;
});
var address = values.toArray().join(', ');
if (address !== '')
onchange();
}
},
__fixMarker: function() {
$('.leaflet-map-pane').css('z-index', '2 !important');
$('.leaflet-google-layer').css('z-index', '1 !important');
}
}
return {
render: LocationField.render.bind(LocationField)
}
}
function dataLocationFieldObserver(callback) {
function _findAndEnableDataLocationFields() {
var dataLocationFields = $('input[data-location-field-options]');
dataLocationFields
.filter(':not([data-location-field-observed])')
.attr('data-location-field-observed', true)
.each(callback);
}
var observer = new MutationObserver(function(mutations){
_findAndEnableDataLocationFields();
});
var container = document.documentElement || document.body;
$(container).ready(function(){
_findAndEnableDataLocationFields();
});
observer.observe(container, {attributes: true});
}
dataLocationFieldObserver(function(){
var el = $(this);
var name = el.attr('name'),
options = el.data('location-field-options'),
basedFields = options.field_options.based_fields,
pluginOptions = {
id: 'map_' + name,
inputField: el,
latLng: el.val() || '0,0',
suffix: options['search.suffix'],
path: options['resources.root_path'],
provider: options['map.provider'],
searchProvider: options['search.provider'],
providerOptions: {
google: {
api: options['provider.google.api'],
apiKey: options['provider.google.api_key'],
mapType: options['provider.google.map_type']
},
mapbox: {
access_token: options['provider.mapbox.access_token']
},
yandex: {
apiKey: options['provider.yandex.api_key']
},
},
mapOptions: {
zoom: options['map.zoom']
}
};
// prefix
var prefixNumber;
try {
prefixNumber = name.match(/-(\d+)-/)[1];
} catch (e) {}
if (options.field_options.prefix) {
var prefix = options.field_options.prefix;
if (prefixNumber != null) {
prefix = prefix.replace(/__prefix__/, prefixNumber);
}
basedFields = basedFields.map(function(n){
return prefix + n
});
}
// based fields
pluginOptions.basedFields = $(basedFields.map(function(n){
return '#id_' + n
}).join(','));
// render
$.locationField(pluginOptions).render();
});
}(jQuery || django.jQuery);
/*!
loadCSS: load a CSS file asynchronously.
[c]2015 @scottjehl, Filament Group, Inc.
Licensed MIT
*/
(function(w){
"use strict";
/* exported loadCSS */
var loadCSS = function( href, before, media ){
// Arguments explained:
// `href` [REQUIRED] is the URL for your CSS file.
// `before` [OPTIONAL] is the element the script should use as a reference for injecting our stylesheet <link> before
// By default, loadCSS attempts to inject the link after the last stylesheet or script in the DOM. However, you might desire a more specific location in your document.
// `media` [OPTIONAL] is the media type or query of the stylesheet. By default it will be 'all'
var doc = w.document;
var ss = doc.createElement( "link" );
var ref;
if( before ){
ref = before;
}
else {
var refs = ( doc.body || doc.getElementsByTagName( "head" )[ 0 ] ).childNodes;
ref = refs[ refs.length - 1];
}
var sheets = doc.styleSheets;
ss.rel = "stylesheet";
ss.href = href;
// temporarily set media to something inapplicable to ensure it'll fetch without blocking render
ss.media = "only x";
// Inject link
// Note: the ternary preserves the existing behavior of "before" argument, but we could choose to change the argument to "after" in a later release and standardize on ref.nextSibling for all refs
// Note: `insertBefore` is used instead of `appendChild`, for safety re: http://www.paulirish.com/2011/surefire-dom-element-insertion/
ref.parentNode.insertBefore( ss, ( before ? ref : ref.nextSibling ) );
// A method (exposed on return object for external use) that mimics onload by polling until document.styleSheets until it includes the new sheet.
var onloadcssdefined = function( cb ){
var resolvedHref = ss.href;
var i = sheets.length;
while( i-- ){
if( sheets[ i ].href === resolvedHref ){
return cb();
}
}
setTimeout(function() {
onloadcssdefined( cb );
});
};
// once loaded, set link's media back to `all` so that the stylesheet applies once it loads
ss.onloadcssdefined = onloadcssdefined;
onloadcssdefined(function() {
ss.media = media || "all";
});
return ss;
};
// commonjs
if( typeof module !== "undefined" ){
module.exports = loadCSS;
}
else {
w.loadCSS = loadCSS;
}
}( typeof global !== "undefined" ? global : this ));
/*!
onloadCSS: adds onload support for asynchronous stylesheets loaded with loadCSS.
[c]2014 @zachleat, Filament Group, Inc.
Licensed MIT
*/
/* global navigator */
/* exported onloadCSS */
function onloadCSS( ss, callback ) {
ss.onload = function() {
ss.onload = null;
if( callback ) {
callback.call( ss );
}
};
// This code is for browsers that dont support onload, any browser that
// supports onload should use that instead.
// No support for onload:
// * Android 4.3 (Samsung Galaxy S4, Browserstack)
// * Android 4.2 Browser (Samsung Galaxy SIII Mini GT-I8200L)
// * Android 2.3 (Pantech Burst P9070)
// Weak inference targets Android < 4.4
if( "isApplicationInstalled" in navigator && "onloadcssdefined" in ss ) {
ss.onloadcssdefined( callback );
}
}

View File

@ -44,6 +44,9 @@ $enable-responsive-typography: true;
// Modal (<dialog>)
--modal-overlay-backdrop-filter: blur(0.05rem);
--background-color-transparent: color-mix(in srgb, var(--background-color), transparent 30%);
--background-color-transparent-light: color-mix(in srgb, var(--background-color), transparent 80%);
}
@ -147,7 +150,7 @@ details[role="list"] summary + ul li.selected>a:hover {
}
}
.suggested-tags {
.suggestions {
font-size: 80%;
}
}
@ -201,10 +204,16 @@ details[role="list"] summary + ul li.selected>a:hover {
text-align: left;
}
.suggested-tags .small-cat {
.suggestions .small-cat {
overflow: visible;
}
.small-location {
@extend .small-cat;
border-color: var(--contrast);
color: var(--contrast);
}
.circ-cat.circ-large {
height: 2.6em;
width: 2.6em;
@ -289,15 +298,7 @@ svg {
width: 100%;
padding: 0.3em;
margin: 0 0 0.5em 0;
}
@media only screen and (min-width: 550px) {
.illustration {
width: 40%;
float: right;
margin: 0 0 0.5em .5em;
}
float: right;
}
@media only screen and (min-width: 992px) {
@ -323,6 +324,7 @@ footer [data-tooltip] {
scroll-behavior: smooth;
transition-duration: 200ms;
.cat {
margin-right: 0;
}
@ -495,6 +497,15 @@ body > main {
padding-top: 0.2em;
}
body.authenticated > main {
padding-top: 0.8em;
}
@media only screen and (min-width: 700px) {
body.authenticated > main {
padding-top: 0.2em;
}
}
article {
margin: 1em 0;
}
@ -603,7 +614,7 @@ header .remarque {
}
.form.recent, .form.main-filter, .search .form {
#id_status>div {
#id_status>div, #id_representative>div {
display: inline-block;
margin-right: 2em;
}
@ -851,7 +862,7 @@ nav>div {
}
@media only screen and (min-width: 992px) {
@media only screen and (min-width: 1400px) {
.header li {
float: left;
}
@ -893,6 +904,39 @@ nav>div {
color: var(--secondary-inverse);
}
#badges {
position: absolute;
font-size: 70%;
top:3.5em;
left: 0;
padding: 0.2em .5em 0.2em 0.2em;
background: var(--card-sectionning-background-color);
display: inline-block;
border-radius: 0 var(--border-radius) var(--border-radius) 0;
.badge {
margin: 0;
border-radius: 0;
}
.link {
margin-left: .5em;
}
}
@media only screen and (min-width: 700px) {
#badges {
border-radius: 0 0 var(--border-radius) var(--border-radius);
left: 50%;
top: 0;
transform: translate(-50%, 0);
padding: 0 .5em .2em .5em;
z-index: 1000;
}
}
.tw-badge {
background: black;
border-color: black;
@ -1400,17 +1444,28 @@ img.preview {
top: 0;
z-index: 10;
}
@media only screen and (min-width: 600px) {
.single-event, .tag-description {
display: grid;
grid-template-columns: 60% auto;
grid-column-gap: 1em;
}
header {
grid-column: 1 / 3;
}
}
@media only screen and (min-width: 992px) {
.resume {
column-count: 4;
}
.single-event, .tag-description {
display: grid;
grid-template-columns: 30% auto 14em;
grid-column-gap: 1em;
header {
margin: 0;
grid-column: 1 / 2;
}
.illustration {
width: auto;
@ -1428,26 +1483,50 @@ img.preview {
}
}
form.messages div, form.moderation-events {
@media only screen and (min-width: 992px) {
display: grid;
grid-template-columns: repeat(2, 50%);
}
fieldset {
.header-complement {
float: none;
}
@media only screen and (min-width: 992px) {
.header-complement {
float: left;
margin-right: 1em;
}
label {
clear: both;
float: left;
}
}
form.messages {
div {
width: 100%;
display: block;
fieldset div {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
margin-right: 1em;
}
}
@media only screen and (min-width: 800px) {
display: grid;
grid-template-columns: repeat(3, 1fr);
:last-child {
grid-column: 1 / 4;
}
div fieldset div {
display: block;
}
}
}
.moderate-preview .event-body {
max-height: 400px;
overflow-y: auto;
}
#moderate-form #id_status {
display: grid;
grid-template-columns: repeat(3, 1fr);
}
label.required::after {
content: ' *';
color: red;
@ -1493,6 +1572,234 @@ label.required::after {
}
}
.maskable_group .body_group.closed {
display: none;
.maskable_group {
margin: 0.5em 0;
.body_group.closed {
display: none;
}
}
.form-place {
display: grid;
grid-template-columns: repeat(1, 1fr);
row-gap: .5em;
margin-bottom: 0.5em;
.map-widget {
grid-row: 3;
}
#group_address .body_group {
display: grid;
grid-template-columns: repear(2, 1fr);
column-gap: .5em;
#div_id_address, #div_id_location {
grid-column: 1 / 3;
}
}
}
@media only screen and (min-width: 992px) {
.form-place {
grid-template-columns: repeat(2, 1fr);
.map-widget {
grid-column: 2 / 3;
grid-row: 1 / 3;
}
#group_other {
grid-column: 1 / 3;
}
}
}
.line-now {
font-size: 60%;
div {
display: grid;
grid-template-columns: fit-content(2em) auto;
column-gap: .2em;
color: red;
.line {
margin-top: .7em;
border-top: 1px solid red;
}
}
margin-bottom: 0;
list-style: none;
}
.a-venir .line-now {
margin-left: -2em;
}
#chronology {
.entree {
display: grid;
grid-template-columns: fit-content(2em) auto;
column-gap: .7em;
.texte {
background: var(--background-color);
padding: 0.1em 0.8em;
border-radius: var(--border-radius);
p {
font-size: 100%;
}
p:last-child {
margin-bottom: 0.1em;
}
}
}
font-size: 85%;
footer {
margin-top: 1.8em;
padding: 0.2em .8em;
}
.ts {
@extend .badge-small;
border-radius: var(--border-radius);
display: inline-block;
width: 14em;
margin-right: 1.2em;
}
}
.moderation_heatmap {
overflow-x: auto;
table {
max-width: 600px;
margin: auto;
.total, .month {
display: none;
}
.label {
display: none;
}
th {
font-size: 90%;
text-align: center;
}
td {
font-size: 80%;
text-align: center;
}
tbody th {
text-align: right;
}
.ratio {
padding: 0.1em;
a, .a {
margin: auto;
border-radius: var(--border-radius);
color: black;
padding: 0;
display: block;
max-width: 6em;
width: 3.2em;
height: 2em;
line-height: 2em;
text-decoration: none;
}
}
.score_0 {
a, .a {
background: rgb(0, 128, 0);
}
a:hover {
background: rgb(0, 176, 0);
}
}
.score_1 {
a, .a {
background: rgb(255, 255, 0);
}
a:hover {
background: rgb(248, 248, 121);
}
}
.score_2 {
a, .a {
background: rgb(255, 166, 0);
}
a:hover {
background: rgb(255, 182, 47);
}
}
.score_3 {
a, .a {
background: rgb(255, 0, 0);
}
a:hover {
background: rgb(255, 91, 91);
}
}
.score_4 {
a, .a {
background: rgb(128, 0, 128);
color: white;
}
a:hover {
background: rgb(178, 0, 178);
}
}
@media only screen and (min-width: 1800px) {
.total, .month {
display: inline;
opacity: .35;
}
.label {
display: table-cell;
}
}
@media only screen and (min-width: 1600px) {
.ratio {
a, .a {
width: 5em;
height: 3.2em;
line-height: 3.2em;
}
}
font-size: 100%;
}
}
}
dialog {
.h-image {
background-repeat: no-repeat;
background-size: cover;
background-position: center, center;
}
.h-mask {
background-color: var(--background-color);
margin: calc(var(--spacing) * -1.5);
padding: calc(var(--spacing) * 1.5);
}
.h-mask.visible {
background-color: var(--background-color-transparent);
transition: background-color .8s ease-in;
}
.h-mask.visible:hover {
background-color: var(--background-color-transparent-light);
}
}
.visible-link {
text-decoration: underline;
}
.detail-link {
text-align: right;
padding-right: 0.4em;
.visible-link {
color: var(--contrast);
}
}
.week-in-month {
article {
.visible-link {
color: var(--contrast);
}
}
}

View File

@ -17,13 +17,53 @@
{% block content %}
<div class="grid two-columns">
<div id="contenu-principal">
<div>
<article>
<header>
<div class="slide-buttons">
<a href="{% url 'moderate' %}" role="button">Modérer {% picto_from_name "check-square" %}</a>
<article>
<header>
<div class="slide-buttons">
<a href="{% url 'moderate' %}" role="button">Modérer {% picto_from_name "check-square" %}</a>
</div>
<h2>Modération à venir</h2>
</header>
<div class="grid">
<div>
{% url 'administration' as local_url %}
{% include "agenda_culturel/static_content.html" with name="administration" url_path=local_url %}
</div>
<div class="moderation_heatmap">
{% for w in nb_not_moderated %}
<table>
<thead>
<th class="label"></th>
{% for m in w %}
<th><a href="{% url 'day_view' m.start_day.year m.start_day.month m.start_day.day %}">{{ m.start_day|date:"d" }}<span class="month"> {{ m.start_day|date:"M"|lower }}</span></a></th>
{% endfor %}
</thead>
<tbody>
<tr>
<th class="label">reste à modérer</h>
{% for m in w %}
<td class="ratio score_{{ m.note }}">
<{% if m.not_moderated > 0 %}a href="{% if m.is_today %}
{% url 'moderate' %}
{% else %}
{% url 'moderate_from_date' m.start_day.year m.start_day.month m.start_day.day %}
{% endif %}"{% else %}span class="a"{% endif %}>
{{ m.not_moderated }}<span class="total"> / {{ m.nb_events }}</span></{% if m.not_moderated > 0 %}a{% else %}span{% endif %}>
</td>
{% endfor %}
</tr>
</tbody>
</table>
{% endfor %}
</div>
</div>
</article>
<article>
<header>
<h2>Activité des derniers jours</h2>
</header>
<h3>Résumé des activités</h3>
@ -36,7 +76,8 @@
{% include "agenda_culturel/rimports-info-inc.html" with all=1 %}</p>
</article>
</div>
<article>
<header>
<div class="slide-buttons">

View File

@ -0,0 +1,27 @@
{% extends "agenda_culturel/page-admin.html" %}
{% block fluid %}{% endblock %}
{% block content %}
<article>
<header>
<h1>{% block title %}{% block og_title %}Vider le cache{% endblock %}{% endblock %}</h1>
</header>
<form method="post">{% csrf_token %}
<p>Êtes-vous sûr·e de vouloir vider le cache&nbsp;? Toutes les pages seront
générées lors de leur consultation, mais cela peut ralentir temporairemenet l'expérience de navigation.
</p>
{{ form }}
<footer>
<div class="grid buttons">
<a href="{{ cancel_url }}" role="button" class="secondary">Annuler</a>
<input type="submit" value="Confirmer">
</div>
</footer>
</form>
</article>
{% endblock %}

View File

@ -1,37 +0,0 @@
{% extends "agenda_culturel/page-admin.html" %}
{% load static %}
{% block title %}{% block og_title %}Contact{% endblock %}{% endblock %}
{% block entete_header %}
<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 %}
<h1>Contact</h1>
<article>
{% url 'contact' as local_url %}
{% include "agenda_culturel/static_content.html" with name="contact" url_path=local_url %}
</article>
<article>
<header>
<p class="message warning"><strong>Attention&nbsp:</strong> n'utilisez pas le formulaire ci-dessous pour proposer un événement, il sera ignoré. Utilisez plutôt la page <a href="{% url 'add_event' %}">ajouter un événement</a>.</p>
</header>
<form method="post">{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Envoyer">
</form>
</article>
{% endblock %}

View File

@ -1,52 +0,0 @@
{% extends "agenda_culturel/page-admin.html" %}
{% load static %}
{% load utils_extra %}
{% block title %}{% block og_title %}Message de contact : {{ obj.subject }}{% endblock %}{% endblock %}
{% block entete_header %}
<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 sidemenu-bouton %}
<li><a href="#contenu-principal" aria-label="Aller au contenu">{% picto_from_name "chevron-up" %}</a></li>
<li><a href="#sidebar" aria-label="Aller au menu latéral">{% picto_from_name "chevron-down" %}</a></li>
{% endblock %}
{% block content %}
<div class="grid two-columns">
<div>
<article>
<header>
<div class="slide-buttons">
<a href="{% url 'delete_contactmessage' object.id %}" role="button" data-tooltip="Supprimer le message">Supprimer {% picto_from_name "trash-2" %}</a>
</div>
<h1>Modération du message «&nbsp;{{ object.subject }}&nbsp;»</h1>
<ul>
<li>Date&nbsp;: {{ object.date }}</li>
<li>Auteur&nbsp;: {{ object.name }} <a href="mailto:{{ object.email }}">{{ object.email }}</a></li>
</ul>
</header>
<div>
{{ object.message }}
</div>
</article>
<article>
<form method="post">{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Enregistrer">
</form>
</article>
</div>
{% include "agenda_culturel/side-nav.html" with current="contactmessages" %}
</div>
{% endblock %}

View File

@ -12,7 +12,7 @@
{% if local %}
<a href="{{ local.get_absolute_url }}" role="button">voir la version locale {% picto_from_name "eye" %}</a>
{% else %}
<a href="{% url 'clone_edit' event.id %}" role="button">créer une copie locale {% picto_from_name "plus-circle" %}</a>
<a href="{% url 'clone_edit' event.id %}" role="button">modifier en copie locale {% picto_from_name "plus-circle" %}</a>
{% endif %}
{% endwith %}
{% else %}

View File

@ -0,0 +1,9 @@
Bonjour,
Nous avons le plaisir de t'informer que l'événement « {{ event_title }} » que tu as proposé sur {{ sitename }} a été validé et publié par l'équipe de modération.
Tu peux dès maintenant le retrouver à l'adresse suivante :
- {{ url }}
Merci de participer à l'amélioration de {{ sitename }}. N'hésites pas à continuer à contribuer en ajoutant de nouveaux événements, ça nous fait bien plaisir.
L'équipe de modération.

View File

@ -0,0 +1,8 @@
Bonjour,
Nous avons la dure tâche de t'informer que l'événement « {{ event_title }} » que tu avais proposé sur {{ sitename }} n'a pas été retenu par l'équipe de modération.
Nous te remercions pour cette proposition, et espérons qu'une prochaine fois, ta proposition correspondra à la ligne portée par {{ sitename }}.
L'équipe de modération.

View File

@ -1,13 +1,13 @@
{% if user.is_authenticated %}
<p class="footer">Création&nbsp;: {{ event.created_date }}
<p class="footer">Création&nbsp;: {{ event.created_date }}{% if event.created_by_user %} par <em>{{ event.created_by_user.username }}</em>{% endif %}
{% if event.modified %}
— dernière modification&nbsp;: {{ event.modified_date }}
— dernière modification&nbsp;: {{ event.modified_date }}{% if event.modified_by_user %} par <em>{{ event.modified_by_user.username }}</em>{% endif %}
{% endif %}
{% if event.imported_date %}
— dernière importation&nbsp;: {{ event.imported_date }}
— dernière importation&nbsp;: {{ event.imported_date }}{% if event.imported_by_user %} par <em>{{ event.imported_by_user.username }}</em>{% endif %}
{% endif %}
{% if event.moderated_date %}
— dernière modération&nbsp;: {{ event.moderated_date }}
— dernière modération&nbsp;: {{ event.moderated_date }}{% if event.moderated_by_user %} par <em>{{ event.moderated_by_user.username }}</em>{% endif %}
{% endif %}
{% if event.pure_import %}
<strong>version importée</strong>

View File

@ -1,10 +1,12 @@
<footer class="remarque">
Informations complémentaires non éditables&nbsp;:
<strong>Informations complémentaires non éditables</strong>
<ul>
{% if object.created_date %}<li>Création&nbsp;: {{ object.created_date }}</li>{% endif %}
{% if object.modified_date %}<li>Dernière modification&nbsp;: {{ object.modified_date }}</li>{% endif %}
{% if object.moderated_date %}<li>Dernière modération&nbsp;: {{ object.moderated_date }}</li>{% endif %}
{% if object.imported_date %}<li>Dernière importation&nbsp;: {{ object.imported_date }}</li>{% endif %}
{% if not allbutdates %}
{% if object.created_date %}<li>Création&nbsp;: {{ object.created_date }}{% if object.created_by_user %} par <em>{{ object.created_by_user.username }}</em>{% endif %}</li>{% endif %}
{% if object.modified_date %}<li>Dernière modification&nbsp;: {{ object.modified_date }}{% if object.modified_by_user %} par <em>{{ object.modified_by_user.username }}</em>{% endif %}</li>{% endif %}
{% if object.moderated_date %}<li>Dernière modération&nbsp;: {{ object.moderated_date }}{% if object.moderated_by_user %} par <em>{{ object.moderated_by_user.username }}</em>{% endif %}</li>{% endif %}
{% if object.imported_date %}<li>Dernière importation&nbsp;: {{ object.imported_date }}{% if object.imported_by_user %} par <em>{{ object.imported_by_user.username }}</em>{% endif %}</li>{% endif %}
{% endif %}
{% if object.uuids %}
{% if object.uuids|length > 0 %}
<li>UUIDs (identifiants uniques d'événements dans les sources)&nbsp;:

View File

@ -81,7 +81,7 @@ Duplication de {% else %}
{{ form }}
<div class="grid buttons stick-bottom">
<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="Enregistrer">
<input type="submit" value="Enregistrer{% if form.is_clone_from_url %} et modérer{% endif %}">
</div>
</form>

View File

@ -33,31 +33,39 @@
</p>
</header>
<form method="post" enctype="multipart/form-data">{% csrf_token %}
<form method="post" enctype="multipart/form-data" id="moderate-form">{% csrf_token %}
<div class="grid moderate-preview">
{% include "agenda_culturel/single-event/event-single-inc.html" with event=event noedit=1 %}
<div>
{% include "agenda_culturel/single-event/event-single-inc.html" with event=event onlyedit=1 %}
{% with event.get_concurrent_events as concurrent_events %}
{% if concurrent_events %}
<article>
<header>
<h2>En même temps</h2>
<p class="remarque">{% if concurrent_events|length > 1 %}Plusieurs événements se déroulent en même temps.{% else %}Un autre événement se déroule en même temps.{% endif %}</p>
</header>
<ul>
{% for e in concurrent_events %}
<li>
{{ e.category|circle_cat }} {% if e.start_time %}{{ e.start_time }}{% else %}<em>toute la journée</em>{% endif %} <a href="{{ e.get_absolute_url }}">{{ e.title }}</a>
</li>
{% endfor %}
</ul>
</article>
{% endif %}
{% endwith %}
</div>
<article>
<header>
<div class="slide-buttons">
{% with event.get_local_version as local %}
{% if local %}
{% if event != local %}
<input type="submit" value="Enregistrer et éditer la version locale" name="save_and_edit_local">
{% else %}
<input type="submit" value="Enregistrer et éditer" name="save_and_edit">
{% endif %}
{% else %}
<input type="submit" value="Enregistrer et créer une version locale" name="save_and_create_local">
{% endif %}
{% endwith %}
</div>
<h2>Modification des méta-informations</h2>
{% if event.moderated_date %}
<p class="message info">Cet événement a déjà été modéré par le {{ event.moderated_date }}.
<p class="message info">Cet événement a déjà été modéré {% if event.moderation_by_user %}par {<em>{ event.moderation_by_user.username }}</em> {% endif %}le {{ event.moderated_date }}.
Vous pouvez bien sûr modifier de nouveau ces méta-informations en utilisant
le formulaire ci-après.
</p>
@ -69,23 +77,13 @@
</div>
<div class="grid buttons">
{% if pred %}
<a href="{% url 'moderate_event' pred %}" role="button">🠄 Revenir au précédent</a>
<a href="{% url 'moderate_event' pred %}" class="secondary" role="button">&lt; Revenir au précédent</a>
{% else %}
<a href="{% if request.META.HTTP_REFERER %}{{ request.META.HTTP_REFERER }}{% else %}{{ object.get_absolute_url }}{% endif %}" role="button" class="secondary">Annuler</a>
{% endif %}
<input type="submit" value="Enregistrer" name="save">
{% with event.get_local_version as local %}
{% if local %}
{% if local == event %}
<input type="submit" value="Enregistrer et éditer la version locale" name="save_and_edit_local">
{% else %}
<input type="submit" value="Enregistrer et éditer" name="save_and_edit">
{% endif %}
{% else %}
<input type="submit" value="Enregistrer et créer une version locale" name="save_and_create_local">
{% endif %}
{% endwith %}
<input type="submit" value="Enregistrer et passer au suivant 🠆" name="save_and_next">
<input type="submit" value="Enregistrer et passer au suivant &gt;" name="save_and_next">
<a href="{% url 'moderate_event_next' event.pk %}" class="secondary" role="button">Passer au suivant sans enregistrer &gt;</a>
</div>
</form>
</article>

View File

@ -2,6 +2,7 @@
{% load tag_extra %}
{% load utils_extra %}
{% load static %}
{% load locations_extra %}
{% if noarticle == 0 %}
<article id="filters">
@ -65,8 +66,10 @@
<button type="submit">Appliquer le filtre</button>
</form>
</details>
<div class="suggested-tags">
{% show_suggested_tags filter=filter %}
<div class="suggestions">
Suggestion&nbsp;:
{% show_suggested_positions filter=filter %}
{% show_suggested_tags filter=filter %}
</div>
</div>
<div class="clear"></div>

View File

@ -6,7 +6,13 @@
{% for group, fields in form.fields_by_group %}
<div {% if group.maskable %}class="maskable_group"{% endif %} id="group_{{ group.id }}">
{% if group.maskable %}
<input class="toggle_body" type="checkbox" id="maskable_group_{{ group.id }}" name="group_{{ group.id }}"><label for="maskable_group_{{ group.id }}">{{ group.label }}</label>
<input
class="toggle_body"
type="checkbox"
id="maskable_group_{{ group.id }}"
name="group_{{ group.id }}"
{% if not group.default_masked %}checked{% endif %}
><label for="maskable_group_{{ group.id }}">{{ group.label }}</label>
{% endif %}
<div class="error_group">
{% for field in fields %}
@ -36,7 +42,9 @@
<script>
const maskables = document.querySelectorAll('.maskable_group');
maskables.forEach(function (item) {
item.querySelector('.body_group').classList.add('closed');
if (!item.checked) {
item.querySelector('.body_group').classList.add('closed');
}
console.log('item ' + item);
item.querySelector('.toggle_body').addEventListener('change', (event) => {

View File

@ -21,7 +21,7 @@
<form method="post" action="">
{% csrf_token %}
{{ form.media }}
{{ form.as_p }}
{{ form }}
<input type="submit" value="Lancer l'import" id="import-button">
</form>
<p>Si tu as plein d'événements à ajouter, tu peux les <a href="{% url 'add_event_urls' %}" >ajouter par lots</a>.</p>

View File

@ -28,6 +28,12 @@
{{ formset.management_form }}
{% csrf_token %}
{% if contactform %}
<article>
{{ contactform }}
</article>
{% endif %}
{% for form in formset %}
<article>
<header>

View File

@ -0,0 +1,51 @@
{% extends "agenda_culturel/page-admin.html" %}
{% load static %}
{% load honeypot %}
{% block title %}{% block og_title %}{% if form.event %}Contact au sujet de l'événement {{ form.event.title }}{% else %}
Contact{% endif %}{% endblock %}{% endblock %}
{% block entete_header %}
<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 %}
<article>
<header>
<h1>{% if form.event %}Contact au sujet de l'événement «&nbsp;{{ form.event.title }}&nbsp;»{% else %}
Contact{% endif %}</h1>
{% if not form.event %}
<article>
{% url 'contact' as local_url %}
{% include "agenda_culturel/static_content.html" with name="contact" url_path=local_url %}
</article>
{% endif %}
<p class="message warning"><strong>Attention&nbsp;:</strong> n'utilisez pas le formulaire ci-dessous pour proposer un événement, il sera ignoré. Utilisez plutôt la page <a href="{% url 'add_event' %}">ajouter un événement</a>.</p>
{% if form.event %}
<p>Tu nous contactes au sujet de l'événement «&nbsp;{{ form.event.title }}&nbsp;» du {{ form.event.start_day }}.
N'hésites pas à nous indiquer le maximum de contexte et à nous laisser ton adresse
afin que l'on puisse répondre à tes demandes ou remarques.
</p>
{% endif %}
</header>
<form method="post">{% csrf_token %}
{% render_honeypot_field "alias_name" %}
{{ form.media }}
{{ form.as_p }}
<input type="submit" value="Envoyer">
</form>
</article>
{% endblock %}

View File

@ -0,0 +1,63 @@
{% extends "agenda_culturel/page-admin.html" %}
{% load static %}
{% load utils_extra %}
{% block title %}{% block og_title %}Message {{ obj.subject }}{% endblock %}{% endblock %}
{% block entete_header %}
<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 sidemenu-bouton %}
<li><a href="#contenu-principal" aria-label="Aller au contenu">{% picto_from_name "chevron-up" %}</a></li>
<li><a href="#sidebar" aria-label="Aller au menu latéral">{% picto_from_name "chevron-down" %}</a></li>
{% endblock %}
{% block content %}
<div class="grid two-columns">
<div>
<article>
<header>
<div class="slide-buttons">
<a href="{% url 'delete_message' object.id %}" role="button" data-tooltip="Supprimer le message">Supprimer {% picto_from_name "trash-2" %}</a>
</div>
<h1>Message «&nbsp;{{ object.subject }}&nbsp;»</h1>
<ul>
<li>Date&nbsp;: {{ object.date.date }} à {{ object.date.time }}</li>
<li>Auteur&nbsp;: {% if object.user %}<em>{{ object.user }}</em>{% else %}{{ object.name }}{% endif %} {% if object.email %}<a href="mailto:{{ object.email }}">{{ object.email }}</a>{% endif %}</li>
{% if object.related_event %}<li>Événement associé&nbsp;: <a href="{{ object.related_event.get_absolute_url }}">{{ object.related_event.title }}</a> du {{ object.related_event.start_day }}</li>{% endif %}
<li>Type&nbsp;: {% if object.message_type %}{{ object.get_message_type_display }}{% else %}-{% endif %}</li>
</ul>
</header>
<div>
{{ object.message | safe }}
</div>
</article>
<article>
{% if object.message_type == "from_contributor" or object.message_type == "from_contrib_no_msg" %}<p class="message info">Ce message a été envoyé par une personne lors
de l'ajout d'un événement.
{% if object.closed %}
En décochant fermé, vous modifiez manuellement son statut, et cela pourra entraîner l'envoi d'un message de notification
lors de la modification future de l'événement associé.{% else %}
En cochant fermé, vous modifiez manuellement son statut, et cela empêchera l'envoi d'un message de notification
lors de la modification future de l'événement associé.
{% endif %}
</p>{% endif %}
<form method="post">{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Enregistrer">
</form>
</article>
</div>
{% include "agenda_culturel/side-nav.html" with current="messages" %}
</div>
{% endblock %}

View File

@ -34,8 +34,10 @@
<thead>
<tr>
<th>Date</th>
<th>Type</th>
<th>Sujet</th>
<th>Auteur</th>
<th>Événement</th>
<th>Fermé</th>
<th>Spam</th>
</tr>
@ -44,8 +46,10 @@
{% for obj in paginator_filter %}
<tr>
<td>{{ obj.date }}</td>
<td><a href="{% url 'contactmessage' obj.pk %}">{{ obj.subject }}</a></td>
<td>{{ obj.name }}</td>
<td>{% if obj.message_type %}{{ obj.get_message_type_display }}{% else %}-{% endif %}</td>
<td><a href="{% url 'message' obj.pk %}">{{ obj.subject }}</a></td>
<td>{% if obj.user %}<em>{{ obj.user }}</em>{% else %}{% if obj.name %}{{ obj.name }}{% else %}-{% endif %}{% endif %}</td>
<td>{% if obj.related_event %}<a href="{{ obj.related_event.get_absolute_url }}">{{ obj.related_event.pk }}</a>{% else %}-{% endif %}</td>
<td>{% if obj.closed %}{% picto_from_name "check-square" "fermé" %}{% else %}{% picto_from_name "square" "ouvert" %}{% endif %}</td>
<td>{% if obj.spam %}{% picto_from_name "check-square" "spam" %}{% else %}{% picto_from_name "square" "non spam" %}{% endif %}</td>
</tr>
@ -57,7 +61,7 @@
</footer>
</article>
{% include "agenda_culturel/side-nav.html" with current="contactmessages" %}
{% include "agenda_culturel/side-nav.html" with current="messages" %}
</div>
{% endblock %}

View File

@ -35,12 +35,8 @@
const places = document.querySelector('#id_principal_place');
const choices_places = new Choices(places,
{
placeholderValue: 'Sélectionner le lieu principal ',
allowHTML: true,
delimiter: ',',
removeItemButton: true,
shouldSort: false,
}
}
);
</script>

View File

@ -3,10 +3,11 @@
{% load cat_extra %}
{% load utils_extra %}
{% load event_extra %}
{% load cache %}
{% block title %}{% block og_title %}{{ event.title }}{% endblock %}{% endblock %}
{% block og_image %}{% if event.has_image_url %}{{ event.get_image_url }}{% else %}{{ block.super }}{% endif %}{% endblock %}
{% block og_image %}{% if event.has_image_url %}{{ event|get_image_uri:request }}{% else %}{{ block.super }}{% endif %}{% endblock %}
{% block og_description %}{% if event.description %}{{ event.description |truncatewords:20|linebreaks }}{% else %}{{ block.super }}{% endif %}{% endblock %}
{% block entete_header %}
@ -16,21 +17,62 @@
{% block content %}
<div class="grid two-columns">
<div>
{% with cache_timeout=user.is_authenticated|yesno:"30,600" %}
{% cache cache_timeout event_body user.is_authenticated event %}
{% include "agenda_culturel/single-event/event-single-inc.html" with event=event filter=filter %}
{% endcache %}
{% endwith %}
{% if user.is_authenticated %}
<article>
<article id="chronology">
<header>
<h2>Informations internes</h2>
<h2>Chronologie</h2>
</header>
{% include "agenda_culturel/event-info-inc.html" with object=event %}
{% for step in event.chronology %}
{% if step.is_date %}
<div class="entree dateline">
<div><span class="ts">{{ step.timestamp }}</span></div>
<div>
{% if step.data == "created_date" %}<em>création</em>{% if event.created_by_user %} par {{ event.created_by_user.username }}{% endif %}{% endif %}
{% if step.data == "modified_date" %}<em>dernière modification</em>{% if event.modified_by_user %} par {{ event.modified_by_user.username }}{% endif %}{% endif %}
{% if step.data == "moderated_date" %}<em>dernière modération</em>{% if event.moderated_by_user %} par {{ event.moderated_by_user.username }}{% endif %}{% endif %}
{% if step.data == "imported_date" %}<em>dernière importation</em>{% if event.imported_by_user %} par {{ event.imported_by_user.username }}{% endif %}{% endif %}
</div>
</div>
{% else %}
<div class="entree">
<div><span class="ts">{{ step.timestamp }}</span></div>
<div>
<header><strong>Message{% if not step.data.closed %} (ouvert){% endif %}</strong>{% if step.data.related_event and event != step.data.related_event %} sur
<a href="{{ step.data.related_event.get_absolute_url }}">une autre</a> version{% endif %}&nbsp;:
<a href="{{ step.data.get_absolute_url }}">{{ step.data.subject|truncatechars:20 }}</a>
{% if step.data.user %} par <em>{{ step.data.user }}</em>{% else %} par {% if step.data.name %}{{ step.data.name }}{% if step.data.email %} (<a href="mailto: {{ step.data.email }}">{{ step.data.email }}</a>){% endif %}{% else %} <a href="mailto: {{ step.data.email }}">{{ step.data.email }}</a>{% endif %}{% endif %}</header>
<div class="texte">{{ step.data.message|safe }}</div>
{% if step.data.comments %}
<div><strong>Commentaire&nbsp;:</strong> {{ step.data.comments }}</div>
{% endif %}
</div>
</div>
{% endif %}
{% endfor %}
<form method="post">{% csrf_token %}
{{ form.media }}
{{ form.as_p }}
<input type="submit" value="Commenter">
</form>
{% include "agenda_culturel/event-info-inc.html" with allbutdates=1 %}
</article>
{% endif %}
</div>
{% with cache_timeout=user.is_authenticated|yesno:"30,600" %}
{% cache cache_timeout event_aside user.is_authenticated event %}
<aside>
{% with event.get_concurrent_events as concurrent_events %}
{% if concurrent_events %}
@ -51,39 +93,7 @@
</article>
{% endif %}
{% endwith %}
<article>
{% with event.get_nb_events_same_dates as nb_events_same_dates %}
{% with nb_events_same_dates|length as c_dates %}
<header>
<h2>Voir aussi</h2>
{% if c_dates != 1 %}
<p class="remarque">
Retrouvez ci-dessous tous les événements
{% if event.is_single_day %}
à la même date
{% else %}
aux mêmes dates
{% endif %}
que l'événement affiché.
</p>
{% endif %}
</header>
<nav>
{% if c_dates == 1 %}
<a role="button" href="{% url 'day_view' nb_events_same_dates.0.1.year nb_events_same_dates.0.1.month nb_events_same_dates.0.1.day %}">Toute la journée</a>
{% else %}
<ul>
{% for nbevents_date in nb_events_same_dates %}
<li>
<a href="{% url 'day_view' nbevents_date.1.year nbevents_date.1.month nbevents_date.1.day %}">{{ nbevents_date.0 }} événement{{ nbevents_date.0 | pluralize }} le {{ nbevents_date.1 }}</a>
</li>
{% endfor %}
</ul>
{% endif %}
</nav>
{% endwith %}
{% endwith %}
</article>
{% if event.other_versions and not event.other_versions.fixed %}
{% with poss_dup=event.get_other_versions|only_allowed:user.is_authenticated %}
{% if poss_dup|length > 0 %}
@ -122,12 +132,15 @@
{% else %}
Signaler comme doublon
{% endif %}</a>
</article>
<a role="button" href="{% url 'message_for_event' event.pk %}">Signaler cet événement</a>
</article>
</aside>
{% endcache %}
{% endwith %}
</div>
{% endblock %}

View File

@ -84,7 +84,7 @@
</script>
{% endif %}
<header{% if day.is_today %} id="today"{% endif %}>
<h3><a href="{{ day.date | url_day }}?{{ filter.get_url }}">{{ day.date | date:"l j" }}</a></h3}>
<h3><a href="{{ day.date | url_day }}?{{ filter.get_url }}" class="visible-link">{{ day.date | date:"l j" }}</a></h3}>
</header>
{% if day.events %}
<ul>
@ -107,7 +107,7 @@
{% if event.start_day == day.date and event.start_time %}
{{ event.start_time }}
{% endif %}
<a href="{{ event.get_absolute_url }}">{{ event|picto_status }} {{ event.title }} {{ event|tw_badge }}</a>
<a href="{{ event.get_absolute_url }}">{{ event|picto_status }} {{ event.title|no_emoji }} {{ event|tw_badge }}</a>
</li>
{% endfor %}
</ul>
@ -121,7 +121,8 @@
</article>
</dialog>
{% endfor %}
<ul>
<li class="detail-link"><a href="{{ day.date | url_day }}?{{ filter.get_url }}" class="visible-link">voir en détail {% picto_from_name "chevrons-right" %}</a></li>
</ul>
{% endif %}
</ul>
</article>

View File

@ -39,13 +39,13 @@
<li><strong>Adresse naviguable&nbsp;:</strong> <a href="{{ object.browsable_url }}">{{ object.browsable_url }}</a></li>
<li><strong>Valeurs par défaut&nbsp;:</strong>
<ul>
<li><strong>Publié&nbsp;:</strong> {{ object.defaultPublished }}</li>
<li><strong>Localisation&nbsp;:</strong> {{ object.defaultLocation }}</li>
<li><strong>Publié&nbsp;:</strong> {{ object.defaultPublished|yesno:"Oui,Non" }}</li>
{% if object.defaultLocation %}<li><strong>Localisation{% if object.forceLocation %} (forcée){% endif %}&nbsp;:</strong> {{ object.defaultLocation }}</li>{% endif %}
<li><strong>Catégorie&nbsp;:</strong> {{ object.defaultCategory }}</li>
<li><strong>Organisateur&nbsp;:</strong> <a href="{{ object.defaultOrganiser.get_absolute_url }}">{{ object.defaultOrganiser }}</a></li>
{% if object.defaultOrganiser %}<li><strong>Organisateur&nbsp;:</strong> <a href="{{ object.defaultOrganiser.get_absolute_url }}">{{ object.defaultOrganiser }}</a></li>{% endif %}
<li><strong>Étiquettes&nbsp;:</strong>
{% for tag in object.defaultTags %}
{{ tag|tw_highlight }}{% if not forloop.last %}, {% endif %}
<a href="{% url 'view_tag' tag %}">{{ tag|tw_highlight }}</a>{% if not forloop.last %}, {% endif %}
{% endfor %}
</li>
</ul>

View File

@ -95,13 +95,19 @@
<h3>{{ ti.short_name }} <a class="badge simple" href="#{{ ti.id }}" data-tooltip="Aller à {{ ti.name }}">{{ ti.events|length }} {% picto_from_name "chevrons-down" %}</a></h3>
<ul>
{% for event in ti.events %}
{% if event.is_first_after_now %}
<li class="line-now"><div><div>{% now "H:i" %}</div><div class="line"></div></div></li>
{% endif %}
<li>{{ event.category | circle_cat:event.has_recurrences }}
{% if event.start_time %}
{{ event.start_time }}
{% endif %}
{{ event|picto_status }} <a href="#event-{{ event.id }}">{{ event.title }}</a> {{ event|tw_badge }}
{{ event|picto_status }} <a href="#event-{{ event.id }}">{{ event.title|no_emoji }}</a> {{ event|tw_badge }}
</li>
{% endfor %}
{% if forloop.last and cd.is_today_after_events %}
<li class="line-now"><div><div>{% now "H:i" %}</div><div class="line"></div></div></li>
{% endif %}
</ul>
{% endif %}
{% endfor %}

View File

@ -14,7 +14,7 @@
<script src="{% static 'js/calendar-buttons.js' %}"></script>
{% endblock %}
{% block title %}{% block og_title %}Semaine du {{ calendar.firstdate|date|frdate }}{% endblock %}{% endblock %}
{% block title %}{% block og_title %}{% if calendar.today_in_calendar %}Sorties culturelles cette semaine à Clermont-Ferrand et aux environs{% else %}Semaine du {{ calendar.firstdate|date|frdate }}{% endif %}{% endblock %}{% endblock %}
{% block ce_mois_ci_parameters %}{% block cette_semaine_parameters %}{% block a_venir_parameters %}?{{ filter.get_url }}{% endblock %}{% endblock %}{% endblock %}
@ -37,7 +37,7 @@
<div>
{% if calendar.firstdate|shift_day:-1|not_before_first %}
{% if calendar.lastdate|not_after_last %}
<a role="button" href="{% url 'week_view' calendar.previous_week.year calendar.previous_week|week %}?{{ filter.get_url }}">
<a role="button" href="{% url 'week_view' calendar.previous_week|weekyear calendar.previous_week|week %}?{{ filter.get_url }}">
{% picto_from_name "chevron-left" %} précédente</a>
{% endif %}
{% endif %}
@ -45,7 +45,7 @@
{% if calendar.lastdate|shift_day:+1|not_after_last %}
{% if calendar.lastdate|not_before_first %}
<div class="right">
<a role="button" href="{% url 'week_view' calendar.next_week.year calendar.next_week|week %}?{{ filter.get_url }}">suivante
<a role="button" href="{% url 'week_view' calendar.next_week|weekyear calendar.next_week|week %}?{{ filter.get_url }}">suivante
{% picto_from_name "chevron-right" %}
</a>
</div>
@ -57,7 +57,7 @@
<div class="slider-button slider-button-inside button-left hidden">{% picto_from_name "arrow-left" %}</div>
{% if calendar.firstdate|shift_day:-1|not_before_first %}
{% if calendar.lastdate|not_after_last %}
<div class="slider-button slider-button-page button-left hidden"><a href="{% url 'week_view' calendar.previous_week.year calendar.previous_week|week %}?{{ filter.get_url }}">{% picto_from_name "chevrons-left" %}</a></div>
<div class="slider-button slider-button-page button-left hidden"><a href="{% url 'week_view' calendar.previous_week|weekyear calendar.previous_week|week %}?{{ filter.get_url }}">{% picto_from_name "chevrons-left" %}</a></div>
{% endif %}
{% endif %}
@ -80,20 +80,28 @@
</script>
{% endif %}
<header{% if day.is_today %} id="today"{% endif %}>
<h2><a href="{{ day.date | url_day }}?{{ filter.get_url }}">{{ day.date | date:"l j" }}</a></h2>
<h2><a class="visible-link" href="{{ day.date | url_day }}?{{ filter.get_url }}">{{ day.date | date:"l j" }}</a></h2>
</header>
{% if day.events %}
<ul>
{% for event in day.events %}
{% if event.is_first_after_now %}
<li class="line-now"><div><div>{% now "H:i" %}</div><div class="line"></div></div></li>
{% endif %}
<li>{{ event.category | circle_cat:event.has_recurrences }}
{% if event.start_day == day.date and event.start_time %}
{{ event.start_time }}
{% endif %}
{{ event|picto_status }} <a href="{{ event.get_absolute_url }}" data-target="event-{{ event.id }}" onClick="toggleModal(event)">{{ event.title }}</a>
{{ event|picto_status }} <a href="{{ event.get_absolute_url }}" data-target="event-{{ event.id }}" onClick="toggleModal(event)">{{ event.title|no_emoji }}</a>
{{ event|tw_badge }}
<dialog id="event-{{ event.id }}">
<article>
<header>
{% if event.has_image_url %}
<header style="background-image: url({{ event.get_image_url }});" class="h-image">
{% else %}
<header class="cat-{{ event.category.pk }}">
{% endif %}
<div class="h-mask">
<a href="#event-{{ event.id }}"
aria-label="Fermer"
class="close"
@ -125,6 +133,7 @@
{% endif %}
{% if event.end_time %} {% if not event.end_day|date|frdate or event.end_day == event.start_day %}jusqu'à{% endif %} {{ event.end_time }}{% endif %}
</p>
</div>
</header>
<div class="body-fixed">{{ event.description |linebreaks }}</div>
@ -147,6 +156,10 @@
</dialog>
</li>
{% endfor %}
{% if day.is_today_after_events %}
<li class="line-now"><div><div>{% now "H:i" %}</div><div class="line"></div></div></li>
{% endif %}
<li class="detail-link"><a href="{{ day.date | url_day }}?{{ filter.get_url }}" class="visible-link">voir en détail {% picto_from_name "chevrons-right" %}</a></li>
</ul>
{% endif %}
</ul>
@ -160,7 +173,7 @@
<div class="slider-button slider-button-inside button-right hidden">{% picto_from_name "arrow-right" %}</div>
{% if calendar.lastdate|shift_day:+1|not_after_last %}
{% if calendar.lastdate|not_before_first %}
<div class="slider-button slider-button-page button-right hidden"><a href="{% url 'week_view' calendar.next_week.year calendar.next_week|week %}?{{ filter.get_url }}">{% picto_from_name "chevrons-right" %}</a></div>
<div class="slider-button slider-button-page button-right hidden"><a href="{% url 'week_view' calendar.next_week|weekyear calendar.next_week|week %}?{{ filter.get_url }}">{% picto_from_name "chevrons-right" %}</a></div>
{% endif %}
{% endif %}

View File

@ -1,15 +1,23 @@
<!DOCTYPE html>
<html lang="fr">
{% load event_extra %}
{% load cache %}
{% load messages_extra %}
{% load utils_extra %}
{% load duplicated_extra %}
{% load rimports_extra %}
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pommes de lune — {% block title %}{% endblock %}</title>
<meta name="google-site-verification" content="pvRD0rc_xIE-1IYmbao0kj5ngGo1IWxJqKwoxrQwxuA" />
<meta name="keywords" content="Clermont-Ferrand, Puy-de-Dôme, agenda culturel, agenda participatif, sortir à clermont, sorties, concerts, théâtre, danse, animations, ateliers, lectures">
{% load static %}
<meta property="og:title" content="Pommes de lune — {% block og_title %}{% endblock %}" />
<meta property="og:description" content="{% block og_description %}Événements culturels à Clermont-Ferrand et aux environs{% endblock %}" />
<meta property="og:image" content="{% block og_image %}{% static 'images/capture.png' %}{% endblock %}" />
<meta property="og:description" content="{% block og_description %}Où sortir à Clermont-Ferrand? Retrouve tous les bons plans sur l'agenda participatif des événements culturels à Clermont-Ferrand et dans le Puy-de-Dôme{% endblock %}" />
<meta property="og:image" content="{% block og_image %}https://{{ request.get_host }}{% get_media_prefix %}screenshot.png{% endblock %}" />
<meta property="og:locale" content="fr_FR" />
<meta property="og:url" content="{{ request.build_absolute_uri }}" />
{% if debug %}
@ -27,13 +35,8 @@
{% block entete_header %}
{% endblock %}
</head>
{% load event_extra %}
{% load cache %}
{% load contactmessages_extra %}
{% load utils_extra %}
{% load duplicated_extra %}
{% load rimports_extra %}
<body class="{% block body-class %}contenu{% endblock %}">
<body class="{% block body-class %}contenu{% endblock %} {% if user.is_authenticated %}authenticated{% endif %}">
<div id="boutons-fixes">
<ul>
{% block sidemenu-bouton %}{% endblock %}
@ -47,10 +50,9 @@
<input class="menu-btn" type="checkbox" id="menu-btn" />
<label class="menu-icon" for="menu-btn">{% picto_from_name "menu" %}</label>
<ul class="menu">
{% if user.is_authenticated %}{% block configurer-menu %}<li id="menu-configurer" class="configurer-bouton"><a href="{% url 'administration' %}">Administrer {% picto_from_name "settings" %}</a></li>{% endblock %}{% endif %}
{% block ajouter-menu %}<li id="menu-ajouter" class="ajouter-bouton"><a href="{% url 'add_event' %}">Ajouter un événement {% picto_from_name "plus-circle" %}</a></li>{% endblock %}
{% block rechercher-menu %}<li id="menu-rechercher" class="rechercher-bouton"><a href="{% url 'event_search' %}">Rechercher {% picto_from_name "search" %}</a></li>{% endblock %}
<li><a href="{% url 'a_venir' %}{% block a_venir_parameters %}{% endblock %}">À venir</a></li>
<li><a href="{% url 'a_venir' %}{% block a_venir_parameters %}{% endblock %}">Maintenant</a></li>
<li><a href="{% url 'cette_semaine' %}{% block cette_semaine_parameters %}{% endblock %}">Cette semaine</a></li>
<li><a href="{% url 'ce_mois_ci' %}{% block ce_mois_ci_parameters %}{% endblock %}">Ce mois-ci</a></li>
</ul>
@ -64,30 +66,35 @@
</li>
<li>
<div>
{% if perms.agenda_culturel.view_recurrentimport %}
{% show_badges_rimports "bottom" %}
{% endif %}
{% if perms.agenda_culturel.change_event %}
{% show_badges_events "bottom" %}
{% endif %}
{% 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 %}
{% if user.is_authenticated %}
{{ user.username }} @
{% endif %}
<a href="{% url 'home' %}" aria-label="Retour accueil">Pommes de lune</a>
</div>
<div class="soustitre">Événements culturels à Clermont-Ferrand et aux environs</div>
<div class="soustitre">Agenda participatif des sorties culturelles à Clermont-Ferrand et aux environs</div>
</li>
</ul>
</nav>
{% if user.is_authenticated %}
<div id="badges">
{% if perms.agenda_culturel.view_recurrentimport %}
{% show_badges_rimports "bottom" %}
{% endif %}
{% if perms.agenda_culturel.change_event %}
{% show_badges_events "bottom" %}
{% endif %}
{% 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_message %}
{% show_badge_messages "bottom" %}
{% endif %}
<a class="link" href="{% url 'administration' %}">Administrer {% picto_from_name "settings" %}</a>
</div>
{% endif %}
</div>
<main class="container{% block fluid %}-fluid{% endblock %}">
{% if messages %}

View File

@ -22,9 +22,26 @@
<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>
<p><strong>Remarque&nbsp;:</strong> les champs ont été pré-remplis à partir de la description sous forme libre et n'est probablement pas parfaite.</p>
{% endif %}
<form method="post">{% csrf_token %}
{{ form.as_grid }}
<div class="grid form-place">
{{ form }}
<div class="map-widget">
<div id="map_location" style="width: 100%; aspect-ratio: 16/9"></div>
<p>Cliquez pour ajuster la position GPS</p>
<input type="checkbox" role="switch" id="lock_position">Verrouiller la position</input>
<script>
document.getElementById("lock_position").onclick = function() {
const field = document.getElementById("id_location");
if (this.checked)
field.setAttribute("readonly", true);
else
field.removeAttribute("readonly");
}
</script>
</div>
</div>
<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">

View File

@ -6,6 +6,8 @@
{% block configurer-bouton %}{% endblock %}
{% block entete_header %}
<script src="{% static 'choicejs/choices.min.js' %}"></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">
@ -26,4 +28,35 @@
</form>
</article>
<script>
show_firstgroup = {
choice(classes, choice) {
const i = Choices.defaults.templates.choice.call(this, classes, choice);
if (this.first_group !== null && choice.groupId == this.first_group)
i.classList.add("visible");
return i;
},
choiceGroup(classes, group) {
const g = Choices.defaults.templates.choiceGroup.call(this, classes, group);
if (this.first_group === undefined && group.value == "Suggestions")
this.first_group = group.id;
if (this.first_group !== null && group.id == this.first_group)
g.classList.add("visible");
return g;
}
};
const tags = document.querySelector('#id_defaultTags');
const choices_tags = new Choices(tags,
{
placeholderValue: 'Sélectionner les étiquettes par défaut',
allowHTML: true,
delimiter: ',',
removeItemButton: true,
shouldSort: false,
callbackOnCreateTemplates: () => (show_firstgroup)
}
);
</script>
{% endblock %}

View File

@ -1,5 +1,5 @@
{% load event_extra %}
{% load contactmessages_extra %}
{% load messages_extra %}
{% load duplicated_extra %}
{% load utils_extra %}
<aside id="sidebar">
@ -56,11 +56,11 @@
</ul>
</nav>
{% endif %}
{% if perms.agenda_culturel.view_contactmessage %}
{% if perms.agenda_culturel.view_message %}
<h3>Messages</h3>
<nav>
<ul>
<li><a {% if current == "contactmessages" %}class="selected" {% endif %}href="{% url 'contactmessages' %}">Messages de contact</a>{% show_badge_contactmessages "left" %}</li>
<li><a {% if current == "messages" %}class="selected" {% endif %}href="{% url 'messages' %}">Messages de contact</a>{% show_badge_messages "left" %}</li>
</ul>
</nav>
{% endif %}
@ -68,6 +68,7 @@
<h3>Configuration interne</h3>
<nav>
<ul>
<li><a href="{% url 'clear_cache' %}">Vider le cache</a></li>
<li><a href="{% url 'admin:index' %}">Administration de django</a></li>
</ul>
</nav>

View File

@ -6,7 +6,11 @@
<article id="event-{{ event.pk}}" class="single-event {% if not event.image and not event.local_image %}no-image{% endif %}">
<header class="head">
{% if day != 0 %}
{% if day == 0 %}
<div class="small-ephemeride">
{% include "agenda_culturel/ephemeris-inc.html" with event=event filter=filter %}
</div>
{% else %}
{% if event|can_show_start_time:day %}
{% if event.start_time %}
<article class='ephemeris-hour'>
@ -22,7 +26,7 @@
{% endif %}
{% endif %}
{% endif %}
<div class="header-complement">
{{ event.category | small_cat_recurrent:event.has_recurrences }}
{% if event.location or event.exact_location %}<hgroup>{% endif %}
@ -50,11 +54,7 @@
</hgroup>
{% endif %}
{% if day == 0 %}
<div class="small-ephemeride">
{% include "agenda_culturel/ephemeris-inc.html" with event=event filter=filter %}
</div>
{% endif %}
{% if event|need_complete_display:True %}<p>
{% picto_from_name "calendar" %}
@ -86,12 +86,13 @@
</p>
{% endif %}
</div>
</div>
<div class="buttons" style="clear: both">
{% if perms.agenda_culturel.change_event %}
{% include "agenda_culturel/edit-buttons-inc.html" with event=event %}
{% endif %}
</div>
</header>
<div class="description">

View File

@ -22,16 +22,17 @@
{% endif %}
{% if event.end_time %} {% if not event.end_day|date|frdate or event.end_day == event.start_day %}jusqu'à{% endif %} {{ event.end_time }}{% endif %}
</p>
<p>
{% picto_from_name "map-pin" %}
{% if event.exact_location %}
<a href="{{ event.exact_location.get_absolute_url }}">{{ event.exact_location.name }}, {{ event.exact_location.city }}</a>
<p>{% picto_from_name "map-pin" %}
<a href="{{ event.exact_location.get_absolute_url }}">{{ event.exact_location.name }}, {{ event.exact_location.city }}</a></p>
{% 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>
<p>{% picto_from_name "map-pin" %}
<a href="{% url 'add_place_to_event' event.pk %}" class="missing-data">{% if event.location %}{{ event.location }}{% else %}sans lieu{% endif %}</a></p>
{% else %}
{{ event.location }}
{% if event.location %}<p>{% picto_from_name "map-pin" %} {{ event.location }}</p>{% endif %}
{% endif %}
{% endif %}
</p>
@ -75,6 +76,15 @@
</p>
{% endif %}
{% endif %}
{% if perms.agenda_culturel.change_message %}
{% if event.message_set.all.count > 0 %}
<p class="remarque">Cet événement a été l'objet {% if event.message_set.all.count == 1 %}d'un message{% else %}de messages{% endif %}
{% for cm in event.message_set.all %}
<a href="{{ cm.get_absolute_url }}">le {{ cm.date.date }} à {{ cm.date.time }}</a>{% if not forloop.last %}, {% endif %}
{% endfor %}
</p>
{% endif %}
{% endif %}
</div>
</header>
<div class="event-body">
@ -124,9 +134,24 @@
{% include "agenda_culturel/event-date-info-inc.html" %}
</div>
<div class="buttons">
<a href="{% url 'export_event_ical' event.start_day.year event.start_day.month event.start_day.day event.id %}" role="button">Exporter ical {% picto_from_name "calendar" %}</a>
{% if perms.agenda_culturel.change_event and not noedit %}
{% include "agenda_culturel/edit-buttons-inc.html" with event=event with_clone=1 %}
{% if onlyedit %}
{% if event.pure_import %}
{% with event.get_local_version as local %}
{% if local %}
<a href="{{ local.get_absolute_url }}" role="button">voir la version locale {% picto_from_name "eye" %}</a>
{% else %}
<a href="{% url 'clone_edit' event.id %}" role="button">créer une copie locale {% picto_from_name "plus-circle" %}</a>
{% endif %}
{% endwith %}
{% else %}
<a href="{% url 'edit_event' event.id %}" role="button">modifier {% picto_from_name "edit-3" %}</a>
{% endif %}
{% else %}
<a href="{% url 'export_event_ical' event.start_day.year event.start_day.month event.start_day.day event.id %}" role="button">Exporter ical {% picto_from_name "calendar" %}</a>
{% if perms.agenda_culturel.change_event and not noedit %}
{% include "agenda_culturel/edit-buttons-inc.html" with event=event with_clone=1 %}
{% endif %}
{% endif %}
</div>
</footer>

View File

@ -62,6 +62,12 @@
{% endif %}
<footer>
{% if user.is_authenticated and rimports %}
<p>Cette étiquette est ajoutée par défaut {% if rimports.count == 1 %}à l'import récurrent{% else %}aux imports récurrents&nbsp;:{% endif %}
{% for ri in rimports %}
<a href="{{ ri.get_absolute_url }}">{{ ri.name }}</a>{% if not forloop.last %}, {% endif %}
{% endfor %}</p>
{% endif %}
{% include 'agenda_culturel/paginator.html' %}
</footer>
</article>

View File

@ -20,6 +20,12 @@
{% else %} sera bien sûr conservé, mais perdra cette étiquette.
{% endif %}</p>
{% endif %}
{% if nbi > 0 %}
<p>Remarquez qu'elle est associée à {{ nbi }} import{{ nbs|pluralize }} récurrent{{ nbs|pluralize }}, qui
{% if nbi > 1 %} seront bien sûr conservés, mais perdront cette étiquette.
{% else %} sera bien sûr conservé, mais perdra cette étiquette.
{% endif %}</p>
{% endif %}
{% if obj %}
<p>Différentes informations sont associées à cette étiquette (description, suggestion d'inclusion, etc)
seront également perdues lors de cette suppression.</p>

View File

@ -17,7 +17,7 @@
<article>
<header>
<h1>Renommer l'étiquette {{ tag }}</em></h1>
<p>En renommant l'étiquette {{ tag }}, vous remplacerez cette étiquette par la nouvelle pour tous les événements concernés.</p>
<p>En renommant l'étiquette {{ tag }}, vous remplacerez cette étiquette par la nouvelle pour tous les événements et tous les imports récurrents concernés.</p>
</header>

View File

@ -86,12 +86,6 @@ def css_categories():
)
result += "}"
result += "*:hover>." + c["css_class"] + " {"
result += background_color_adjust_color(
adjust_lightness_saturation(c["color"], 0.02, 1.0)
)
result += "}"
result += "." + c["css_class"] + ".circ-cat, "
result += "form ." + c["css_class"] + ", "
result += ".selected ." + c["css_class"] + " {"

View File

@ -179,4 +179,8 @@ def tw_badge(event):
if event.tags and len([t for t in event.tags if t.startswith("TW:")]) > 0:
return mark_safe('<span class="badge tw-badge">TW</span>')
else:
return ""
return ""
@register.filter
def get_image_uri(event, request):
return event.get_image_url(request)

View File

@ -0,0 +1,25 @@
from django import template
from django.utils.safestring import mark_safe
from django.core.cache import cache
from .utils_extra import picto_from_name
from agenda_culturel.models import ReferenceLocation
register = template.Library()
@register.simple_tag
def show_suggested_positions(filter):
filter.form.full_clean()
if filter.is_filtered_by_position_radius():
return ""
locations = ReferenceLocation.objects.all().filter(suggested_distance__isnull=False).order_by("main", "name")
result = ''
for l in locations:
result += ' <a class="small-location" role="button" href="' + filter.get_url_add_suggested_position(l) + '">' + picto_from_name("map-pin") + ' ' + l.name + ' ' + str(l.suggested_distance) + 'km</a>'
return mark_safe(result)

View File

@ -4,7 +4,7 @@ from django.urls import reverse_lazy
from django.template.defaultfilters import pluralize
from agenda_culturel.models import ContactMessage
from agenda_culturel.models import Message
from .utils_extra import picto_from_name
@ -12,15 +12,15 @@ register = template.Library()
@register.simple_tag
def show_badge_contactmessages(placement="top"):
nb_open = ContactMessage.nb_open_contactmessages()
def show_badge_messages(placement="top"):
nb_open = Message.nb_open_messages()
if nb_open != 0:
return mark_safe(
'<a href="'
+ reverse_lazy("contactmessages")
+ '?closed=False" class="badge" data-placement="'
+ reverse_lazy("messages")
+ '?closed=False&message_type=contact_form&message_type=event_report&message_type=from_contributor" class="badge" data-placement="'
+ placement
+ '"data-tooltip="'
+ '" data-tooltip="'
+ str(nb_open)
+ " message"
+ pluralize(nb_open)

View File

@ -54,7 +54,7 @@ def tag_not_in_db(tag, tags):
def show_suggested_tags(filter):
filter.form.full_clean()
tags = Tag.objects.all().filter(principal=True).order_by("name")
result = "Suggestion&nbsp;:"
result = ""
for t in tags:
if filter.tag_exists(t.name) and not filter.is_selected_tag(t.name):

View File

@ -9,6 +9,7 @@ from django.urls import reverse_lazy
from django.templatetags.static import static
from string import ascii_uppercase as auc
from django.utils.html import strip_tags
import emoji
register = template.Library()
@ -28,6 +29,9 @@ def add_de(txt):
def week(d):
return d.isocalendar()[1]
@register.filter
def weekyear(d):
return d.isocalendar()[0]
@register.filter
def not_before_first(d):
@ -137,3 +141,6 @@ def is_string(val):
def html_vide(val):
return len(strip_tags(val).replace("&nbsp;", "").strip()) == 0
@register.filter
def no_emoji(text):
return emoji.replace_emoji(text, replace='')

View File

@ -4,10 +4,32 @@ from django.contrib import admin
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
from django.urls import path, include, re_path
from django.views.i18n import JavaScriptCatalog
from django.contrib.sitemaps.views import sitemap
from django.contrib.sitemaps import GenericSitemap
from .sitemaps import StaticViewSitemap
from django.views.decorators.cache import cache_page
from .views import *
event_dict = {
"queryset": Event.objects.all(),
"date_field": "modified_date",
}
place_dict = {
"queryset": Place.objects.all(),
}
organisation_dict = {
"queryset": Organisation.objects.all(),
}
sitemaps = {
"static": StaticViewSitemap,
"events": GenericSitemap(event_dict, priority=0.7),
"places": GenericSitemap(place_dict, priority=0.6),
"organisations": GenericSitemap(organisation_dict, priority=0.2),
}
urlpatterns = [
path("", home, name="home"),
path("semaine/<int:year>/<int:week>/", week_view, name="week_view"),
@ -35,11 +57,13 @@ urlpatterns = [
),
path("event/<int:pk>/edit", EventUpdateView.as_view(), name="edit_event"),
path("event/<int:pk>/moderate", EventModerateView.as_view(), name="moderate_event"),
path("event/<int:pk>/moderate-next", EventModerateView.as_view(), name="moderate_event_next"),
path("event/<int:pk>/moderate-next/error", error_next_event, name="error_next_event"),
path("event/<int:pk>/moderate/after/<int:pred>", EventModerateView.as_view(), name="moderate_event_step"),
path("event/<int:pk>/moderate-next", moderate_event_next, name="moderate_event_next"),
path("moderate", EventModerateView.as_view(), name="moderate"),
path("moderate/<int:y>/<int:m>/<int:d>", moderate_from_date, name="moderate_from_date"),
path("event/<int:pk>/simple-clone/edit", EventUpdateView.as_view(), name="simple_clone_edit"),
path("event/<int:pk>/clone/edit", EventUpdateView.as_view(), name="clone_edit"),
path("event/<int:pk>/message", MessageCreateView.as_view(), name="message_for_event"),
path("event/<int:pk>/update-from-source", update_from_source, name="update_from_source"),
path(
"event/<int:pk>/change-status/<status>",
@ -74,18 +98,18 @@ urlpatterns = [
path("mentions-legales", mentions_legales, name="mentions_legales"),
path("a-propos", about, name="about"),
path("merci", thank_you, name="thank_you"),
path("contact", ContactMessageCreateView.as_view(), name="contact"),
path("contactmessages", contactmessages, name="contactmessages"),
path("contactmessages/spams/delete", delete_cm_spam, name="delete_cm_spam"),
path("contact", MessageCreateView.as_view(), name="contact"),
path("messages", view_messages, name="messages"),
path("messages/spams/delete", delete_cm_spam, name="delete_cm_spam"),
path(
"contactmessage/<int:pk>",
ContactMessageUpdateView.as_view(),
name="contactmessage",
"message/<int:pk>",
MessageUpdateView.as_view(),
name="message",
),
path(
"contactmessage/<int:pk>/delete",
ContactMessageDeleteView.as_view(),
name="delete_contactmessage",
"message/<int:pk>/delete",
MessageDeleteView.as_view(),
name="delete_message",
),
path("imports/", imports, name="imports"),
path("imports/add", add_import, name="add_import"),
@ -133,7 +157,8 @@ urlpatterns = [
path("500/", internal_server_error, name="internal_server_error"),
path("organisme/<int:pk>/past", OrganisationDetailViewPast.as_view(), name="view_organisation_past"),
path("organisme/<int:pk>", OrganisationDetailView.as_view(), name="view_organisation"),
path("organisme/<int:pk>", OrganisationDetailView.as_view(), name="view_organisation_shortname"),
path("organisme/<int:pk>-<extra>", OrganisationDetailView.as_view(), name="view_organisation"),
path("organisme/<int:pk>-<extra>/past", OrganisationDetailViewPast.as_view(), name="view_organisation_past_fullname"),
path("organisme/<int:pk>-<extra>", OrganisationDetailView.as_view(), name="view_organisation_fullname"),
path("organisme/<int:pk>/edit", OrganisationUpdateView.as_view(), name="edit_organisation"),
@ -177,6 +202,14 @@ urlpatterns = [
re_path(r'^robots\.txt', include('robots.urls')),
path("__debug__/", include("debug_toolbar.urls")),
path("ckeditor5/", include('django_ckeditor_5.urls')),
path(
"sitemap.xml",
cache_page(86400)(sitemap),
{"sitemaps": sitemaps},
name="cached-sitemap",
),
path("cache/clear", clear_cache, name="clear_cache"),
]
if settings.DEBUG:

View File

@ -0,0 +1,114 @@
from agenda_culturel.models import ReferenceLocation
import re
import unicodedata
class PlaceGuesser:
def __init__(self):
self.__citynames = list(ReferenceLocation.objects.values_list("name__lower__unaccent", "name")) + [("clermont-fd", "Clermont-Ferrand"), ("aurillac", "Aurillac"), ("montlucon", "Montluçon"), ("montferrand", "Clermont-Ferrand")]
self.__citynames = [(x[0].replace("-", " "), x[1]) for x in self.__citynames]
def __remove_accents(self, input_str):
if input_str is None:
return None
nfkd_form = unicodedata.normalize("NFKD", input_str)
return "".join([c for c in nfkd_form if not unicodedata.combining(c)])
def __guess_is_address(self, part):
toponyms = ["bd", "rue", "avenue", "place", "boulevard", "allee", ]
part = part.strip()
if re.match(r'^[0-9]', part):
return True
elems = part.split(" ")
return any([self.__remove_accents(e.lower()) in toponyms for e in elems])
def __clean_address(self, addr):
toponyms = ["bd", "rue", "avenue", "place", "boulevard", "allée", "bis", "ter", "ZI"]
for t in toponyms:
addr = re.sub(" " + t + " ", " " + t + " ", addr, flags=re.IGNORECASE)
return addr
def __guess_city_name(self, part):
part = part.strip().replace(" - ", "-")
if len(part) == 0:
return None
part = self.__remove_accents(part.lower()).replace("-", " ")
match = [x[1] for x in self.__citynames if x[0] == part]
if len(match) > 0:
return match[0]
else:
return None
def __guess_city_name_postcode(self, part):
with_pc = re.search(r'^(.*)(([0-9][ ]*){5})(.*)$', part)
if with_pc:
p1 = self.__guess_city_name(with_pc.group(1).strip())
postcode = with_pc.group(2).replace(" ", "")
p2 = self.__guess_city_name(with_pc.group(4).strip())
return postcode, p2, p1
else:
return None, self.__guess_city_name(part), None
def __guess_name_address(self, part):
with_num = re.search(r'^(([^0-9])+)([0-9]+)(.*)', part)
if with_num:
name = with_num.group(1)
return name, part[len(name):]
else:
return "", part
def guess_address_elements(self, alias):
parts = re.split(r'[,/à]', alias)
parts = [p1 for p1 in [p.strip() for p in parts] if p1 != "" and p1.lower() != "france"]
name = ""
address = ""
postcode = ""
city = ""
possible_city = ""
oparts = []
for part in parts:
p, c, possible_c = self.__guess_city_name_postcode(part)
if not possible_c is None:
possible_city = possible_c
if not c is None and city == "":
city = c
if not p is None and postcode == "":
postcode = p
if p is None and c is None:
oparts.append(part)
if city == "" and possible_city != "":
city = possible_city
else:
if len(oparts) == 0 and not possible_city != "":
oparts = [possible_city]
if city == "":
alias_simple = self.__remove_accents(alias.lower()).replace("-", " ")
mc = [x[1] for x in self.__citynames if alias_simple.endswith(" " + x[0])]
if len(mc) == 1:
city = mc[0]
if len(oparts) > 0:
if not self.__guess_is_address(oparts[0]):
name = oparts[0]
address = ", ".join(oparts[1:])
else:
name, address = self.__guess_name_address(", ".join(oparts))
address = self.__clean_address(address)
if name == "" and possible_city != "" and possible_city != city:
name = possible_city
return name, address, postcode, city

View File

@ -1,6 +1,6 @@
from django.shortcuts import render, get_object_or_404
from django.views.generic import ListView, DetailView
from django.views.generic.edit import CreateView, UpdateView, DeleteView
from django.views.generic.edit import CreateView, UpdateView, DeleteView, ModelFormMixin
from django.contrib.auth.mixins import (
LoginRequiredMixin,
UserPassesTestMixin,
@ -10,6 +10,12 @@ from django import forms
from django.http import Http404
from django.contrib.postgres.search import SearchQuery, SearchHeadline
from django.utils.safestring import mark_safe
from django.utils.decorators import method_decorator
from honeypot.decorators import check_honeypot
from .utils import PlaceGuesser
import hashlib
from django.core.cache import cache
from django.contrib.gis.geos import Point
from django.contrib.gis.measure import D
@ -19,11 +25,9 @@ from django.urls import reverse
from collections import Counter
import emoji
from django.forms import formset_factory
from .forms import (
URLSubmissionForm,
EventForm,
EventFormWithContact,
BatchImportationForm,
FixDuplicates,
SelectEventInList,
@ -36,13 +40,18 @@ from .forms import (
MultipleHiddenInput,
EventModerateForm,
TagForm,
TagRenameForm
TagRenameForm,
MessageForm,
MessageEventForm,
URLSubmissionFormWithContact,
URLSubmissionFormSet,
SimpleContactForm,
)
from .filters import (
EventFilter,
EventFilterAdmin,
ContactMessagesFilterAdmin,
MessagesFilterAdmin,
SimpleSearchEventFilter,
SearchEventFilter,
DuplicatedEventsFilter,
@ -54,7 +63,7 @@ from .models import (
Category,
Tag,
StaticContent,
ContactMessage,
Message,
BatchImportation,
DuplicatedEvents,
RecurrentImport,
@ -68,7 +77,7 @@ from django.utils import timezone
from django.utils.html import escape
from datetime import date, timedelta
from django.utils.timezone import datetime
from django.db.models import Q, Subquery, OuterRef, Count, F, Func
from django.db.models import Q, Subquery, OuterRef, Count, F, Func, BooleanField, ExpressionWrapper
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
@ -217,7 +226,7 @@ def month_view(request, year=None, month=None):
def week_view(request, year=None, week=None, home=False):
now = date.today()
if year is None:
year = now.year
year = now.isocalendar()[0]
if week is None:
week = now.isocalendar()[1]
@ -292,7 +301,7 @@ def update_from_source(request, pk):
if url is None:
messages.warning(request, _("The event cannot be updated because the import process is not available for the referenced sources."))
else:
import_events_from_url.delay(url, None, True)
import_events_from_url.delay(url, None, None, True, user_id=request.user.pk if request.user else None)
messages.success(request, _("The event update has been queued and will be completed shortly."))
return HttpResponseRedirect(event.get_absolute_url())
@ -303,7 +312,6 @@ class EventUpdateView(
model = Event
permission_required = "agenda_culturel.change_event"
form_class = EventForm
success_message = _("The event has been successfully modified.")
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
@ -312,6 +320,16 @@ class EventUpdateView(
kwargs["is_simple_cloning"] = self.is_simple_cloning
return kwargs
def get_success_message(self, cleaned_data):
txt = _(" A message has been sent to the person who proposed the event.") if hasattr(self, "with_msg") else ""
return mark_safe(_('The event has been successfully modified.') + txt)
def form_valid(self, form):
form.instance.set_processing_user(self.request.user)
self.with_message = form.instance.notify_if_required(self.request)
return super().form_valid(form)
def get_initial(self):
self.is_cloning = "clone" in self.request.path.split('/')
self.is_simple_cloning = "simple-clone" in self.request.path.split('/')
@ -327,6 +345,7 @@ class EventUpdateView(
obj.save()
result["other_versions"] = obj.other_versions
result["status"] = Event.STATUS.PUBLISHED
result["cloning"] = True
if self.is_simple_cloning:
result["other_versions"] = None
@ -348,21 +367,28 @@ class EventModerateView(
permission_required = "agenda_culturel.change_event"
template_name = "agenda_culturel/event_form_moderate.html"
form_class = EventModerateForm
success_message = _("The event has been successfully moderated.")
def get_success_message(self, cleaned_data):
txt = _(" A message has been sent to the person who proposed the event.") if hasattr(self, "with_msg") else ""
return mark_safe(_('The event <a href="{}">{}</a> has been moderated with success.').format(self.object.get_absolute_url(), self.object.title) + txt)
def is_moderate_next(self):
return "moderate-next" in self.request.path.split('/')
return "after" in self.request.path.split('/')
def is_starting_moderation(self):
return not "pk" in self.kwargs
def is_moderation_from_date(self):
return "m" in self.kwargs and "y" in self.kwargs and "d" in self.kwargs
def get_next_event(self, start_day, start_time):
def get_next_event(start_day, start_time):
# select non moderated events
qs = Event.objects.filter(moderated_date__isnull=True)
# select events after the current one
if start_time:
qs = qs.filter(Q(start_day__gt=start_day)|(Q(start_day=start_day) & (Q(start_time__isnull=True)|Q(start_time__gte=start_time))))
qs = qs.filter(Q(start_day__gt=start_day)|(Q(start_day=start_day) & (Q(start_time__isnull=True)|Q(start_time__gt=start_time))))
else:
qs = qs.filter(start_day__gte=start_day)
@ -383,19 +409,15 @@ class EventModerateView(
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if self.is_moderate_next():
context['pred'] = self.kwargs["pk"]
context['pred'] = self.kwargs["pred"]
return context
def get_object(self, queryset=None):
if self.is_starting_moderation():
now = datetime.now()
return self.get_next_event(now.date(), now.time())
return EventModerateView.get_next_event(now.date(), now.time())
else:
result = super().get_object(queryset)
if self.is_moderate_next():
return self.get_next_event(result.start_day, result.start_time)
else:
return result
return super().get_object(queryset)
def post(self, request, *args, **kwargs):
try:
@ -407,15 +429,13 @@ class EventModerateView(
def form_valid(self, form):
form.instance.set_no_modification_date_changed()
form.instance.set_in_moderation_process()
form.instance.set_processing_user(self.request.user)
self.with_msg = form.instance.notify_if_required(self.request)
return super().form_valid(form)
def get_success_url(self):
if 'save_and_next' in self.request.POST:
return reverse_lazy("moderate_event_next", args=[self.object.pk])
elif 'save_and_create_local' in self.request.POST:
return reverse_lazy("clone_edit", args=[self.object.pk])
elif 'save_and_edit' in self.request.POST:
return reverse_lazy("edit_event", args=[self.object.pk])
elif 'save_and_edit_local' in self.request.POST:
return reverse_lazy("edit_event", args=[self.object.get_local_version().pk])
else:
@ -434,6 +454,30 @@ def error_next_event(request, pk):
{"pk": pk, "object": obj},
)
@login_required(login_url="/accounts/login/")
@permission_required("agenda_culturel.change_event")
def moderate_event_next(request, pk):
# current event
obj = Event.objects.filter(pk=pk).first()
start_day = obj.start_day
start_time = obj.start_time
next_obj = EventModerateView.get_next_event(start_day, start_time)
if next_obj is None:
return render(
request,
"agenda_culturel/event_next_error_message.html",
{"pk": pk, "object": obj},
)
else:
return HttpResponseRedirect(reverse_lazy("moderate_event_step", args=[next_obj.pk, obj.pk]))
@login_required(login_url="/accounts/login/")
@permission_required("agenda_culturel.change_event")
def moderate_from_date(request, y, m, d):
d = date(y, m, d)
obj = EventModerateView.get_next_event(d, None)
return HttpResponseRedirect(reverse_lazy("moderate_event", args=[obj.pk]))
class EventDeleteView(
@ -445,9 +489,12 @@ class EventDeleteView(
success_message = _("The event has been successfully deleted.")
class EventDetailView(UserPassesTestMixin, DetailView):
class EventDetailView(UserPassesTestMixin, DetailView, ModelFormMixin):
model = Event
form_class = MessageEventForm
template_name = "agenda_culturel/page-event.html"
queryset = Event.objects.select_related("exact_location").select_related("category").select_related("other_versions").select_related("other_versions__representative").prefetch_related("message_set")
def test_func(self):
return (
@ -457,6 +504,7 @@ class EventDetailView(UserPassesTestMixin, DetailView):
def get_object(self):
o = super().get_object()
o.download_missing_image()
y = self.kwargs["year"]
m = self.kwargs["month"]
d = self.kwargs["day"]
@ -464,6 +512,30 @@ class EventDetailView(UserPassesTestMixin, DetailView):
obj.set_current_date(date(y, m, d))
return obj
def get_success_url(self):
return self.get_object().get_absolute_url() + "#chronology"
def post(self, request, *args, **kwargs):
if not request.user.is_authenticated:
return HttpResponseForbidden()
form = self.get_form()
if form.is_valid():
return self.form_valid(form)
else:
return self.form_invalid(form)
def form_valid(self, form):
message = form.save(commit=False)
message.user = self.request.user
message.related_event = self.get_object()
message.subject = _("Comment")
message.spam = False
message.closed = True
message.save()
return super().form_valid(form)
@login_required(login_url="/accounts/login/")
@ -474,7 +546,11 @@ def change_status_event(request, pk, status):
if request.method == "POST":
event.status = Event.STATUS(status)
event.save(update_fields=["status"])
messages.success(request, _("The status has been successfully modified."))
with_msg = event.notify_if_required(request)
if with_msg:
messages.success(request, _("The status has been successfully modified and a message has been sent to the person who proposed the event."))
else:
messages.success(request, _("The status has been successfully modified."))
return HttpResponseRedirect(event.get_absolute_url())
@ -493,7 +569,7 @@ def import_event_proxy(request):
class EventCreateView(SuccessMessageMixin, CreateView):
model = Event
form_class = EventForm
form_class = EventFormWithContact
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
@ -517,31 +593,32 @@ class EventCreateView(SuccessMessageMixin, CreateView):
if form.cleaned_data['simple_cloning']:
form.instance.set_skip_duplicate_check()
if form.cleaned_data['cloning']:
form.instance.set_in_moderation_process()
if form.cleaned_data.get("email") or form.cleaned_data.get("comments"):
has_comments = not form.cleaned_data.get("comments") in ["", None]
form.instance.set_message(
Message(subject=_('during the creation process'),
message=form.cleaned_data.get("comments"),
email=form.cleaned_data.get("email"),
closed=False,
message_type=Message.TYPE.FROM_CONTRIBUTOR if has_comments else Message.TYPE.FROM_CONTRIBUTOR_NO_MSG))
form.instance.import_sources = None
form.instance.set_processing_user(self.request.user)
return super().form_valid(form)
result = super().form_valid(form)
if form.cleaned_data['cloning']:
with_msg = form.instance.notify_if_required(self.request)
if with_msg:
messages.success(self.request, _("A message has been sent to the person who proposed the initial event."))
return result
def import_from_details(request):
form = EventForm(request.POST, is_authenticated=request.user.is_authenticated)
if form.is_valid():
new_event = form.save()
if request.user.is_authenticated:
messages.success(request, _("The event is saved."))
return HttpResponseRedirect(new_event.get_absolute_url())
else:
messages.success(
request,
_(
"The event has been submitted and will be published as soon as it has been validated by the moderation team."
),
)
return HttpResponseRedirect(reverse("home"))
else:
return render(
request, "agenda_culturel/event_form.html", context={"form": form}
)
# A class to evaluate the URL according to the existing events and the authentification
# level of the user
@ -608,11 +685,14 @@ class URLEventEvaluation:
def import_from_urls(request):
URLSubmissionFormSet = formset_factory(URLSubmissionForm, extra=9, min_num=1)
if request.method == "POST":
formset = URLSubmissionFormSet(request.POST, request.FILES)
if formset.is_valid():
if not request.user.is_authenticated:
contactform = SimpleContactForm(request.POST)
if formset.is_valid() and (request.user.is_authenticated or contactform.is_valid()):
# evaluate all the forms
ucat = [URLEventEvaluation(form, request.user.is_authenticated) for form in formset.forms]
@ -639,20 +719,32 @@ def import_from_urls(request):
request,
_('Integrating {} url(s) into our import process.').format(len(ucat))
)
import_events_from_urls.delay(ucat)
email = None
comments = None
if not request.user.is_authenticated:
email = contactform.cleaned_data["email"]
comments = contactform.cleaned_data["comments"]
import_events_from_urls.delay(ucat,
user_id=request.user.pk if request.user else None,
email=email, comments=comments)
return HttpResponseRedirect(reverse("thank_you"))
else:
return HttpResponseRedirect(reverse("home"))
else:
formset = URLSubmissionFormSet()
if not request.user.is_authenticated:
contactform = SimpleContactForm()
return render(request, "agenda_culturel/import_set.html", context={"formset": formset})
context = {"formset": formset}
if not request.user.is_authenticated:
context["contactform"] = contactform
return render(request, "agenda_culturel/import_set.html", context=context)
def import_from_url(request):
form = URLSubmissionForm()
form = URLSubmissionFormWithContact(is_authenticated=request.user.is_authenticated)
initial = {
"start_day": date.today() + timedelta(days=1),
@ -664,7 +756,7 @@ def import_from_url(request):
# if the form has been sent
if request.method == "POST":
form = URLSubmissionForm(request.POST)
form = URLSubmissionFormWithContact(request.POST, is_authenticated=request.user.is_authenticated)
# if the form is valid
if form.is_valid():
@ -689,7 +781,7 @@ def import_from_url(request):
request,
_('Integrating {} into our import process.').format(uc.url)
)
import_events_from_url.delay(uc.url, uc.cat, uc.tags)
import_events_from_url.delay(uc.url, uc.cat, uc.tags, user_id=request.user.pk if request.user else None, email=form.cleaned_data.get("email"), comments=form.cleaned_data.get("comments"))
return HttpResponseRedirect(reverse("thank_you"))
@ -707,7 +799,7 @@ def export_event_ical(request, year, month, day, pk):
events = list()
events.append(event)
cal = Event.export_to_ics(events)
cal = Event.export_to_ics(events, request)
response = HttpResponse(content_type="text/calendar")
response.content = cal.to_ical().decode("utf-8").replace("\r\n", "\n")
@ -717,14 +809,17 @@ def export_event_ical(request, year, month, day, pk):
return response
def export_ical(request):
now = date.today()
request = EventFilter.set_default_values(request)
filter = EventFilter(request.GET, queryset=get_event_qs(request), request=request)
calendar = CalendarList(now + timedelta(days=-7), now + timedelta(days=+60), filter)
ical = calendar.export_to_ics()
id_cache = hashlib.md5(filter.get_url().encode("utf8")).hexdigest()
ical = cache.get(id_cache)
if not ical:
calendar = CalendarList(now + timedelta(days=-7), now + timedelta(days=+60), filter)
ical = calendar.export_to_ics(request)
cache.set(id_cache, ical, 3600) # 1 heure
response = HttpResponse(content_type="text/calendar")
response.content = ical.to_ical().decode("utf-8").replace("\r\n", "\n")
@ -735,41 +830,71 @@ def export_ical(request):
return response
class ContactMessageCreateView(SuccessMessageMixin, CreateView):
model = ContactMessage
template_name = "agenda_culturel/contactmessage_create_form.html"
fields = ["subject", "name", "email", "message"]
@method_decorator(check_honeypot, name='post')
class MessageCreateView(SuccessMessageMixin, CreateView):
model = Message
template_name = "agenda_culturel/message_create_form.html"
form_class = MessageForm
success_url = reverse_lazy("home")
success_message = _("Your message has been sent successfully.")
def __init__(self, *args, **kwargs):
self.event = None
super().__init__(*args, **kwargs)
def get_form(self, form_class=None):
if form_class is None:
form_class = self.get_form_class()
return form_class(**self.get_form_kwargs())
class ContactMessageDeleteView(SuccessMessageMixin, DeleteView):
model = ContactMessage
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["event"] = self.event
if self.request.user.is_authenticated:
kwargs["internal"] = True
return kwargs
def form_valid(self, form):
if self.request.user.is_authenticated:
form.instance.user = self.request.user
form.instance.message_type = Message.TYPE.EVENT_REPORT if "pk" in self.kwargs else Message.TYPE.CONTACT_FORM
return super().form_valid(form)
def get_initial(self):
result = super().get_initial()
if "pk" in self.kwargs:
self.event = get_object_or_404(Event, pk=self.kwargs["pk"])
result["related_event"] = self.event
result["subject"] = _('Reporting the event {} on {}').format(self.event.title, self.event.start_day)
else:
result["related_event"] = None
return result
class MessageDeleteView(SuccessMessageMixin, DeleteView):
model = Message
success_message = _(
"The contact message has been successfully deleted."
)
success_url = reverse_lazy("contactmessages")
success_url = reverse_lazy("messages")
class ContactMessageUpdateView(
class MessageUpdateView(
SuccessMessageMixin, PermissionRequiredMixin, LoginRequiredMixin, UpdateView
):
model = ContactMessage
permission_required = "agenda_culturel.change_contactmessage"
template_name = "agenda_culturel/contactmessage_moderation_form.html"
model = Message
permission_required = "agenda_culturel.change_message"
template_name = "agenda_culturel/message_moderation_form.html"
fields = ("spam", "closed", "comments")
success_message = _(
"The contact message properties has been successfully modified."
)
success_url = reverse_lazy("contactmessages")
success_url = reverse_lazy("messages")
def get_form_kwargs(self):
"""Return the keyword arguments for instantiating the form."""
@ -806,23 +931,27 @@ def activite(request):
@login_required(login_url="/accounts/login/")
@permission_required("agenda_culturel.view_event")
def administration(request):
nb_mod_days = 21
nb_classes = 4
today = date.today()
start_time = datetime.now().time()
# get information about recent modifications
days = [date.today()]
days = [today]
for i in range(0, 2):
days.append(days[-1] + timedelta(days=-1))
daily_modifications = Event.get_count_modifications([(d, 1) for d in days])
# get last created events
events = Event.objects.all().order_by("-created_date")[:5]
events = Event.objects.all().order_by("-created_date").select_related("exact_location", "category")[:5]
# get last batch imports
batch_imports = BatchImportation.objects.all().order_by("-created_date")[:5]
batch_imports = BatchImportation.objects.all().select_related("recurrentImport").order_by("-created_date")[:5]
# get info about batch information
newest = BatchImportation.objects.filter(recurrentImport=OuterRef("pk")).order_by(
"-created_date"
)
).select_related("recurrentImport")
imported_events = RecurrentImport.objects.annotate(
last_run_status=Subquery(newest.values("status")[:1])
)
@ -838,13 +967,41 @@ def administration(request):
.count())
nb_all = imported_events.count()
window_end = today + timedelta(days=nb_mod_days)
# get all non moderated events
nb_not_moderated = Event.objects.filter(~Q(status=Event.STATUS.TRASH)). \
filter(Q(start_day__gte=today)&Q(start_day__lte=window_end)). \
filter(
Q(other_versions__isnull=True) |
Q(other_versions__representative=F('pk')) |
Q(other_versions__representative__isnull=True)).values("start_day").\
annotate(not_moderated=Count("start_day", filter=Q(moderated_date__isnull=True))). \
annotate(nb_events=Count("start_day")). \
order_by("start_day").values("not_moderated", "nb_events", "start_day")
max_not_moderated = max([x["not_moderated"] for x in nb_not_moderated])
if max_not_moderated == 0:
max_not_moderated = 1
nb_not_moderated_dict = dict([(x["start_day"], (x["not_moderated"], x["nb_events"])) for x in nb_not_moderated])
# add missing dates
date_list = [today + timedelta(days=x) for x in range(0, nb_mod_days)]
nb_not_moderated = [{"start_day": d,
"is_today": d == today,
"nb_events": nb_not_moderated_dict[d][1] if d in nb_not_moderated_dict else 0,
"not_moderated": nb_not_moderated_dict[d][0] if d in nb_not_moderated_dict else 0} for d in date_list]
nb_not_moderated = [ x | { "note": 0 if x["not_moderated"] == 0 else int((nb_classes - 1) * x["not_moderated"] / max_not_moderated) + 1 } for x in nb_not_moderated]
nb_not_moderated = [nb_not_moderated[x:x + 7] for x in range(0, len(nb_not_moderated), 7)]
return render(
request,
"agenda_culturel/administration.html",
{"daily_modifications": daily_modifications,
"events": events, "batch_imports": batch_imports,
"nb_failed": nb_failed, "nb_canceled": nb_canceled,
"nb_running": nb_running, "nb_all": nb_all},
"nb_running": nb_running, "nb_all": nb_all,
"nb_not_moderated": nb_not_moderated},
)
@ -873,15 +1030,15 @@ def recent(request):
@login_required(login_url="/accounts/login/")
@permission_required("agenda_culturel.view_contactmessage")
def contactmessages(request):
filter = ContactMessagesFilterAdmin(
request.GET, queryset=ContactMessage.objects.all().order_by("-date")
@permission_required("agenda_culturel.view_message")
def view_messages(request):
filter = MessagesFilterAdmin(
request.GET, queryset=Message.objects.all().order_by("-date")
)
paginator = PaginatorFilter(filter, 10, request)
page = request.GET.get("page")
nb_spams = ContactMessage.objects.filter(spam=True).count()
nb_spams = Message.objects.filter(spam=True).count()
try:
response = paginator.page(page)
@ -892,24 +1049,24 @@ def contactmessages(request):
return render(
request,
"agenda_culturel/contactmessages.html",
"agenda_culturel/messages.html",
{"filter": filter, "nb_spams": nb_spams, "paginator_filter": response},
)
@login_required(login_url="/accounts/login/")
@permission_required("agenda_culturel.view_contactmessage")
@permission_required("agenda_culturel.view_message")
def delete_cm_spam(request):
if request.method == "POST":
ContactMessage.objects.filter(spam=True).delete()
Message.objects.filter(spam=True).delete()
messages.success(request, _("Spam has been successfully deleted."))
return HttpResponseRedirect(reverse_lazy("contactmessages"))
return HttpResponseRedirect(reverse_lazy("messages"))
else:
nb_msgs = ContactMessage.objects.values('spam').annotate(total=Count('spam'))
nb_msgs = Message.objects.values('spam').annotate(total=Count('spam'))
nb_total = sum([nb["total"] for nb in nb_msgs])
nb_spams = sum([nb["total"] for nb in nb_msgs if nb["spam"]])
cancel_url = reverse_lazy("contactmessages")
cancel_url = reverse_lazy("messages")
return render(
request,
"agenda_culturel/delete_spams_confirm.html",
@ -1465,6 +1622,7 @@ def set_duplicate(request, year, month, day, pk):
event.other_versions is None
or event.other_versions != e.other_versions
)
and e.status != Event.STATUS.TRASH
]
form = SelectEventInList(events=others)
@ -1840,6 +1998,7 @@ class UnknownPlaceAddView(PermissionRequiredMixin, SuccessMessageMixin, UpdateVi
class PlaceFromEventCreateView(PlaceCreateView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["event"] = self.event
@ -1850,6 +2009,14 @@ class PlaceFromEventCreateView(PlaceCreateView):
self.event = get_object_or_404(Event, pk=self.kwargs["pk"])
if self.event.location and "add" in self.request.GET:
initial["aliases"] = [self.event.location]
guesser = PlaceGuesser()
name, address, postcode, city = guesser.guess_address_elements(self.event.location)
initial["name"] = name
initial["address"] = address
initial["postcode"] = postcode
initial["city"] = city
initial["location"] = ""
return initial
def form_valid(self, form):
@ -1971,10 +2138,10 @@ def view_tag(request, t):
except EmptyPage:
response = paginator.page(paginator.num_pages)
rimports = RecurrentImport.objects.filter(defaultTags__contains=[t])
tag = Tag.objects.filter(name=t).first()
context = {"tag": t, "paginator_filter": response, "object": tag}
context = {"tag": t, "paginator_filter": response, "object": tag, "rimports": rimports}
return render(request, "agenda_culturel/tag.html", context)
@ -2037,11 +2204,21 @@ def rename_tag(request, t):
e.tags += [new_name]
Event.objects.bulk_update(events, fields=["tags"])
# find all recurrent imports and fix them
rimports = RecurrentImport.objects.filter(defaultTags__contains=[t])
for ri in rimports:
ri.tags = [te for te in ri.defaultTags if te != t]
if not new_name in ri.tags:
ri.tags += [new_name]
RecurrentImport.objects.bulk_update(rimports, fields=["defaultTags"])
# find tag object
tag_object = Tag.objects.filter(name=t).first()
if tag_object:
tag_object.name = new_name
tag_object.save()
messages.success(
request,
(_(
@ -2060,8 +2237,6 @@ def rename_tag(request, t):
@permission_required("agenda_culturel.delete_tag")
def delete_tag(request, t):
respage = reverse_lazy("view_all_tags")
nb = Event.objects.filter(tags__contains=[t]).count()
obj = Tag.objects.filter(name=t).first()
if request.method == "POST":
@ -2071,6 +2246,12 @@ def delete_tag(request, t):
e.tags = [te for te in e.tags if te != t]
Event.objects.bulk_update(events, fields=["tags"])
# remove tag from recurrent imports
rimports = RecurrentImport.objects.filter(defaultTags__contains=[t])
for ri in rimports:
ri.tags = [te for te in ri.defaultTags if te != t]
RecurrentImport.objects.bulk_update(rimports, fields=["defaultTags"])
# find tag object
tag_object = Tag.objects.filter(name=t).first()
if tag_object:
@ -2084,11 +2265,26 @@ def delete_tag(request, t):
)
return HttpResponseRedirect(respage)
else:
nb = Event.objects.filter(tags__contains=[t]).count()
obj = Tag.objects.filter(name=t).first()
nbi = RecurrentImport.objects.filter(defaultTags__contains=[t]).count()
cancel_url = request.META.get("HTTP_REFERER", "")
if cancel_url == "":
cancel_url = respage
return render(
request,
"agenda_culturel/tag_confirm_delete_by_name.html",
{"tag": t, "nb": nb, "cancel_url": cancel_url, "obj": obj},
{"tag": t, "nb": nb, "nbi": nbi, "cancel_url": cancel_url, "obj": obj},
)
def clear_cache(request):
if request.method == "POST":
cache.clear()
messages.success(request, _("Cache successfully cleared."))
return HttpResponseRedirect(reverse_lazy("administration"))
else:
return render(
request,
"agenda_culturel/clear_cache.html",
)

View File

@ -42,4 +42,5 @@ django-location-field==2.7.3
django-robots==6.1
django-debug-toolbar==4.4.6
django-cache-cleaner==0.1.0
emoji==2.14.0
emoji==2.14.0
django-honeypot==1.2.1

View File

@ -289,6 +289,7 @@
{
"com_name": "Clermont-Ferrand",
"main": 10,
"suggested": 10,
"geo_point_2d": {
"lon": 3.1153994509459313,
"lat": 45.78590931605406
@ -339,6 +340,7 @@
{
"com_name": "Riom",
"main": 3,
"suggested": 10,
"geo_point_2d": {
"lon": 3.13259085594027,
"lat": 45.89435053196184

View File

@ -13,7 +13,10 @@ def run():
# remove all locations
ReferenceLocation.objects.all().delete()
objs = [ReferenceLocation(location=Point(c["geo_point_2d"]["lon"], c["geo_point_2d"]["lat"]), name=c["com_name"], main=c["main"] if "main" in c else 0) for c in data]
objs = [ReferenceLocation(location=Point(c["geo_point_2d"]["lon"], c["geo_point_2d"]["lat"]),
name=c["com_name"],
main=c["main"] if "main" in c else 0,
suggested_distance=c["suggested"] if "suggested" in c else None) for c in data]
objs = ReferenceLocation.objects.bulk_create(objs, ignore_conflicts=True)

View File

@ -1,5 +1,5 @@
from agenda_culturel.models import ContactMessage
from agenda_culturel.models import Message
def run():
ContactMessage.objects.all().update(spam=True)
Message.objects.all().update(spam=True)