From 50579a65976989fcff7440dda8cd2cbe70becbc0 Mon Sep 17 00:00:00 2001 From: Jean-Marie Favreau Date: Sat, 12 Apr 2025 15:38:54 +0200 Subject: [PATCH] Ajout import Helloasso --- .gitignore | 1 + env.example | 3 + experimentations/get_helloasso.py | 50 +++ src/agenda_culturel/celery.py | 3 + .../import_tasks/downloader.py | 5 +- src/agenda_culturel/import_tasks/extractor.py | 20 +- .../generic_extractors/helloasso.py | 156 ++++++++ src/agenda_culturel/import_tasks/importer.py | 5 +- .../locale/fr/LC_MESSAGES/django.po | 335 +++++++++--------- .../0167_alter_recurrentimport_processor.py | 18 + src/agenda_culturel/models.py | 16 +- src/agenda_culturel/settings/base.py | 4 + 12 files changed, 431 insertions(+), 185 deletions(-) create mode 100755 experimentations/get_helloasso.py create mode 100644 src/agenda_culturel/import_tasks/generic_extractors/helloasso.py create mode 100644 src/agenda_culturel/migrations/0167_alter_recurrentimport_processor.py diff --git a/.gitignore b/.gitignore index b5bd07b..d88bdbb 100644 --- a/.gitignore +++ b/.gitignore @@ -92,3 +92,4 @@ experimentations/events-augustes.json # MacOS .DS_Store +src/errors/ diff --git a/env.example b/env.example index 8d3f633..c0e0a39 100644 --- a/env.example +++ b/env.example @@ -23,3 +23,6 @@ APP_DOMAIN=app.backend.dev # APIs ECHOSCIENCES_TOKEN= + +HELLOASSO_ID= +HELLOASSO_SECRET= diff --git a/experimentations/get_helloasso.py b/experimentations/get_helloasso.py new file mode 100755 index 0000000..92f621d --- /dev/null +++ b/experimentations/get_helloasso.py @@ -0,0 +1,50 @@ +#!/usr/bin/python3 +# coding: utf-8 + +# To be able to run this import out of django, you'll have to set an environment variable with the Helloasso client id/secret: +# export HELLOASSO_SECRET= +# export HELLOASSO_ID= + +import json +import os +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) +sys.path.append(parent + "/src") + +from src.agenda_culturel.import_tasks.downloader import SimpleDownloader +from src.agenda_culturel.import_tasks.generic_extractors.helloasso import ( + CExtractor, +) +from src.agenda_culturel.import_tasks.importer import URL2Events + + +if __name__ == "__main__": + u2e = URL2Events(SimpleDownloader(), CExtractor()) + #url = "https://www.helloasso.com/associations/federation-des-associations-laiques-du-puy-de-dome/evenements/de-fil-en-chemin-2025-11e-edition-rencontres-de-danse-amateur" + url = "https://www.helloasso.com/associations/federation-des-associations-laiques-du-puy-de-dome" + + url_human = url + + events = u2e.process( + url, + url_human, + cache="cache-helloasso.html", + default_values={}, + published=True, + ) + + exportfile = "events-helloasso.json" + print("Saving events to file {}".format(exportfile)) + with open(exportfile, "w") as f: + json.dump(events, f, indent=4, default=str) diff --git a/src/agenda_culturel/celery.py b/src/agenda_culturel/celery.py index 9008707..c022b02 100644 --- a/src/agenda_culturel/celery.py +++ b/src/agenda_culturel/celery.py @@ -39,6 +39,7 @@ from .import_tasks.generic_extractors import ( ical, fbevents, echosciences, + helloasso, ) from .import_tasks.importer import URL2Events @@ -211,6 +212,8 @@ def run_recurrent_import_internal(rimport, downloader, req_id): extractor = lecameleon.CExtractor() elif rimport.processor == RecurrentImport.PROCESSOR.ECHOSCIENCES: extractor = echosciences.CExtractor() + elif rimport.processor == RecurrentImport.PROCESSOR.HELLOASSO: + extractor = helloasso.CExtractor() else: extractor = None diff --git a/src/agenda_culturel/import_tasks/downloader.py b/src/agenda_culturel/import_tasks/downloader.py index 671970d..59be269 100644 --- a/src/agenda_culturel/import_tasks/downloader.py +++ b/src/agenda_culturel/import_tasks/downloader.py @@ -121,7 +121,10 @@ class ChromiumHeadlessDownloader(Downloader): self.driver = webdriver.Chrome(service=self.service, options=self.options) def __del__(self): - self.driver.quit() + try: + self.driver.quit() + except Exception as e: + print('Error: ' + str(e)) def screenshot(self, url, path_image): print("Screenshot {}".format(url)) diff --git a/src/agenda_culturel/import_tasks/extractor.py b/src/agenda_culturel/import_tasks/extractor.py index b86e701..e8a8944 100644 --- a/src/agenda_culturel/import_tasks/extractor.py +++ b/src/agenda_culturel/import_tasks/extractor.py @@ -316,21 +316,23 @@ class Extractor(ABC): ) from .generic_extractors.ical import ICALExtractor from .custom_extractors.associations_cf import CExtractor as AssociationsCF + from .generic_extractors.helloasso import CExtractor as HelloAssoExtractor if single_event: return [ - FacebookEventExtractor(), - GoogleCalendarLinkEventExtractor(), - AssociationsCF(), - ICALExtractor(), - EventNotFoundExtractor(), + FacebookEventExtractor, + GoogleCalendarLinkEventExtractor, + AssociationsCF, + ICALExtractor, + HelloAssoExtractor, + EventNotFoundExtractor, ] else: return [ - ICALExtractor(), - FacebookEventExtractor(), - GoogleCalendarLinkEventExtractor(), - EventNotFoundExtractor(), + ICALExtractor, + FacebookEventExtractor, + GoogleCalendarLinkEventExtractor, + EventNotFoundExtractor, ] diff --git a/src/agenda_culturel/import_tasks/generic_extractors/helloasso.py b/src/agenda_culturel/import_tasks/generic_extractors/helloasso.py new file mode 100644 index 0000000..ecb240d --- /dev/null +++ b/src/agenda_culturel/import_tasks/generic_extractors/helloasso.py @@ -0,0 +1,156 @@ +import logging +from datetime import datetime, timedelta +from django.conf import settings +import dateutil.parser +import requests +from bs4 import BeautifulSoup +import re +import json + +from ..extractor import Extractor + +logger = logging.getLogger(__name__) + + +# A class dedicated to get events from helloasso +class CExtractor(Extractor): + patternEvent = r'^https://www\.helloasso\.com/associations/([\w\-]+)/evenements/([\w\-]+)' + patternOrg = r'^https://www\.helloasso\.com/associations/([\w\-]+)' + + urlAPI = "https://api.helloasso.com" + + def __init__(self): + super().__init__() + self.no_downloader = True + + def id_and_secret_available(self): + return settings.HELLOASSO_ID != "" and settings.HELLOASSO_SECRET != "" + + def is_known_url(url): + return re.match(CExtractor.patternEvent, url) is not None or re.match(CExtractor.patternOrg, url) is not None + + def get_header_with_token(): + url = CExtractor.urlAPI + "/oauth2/token" + + payload = { + "grant_type": "client_credentials", + "client_id": settings.HELLOASSO_ID, + "client_secret": settings.HELLOASSO_SECRET + } + headers = { + "accept": "application/json", + "Content-Type": "application/x-www-form-urlencoded" + } + + response = requests.post(url, data=payload, headers=headers) + + data = json.loads(response.text) + + return { + "accept": "application/json", + "authorization": "Bearer " + data["access_token"] + } + + def _get_single_event(org, event, headers=None): + if headers is None: + headers = CExtractor.get_header_with_token() + url = CExtractor.urlAPI + "/v5/organizations/{}/forms/Event/{}/public".format(org, event) + response = requests.get(url, headers=headers) + return json.loads(response.text) + + def _get_all_events(org): + url = CExtractor.urlAPI + "/v5/organizations/{}/forms?states=Public&formTypes=Event&pageSize=20".format(org) + headers = CExtractor.get_header_with_token() + result = [] + for i in range(1, 200): + response = requests.get(url + "&pageIndex={}".format(i), headers=headers) + events = json.loads(response.text)["data"] + if len(events) == 0: + return result + else: + for e in events: + result.append(CExtractor._get_single_event(org, e["formSlug"], headers)) + + + def _get_events(self, url): + match = re.match(CExtractor.patternEvent, url) + if match: + return [CExtractor._get_single_event(match.group(1), match.group(2))] + else: + match = re.match(CExtractor.patternOrg, url) + if match: + return CExtractor._get_all_events(match.group(1)) + + return [] + + def extract( + self, + content, + url, + url_human=None, + default_values=None, + published=False, + ): + + self.set_header(url) + self.clear_events() + + if not self.id_and_secret_available(): + logger.error("ID/Secret not available") + raise Exception("ID/Secret not available") + + if not CExtractor.is_known_url(url): + logger.error("Unknown url") + raise Exception("Unknown url") + + events = self._get_events(url) + + for event in events: + title = event["title"] + event_url = event["url"] + + image = None + image_alt = None + if "banner" in event: + image = event["banner"]["publicUrl"] + if "description" in event["banner"]: + image_alt = event["banner"]["description"] + + + location = [] + if "place" in event and event["place"] is not None: + for elplace in ["name", "address", "zipCode", "city"]: + if elplace in event["place"]: + location.append(event["place"][elplace]) + location = ", ".join(location) + + description = event["description"] + + start = dateutil.parser.isoparse(event["startDate"]) + end = dateutil.parser.isoparse(event["endDate"]) + + start_day = start.date() + start_time = start.time() + end_day = end.date() + end_time = end.time() + + self.add_event( + default_values, + title, + None, + start_day, + location, + description, + [], + uuids=[event_url], + recurrences=None, + url_human=event_url, + start_time=start_time, + published=published, + image=image, + image_alt=image_alt, + end_day=end_day, + end_time=end_time, + ) + + return self.get_structure() diff --git a/src/agenda_culturel/import_tasks/importer.py b/src/agenda_culturel/import_tasks/importer.py index 1793fd3..f850e23 100644 --- a/src/agenda_culturel/import_tasks/importer.py +++ b/src/agenda_culturel/import_tasks/importer.py @@ -54,8 +54,9 @@ class URL2Events: ) else: # if the extractor is not defined, use a list of default extractors - for e in Extractor.get_default_extractors(self.single_event): - logger.info("Extractor::" + type(e).__name__) + for extrClass in Extractor.get_default_extractors(self.single_event): + e = extrClass() + logger.info("Extractor::" + str(type(e))) e.set_downloader(self.downloader) try: events = e.extract( diff --git a/src/agenda_culturel/locale/fr/LC_MESSAGES/django.po b/src/agenda_culturel/locale/fr/LC_MESSAGES/django.po index e89be7b..ca93d62 100644 --- a/src/agenda_culturel/locale/fr/LC_MESSAGES/django.po +++ b/src/agenda_culturel/locale/fr/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: agenda_culturel\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-04-06 13:18+0200\n" +"POT-Creation-Date: 2025-04-12 14:10+0200\n" "PO-Revision-Date: 2023-10-29 14:16+0000\n" "Last-Translator: Jean-Marie Favreau \n" "Language-Team: Jean-Marie Favreau \n" @@ -143,11 +143,11 @@ msgid "Imported from" msgstr "Importé depuis" #: agenda_culturel/filters.py:470 agenda_culturel/models.py:875 -#: agenda_culturel/models.py:2991 +#: agenda_culturel/models.py:3000 msgid "Status" msgstr "Status" -#: agenda_culturel/filters.py:471 agenda_culturel/models.py:2755 +#: agenda_culturel/filters.py:471 agenda_culturel/models.py:2763 msgid "Closed" msgstr "Fermé" @@ -156,7 +156,7 @@ msgid "Open" msgstr "Ouvert" #: agenda_culturel/filters.py:475 agenda_culturel/filters.py:476 -#: agenda_culturel/models.py:2749 +#: agenda_culturel/models.py:2757 msgid "Spam" msgstr "Spam" @@ -164,7 +164,7 @@ msgstr "Spam" msgid "Non spam" msgstr "Non spam" -#: agenda_culturel/filters.py:481 agenda_culturel/models.py:2770 +#: agenda_culturel/filters.py:481 agenda_culturel/models.py:2778 msgid "Type" msgstr "Type" @@ -195,11 +195,11 @@ msgstr "" msgid "Your email" msgstr "Votre adresse email" -#: agenda_culturel/forms.py:168 agenda_culturel/models.py:2737 +#: agenda_culturel/forms.py:168 agenda_culturel/models.py:2745 msgid "Your email address" msgstr "Votre adresse email" -#: agenda_culturel/forms.py:174 agenda_culturel/models.py:2762 +#: agenda_culturel/forms.py:174 agenda_culturel/models.py:2770 msgid "Comments" msgstr "Commentaires" @@ -215,8 +215,8 @@ msgid "Receive notification of publication or leave a message for moderation" msgstr "Être notifié de la publication ou laisser un message à la modération" #: agenda_culturel/forms.py:208 agenda_culturel/models.py:315 -#: agenda_culturel/models.py:883 agenda_culturel/models.py:2915 -#: agenda_culturel/models.py:3026 +#: agenda_culturel/models.py:883 agenda_culturel/models.py:2924 +#: agenda_culturel/models.py:3035 msgid "Category" msgstr "Catégorie" @@ -271,7 +271,7 @@ msgid "Details" msgstr "Détails" #: agenda_culturel/forms.py:383 agenda_culturel/models.py:912 -#: agenda_culturel/models.py:2890 +#: agenda_culturel/models.py:2899 msgid "Location" msgstr "Localisation" @@ -351,7 +351,7 @@ msgid "Apply category {} to the event {}" msgstr "Appliquer la catégorie {} à l'événement {}" #: agenda_culturel/forms.py:868 agenda_culturel/models.py:700 -#: agenda_culturel/models.py:3078 +#: agenda_culturel/models.py:3087 msgid "Place" msgstr "Lieu" @@ -389,7 +389,7 @@ msgstr "Informations" msgid "Add a comment" msgstr "Ajouter un commentaire" -#: agenda_culturel/forms.py:1011 agenda_culturel/models.py:3179 +#: agenda_culturel/forms.py:1011 agenda_culturel/models.py:3188 msgid "Period type" msgstr "Type de période" @@ -505,7 +505,7 @@ msgstr "" #: agenda_culturel/models.py:198 agenda_culturel/models.py:245 #: agenda_culturel/models.py:324 agenda_culturel/models.py:619 #: agenda_culturel/models.py:660 agenda_culturel/models.py:766 -#: agenda_culturel/models.py:2729 agenda_culturel/models.py:2840 +#: agenda_culturel/models.py:2737 agenda_culturel/models.py:2849 msgid "Name" msgstr "Nom" @@ -574,8 +574,8 @@ msgstr "Description" msgid "Description of the tag" msgstr "Description de l'étiquette" -#: agenda_culturel/models.py:339 agenda_culturel/models.py:2696 -#: agenda_culturel/models.py:2743 +#: agenda_culturel/models.py:339 agenda_culturel/models.py:2704 +#: agenda_culturel/models.py:2751 msgid "Message" msgstr "Message" @@ -742,8 +742,8 @@ msgid "" "Place mainly associated with this organizer. Mainly used if there is a " "similarity in the name, to avoid redundant displays." msgstr "" -"Lieu principalement associé à cette organisation. Principalement utilisé s'il " -"y a une similarité de nom, pour éviter les affichages redondants." +"Lieu principalement associé à cette organisation. Principalement utilisé " +"s'il y a une similarité de nom, pour éviter les affichages redondants." #: agenda_culturel/models.py:800 msgid "Organisation" @@ -753,7 +753,7 @@ msgstr "Organisme" msgid "Organisations" msgstr "Organismes" -#: agenda_culturel/models.py:812 agenda_culturel/models.py:2885 +#: agenda_culturel/models.py:812 agenda_culturel/models.py:2894 msgid "Published" msgstr "Publié" @@ -789,7 +789,7 @@ msgstr "Auteur de la dernière modération" msgid "Title" msgstr "Titre" -#: agenda_culturel/models.py:889 agenda_culturel/models.py:3164 +#: agenda_culturel/models.py:889 agenda_culturel/models.py:3173 msgid "Start day" msgstr "Date de début" @@ -797,7 +797,7 @@ msgstr "Date de début" msgid "Start time" msgstr "Heure de début" -#: agenda_culturel/models.py:897 agenda_culturel/models.py:3165 +#: agenda_culturel/models.py:897 agenda_culturel/models.py:3174 msgid "End day" msgstr "Date de fin" @@ -899,60 +899,60 @@ msgstr "import récurrent" msgid "a non authenticated user" msgstr "un utilisateur non connecté" -#: agenda_culturel/models.py:1777 +#: agenda_culturel/models.py:1781 msgid "Your event has been published" msgstr "Ton événement a été publié" -#: agenda_culturel/models.py:1782 +#: agenda_culturel/models.py:1786 msgid "Your message has not been retained" msgstr "Ton événement n'a pas été retenu" -#: agenda_culturel/models.py:1870 agenda_culturel/models.py:2693 +#: agenda_culturel/models.py:1874 agenda_culturel/models.py:2701 msgid "Warning" msgstr "Warning" -#: agenda_culturel/models.py:1872 agenda_culturel/models.py:1978 +#: agenda_culturel/models.py:1876 agenda_culturel/models.py:1982 msgid "the date has not been imported correctly." msgstr "la date n'a pas été importée correctement." -#: agenda_culturel/models.py:1960 +#: agenda_culturel/models.py:1964 msgid "during import process" msgstr "pendant le processus d'import" -#: agenda_culturel/models.py:1976 agenda_culturel/models.py:1986 -#: agenda_culturel/models.py:1997 +#: agenda_culturel/models.py:1980 agenda_culturel/models.py:1990 +#: agenda_culturel/models.py:2001 msgid "warning" msgstr "attention" -#: agenda_culturel/models.py:1988 +#: agenda_culturel/models.py:1992 msgid "the title has not been imported correctly." msgstr "le titre n'a pas été importé correctement." -#: agenda_culturel/models.py:2000 +#: agenda_culturel/models.py:2004 msgid "The import was unable to find an event in the page." msgstr "L'import a été incapable de trouver un événement dans la page." -#: agenda_culturel/models.py:2348 +#: agenda_culturel/models.py:2356 msgid "Updated field(s): " msgstr "Champ(s) mis à jour: " -#: agenda_culturel/models.py:2352 +#: agenda_culturel/models.py:2360 msgid "Update" msgstr "Mise à jour" -#: agenda_culturel/models.py:2353 +#: agenda_culturel/models.py:2361 msgid "update process" msgstr "processus de mise à jour" -#: agenda_culturel/models.py:2422 +#: agenda_culturel/models.py:2430 msgid "Import" msgstr "Import" -#: agenda_culturel/models.py:2423 +#: agenda_culturel/models.py:2431 msgid "import process" msgstr "processus d'import" -#: agenda_culturel/models.py:2425 +#: agenda_culturel/models.py:2433 msgid "" "The duration of the event is a little too long for direct publication. " "Moderators can choose to publish it or not." @@ -960,195 +960,199 @@ msgstr "" "La durée de l'événement est un peu trop longue pour qu'il soit publié " "directement. Les modérateurs peuvent choisir de le publier ou non." -#: agenda_culturel/models.py:2684 +#: agenda_culturel/models.py:2692 msgid "From contributor" msgstr "D'un·e contributeurice" -#: agenda_culturel/models.py:2685 +#: agenda_culturel/models.py:2693 msgid "Import process" msgstr "Processus d'import" -#: agenda_culturel/models.py:2686 +#: agenda_culturel/models.py:2694 msgid "Update process" msgstr "Processus de mise à jour" -#: agenda_culturel/models.py:2687 +#: agenda_culturel/models.py:2695 msgid "Contact form" msgstr "Formulaire de contact" -#: agenda_culturel/models.py:2688 +#: agenda_culturel/models.py:2696 msgid "Event report" msgstr "Signalemet d'événement" -#: agenda_culturel/models.py:2691 +#: agenda_culturel/models.py:2699 msgid "From contributor (without message)" msgstr "D'un·e contributeurice (sans message)" -#: agenda_culturel/models.py:2697 +#: agenda_culturel/models.py:2705 msgid "Messages" msgstr "Messages" -#: agenda_culturel/models.py:2706 +#: agenda_culturel/models.py:2714 msgid "Subject" msgstr "Sujet" -#: agenda_culturel/models.py:2707 +#: agenda_culturel/models.py:2715 msgid "The subject of your message" msgstr "Sujet de votre message" -#: agenda_culturel/models.py:2713 +#: agenda_culturel/models.py:2721 msgid "Related event" msgstr "Événement associé" -#: agenda_culturel/models.py:2714 +#: agenda_culturel/models.py:2722 msgid "The message is associated with this event." msgstr "Le message est associé à cet événement." -#: agenda_culturel/models.py:2722 +#: agenda_culturel/models.py:2730 msgid "Author of the message" msgstr "Auteur du message" -#: agenda_culturel/models.py:2730 +#: agenda_culturel/models.py:2738 msgid "Your name" msgstr "Votre nom" -#: agenda_culturel/models.py:2736 +#: agenda_culturel/models.py:2744 msgid "Email address" msgstr "Adresse email" -#: agenda_culturel/models.py:2743 +#: agenda_culturel/models.py:2751 msgid "Your message" msgstr "Votre message" -#: agenda_culturel/models.py:2750 +#: agenda_culturel/models.py:2758 msgid "This message is a spam." msgstr "Ce message est un spam." -#: agenda_culturel/models.py:2757 +#: agenda_culturel/models.py:2765 msgid "this message has been processed and no longer needs to be handled" msgstr "Ce message a été traité et ne nécessite plus d'être pris en charge" -#: agenda_culturel/models.py:2763 +#: agenda_culturel/models.py:2771 msgid "Comments on the message from the moderation team" msgstr "Commentaires sur ce message par l'équipe de modération" -#: agenda_culturel/models.py:2796 agenda_culturel/models.py:2973 +#: agenda_culturel/models.py:2804 agenda_culturel/models.py:2982 msgid "Recurrent import" msgstr "Import récurrent" -#: agenda_culturel/models.py:2797 +#: agenda_culturel/models.py:2805 msgid "Recurrent imports" msgstr "Imports récurrents" -#: agenda_culturel/models.py:2801 +#: agenda_culturel/models.py:2809 msgid "ical" msgstr "ical" -#: agenda_culturel/models.py:2802 +#: agenda_culturel/models.py:2810 msgid "ical no busy" msgstr "ical sans busy" -#: agenda_culturel/models.py:2803 +#: agenda_culturel/models.py:2811 msgid "ical no VC" msgstr "ical sans VC" -#: agenda_culturel/models.py:2804 +#: agenda_culturel/models.py:2812 msgid "ical naive timezone" msgstr "ical timezone naïve" -#: agenda_culturel/models.py:2805 +#: agenda_culturel/models.py:2813 msgid "lacoope.org" msgstr "lacoope.org" -#: agenda_culturel/models.py:2806 +#: agenda_culturel/models.py:2814 msgid "la comédie" msgstr "la comédie" -#: agenda_culturel/models.py:2807 +#: agenda_culturel/models.py:2815 msgid "le fotomat" msgstr "le fotomat" -#: agenda_culturel/models.py:2808 +#: agenda_culturel/models.py:2816 msgid "la puce à l'oreille" msgstr "la puce à loreille" -#: agenda_culturel/models.py:2809 +#: agenda_culturel/models.py:2817 msgid "Plugin wordpress MEC" msgstr "Plugin wordpress MEC" -#: agenda_culturel/models.py:2810 +#: agenda_culturel/models.py:2818 msgid "Événements d'une page FB" msgstr "Événements d'une page FB" -#: agenda_culturel/models.py:2811 +#: agenda_culturel/models.py:2819 msgid "Billetterie Clermont-Ferrand" msgstr "" -#: agenda_culturel/models.py:2812 +#: agenda_culturel/models.py:2820 msgid "Arachnée concert" msgstr "Arachnée concert" -#: agenda_culturel/models.py:2813 +#: agenda_culturel/models.py:2821 msgid "Le Rio" msgstr "Le Rio" -#: agenda_culturel/models.py:2814 +#: agenda_culturel/models.py:2822 msgid "La Raymonde" msgstr "La Raymone" -#: agenda_culturel/models.py:2815 +#: agenda_culturel/models.py:2823 msgid "Agenda apidae tourisme" msgstr "Agenda apidae tourisme" -#: agenda_culturel/models.py:2816 +#: agenda_culturel/models.py:2824 msgid "Agenda iguana (médiathèques)" msgstr "Agenda iguana (médiathèques)" -#: agenda_culturel/models.py:2817 +#: agenda_culturel/models.py:2825 msgid "Mille formes" msgstr "Mille Formes" -#: agenda_culturel/models.py:2818 +#: agenda_culturel/models.py:2826 msgid "Les Amis du Temps des Cerises" msgstr "Les Amis du Temps des Cerises" -#: agenda_culturel/models.py:2819 +#: agenda_culturel/models.py:2827 msgid "Mobilizon" msgstr "Mobilizon" -#: agenda_culturel/models.py:2820 +#: agenda_culturel/models.py:2828 msgid "Le caméléon" msgstr "" -#: agenda_culturel/models.py:2821 +#: agenda_culturel/models.py:2829 msgid "Echosciences" msgstr "" -#: agenda_culturel/models.py:2824 +#: agenda_culturel/models.py:2830 +msgid "Hello Asso" +msgstr "Hello Asso" + +#: agenda_culturel/models.py:2833 msgid "simple" msgstr "simple" -#: agenda_culturel/models.py:2825 +#: agenda_culturel/models.py:2834 msgid "Headless Chromium" msgstr "chromium sans interface" -#: agenda_culturel/models.py:2828 +#: agenda_culturel/models.py:2837 msgid "Headless Chromium (pause)" msgstr "chromium sans interface (pause)" -#: agenda_culturel/models.py:2834 +#: agenda_culturel/models.py:2843 msgid "daily" msgstr "chaque jour" -#: agenda_culturel/models.py:2836 +#: agenda_culturel/models.py:2845 msgid "weekly" msgstr "chaque semaine" -#: agenda_culturel/models.py:2837 +#: agenda_culturel/models.py:2846 msgid "never" msgstr "jamais" -#: agenda_culturel/models.py:2842 +#: agenda_culturel/models.py:2851 msgid "" "Recurrent import name. Be careful to choose a name that is easy to " "understand, as it will be public and displayed on the sites About page." @@ -1156,151 +1160,151 @@ msgstr "" "Nom de l'import récurrent. Attention à choisir un nom compréhensible, car il " "sera public, et affiché sur la page à propos du site." -#: agenda_culturel/models.py:2849 +#: agenda_culturel/models.py:2858 msgid "Processor" msgstr "Processeur" -#: agenda_culturel/models.py:2855 +#: agenda_culturel/models.py:2864 msgid "Downloader" msgstr "Téléchargeur" -#: agenda_culturel/models.py:2862 +#: agenda_culturel/models.py:2871 msgid "Import recurrence" msgstr "Récurrence d'import" -#: agenda_culturel/models.py:2869 +#: agenda_culturel/models.py:2878 msgid "Source" msgstr "Source" -#: agenda_culturel/models.py:2870 +#: agenda_culturel/models.py:2879 msgid "URL of the source document" msgstr "URL du document source" -#: agenda_culturel/models.py:2875 +#: agenda_culturel/models.py:2884 msgid "Browsable url" msgstr "URL navigable" -#: agenda_culturel/models.py:2877 +#: agenda_culturel/models.py:2886 msgid "URL of the corresponding document that will be shown to visitors." msgstr "URL correspondant au document et qui sera montrée aux visiteurs" -#: agenda_culturel/models.py:2886 +#: agenda_culturel/models.py:2895 msgid "Status of each imported event (published or draft)" msgstr "Status de chaque événement importé (publié ou brouillon)" -#: agenda_culturel/models.py:2891 +#: agenda_culturel/models.py:2900 msgid "Address for each imported event" msgstr "Adresse de chaque événement importé" -#: agenda_culturel/models.py:2898 +#: agenda_culturel/models.py:2907 msgid "Force location" msgstr "Focer la localisation" -#: agenda_culturel/models.py:2899 +#: agenda_culturel/models.py:2908 msgid "force location even if another is detected." msgstr "Forcer la localisation même si une autre a été détectée." -#: agenda_culturel/models.py:2905 +#: agenda_culturel/models.py:2914 msgid "Organiser" msgstr "Organisation" -#: agenda_culturel/models.py:2906 +#: agenda_culturel/models.py:2915 msgid "Organiser of each imported event" msgstr "Organisme à l'origine de chaque événement importé" -#: agenda_culturel/models.py:2916 +#: agenda_culturel/models.py:2925 msgid "Category of each imported event" msgstr "Catégorie de chaque événement importé" -#: agenda_culturel/models.py:2924 +#: agenda_culturel/models.py:2933 msgid "Tags for each imported event" msgstr "Étiquettes de chaque événement importé" -#: agenda_culturel/models.py:2925 +#: agenda_culturel/models.py:2934 msgid "A list of tags that describe each imported event." msgstr "Une liste d'étiquettes décrivant chaque événement importé" -#: agenda_culturel/models.py:2954 +#: agenda_culturel/models.py:2963 msgid "Running" msgstr "En cours" -#: agenda_culturel/models.py:2955 +#: agenda_culturel/models.py:2964 msgid "Canceled" msgstr "Annulé" -#: agenda_culturel/models.py:2956 +#: agenda_culturel/models.py:2965 msgid "Success" msgstr "Succès" -#: agenda_culturel/models.py:2957 +#: agenda_culturel/models.py:2966 msgid "Failed" msgstr "Erreur" -#: agenda_culturel/models.py:2960 +#: agenda_culturel/models.py:2969 msgid "Batch importation" msgstr "Importation par lot" -#: agenda_culturel/models.py:2961 +#: agenda_culturel/models.py:2970 msgid "Batch importations" msgstr "Importations par lot" -#: agenda_culturel/models.py:2974 +#: agenda_culturel/models.py:2983 msgid "Reference to the recurrent import processing" msgstr "Référence du processus d'import récurrent" -#: agenda_culturel/models.py:2982 +#: agenda_culturel/models.py:2991 msgid "URL (if not recurrent import)" msgstr "URL (si pas d'import récurrent)" -#: agenda_culturel/models.py:2983 +#: agenda_culturel/models.py:2992 msgid "Source URL if no RecurrentImport is associated." msgstr "URL source si aucun import récurrent n'est associé" -#: agenda_culturel/models.py:2998 +#: agenda_culturel/models.py:3007 msgid "Error message" msgstr "Votre message" -#: agenda_culturel/models.py:3002 +#: agenda_culturel/models.py:3011 msgid "Number of collected events" msgstr "Nombre d'événements collectés" -#: agenda_culturel/models.py:3005 +#: agenda_culturel/models.py:3014 msgid "Number of imported events" msgstr "Nombre d'événements importés" -#: agenda_culturel/models.py:3008 +#: agenda_culturel/models.py:3017 msgid "Number of updated events" msgstr "Nombre d'événements mis à jour" -#: agenda_culturel/models.py:3011 +#: agenda_culturel/models.py:3020 msgid "Number of removed events" msgstr "Nombre d'événements supprimés" -#: agenda_culturel/models.py:3019 +#: agenda_culturel/models.py:3028 msgid "Weight" msgstr "Poids" -#: agenda_culturel/models.py:3020 +#: agenda_culturel/models.py:3029 msgid "The lower is the weight, the earlier the filter is applied" msgstr "Plus le poids est léger, plus le filtre sera appliqué tôt" -#: agenda_culturel/models.py:3027 +#: agenda_culturel/models.py:3036 msgid "Category applied to the event" msgstr "Catégorie appliquée à l'événement" -#: agenda_culturel/models.py:3032 +#: agenda_culturel/models.py:3041 msgid "Contained in the title" msgstr "Contenu dans le titre" -#: agenda_culturel/models.py:3033 +#: agenda_culturel/models.py:3042 msgid "Text contained in the event title" msgstr "Texte contenu dans le titre de l'événement" -#: agenda_culturel/models.py:3039 +#: agenda_culturel/models.py:3048 msgid "Exact title extract" msgstr "Extrait exact du titre" -#: agenda_culturel/models.py:3041 +#: agenda_culturel/models.py:3050 msgid "" "If checked, the extract will be searched for in the title using the exact " "form (capitals, accents)." @@ -1308,19 +1312,19 @@ msgstr "" "Si coché, l'extrait sera recherché dans le titre en utilisant la forme " "exacte (majuscules, accents)" -#: agenda_culturel/models.py:3047 +#: agenda_culturel/models.py:3056 msgid "Contained in the description" msgstr "Contenu dans la description" -#: agenda_culturel/models.py:3048 +#: agenda_culturel/models.py:3057 msgid "Text contained in the description" msgstr "Texte contenu dans la description" -#: agenda_culturel/models.py:3054 +#: agenda_culturel/models.py:3063 msgid "Exact description extract" msgstr "Extrait exact de description" -#: agenda_culturel/models.py:3056 +#: agenda_culturel/models.py:3065 msgid "" "If checked, the extract will be searched for in the description using the " "exact form (capitals, accents)." @@ -1328,19 +1332,19 @@ msgstr "" "Si coché, l'extrait sera recherché dans la description en utilisant la forme " "exacte (majuscules, accents)" -#: agenda_culturel/models.py:3062 +#: agenda_culturel/models.py:3071 msgid "Contained in the location" msgstr "Contenu dans la localisation" -#: agenda_culturel/models.py:3063 +#: agenda_culturel/models.py:3072 msgid "Text contained in the event location" msgstr "Texte contenu dans la localisation de l'événement" -#: agenda_culturel/models.py:3069 +#: agenda_culturel/models.py:3078 msgid "Exact location extract" msgstr "Extrait exact de localisation" -#: agenda_culturel/models.py:3071 +#: agenda_culturel/models.py:3080 msgid "" "If checked, the extract will be searched for in the location using the exact " "form (capitals, accents)." @@ -1348,55 +1352,55 @@ msgstr "" "Si coché, l'extrait sera recherché dans la localisation en utilisant la " "forme exacte (majuscules, accents)" -#: agenda_culturel/models.py:3079 +#: agenda_culturel/models.py:3088 msgid "Location from place" msgstr "Localisation depuis le lieu" -#: agenda_culturel/models.py:3088 +#: agenda_culturel/models.py:3097 msgid "Categorisation rule" msgstr "Règle de catégorisation" -#: agenda_culturel/models.py:3089 +#: agenda_culturel/models.py:3098 msgid "Categorisation rules" msgstr "Règles de catégorisation" -#: agenda_culturel/models.py:3163 +#: agenda_culturel/models.py:3172 msgid "Period name" msgstr "Nom de la période" -#: agenda_culturel/models.py:3168 +#: agenda_culturel/models.py:3177 msgid "Special period" msgstr "Période remarquable" -#: agenda_culturel/models.py:3169 +#: agenda_culturel/models.py:3178 msgid "Special periods" msgstr "Périodes remarquables" -#: agenda_culturel/models.py:3175 +#: agenda_culturel/models.py:3184 msgid "public holidays" msgstr "Jour férié" -#: agenda_culturel/models.py:3176 +#: agenda_culturel/models.py:3185 msgid "school vacations" msgstr "Vacances scolaires" -#: agenda_culturel/models.py:3194 +#: agenda_culturel/models.py:3203 msgid "The end date must be after or equal to the start date." msgstr "La date de fin doit être après ou identique à la date de début." -#: agenda_culturel/models.py:3202 +#: agenda_culturel/models.py:3211 msgid " on " msgstr " du " -#: agenda_culturel/models.py:3205 +#: agenda_culturel/models.py:3214 msgid " from " msgstr " du " -#: agenda_culturel/models.py:3205 +#: agenda_culturel/models.py:3214 msgid " to " msgstr " au " -#: agenda_culturel/settings/base.py:184 +#: agenda_culturel/settings/base.py:188 msgid "French" msgstr "français" @@ -1668,17 +1672,17 @@ msgstr "" "par défaut ont été catégorisés" #: agenda_culturel/views.py:2495 agenda_culturel/views.py:2557 -#: agenda_culturel/views.py:2597 +#: agenda_culturel/views.py:2601 msgid "{} events have been updated." msgstr "{} événements ont été mis à jour." #: agenda_culturel/views.py:2498 agenda_culturel/views.py:2559 -#: agenda_culturel/views.py:2600 +#: agenda_culturel/views.py:2604 msgid "1 event has been updated." msgstr "1 événement a été mis à jour" #: agenda_culturel/views.py:2500 agenda_culturel/views.py:2561 -#: agenda_culturel/views.py:2602 +#: agenda_culturel/views.py:2606 msgid "No events have been modified." msgstr "Aucun événement n'a été modifié." @@ -1690,37 +1694,36 @@ msgstr "Le lieu a été modifié avec succès." msgid "The place has been successfully created." msgstr "Le lieu a été créé avec succès." -#: agenda_culturel/views.py:2584 +#: agenda_culturel/views.py:2586 msgid "" "The selected place has been assigned to the event {}." -msgstr "" -"Le lieu sélectionné a été assigné à l'événement {}." +msgstr "Le lieu sélectionné a été assigné à l'événement {}." -#: agenda_culturel/views.py:2589 +#: agenda_culturel/views.py:2593 msgid "A new alias has been added to the selected place." msgstr "Un nouvel alias a été créé pour le lieu sélectionné." -#: agenda_culturel/views.py:2716 +#: agenda_culturel/views.py:2720 msgid "The organisation has been successfully updated." msgstr "L'organisme a été modifié avec succès." -#: agenda_culturel/views.py:2723 +#: agenda_culturel/views.py:2727 msgid "The organisation has been successfully created." msgstr "L'organisme a été créé avec succès." -#: agenda_culturel/views.py:2742 +#: agenda_culturel/views.py:2746 msgid "The tag has been successfully updated." msgstr "L'étiquette a été modifiée avec succès." -#: agenda_culturel/views.py:2749 +#: agenda_culturel/views.py:2753 msgid "The tag has been successfully created." msgstr "L'étiquette a été créée avec succès." -#: agenda_culturel/views.py:2849 +#: agenda_culturel/views.py:2853 msgid "You have not modified the tag name." msgstr "Vous n'avez pas modifié le nom de l'étiquette." -#: agenda_culturel/views.py:2864 +#: agenda_culturel/views.py:2868 msgid "" "This tag {} is already in use, and is described by different information " "from the current tag. You can force renaming by checking the corresponding " @@ -1733,7 +1736,7 @@ msgstr "" "sera supprimée, et tous les événements associés à l'étiquette {} seront " "associés à l'étiquette {}." -#: agenda_culturel/views.py:2878 +#: agenda_culturel/views.py:2882 msgid "" "This tag {} is already in use. You can force renaming by checking the " "corresponding option." @@ -1741,55 +1744,55 @@ msgstr "" "Cette étiquette {} est déjà utilisée. Vous pouvez forcer le renommage en " "cochant l'option correspondante." -#: agenda_culturel/views.py:2911 +#: agenda_culturel/views.py:2915 msgid "The tag {} has been successfully renamed to {}." msgstr "L'étiquette {} a été renommée avec succès en {}." -#: agenda_culturel/views.py:2953 +#: agenda_culturel/views.py:2957 msgid "The tag {} has been successfully deleted." msgstr "L'événement {} a été supprimé avec succès." -#: agenda_culturel/views.py:3100 +#: agenda_culturel/views.py:3104 msgid "Cache successfully cleared." msgstr "Le cache a été vidé avec succès." -#: agenda_culturel/views.py:3120 +#: agenda_culturel/views.py:3124 msgid "Your user profile has been successfully modified." msgstr "Votre profil utilisateur a été modifié avec succès." -#: agenda_culturel/views.py:3138 +#: agenda_culturel/views.py:3142 msgid "The special period has been successfully created." msgstr "La période remarquable a été créée avec succès." -#: agenda_culturel/views.py:3159 +#: agenda_culturel/views.py:3163 msgid "The special period has been successfully deleted." msgstr "La période remarquable a été supprimée avec succès." -#: agenda_culturel/views.py:3170 +#: agenda_culturel/views.py:3174 msgid "The special period has been successfully updated." msgstr "La période remarquable a été modifiée avec succès." -#: agenda_culturel/views.py:3187 +#: agenda_culturel/views.py:3191 #, python-format msgid "%(nb_created)d interval inserted." msgid_plural "%(nb_created)d intervals inserted." msgstr[0] "%(nb_created)d intervalle inséré." msgstr[1] "%(nb_created)d intervalles insérés." -#: agenda_culturel/views.py:3198 +#: agenda_culturel/views.py:3202 #, python-format msgid "%(nb_overlap)d insersion was not possible due to overlap." msgid_plural "%(nb_overlap)d insersion were not possible due to overlap." msgstr[0] "%(nb_overlap)d insersion impossible à cause d'une intersection." msgstr[1] "%(nb_overlap)d insersions impossibles à cause d'une intersection." -#: agenda_culturel/views.py:3209 +#: agenda_culturel/views.py:3213 #, python-format msgid "%(nb_error)d error while reading ical file." msgid_plural "%(nb_error)d error while reading ical file." msgstr[0] "%(nb_error)d erreur pendant la lecture du fichier ical." msgstr[1] "%(nb_error)d erreurs pendant la lecture du fichier ical." -#: agenda_culturel/views.py:3217 +#: agenda_culturel/views.py:3221 msgid "Error during file reading: {}" msgstr "Erreur pendant la lecture du fichier: {}" diff --git a/src/agenda_culturel/migrations/0167_alter_recurrentimport_processor.py b/src/agenda_culturel/migrations/0167_alter_recurrentimport_processor.py new file mode 100644 index 0000000..3c8473b --- /dev/null +++ b/src/agenda_culturel/migrations/0167_alter_recurrentimport_processor.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.19 on 2025-04-12 14:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('agenda_culturel', '0166_specialperiod_agenda_cult_start_d_40e6a4_idx'), + ] + + operations = [ + migrations.AlterField( + model_name='recurrentimport', + name='processor', + field=models.CharField(choices=[('ical', 'ical'), ('icalnobusy', 'ical no busy'), ('icalnovc', 'ical no VC'), ('ical naive tz', 'ical naive timezone'), ('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"), ('Billetterie CF', 'Billetterie Clermont-Ferrand'), ('arachnee', 'Arachnée concert'), ('rio', 'Le Rio'), ('raymonde', 'La Raymonde'), ('apidae', 'Agenda apidae tourisme'), ('iguana', 'Agenda iguana (médiathèques)'), ('Mille formes', 'Mille formes'), ('Amis cerises', 'Les Amis du Temps des Cerises'), ('Mobilizon', 'Mobilizon'), ('Le Caméléon', 'Le caméléon'), ('Echosciences', 'Echosciences'), ('HelloAsso', 'Hello Asso')], default='ical', max_length=20, verbose_name='Processor'), + ), + ] diff --git a/src/agenda_culturel/models.py b/src/agenda_culturel/models.py index 430756e..dcf3223 100644 --- a/src/agenda_culturel/models.py +++ b/src/agenda_culturel/models.py @@ -1717,6 +1717,11 @@ class Event(models.Model): self.update_recurrence_dtstartend() + # clear cache + for is_auth in [False, True]: + key = make_template_fragment_key("event_body", [is_auth, self.pk]) + cache.delete(key) + # if the image is defined but not locally downloaded if self.image and ( not self.local_image or not default_storage.exists(self.local_image.name) @@ -1835,11 +1840,6 @@ class Event(models.Model): if "request" in kwargs: self.notify_if_required(kwargs.get("request")) - # clear cache - for is_auth in [False, True]: - key = make_template_fragment_key("event_body", [is_auth, self.pk]) - cache.delete(key) - # save message if required if self.has_message(): for msg in self.get_messages(): @@ -2053,8 +2053,9 @@ class Event(models.Model): def get_updateable_uuid(self): if self.uuids and len(self.uuids) > 0: for s in self.uuids: - if FacebookEventExtractor.is_known_url(s): - return s + for e in Extractor.get_default_extractors(True): + if e.is_known_url(s): + return s return None def is_updateable(self): @@ -2827,6 +2828,7 @@ class RecurrentImport(models.Model): MOBILIZON = "Mobilizon", _("Mobilizon") LECAMELEON = "Le Caméléon", _("Le caméléon") ECHOSCIENCES = "Echosciences", _("Echosciences") + HELLOASSO = "HelloAsso", _("Hello Asso") class DOWNLOADER(models.TextChoices): SIMPLE = "simple", _("simple") diff --git a/src/agenda_culturel/settings/base.py b/src/agenda_culturel/settings/base.py index 48423f2..2baf7db 100644 --- a/src/agenda_culturel/settings/base.py +++ b/src/agenda_culturel/settings/base.py @@ -37,6 +37,10 @@ SERVER_EMAIL = os_getenv("SERVER_EMAIL", "") ECHOSCIENCES_TOKEN = os_getenv("ECHOSCIENCES_TOKEN", "") +HELLOASSO_ID = os_getenv("HELLOASSO_ID", "") +HELLOASSO_SECRET = os_getenv("HELLOASSO_SECRET", "") + + # Application definition INSTALLED_APPS = [