GeotrekCE / Geotrek-admin

Paths management for National Parks and Tourism organizations
https://geotrek.fr
BSD 2-Clause "Simplified" License
135 stars 76 forks source link

Event Apidae Import error #1956

Open Cynthia-Borot-PNE opened 5 years ago

Cynthia-Borot-PNE commented 5 years ago

Bonjour,

Nous essayons d'importer par héritage de la class TouristicEventApidaeParser, des animations d'Apidae.

Création de la class AnimationParser dans bulkimport/parser.py :

class AnimationParser(TouristicEventApidaeParser):
    label = u"Animations PNE"
    api_key = 'xxxxxx'
    project_id = xxx
    selection_id = xxx
    source = ['Apidae']
    portal = ['Rando Ecrins']
    delete = True

    def parse_obj(self, row, operation):
        if not self.obj.pk:
            self.obj.published = True
        super(AnimationParser, self).parse_obj(row, operation)

A l'execution de l'import

./bin/django import bulkimport.parsers.AnimationParser

Nous obtenons l'erreur suivante :

[...] File "/home/geotrek/Geotrek-admin-2.27.1/geotrek/common/management/commands/import.py", line 41, in handle parser.parse(options['shapefile'], limit=limit) File "/home/geotrek/Geotrek-admin-2.27.1/geotrek/common/parsers.py", line 443, in parse for i, row in enumerate(self.next_row()): File "/home/geotrek/Geotrek-admin-2.27.1/geotrek/tourism/parsers.py", line 83, in next_row for row in self.items: File "/home/geotrek/Geotrek-admin-2.27.1/geotrek/tourism/parsers.py", line 65, in items return self.root['objetsTouristiques'] KeyError: 'objetsTouristiques'

En effet la clé 'objetsTouristiques' dont fait référence la méthode ApidaeParser.getItems() n'existe dans aucune des classes héritées.

babastienne commented 10 months ago

Problème toujours d'actualité @camillemonchicourt ?

De notre côté on a des parsers APIDAE Agenda qui fonctionnent très bien sans avoir à surcharger le code particulièrement :

Code source

```python class TouristicEventsParser(TouristicEventApidaeParser): api_key = 'XXXXXXXX' project_id = 1234 selection_id = 12345 label = "Manifestations Sport et Nature" def __init__(self, *args, **kwarg): super(TouristicEventsParser, self).__init__(*args, **kwarg) # Do not import themes del self.m2m_fields['themes'] ```

Si le problème existe toujours chez vous, peut-être faut-il surcharger plus finement la classe et en particulier la correspondance des champs geotrek <> apidae ? Voici un extrait de classe de Parser créant des evenements issus d'APIDAE plus complète qu'on utilise pour un parc :

Code source

```python class PNRXXAgendaParser(TouristicEventApidaeParser): api_key = "XXXXXXXXX" project_id = 1234 selection_id = 12345 label = u"Agenda" delete = True constant_fields = { 'published': True, 'approved': False, } responseFields = [ 'id', 'nom', 'ouverture', 'informationsFeteEtManifestation', 'presentation.descriptifCourt', 'presentation.descriptifDetaille', 'localisation.adresse', 'localisation.geolocalisation.geoJson.coordinates', 'localisation.geolocalisation.complement.libelleFr', 'informations.moyensCommunication', 'informations.structureGestion.nom.libelleFr', 'descriptionTarif.tarifsEnClair', 'descriptionTarif.modesPaiement', 'prestations', 'gestion.dateModification', 'gestion.membreProprietaire.nom', 'illustrations', 'informationsCommerceEtService', 'informationsDegustation', 'criteresInternes' ] fields = { 'name': 'nom.libelleFr', 'eid': 'id', 'description_teaser': 'presentation.descriptifCourt.libelleFr', 'description': ('presentation.descriptifDetaille.libelleFr', "ouverture.periodeEnClair.libelleFr", 'prestations.labelsTourismeHandicap.*.elementReferenceType', 'informationsFeteEtManifestation.themes.*.libelleFr', 'ouverture.periodesOuvertures'), 'begin_date': ('ouverture.periodesOuvertures'), 'end_date': ('ouverture.periodesOuvertures'), 'start_time': ('ouverture.periodesOuvertures'), 'end_time': ('ouverture.periodesOuvertures'), 'contact': ('localisation.adresse.adresse1', 'localisation.adresse.adresse2', 'localisation.adresse.adresse3', 'localisation.adresse.codePostal', 'localisation.adresse.commune.nom', 'informations.moyensCommunication'), 'geom': 'localisation.geolocalisation.geoJson.coordinates', 'type': 'informationsFeteEtManifestation.typesManifestation.*.libelleFr', 'organizer': ('criteresInternes.*.libelle'), } def filter_contact(self, src, val): (address1, address2, address3, zipCode, commune, comm) = val tel = self.filter_comm(comm, 201, multiple=True) mail = self.filter_comm(comm, 204, multiple=True) web = self.filter_comm(comm, 205, multiple=True) fax = self.filter_comm(comm, 202, multiple=True) if tel: tel = "Tél. " + tel if mail: mail = "Mail " + mail if web: web = "Site web " + f"{web}" if fax: fax = "Fax " + fax lines = [line for line in [ address1, address2, address3, ' '.join([part for part in [zipCode, commune] if part]), tel, mail, web, fax ] if line] return '
'.join(lines) def filter_comm(self, val, code, multiple=True): if not val: return None vals = [subval['coordonnees']['fr'] for subval in val if subval['type']['id'] == code] if multiple: return ' / '.join(vals) if vals: return vals[0] return None def filter_attachments(self, src, val): dst = [] for atta in val: url = atta.get("traductionFichiers", None)[0].get("url", None) if url is None: continue legend = atta.get("nom", None) if legend is not None: legend = legend.get("libelleFr", None) author = atta.get("copyright", None) if author is not None: author = author.get("libelleFr", None) dst.append((url, legend, author)) return dst def filter_type(self, src, val): type = None if val: for label in val: if label == "Manifestations commerciales": type, _ = TouristicEventType.objects.get_or_create(type="Marchés") elif label in ["Sports", "Nature et détente"]: type, _ = TouristicEventType.objects.get_or_create(type="Manifestations sportives") elif label in ["Traditions et folklore", "Culture", "Distractions et loisirs"]: type, _ = TouristicEventType.objects.get_or_create(type="Manifestations culturelles") else: type, _ = TouristicEventType.objects.get_or_create(type=label) return type def filter_organizer(self, src, val): partenariats = val orga = None if partenariats: for libelle in partenariats: if libelle == "Animation Parc" or libelle == "Partenaire du Parc": orga, _ = TouristicEventOrganizer.objects.get_or_create(label=libelle) return orga def filter_begin_date(self, src, val): begin_date_to_keep = None end_date_to_keep = None if val: if len(val) == 1: val = val[0] begin_date = val.get('dateDebut', None) if begin_date: parsed_begin_date = datetime.strptime(begin_date, "%Y-%m-%d") end_date = val.get('dateFin', None) if end_date: parsed_end_date = datetime.strptime(end_date, "%Y-%m-%d") time = val.get('horaireOuverture', None) if time: parsed_time = datetime.strptime(time, "%H:%M:%S").time() begin_date_to_keep = parsed_begin_date else: for subval in val: begin_date = subval.get('dateDebut', None) if begin_date: parsed_begin_date = datetime.strptime(begin_date, "%Y-%m-%d") end_date = subval.get('dateFin', None) if end_date: parsed_end_date = datetime.strptime(end_date, "%Y-%m-%d") if parsed_end_date.date() > date.today(): # Keep these ones as next event begin_date_to_keep = parsed_begin_date end_date_to_keep = parsed_end_date time = subval.get('horaireOuverture', None) if time: parsed_time = datetime.strptime(time, "%H:%M:%S").time() break if not begin_date_to_keep: raise ValueImportError("Empty begin_date") return begin_date_to_keep def filter_end_time(self, src, val): end_time_to_keep = None end_date_to_keep = None if val: if len(val) == 1: val = val[0] begin_date = val.get('dateDebut', None) if begin_date: parsed_begin_date = datetime.strptime(begin_date, "%Y-%m-%d") end_date = val.get('dateFin', None) if end_date: parsed_end_date = datetime.strptime(end_date, "%Y-%m-%d") time = val.get('horaireFermeture', None) if time: parsed_time = datetime.strptime(time, "%H:%M:%S").time() end_time_to_keep = parsed_time begin_date_to_keep = parsed_begin_date end_date_to_keep = parsed_end_date else: for subval in val: begin_date = subval.get('dateDebut', None) if begin_date: parsed_begin_date = datetime.strptime(begin_date, "%Y-%m-%d") end_date = subval.get('dateFin', None) if end_date: parsed_end_date = datetime.strptime(end_date, "%Y-%m-%d") if parsed_end_date.date() > date.today(): # Keep these ones as next event begin_date_to_keep = parsed_begin_date end_date_to_keep = parsed_end_date time = subval.get('horaireFermeture', None) if time: parsed_time = datetime.strptime(time, "%H:%M:%S").time() end_time_to_keep = parsed_time break return end_time_to_keep def filter_end_date(self, src, val): begin_date_to_keep = None end_date_to_keep = None if val: if len(val) == 1: val = val[0] begin_date = val.get('dateDebut', None) if begin_date: parsed_begin_date = datetime.strptime(begin_date, "%Y-%m-%d") end_date = val.get('dateFin', None) if end_date: parsed_end_date = datetime.strptime(end_date, "%Y-%m-%d") time = val.get('horaireOuverture', None) if time: parsed_time = datetime.strptime(time, "%H:%M:%S").time() begin_date_to_keep = parsed_begin_date end_date_to_keep = parsed_end_date else: for subval in val: begin_date = subval.get('dateDebut', None) if begin_date: parsed_begin_date = datetime.strptime(begin_date, "%Y-%m-%d") end_date = subval.get('dateFin', None) if end_date: parsed_end_date = datetime.strptime(end_date, "%Y-%m-%d") if parsed_end_date.date() > date.today(): # Keep these ones as next event begin_date_to_keep = parsed_begin_date end_date_to_keep = parsed_end_date time = subval.get('horaireOuverture', None) if time: parsed_time = datetime.strptime(time, "%H:%M:%S").time() break return end_date_to_keep def filter_start_time(self, src, val): start_time_to_keep = None end_date_to_keep = None if val: if len(val) == 1: val = val[0] begin_date = val.get('dateDebut', None) if begin_date: parsed_begin_date = datetime.strptime(begin_date, "%Y-%m-%d") end_date = val.get('dateFin', None) if end_date: parsed_end_date = datetime.strptime(end_date, "%Y-%m-%d") time = val.get('horaireOuverture', None) if time: parsed_time = datetime.strptime(time, "%H:%M:%S").time() start_time_to_keep = parsed_time begin_date_to_keep = parsed_begin_date end_date_to_keep = parsed_end_date else: for subval in val: begin_date = subval.get('dateDebut', None) if begin_date: parsed_begin_date = datetime.strptime(begin_date, "%Y-%m-%d") end_date = subval.get('dateFin', None) if end_date: parsed_end_date = datetime.strptime(end_date, "%Y-%m-%d") if parsed_end_date.date() > date.today(): # Keep these ones as next event begin_date_to_keep = parsed_begin_date end_date_to_keep = parsed_end_date time = subval.get('horaireOuverture', None) if time: parsed_time = datetime.strptime(time, "%H:%M:%S").time() start_time_to_keep = parsed_time break return start_time_to_keep def filter_description(self, src, val): desc, ouverture, label, themes, ouverture_parsed = val desc_txt="" if desc: desc_txt = '
'.join(desc.splitlines()) next_date = "" meeting_time_txt = "" end_date_txt = "" end_time_txt = "" begin_date_txt = "" ouverture_txt = "" label_txt = "" themes_txt = "" if ouverture: ouverture_txt = "
Ouverture:
" + "
".join(ouverture.splitlines()) + "
" if label: label_txt = "
Label Tourisme et Handicap:
" + "
Oui
" if themes: themes_txt = "
Thèmes:
" + "
".join(themes) + "
" if ouverture_parsed: for subval in ouverture_parsed: begin_date = subval.get('dateDebut', None) if begin_date: parsed_begin_date = datetime.strptime(begin_date, "%Y-%m-%d") end_date = subval.get('dateFin', None) if end_date: parsed_end_date = datetime.strptime(end_date, "%Y-%m-%d") if parsed_end_date.date() > date.today(): continue begin_date_txt = f"{parsed_begin_date.day}/{parsed_begin_date.month}/{parsed_begin_date.year}" end_date = subval.get('dateFin', None) if end_date: parsed_end_date = datetime.strptime(end_date, "%Y-%m-%d") if parsed_end_date.date() > date.today(): continue end_date_txt = f"{parsed_end_date.day}/{parsed_end_date.month}/{parsed_end_date.year}" begin_date = subval.get('dateDebut', None) if begin_date: parsed_begin_date = datetime.strptime(begin_date, "%Y-%m-%d") if parsed_begin_date.date() > date.today(): continue time = subval.get('horaireOuverture', None) if time: parsed_time = datetime.strptime(time, "%H:%M:%S").time() meeting_time_txt = str(parsed_time) end_time = subval.get('horaireFermeture', None) if end_time: parsed_end_time = datetime.strptime(end_time, "%H:%M:%S").time() end_time_txt = str(parsed_time) if meeting_time_txt and begin_date_txt and end_date_txt: next_date = f"Prochain évènement du {begin_date_txt} à {meeting_time_txt} au {end_date_txt}" if end_time_txt: next_date = next_date + f" à {end_time_txt}" elif begin_date_txt and end_date_txt: next_date = f"Prochain évènement du {begin_date_txt} au {end_date_txt}" elif meeting_time_txt and begin_date_txt: next_date = f"Prochain évènement le {begin_date_txt} à {meeting_time_txt}" lines = [line for line in [desc_txt, ouverture_txt, next_date, themes_txt, label_txt] if line] return '
'.join(lines) ```

camillemonchicourt commented 9 months ago

OK merci pour ces exemples intéressants. On n'utilise pas actuellement les imports d’événements depuis Apidae, donc je n'ai pas de retour sur le fonctionnement.

Mais OK si ça fonctionne pour vous. Peut-être mentionner la possibilité d'importer des événements Apidae dans Geotrek dans la doc d'import avec le premier exemple et un lien vers ce ticket pour l'exemple plus spécifique ?

xavyeah39 commented 4 months ago

Salut,

On a rencontré un soucis au PNR des Volcans d'Auvergne avec l'import via parser des Touristic Events depuis Apidae. Le parser en question est construit sur le même modèle que le 2ème exemple custom que tu cite dans ton dernier message de cette issue @babastienne.

L'exécution du parser PNRXXAgendaParser lèvait l'erreur suivante :

  File "/opt/geotrek-admin/lib/python3.8/site-packages/django/db/models/options.py", line 610, in get_field
    raise FieldDoesNotExist("%s has no field named '%s'" % (self.object_name, field_name))
django.core.exceptions.FieldDoesNotExist: TouristicEvent has no field named 'organizer'

J'ai déduis que cela semble lié à l'implémentation d'une relation many to many sur les organizers des touristic-events depuis la v2.102.2 de GTA (#3587)

J'ai donc simplement commenté la ligne 'organizer': ('criteresInternes.*.libelle'), dans fields ce qui semble solutionner le problème.

J'avais tout de même une autre erreur de levée : UnboundLocalError : local variable 'parsed_time' referenced before assignment

J'ai donc aussi adapté les lignes suivantes :

if end_time:
    parsed_end_time = datetime.strptime(end_time, "%H:%M:%S").time()
    end_time_txt = str(parsed_time)

En remplaçant str(parsed_time) par str(parsed_end_time) , l'import va désormais au bout sans erreur.

Les organizers et les relations aux events ainsi que les dates de fin me semblent correctement intégrées dans la BDD et remontent bien dans GTRv3 (sans mails d'erreur de l'API) mais pourriez-vous me confirmer que ces adaptations sont pertinentes et suffisantes ?

Merci d'avance !

babastienne commented 3 months ago

Salut @xavyeah39

Le champ organizer a en effet été passé en Many2Many et par la même occasion renommé en organizers.

Tes modifications semblent tout à fait correctes, pour moi c'est ok.

Merci pour le commentaire qui servira sûrement pour d'autres personnes. :pray:

babastienne commented 3 months ago

@xavyeah39 essaye dans ton parser d'ajouter la ligne suivante ?

m2m_fields = {
    'themes': 'informationsFeteEtManifestation.themes.*.libelleFr',
    'organizers': ('criteresInternes.*.libelle',),
}

Si tu relances est-ce que ça permet d'avoir des valeurs différentes pour le champ 'organisateur' ?

xavyeah39 commented 3 months ago

Merci @babastienne.

Pour mémo du problème rencontré au PNR des Volcans d'Auvergne :

En effet pour corriger l’exécution du parser PNRXXAgendaParser suite au passage du champ organizer en many2many, j'ai commenté la ligne 'organizer': ('criteresInternes.*.libelle'), dans fields. Mais du coup, le peuplement des organisateurs avec ce nouveaux fonctionnement n'exploite plus le champ criteresInternes renseigné dans Apidae qui nous permettait de simplifier les organisateurs selon 2 valeurs : "Partenaires du Parc" et "Animation Parc".

Il faudrait donc adapter le parser pour permettre cela dans la relation m2m implémentée en peuplant non plus le champ "organizer" mais les tables tourism_touristiceventorganizer et tourism_touristicevent_organizers".

J'ai ajouté la ligne m2m_fields que tu indiques mais j'ai l'erreur suivante désormais :

===== Run PNRVAAgendaParser parser Read env configuration from /opt/geotrek-admin/lib/python3.8/site-packages/geotrek/settings/env_prod.py Read custom configuration from /opt/geotrek-admin/var/conf/custom.py 0001: 4640813 (00%) 0002: 4640814 (00%) 0003: 4640815 (00%) Traceback (most recent call last): File "/usr/sbin/geotrek", line 20, in execute_from_command_line(sys.argv) File "/opt/geotrek-admin/lib/python3.8/site-packages/django/core/management/init.py", line 442, in execute_from_command_line utility.execute() File "/opt/geotrek-admin/lib/python3.8/site-packages/django/core/management/init.py", line 436, in execute self.fetch_command(subcommand).run_from_argv(self.argv) File "/opt/geotrek-admin/lib/python3.8/site-packages/django/core/management/base.py", line 412, in run_from_argv self.execute(*args, *cmd_options) File "/opt/geotrek-admin/lib/python3.8/site-packages/django/core/management/base.py", line 458, in execute output = self.handle(args, options) File "/opt/geotrek-admin/lib/python3.8/site-packages/geotrek/common/management/commands/import.py", line 55, in handle parser.parse(options['filename'], limit=limit) File "/opt/geotrek-admin/lib/python3.8/site-packages/geotrek/common/parsers.py", line 545, in parse self.parse_row(row) File "/opt/geotrek-admin/lib/python3.8/site-packages/geotrek/common/parsers.py", line 407, in parse_row self.parse_obj(row, operation) File "/opt/geotrek-admin/lib/python3.8/site-packages/geotrek/common/parsers.py", line 342, in parse_obj update_fields += self.parse_fields(row, self.m2m_fields) File "/opt/geotrek-admin/lib/python3.8/site-packages/geotrek/common/parsers.py", line 318, in parse_fields self.parse_field(row, dst, src, updated, non_field) File "/opt/geotrek-admin/lib/python3.8/site-packages/geotrek/common/parsers.py", line 307, in parse_field modified = self.parse_real_field(dst, src, val) File "/opt/geotrek-admin/lib/python3.8/site-packages/geotrek/common/parsers.py", line 230, in parse_real_field val = self.apply_filter(dst, src, val) File "/opt/geotrek-admin/lib/python3.8/site-packages/geotrek/common/parsers.py", line 194, in apply_filter val = self.filter_m2m(src, val, to, natural_key, kwargs) File "/opt/geotrek-admin/lib/python3.8/site-packages/geotrek/common/parsers.py", line 484, in filter_m2m self.addwarning(("{model} '{val}' did not exist in Geotrek-Admin and was automatically created").format(model=model._meta.verbose_name.title(), val=subval)) TypeError: str returned non-string (type list)

Merci !