Compare commits

..

186 Commits

Author SHA1 Message Date
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
ce602c10bd ajout de catégories à ne pas importer 2024-11-27 14:42:55 +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
Jean-Marie Favreau
1287d9ee06 Correction création par fusion
Fix #218
2024-11-27 12:20:27 +01:00
Jean-Marie Favreau
d7ec80ff01 on filtre les événements debrif de la comédie qui ne nous intéressent pas
Voir #216
2024-11-27 11:39:51 +01:00
Jean-Marie Favreau
555bae8dc8 Ajout fichier manquant 2024-11-27 11:39:41 +01:00
Jean-Marie Favreau
ac8ddc5123 Fix import 2024-11-27 11:38:53 +01:00
Jean-Marie Favreau
f9678bbf81 Fix erreur dans l'import (visible avec le fotomat) 2024-11-27 11:04:34 +01:00
2680622dfc On ajoute un autre robot récurrent (IA) 2024-11-25 00:41:34 +01:00
Jean-Marie Favreau
3c5b5a9fd6 Fix bot blocker 2024-11-25 00:12:03 +01:00
Jean-Marie Favreau
db7604623c on bloque aussi le robot de meta 2024-11-25 00:06:22 +01:00
Jean-Marie Favreau
afa1844d21 On affiche 20 imports récurrents plutôt que 10 par page
Fix #193
2024-11-24 19:15:15 +01:00
Jean-Marie Favreau
b0b653c1b1 On ne montre que les événements publics aux utilisateurs non connectés 2024-11-24 19:11:52 +01:00
Jean-Marie Favreau
3001685937 Amélioration UX 2024-11-24 18:59:21 +01:00
Jean-Marie Favreau
a3e13429eb Ajout d'un bouton pour éditer après modération
Fix #215
2024-11-24 12:52:42 +01:00
Jean-Marie Favreau
ea5372cae5 Amélioration de la performance des templates 2024-11-24 12:40:43 +01:00
Jean-Marie Favreau
5e65ecdb5c On fixe le cas des événements sans description 2024-11-24 12:37:20 +01:00
Jean-Marie Favreau
ed7944aaa9 Ajout d'un outil de profiling 2024-11-24 12:05:28 +01:00
Jean-Marie Favreau
5a2dea6989 On regénère le cache du css qu'une seule fois par jour 2024-11-24 11:55:37 +01:00
Jean-Marie Favreau
98092de1f0 On ne met pas à jour les informations modérées quand on met à jour un import
Fix #214
2024-11-24 10:56:05 +01:00
Jean-Marie Favreau
03e10e91e2 On supprime tags en double 2024-11-24 10:48:43 +01:00
Jean-Marie Favreau
7a9e74b057 Au moment de l'import, si un événement fait plus de x jours, on le place comme brouillon
Fix #194
2024-11-23 16:51:22 +01:00
Jean-Marie Favreau
0872af5144 On peut filtrer les imports récurrents 2024-11-23 16:31:04 +01:00
Jean-Marie Favreau
720a187116 On peut supprimer le champ 2024-11-23 16:30:54 +01:00
Jean-Marie Favreau
3d8fd1cfdf Fix html 2024-11-23 16:30:23 +01:00
Jean-Marie Favreau
524d178055 Amélioration choix 2024-11-23 15:57:03 +01:00
Jean-Marie Favreau
2da854545f Quelques optimisation BD 2024-11-23 15:00:55 +01:00
Jean-Marie Favreau
5a66caae55 On remplace prefetch_related par select_related là où c'est possible 2024-11-23 10:52:59 +01:00
Jean-Marie Favreau
ecc347219c Fix url bas de page 2024-11-23 09:19:35 +01:00
Jean-Marie Favreau
918e19fa4f ajout des imports récurrents dans les résultats de la recherche 2024-11-23 00:34:17 +01:00
Jean-Marie Favreau
70260fcb4f On ajoute un lien pour faciliter l'administration 2024-11-23 00:28:37 +01:00
Jean-Marie Favreau
0190d91268 On ajoute les organisateurs dans les résultats de recherche 2024-11-23 00:06:48 +01:00
Jean-Marie Favreau
769c607550 On met à jour les sources d'importation, car dans le cas où un événement
a été importé indépendamment auparavant, on veut aussi qu'il soit associé malgré tout
à l'importation récurrente.
2024-11-23 00:01:46 +01:00
Jean-Marie Favreau
7f029ae541 On affiche l'organisateur 2024-11-22 23:55:07 +01:00
Jean-Marie Favreau
386eca261a On supprime les traces inutiles 2024-11-22 23:54:57 +01:00
Jean-Marie Favreau
37817cc8f5 On introduit la notion d'organisateur
Fix #202
2024-11-22 23:30:46 +01:00
Jean-Marie Favreau
96401b6519 Affichage des listes sous forme de puce dans les diff 2024-11-22 19:20:59 +01:00
Jean-Marie Favreau
4e1441a92f Fix événements de lieux dans le passé 2024-11-22 19:20:12 +01:00
Jean-Marie Favreau
b569464894 mise à jour traductions 2024-11-22 10:48:17 +01:00
Jean-Marie Favreau
507670ebde Suppression d'espace en trop 2024-11-21 22:17:18 +01:00
Jean-Marie Favreau
c5c68bcfef Les catégories d'import ont l'utf8 à tous les étages 2024-11-21 00:29:51 +01:00
Jean-Marie Favreau
d39ea43efb les tags ne sont plus des sous-catégories 2024-11-20 22:55:04 +01:00
Jean-Marie Favreau
11bd53cbeb Quand on renomme, on renomme 2024-11-20 22:36:56 +01:00
Jean-Marie Favreau
4cc6db84e2 Amélioration du rendu 2024-11-20 21:34:10 +01:00
Jean-Marie Favreau
463dd6b3b9 Ajout d'une description aux lieux
Fix #204
2024-11-20 16:49:09 +01:00
Jean-Marie Favreau
09c2c2117c On saute à la ligne pour plus de visibilité 2024-11-20 13:08:59 +01:00
Jean-Marie Favreau
c4bb86dab4 on trie les tags sur les événements 2024-11-20 13:07:08 +01:00
Jean-Marie Favreau
283ffc4348 On force la langue française 2024-11-20 11:16:21 +01:00
Jean-Marie Favreau
2a0abf8e5a Fix mise en page 2024-11-20 11:15:36 +01:00
Jean-Marie Favreau
62b73dd836 Amélioration des messages de haut de page 2024-11-20 11:11:20 +01:00
Jean-Marie Favreau
1e278581ed Ajout d'une fonction de copie d'événements
Fix #208
2024-11-20 11:06:27 +01:00
Jean-Marie Favreau
0924d5d36c Amélioration (détails) 2024-11-20 10:12:04 +01:00
Jean-Marie Favreau
be62272487 Ajout d'étoiles pour les champs obligatoires 2024-11-20 00:40:42 +01:00
Jean-Marie Favreau
bf5db35e57 On trie les emoji en premier
Fix #212
2024-11-19 23:36:33 +01:00
Jean-Marie Favreau
af2948827d Ajout nb modérations 2024-11-19 22:04:42 +01:00
Jean-Marie Favreau
182208a6f8 Quand le fichier est absent, on ne le copie pas
Fix #211
2024-11-19 20:39:08 +01:00
Jean-Marie Favreau
9ad3e9e972 On supprime la création de tags pendant la migration 2024-11-19 20:36:05 +01:00
Jean-Marie Favreau
fe97c4cb32 Fix problème mise en page 2024-11-18 00:23:37 +01:00
Jean-Marie Favreau
956ec7210c Les trigger warning sont signalés
Fix #201
2024-11-17 23:59:25 +01:00
Jean-Marie Favreau
5a6f33f8e2 Fix templates 2024-11-17 23:31:25 +01:00
Jean-Marie Favreau
c3f6d6920e Correction résultats recherche avec tags
Fix #203
2024-11-17 23:23:15 +01:00
Jean-Marie Favreau
47aedc706b amélioration gestion des étiquettes:
- suppression des étiquettes: Fix #197
- renommage des étiquettes: Fix #200
2024-11-17 22:52:19 +01:00
Jean-Marie Favreau
1e9698da91 Amélioration de la vue des étiquettes 2024-11-17 20:52:04 +01:00
Jean-Marie Favreau
4a0f5b3b14 Correction problème page 2024-11-17 15:17:19 +01:00
Jean-Marie Favreau
33a68ee7eb correction recherche 2024-11-17 15:04:06 +01:00
Jean-Marie Favreau
9cab07cb6f On trie les lieux de manière plus pratique 2024-11-17 14:12:14 +01:00
Jean-Marie Favreau
6efd6f18c8 Fix calcul distance 2024-11-17 13:42:58 +01:00
Jean-Marie Favreau
493b42c457 Ajout d'un mécanisme de modération en chaîne 2024-11-16 22:06:28 +01:00
Jean-Marie Favreau
0be3c30489 Ajout de l'information que l'événement a déjà été modifié 2024-11-16 20:47:10 +01:00
Jean-Marie Favreau
44a04deb26 Amélioration de la mise en page 2024-11-16 20:27:29 +01:00
Jean-Marie Favreau
43e1d3fd26 Ajout de la traduction manquante 2024-11-16 20:17:33 +01:00
Jean-Marie Favreau
ae542f76c8 Correction des traductions 2024-11-16 20:15:28 +01:00
Jean-Marie Favreau
5cfb53de23 Ajout d'un peu d'explication au sujet des conséquences de l'utilisation de la modération 2024-11-16 20:14:17 +01:00
Jean-Marie Favreau
11d5cf9aa4 Ajout d'un outil de modération pour ajouter modifier quelques éléments simples d'un événement sans le modifiier 2024-11-16 20:11:10 +01:00
Jean-Marie Favreau
e3c14437ac Le formulaire d'édition d'événements ne permet de sélectionner que des étiquettes
existantes, et avec l'interface "moderne"
2024-11-16 19:00:02 +01:00
Jean-Marie Favreau
28f5b2a01b Suppression d'include inutiles 2024-11-15 17:11:12 +01:00
Jean-Marie Favreau
0263976573 Ajout d'une illustration dans le about 2024-11-15 17:10:46 +01:00
Jean-Marie Favreau
79a73d6459 Fix problème clés trop longues (cache) 2024-11-15 16:38:44 +01:00
Jean-Marie Favreau
3bd4ef5771 On sépare le code pour plus de lisibilité 2024-11-15 16:35:32 +01:00
Jean-Marie Favreau
637b976442 Amélioration css 2024-11-15 14:46:51 +01:00
Jean-Marie Favreau
d47991d1e0 Alignement des liens internes (entête sticky) 2024-11-15 14:41:49 +01:00
Jean-Marie Favreau
350a555bea Ajustement 2024-11-15 01:05:27 +01:00
Jean-Marie Favreau
dbf62f3b4a Traduction 2024-11-15 01:03:37 +01:00
Jean-Marie Favreau
4af14c523c Ajout des tags au formulaire de soumission
Fix #158
2024-11-15 00:52:11 +01:00
Jean-Marie Favreau
0ae9c399dd Par défaut, un événemnt dure 2 heures quand on calcule les intersections 2024-11-14 23:40:52 +01:00
Jean-Marie Favreau
e767babd8e Tentative de corriger ce truc de cache 2024-11-14 23:24:06 +01:00
Jean-Marie Favreau
df27949036 Le cache des pages ne contient pas le formulaire 2024-11-14 22:38:39 +01:00
Jean-Marie Favreau
1f60bf0c39 Ajout de l'affichage des erreurs du filtre 2024-11-14 19:36:17 +01:00
Jean-Marie Favreau
bb6d83f6fb On supprime les titres inutiles 2024-11-14 00:09:36 +01:00
Jean-Marie Favreau
f93a6164ca Amélioration des filtres de tags:
- on ne peut pas éditer les tags
- on ne passe pas search_items dans le POST
2024-11-14 00:06:15 +01:00
Jean-Marie Favreau
fe1061e638 Ajout de suggestions dans les filtres d'inclusion et exclusion 2024-11-13 23:52:02 +01:00
Jean-Marie Favreau
ed2f530f0c Amélioration des preformances des requêtes 2024-11-13 12:00:21 +01:00
Jean-Marie Favreau
0bdd8693ec Amélioration de la requête des tags 2024-11-13 11:22:06 +01:00
Jean-Marie Favreau
2ce8f30275 On renomme "moderation" en "recent" (plus logique) 2024-11-13 11:00:41 +01:00
Jean-Marie Favreau
4c2dd9e98c Suppression des questions de migration pas utilisées (branche à part) 2024-11-13 10:56:45 +01:00
136 changed files with 6998 additions and 2283 deletions

View File

@ -36,7 +36,7 @@ Pour ajouter une nouvelle source custom:
### Récupérer un dump du prod sur un serveur dev ### Récupérer un dump du prod sur un serveur dev
* sur le serveur de 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: * sur le serveur de prod:
* On récupère le dump json ```scp $SERVEUR:$PATH/fixtures/postgres-backup-20241101.json src/fixtures/``` * 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 * ```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 \ RUN --mount=type=cache,target=/var/cache/apt \
apt-get update && \ 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/* && rm -rf /var/lib/apt/lists/*
COPY src/requirements.txt ./requirements.txt COPY src/requirements.txt ./requirements.txt
RUN --mount=type=cache,target=/root/.cache/pip \ RUN --mount=type=cache,target=/root/.cache/pip \

View File

@ -32,7 +32,7 @@ http {
error_page 502 /static/html/500.html; error_page 502 /static/html/500.html;
error_page 503 /static/html/500.html; error_page 503 /static/html/500.html;
if ($http_user_agent ~* "(?:Amazonbot)") { if ($http_user_agent ~* (Amazonbot|meta-externalagent|ClaudeBot)) {
return 444; return 444;
} }

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" echobold "Setup database stucture according to the selected commit"
docker exec -i agenda_culturel-backend python3 manage.py migrate agenda_culturel 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 # import data
echobold "Import data" echobold "Import data"
docker exec -i agenda_culturel-backend python3 manage.py loaddata --format=json $FFILE docker exec -i agenda_culturel-backend python3 manage.py loaddata --format=json $FFILE
@ -85,7 +89,4 @@ git checkout main
echobold "Update database" echobold "Update database"
docker exec -i agenda_culturel-backend python3 manage.py migrate agenda_culturel 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,8 +9,9 @@ from .models import (
BatchImportation, BatchImportation,
RecurrentImport, RecurrentImport,
Place, Place,
ContactMessage, Message,
ReferenceLocation ReferenceLocation,
Organisation
) )
from django_better_admin_arrayfield.admin.mixins import DynamicArrayMixin from django_better_admin_arrayfield.admin.mixins import DynamicArrayMixin
from django_better_admin_arrayfield.forms.widgets import DynamicArrayWidget from django_better_admin_arrayfield.forms.widgets import DynamicArrayWidget
@ -24,8 +25,9 @@ admin.site.register(DuplicatedEvents)
admin.site.register(BatchImportation) admin.site.register(BatchImportation)
admin.site.register(RecurrentImport) admin.site.register(RecurrentImport)
admin.site.register(Place) admin.site.register(Place)
admin.site.register(ContactMessage) admin.site.register(Message)
admin.site.register(ReferenceLocation) admin.site.register(ReferenceLocation)
admin.site.register(Organisation)
class URLWidget(DynamicArrayWidget): class URLWidget(DynamicArrayWidget):

View File

@ -117,6 +117,23 @@ class DayInCalendar:
if e.start_time is None if e.start_time is None
else e.start_time 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): def events_by_category_ordered(self):
from .models import Category from .models import Category
@ -175,12 +192,13 @@ class IntervalInDay(DayInCalendar):
self.id = self.id + '-' + str(id) self.id = self.id + '-' + str(id)
class CalendarList: 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.firstdate = firstdate
self.lastdate = lastdate self.lastdate = lastdate
self.now = date.today() self.now = date.today()
self.filter = filter self.filter = filter
self.ignore_dup = ignore_dup self.ignore_dup = ignore_dup
self.qs = qs
if exact: if exact:
self.c_firstdate = self.firstdate self.c_firstdate = self.firstdate
@ -219,9 +237,12 @@ class CalendarList:
def fill_calendar_days(self): def fill_calendar_days(self):
if self.filter is None: 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: else:
qs = self.filter.qs qs = self.filter.qs
@ -229,7 +250,7 @@ class CalendarList:
qs = qs.exclude(other_versions=self.ignore_dup) qs = qs.exclude(other_versions=self.ignore_dup)
startdatetime = timezone.make_aware(datetime.combine(self.c_firstdate, time.min), timezone.get_default_timezone()) 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()) 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=True) & Q(recurrence_dtstart__lte=lastdatetime))
| ( | (
Q(recurrence_dtend__isnull=False) Q(recurrence_dtend__isnull=False)
@ -238,11 +259,15 @@ class CalendarList:
| Q(recurrence_dtend__lt=startdatetime) | Q(recurrence_dtend__lt=startdatetime)
) )
) )
| (Q(start_day__lte=self.c_firstdate) & (Q(end_day__isnull=True) | Q(end_day__gte=self.c_firstdate)))
).filter( ).filter(
Q(other_versions__isnull=True) | Q(other_versions__isnull=True) |
Q(other_versions__representative=F('pk')) | Q(other_versions__representative=F('pk')) |
Q(other_versions__representative__isnull=True) Q(other_versions__representative__isnull=True)
).order_by("start_time", "title__unaccent__lower").prefetch_related("exact_location").prefetch_related("category").prefetch_related("other_versions") ).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()) firstdate = datetime.fromordinal(self.c_firstdate.toordinal())
if firstdate.tzinfo is None or firstdate.tzinfo.utcoffset(firstdate) is None: if firstdate.tzinfo is None or firstdate.tzinfo.utcoffset(firstdate) is None:
@ -290,14 +315,14 @@ class CalendarList:
def time_intervals_list_first(self): def time_intervals_list_first(self):
return self.time_intervals_list(True) return self.time_intervals_list(True)
def export_to_ics(self): def export_to_ics(self, request):
from .models import Event from .models import Event
events = [event for day in self.get_calendar_days().values() for event in day.events] 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): class CalendarMonth(CalendarList):
def __init__(self, year, month, filter): def __init__(self, year, month, filter, qs=None):
self.year = year self.year = year
self.month = month self.month = month
r = calendar.monthrange(year, month) r = calendar.monthrange(year, month)
@ -305,7 +330,7 @@ class CalendarMonth(CalendarList):
first = date(year, month, 1) first = date(year, month, 1)
last = date(year, month, r[1]) last = date(year, month, r[1])
super().__init__(first, last, filter) super().__init__(first, last, filter, qs)
def get_month_name(self): def get_month_name(self):
return self.firstdate.strftime("%B") return self.firstdate.strftime("%B")
@ -318,14 +343,14 @@ class CalendarMonth(CalendarList):
class CalendarWeek(CalendarList): class CalendarWeek(CalendarList):
def __init__(self, year, week, filter): def __init__(self, year, week, filter, qs=None):
self.year = year self.year = year
self.week = week self.week = week
first = date.fromisocalendar(self.year, self.week, 1) first = date.fromisocalendar(self.year, self.week, 1)
last = date.fromisocalendar(self.year, self.week, 7) last = date.fromisocalendar(self.year, self.week, 7)
super().__init__(first, last, filter) super().__init__(first, last, filter, qs)
def next_week(self): def next_week(self):
return self.firstdate + timedelta(days=7) return self.firstdate + timedelta(days=7)
@ -335,8 +360,8 @@ class CalendarWeek(CalendarList):
class CalendarDay(CalendarList): class CalendarDay(CalendarList):
def __init__(self, date, filter=None): def __init__(self, date, filter=None, qs=None):
super().__init__(date, date, filter, exact=True) super().__init__(date, date, filter=filter, qs=qs, exact=True)
def get_events(self): def get_events(self):
return self.calendar_days_list()[0].events 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.utils.log import get_task_logger
from celery.exceptions import MaxRetriesExceededError from celery.exceptions import MaxRetriesExceededError
import time as time_ import time as time_
from django.conf import settings
from celery.signals import worker_ready
from contextlib import contextmanager from contextlib import contextmanager
@ -147,6 +148,8 @@ def run_recurrent_import_internal(rimport, downloader, req_id):
extractor = c3c.CExtractor() extractor = c3c.CExtractor()
elif rimport.processor == RecurrentImport.PROCESSOR.ARACHNEE: elif rimport.processor == RecurrentImport.PROCESSOR.ARACHNEE:
extractor = arachnee.CExtractor() extractor = arachnee.CExtractor()
elif rimport.processor == RecurrentImport.PROCESSOR.LERIO:
extractor = lerio.CExtractor()
else: else:
extractor = None extractor = None
@ -162,16 +165,22 @@ def run_recurrent_import_internal(rimport, downloader, req_id):
location = rimport.defaultLocation location = rimport.defaultLocation
tags = rimport.defaultTags tags = rimport.defaultTags
published = rimport.defaultPublished published = rimport.defaultPublished
organisers = [] if rimport.defaultOrganiser is None else [rimport.defaultOrganiser.pk]
try: try:
# get events from website # get events from website
events = u2e.process( events = u2e.process(
url, url,
browsable_url, browsable_url,
default_values={"category": category, "location": location, "tags": tags}, default_values={"category": category, "location": location, "tags": tags, "organisers": organisers},
published=published, 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 # convert it to json
json_events = json.dumps(events, default=str) json_events = json.dumps(events, default=str)
@ -247,6 +256,23 @@ def daily_imports(self):
run_recurrent_imports_from_list([imp.pk for imp in imports]) 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) @app.task(bind=True)
def run_all_recurrent_imports(self): def run_all_recurrent_imports(self):
from agenda_culturel.models import RecurrentImport from agenda_culturel.models import RecurrentImport
@ -288,7 +314,7 @@ def weekly_imports(self):
run_recurrent_imports_from_list([imp.pk for imp in imports]) run_recurrent_imports_from_list([imp.pk for imp in imports])
@app.task(base=ChromiumTask, bind=True) @app.task(base=ChromiumTask, bind=True)
def import_events_from_url(self, url, cat, force=False): def import_events_from_url(self, url, cat, tags, force=False, user_id=None):
from .db_importer import DBImporterEvents from .db_importer import DBImporterEvents
from agenda_culturel.models import RecurrentImport, BatchImportation from agenda_culturel.models import RecurrentImport, BatchImportation
from agenda_culturel.models import Event, Category from agenda_culturel.models import Event, Category
@ -322,7 +348,7 @@ def import_events_from_url(self, url, cat, force=False):
# set default values # set default values
values = {} values = {}
if cat is not None: if cat is not None:
values = {"category": cat} values = {"category": cat, "tags": tags}
# get event # get event
events = u2e.process( events = u2e.process(
@ -334,7 +360,7 @@ def import_events_from_url(self, url, cat, force=False):
json_events = json.dumps(events, default=str) json_events = json.dumps(events, default=str)
# import events (from json) # 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 # finally, close task
close_import_task(self.request.id, success, error_message, importer) close_import_task(self.request.id, success, error_message, importer)
@ -351,13 +377,14 @@ def import_events_from_url(self, url, cat, force=False):
@app.task(base=ChromiumTask, bind=True) @app.task(base=ChromiumTask, bind=True)
def import_events_from_urls(self, urls_and_cats): def import_events_from_urls(self, urls_cat_tags, user_id=None):
for ucat in urls_and_cats: for ucat in urls_cat_tags:
if ucat is not None: if ucat is not None:
url = ucat[0] url = ucat[0]
cat = ucat[1] cat = ucat[1]
tags = ucat[2]
import_events_from_url.delay(url, cat) import_events_from_url.delay(url, cat, tags, user_id=user_id)
app.conf.beat_schedule = { app.conf.beat_schedule = {
@ -366,6 +393,10 @@ app.conf.beat_schedule = {
# Daily imports at 3:14 a.m. # Daily imports at 3:14 a.m.
"schedule": crontab(hour=3, minute=14), "schedule": crontab(hour=3, minute=14),
}, },
"daily_screenshot": {
"task": "agenda_culturel.celery.screenshot",
"schedule": crontab(hour=3, minute=3),
},
"weekly_imports": { "weekly_imports": {
"task": "agenda_culturel.celery.weekly_imports", "task": "agenda_culturel.celery.weekly_imports",
# Daily imports on Mondays at 2:22 a.m. # Daily imports on Mondays at 2:22 a.m.

View File

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

View File

@ -0,0 +1,505 @@
import django_filters
from django.utils.translation import gettext_lazy as _
from django import forms
from django.contrib.postgres.search import SearchQuery, SearchHeadline
from django.db.models import Count, Q
from django.http import QueryDict
from django.contrib.gis.measure import D
from django.forms import (
ModelForm,
ValidationError,
TextInput,
Form,
URLField,
MultipleHiddenInput,
Textarea,
CharField,
ChoiceField,
RadioSelect,
MultipleChoiceField,
BooleanField,
HiddenInput,
ModelChoiceField,
)
from .forms import (
URLSubmissionForm,
EventForm,
BatchImportationForm,
FixDuplicates,
SelectEventInList,
MergeDuplicates,
RecurrentImportForm,
CategorisationRuleImportForm,
CategorisationForm,
EventAddPlaceForm,
PlaceForm,
)
from .models import (
ReferenceLocation,
RecurrentImport,
Tag,
Event,
Category,
Message,
DuplicatedEvents
)
class EventFilter(django_filters.FilterSet):
RECURRENT_CHOICES = [
("remove_recurrent", "Masquer les événements récurrents"),
("only_recurrent", "Montrer uniquement les événements récurrents"),
]
DISTANCE_CHOICES = [5, 10, 15, 30]
position = django_filters.ModelChoiceFilter(
label="À proximité de",
method="no_filter",
empty_label=_("Select a location"),
queryset=ReferenceLocation.objects.all().order_by("-main", "name__unaccent")
)
radius = django_filters.ChoiceFilter(
label="Dans un rayon de",
method="no_filter",
choices=[(x, str(x) + " km") for x in DISTANCE_CHOICES],
null_label=None,
empty_label=None
)
exclude_tags = django_filters.MultipleChoiceFilter(
label="Exclure les étiquettes",
choices=[],
lookup_expr="icontains",
field_name="tags",
exclude=True,
widget=forms.SelectMultiple,
)
tags = django_filters.MultipleChoiceFilter(
label="Inclure les étiquettes",
choices=[],
lookup_expr="icontains",
conjoined=True,
field_name="tags",
widget=forms.SelectMultiple,
)
recurrences = django_filters.ChoiceFilter(
label="Inclure la récurrence",
choices=RECURRENT_CHOICES,
method="filter_recurrences",
)
category = django_filters.ModelMultipleChoiceFilter(
label="Filtrer par catégories",
field_name="category__id",
to_field_name="id",
queryset=Category.objects.all(),
widget=MultipleHiddenInput,
)
status = django_filters.MultipleChoiceFilter(
label="Filtrer par status",
choices=Event.STATUS.choices,
field_name="status",
widget=forms.CheckboxSelectMultiple,
)
class Meta:
model = Event
fields = ["category", "tags", "exclude_tags", "status", "recurrences"]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not kwargs["request"].user.is_authenticated:
self.form.fields.pop("status")
self.form.fields["exclude_tags"].choices = Tag.get_tag_groups(exclude=True, nb_suggestions=0)
self.form.fields["tags"].choices = Tag.get_tag_groups(include=True)
def filter_recurrences(self, queryset, name, value):
# construct the full lookup expression
lookup = "__".join([name, "isnull"])
return queryset.filter(**{lookup: value == "remove_recurrent"})
def no_filter(self, queryset, name, value):
return queryset
@property
def qs(self):
parent = super().qs
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")
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):
if isinstance(self.form.data, QueryDict):
return self.form.data.urlencode()
else:
return ""
def get_full_url(self):
return self.request.get_full_path()
def get_url_remove_categories(self, catpks, full_path = None):
if full_path is None:
full_path = self.request.get_full_path()
result = full_path
for catpk in catpks:
result = result.replace('category=' + str(catpk), '')
result = result.replace('?&', '?')
result = result.replace('&&', '&')
return result
def get_url_add_categories(self, catpks, full_path = None):
if full_path is None:
full_path = self.request.get_full_path()
result = full_path
for catpk in catpks:
result = result + ('&' if '?' in full_path else '?') + 'category=' + str(catpk)
return result
def get_url_without_filters_only_cats(self):
return self.get_url_without_filters(True)
def get_url_without_filters(self, only_categories=False):
if only_categories:
# on repart d'une url sans option
result = self.request.get_full_path().split("?")[0]
# on ajoute toutes les catégories
result = self.get_url_add_categories([c.pk for c in self.get_categories()], result)
else:
# on supprime toutes les catégories
result = self.get_url_remove_categories([c.pk for c in self.get_categories()])
return result
def get_cleaned_data(self, name):
try:
return self.form.cleaned_data[name]
except AttributeError:
return {}
except KeyError:
return {}
def get_categories(self):
return self.get_cleaned_data("category")
def has_category(self):
return "category" in self.form.cleaned_data and len(self.get_cleaned_data("category")) > 0
def get_tags(self):
return self.get_cleaned_data("tags")
def get_exclude_tags(self):
return self.get_cleaned_data("exclude_tags")
def get_status(self):
return self.get_cleaned_data("status")
def get_position(self):
return self.get_cleaned_data("position")
def get_radius(self):
return self.get_cleaned_data("radius")
def to_str(self, prefix=''):
self.form.full_clean()
result = ' '.join([c.name for c in self.get_categories()] + [t for t in self.get_tags()] + ["~" + t for t in self.get_exclude_tags()] + [str(self.get_position()), str(self.get_radius())])
if len(result) > 0:
result = prefix + result
return result
def get_status_names(self):
if "status" in self.form.cleaned_data:
return [
dict(Event.STATUS.choices)[s] for s in self.get_cleaned_data("status")
]
else:
return []
def get_recurrence_filtering(self):
if "recurrences" in self.form.cleaned_data:
d = dict(self.RECURRENT_CHOICES)
v = self.form.cleaned_data["recurrences"]
if v in d:
return d[v]
else:
return ""
else:
return ""
def is_resetable(self, only_categories=False):
if only_categories:
return len(self.get_cleaned_data("category")) != 0
else:
if self.request.user.is_authenticated:
if (
len(self.get_cleaned_data("status")) != 1
or
self.get_cleaned_data("status")[0] != Event.STATUS.PUBLISHED
):
return True
else:
if (
len(self.get_cleaned_data("status")) != 0
):
return True
return (
len(self.get_cleaned_data("tags")) != 0
or len(self.get_cleaned_data("exclude_tags")) != 0
or len(self.get_cleaned_data("recurrences")) != 0
or ((not self.get_cleaned_data("position") is None) and (not self.get_cleaned_data("radius") is None))
)
def is_active(self, only_categories=False):
if only_categories:
return len(self.get_cleaned_data("category")) != 0
else:
return (
len(self.get_cleaned_data("status")) != 0
or len(self.get_cleaned_data("tags")) != 0
or len(self.get_cleaned_data("exclude_tags")) != 0
or len(self.get_cleaned_data("recurrences")) != 0
or ((not self.get_cleaned_data("position") is None) and (not self.get_cleaned_data("radius") is None))
)
def is_selected(self, cat):
return "category" in self.form.cleaned_data and cat in self.form.cleaned_data["category"]
def is_selected_tag(self, tag):
return "tags" in self.form.cleaned_data and tag in self.form.cleaned_data["tags"]
def get_url_add_tag(self, tag):
full_path = self.request.get_full_path()
result = full_path + ('&' if '?' in full_path else '?') + 'tags=' + str(tag)
return result
def tag_exists(self, tag):
return tag in [t[0] for g in self.form.fields["tags"].choices for t in g[1]]
def set_default_values(request):
if request.user.is_authenticated:
if request.GET.get('status', None) == None:
tempdict = request.GET.copy()
tempdict['status'] = 'published'
request.GET = tempdict
return request
return request
def get_position_radius(self):
if self.get_cleaned_data("position") is None or self.get_cleaned_data("radius") is None:
return ""
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(
choices=Event.STATUS.choices, widget=forms.CheckboxSelectMultiple
)
representative = django_filters.MultipleChoiceFilter(
label=_("Representative version"),
choices=[(True, _("Yes")), (False, _("Non"))],
method="filter_by_representative",
widget=forms.CheckboxSelectMultiple)
import_sources = django_filters.ModelChoiceFilter(
label=_("Imported from"),
method="filter_by_source",
queryset=RecurrentImport.objects.all().order_by("name__unaccent")
)
def filter_by_source(self, queryset, name, value):
src = RecurrentImport.objects.get(pk=value.pk).source
return queryset.filter(import_sources__contains=[src])
def filter_by_representative(self, queryset, name, value):
if value is None or len(value) != 1:
return queryset
else:
q = (Q(other_versions__isnull=True) |
Q(other_versions__representative=F('pk')) |
Q(other_versions__representative__isnull=True))
if value[0] == True:
return queryset.filter(q)
else:
return queryset.exclude(q)
class Meta:
model = Event
fields = ["status"]
class MessagesFilterAdmin(django_filters.FilterSet):
closed = django_filters.MultipleChoiceFilter(
label="Status",
choices=((True, _("Closed")), (False, _("Open"))),
widget=forms.CheckboxSelectMultiple,
)
spam = django_filters.MultipleChoiceFilter(
label="Spam",
choices=((True, _("Spam")), (False, _("Non spam"))),
widget=forms.CheckboxSelectMultiple,
)
class Meta:
model = Message
fields = ["closed", "spam"]
class SimpleSearchEventFilter(django_filters.FilterSet):
q = django_filters.CharFilter(method="custom_filter",
label=_("Search"),
widget=forms.TextInput(attrs={"type": "search"})
)
status = django_filters.MultipleChoiceFilter(
label="Filtrer par status",
choices=Event.STATUS.choices,
field_name="status",
widget=forms.CheckboxSelectMultiple,
)
def custom_filter(self, queryset, name, value):
search_query = SearchQuery(value, config="french")
qs = queryset.filter(
Q(title__icontains=value)
| Q(category__name__icontains=value)
| Q(tags__icontains=[value])
| Q(exact_location__name__icontains=value)
| Q(description__icontains=value)
)
for f in ["title", "category__name", "exact_location__name", "description"]:
params = {
f
+ "_hl": SearchHeadline(
f,
search_query,
start_sel='<span class="highlight">',
stop_sel="</span>",
config="french",
)
}
qs = qs.annotate(**params)
return qs
class Meta:
model = Event
fields = ["q"]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not kwargs["request"].user.is_authenticated:
self.form.fields.pop("status")
class SearchEventFilter(django_filters.FilterSet):
tags = django_filters.CharFilter(lookup_expr="icontains")
title = django_filters.CharFilter(method="hl_filter_contains")
location = django_filters.CharFilter(method="hl_filter_contains")
description = django_filters.CharFilter(method="hl_filter_contains")
start_day = django_filters.DateFromToRangeFilter(
widget=django_filters.widgets.RangeWidget(attrs={"type": "date"})
)
status = django_filters.MultipleChoiceFilter(
label="Filtrer par status",
choices=Event.STATUS.choices,
field_name="status",
widget=forms.CheckboxSelectMultiple,
)
o = django_filters.OrderingFilter(
# tuple-mapping retains order
fields=(
("title", "title"),
("description", "description"),
("start_day", "start_day"),
),
)
def hl_filter_contains(self, queryset, name, value):
# first check if it contains
filter_contains = {name + "__contains": value}
queryset = queryset.filter(**filter_contains)
# then hightlight the result
search_query = SearchQuery(value, config="french")
params = {
name
+ "_hl": SearchHeadline(
name,
search_query,
start_sel='<span class="highlight">',
stop_sel="</span>",
config="french",
)
}
return queryset.annotate(**params)
class Meta:
model = Event
fields = ["title", "location", "description", "category", "tags", "start_day"]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not kwargs["request"].user.is_authenticated:
self.form.fields.pop("status")
class DuplicatedEventsFilter(django_filters.FilterSet):
fixed = django_filters.BooleanFilter(
label="Résolu",
field_name='representative', method="fixed_qs")
class Meta:
model = DuplicatedEvents
fields = []
def fixed_qs(self, queryset, name, value):
return DuplicatedEvents.not_fixed_qs(queryset, value)
class RecurrentImportFilter(django_filters.FilterSet):
name = django_filters.ModelMultipleChoiceFilter(
label="Filtrer par nom",
field_name="name",
queryset=RecurrentImport.objects.all().order_by("name__unaccent")
)
class Meta:
model = RecurrentImport
fields = ["name"]

View File

@ -16,14 +16,15 @@ from django.forms import (
) )
from django_better_admin_arrayfield.forms.widgets import DynamicArrayWidget from django_better_admin_arrayfield.forms.widgets import DynamicArrayWidget
from .utils import PlaceGuesser
from .models import ( from .models import (
Event, Event,
RecurrentImport, RecurrentImport,
CategorisationRule, CategorisationRule,
ModerationAnswer,
ModerationQuestion,
Place, Place,
Category, Category,
Tag,
Message
) )
from django.conf import settings from django.conf import settings
from django.core.files import File from django.core.files import File
@ -32,24 +33,119 @@ from django.utils.translation import gettext_lazy as _
from string import ascii_uppercase as auc from string import ascii_uppercase as auc
from .templatetags.utils_extra import int_to_abc from .templatetags.utils_extra import int_to_abc
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.timezone import localtime
from django.utils.formats import localize from django.utils.formats import localize
from .templatetags.event_extra import event_field_verbose_name, field_to_html from .templatetags.event_extra import event_field_verbose_name, field_to_html
import os
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class GroupFormMixin:
template_name = 'agenda_culturel/forms/div_group.html'
class FieldGroup:
def __init__(self, id, label, display_label=False, maskable=False, default_masked=True):
self.id = id
self.label = label
self.display_label = display_label
self.maskable = maskable
self.default_masked = default_masked
def toggle_field_name(self):
return 'group_' + self.id
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.groups = []
def add_group(self, *args, **kwargs):
self.groups.append(GroupFormMixin.FieldGroup(*args, **kwargs))
if self.groups[-1].maskable:
self.fields[self.groups[-1].toggle_field_name()] = BooleanField(required=False)
self.fields[self.groups[-1].toggle_field_name()].toggle_group = True
def get_fields_in_group(self, g):
return [f for f in self.visible_fields() if not hasattr(f.field, "toggle_group") and hasattr(f.field, "group_id") and f.field.group_id == g.id]
def get_no_group_fields(self):
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] + [(GroupFormMixin.FieldGroup("other", _("Other")), self.get_no_group_fields())]
def clean(self):
result = super().clean()
if result:
data = dict(self.data)
# for each masked group, we remove data
for g in self.groups:
if g.maskable and not g.toggle_field_name() in data:
fields = self.get_fields_in_group(g)
for f in fields:
self.cleaned_data[f.name] = None
return result
class TagForm(ModelForm):
required_css_class = 'required'
class Meta:
model = Tag
fields = ["name", "description", "in_included_suggestions", "in_excluded_suggestions", "principal"]
widgets = {
"name": HiddenInput()
}
class TagRenameForm(Form):
required_css_class = 'required'
name = CharField(
label=_('Name of new tag'),
required=True
)
force = BooleanField(
label=_('Force renaming despite the existence of events already using the chosen tag.'),
)
def __init__(self, *args, **kwargs):
force = kwargs.pop("force", False)
name = kwargs.pop("name", None)
super().__init__(*args, **kwargs)
if not (force or (not len(args) == 0 and 'force' in args[0])):
del self.fields["force"]
if not name is None and self.fields["name"].initial is None:
self.fields["name"].initial = name
def is_force(self):
return "force" in self.fields and self.cleaned_data["force"] == True
class URLSubmissionForm(Form): class URLSubmissionForm(Form):
required_css_class = 'required'
url = URLField(max_length=512) url = URLField(max_length=512)
category = ModelChoiceField( category = ModelChoiceField(
label=_("Category"), label=_("Category"),
queryset=Category.objects.all().order_by("name"), queryset=Category.objects.all().order_by("name"),
initial=None, initial=None,
help_text=_('Optional. If you don''t specify a category, we''ll find it for you.'),
required=False, required=False,
) )
tags = MultipleChoiceField(
label=_("Tags"),
initial=None,
choices=[],
required=False
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["tags"].choices = Tag.get_tag_groups(all=True)
@ -62,22 +158,46 @@ class DynamicArrayWidgetTags(DynamicArrayWidget):
class RecurrentImportForm(ModelForm): class RecurrentImportForm(ModelForm):
required_css_class = 'required'
defaultTags = MultipleChoiceField(
label=_("Tags"),
initial=None,
choices=[],
required=False
)
class Meta: class Meta:
model = RecurrentImport model = RecurrentImport
fields = "__all__" 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): class CategorisationRuleImportForm(ModelForm):
required_css_class = 'required'
class Meta: class Meta:
model = CategorisationRule model = CategorisationRule
fields = "__all__" fields = "__all__"
class EventForm(ModelForm): class EventForm(GroupFormMixin, ModelForm):
required_css_class = 'required'
old_local_image = CharField(widget=HiddenInput(), required=False) 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"),
initial=None,
choices=[],
required=False
)
class Meta: class Meta:
model = Event model = Event
@ -86,7 +206,11 @@ class EventForm(ModelForm):
"modified_date", "modified_date",
"moderated_date", "moderated_date",
"import_sources", "import_sources",
"image" "image",
"moderated_by_user",
"modified_by_user",
"created_by_user",
"imported_by_user"
] ]
widgets = { widgets = {
"start_day": TextInput( "start_day": TextInput(
@ -108,22 +232,73 @@ class EventForm(ModelForm):
"other_versions": HiddenInput(), "other_versions": HiddenInput(),
"uuids": MultipleHiddenInput(), "uuids": MultipleHiddenInput(),
"reference_urls": DynamicArrayWidgetURLs(), "reference_urls": DynamicArrayWidgetURLs(),
"tags": DynamicArrayWidgetTags(),
} }
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
is_authenticated = kwargs.pop("is_authenticated", False) is_authenticated = kwargs.pop("is_authenticated", False)
self.cloning = kwargs.pop("is_cloning", False) self.cloning = kwargs.pop("is_cloning", False)
self.simple_cloning = kwargs.pop("is_simple_cloning", False)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if not is_authenticated: if not is_authenticated:
del self.fields["status"] del self.fields["status"]
del self.fields["organisers"]
self.fields['category'].queryset = self.fields['category'].queryset.order_by('name') self.fields['category'].queryset = self.fields['category'].queryset.order_by('name')
self.fields['category'].empty_label = None self.fields['category'].empty_label = None
self.fields['category'].initial = Category.get_default_category() self.fields['category'].initial = Category.get_default_category()
self.fields['tags'].choices = Tag.get_tag_groups(all=True)
# set groups
self.add_group('main', _('Main fields'))
self.fields['title'].group_id = 'main'
self.add_group('start', _('Start of event'))
self.fields['start_day'].group_id = 'start'
self.fields['start_time'].group_id = 'start'
self.add_group('end', _('End of event'))
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=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'))
self.fields['description'].group_id = 'details'
if is_authenticated:
self.fields['organisers'].group_id = 'details'
self.add_group('location', _('Location'))
self.fields['location'].group_id = 'location'
self.fields['exact_location'].group_id = 'location'
self.add_group('illustration', _('Illustration'))
self.fields['local_image'].group_id = 'illustration'
self.fields['image_alt'].group_id = 'illustration'
if is_authenticated:
self.add_group('meta-admin', _('Meta information'))
self.fields['category'].group_id = 'meta-admin'
self.fields['tags'].group_id = 'meta-admin'
self.fields['status'].group_id = 'meta-admin'
else:
self.add_group('meta', _('Meta information'))
self.fields['category'].group_id = 'meta'
self.fields['tags'].group_id = 'meta'
def is_clone_from_url(self): def is_clone_from_url(self):
return self.cloning return self.cloning
def is_simple_clone_from_url(self):
return self.simple_cloning
def clean_end_day(self): def clean_end_day(self):
start_day = self.cleaned_data.get("start_day") start_day = self.cleaned_data.get("start_day")
end_day = self.cleaned_data.get("end_day") end_day = self.cleaned_data.get("end_day")
@ -159,11 +334,69 @@ class EventForm(ModelForm):
self.cleaned_data['old_local_image'] != "": self.cleaned_data['old_local_image'] != "":
basename = self.cleaned_data['old_local_image'] basename = self.cleaned_data['old_local_image']
old = settings.MEDIA_ROOT + "/" + basename old = settings.MEDIA_ROOT + "/" + basename
self.cleaned_data['local_image'] = File(name=basename, file=open(old, "rb")) if os.path.isfile(old):
self.cleaned_data['local_image'] = File(name=basename, file=open(old, "rb"))
class MultipleChoiceFieldAcceptAll(MultipleChoiceField):
def validate(self, value):
pass
class EventModerateForm(ModelForm):
required_css_class = 'required'
tags = MultipleChoiceField(
label=_("Tags"),
help_text=_('Select tags from existing ones.'),
required=False
)
new_tags = MultipleChoiceFieldAcceptAll(
label=_("New tags"),
help_text=_('Create new labels (sparingly). Note: by starting your tag with the characters “TW:”, you''ll create a “trigger warning” tag, and the associated events will be announced as such.'),
widget=DynamicArrayWidget(),
required=False
)
class Meta:
model = Event
fields = [
"status",
"category",
"organisers",
"exact_location",
"tags"
]
widgets = {
"status": RadioSelect
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['category'].queryset = self.fields['category'].queryset.order_by('name')
self.fields['category'].empty_label = None
self.fields['category'].initial = Category.get_default_category()
self.fields['tags'].choices = Tag.get_tag_groups(all=True)
def clean_new_tags(self):
return list(set(self.cleaned_data.get("new_tags")))
def clean(self):
super().clean()
if self.cleaned_data['tags'] is None:
self.cleaned_data['tags'] = []
if not self.cleaned_data.get('new_tags') is None:
self.cleaned_data['tags'] += self.cleaned_data.get('new_tags')
self.cleaned_data['tags'] = list(set(self.cleaned_data['tags']))
class BatchImportationForm(Form): class BatchImportationForm(Form):
required_css_class = 'required'
json = CharField( json = CharField(
label="JSON", label="JSON",
widget=Textarea(attrs={"rows": "10"}), widget=Textarea(attrs={"rows": "10"}),
@ -173,6 +406,8 @@ class BatchImportationForm(Form):
class FixDuplicates(Form): class FixDuplicates(Form):
required_css_class = 'required'
action = ChoiceField() action = ChoiceField()
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -250,7 +485,6 @@ class FixDuplicates(Form):
def get_selected_event(self, edup): def get_selected_event(self, edup):
selected = self.get_selected_event_code() selected = self.get_selected_event_code()
logger.warning("selected " + str(selected))
for e in edup.get_duplicated(): for e in edup.get_duplicated():
if e.pk == selected: if e.pk == selected:
return e return e
@ -258,7 +492,9 @@ class FixDuplicates(Form):
class SelectEventInList(Form): class SelectEventInList(Form):
event = ChoiceField() required_css_class = 'required'
event = ChoiceField(label=_('Event'))
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
events = kwargs.pop("events", None) events = kwargs.pop("events", None)
@ -270,7 +506,9 @@ class SelectEventInList(Form):
class MergeDuplicates(Form): class MergeDuplicates(Form):
checkboxes_fields = ["reference_urls", "description"] required_css_class = 'required'
checkboxes_fields = ["reference_urls", "description", "tags"]
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.duplicates = kwargs.pop("duplicates", None) self.duplicates = kwargs.pop("duplicates", None)
@ -311,17 +549,17 @@ class MergeDuplicates(Form):
'<li><a href="' + e.get_absolute_url() + '">' + e.title + "</a></li>" '<li><a href="' + e.get_absolute_url() + '">' + e.title + "</a></li>"
) )
result += ( result += (
"<li>Création&nbsp;: " + localize(localtime(e.created_date)) + "</li>" "<li>Création&nbsp;: " + localize(e.created_date) + "</li>"
) )
result += ( result += (
"<li>Dernière modification&nbsp;: " "<li>Dernière modification&nbsp;: "
+ localize(localtime(e.modified_date)) + localize(e.modified_date)
+ "</li>" + "</li>"
) )
if e.imported_date: if e.imported_date:
result += ( result += (
"<li>Dernière importation&nbsp;: " "<li>Dernière importation&nbsp;: "
+ localize(localtime(e.imported_date)) + localize(e.imported_date)
+ "</li>" + "</li>"
) )
result += "</ul>" result += "</ul>"
@ -372,7 +610,7 @@ class MergeDuplicates(Form):
result += '<input id="' + id + '" name="' + key + '"' result += '<input id="' + id + '" name="' + key + '"'
if key in MergeDuplicates.checkboxes_fields: if key in MergeDuplicates.checkboxes_fields:
result += ' type="checkbox"' result += ' type="checkbox"'
if value in checked: if checked and value in checked:
result += " checked" result += " checked"
else: else:
result += ' type="radio"' result += ' type="radio"'
@ -404,7 +642,7 @@ class MergeDuplicates(Form):
result = [] result = []
for s in selected: for s in selected:
for e in self.duplicates.get_duplicated(): for e in self.duplicates.get_duplicated():
if e.pk == selected: if e.pk == s:
result.append(e) result.append(e)
break break
return result return result
@ -417,47 +655,9 @@ class MergeDuplicates(Form):
return None return None
class ModerationQuestionForm(ModelForm):
class Meta:
model = ModerationQuestion
fields = "__all__"
class ModerationAnswerForm(ModelForm):
class Meta:
model = ModerationAnswer
exclude = ["question"]
widgets = {
"adds_tags": DynamicArrayWidgetTags(),
"removes_tags": DynamicArrayWidgetTags(),
}
class ModerateForm(ModelForm):
class Meta:
model = Event
fields = []
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
mqs = ModerationQuestion.objects.all()
mas = ModerationAnswer.objects.all()
for q in mqs:
self.fields[q.complete_id()] = ChoiceField(
widget=RadioSelect,
label=q.question,
choices=[(a.pk, a.html_description()) for a in mas if a.question == q],
required=True,
)
for a in mas:
if a.question == q and a.valid_event(self.instance):
self.fields[q.complete_id()].initial = a.pk
break
class CategorisationForm(Form): class CategorisationForm(Form):
required_css_class = 'required'
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
if "events" in kwargs: if "events" in kwargs:
events = kwargs.pop("events", None) events = kwargs.pop("events", None)
@ -488,6 +688,8 @@ class CategorisationForm(Form):
class EventAddPlaceForm(Form): class EventAddPlaceForm(Form):
required_css_class = 'required'
place = ModelChoiceField( place = ModelChoiceField(
label=_("Place"), label=_("Place"),
queryset=Place.objects.all().order_by("name"), queryset=Place.objects.all().order_by("name"),
@ -524,7 +726,9 @@ class EventAddPlaceForm(Form):
return self.instance return self.instance
class PlaceForm(ModelForm): class PlaceForm(GroupFormMixin, ModelForm):
required_css_class = 'required'
apply_to_all = BooleanField( apply_to_all = BooleanField(
initial=True, initial=True,
label=_( label=_(
@ -538,13 +742,70 @@ class PlaceForm(ModelForm):
fields = "__all__" fields = "__all__"
widgets = {"location": TextInput()} 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): def as_grid(self):
return mark_safe( result = ('<div class="grid"><div>'
'<div class="grid"><div>'
+ super().as_p() + super().as_p()
+ '</div><div><div class="map-widget">' + '''</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 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): def apply(self):
return self.cleaned_data.get("apply_to_all") 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

@ -70,17 +70,17 @@ class CExtractor(TwoStepsExtractorNoPause):
tags = [] tags = []
if first_cat in ["grand spectacle"]: if first_cat in ["grand spectacle"]:
category = "Spectacles" category = "Spectacles"
tags.append("danse") tags.append("💃 danse")
elif first_cat in ["theatre", "humour / one man show"]: elif first_cat in ["theatre", "humour / one man show"]:
category = "Spectacles" category = "Spectacles"
tags.append("théâtre") tags.append("🎭 théâtre")
elif first_cat in ["chanson francaise", "musique du monde", "pop / rock", "rap", "rnb", "raggae", "variete"]: elif first_cat in ["chanson francaise", "musique du monde", "pop / rock", "rap", "rnb", "raggae", "variete"]:
category = "Fêtes & Concerts" category = "Fêtes & Concerts"
tags.append("concert") tags.append("🎵 concert")
elif first_cat in ["comedie musicale", "humour / one man show", "spectacle equestre"]: elif first_cat in ["comedie musicale", "humour / one man show", "spectacle equestre"]:
category = "Spectacles" category = "Spectacles"
elif first_cat in ["spectacle pour enfant"]: elif first_cat in ["spectacle pour enfant"]:
tags = ["jeune public"] tags = ["🎈 jeune public"]
category = None category = None
else: else:
category = None category = None

View File

@ -11,7 +11,7 @@ class CExtractor(TwoStepsExtractor):
if not category: if not category:
return None return None
mapping = {"Théâtre": "Spectacles", "Concert": "Fêtes & Concerts", "Projection": "Cinéma"} mapping = {"Théâtre": "Spectacles", "Concert": "Fêtes & Concerts", "Projection": "Cinéma"}
mapping_tag = {"Théâtre": "théâtre", "Concert": "concert", "Projection": None} mapping_tag = {"Théâtre": "🎭 théâtre", "Concert": "🎵 concert", "Projection": None}
if category in mapping: if category in mapping:
return mapping[category], mapping_tag[category] return mapping[category], mapping_tag[category]
else: else:

View File

@ -3,6 +3,12 @@ from ..extractor_facebook import FacebookEvent
import json5 import json5
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
import json 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 # 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): def build_event_url_list(self, content):
soup = BeautifulSoup(content, "html.parser") soup = BeautifulSoup(content, "html.parser")
debug = False
found = False
links = soup.find_all("a") links = soup.find_all("a")
for link in links: for link in links:
if link.get("href").startswith('https://www.facebook.com/events/'): if link.get("href").startswith('https://www.facebook.com/events/'):
self.add_event_url(link.get('href').split('?')[0]) 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( def add_event_from_content(
@ -42,4 +65,7 @@ class CExtractor(TwoStepsExtractor):
event["published"] = published event["published"] = published
self.add_event(default_values, **event) self.add_event(default_values, **event)
else:
logger.warning("cannot find any event in page")

View File

@ -9,6 +9,12 @@ class CExtractor(TwoStepsExtractor):
nom_lieu = "La Comédie de Clermont" nom_lieu = "La Comédie de Clermont"
url_referer = "https://lacomediedeclermont.com/saison24-25/" url_referer = "https://lacomediedeclermont.com/saison24-25/"
def is_to_import_from_url(self, url):
if any(keyword in url for keyword in ["podcast", "on-debriefe", "popcorn", "rencontreautour","rencontre-autour"]):
return False
else:
return True
def category_comedie2agenda(self, category): def category_comedie2agenda(self, category):
mapping = { mapping = {
"Théâtre": "Spectacles", "Théâtre": "Spectacles",
@ -18,8 +24,8 @@ class CExtractor(TwoStepsExtractor):
"PopCorn Live": "Sans catégorie", "PopCorn Live": "Sans catégorie",
} }
mapping_tag = { mapping_tag = {
"Théâtre": "théâtre", "Théâtre": "🎭 théâtre",
"Danse": "danse", "Danse": "💃 danse",
"Rencontre": None, "Rencontre": None,
"Sortie de résidence": "sortie de résidence", "Sortie de résidence": "sortie de résidence",
"PopCorn Live": None, "PopCorn Live": None,
@ -50,33 +56,35 @@ class CExtractor(TwoStepsExtractor):
e_url = ( e_url = (
e.select("a")[0]["href"] + "#" + d e.select("a")[0]["href"] + "#" + d
) # a "fake" url specific for each day of this show ) # a "fake" url specific for each day of this show
self.add_event_url(e_url)
self.add_event_start_day(e_url, d) if self.is_to_import_from_url(e_url):
t = ( self.add_event_url(e_url)
str(e.select("div#datecal")[0]) self.add_event_start_day(e_url, d)
.split(" ")[-1] t = (
.split("<")[0] str(e.select("div#datecal")[0])
) .split(" ")[-1]
self.add_event_start_time(e_url, t) .split("<")[0]
title = e.select("a")[0].contents[0]
self.add_event_title(e_url, title)
category = e.select("div#lieuevtcal span")
if len(category) > 0:
category, tag = self.category_comedie2agenda(
category[-1].contents[0]
) )
if category: self.add_event_start_time(e_url, t)
self.add_event_category(e_url, category) title = e.select("a")[0].contents[0]
if tag: self.add_event_title(e_url, title)
self.add_event_tag(e_url, tag) category = e.select("div#lieuevtcal span")
location = ( if len(category) > 0:
e.select("div#lieuevtcal")[0] category, tag = self.category_comedie2agenda(
.contents[-1] category[-1].contents[0]
.split("")[-1] )
) if category:
if location.replace(" ", "") == "": self.add_event_category(e_url, category)
location = self.nom_lieu if tag:
self.add_event_location(e_url, location) self.add_event_tag(e_url, tag)
location = (
e.select("div#lieuevtcal")[0]
.contents[-1]
.split("")[-1]
)
if location.replace(" ", "") == "":
location = self.nom_lieu
self.add_event_location(e_url, location)
def add_event_from_content( def add_event_from_content(
self, self,
@ -98,6 +106,15 @@ class CExtractor(TwoStepsExtractor):
description = soup.select("#descspec") description = soup.select("#descspec")
if description and len(description) > 0: if description and len(description) > 0:
description = description[0].get_text().replace("Lire plus...", "") description = description[0].get_text().replace("Lire plus...", "")
# on ajoute éventuellement les informations complémentaires
d_suite = ""
for d in ["typedesc", "dureedesc", "lieuspec"]:
comp_desc = soup.select("#" + d)
if comp_desc and len(comp_desc) > 0:
d_suite += "\n\n" + comp_desc[0].get_text()
if d_suite != "":
description += "\n\n> Informations complémentaires:" + d_suite
else: else:
description = None description = None

View File

@ -22,7 +22,7 @@ class CExtractor(TwoStepsExtractor):
for e in data["events"]: for e in data["events"]:
self.add_event_url(e["url"]) self.add_event_url(e["url"])
if e["tag"] == "Gratuit": if e["tag"] == "Gratuit":
self.add_event_tag(e["url"], "gratuit") self.add_event_tag(e["url"], "💶 gratuit")
else: else:
raise Exception("Cannot extract events from javascript") raise Exception("Cannot extract events from javascript")
@ -53,7 +53,7 @@ class CExtractor(TwoStepsExtractor):
if description is None: if description is None:
description = "" description = ""
tags = ["concert"] tags = ["🎵 concert"]
link_calendar = soup.select('a[href^="https://calendar.google.com/calendar/"]') link_calendar = soup.select('a[href^="https://calendar.google.com/calendar/"]')
if len(link_calendar) == 0: if len(link_calendar) == 0:

View File

@ -58,7 +58,7 @@ class CExtractor(TwoStepsExtractor):
end_day = Extractor.guess_end_day(start_day, start_time, end_time) end_day = Extractor.guess_end_day(start_day, start_time, end_time)
url_human = event_url url_human = event_url
tags = ["concert"] tags = ["🎵 concert"]
image = soup.select("wow-image img[fetchpriority=high]") image = soup.select("wow-image img[fetchpriority=high]")
if image: if image:

View File

@ -10,7 +10,7 @@ class CExtractor(TwoStepsExtractor):
if not category: if not category:
return None return None
mapping = {"Concerts": "Fêtes & Concerts"} mapping = {"Concerts": "Fêtes & Concerts"}
mapping_tag = {"Concerts": "concert"} mapping_tag = {"Concerts": "🎵 concert"}
if category in mapping: if category in mapping:
return mapping[category], mapping_tag[category] return mapping[category], mapping_tag[category]
else: else:

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): class ChromiumHeadlessDownloader(Downloader):
def __init__(self, pause=True): def __init__(self, pause=True, noimage=True):
super().__init__() super().__init__()
self.pause = pause self.pause = pause
self.options = Options() self.options = Options()
@ -78,17 +78,31 @@ class ChromiumHeadlessDownloader(Downloader):
self.options.add_argument("--disable-dev-shm-usage") self.options.add_argument("--disable-dev-shm-usage")
self.options.add_argument("--disable-browser-side-navigation") self.options.add_argument("--disable-browser-side-navigation")
self.options.add_argument("--disable-gpu") self.options.add_argument("--disable-gpu")
self.options.add_experimental_option( if noimage:
"prefs", { self.options.add_experimental_option(
# block image loading "prefs", {
"profile.managed_default_content_settings.images": 2, # block image loading
} "profile.managed_default_content_settings.images": 2,
) }
)
self.service = Service("/usr/bin/chromedriver") self.service = Service("/usr/bin/chromedriver")
self.driver = webdriver.Chrome(service=self.service, options=self.options) 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): def download(self, url, referer=None, post=None):
if post: if post:
raise Exception("POST method with Chromium headless not yet implemented") 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 from datetime import datetime, time, date, timedelta
import re import re
import unicodedata import unicodedata
from django.utils import timezone
@ -49,7 +49,7 @@ class Extractor(ABC):
return i + 1 return i + 1
return None return None
def parse_french_date(text): def parse_french_date(text, default_year=None):
# format NomJour Numero Mois Année # format NomJour Numero Mois Année
m = re.search( m = re.search(
"[a-zA-ZéÉûÛ:.]+[ ]*([0-9]+)[er]*[ ]*([a-zA-ZéÉûÛ:.]+)[ ]*([0-9]+)", text "[a-zA-ZéÉûÛ:.]+[ ]*([0-9]+)[er]*[ ]*([a-zA-ZéÉûÛ:.]+)[ ]*([0-9]+)", text
@ -73,8 +73,15 @@ class Extractor(ABC):
month = int(m.group(2)) month = int(m.group(2))
year = m.group(3) year = m.group(3)
else: else:
# TODO: consolider les cas non satisfaits # format Numero Mois Annee
return None 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: if month is None:
return None return None
@ -187,6 +194,7 @@ class Extractor(ABC):
"start_day": start_day, "start_day": start_day,
"uuids": uuids, "uuids": uuids,
"location": location if location else self.default_value_if_exists(default_values, "location"), "location": location if location else self.default_value_if_exists(default_values, "location"),
"organisers": self.default_value_if_exists(default_values, "organisers"),
"description": description, "description": description,
"tags": tags + tags_default, "tags": tags + tags_default,
"published": published, "published": published,
@ -239,6 +247,28 @@ class Extractor(ABC):
from .extractor_ggcal_link import GoogleCalendarLinkEventExtractor from .extractor_ggcal_link import GoogleCalendarLinkEventExtractor
if single_event: if single_event:
return [FacebookEventExtractor(), GoogleCalendarLinkEventExtractor()] return [FacebookEventExtractor(), GoogleCalendarLinkEventExtractor(), EventNotFoundExtractor()]
else: 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 result = "https://www.facebook.com" + u.path
# remove name in the url # 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: if match:
result = match[1] + "/" + match[3] result = match[1] + "/" + match[3]

View File

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

View File

@ -1,6 +1,11 @@
from .downloader import * from .downloader import *
from .extractor import * from .extractor import *
import logging
logger = logging.getLogger(__name__)
class URL2Events: class URL2Events:
def __init__( def __init__(
@ -29,8 +34,9 @@ class URL2Events:
else: else:
# if the extractor is not defined, use a list of default extractors # if the extractor is not defined, use a list of default extractors
for e in Extractor.get_default_extractors(self.single_event): for e in Extractor.get_default_extractors(self.single_event):
logger.warning('Extractor::' + type(e).__name__)
e.set_downloader(self.downloader) e.set_downloader(self.downloader)
events = e.extract(content, url, url_human, default_values, published) 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 events
return None 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 # Generated by Django 4.2.9 on 2024-10-10 20:35
from django.db import migrations from django.db import migrations
from agenda_culturel.models import Place
from django.contrib.gis.geos import Point from django.contrib.gis.geos import Point
def change_coord_format(apps, schema_editor): 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: for p in places:
l = p.location.split(',') 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])) p.location_pt = Point(float(l[1]), float(l[0]))
else: else:
p.location_pt = Point(3.08333, 45.783329) p.location_pt = Point(3.08333, 45.783329)
p.save() p.save(update_fields=["location_pt"])
def reverse_coord_format(apps, schema_editor): 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: for p in places:
p.location = ','.join([p.location_pt[1], p.location_pt[0]]) p.location = ','.join([p.location_pt[1], p.location_pt[0]])
p.save() p.save(update_fields=["location"])

View File

@ -184,15 +184,6 @@ def update_database_new(apps, schema_editor):
def update_database_old(apps, schema_editor): def update_database_old(apps, schema_editor):
update_database(apps, new_cats) update_database(apps, new_cats)
def add_tags(apps, schema_editor):
Tag = apps.get_model("agenda_culturel", "Tag")
new_tags = ["cinéma", "théâtre", "concert", "conférence", "exposition"]
new_tags = [Tag(name=t, description="", principal=True) for t in new_tags if Tag.objects.filter(name=t).count() == 0]
Tag.objects.bulk_create(new_tags)
def do_nothing(apps, schema_editor): def do_nothing(apps, schema_editor):
pass pass
@ -206,7 +197,6 @@ class Migration(migrations.Migration):
migrations.RunPython(create_new_categories, reverse_code=delete_new_categories), migrations.RunPython(create_new_categories, reverse_code=delete_new_categories),
migrations.RunPython(update_preserved_categories_new, reverse_code=update_preserved_categories_old), migrations.RunPython(update_preserved_categories_new, reverse_code=update_preserved_categories_old),
migrations.RunPython(update_database_new, reverse_code=update_database_old), migrations.RunPython(update_database_new, reverse_code=update_database_old),
migrations.RunPython(delete_old_categories, reverse_code=create_old_categories), migrations.RunPython(delete_old_categories, reverse_code=create_old_categories)
migrations.RunPython(add_tags, reverse_code=do_nothing)
] ]

View File

@ -0,0 +1,19 @@
# Generated by Django 4.2.9 on 2024-11-13 09:56
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('agenda_culturel', '0108_remove_duplicated_categories'),
]
operations = [
migrations.DeleteModel(
name='ModerationAnswer',
),
migrations.DeleteModel(
name='ModerationQuestion',
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 4.2.9 on 2024-11-13 17:27
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agenda_culturel', '0109_delete_moderationanswer_delete_moderationquestion'),
]
operations = [
migrations.AddField(
model_name='tag',
name='in_excluded_suggestions',
field=models.BooleanField(default=False, help_text='This tag will be part of the excluded suggestions.', verbose_name='In excluded suggestions'),
),
migrations.AddField(
model_name='tag',
name='in_included_suggestions',
field=models.BooleanField(default=False, help_text='This tag will be part of the included suggestions.', verbose_name='In included suggestions'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.9 on 2024-11-17 12:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agenda_culturel', '0110_tag_in_excluded_suggestions_and_more'),
]
operations = [
migrations.AlterField(
model_name='referencelocation',
name='main',
field=models.IntegerField(default=0, help_text='This location is one of the main locations (shown first higher values).', verbose_name='Main'),
),
]

View File

@ -0,0 +1,19 @@
# Generated by Django 4.2.9 on 2024-11-20 15:42
from django.db import migrations
import django_ckeditor_5.fields
class Migration(migrations.Migration):
dependencies = [
('agenda_culturel', '0111_alter_referencelocation_main'),
]
operations = [
migrations.AddField(
model_name='place',
name='description',
field=django_ckeditor_5.fields.CKEditor5Field(blank=True, help_text='Description of the place, including accessibility.', null=True, verbose_name='Description'),
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 4.2.9 on 2024-11-20 21:40
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('agenda_culturel', '0112_place_description'),
]
operations = [
migrations.RemoveField(
model_name='tag',
name='category',
),
]

View File

@ -0,0 +1,35 @@
# Generated by Django 4.2.9 on 2024-11-22 10:12
from django.db import migrations, models
import django.db.models.deletion
import django_ckeditor_5.fields
class Migration(migrations.Migration):
dependencies = [
('agenda_culturel', '0113_remove_tag_category'),
]
operations = [
migrations.CreateModel(
name='Organisation',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(help_text='Organisation name', max_length=512, unique=True, verbose_name='Name')),
('website', models.URLField(blank=True, help_text='Website of the organisation', max_length=1024, null=True, verbose_name='Website')),
('description', django_ckeditor_5.fields.CKEditor5Field(blank=True, help_text='Description of the organisation.', null=True, verbose_name='Description')),
('principal_place', models.ForeignKey(blank=True, help_text='Place mainly associated with this organizer. Mainly used if there is a similarity in the name, to avoid redundant displays.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='agenda_culturel.place', verbose_name='Principal place')),
],
),
migrations.AddField(
model_name='event',
name='organisers',
field=models.ManyToManyField(blank=True, help_text='list of event organisers. Organizers will only be displayed if one of them does not normally use the venue.', related_name='organised_events', to='agenda_culturel.organisation', verbose_name='Location (free form)'),
),
migrations.AddField(
model_name='recurrentimport',
name='defaultOrganiser',
field=models.ForeignKey(blank=True, default=None, help_text='Organiser of each imported event', null=True, on_delete=django.db.models.deletion.SET_DEFAULT, to='agenda_culturel.organisation', verbose_name='Organiser'),
),
]

View File

@ -0,0 +1,22 @@
# Generated by Django 4.2.9 on 2024-11-22 10:30
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agenda_culturel', '0114_organisation_event_organisers_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='organisation',
options={'verbose_name': 'Organisation', 'verbose_name_plural': 'Organisations'},
),
migrations.AlterField(
model_name='event',
name='organisers',
field=models.ManyToManyField(blank=True, help_text='list of event organisers. Organizers will only be displayed if one of them does not normally use the venue.', related_name='organised_events', to='agenda_culturel.organisation', verbose_name='Organisers'),
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 4.2.9 on 2024-11-23 09:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agenda_culturel', '0115_alter_organisation_options_alter_event_organisers'),
]
operations = [
migrations.AddIndex(
model_name='event',
index=models.Index(fields=['start_day', 'start_time'], name='agenda_cult_start_d_68ab5f_idx'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.9 on 2024-11-23 10:11
from django.db import migrations, models
import django.db.models.functions.text
class Migration(migrations.Migration):
dependencies = [
('agenda_culturel', '0116_event_agenda_cult_start_d_68ab5f_idx'),
]
operations = [
migrations.AddIndex(
model_name='event',
index=models.Index(models.F('start_time'), django.db.models.functions.text.Lower('title'), name='start_time title'),
),
]

View File

@ -0,0 +1,33 @@
# Generated by Django 4.2.9 on 2024-11-23 10:16
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agenda_culturel', '0117_event_start_time_title'),
]
operations = [
migrations.AlterModelOptions(
name='tag',
options={'verbose_name': 'Étiquette', 'verbose_name_plural': 'Étiquettes'},
),
migrations.AddIndex(
model_name='category',
index=models.Index(fields=['name'], name='agenda_cult_name_28aa03_idx'),
),
migrations.AddIndex(
model_name='referencelocation',
index=models.Index(fields=['name'], name='agenda_cult_name_76f079_idx'),
),
migrations.AddIndex(
model_name='staticcontent',
index=models.Index(fields=['name'], name='agenda_cult_name_fe4995_idx'),
),
migrations.AddIndex(
model_name='tag',
index=models.Index(fields=['name'], name='agenda_cult_name_9c9c74_idx'),
),
]

View File

@ -0,0 +1,74 @@
# Generated by Django 4.2.9 on 2024-11-27 09:00
from django.db import migrations, models
import django.db.models.deletion
import django_better_admin_arrayfield.models.fields
class Migration(migrations.Migration):
dependencies = [
('agenda_culturel', '0118_alter_tag_options_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='tag',
options={'verbose_name': 'Tag', 'verbose_name_plural': 'Tags'},
),
migrations.AlterField(
model_name='event',
name='category',
field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.SET_DEFAULT, to='agenda_culturel.category', verbose_name='Category'),
),
migrations.AlterField(
model_name='event',
name='description',
field=models.TextField(blank=True, null=True, verbose_name='Description'),
),
migrations.AlterField(
model_name='event',
name='end_day',
field=models.DateField(blank=True, null=True, verbose_name='End day'),
),
migrations.AlterField(
model_name='event',
name='end_time',
field=models.TimeField(blank=True, null=True, verbose_name='End time'),
),
migrations.AlterField(
model_name='event',
name='exact_location',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='agenda_culturel.place', verbose_name='Location'),
),
migrations.AlterField(
model_name='event',
name='image',
field=models.URLField(blank=True, help_text='External URL of the illustration image', max_length=1024, null=True, verbose_name='Illustration (URL)'),
),
migrations.AlterField(
model_name='event',
name='local_image',
field=models.ImageField(blank=True, max_length=1024, null=True, upload_to='', verbose_name='Illustration'),
),
migrations.AlterField(
model_name='event',
name='start_day',
field=models.DateField(verbose_name='Start day'),
),
migrations.AlterField(
model_name='event',
name='start_time',
field=models.TimeField(blank=True, null=True, verbose_name='Start time'),
),
migrations.AlterField(
model_name='event',
name='tags',
field=django_better_admin_arrayfield.models.fields.ArrayField(base_field=models.CharField(max_length=64), blank=True, null=True, size=None, verbose_name='Tags'),
),
migrations.AlterField(
model_name='event',
name='title',
field=models.CharField(max_length=512, verbose_name='Title'),
),
]

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

File diff suppressed because it is too large Load Diff

View File

@ -14,7 +14,8 @@ APP_ENV = os_getenv("APP_ENV", "dev")
DEBUG = os_getenv("DEBUG", "true").lower() in ["True", "true", "1", "yes", "y"] DEBUG = os_getenv("DEBUG", "true").lower() in ["True", "true", "1", "yes", "y"]
ALLOWED_HOSTS = os_getenv("ALLOWED_HOSTS", "localhost").split(",") ALLOWED_HOSTS = os_getenv("ALLOWED_HOSTS", "localhost").split(",")
if DEBUG:
ALLOWED_HOSTS = ALLOWED_HOSTS + ['testserver']
if DEBUG: if DEBUG:
CSRF_TRUSTED_ORIGINS = os_getenv("CSRF_TRUSTED_ORIGINS", "http://localhost").split( CSRF_TRUSTED_ORIGINS = os_getenv("CSRF_TRUSTED_ORIGINS", "http://localhost").split(
@ -55,9 +56,10 @@ INSTALLED_APPS = [
"robots", "robots",
"debug_toolbar", "debug_toolbar",
"cache_cleaner", "cache_cleaner",
"honeypot",
] ]
SITE_ID = 1 HONEYPOT_FIELD_NAME = "alias_name"
MIDDLEWARE = [ MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware", "django.middleware.security.SecurityMiddleware",
@ -71,6 +73,7 @@ MIDDLEWARE = [
"django.contrib.messages.middleware.MessageMiddleware", "django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware",
"debug_toolbar.middleware.DebugToolbarMiddleware", "debug_toolbar.middleware.DebugToolbarMiddleware",
'django.contrib.sites.middleware.CurrentSiteMiddleware',
# "django.middleware.cache.UpdateCacheMiddleware", # "django.middleware.cache.UpdateCacheMiddleware",
# "django.middleware.common.CommonMiddleware", # "django.middleware.common.CommonMiddleware",
# "django.middleware.cache.FetchFromCacheMiddleware", # "django.middleware.cache.FetchFromCacheMiddleware",
@ -144,10 +147,9 @@ TIME_ZONE = "Europe/Paris"
USE_I18N = True USE_I18N = True
USE_TZ = True USE_TZ = False
LANGUAGES = ( LANGUAGES = (
("en-us", _("English")),
("fr", _("French")), ("fr", _("French")),
) )

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)

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,589 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="196.25674mm"
height="195.90508mm"
viewBox="0 0 196.25674 195.90508"
version="1.1"
id="svg5"
xml:space="preserve"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="alunissage.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="0.67644895"
inkscape:cx="338.53257"
inkscape:cy="476.75438"
inkscape:window-width="1920"
inkscape:window-height="1020"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1"
showguides="true"><sodipodi:guide
position="87.392073,277.9802"
orientation="0,-1"
id="guide14594"
inkscape:locked="false" /></sodipodi:namedview><defs
id="defs2"><inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : 95.972889 : 1"
inkscape:vp_y="0 : 999.99997 : 0"
inkscape:vp_z="210.00001 : 47.405071 : 1"
inkscape:persp3d-origin="105 : -2.0949277 : 1"
id="perspective240" /><mask
maskUnits="userSpaceOnUse"
id="mask15432"><rect
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
id="rect15434"
width="178.99583"
height="178.99583"
x="-2.2718451"
y="12.442876"
ry="12.835461"
transform="rotate(-6.4517442)" /></mask><mask
maskUnits="userSpaceOnUse"
id="mask15436"><rect
style="fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
id="rect15438"
width="178.99583"
height="178.99583"
x="-2.2718451"
y="12.442876"
ry="12.835461"
transform="rotate(-6.4517442)" /></mask></defs><g
inkscape:label="Calque 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-6.8716276,-21.840211)"><rect
style="fill:#003737;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
id="rect14856"
width="253.23183"
height="167.6301"
x="-24.006699"
y="-10.841731"
ry="12.835461"
mask="url(#mask15436)"
transform="rotate(11.86311,-35.469528,138.62496)" /><path
sodipodi:type="star"
style="fill:#ffcc00;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
id="path15414-7"
inkscape:flatsided="false"
sodipodi:sides="5"
sodipodi:cx="117.51744"
sodipodi:cy="29.413408"
sodipodi:r1="11.625795"
sodipodi:r2="5.8128977"
sodipodi:arg1="-0.43776009"
sodipodi:arg2="0.19055844"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 128.04696,24.485096 -4.82184,6.029317 2.23323,7.390235 -7.22426,-2.722688 -6.33842,4.407635 0.35701,-7.712031 -6.15059,-4.666166 7.4449,-2.043609 2.53715,-7.291484 4.24419,6.44901 z"
inkscape:transform-center-x="-0.079786515"
inkscape:transform-center-y="-0.21888701"
transform="matrix(0.26075369,0.0547741,-0.0547741,0.26075369,114.71036,76.196413)" /><path
id="path14500"
mask="url(#mask15432)"
style="fill:#c5beb2;fill-opacity:1;stroke:none;stroke-linecap:round;stroke-linejoin:round"
d="m 52.195264,77.951831 c -0.0995,-0.0019 -0.200891,0.01327 -0.300757,0.04754 -0.456533,0.156667 -0.698235,0.650377 -0.541569,1.10691 l 2.971911,8.660453 c -3.387288,2.212886 -7.162412,5.308918 -10.404532,3.340364 -1.529299,-0.928561 -15.94685,17.177302 -17.573605,17.823722 -32.2185841,12.80254 -53.107353,33.35874 -53.107352,56.53505 2e-6,38.81626 58.59402,70.28305 130.87366,70.28305 72.27965,0 130.87419,-31.46679 130.87419,-70.28305 0,-38.81626 -59.1882,-79.527878 -130.87419,-70.283051 -23.606895,3.044413 -33.441303,-5.838959 -44.427257,-9.480042 -0.903167,-0.299337 -2.018561,2.77e-4 -3.188953,0.712101 l -0.656291,0.399459 -2.839103,-8.273397 c -0.122395,-0.356666 -0.450783,-0.582285 -0.806152,-0.589111 z"
transform="rotate(11.86311,-33.77711,138.8008)" /><g
id="g1576"
style="stroke-width:0.6;stroke-dasharray:none"
transform="rotate(11.86311,105,119.79275)"><path
style="fill:#fff7e7;fill-opacity:0.55895;stroke:none;stroke-width:0.6;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="m 102.8368,132.68366 c 1.03176,3.55726 17.34099,3.88136 17.73538,2.3582 0.39438,-1.52316 -1.52847,-2.36494 -1.52847,-2.36494 -1.08228,3.21929 -7.28172,2.38299 -13.21987,2.03963 z"
id="path1162-6"
sodipodi:nodetypes="csccc" /><path
style="fill:#ffffff;fill-opacity:0.294988;stroke:none;stroke-width:0.6;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="m 102.6432,132.68366 c -0.93632,0.70147 0.77648,-0.0567 -1.60327,0.4643 1.39003,2.05537 5.21589,1.69949 5.99161,2.3598 l -3.68801,-1.88107 z"
id="path1533"
sodipodi:nodetypes="ccccc" /><path
style="fill:#000000;fill-opacity:0.294988;stroke:none;stroke-width:0.6;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="m 120.6255,133.36309 c 1.54184,3.26173 -10.17172,2.70066 -13.59396,2.14467 8.66787,2.8241 15.90667,3.7079 17.91461,-0.53782 -2.54611,-0.0305 -3.04763,-0.99487 -4.32065,-1.60685 z"
id="path1533-7"
sodipodi:nodetypes="cccc" /><path
style="fill:none;stroke:#000000;stroke-width:0.6;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="m 102.6432,132.68366 c 1.03176,3.55726 17.34099,3.88136 17.73538,2.3582 0.39438,-1.52316 -1.52847,-2.36494 -1.52847,-2.36494"
id="path1162" /></g><g
id="g1576-5"
transform="matrix(0.51059462,0.20026137,-0.21398251,0.68212303,154.90211,50.373061)"
style="stroke-width:0.679551;stroke-dasharray:none"><path
style="fill:#fff7e7;fill-opacity:0.55895;stroke:none;stroke-width:0.679551;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="m 103.34353,133.62669 c 1.03176,3.55726 16.83426,2.93833 17.22865,1.41517 0.39438,-1.52316 -1.52847,-2.36494 -1.52847,-2.36494 -0.19099,0.56812 -1.44864,0.34803 -1.9288,0.68854 -2.24071,1.58902 -6.40085,1.63386 -11.29107,1.35109 z"
id="path1162-6-3"
sodipodi:nodetypes="cscscc" /><path
style="fill:#ffffff;fill-opacity:0.294988;stroke:none;stroke-width:0.679551;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="m 103.34353,133.62669 c -0.93632,0.70147 0.77648,-0.0567 -1.60327,0.4643 1.39003,2.05537 4.51556,0.75646 5.29128,1.41677 z"
id="path1533-5"
sodipodi:nodetypes="ccccc" /><path
style="fill:#000000;fill-opacity:0.294988;stroke:none;stroke-width:0.679551;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="m 119.62517,132.53874 c 4.06792,4.14821 -10.28515,2.76677 -13.80133,2.17781 8.66787,2.8241 18.39394,5.38385 20.40188,1.13813 -3.61173,-2.57319 -5.4227,-2.93647 -6.60055,-3.31594 z"
id="path1533-7-6"
sodipodi:nodetypes="cccc" /><path
style="fill:none;stroke:#000000;stroke-width:0.679551;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="m 106.64826,132.51145 c -11.319438,2.14423 12.45226,3.77801 13.73032,2.53041 3.06792,-2.99478 -5.8967,-3.56684 -6.68296,-3.13405"
id="path1162-2"
sodipodi:nodetypes="csc" /></g><g
id="g1576-9"
transform="matrix(1.9425519,0.40805381,-0.39033153,1.8581845,-7.6916871,-95.795008)"
style="stroke-width:0.390962;stroke-dasharray:none"><path
style="fill:#fff7e7;fill-opacity:0.55895;stroke:none;stroke-width:0.390962;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="m 102.8368,132.68366 c 3.10079,2.69207 17.34099,3.88136 17.73538,2.3582 0.39438,-1.52316 -0.91154,-1.8437 -0.91154,-1.8437 -1.81421,2.61261 -7.77833,1.56442 -13.71648,1.22106 z"
id="path1162-6-1"
sodipodi:nodetypes="csccc" /><path
style="fill:#ffffff;fill-opacity:0.294988;stroke:none;stroke-width:0.390962;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="m 102.6432,132.68366 c -0.93632,0.70147 0.77648,-0.0567 -1.60327,0.4643 1.39003,2.05537 5.38317,0.9933 6.15889,1.65361 l -3.85529,-1.17488 z"
id="path1533-2"
sodipodi:nodetypes="ccccc" /><path
style="fill:#000000;fill-opacity:0.294988;stroke:none;stroke-width:0.390962;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="m 119.79645,132.67692 c 3.69728,4.17523 -9.13801,2.69648 -12.59763,2.12465 0.36908,0.83812 13.1969,4.98485 16.72321,1.10249 -0.33987,-0.41869 -2.85256,-2.61516 -4.12558,-3.22714 z"
id="path1533-7-7"
sodipodi:nodetypes="cccc" /><path
style="fill:none;stroke:#000000;stroke-width:0.390962;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="m 106.05918,131.55532 c -1.77289,-0.20918 -3.71225,0.78168 -3.22238,1.1286 3.42679,2.42499 17.19672,3.69085 17.54178,2.3582 0.21258,-0.82098 -0.24806,-1.44401 -0.71794,-1.84396"
id="path1162-0"
sodipodi:nodetypes="cssc" /></g><g
id="g1576-9-9"
transform="matrix(0.84888437,-0.29780159,0.16736444,0.96227663,-93.058232,39.699665)"
style="stroke-width:0.462959;stroke-dasharray:none"><path
style="fill:#fff7e7;fill-opacity:0.55895;stroke:none;stroke-width:0.462959;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="m 102.8368,132.68366 c 1.03176,3.55726 17.34099,3.88136 17.73538,2.3582 0.39438,-1.52316 -0.95533,-1.64385 -0.95533,-1.64385 -1.08228,3.21929 -7.85486,1.6619 -13.79301,1.31854 z"
id="path1162-6-1-3"
sodipodi:nodetypes="csccc" /><path
style="fill:#000000;fill-opacity:0.294988;stroke:none;stroke-width:0.462959;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="m 120.6255,133.36309 c 1.54184,3.26173 -10.20493,2.23076 -13.62717,1.67477 8.66787,2.8241 15.08072,5.33692 17.08866,1.0912 -1.40447,-0.83726 -2.18847,-2.15399 -3.46149,-2.76597 z"
id="path1533-7-7-0"
sodipodi:nodetypes="cccc" /><path
style="fill:none;stroke:#000000;stroke-width:0.462959;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="m 104.98821,132.30093 c -2.97484,-0.16805 -2.07389,0.81205 -1.79877,1.01131 3.05188,2.21032 16.82736,3.12682 17.18914,1.72962 0.14178,-0.54755 -0.0159,-1.00705 -0.27606,-1.37105"
id="path1162-0-6"
sodipodi:nodetypes="cssc" /></g><g
id="g13683"
transform="matrix(1.1100761,0.33327025,-0.33327025,1.1100761,40.007817,-13.329961)"
style="stroke-width:0.999979;stroke-dasharray:none"><path
style="fill:#010000;fill-opacity:0.254893;stroke:none;stroke-width:0.707892;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="m 154.14006,144.23649 c 0.96552,4.45831 37.8521,2.93154 43.31493,0.19171 13.87936,-2.9076 -21.07506,-6.50284 -27.9251,-6.90834 -4.61395,0.0322 -15.93897,3.74541 -15.38983,6.71663 z"
id="path38464-7-3"
sodipodi:nodetypes="cccc"
transform="matrix(1.4126153,0,0,1.4126153,-123.1007,-68.10763)" /><path
style="fill:#010000;fill-opacity:0.254893;stroke:none;stroke-width:0.999979;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="m 101.41768,142.86865 c 0.56665,2.63741 20.69879,2.10306 23.65815,0.54228 7.55455,-1.57625 -11.56752,-4.04179 -15.31224,-4.34869 -2.51963,-0.0268 -8.67203,2.04966 -8.34591,3.80641 z"
id="path38464-7-3-1"
sodipodi:nodetypes="cccc" /><path
style="fill:#fffaed;fill-opacity:1;stroke:#2d002d;stroke-width:0.999979;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 120.30447,118.03891 -2.84964,9.16842 11.39857,0.61949 3.09743,-0.61949 -1.85846,-10.53129 z"
id="path4251" /><rect
style="fill:#ded6c3;fill-opacity:1;stroke:#2d002d;stroke-width:0.999979;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
id="rect611"
width="2.1245613"
height="30.266191"
x="138.8942"
y="56.15984"
ry="0.68848425"
transform="rotate(23.593176)" /><path
id="rect611-5"
style="fill:#e3c56d;fill-opacity:1;stroke:#2d002d;stroke-width:0.999979;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none"
d="m 96.397714,128.03026 0.685099,0.29921 c 0.349537,0.15266 0.508031,0.55695 0.355371,0.90649 l -2.798277,6.40709 c -0.15266,0.34953 -4.707521,1.9948 -5.057058,1.84214 l -0.6851,-0.29921 c -0.349537,-0.15266 -0.67203,-0.69387 -0.355371,-0.90649 l 4.953966,-3.32627 1.994881,-4.56759 c 0.152659,-0.34954 0.556951,-0.50803 0.906489,-0.35537 z"
sodipodi:nodetypes="ssssssscss" /><rect
style="fill:#969184;fill-opacity:1;stroke:#2d002d;stroke-width:0.999979;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
id="rect611-3-6"
width="1.5656743"
height="26.733509"
x="-82.484116"
y="149.62541"
ry="0.68848425"
transform="matrix(-0.91641041,0.40023989,0.40023989,0.91641041,0,0)" /><rect
style="fill:#c2bbaa;fill-opacity:1;stroke:#2d002d;stroke-width:0.999979;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
id="rect611-3-6-7"
width="1.9859811"
height="27.541084"
x="147.46117"
y="46.883339"
ry="0.68848425"
transform="rotate(23.593176)" /><rect
style="fill:#ded6c3;fill-opacity:1;stroke:#2d002d;stroke-width:0.999979;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
id="rect611-3"
width="2.1245613"
height="30.266191"
x="-90.528412"
y="155.31551"
ry="0.68848425"
transform="matrix(-0.91641041,0.40023989,0.40023989,0.91641041,0,0)" /><path
style="fill:#fffaed;fill-opacity:1;stroke:#2d002d;stroke-width:0.999979;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 125.74289,77.772227 -13.47406,5.823183 1.85846,8.672825 11.79229,0.697766 z"
id="path4138"
sodipodi:nodetypes="ccccc" /><path
style="fill:#99978f;fill-opacity:1;stroke:#2d002d;stroke-width:0.999979;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 135.31376,80.374076 -9.57087,-2.601849 -2.51485,14.293188 11.83793,1.441795 z"
id="path4138-5"
sodipodi:nodetypes="ccccc" /><path
style="fill:#fffaed;fill-opacity:1;stroke:#2d002d;stroke-width:0.999979;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 105.09002,106.45299 7.6744,-3.15788 6.19488,-0.37169 2.08204,-10.246073 9.75647,0.577307 1.97857,10.807906 8.74996,-0.78515 4.21251,2.40094 -1.68214,0.43101 -9.50403,14.6199 -23.12458,0.14575 z"
id="path4148"
sodipodi:nodetypes="cccccccccccc" /><path
style="fill:#43423e;fill-opacity:1;stroke:none;stroke-width:0.999979;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="m 115.05652,103.47421 -2.61954,1.27227 6.03364,1.0729 10.80028,0.28998 8.31727,-0.5 3.1935,-1.8063 -8.29002,0.63247 -0.60277,-4.08554 -10.78233,-1.921271 z"
id="path5886"
sodipodi:nodetypes="cccccccccc" /><path
style="fill:#c9c5ba;fill-opacity:1;stroke:#2d002d;stroke-width:0.999979;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 129.2709,97.103982 -5.22158,4.319628 -2.94277,-2.994889 1.62836,-8.44273 2.95585,-3.079736"
id="path4146"
sodipodi:nodetypes="ccccc" /><path
style="fill:#a29f96;fill-opacity:1;stroke:#2d002d;stroke-width:0.999979;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 141.17794,101.27836 -9.8941,1.84077 -7.23452,-1.69552 4.64514,-3.842756"
id="path4146-6"
sodipodi:nodetypes="cccc" /><path
style="fill:#fffaed;fill-opacity:1;stroke:#2d002d;stroke-width:0.999979;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 127.67311,98.800416 13.50483,2.477954 3.46913,-4.832008 -6.81436,-9.787903 c 0,0 -5.94708,-2.354053 -6.31877,-1.982359 -0.37169,0.371692 -5.82318,2.230155 -5.82318,2.230155 l -0.1239,8.672824 z"
id="path751" /><path
style="fill:#d6d2c7;fill-opacity:1;stroke:#2d002d;stroke-width:0.999979;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 106.50828,94.250596 4.27378,-3.978166 h 2.23016 l 8.02912,2.404919 1.75878,3.060015 c 0,0 0.24779,4.336416 -0.1239,4.212516 -0.37169,-0.123897 -3.71692,2.97354 -3.71692,2.97354 l -6.19488,0.37169 c 0,0 -2.84964,0.74339 -3.22134,0.61949 -0.37169,-0.1239 -3.0348,-3.96472 -3.0348,-3.96472 z"
id="path4140" /><path
style="fill:#96938b;fill-opacity:1;stroke:#2d002d;stroke-width:0.999979;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 111.76884,94.583154 11.03128,1.57438 -0.1239,4.212506 -3.71692,2.97354 -7.80555,0.26127 z"
id="path4140-7"
sodipodi:nodetypes="cccccc" /><path
style="fill:#fffaed;fill-opacity:1;stroke:#2d002d;stroke-width:0.999979;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 106.50828,94.250596 4.27378,-3.978166 h 2.23016 l 3.44263,4.764064 -0.68593,4.733009 -3.0045,3.525607 -3.22134,0.61949 -3.0348,-3.96472 z"
id="path4140-2"
sodipodi:nodetypes="ccccccccc" /><path
style="fill:#d4aa00;fill-opacity:1;stroke:#2d002d;stroke-width:0.999979;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 135.44739,107.04067 8.60932,-0.93131 -0.64536,13.81457 -8.85867,0.80533 z"
id="path4142" /><path
style="fill:#d4aa00;fill-opacity:1;stroke:#2d002d;stroke-width:0.999979;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 105.09002,106.45299 9.37357,-0.6336 0.80533,15.05562 h -7.68164 z"
id="path4144" /><rect
style="fill:#84827b;fill-opacity:1;stroke:#2d002d;stroke-width:0.999979;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
id="rect4379"
width="15.495975"
height="7.5343547"
x="116.95485"
y="111.96205"
ry="0.68848425" /><path
id="rect611-5-3-2"
style="fill:#e3c56d;fill-opacity:1;stroke:#2d002d;stroke-width:0.999979;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none"
d="m 143.65023,123.32624 -0.6851,0.29921 c -0.34954,0.15266 -0.50965,0.55766 -0.35537,0.90649 l 1.94251,4.39209 c -0.1135,0.3747 3.00822,0.0602 1.14357,-2.68983 l -1.13913,-2.55259 c -0.15544,-0.34831 -0.55695,-0.50802 -0.90648,-0.35537 z"
sodipodi:nodetypes="sssccss" /><path
id="rect611-5-3-2-1"
style="fill:#e3c56d;fill-opacity:1;stroke:#2d002d;stroke-width:0.999979;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none"
d="m 108.24636,122.39419 0.6851,0.29921 c 0.34954,0.15266 0.50965,0.55766 0.35537,0.90649 l -1.94251,4.39209 c 0.1135,0.3747 -3.00822,0.0602 -1.14357,-2.68983 l 1.13913,-2.55259 c 0.15544,-0.34831 0.55695,-0.50802 0.90648,-0.35537 z"
sodipodi:nodetypes="sssccss" /><g
id="g11682"
transform="translate(68.350363,-11.452931)"
style="stroke-width:0.999979;stroke-dasharray:none"><g
id="g11781"
transform="matrix(-0.01139951,0.54218684,-0.54218684,-0.01139951,138.28173,106.48398)"
style="stroke-width:1.84394;stroke-dasharray:none"><path
style="fill:#eb4402;fill-opacity:1;stroke:#000000;stroke-width:1.84394;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="m 71.402761,191.27378 c -2.113043,0.27054 -16.51583,0.34774 -21.68703,-7.93835 -5.171192,-8.2861 -3.76397,-13.58169 -2.23407,-17.59746 1.52989,-4.01577 2.90744,-5.92547 5.80687,-8.7927 2.89944,-2.86723 5.99845,-8.59829 11.9153,-8.91316 5.91685,-0.31487 13.24668,1.03438 18.14476,6.51896 15.030422,13.3405 3.49522,36.12115 -11.94583,36.72271 z"
id="path38521"
sodipodi:nodetypes="czzzzcc" /><path
style="fill:none;stroke:#000000;stroke-width:1.84394;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 52.045331,168.07345 c 0,0 4.20442,-1.16049 5.95753,-1.68797 0.53786,-0.16183 0.79434,-3.57452 0.69505,-3.97169 -0.0993,-0.39717 -1.09221,-2.4823 -1.09221,-2.4823"
id="path39627"
sodipodi:nodetypes="cszc" /><path
style="fill:#510701;fill-opacity:1;stroke:#000000;stroke-width:1.84394;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="m 57.857211,165.65634 c -0.10531,-0.14042 -1.84121,-1.73181 -2.63288,-2.7733 -0.79167,-1.04149 -1.69399,-2.52693 -2.0712,-3.45155 -0.37721,-0.92462 -0.73899,-1.48616 -0.45636,-1.91264 0.28262,-0.42648 0.73423,-0.30422 1.08825,-0.18241 0.35403,0.12181 0.68488,0.55237 0.77231,0.84252 0.0874,0.29015 -0.11967,0.3973 -0.10531,0.667 0.0144,0.2697 -0.0456,0.27495 0.24573,0.91273 0.29133,0.63778 1.16007,1.94648 1.82547,2.70309 0.6654,0.75661 1.8591,1.04674 2.08899,1.83989 0.22989,0.79315 -0.0927,2.0968 -0.60935,2.08381 -0.51669,-0.013 -0.21586,-0.90466 -0.21586,-0.90466"
id="path39629"
sodipodi:nodetypes="czzzzzzzzzzc" /><path
style="fill:#006060;fill-opacity:1;stroke:#000000;stroke-width:1.84394;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="m 54.017211,161.49841 c -8.81401,3.40711 -10.967102,13.7965 -9.431327,18.16737 1.535777,4.37087 2.112697,8.01053 6.643607,9.05239 -1.87367,-6.18344 -0.6994,-9.06026 -0.24737,-13.55754 0.46657,-4.6417 3.29718,-7.78272 3.03509,-13.66222"
id="path1596-0"
sodipodi:nodetypes="czcac" /></g></g><path
id="rect611-5-3"
style="fill:#e3c56d;fill-opacity:1;stroke:#2d002d;stroke-width:0.999979;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none"
d="m 154.06944,128.30107 -0.6851,0.29921 c -0.34954,0.15266 -0.50803,0.55695 -0.35537,0.90649 l 2.79827,6.40709 c 0.15266,0.34953 4.70752,1.9948 5.05706,1.84214 l 0.6851,-0.29921 c 0.34954,-0.15266 0.67203,-0.69387 0.35537,-0.90649 l -4.95396,-3.32627 -1.99489,-4.56759 c -0.15265,-0.34954 -0.55695,-0.50803 -0.90648,-0.35537 z"
sodipodi:nodetypes="ssssssscss" /><path
style="fill:#010000;fill-opacity:0.0677805;stroke:none;stroke-width:0.999979;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:0.448687"
d="m 55.100582,93.762192 c 2.207116,5.124587 9.321543,8.525138 5.634754,17.629658 9.491228,-1.65632 17.957048,-1.89985 23.547213,-12.53699 -7.75868,1.85165 -15.669366,-1.119461 -22.122416,-2.661586 -3.017395,-0.721085 -6.271335,-2.616591 -7.059551,-2.431082 z"
id="path1056"
sodipodi:nodetypes="cccsc" /></g><g
id="g13651"
transform="matrix(1.1630717,-0.1292531,0.1292531,1.1630717,-24.892917,-1.9386015)"><path
style="fill:#008989;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="m 42.665935,137.70912 c -1.194356,-3.34818 -2.12459,-5.15105 -4.19392,-6.18037 -3.842759,-1.91146 -7.019594,-1.96348 -11.288621,-1.52093 -2.496518,0.2588 -5.02284,1.69478 -6.752563,3.33155 4.475795,1.38898 6.045382,3.25862 9.364751,3.91095 4.212272,0.82782 8.580238,0.30587 12.870353,0.4588"
id="path1596"
sodipodi:nodetypes="cccccc" /><path
style="fill:#010000;fill-opacity:0.268258;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 49.779293,186.08404 c 0.96552,4.45831 25.01987,3.96877 30.4827,1.22894 13.87936,-2.9076 -16.08645,-5.72863 -22.93649,-6.13413 -4.61395,0.0322 -8.09535,1.93397 -7.54621,4.90519 z"
id="path38464-7"
sodipodi:nodetypes="cccc"
transform="matrix(1.036102,0,0,1.036102,-21.713571,-27.542843)" /><path
style="fill:#a30b0e;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="m 33.983044,165.18272 c 0,0 1.529564,0.72114 2.455335,0.77196 0.925764,0.0508 1.834838,-0.45446 2.932539,-0.57897 1.097708,-0.1245 2.418383,-0.24077 3.637575,-0.11265 1.219185,0.12811 1.952311,1.0547 3.609267,0.88462 1.656956,-0.17008 3.491919,-0.39037 5.666685,-2.36448 2.174767,-1.9741 4.865746,-7.29359 5.848185,-10.05882 0.982432,-2.76524 0.703154,-5.07155 0.643539,-6.39325 -0.05959,-1.3217 -0.208211,-1.49572 -0.208211,-1.49572"
id="path6267"
sodipodi:nodetypes="czzzzzzzc" /><path
style="fill:#c07f16;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="m 38.461151,136.9473 c 1.35573,0.37324 2.309562,0.8613 4.096886,1.11122 0.994124,-0.15196 1.841666,-0.0987 3.149379,-0.68969 l -0.995306,6.88319 c 0,0 -4.907344,0.0713 -4.771272,-0.3995 0.136064,-0.47077 -1.479687,-6.90522 -1.479687,-6.90522 z"
id="path24930"
sodipodi:nodetypes="ccccsc" /><path
style="fill:#f1e890;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="m 43.034803,142.60286 c -0.935713,-2.59107 -3.812328,-6.78313 -7.729574,-6.24009 -3.917239,0.54304 -7.35144,5.76415 -7.971266,11.25782 -0.619818,5.49367 5.398984,17.75754 7.434345,17.89209 2.355997,0.40404 4.34644,-1.17116 4.886578,-1.87052 0.540145,-0.69936 0.768947,1.03528 1.371544,1.15912 0.602598,0.12384 1.077606,-0.73571 1.812803,-0.55339 0.735197,0.18233 2.932036,1.63625 4.414655,0.73425 6.133481,-3.8826 11.61906,-11.23177 11.344545,-18.37487 -0.167597,-4.36102 -3.17722,-9.92296 -7.46061,-10.7591 -5.082506,-0.99215 -6.805686,2.91188 -8.10302,6.75469 z"
id="path6257"
sodipodi:nodetypes="czzczzzcssc" /><path
style="fill:#744e0b;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="m 42.473921,135.50687 -0.310153,-1.00963 0.689003,-0.35083 0.613684,0.15782 0.04829,3.81859 v 2.48554 c -0.267731,0.17111 -0.618885,0.0114 -0.964969,-0.1281 -2.2e-5,-0.0968 -0.07585,-4.97339 -0.07585,-4.97339 z"
id="path6259"
sodipodi:nodetypes="cccccccc" /><path
style="fill:#60140f;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="m 46.582777,147.89717 c -0.07872,0.32449 -0.06696,0.58859 -0.03684,0.95049 0.03013,0.36191 -0.201055,1.05388 0.249846,1.20205 0.450901,0.14817 1.057495,-0.43819 1.206215,-1.08137 0.14872,-0.64317 -0.926857,-2.09678 -0.926857,-2.09678 0,0 -0.413643,0.70112 -0.492369,1.02561 z"
id="path6261"
sodipodi:nodetypes="zzzzcz" /><path
style="fill:#60140f;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="m 38.823632,147.137 c -0.387631,0.0217 -0.158494,0.22509 -0.289482,0.55826 -0.130995,0.33317 -0.62437,1.08311 -0.406436,1.46373 0.217941,0.38061 0.737903,0.41781 1.075111,0.2859 0.337208,-0.13192 0.573508,-0.4352 0.567666,-0.89527 -0.0058,-0.46008 -0.559227,-1.43438 -0.946859,-1.41262 z"
id="path6263"
sodipodi:nodetypes="zzzzzz" /><path
style="fill:#f5dc0c;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="m 42.742754,144.31587 -0.592736,3.33348 -0.231085,5.08667 0.729398,2.86364 0.550576,-3.43708 -0.02801,-2.59306 z"
id="path6265" /><path
style="fill:#ff6600;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 63.571721,126.76017 c 1.094536,11.13944 0.818311,21.64168 -1.928813,33.00992 0,0 3.528354,-4.80953 3.519073,-2.8943 -0.02658,5.48412 2.630888,-29.5498 1.591832,-29.40137 z"
id="path3853"
sodipodi:nodetypes="ccscc" /><path
d="m 57.958721,157.97494 c 0,0 -0.355132,-4.22053 2.994472,-5.6209 0,0 3.297109,-0.26031 3.603353,0.72059 0,0 3.320156,-0.85575 3.303628,1.60641 0,0 0.195132,0.99026 -0.655974,1.50929 0,0 2.073953,0.72031 0.890684,2.60951 l -1.146963,0.99133 c 0,0 1.14913,1.69296 -0.90507,2.53692 0,0 -0.619668,0.61141 -1.665989,0.14018 0,0 -2.66452,1.56645 -4.971036,-0.58839 0,0 -1.499942,-0.54491 -1.447105,-3.90494"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:0.729413;stroke-dasharray:none;stroke-opacity:1"
id="path2902" /><path
d="m 57.958721,157.97494 c 0,0 -0.355132,-4.22053 2.994472,-5.6209 0,0 3.231666,-0.34283 3.53791,0.63807 0,0 3.385599,-0.77323 3.369071,1.68893 0,0 0.195132,0.99026 -0.655974,1.50929 0,0 2.073953,0.72031 0.890684,2.60951 l -1.146963,0.99133 c 0,0 1.14913,1.69296 -0.90507,2.53692 0,0 -0.619668,0.61141 -1.665989,0.14018 0,0 -2.66452,1.56645 -4.971036,-0.58839 0,0 -1.499942,-0.54491 -1.447105,-3.90494 z"
style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
id="path2904"
sodipodi:nodetypes="ccccccccccc" /><path
d="m 64.491103,152.99211 c 0.356304,1.02136 -0.206169,1.90598 -0.452664,2.81293 -0.285407,1.0506 0.531528,2.66525 -0.872464,3.18617 -3.056254,1.13409 -2.820371,-4.2538 -1.556164,-5.13196"
style="fill:#ffffff;stroke:#000000;stroke-width:0.729413;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
id="path2906"
sodipodi:nodetypes="cccc" /><path
d="m 60.574986,156.18338 c -0.429251,-0.21891 -0.583819,-0.69727 -0.547068,-1.18435"
style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
id="path2908" /><path
d="m 67.2042,156.19033 c -0.717377,0.62372 -2.974059,1.02627 -3.230626,0.32462"
style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
id="path2910"
sodipodi:nodetypes="cc" /><path
d="m 67.436314,159.29719 c -1.317174,0.93586 -2.860074,0.81234 -3.832335,-0.34865"
style="fill:none;stroke:#000000;stroke-width:0.729413;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
id="path2912" /><path
style="fill:#ff6600;stroke:none;stroke-width:0.729413;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 61.642908,159.77009 v 0 l -0.196868,6.76242 c 0.138065,0.90621 3.047532,1.30724 2.913473,0.10362 -0.0029,-0.0261 0.07136,-3.35353 0.144362,-4.1272 -2.987756,-1.7577 -1.763448,-2.73153 -2.860967,-2.73884 z"
id="path3853-6-4"
sodipodi:nodetypes="cccscc" /><path
d="m 62.40537,159.2052 c -1.058657,1.47852 2.221477,3.35076 2.098505,3.30373"
style="fill:none;stroke:#000000;stroke-width:0.729413;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
id="path2914"
sodipodi:nodetypes="cc" /><path
d="m 61.475024,158.90397 c -0.304724,0.26545 -0.690797,0.6674 -1.175003,0.65813"
style="fill:none;stroke:#000000;stroke-width:0.729413;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1"
id="path2916" /><path
style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 61.678487,158.97468 v 0 l -0.232447,7.55783 c 0.06153,1.02855 2.935348,1.19145 2.91348,0.10362 -6.49e-4,-0.0263 0.07135,-3.35353 0.144355,-4.1272"
id="path3853-6"
sodipodi:nodetypes="cccsc" /><path
style="fill:#ff6600;stroke:none;stroke-width:0.729413;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 63.641927,124.75769 v 0 l -0.196869,-6.76242 c 0.138065,-0.90621 3.047532,-1.30724 2.913473,-0.10362 -0.0029,0.0261 0.07136,3.35353 0.144362,4.1272 -2.987755,1.7577 -1.763448,2.73153 -2.860966,2.73884 z"
id="path3853-6-4-7"
sodipodi:nodetypes="cccscc" /><path
style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 63.677506,125.5531 v 0 l -0.232448,-7.55783 c 0.06153,-1.02855 2.935348,-1.19145 2.913481,-0.10362 -6.49e-4,0.0263 0.07135,3.35353 0.144355,4.1272"
id="path3853-6-9"
sodipodi:nodetypes="cccsc" /><path
style="fill:#00b7b7;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 63.295152,131.49341 c 0.393707,0.51918 1.612281,0.99644 3.217121,0.7211 0,0 3.906619,1.4742 5.936024,1.38123 0.747349,-0.0342 1.382417,-0.72421 2.130338,-0.70639 2.811582,0.067 7.872764,3.03397 7.872764,3.03397 L 82.999,117.56681 c 0,0 -5.28177,-0.18252 -7.822554,0.42013 -0.958982,0.22745 -1.734177,1.04049 -2.709213,1.18432 -1.988881,0.29339 -5.988418,-0.7172 -5.988418,-0.7172 -0.919318,0.55366 -2.006773,0.51884 -3.183663,0.17099 z"
id="path12766"
sodipodi:nodetypes="ccssccssccc" /></g><path
sodipodi:type="star"
style="fill:#ffcc00;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
id="path15414"
inkscape:flatsided="false"
sodipodi:sides="5"
sodipodi:cx="117.51744"
sodipodi:cy="29.413408"
sodipodi:r1="11.625795"
sodipodi:r2="5.8128977"
sodipodi:arg1="-0.43776009"
sodipodi:arg2="0.19055844"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 128.04696,24.485096 -4.82184,6.029317 2.23323,7.390235 -7.22426,-2.722688 -6.33842,4.407635 0.35701,-7.712031 -6.15059,-4.666166 7.4449,-2.043609 2.53715,-7.291484 4.24419,6.44901 z"
inkscape:transform-center-x="-0.15550371"
inkscape:transform-center-y="-0.42661129"
transform="matrix(0.5082118,0.10675533,-0.10675533,0.5082118,75.701411,28.86227)" /><path
sodipodi:type="star"
style="fill:#ffcc00;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
id="path15414-2"
inkscape:flatsided="false"
sodipodi:sides="5"
sodipodi:cx="117.51744"
sodipodi:cy="29.413408"
sodipodi:r1="11.625795"
sodipodi:r2="5.8128977"
sodipodi:arg1="-0.43776009"
sodipodi:arg2="0.19055844"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 128.04696,24.485096 -4.82184,6.029317 2.23323,7.390235 -7.22426,-2.722688 -6.33842,4.407635 0.35701,-7.712031 -6.15059,-4.666166 7.4449,-2.043609 2.53715,-7.291484 4.24419,6.44901 z"
inkscape:transform-center-x="-0.11728461"
inkscape:transform-center-y="-0.32177051"
transform="matrix(0.38331637,0.0805197,-0.0805197,0.38331637,133.48215,38.188561)" /><path
sodipodi:type="star"
style="fill:#ffcc00;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
id="path15414-2-2"
inkscape:flatsided="false"
sodipodi:sides="5"
sodipodi:cx="117.51744"
sodipodi:cy="29.413408"
sodipodi:r1="11.625795"
sodipodi:r2="5.8128977"
sodipodi:arg1="-0.43776009"
sodipodi:arg2="0.19055844"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 128.04696,24.485096 -4.82184,6.029317 2.23323,7.390235 -7.22426,-2.722688 -6.33842,4.407635 0.35701,-7.712031 -6.15059,-4.666166 7.4449,-2.043609 2.53715,-7.291484 4.24419,6.44901 z"
inkscape:transform-center-x="-0.054016501"
inkscape:transform-center-y="-0.14818899"
transform="matrix(0.17653378,0.03708281,-0.03708281,0.17653378,74.231021,67.773286)" /><path
sodipodi:type="star"
style="fill:#ffcc00;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
id="path15414-2-6"
inkscape:flatsided="false"
sodipodi:sides="5"
sodipodi:cx="117.51744"
sodipodi:cy="29.413408"
sodipodi:r1="11.625795"
sodipodi:r2="5.8128977"
sodipodi:arg1="-0.43776009"
sodipodi:arg2="0.19055844"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 128.04696,24.485096 -4.82184,6.029317 2.23323,7.390235 -7.22426,-2.722688 -6.33842,4.407635 0.35701,-7.712031 -6.15059,-4.666166 7.4449,-2.043609 2.53715,-7.291484 4.24419,6.44901 z"
inkscape:transform-center-x="-0.10331594"
inkscape:transform-center-y="-0.28344015"
transform="matrix(0.33765476,0.070928,-0.070928,0.33765476,18.883161,63.313526)" /><path
sodipodi:type="star"
style="fill:#ffcc00;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
id="path15414-0"
inkscape:flatsided="false"
sodipodi:sides="5"
sodipodi:cx="117.51744"
sodipodi:cy="29.413408"
sodipodi:r1="11.625795"
sodipodi:r2="5.8128977"
sodipodi:arg1="-0.43776009"
sodipodi:arg2="0.19055844"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 128.04696,24.485096 -4.82184,6.029317 2.23323,7.390235 -7.22426,-2.722688 -6.33842,4.407635 0.35701,-7.712031 -6.15059,-4.666166 7.4449,-2.043609 2.53715,-7.291484 4.24419,6.44901 z"
inkscape:transform-center-x="-0.15550652"
inkscape:transform-center-y="-0.42661155"
transform="matrix(0.5082118,0.10675533,-0.10675533,0.5082118,126.66807,99.717697)" /><path
sodipodi:type="star"
style="fill:#ffcc00;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
id="path15416"
inkscape:flatsided="false"
sodipodi:sides="5"
sodipodi:cx="156.13559"
sodipodi:cy="46.936283"
sodipodi:r1="5.2078781"
sodipodi:r2="2.6039393"
sodipodi:arg1="-0.43409146"
sodipodi:arg2="0.19422707"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 160.86045,44.745921 -2.16988,2.692944 0.98824,3.314168 -3.23167,-1.231515 -2.84658,1.964009 0.1726,-3.454063 -2.74753,-2.100344 3.33835,-0.903212 1.14851,-3.262094 1.89061,2.895847 z"
inkscape:transform-center-x="-0.14002212"
inkscape:transform-center-y="-0.36225272"
transform="rotate(11.86311,-94.558638,67.518227)" /><path
sodipodi:type="star"
style="fill:#ffcc00;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
id="path15418"
inkscape:flatsided="false"
sodipodi:sides="5"
sodipodi:cx="65.107475"
sodipodi:cy="28.732956"
sodipodi:r1="11.61906"
sodipodi:r2="5.8095307"
sodipodi:arg1="2.7426294"
sodipodi:arg2="3.3709479"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 54.400928,33.246533 5.04915,-5.834372 -1.943775,-7.466965 7.109091,2.999107 6.500846,-4.156059 -0.65549,7.687923 5.961519,4.898379 -7.514207,1.75229 -2.816425,7.183424 -3.988544,-6.604947 z"
inkscape:transform-center-x="0.43783231"
inkscape:transform-center-y="0.68483807"
transform="rotate(11.86311,-35.469528,138.62496)" /><path
sodipodi:type="star"
style="fill:#ffcc00;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
id="path15420"
inkscape:flatsided="false"
sodipodi:sides="5"
sodipodi:cx="29.340046"
sodipodi:cy="46.625675"
sodipodi:r1="4.2978187"
sodipodi:r2="2.1489098"
sodipodi:arg1="0.47181668"
sodipodi:arg2="1.1001352"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 33.168302,48.579056 -2.853778,-0.03813 -1.649258,2.32926 -0.845606,-2.725885 -2.724907,-0.848757 2.331164,-1.646565 -0.03483,-2.85382 2.286346,1.708253 2.703382,-0.915002 -0.918125,2.702323 z"
inkscape:transform-center-x="0.34606094"
inkscape:transform-center-y="-0.06710565"
transform="rotate(11.86311,-35.469528,138.62496)" /><path
sodipodi:type="star"
style="fill:#ffcc00;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
id="path15422"
inkscape:flatsided="false"
sodipodi:sides="5"
sodipodi:cx="105.60214"
sodipodi:cy="62.81638"
sodipodi:r1="2.3678966"
sodipodi:r2="1.1839486"
sodipodi:arg1="0.48083515"
sodipodi:arg2="1.1091537"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 107.70154,63.911579 -1.57204,-0.03518 -0.9202,1.275066 -0.45233,-1.505976 -1.49702,-0.481145 1.29249,-0.89556 -0.005,-1.57243 1.25113,0.952488 1.49392,-0.49067 -0.51925,1.484231 z"
inkscape:transform-center-x="0.18428571"
inkscape:transform-center-y="-0.043561917"
transform="rotate(11.86311,-35.469528,138.62496)" /><path
sodipodi:type="star"
style="fill:#ffcc00;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
id="path15414-5"
inkscape:flatsided="false"
sodipodi:sides="5"
sodipodi:cx="117.51744"
sodipodi:cy="29.413408"
sodipodi:r1="11.625795"
sodipodi:r2="5.8128977"
sodipodi:arg1="-0.43776009"
sodipodi:arg2="0.19055844"
inkscape:rounded="0"
inkscape:randomized="0"
d="m 128.04696,24.485096 -4.82184,6.029317 2.23323,7.390235 -7.22426,-2.722688 -6.33842,4.407635 0.35701,-7.712031 -6.15059,-4.666166 7.4449,-2.043609 2.53715,-7.291484 4.24419,6.44901 z"
inkscape:transform-center-x="-0.094976792"
inkscape:transform-center-y="-0.26055941"
transform="matrix(0.31039809,0.06520244,-0.06520244,0.31039809,0.09441247,63.170233)" /><path
style="fill:#ffffff;fill-opacity:0.147971;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 49.931372,110.52643 c -4.164658,0.57437 -11.397846,8.11038 -17.096209,12.16736 2.641961,4.65789 5.268944,5.61059 7.896367,6.67201 0.698573,-4.82863 3.070763,-10.68288 9.199842,-18.83937 z"
id="path1154"
sodipodi:nodetypes="cccc" /><path
style="fill:#000000;fill-opacity:0.124582;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 74.850969,128.37443 1.67781,14.87299 c 0.555976,-0.0887 0.798905,-0.36021 1.00794,-0.65158 -0.555596,-5.28217 -1.141753,-10.59146 -0.941037,-15.20195 -0.386159,0.44667 -0.949792,0.78452 -1.744713,0.98054 z"
id="path1156"
sodipodi:nodetypes="ccccc" /><path
style="fill:#ffffff;fill-opacity:0.355131;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 67.779789,128.47502 1.511355,13.66693 c -0.147003,0.28974 -0.72278,0.31653 -1.265416,0.36367 l -1.305133,-13.59823 c 0.606151,-0.0574 0.913932,-0.20785 1.061531,-0.40815"
id="path1158"
sodipodi:nodetypes="ccccc" /><path
style="fill:#000000;fill-opacity:0.391885;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 114.37266,178.32121 c 0,0 3.30143,3.56816 9.722,3.14179 6.42058,-0.42637 9.65822,-5.25905 11.32829,-9.07146 -7.10641,6.30325 -14.07144,7.76835 -21.05029,5.92967 z"
id="path1160"
sodipodi:nodetypes="czcc" /></g></svg>

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 237 KiB

View File

@ -43,6 +43,10 @@ const update_datetimes = (event) => {
start_day.oldvalue = start_day.value; start_day.oldvalue = start_day.value;
} }
else {
new_date = new Date(start_day.value);
end_day.value = formatDate(new_date);
}
} }
else { else {
if (end_day.value && end_time.value && start_day.value) { if (end_day.value && end_time.value && start_day.value) {

View File

@ -34,11 +34,17 @@ const openModal = (modal, back=true) => {
} }
setTimeout(function() { setTimeout(function() {
visibleModal = modal; visibleModal = modal;
}, 500);
console.log("ici");
const mask = visibleModal.querySelector(".h-mask");
mask.classList.add("visible");
}, 350);
}; };
const hideModal = (modal) => { const hideModal = (modal) => {
if (modal != null) { if (modal != null) {
const mask = visibleModal.querySelector(".h-mask");
mask.classList.remove("visible");
visibleModal = null; visibleModal = null;
document.documentElement.style.removeProperty("--scrollbar-width"); document.documentElement.style.removeProperty("--scrollbar-width");
modal.removeAttribute("open"); 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 (<dialog>)
--modal-overlay-backdrop-filter: blur(0.05rem); --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%; font-size: 80%;
} }
} }
@ -198,8 +201,18 @@ details[role="list"] summary + ul li.selected>a:hover {
@extend .outline; @extend .outline;
font-size: 90%; font-size: 90%;
padding: 0.15em 0.4em 0.3em 0.4em; padding: 0.15em 0.4em 0.3em 0.4em;
text-align: left;
} }
.suggestions .small-cat {
overflow: visible;
}
.small-location {
@extend .small-cat;
border-color: var(--contrast);
color: var(--contrast);
}
.circ-cat.circ-large { .circ-cat.circ-large {
height: 2.6em; height: 2.6em;
@ -267,6 +280,12 @@ svg {
} }
} }
@media only screen and (min-width: 600px) {
.details-entete {
padding-left: 28%;
}
}
.ephemeris-hour { .ephemeris-hour {
@extend .ephemeris; @extend .ephemeris;
padding: 1.5em 0.1em; padding: 1.5em 0.1em;
@ -313,6 +332,7 @@ footer [data-tooltip] {
scroll-behavior: smooth; scroll-behavior: smooth;
transition-duration: 200ms; transition-duration: 200ms;
.cat { .cat {
margin-right: 0; margin-right: 0;
} }
@ -549,6 +569,12 @@ article#filters {
font-size: 100%; font-size: 100%;
} }
} }
.helptext {
display: block;
margin-top: 0;
}
header .remarque { header .remarque {
font-style: italic; font-style: italic;
} }
@ -567,6 +593,8 @@ header .remarque {
.slide-buttons { .slide-buttons {
float: right; float: right;
max-width: 30em;
text-align: right;
} }
.left-buttons { .left-buttons {
display: inline-block; display: inline-block;
@ -863,6 +891,7 @@ nav>div {
} }
.badge.error { .badge.error {
background: #b71c1c; background: #b71c1c;
border-color: #cb1b1b; border-color: #cb1b1b;
@ -874,6 +903,25 @@ nav>div {
color: var(--secondary-inverse); color: var(--secondary-inverse);
} }
.tw-badge {
background: black;
border-color: black;
color: #f2f207;
font-weight: bold;
padding: 0.2em 0.4em;
font-size: 60%;
opacity: 80%;
}
.tw-prefix {
background: black;
color: #f2f207;
height: 2.1em;
margin: -0.5em 0 -0.5em -0.5em;
padding: 0.3em;
display: inline-block;
}
form [role="button"], form button { form [role="button"], form button {
margin: var(--spacing) 0 var(--spacing) 0; margin: var(--spacing) 0 var(--spacing) 0;
} }
@ -886,6 +934,10 @@ form .buttons [role="button"] {
margin: 2em auto; margin: 2em auto;
} }
.min-y-grid {
min-height: 4em;
}
@media (min-width: 992px) { @media (min-width: 992px) {
.grid.two-columns { .grid.two-columns {
grid-column-gap: var(--nav-element-spacing-vertical); grid-column-gap: var(--nav-element-spacing-vertical);
@ -930,7 +982,7 @@ aside nav.paragraph li a, aside .no-breakline li a {
} }
/* mise en forme pour les récurrences */ /* mise en forme pour les récurrences */
article form p .recurrence-widget { article form div .recurrence-widget {
width: 100%; width: 100%;
border: 0; border: 0;
@ -1110,6 +1162,17 @@ table .buttons {
} }
} }
.image-flottante {
width: 30%;
float: right;
margin: 0 1em;
img {
width: 100%;
margin: -4em 0 0 0;
}
}
img.preview { img.preview {
height: 100px; height: 100px;
} }
@ -1252,15 +1315,31 @@ img.preview {
} }
} }
.choices__list--dropdown { .choices__list--dropdown {
display: none; font-size: 80%;
display: block;
will-change: display; will-change: display;
background: var(--background-color);
padding: 0.4em;
border-radius: 0.2em;
box-shadow: 0 -1px 0 rgba(115, 130, 140, 0.2);
position: relative; position: relative;
max-height: 300px; max-height: 300px;
overflow: auto; overflow: auto;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
will-change: scroll-position; will-change: scroll-position;
} }
.choices__list--dropdown.is-active { .choices__list--dropdown .choices__group,
.choices__list--dropdown .choices__item {
display: none;
}
.choices__list--dropdown .choices__item.visible,
.choices__list--dropdown.is-active .choices__item {
display: inline-block;
}
.choices__list--dropdown .choices__group.visible,
.choices__list--dropdown.is-active .choices__group {
display: block; display: block;
} }
} }
@ -1268,17 +1347,15 @@ img.preview {
article { .stick-bottom {
.stick-bottom { background: var(--card-sectionning-background-color);
background: var(--card-sectionning-background-color); margin-right: calc(var(--block-spacing-horizontal) * -1);
margin-right: calc(var(--block-spacing-horizontal) * -1); margin-left: calc(var(--block-spacing-horizontal) * -1);
margin-left: calc(var(--block-spacing-horizontal) * -1); padding: calc(var(--block-spacing-vertical) * 0.66) var(--block-spacing-horizontal);
padding: calc(var(--block-spacing-vertical) * 0.66) var(--block-spacing-horizontal); margin-bottom: calc(var(--block-spacing-vertical) * -1);
margin-bottom: calc(var(--block-spacing-vertical) * -1); position: sticky;
position: sticky; bottom: 0;
bottom: 0; box-shadow: 0 -1px 0 rgba(115, 130, 140, 0.2);
box-shadow: 0 -1px 0 rgba(115, 130, 140, 0.2);
}
} }
/*#menu-rechercher, #menu-ajouter, #menu-configurer { /*#menu-rechercher, #menu-ajouter, #menu-configurer {
@ -1320,7 +1397,11 @@ article {
} }
.a-venir, .place, .tag, .tag-descriptions { .a-venir article.single-event {
scroll-margin-top: 7em;
}
.a-venir, .place, .tag, .tag-descriptions, .organisation {
article#filters { article#filters {
margin: 2em 0; margin: 2em 0;
} }
@ -1357,6 +1438,17 @@ article {
} }
} }
.header-complement {
float: none;
}
@media only screen and (min-width: 992px) {
.header-complement {
float: left;
clear: both;
}
}
form.messages div, form.moderation-events { form.messages div, form.moderation-events {
@media only screen and (min-width: 992px) { @media only screen and (min-width: 992px) {
display: grid; display: grid;
@ -1371,3 +1463,287 @@ form.messages div, form.moderation-events {
float: left; float: left;
} }
} }
.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;
}
.contenu #event_form {
div#group_start, div#group_end {
grid-column: auto;
.body_group {
display: grid;
grid-gap: 10px;
grid-template-columns: repeat(2, 1fr);
}
}
}
/* group form */
@media only screen and (min-width: 992px) {
.contenu #event_form form {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-gap: 10px;
>div {
grid-column: 1 / 3;
}
div#group_meta, div#group_meta-admin {
.body_group {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-gap: 10px;
div {
grid-column: auto;
}
}
}
div#group_meta {
.body_group {
display: grid;
grid-template-columns: repeat(2, 1fr);
}
}
}
}
.maskable_group .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,8 +17,52 @@
{% block content %} {% block content %}
<div class="grid two-columns"> <div class="grid two-columns">
<div id="contenu-principal"> <div id="contenu-principal">
<div>
<article>
<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> <header>
<h2>Activité des derniers jours</h2> <h2>Activité des derniers jours</h2>
</header> </header>
@ -32,6 +76,7 @@
{% include "agenda_culturel/rimports-info-inc.html" with all=1 %}</p> {% include "agenda_culturel/rimports-info-inc.html" with all=1 %}</p>
</article> </article>
</div>
<article> <article>
<header> <header>

View File

@ -47,15 +47,21 @@
<p> <p>
{% picto_from_name "calendar" %} {% picto_from_name "calendar" %}
{% if event.end_day and event.end_day != event.start_day %}du{% else %}le{% endif %} {% if event.end_day and event.end_day != event.start_day %}du{% else %}le{% endif %}
{% include "agenda_culturel/date-times-inc.html" with event=event %} {{ event.start_day|date|frdate }}
</p> {% if event.start_time %} {% if not event.end_day or event.end_day == event.start_day %}{% if event.end_time %}de{% else %}à{% endif %}{% endif %}
{{ event.start_time }}
{% endif %}
{% if event.end_day and event.end_day != event.start_day %}
au {% if event.end_day and event.end_day != event.start_day %}{{ event.end_day|date|frdate }}{% endif %}
{% 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>
</header> </header>
<div class="body-fixed">{{ event.description |linebreaks }}</div> <div class="body-fixed">{{ event.description |linebreaks }}</div>
<p> <p>
{% for tag in event.tags %} {% for tag in event.sorted_tags %}
{{ tag | tag_button }} {{ tag | tag_button }}
{% endfor %} {% endfor %}
</p> </p>

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,10 +0,0 @@
{% load utils_extra %}
{{ event.start_day|date|frdate }}
{% if event.start_time %} {% if not event.end_day or event.end_day == event.start_day %}{% if event.end_time %}de{% else %}à{% endif %}{% endif %}
{{ event.start_time }}
{% endif %}
{% if event.end_day and event.end_day != event.start_day %}
au {% if event.end_day and event.end_day != event.start_day %}{{ event.end_day|date|frdate }}{% endif %}
{% 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 %}

View File

@ -1,82 +0,0 @@
{% load cat_extra %}
{% load event_extra %}
{% load utils_extra %}
{% load static %}
{% with day.date|date:"Y-m-d" as daytag %}
{% with "date-"|add:daytag as daytag %}
<article class="day{{ day|calendar_classes:fixed_style }}" id="{{ daytag }}">
{% if day.is_today %}
<script>
document.addEventListener("DOMContentLoaded", function(e) {
agenda = document.getElementById("calendar");
todayArticle = document.getElementById("today").parentElement;
left = todayArticle.offsetLeft;
top = todayArticle.offsetTop;
agenda.scrollTo({top: top, left: left});
});
</script>
{% endif %}
<header{% if day.is_today %} id="today"{% endif %}>
<{% if headers != "" %}{{ headers }}{% else %}h2{% endif %}><a href="{{ day.date | url_day }}?{{ filter.get_url }}">{{ day.date | date:"l j" }}</a></{% if headers != "" %}{{ headers }}{% else %}h2{% endif %}>
</header>
{% if day.events %}
{% if resume == 1 %}
<ul>
{% for category, events in day.events_by_category_ordered %}
<li>{{ events.0.category | circle_cat }}
<a href="{{ day.date | url_day }}?{{ filter.get_url|add_url_category:events.0.category }}" data-target="{{ daytag }}-category-{{ events.0.category.pk }}" onClick="toggleModal(event)">{{ events | length }} {{ category }}</a></li>
<dialog id="{{ daytag }}-category-{{ events.0.category.pk }}">
<article>
<header>
<a href="#{{ daytag }}-category-{{ events.0.category.pk }}"
aria-label="Fermer"
class="close"
data-target="{{ daytag }}-category-{{ events.0.category.pk }}"
onClick="toggleModal(event)"></a>
<h3>{{ events.0.category | small_cat }} du {{ day.date | date:"l j F" }}</h3>
</header>
<ul>
{% for event in events %}
<li>
{% 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 }}</a>
</li>
{% endfor %}
</ul>
<footer>
<div class="buttons">
<a href="{{ day.date | url_day }}?{{ filter.get_url }}" role="button">Voir la journée <svg width="1em" height="1em" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<use href="{% static 'images/feather-sprite.svg' %}#chevron-right" />
</svg></a>
</div>
</footer>
</article>
</dialog>
{% endfor %}
<ul>
{% else %}
<ul>
{% for event in day.events %}
<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>
<dialog id="event-{{ event.id }}">
{% include "agenda_culturel/single-event/event-modal-inc.html" with event=event filter=filter %}
</dialog>
</li>
{% endfor %}
</ul>
{% endif %}
{% endif %}
</ul>
</article>
{% endwith %}
{% endwith %}

View File

@ -1,13 +1,18 @@
{% load utils_extra %} {% load utils_extra %}
<!-- a href="{% url 'moderate_event' event.id %}" role="button">modérer {% picto_from_name "edit" %}</a--> {% if event.moderated_date %}
<a href="{% url 'moderate_event' event.id %}" role="button">Modérer de nouveau {% picto_from_name "check-square" %}</a>
{% else %}
<a href="{% url 'moderate_event' event.id %}" role="button">Modérer {% picto_from_name "check-square" %}</a>
{% endif %}
{% if event.pure_import %} {% if event.pure_import %}
{% with event.get_local_version as local %} {% with event.get_local_version as local %}
{% if local %} {% if local %}
<a href="{{ local.get_absolute_url }}" role="button">voir la version locale {% picto_from_name "eye" %}</a> <a href="{{ local.get_absolute_url }}" role="button">voir la version locale {% picto_from_name "eye" %}</a>
{% else %} {% 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 %} {% endif %}
{% endwith %} {% endwith %}
{% else %} {% else %}
@ -36,3 +41,6 @@
<a href="{% url 'delete_event' event.id %}" role="button">supprimer définitivement {% picto_from_name "x-circle" %}</a> <a href="{% url 'delete_event' event.id %}" role="button">supprimer définitivement {% picto_from_name "x-circle" %}</a>
{% endif %} {% endif %}
{% if with_clone %}
<a href="{% url 'simple_clone_edit' event.id %}" role="button">Dupliquer {% picto_from_name "copy" %}</a>
{% endif %}

View File

@ -1,13 +1,13 @@
{% if user.is_authenticated %} {% 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 %} {% 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 %} {% endif %}
{% if event.imported_date %} {% 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 %} {% endif %}
{% if event.moderated_date %} {% 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 %} {% endif %}
{% if event.pure_import %} {% if event.pure_import %}
<strong>version importée</strong> <strong>version importée</strong>

View File

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

View File

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

View File

@ -14,16 +14,18 @@ Mettre l'événement {{ event.title }} à la corbeille
{% block content %} {% block content %}
<h1>
{% if status == "published" %}
Publier l'événement {{ event.title }}
{% elif status == "draft" %}
Déplacer l'événement {{ event.title }} dans les brouillons
{% elif status == "trash" %}
Mettre l'événement {{ event.title }} à la corbeille
{% endif %}
</h1>
<article> <article>
<header>
<h1>
{% if status == "published" %}
Publier l'événement {{ event.title }}
{% elif status == "draft" %}
Déplacer l'événement {{ event.title }} dans les brouillons
{% elif status == "trash" %}
Mettre l'événement {{ event.title }} à la corbeille
{% endif %}
</h1>
</header>
<form method="post">{% csrf_token %} <form method="post">{% csrf_token %}
<p class="large">Êtes-vous sûr·e de vouloir <p class="large">Êtes-vous sûr·e de vouloir
{% if status == "published" %} {% if status == "published" %}

View File

@ -4,7 +4,11 @@
{% block title %}{% block og_title %} {% block title %}{% block og_title %}
{% if object %}{% if form.is_clone_from_url %} {% if object %}{% if form.is_clone_from_url %}
Création d'une copie de {% else %} Création d'une copie locale de {% else %}
Édition de l'événement {{ object.title }} ({{ object.start_day }})
{% endif %}
{% if form.is_simple_clone_from_url %}
Duplication de {% else %}
Édition de l'événement {{ object.title }} ({{ object.start_day }}) Édition de l'événement {{ object.title }} ({{ object.start_day }})
{% endif %} {% endif %}
{% else %} {% else %}
@ -44,13 +48,21 @@ Création d'une copie de {% else %}
{% block content %} {% block content %}
{% load static_content_extra %} {% load static_content_extra %}
<article> <article id="event_form">
<header> <header>
{% if object %} {% if object %}
<h1>{% if form.is_clone_from_url %} <h1>{% if form.is_clone_from_url %}
Création d'une copie de {% else %} Création d'une copie locale de {% else %}
Édition de l'événement{% endif %} {{ object.title }} ({{ object.start_day }})</h1> {% if form.is_simple_clone_from_url %}
Duplication de {% else %}
Édition de l'événement{% endif %}{% endif %} {{ object.title }} ({{ object.start_day }})</h1>
{% if form.is_simple_clone_from_url %}
<p>Vous allez créer une copie de l'événement original qui n'aura plus aucun lien avec ce dernier. Cette possibilité
est généralement utilisée pour créer un événement semblable à une date différente, par exemple.
</p>
{% endif %}
{% else %} {% else %}
{% if from_import %} {% if from_import %}
<h1>Ajuster l'événement importé</h1> <h1>Ajuster l'événement importé</h1>
{% else %} {% else %}
@ -60,16 +72,16 @@ Création d'une copie de {% else %}
{% endif %} {% endif %}
</header> </header>
<div id="container"></div>
{% if form.is_clone_from_url %} {% if form.is_clone_from_url or form.is_simple_clone_from_url %}
{% url "add_event_details" as urlparam %} {% url "add_event_details" as urlparam %}
{% endif %} {% endif %}
<form method="post" action="{{ urlparam }}" enctype="multipart/form-data">{% csrf_token %} <form method="post" action="{{ urlparam }}" enctype="multipart/form-data">{% csrf_token %}
{{ form.media }} {{ form.media }}
{{ form.as_p }} {{ form }}
<div class="grid buttons"> <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> <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> </div>
</form> </form>
@ -82,5 +94,44 @@ Création d'une copie de {% else %}
const element = document.querySelector('#id_exact_location'); const element = document.querySelector('#id_exact_location');
const choices = new Choices(element); const choices = new Choices(element);
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_tags');
const choices_tags = new Choices(tags,
{
placeholderValue: 'Sélectionner les étiquettes à ajouter',
allowHTML: true,
delimiter: ',',
removeItemButton: true,
shouldSort: false,
callbackOnCreateTemplates: () => (show_firstgroup)
}
);
const organisers = document.querySelector('#id_organisers');
const choices_organisers = new Choices(organisers,
{
placeholderValue: 'Sélectionner les organisateurs ',
allowHTML: true,
delimiter: ',',
removeItemButton: true,
shouldSort: true,
});
</script> </script>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,131 @@
{% extends "agenda_culturel/page.html" %}
{% load static %}
{% load utils_extra %}
{% load cat_extra %}
{% block title %}{% block og_title %}Modération de l'événement {{ object.title }}{% endblock %}{% endblock %}
{% block ajouter-bouton %}{% block ajouter-menu %}{% endblock %}{% endblock %}
{% block entete_header %}
<script src="{% static 'choicejs/choices.min.js' %}"></script>
{% css_categories %}
<script src="/static/admin/js/vendor/jquery/jquery.js"></script>
<script src="/static/admin/js/jquery.init.js"></script>
<link href="{% static 'css/django_better_admin_arrayfield.min.css' %}" type="text/css" media="all" rel="stylesheet">
<script src="{% static 'js/django_better_admin_arrayfield.min.js' %}"></script>
{% endblock %}
{% block content %}
{% load static_content_extra %}
<article>
<header>
<h1>Modération de l'événement {{ object.title }}</h1>
<p class="remarque">En utilisant cet outil de modération, l'événement ne sera pas modifié{% if event.pure_import %}
et restera considéré comme une version importée, peut importe les modifications d'étiquette,
de catégorie ou de lieu que vous lui appliquerez{% endif %}.
</p>
</header>
<form method="post" enctype="multipart/form-data" id="moderate-form">{% csrf_token %}
<div class="grid moderate-preview">
<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>
<h2>Modification des méta-informations</h2>
{% if 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>
{% endif %}
</header>
{{ form.media }}
{{ form.as_p }}
</article>
</div>
<div class="grid buttons">
{% if pred %}
<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">
<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>
<script>
const element = document.querySelector('#id_exact_location');
const choices = new Choices(element);
show_firstgroup = {
choice(classes, choice) {
const i = Choices.defaults.templates.choice.call(this, classes, choice);
i.classList.add("visible");
return i;
},
choiceGroup(classes, group) {
const g = Choices.defaults.templates.choiceGroup.call(this, classes, group);
g.classList.add("visible");
return g;
}
};
const tags = document.querySelector('#id_tags');
const choices_tags = new Choices(tags,
{
placeholderValue: 'Sélectionner les étiquettes à ajouter',
allowHTML: true,
delimiter: ',',
removeItemButton: true,
shouldSort: false,
callbackOnCreateTemplates: () => (show_firstgroup)
}
);
const organisers = document.querySelector('#id_organisers');
const choices_organisers = new Choices(organisers,
{
placeholderValue: 'Sélectionner les organisateurs ',
allowHTML: true,
delimiter: ',',
removeItemButton: true,
shouldSort: true,
});
</script>
{% endblock %}

View File

@ -1,39 +0,0 @@
{% extends "agenda_culturel/event_form.html" %}
{% load static %}
{% load cat_extra %}
{% block entete_header %}
{% css_categories %}
{% endblock %}
{% block title %}{% block og_title %}Modérer {{ object.title }}{% endblock %}{% endblock %}
{% block content %}
{% load static_content_extra %}
<article>
<header>
<h1>Modération de l'événement {{ object.title }} ({{ object.start_day }})</h1>
</header>
{% include "agenda_culturel/single-event/event-single-inc.html" with event=object noedit=1 %}
<div id="container"></div>
<form method="post">{% csrf_token %}
{{ form.media }}
{{ form.as_p }}
<div class="grid buttons">
<a href="{% if request.META.HTTP_REFERER %}{{ request.META.HTTP_REFERER }}{% else %}{{ object.get_absolute_url }}{% endif %}" role="button" class="secondary">Annuler</a>
<input type="submit" value="Enregistrer">
</div>
</form>
{% include "agenda_culturel/event-info-inc.html" %}
</article>
{% endblock %}

View File

@ -0,0 +1,40 @@
{% extends "agenda_culturel/page.html" %}
{% load utils_extra %}
{% load static %}
{% block title %}{% block og_title %}Erreur pendant la recherche d'événement suivant{% endblock %}{% endblock %}
{% block entete_header %}
<script src="{% static 'js/modal.js' %}"></script>
{% endblock %}
{% block fluid %}{% endblock %}
{% block content %}
<article>
<header>
<h1>Erreur pendant la recherche d'événement suivant</h1>
</header>
{% if object %}
<p>Nous n'avons pas été capable de trouver un événement à modérer après
l'événement <a href="{{ object.get_absolute_url }}">{{ object.title }}</a>. Peut-être est-ce le dernier événement
avant la fin du monde&nbsp;?
</p>
{% else %}
<p>Nous n'avons pas été capable de trouver l'événement #{{ pk }} sur l'agenda.
Il a certainement été supprimé depuis que vous l'avez consulté.
</p>
{% endif %}
<footer>
<div class="grid buttons">
<a href="{% url 'recent' %}" role="button">Retour aux événements récents</a>
</div>
</footer>
</article>
{% endblock %}

View File

@ -2,6 +2,7 @@
{% load tag_extra %} {% load tag_extra %}
{% load utils_extra %} {% load utils_extra %}
{% load static %} {% load static %}
{% load locations_extra %}
{% if noarticle == 0 %} {% if noarticle == 0 %}
<article id="filters"> <article id="filters">
@ -13,6 +14,7 @@
<div class="filtres"> <div class="filtres">
<details {% if filter.is_active %}class="active"{% endif %} {% if filter.form and filter.form.errors %}open=""{% endif %}> <details {% if filter.is_active %}class="active"{% endif %} {% if filter.form and filter.form.errors %}open=""{% endif %}>
{{ filter.form.non_field_errors }}
<summary role="button" class="outline secondary"> <summary role="button" class="outline secondary">
{% if filter.is_active %} {% if filter.is_active %}
<strong>Filtres&nbsp;: </strong> <strong>Filtres&nbsp;: </strong>
@ -20,7 +22,7 @@
{{ t | tag_button:"true" }} {{ t | tag_button:"true" }}
{% endfor %} {% endfor %}
{% for t in filter.get_exclude_tags %} {% for t in filter.get_exclude_tags %}
{{ t | tag_button_strike }} {{ t | tag_button_strike:"true" }}
{% endfor %} {% endfor %}
{% for s in filter.get_status_names %} {% for s in filter.get_status_names %}
{{ s }} {{ s }}
@ -39,7 +41,6 @@
<form method="get" class="form django-form main-filter"> <form method="get" class="form django-form main-filter">
<div class="grid two-columns-equal"> <div class="grid two-columns-equal">
<article> <article>
<header>Localisation</header>
{% for f in filter.form.visible_fields %} {% for f in filter.form.visible_fields %}
{% if f.id_for_label and f.id_for_label in "id_position,id_radius" %} {% if f.id_for_label and f.id_for_label in "id_position,id_radius" %}
<div> <div>
@ -51,7 +52,6 @@
{% endfor %} {% endfor %}
</article> </article>
<article> <article>
<header>Autres filtres</header>
{% for f in filter.form.visible_fields %} {% for f in filter.form.visible_fields %}
{% if not f.id_for_label or not f.id_for_label in "id_position,id_radius" %} {% if not f.id_for_label or not f.id_for_label in "id_position,id_radius" %}
<p> <p>
@ -66,25 +66,50 @@
<button type="submit">Appliquer le filtre</button> <button type="submit">Appliquer le filtre</button>
</form> </form>
</details> </details>
<div class="suggested-tags"> <div class="suggestions">
{% show_suggested_tags filter=filter %} Suggestion&nbsp;:
{% show_suggested_positions filter=filter %}
{% show_suggested_tags filter=filter %}
</div> </div>
</div> </div>
<div class="clear"></div> <div class="clear"></div>
</div> </div>
<script src="{% static 'choicejs/choices.min.js' %}"></script> <script src="{% static 'choicejs/choices.min.js' %}"></script>
<script> <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 position = document.querySelector('#id_position'); const position = document.querySelector('#id_position');
const choices_position = new Choices(position); const choices_position = new Choices(position,
{
shouldSort: false,
}
);
const tags = document.querySelector('#id_tags'); const tags = document.querySelector('#id_tags');
const choices_tags = new Choices(tags, const choices_tags = new Choices(tags,
{ {
placeholderValue: 'Chercher les étiquettes à inclure', placeholderValue: 'Chercher les étiquettes à inclure',
allowHTML: true, allowHTML: true,
delimiter: ',', delimiter: ',',
editItems: true,
removeItemButton: true, removeItemButton: true,
} shouldSort: false,
callbackOnCreateTemplates: () => (show_firstgroup)
}
); );
const exclude_tags = document.querySelector('#id_exclude_tags'); const exclude_tags = document.querySelector('#id_exclude_tags');
const choices_exclude_tags = new Choices(exclude_tags, const choices_exclude_tags = new Choices(exclude_tags,
@ -92,15 +117,12 @@
placeholderValue: 'Chercher les étiquettes à exclure', placeholderValue: 'Chercher les étiquettes à exclure',
allowHTML: true, allowHTML: true,
delimiter: ',', delimiter: ',',
editItems: true,
removeItemButton: true, removeItemButton: true,
shouldSort: false,
callbackOnCreateTemplates: () => (show_firstgroup)
} }
); );
document.querySelector("#id_position .choices__inner").classList.add("contrast");
document.querySelector("#id_position .choices__inner").classList.add("outline");
document.querySelector("#id_position .choices__inner").setAttribute("role", "button");
</script> </script>
{% if noarticle == 0 %} {% if noarticle == 0 %}

View File

@ -0,0 +1,67 @@
{{ errors }}
{% if errors and not fields %}
<p>{% for field in hidden_fields %}{{ field }}{% endfor %}</p>
{% endif %}
{% 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 }}"
{% 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 %}
<div id="error_{{ field.auto_id }}">
{{ field.errors }}
</div>
{% endfor %}
</div>
<div class="body_group">
{% for field in fields %}
<div id="div_{{ field.auto_id }}">
<div{% with classes=field.css_classes %}{% if classes %} class="{{ classes }}"{% endif %}{% endwith %}>
{% if field.label %}{{ field.label_tag }}{% endif %}
{{ field }}
{% if field.help_text %}
<span class="helptext"{% if field.auto_id %} id="{{ field.auto_id }}_helptext"{% endif %}>{{ field.help_text|safe }}</span>
{% endif %}
</div>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
{% if not errors %}
{% for field in hidden_fields %}{{ field }}{% endfor %}
{% endif %}
<script>
const maskables = document.querySelectorAll('.maskable_group');
maskables.forEach(function (item) {
if (!item.checked) {
item.querySelector('.body_group').classList.add('closed');
}
console.log('item ' + item);
item.querySelector('.toggle_body').addEventListener('change', (event) => {
if (event.currentTarget.checked) {
item.querySelector('.body_group').classList.remove('closed');
} else {
item.querySelector('.body_group').classList.add('closed');
}
})
window.addEventListener('load', function(event) {
if (item.querySelector('.toggle_body').checked) {
item.querySelector('.body_group').classList.remove('closed');
}
else {
item.querySelector('.body_group').classList.add('closed');
}
});
})
</script>

View File

@ -31,4 +31,35 @@
tout de même ajouter l'événement en le décrivant sur la page <a href="{% url 'add_event_details' %}" >d'ajout d'événement</a>.</p> tout de même ajouter l'événement en le décrivant sur la page <a href="{% url 'add_event_details' %}" >d'ajout d'événement</a>.</p>
</article> </article>
<script src="{% static 'choicejs/choices.min.js' %}"></script>
<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_tags');
const choices_tags = new Choices(tags,
{
placeholderValue: 'Sélectionner les étiquettes à ajouter',
allowHTML: true,
delimiter: ',',
removeItemButton: true,
shouldSort: false,
callbackOnCreateTemplates: () => (show_firstgroup)
}
);
</script>
{% endblock %} {% endblock %}

View File

@ -18,27 +18,62 @@
{% url 'event_import_urls' as local_url %} {% url 'event_import_urls' as local_url %}
{% include "agenda_culturel/static_content.html" with name="import_set" url_path=local_url %} {% include "agenda_culturel/static_content.html" with name="import_set" url_path=local_url %}
</header> </header>
<p>Si l'import automatique ne marche pas, ou si l'événement n'est pas en ligne, vous pouvez
tout de même ajouter l'événement en le décrivant sur la page <a href="{% url 'add_event_details' %}" >d'ajout d'événement</a>.</p>
</article>
<form method="post"> <form method="post">
{{ formset.management_form }} {{ formset.management_form }}
{% csrf_token %} {% csrf_token %}
{% for form in formset %} {% for form in formset %}
<h2>Nouvel événement #{{ forloop.counter }}</h2> <article>
<header>
<h2>Nouvel événement #{{ forloop.counter }}</h2>
</header>
{{ form.as_p }} {{ form.as_p }}
</article>
{% endfor %} {% endfor %}
<div class="stick-bottom"> <div class="stick-bottom">
<input type="submit"value="Lancer l'import" id="import-button"> <input type="submit"value="Lancer l'import" id="import-button">
</div> </div>
</form> </form>
<footer>
<p>Si l'import automatique ne marche pas, ou si l'événement n'est pas en ligne, vous pouvez <script src="{% static 'choicejs/choices.min.js' %}"></script>
tout de même ajouter l'événement en le décrivant sur la page <a href="{% url 'add_event_details' %}" >d'ajout d'événement</a>.</p> <script>
</footer> show_firstgroup = {
</article> choice(classes, choice) {
<dialog id="modal-import"> const i = Choices.defaults.templates.choice.call(this, classes, choice);
<article aria-busy="true"> if (this.first_group !== null && choice.groupId == this.first_group)
Veuillez patienter, lien en cours d'importation... i.classList.add("visible");
</article> return i;
</dialog> },
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.querySelectorAll("select[name$='-tags']");
Array.from(tags).forEach((element,index) => {
console.log(element);
const choices_tags = new Choices(element,
{
placeholderValue: 'Sélectionner les étiquettes à ajouter',
allowHTML: true,
delimiter: ',',
removeItemButton: true,
shouldSort: false,
callbackOnCreateTemplates: () => (show_firstgroup)
}
);
});
</script>
{% endblock %} {% endblock %}

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

@ -24,17 +24,18 @@
<article> <article>
<header> <header>
<div class="slide-buttons"> <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> <a href="{% url 'delete_message' object.id %}" role="button" data-tooltip="Supprimer le message">Supprimer {% picto_from_name "trash-2" %}</a>
</div> </div>
<h1>Modération du message «&nbsp;{{ object.subject }}&nbsp;»</h1> <h1>Modération du message «&nbsp;{{ object.subject }}&nbsp;»</h1>
<ul> <ul>
<li>Date&nbsp;: {{ object.date }}</li> <li>Date&nbsp;: {{ object.date.date }} à {{ object.date.time }}</li>
<li>Auteur&nbsp;: {{ object.name }} <a href="mailto:{{ object.email }}">{{ object.email }}</a></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 %}
</ul> </ul>
</header> </header>
<div> <div>
{{ object.message }} {{ object.message | safe }}
</div> </div>
</article> </article>
@ -46,7 +47,7 @@
</article> </article>
</div> </div>
{% include "agenda_culturel/side-nav.html" with current="contactmessages" %} {% include "agenda_culturel/side-nav.html" with current="messages" %}
</div> </div>
{% endblock %} {% endblock %}

View File

@ -26,7 +26,8 @@
<h1>Derniers messages de contact reçus</h1> <h1>Derniers messages de contact reçus</h1>
<form method="get" class="form django-form recent messages"> <form method="get" class="form django-form recent messages">
{{ filter.form.as_div }}<br /> {{ filter.form.as_div }}<br />
<button type="submit">Filtrer</button><br /> <button type="submit">Filtrer</button>
</form>
</header> </header>
<table role="grid"> <table role="grid">
@ -35,6 +36,7 @@
<th>Date</th> <th>Date</th>
<th>Sujet</th> <th>Sujet</th>
<th>Auteur</th> <th>Auteur</th>
<th>Événement</th>
<th>Fermé</th> <th>Fermé</th>
<th>Spam</th> <th>Spam</th>
</tr> </tr>
@ -43,8 +45,9 @@
{% for obj in paginator_filter %} {% for obj in paginator_filter %}
<tr> <tr>
<td>{{ obj.date }}</td> <td>{{ obj.date }}</td>
<td><a href="{% url 'contactmessage' obj.pk %}">{{ obj.subject }}</a></td> <td><a href="{% url 'message' obj.pk %}">{{ obj.subject }}</a></td>
<td>{{ obj.name }}</td> <td>{% if obj.user %}<em>{{ obj.user }}</em>{% else %}{{ obj.name }}{% 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.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> <td>{% if obj.spam %}{% picto_from_name "check-square" "spam" %}{% else %}{% picto_from_name "square" "non spam" %}{% endif %}</td>
</tr> </tr>
@ -56,7 +59,7 @@
</footer> </footer>
</article> </article>
{% include "agenda_culturel/side-nav.html" with current="contactmessages" %} {% include "agenda_culturel/side-nav.html" with current="messages" %}
</div> </div>
{% endblock %} {% endblock %}

View File

@ -1,20 +0,0 @@
{% extends "agenda_culturel/page-admin.html" %}
{% block title %}{% block og_title %}Supprimer la réponse #{{ object.pk }}{% endblock %}{% endblock %}
{% block fluid %}{% endblock %}
{% block content %}
<h1>Suppression de la réponse de modération {{ object.pk }}</h1>
<form method="post">{% csrf_token %}
<p>Êtes-vous sûr·e de vouloir supprimer la réponse de modération #{{object.pk}} «&nbsp;{{ object.answer }}&nbsp;» associée à la question «&nbsp;{{ object.question.question }}&nbsp;»&nbsp;?
</p>
{{ form }}
<div class="grid buttons">
<a href="{% if request.META.HTTP_REFERER %}{{ request.META.HTTP_REFERER }}{% else %}{{ object.get_absolute_url }}{% endif %}" role="button" class="secondary">Annuler</a>
<input type="submit" value="Confirmer">
</div>
</form>
{% endblock %}

View File

@ -1,31 +0,0 @@
{% extends "agenda_culturel/page-admin.html" %}
{% load static %}
{% block title %}{% block og_title %}{% if form.instance.pk %}Modification{% else %}Création{% endif %} d'une réponse de modération{% 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 content %}
<h1>{% if form.instance.pk %}Modification{% else %}Création{% endif %} d'une réponse de modération</h1>
<p>{% if form.instance.pk %}Modifier{% else %}Ajouter{% endif %} une réponse à la question « {{ question }} »</p>
<article>
<form method="post">{% csrf_token %}
{{ form.as_p }}
<div class="grid buttons">
<a href="{% if request.META.HTTP_REFERER %}{{ request.META.HTTP_REFERER }}{% else %}{% url 'view_mquestion' question.pk %}{% endif %}" role="button" class="secondary">Annuler</a>
<input type="submit" value="Envoyer">
</div>
</form>
</article>
{% endblock %}

View File

@ -1,20 +0,0 @@
{% extends "agenda_culturel/page-admin.html" %}
{% block title %}{% block og_title %}Supprimer la question #{{ object.pk }}{% endblock %}{% endblock %}
{% block fluid %}{% endblock %}
{% block content %}
<h1>Suppression de la question de modération {{ object.pk }}</h1>
<form method="post">{% csrf_token %}
<p>Êtes-vous sûr·e de vouloir supprimer la question de modération #{{object.pk}} «&nbsp;{{ object.question }}&nbsp;» ainsi que les réponses associées&nbsp;?
</p>
{{ form }}
<div class="grid buttons">
<a href="{% if request.META.HTTP_REFERER %}{{ request.META.HTTP_REFERER }}{% else %}{{ object.get_absolute_url }}{% endif %}" role="button" class="secondary">Annuler</a>
<input type="submit" value="Confirmer">
</div>
</form>
{% endblock %}

View File

@ -1,71 +0,0 @@
{% extends "agenda_culturel/page-admin.html" %}
{% block title %}{% block og_title %}Question de modération #{{ object.pk }}{% endblock %}{% endblock %}
{% load tag_extra %}
{% load utils_extra %}
{% load cat_extra %}
{% block entete_header %}
{% css_categories %}
{% 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">
<article>
<header>
<a href="{% url 'view_mquestions' %}" role="button">&lt; Retour</a>
<div class="slide-buttons">
<a href="{% url 'delete_mquestion' object.pk %}" role="button">Supprimer</a>
<a href="{% url 'edit_mquestion' object.pk %}" role="button">Modifier</a>
<a href="{% url 'add_manswer' object.pk %}" role="button">Ajouter une réponse {% picto_from_name "plus-circle" %}</a>
</div>
<h1>Question de modération #{{ object.pk }}</h1>
<p>{{ object.question }}</p>
</header>
{% if object.answers %}
{% for answer in object.answers.all %}
<article>
<div class="slide-buttons">
<a href="{% url 'edit_manswer' object.pk answer.pk %}" role="button">Modifier</a>
<a href="{% url 'delete_manswer' object.pk answer.pk %}" role="button">Supprimer</a>
</div>
<header>
<h4><strong>Réponse #{{ answer.pk }}&nbsp;:</strong> «&nbsp;{{ answer.answer }}&nbsp;»</h4>
{% if answer.adds_tags %}
<p>Cette réponse ajoute les étiquettes suivantes à l'événement&nbsp;:
{% for tag in answer.adds_tags %}
{{ tag | tag_button }}
{% endfor %}
</p>
{% else %}
<p><em>Cette réponse n'ajoute pas d'étiquette à l'événement.</em></p>
{% endif %}
{% if answer.removes_tags %}
<p>Cette réponse supprimer les étiquettes suivantes à l'événement&nbsp;:
{% for tag in answer.removes_tags %}
{{ tag | tag_button }}
{% endfor %}
</p>
{% else %}
<p><em>Cette réponse ne supprime pas d'étiquette à l'événement.</em></p>
{% endif %}
</header>
</article>
{% endfor %}
{% else %}
Il n'y a pas encore de réponse associée à cette question.
{% endif %}
</article>
{% include "agenda_culturel/side-nav.html" with current="moderationquestions" %}
</div>
{% endblock %}

View File

@ -1,23 +0,0 @@
{% extends "agenda_culturel/page-admin.html" %}
{% load static %}
{% block title %}{% block og_title %}
{% if form.instance.pk %}Modification{% else %}Création{% endif %} d'une question de modération
{% endblock %}{% endblock %}
{% block content %}
<h1>{% if form.instance.pk %}Modification{% else %}Création{% endif %} d'une question de modération</h1>
<article>
<form method="post">{% csrf_token %}
{{ form.as_p }}
<div class="grid buttons">
<a href="{% if request.META.HTTP_REFERER %}{{ request.META.HTTP_REFERER }}{% else %}{{ object.get_absolute_url }}{% endif %}" role="button" class="secondary">Annuler</a>
<input type="submit" value="Envoyer">
</div>
</form>
</article>
{% endblock %}

View File

@ -1,71 +0,0 @@
{% extends "agenda_culturel/page-admin.html" %}
{% block title %}{% block og_title %}Questions de modération{% endblock %}{% endblock %}
{% load utils_extra %}
{% load cat_extra %}
{% block entete_header %}
{% css_categories %}
{% 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">
<article>
<header>
<a class="slide-buttons" href="{% url 'add_mquestion'%}" role="button">Ajouter {% picto_from_name "plus-circle" %}</a>
<h1>Questions de modération</h1>
</header>
{% if object_list %}
{% for question in object_list %}
<article>
<header>
<a class="slide-buttons" href="{{ question.get_absolute_url }}" role="button">Détails...</a>
<h2>Question #{{ question.pk }}&nbsp;: {{ question.question }}</h2>
<p>{% if question.answers %}
<p>réponses possibles&nbsp;:</p>
<ul>
{% for answer in question.answers.all %}
<li>{{ answer.answer }}</li>
{% endfor %}
</ul>
{% else %}
<p><em>aucune réponse définie</em></p>
{% endif %}
</p>
</header>
</article>
{% endfor %}
{% else %}
<p>Il n'y a aucune question définie.</p>
{% endif %}
<footer>
<span>
{% if page_obj.has_previous %}
<a href="?page=1" role="button">&laquo; premier</a>
<a href="?page={{ page_obj.previous_page_number }}" role="button">précédent</a>
{% endif %}
<span>
Page {{ page_obj.number }} sur {{ page_obj.paginator.num_pages }}
</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}" role="button">suivant</a>
<a href="?page={{ page_obj.paginator.num_pages }}" role="button">dernier &raquo;</a>
{% endif %}
</span>
</footer>
</article>
{% include "agenda_culturel/side-nav.html" with current="moderationquestions" %}
</div>
{% endblock %}

View File

@ -4,9 +4,11 @@
<a href="?page={{ page_obj.previous_page_number }}" role="button">précédent</a> <a href="?page={{ page_obj.previous_page_number }}" role="button">précédent</a>
{% endif %} {% endif %}
{% if page_obj.paginator.num_pages != 1 %}
<span> <span>
Page {{ page_obj.number }} sur {{ page_obj.paginator.num_pages }} Page {{ page_obj.number }} sur {{ page_obj.paginator.num_pages }}
</span> </span>
{% endif %}
{% if page_obj.has_next %} {% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}" role="button">suivant</a> <a href="?page={{ page_obj.next_page_number }}" role="button">suivant</a>

View File

@ -0,0 +1,25 @@
{% extends "agenda_culturel/page.html" %}
{% block title %}{% block og_title %}Supprimer l'organisateur {{ object.name }}{% endblock %}{% endblock %}
{% block fluid %}{% endblock %}
{% block configurer-bouton %}{% endblock %}
{% block content %}
<article>
<header>
<h1>Supprimer l'organisateur {{ object.name }}</h1>
</header>
<form method="post">{% csrf_token %}
<p>Êtes-vous sûr·e de vouloir supprimer l'organisateur «&nbsp;{{ object.name }} ({{ object.pk }})&nbsp;»&nbsp;?</p>
{{ form }}
<div class="grid buttons">
<a href="{% if request.META.HTTP_REFERER %}{{ request.META.HTTP_REFERER }}{% else %}{{ object.get_absolute_url }}{% endif %}" role="button" class="secondary">Annuler</a>
<input type="submit" value="Confirmer">
</div>
</form>
</article>
{% endblock %}

View File

@ -0,0 +1,95 @@
{% extends "agenda_culturel/page.html" %}
{% block title %}{% block og_title %}{{ object.name }}{% endblock %}{% endblock %}
{% load tag_extra %}
{% load utils_extra %}
{% load cat_extra %}
{% load static %}
{% load cache %}
{% load l10n %}
{% block entete_header %}
{% css_categories %}
<script src="/static/admin/js/vendor/jquery/jquery.js"></script>
<script src="/static/admin/js/jquery.init.js"></script>
<script src="{% static 'location_field/leaflet/leaflet.js' %}"></script>
<link href="{% static 'location_field/leaflet/leaflet.css' %}" type="text/css" media="all" rel="stylesheet">
{% endblock %}
{% block fluid %}{% endblock %}
{% block body-class %}organisation{% endblock %}
{% block content %}
<article>
<header>
<a href="{% url 'view_organisations' %}" role="button">{% picto_from_name "chevron-left" %} Toutes les organisations</a>
{% if perms.agenda_culturel.change_organisation %}
<div class="slide-buttons">
<a href="{% url 'edit_organisation' object.pk %}" role="button">Modifier {% picto_from_name "edit-3" %}</a>
<a href="{% url 'delete_organisation' object.pk %}" role="button">Supprimer {% picto_from_name "trash-2" %}</a>
</div>
{% endif %}
<h1>{{ object.name }}</h1>
{% if object.website or object.principal_place %}
<ul>
{% if object.website %}
<li><strong>Site internet&nbsp;:</strong> <a href="{{ object.website }}">{{ object.website }}</a></li>
{% endif %}
{% if object.principal_place %}
<li><strong>Lieu principal&nbsp;:</strong> <a href="{{ object.principal_place.get_absolute_url }}">{{ object.principal_place }}</a></li>
{% endif %}
</ul>
{% endif %}
{% if user.is_authenticated %}
{% if object.recurrentimport_set.all|length == 1 %}
<p>Cet organisateur est associé à l'import récurrent <a href="{{ object.recurrentimport_set.all.0.get_absolute_url }}">{{ object.recurrentimport_set.all.0 }}</a>.</p>
{% endif %}
{% if object.recurrentimport_set.count > 1 %}
<p>Cet organisateur est associé aux imports récurrents&nbsp;:</p>
<ul>
{% for ir in object.recurrentimport_set.all %}
<li><a href="{{ ir.get_absolute_url }}">{{ ir }}</a></li>
{% endfor %}
</ul>
{% endif %}
{% endif %}
{{ object.description|safe }}
</header>
{% with cache_timeout=user.is_authenticated|yesno:"30,600" %}
{% cache cache_timeout organisation_list user.is_authenticated object page_obj.number past %}
<div class="slide-buttons">
{% if past %}
<a href="{{ object.get_absolute_url }}" role="button">Voir les événements à venir</a>
{% else %}
<a href="{% url 'view_organisation_past' object.pk %}" role="button">Voir les événements passés</a>
{% endif %}
</div>
{% if past %}
<h2>Événements passés</h2>
{% else %}
<h2>Événements à venir</h2>
{% endif %}
{% if object_list %}
{% include "agenda_culturel/navigation.html" with page_obj=page_obj %}
{% for event in object_list %}
{% include "agenda_culturel/single-event/event-elegant-inc.html" with event=event day=0 no_location=1 %}
{% endfor %}
{% include "agenda_culturel/navigation.html" with page_obj=page_obj %}
{% else %}
<p><em>Aucun événement</em></p>
{% endif %}
{% endcache %}
{% endwith %}
<footer>
</footer>
</article>
{% endblock %}

View File

@ -0,0 +1,43 @@
{% extends "agenda_culturel/page-admin.html" %}
{% load static %}
{% block entete_header %}
<script src="{% url 'jsi18n' %}"></script>
<script src="/static/admin/js/vendor/jquery/jquery.js"></script>
<script src="/static/admin/js/jquery.init.js"></script>
<script src="{% static 'choicejs/choices.min.js' %}"></script>
<script>window.CKEDITOR_BASEPATH = '/static/ckeditor/ckeditor/';</script>
{% endblock %}
{% block title %}{% block og_title %}{% if object %}Description de {{ object.name }}{% else %}Description d'un organisme{% endif %}{% endblock %}{% endblock %}
{% block fluid %}{% endblock %}
{% block content %}
<article>
<header>
<h1>{% if object %}Description de {{ object.name }}{% else %}Description d'un organisme{% endif %}</h1>
</header>
<form method="post">{% csrf_token %}
{{ form.media }}
{{ form.as_p }}
<div class="grid buttons">
<a href="{% if request.META.HTTP_REFERER %}{{ request.META.HTTP_REFERER }}{% else %}{% if object %}{{ object.get_absolute_url }}{% else %}{% url 'view_organisations' %}{% endif %}{% endif %}" role="button" class="secondary">Annuler</a>
<input type="submit" value="Envoyer">
</div>
</form>
</article>
<script>
const places = document.querySelector('#id_principal_place');
const choices_places = new Choices(places,
{
shouldSort: false,
}
);
</script>
{% endblock %}

View File

@ -0,0 +1,76 @@
{% extends "agenda_culturel/page.html" %}
{% block title %}{% block og_title %}Liste des organismes{% endblock %}{% endblock %}
{% block fluid %}{% endblock %}
{% load utils_extra %}
{% load cat_extra %}
{% block entete_header %}
{% css_categories %}
{% 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 %}
<article>
<header>
{% if user.is_authenticated %}
<div class="slide-buttons">
<a href="{% url 'add_organisation' %}" role="button">Ajouter {% picto_from_name "plus-circle" %}</a>
</div>
{% endif %}
<h1>Liste des organismes</h1>
</header>
{% include "agenda_culturel/navigation.html" with page_obj=page_obj %}
</article>
{% if object_list %}
{% for organisation in object_list %}
<article>
<header>
{% if user.is_authenticated %}
<div class="slide-buttons">
<a href="{% url 'edit_organisation' organisation.pk %}" role="button">Modifier {% picto_from_name "edit-3" %}</a>
<a href="{% url 'delete_organisation' organisation.pk %}" role="button">Supprimer {% picto_from_name "trash-2" %}</a>
</div>
{% endif %}
<h2>{{ organisation.name }}</h2>
</header>
{% if organisation.website or organisation.principal_place %}
<ul>
{% if organisation.website %}
<li><strong>Site internet&nbsp;:</strong> <a href="{{ organisation.website }}">{{ organisation.website }}</a></li>
{% endif %}
{% if organisation.principal_place %}
<li><strong>Lieu principal&nbsp;:</strong> <a href="{{ organisation.principal_place.get_absolute_url }}">{{ organisation.principal_place }}</a></li>
{% endif %}
</ul>
{% endif %}
<footer>
{% if organisation.organised_events.all|length > 1 %}
<p class="slide-buttons"><a role="button" href="{{ organisation.get_absolute_url }}">voir les {{ organisation.organised_events.all|length }} événements {% picto_from_name "chevron-right" %}</a></p>
{% else %}
<p class="slide-buttons"><a role="button" href="{{ organisation.get_absolute_url }}">voir les détails {% picto_from_name "chevron-right" %}</a></p>
{% endif %}
<div style="clear: both"></div>
</footer>
</article>
{% endfor %}
{% else %}
<p>Il n'y a aucun organisme défini.</p>
{% endif %}
<article>
<footer>
{% include "agenda_culturel/navigation.html" with page_obj=page_obj %}
</footer>
</article>
{% endblock %}

View File

@ -3,10 +3,11 @@
{% load cat_extra %} {% load cat_extra %}
{% load utils_extra %} {% load utils_extra %}
{% load event_extra %} {% load event_extra %}
{% load cache %}
{% block title %}{% block og_title %}{{ event.title }}{% endblock %}{% endblock %} {% 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 og_description %}{% if event.description %}{{ event.description |truncatewords:20|linebreaks }}{% else %}{{ block.super }}{% endif %}{% endblock %}
{% block entete_header %} {% block entete_header %}
@ -16,21 +17,59 @@
{% block content %} {% block content %}
<div class="grid two-columns"> <div class="grid two-columns">
<div> <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 %} {% include "agenda_culturel/single-event/event-single-inc.html" with event=event filter=filter %}
{% endcache %}
{% endwith %}
{% if user.is_authenticated %} {% if user.is_authenticated %}
<article> <article id="chronology">
<header> <header>
<h2>Informations internes</h2> <h2>Chronologie</h2>
</header> </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</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 {{ step.data.name }}{% if step.data.email %} (<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> </article>
{% endif %} {% endif %}
</div> </div>
{% with cache_timeout=user.is_authenticated|yesno:"30,600" %}
{% cache cache_timeout event_aside user.is_authenticated event %}
<aside> <aside>
{% with event.get_concurrent_events as concurrent_events %} {% with event.get_concurrent_events as concurrent_events %}
{% if concurrent_events %} {% if concurrent_events %}
@ -51,39 +90,7 @@
</article> </article>
{% endif %} {% endif %}
{% endwith %} {% 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 %} {% if event.other_versions and not event.other_versions.fixed %}
{% with poss_dup=event.get_other_versions|only_allowed:user.is_authenticated %} {% with poss_dup=event.get_other_versions|only_allowed:user.is_authenticated %}
{% if poss_dup|length > 0 %} {% if poss_dup|length > 0 %}
@ -122,12 +129,15 @@
{% else %} {% else %}
Signaler comme doublon Signaler comme doublon
{% endif %}</a> {% endif %}</a>
</article> <a role="button" href="{% url 'message_for_event' event.pk %}">Signaler cet événement</a>
</article>
</aside> </aside>
{% endcache %}
{% endwith %}
</div> </div>
{% endblock %} {% endblock %}

View File

@ -1,9 +1,6 @@
{% extends "agenda_culturel/page.html" %} {% extends "agenda_culturel/page.html" %}
{% load i18n %}
{% load cat_extra %} {% load cat_extra %}
{% load event_extra %} {% load event_extra %}
{% load utils_extra %} {% load utils_extra %}
@ -25,12 +22,11 @@
{% block content %} {% block content %}
{% get_current_language as LANGUAGE_CODE %}
{% with cache_timeout=user.is_authenticated|yesno:"30,600" %}
{% cache cache_timeout month user.is_authenticated calendar.firstdate filter.to_str LANGUAGE_CODE %}
{% include "agenda_culturel/filter-inc.html" with filter=filter noarticle=0 %} {% include "agenda_culturel/filter-inc.html" with filter=filter noarticle=0 %}
{% with cache_timeout=user.is_authenticated|yesno:"30,600" %}
{% cache cache_timeout month user.is_authenticated calendar.firstdate filter.get_url %}
<article> <article>
<header> <header>
@ -64,12 +60,76 @@
{% endif %} {% endif %}
<div class="grid week-in-month"> <div class="grid week-in-month">
{% for d in calendar.calendar_days_list %} {% for day in calendar.calendar_days_list %}
{% if forloop.counter0|divisibleby:7 %} {% if forloop.counter0|divisibleby:7 %}
{% if not forloop.first %}</div><div class="grid week-in-month">{% endif %} {% if not forloop.first %}</div><div class="grid week-in-month">{% endif %}
<div id="week-{{ d.date.isocalendar.1 }}" class="entete-semaine"><h2><a href="{% url 'week_view' calendar.year d.week %}?{{ filter.get_url }}">Semaine {{ d.week }}</a></h2></div> <div id="week-{{ day.date.isocalendar.1 }}" class="entete-semaine"><h2><a href="{% url 'week_view' calendar.year day.week %}?{{ filter.get_url }}">Semaine {{ d.week }}</a></h2></div>
{% endif %} {% endif %}
{% include "agenda_culturel/day-inc.html" with day=d resume=1 fixed_style=calendar.all_in_past filter=filter headers="h3" %}
{% with day.date|date:"Y-m-d" as daytag %}
{% with "date-"|add:daytag as daytag %}
<article class="day{{ day|calendar_classes:calendar.all_in_past }}" id="{{ daytag }}">
{% if day.is_today %}
<script>
document.addEventListener("DOMContentLoaded", function(e) {
agenda = document.getElementById("calendar");
todayArticle = document.getElementById("today").parentElement;
left = todayArticle.offsetLeft;
top = todayArticle.offsetTop;
agenda.scrollTo({top: top, left: left});
});
</script>
{% endif %}
<header{% if day.is_today %} id="today"{% endif %}>
<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>
{% for category, events in day.events_by_category_ordered %}
<li>{{ events.0.category | circle_cat }}
<a href="{{ day.date | url_day }}?{{ filter.get_url|add_url_category:events.0.category }}" data-target="{{ daytag }}-category-{{ events.0.category.pk }}" onClick="toggleModal(event)">{{ events | length }} {{ category }}</a></li>
<dialog id="{{ daytag }}-category-{{ events.0.category.pk }}">
<article>
<header>
<a href="#{{ daytag }}-category-{{ events.0.category.pk }}"
aria-label="Fermer"
class="close"
data-target="{{ daytag }}-category-{{ events.0.category.pk }}"
onClick="toggleModal(event)"></a>
<h3>{{ events.0.category | small_cat }} du {{ day.date | date:"l j F" }}</h3>
</header>
<ul>
{% for event in events %}
<li>
{% 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|no_emoji }} {{ event|tw_badge }}</a>
</li>
{% endfor %}
</ul>
<footer>
<div class="buttons">
<a href="{{ day.date | url_day }}?{{ filter.get_url }}" role="button">Voir la journée <svg width="1em" height="1em" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<use href="{% static 'images/feather-sprite.svg' %}#chevron-right" />
</svg></a>
</div>
</footer>
</article>
</dialog>
{% endfor %}
<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>
{% endwith %}
{% endwith %}
{% endfor %} {% endfor %}
</div> </div>
{% if calendar.lastdate|shift_day:+1|not_after_last %} {% if calendar.lastdate|shift_day:+1|not_after_last %}

View File

@ -4,6 +4,7 @@
{% load cat_extra %} {% load cat_extra %}
{% load utils_extra %} {% load utils_extra %}
{% load tag_extra %}
{% block entete_header %} {% block entete_header %}
{% css_categories %} {% css_categories %}
@ -38,12 +39,13 @@
<li><strong>Adresse naviguable&nbsp;:</strong> <a href="{{ object.browsable_url }}">{{ object.browsable_url }}</a></li> <li><strong>Adresse naviguable&nbsp;:</strong> <a href="{{ object.browsable_url }}">{{ object.browsable_url }}</a></li>
<li><strong>Valeurs par défaut&nbsp;:</strong> <li><strong>Valeurs par défaut&nbsp;:</strong>
<ul> <ul>
<li><strong>Publié&nbsp;:</strong> {{ object.defaultPublished }}</li> <li><strong>Publié&nbsp;:</strong> {{ object.defaultPublished|yesno:"Oui,Non" }}</li>
<li><strong>Localisation&nbsp;:</strong> {{ object.defaultLocation }}</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>Catégorie&nbsp;:</strong> {{ object.defaultCategory }}</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> <li><strong>Étiquettes&nbsp;:</strong>
{% for tag in object.defaultTags %} {% for tag in object.defaultTags %}
{{ tag }}{% if not forloop.last %}, {% endif %} <a href="{% url 'view_tag' tag %}">{{ tag|tw_highlight }}</a>{% if not forloop.last %}, {% endif %}
{% endfor %} {% endfor %}
</li> </li>
</ul> </ul>

View File

@ -1,10 +1,12 @@
{% extends "agenda_culturel/page-single.html" %} {% extends "agenda_culturel/page-single.html" %}
{% load static %}
{% block content %} {% block content %}
<article> <article>
<header> <header>
<h1>{{ title }}</h1> <h1>{{ title }}</h1>
</header> </header>
<div class="image-flottante"><img src="{% static 'images/alunissage.svg' %}" alt="deux pommes qui alunissent" /></div>
{% include "agenda_culturel/static_content.html" with name=static_content url_path=url_path %} {% include "agenda_culturel/static_content.html" with name=static_content url_path=url_path %}
<ul> <ul>
{% for rimport in rimports %} {% for rimport in rimports %}

View File

@ -1,7 +1,6 @@
{% extends "agenda_culturel/page.html" %} {% extends "agenda_culturel/page.html" %}
{% load i18n %}
{% load cache %} {% load cache %}
{% load cat_extra %} {% load cat_extra %}
{% load event_extra %} {% load event_extra %}
@ -21,9 +20,6 @@
{% block content %} {% block content %}
{% get_current_language as LANGUAGE_CODE %}
{% with cache_timeout=user.is_authenticated|yesno:"30,600" %}
{% cache cache_timeout upcoming user.is_authenticated calendar.firstdate filter.to_str calendar.calendar_days_list|length LANGUAGE_CODE %}
<article id="filters"> <article id="filters">
<header><h1 id="index-avenir">{% block title %}{% block og_title %} <header><h1 id="index-avenir">{% block title %}{% block og_title %}
@ -40,6 +36,9 @@
{% include "agenda_culturel/filter-inc.html" with filter=filter noarticle=1 %} {% include "agenda_culturel/filter-inc.html" with filter=filter noarticle=1 %}
{% with cache_timeout=user.is_authenticated|yesno:"30,600" %}
{% cache cache_timeout upcoming user.is_authenticated calendar.firstdate filter.get_url calendar.calendar_days_list|length %}
<footer> <footer>
<a href="{% if calendar.calendar_days_list|length == 1 %}{% url 'day_view' date_pred.year date_pred.month date_pred.day %}{% else %}{% url 'a_venir_jour' date_pred.year date_pred.month date_pred.day %}{% endif %}" aria-label="dates précédentes" role="button"> <a href="{% if calendar.calendar_days_list|length == 1 %}{% url 'day_view' date_pred.year date_pred.month date_pred.day %}{% else %}{% url 'a_venir_jour' date_pred.year date_pred.month date_pred.day %}{% endif %}" aria-label="dates précédentes" role="button">
{% picto_from_name "chevrons-left" %} {% picto_from_name "chevrons-left" %}
@ -96,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> <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> <ul>
{% for event in ti.events %} {% 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 }} <li>{{ event.category | circle_cat:event.has_recurrences }}
{% if event.start_time %} {% if event.start_time %}
{{ event.start_time }} {{ event.start_time }}
{% endif %} {% endif %}
{{ event|picto_status }} <a href="#event-{{ event.id }}">{{ event.title }}</a> {{ event|picto_status }} <a href="#event-{{ event.id }}">{{ event.title|no_emoji }}</a> {{ event|tw_badge }}
</li> </li>
{% endfor %} {% 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> </ul>
{% endif %} {% endif %}
{% endfor %} {% endfor %}

View File

@ -1,13 +1,12 @@
{% extends "agenda_culturel/page.html" %} {% extends "agenda_culturel/page.html" %}
{% load i18n %}
{% load cache %} {% load cache %}
{% load cat_extra %} {% load cat_extra %}
{% load event_extra %} {% load event_extra %}
{% load utils_extra %} {% load utils_extra %}
{% load static %} {% load static %}
{% load i18n %} {% load tag_extra %}
{% block entete_header %} {% block entete_header %}
{% css_categories %} {% css_categories %}
@ -21,11 +20,10 @@
{% block content %} {% block content %}
{% get_current_language as LANGUAGE_CODE %} {% include "agenda_culturel/filter-inc.html" with filter=filter noarticle=0 %}
{% with cache_timeout=user.is_authenticated|yesno:"30,600" %}
{% cache cache_timeout week user.is_authenticated calendar.firstdate filter.to_str LANGUAGE_CODE %}
{% include "agenda_culturel/filter-inc.html" with filter=filter noarticle=0 %} {% with cache_timeout=user.is_authenticated|yesno:"30,600" %}
{% cache cache_timeout week user.is_authenticated calendar.firstdate filter.get_url %}
<article> <article>
<header> <header>
@ -39,7 +37,7 @@
<div> <div>
{% if calendar.firstdate|shift_day:-1|not_before_first %} {% if calendar.firstdate|shift_day:-1|not_before_first %}
{% if calendar.lastdate|not_after_last %} {% 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> {% picto_from_name "chevron-left" %} précédente</a>
{% endif %} {% endif %}
{% endif %} {% endif %}
@ -47,7 +45,7 @@
{% if calendar.lastdate|shift_day:+1|not_after_last %} {% if calendar.lastdate|shift_day:+1|not_after_last %}
{% if calendar.lastdate|not_before_first %} {% if calendar.lastdate|not_before_first %}
<div class="right"> <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" %} {% picto_from_name "chevron-right" %}
</a> </a>
</div> </div>
@ -59,19 +57,123 @@
<div class="slider-button slider-button-inside button-left hidden">{% picto_from_name "arrow-left" %}</div> <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.firstdate|shift_day:-1|not_before_first %}
{% if calendar.lastdate|not_after_last %} {% 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 %}
{% endif %} {% endif %}
<div class="grid"> <div class="grid">
{% for d in calendar.calendar_days_list %} {% for day in calendar.calendar_days_list %}
{% include "agenda_culturel/day-inc.html" with day=d resume=0 fixed_style=calendar.all_in_past filter=filter headers="" %}
{% with day.date|date:"Y-m-d" as daytag %}
{% with "date-"|add:daytag as daytag %}
<article class="day{{ day|calendar_classes:calendar.all_in_past }}" id="{{ daytag }}">
{% if day.is_today %}
<script>
document.addEventListener("DOMContentLoaded", function(e) {
agenda = document.getElementById("calendar");
todayArticle = document.getElementById("today").parentElement;
left = todayArticle.offsetLeft;
top = todayArticle.offsetTop;
agenda.scrollTo({top: top, left: left});
});
</script>
{% endif %}
<header{% if day.is_today %} id="today"{% endif %}>
<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|no_emoji }}</a>
{{ event|tw_badge }}
<dialog id="event-{{ event.id }}">
<article>
{% 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"
data-target="event-{{ event.id }}"
onClick="toggleModal(event)"></a>
<h3>{{ event.category|small_cat_recurrent:event.has_recurrences }} {{ event|picto_status }} {{ event.title }} {{ event|picto_visibility:user.is_authenticated }}</h3>
<p>
{% picto_from_name "map-pin" %}
{% if event.exact_location %}
{{ event.exact_location.name }}, {{ event.exact_location.city }}
{% else %}
{% if perms.agenda_culturel.change_event and perms.agenda_culturel.change_place %}
<a href="{% url 'add_place_to_event' event.pk %}" class="missing-data">{{ event.location }}</a>
{% else %}
{{ event.location }}
{% endif %}
{% endif %} </p>
<p>
{% picto_from_name "calendar" %}
{% if event.end_day and event.end_day != event.start_day %}du{% else %}le{% endif %}
{{ event.start_day|date|frdate }}
{% if event.start_time %} {% if not event.end_day or event.end_day == event.start_day %}{% if event.end_time %}de{% else %}à{% endif %}{% endif %}
{{ event.start_time }}
{% endif %}
{% if event.end_day and event.end_day != event.start_day %}
au {% if event.end_day and event.end_day != event.start_day %}{{ event.end_day|date|frdate }}{% endif %}
{% 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>
<p>
{% for tag in event.sorted_tags %}
{{ tag | tag_button }}
{% endfor %}
</p>
<footer class="infos-and-buttons">
<div class="buttons">
<a href="{{ event.get_absolute_url }}" role="button">Voir l'événement <svg width="1em" height="1em" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<use href="{% static 'images/feather-sprite.svg' %}#chevron-right" />
</svg></a>
</div>
</footer>
</article>
</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>
</article>
{% endwith %}
{% endwith %}
{% endfor %} {% endfor %}
</div> </div>
<div class="slider-button slider-button-inside button-right hidden">{% picto_from_name "arrow-right" %}</div> <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|shift_day:+1|not_after_last %}
{% if calendar.lastdate|not_before_first %} {% 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 %}
{% endif %} {% endif %}

View File

@ -1,5 +1,11 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="fr"> <html lang="fr">
{% load event_extra %}
{% load cache %}
{% load messages_extra %}
{% load utils_extra %}
{% load duplicated_extra %}
{% load rimports_extra %}
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
@ -9,7 +15,7 @@
{% load static %} {% load static %}
<meta property="og:title" content="Pommes de lune — {% block og_title %}{% endblock %}" /> <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: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:image" content="{% block og_image %}https://{{ request.get_host }}{% get_media_prefix %}screenshot.png{% endblock %}" />
<meta property="og:url" content="{{ request.build_absolute_uri }}" /> <meta property="og:url" content="{{ request.build_absolute_uri }}" />
{% if debug %} {% if debug %}
@ -27,12 +33,7 @@
{% block entete_header %} {% block entete_header %}
{% endblock %} {% endblock %}
</head> </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 %}">
<div id="boutons-fixes"> <div id="boutons-fixes">
<ul> <ul>
@ -50,7 +51,7 @@
{% 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 %} {% 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 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 %} {% 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 '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> <li><a href="{% url 'ce_mois_ci' %}{% block ce_mois_ci_parameters %}{% endblock %}">Ce mois-ci</a></li>
</ul> </ul>
@ -65,8 +66,7 @@
<li> <li>
<div> <div>
{% if perms.agenda_culturel.view_recurrentimport %} {% if perms.agenda_culturel.view_recurrentimport %}
{% show_badge_rimports "bottom" "failed" %} {% show_badges_rimports "bottom" %}
{% show_badge_rimports "bottom" "running" %}
{% endif %} {% endif %}
{% if perms.agenda_culturel.change_event %} {% if perms.agenda_culturel.change_event %}
{% show_badges_events "bottom" %} {% show_badges_events "bottom" %}
@ -77,8 +77,8 @@
{% if perms.agenda_culturel.change_place and perms.agenda_culturel.change_event %} {% if perms.agenda_culturel.change_place and perms.agenda_culturel.change_event %}
{% show_badge_unknown_places "bottom" %} {% show_badge_unknown_places "bottom" %}
{% endif %} {% endif %}
{% if perms.agenda_culturel.view_contactmessage %} {% if perms.agenda_culturel.view_message %}
{% show_badge_contactmessages "bottom" %} {% show_badge_messages "bottom" %}
{% endif %} {% endif %}
{% if user.is_authenticated %} {% if user.is_authenticated %}
{{ user.username }} @ {{ user.username }} @
@ -108,6 +108,9 @@
<div> <div>
<a href="{% url 'view_places' %}">{% picto_from_name "map-pin" %} lieux</a> <a href="{% url 'view_places' %}">{% picto_from_name "map-pin" %} lieux</a>
</div> </div>
<div>
<a href="{% url 'view_organisations' %}">{% picto_from_name "users" %} organisateurs</a>
</div>
<div> <div>
<a href="{% url 'view_all_tags' %}">{% picto_from_name "tag" %} étiquettes</a> <a href="{% url 'view_all_tags' %}">{% picto_from_name "tag" %} étiquettes</a>
</div> </div>

Some files were not shown because too many files have changed in this diff Show More