-
Notifications
You must be signed in to change notification settings - Fork 77
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Importer itinéraires dans Geotrek via Tourinsoft #2534
Comments
Oui pouvoir importer des itinéraires directement depuis une source externe, est un sujet souvent évoqué, ça intéresse beaucoup de monde. Ca n'a jamais été réellement creusé à ma connaissance, car c'est assez complexe du fait de la segmentation dynamique au cœur de Geotrek-admin, où les itinéraires n'existent pas en tant que tel, mais uniquement par les tronçons qu"ils utilisent. Néanmoins des pistes de reflexion sur le sujet ont déjà été initiés, voir par exemple #2139 |
Bonjour, |
Bonjour, Nous avons créé avec Makina Corpus un fichier parsers permettant d'importer des données de Tourinsoft vers Geotrek (restaurants, hébergements, loisirs). Les éléments sont à affiner en fonction des données à récupérer et de la catégorisation qu'on veut leur donner dans Geotrek mais ce fichier peut servir de base. Les champs ne sont pas les mêmes pour tous dans Tourinsoft, le fichier est donc à adapter avec les données de chacun. Attention l'importation va mettre à jour les données, les créer si nécessaire mais également supprimer les éléments qui ne seraient pas dans la base de Tourinsoft (données qui auraient été saisies à la main dans Geotrek par exemple). La procédure à suivre est la suivante :
Voilà le code du fichier parsers que nous avons utilisé : # coding=utf-8
from __future__ import unicode_literals
from django.conf import settings
from django.contrib.gis.geos import Point
from geotrek.common.models import RecordSource
from geotrek.common.parsers import ValueImportError
from geotrek.tourism.models import TouristicContent, TouristicContentCategory, TouristicContentType1, TouristicContentType2
from geotrek.tourism.parsers import TouristicContentTourInSoftParserV3
class TourinsoftParser64(TouristicContentTourInSoftParserV3):
"""
On hérite d'une classe Geotrek qui fourni un ensemble de méthodes pour parser les flux de données Tourinsoft.
Le classe TourinsoftParser64 servira ensuite pour chaque classe qu'on créera pour fournir des méthodes communes (par exemple les méthodes qui formatte les données d'adresse, de contact, etc.).
"""
# override next_row method from TourInSoftParser to match particular behavior
def next_row(self):
skip = 0
while True:
params = {
'format': 'json',
'inlinecount': 'allpages',
'top': 1000,
'skip': skip,
}
response = self.request_or_retry(self.url, params=params)
self.root = response.json()
self.nb = self.get_nb()
for row in self.items:
yield {self.normalize_field_name(src): val for src, val in row.items()}
skip += 1000
if skip >= self.nb:
return
# override get_nb method from TourInSoftParser to match particular behavior with new Tourinsoft API
def get_nb(self):
return len(list(self.root['value']))
non_fields = {
'attachments': 'PHOTOSs',
}
def filter_name(self, src, val):
if not val:
raise ValueImportError("No Name")
return val
def filter_geom(self, src, val):
lng, lat = val
if lng and lat:
geom = Point(float(lng), float(lat), srid=4326)
geom.transform(settings.SRID)
else:
raise ValueImportError('Touristic content needs a geometry')
return geom
def filter_contact(self, src, val):
communication, adresses = val
infos = ""
if communication:
for comm in communication:
if comm['TypedaccesTelecom'] and comm['TypedaccesTelecom']['ThesCode'] == 'C1':
infos += u"<strong>Téléphone :</strong><br>"
infos += u"%s<br>" % comm['CoordonneesTelecom']
if comm['TypedaccesTelecom'] and comm['TypedaccesTelecom']['ThesCode'] == 'C6':
infos += u"<strong>Téléphone mobile :</strong><br>"
infos += u"%s<br>" % comm['CoordonneesTelecom']
if comm['TypedaccesTelecom'] and comm['TypedaccesTelecom']['ThesCode'] == 'FB':
infos += u"<strong>Page facebook :</strong><br>"
infos += u"%s<br>" % comm['CoordonneesTelecom']
if adresses:
for ad in adresses:
infos += u"<br><strong>Adresse :</strong><br>"
if ad['Adresse1']:
infos += u"%s<br>" % ad['Adresse1']
if ad['Adresse1suite']:
infos += u"%s<br>" % ad['Adresse1suite']
if ad['Adresse2']:
infos += u"%s<br>" % ad['Adresse2']
if ad['Adresse3']:
infos += u"%s<br>" % ad['Adresse3']
if ad['CodePostal']:
infos += u"%s<br>" % ad['CodePostal']
if ad['Commune']:
infos += u"%s" % ad['Commune']
if infos[-4:] == u'<br>':
infos = infos[-4:]
return infos.replace('\n', '<br>')
def filter_email(self, src, values):
# values = toutes les informations présentent dans le champ tourinsoft "MOYENSCOMs"
if values:
for value in values:
# pour chaque entrée du tableau, on vérifie si le type d'information présent est bien C4 (C4 = email)
if value['TypedaccesTelecom'] and value['TypedaccesTelecom']['ThesCode'] == 'C4':
# si c'est le cas alors c'est la valeur qu'on retournera pour remplir le champ Geotrk "email"
val = value['CoordonneesTelecom']
val = val.replace(' ', '')
return val
return ''
def filter_website(self, src, values):
if values:
for value in values:
if value['TypedaccesTelecom'] and value['TypedaccesTelecom']['ThesCode'] == 'C5':
val = value['CoordonneesTelecom']
val = val.replace(' ', '')
if not val.startswith('http'):
return 'http://{}'.format(val)
return val
return ''
def filter_attachments(self, src, val):
result = []
if val:
for photo in val:
if photo and photo['Photo'] and photo['Photo']['Url']:
url = photo['Photo']['Url']
legend = photo['Photo']['Titre']
credits = photo['Photo']['Credit']
result.append((url, legend, credits))
return result
def filter_source(self, src, val):
sirtaqui = RecordSource.objects.get(name="SIRTAQUI 64")
result = [sirtaqui]
return result
def filter_description(self, src, val):
descriptions = val
infos = ''
if descriptions:
for desc in descriptions:
if desc['Descriptioncommerciale']:
infos += '%s <br><br>' % desc['Descriptioncommerciale']
if infos[-4:] == '<br>':
infos = infos[:-4]
if not infos or infos == u'<br>':
return None
return infos.replace('\n', '<br>')
class TourinsoftParser64Restaurants(TourinsoftParser64):
"""
A partir de maintenant une classe = un flux tourinsoft
Dans celle-ci on s'occupe du flux tourinsoft pour les Restaurants
"""
label = "Restaurants"
category = "Restaurants"
model = TouristicContent # On indique que l'objet Geotrk qu'on souhaite créer et un object de type "Contenu touristique"
url = "https://api-v3.tourinsoft.com/api/syndications/PRECISER VOTRE URL"
field_options = { # Pour pouvoir créer un Contenu touristique, on indique ici qu'on souhaite à minima que notre objet possède une géométrie et une sous-catégorie (en plus du nom obligatoire par contrainte SQL donc non spécifié ici)
'geom': {'required': True},
'type1': {'required': True},
}
fields = {
'eid': 'SyndicObjectID',
'name': 'SyndicObjectName',
'geom': ('GmapLongitude', 'GmapLatitude'), # Ici on associe deux champs Tourinsoft à un champ Geotrek. Geotrek va donc chercher une méthode "filter_geom" pour savoir que faire avec les deux valeurs provnenant de tourinsoft. La méthode filter_geom est définie L57
'practical_info': (
'OUVERTURETEXTEs',
'TARIFSTEXTEs',
'CAPACITEs',
),
'contact': (
'MOYENSCOMs',
'ADRESSEs',
),
'description': 'DESCRIPTIFSs',
'email': 'MOYENSCOMs', # Ici on pourrait ne pas faire de méthode dédiée, mais comme il s'agit d'un champ Tourinsoft qui est un tableau on doit créer une méthode pour pouvoir parcourir le tableau et trouver l'information qui nous intéresse. Cf L96
'website': 'MOYENSCOMs',
}
m2m_fields = {
'type1': 'TYPE2s', # Ici on associe le champ Tourinsoft "TYPE2s" au champ Many2Many Geotrek "type1". Ce champ correspond à une liste de plusieurs objets de type "Sous-Catégorie de Contenu touristique de niveau 1". Voir L176
'source': 'TYPE2s',
}
def start(self):
"""
Méthode d'initialisation de note parser. On va s'occuper ici de créer une catégorie de contenu touristique nommée "Restaurant". Si la cétégorie existe déjà on la récupère afin de pouvoir l'associer aux objets que notr Parser va créer.
"""
super().start()
category, created = TouristicContentCategory.objects.get_or_create(label=self.label)
self.category = category
def filter_type1(self, src, val):
"""
Dans cette méthode on s'occupe de parcourir les valeurs contenues dans TYPE2s.
Pour chaque valeur on va soit créer s'il n'existe pas encore, soit récupérer un objet de type TouristicContentType1 correspondant à la valeur rencontrée dans le flux Tourinsoft.
Ensuite on va renvoyer un tableau contenu l'ensemble des objet rencontrés. Ainsi, le contenu touristique créé dans Geotrek sera associé à plusieurs sous-catégories issues de Tourinsoft.
"""
instances = []
if not val:
return instances
for entry in val:
if entry['Categorie']:
for cat in entry['Categorie']:
instance_type, created = TouristicContentType1.objects.get_or_create(
category_id=self.category.id,
label=cat['ThesLibelle']
)
instances.append(instance_type)
return instances
def filter_practical_info(self, src, val):
ouverture_textes, tarifs, capacites = val
infos = ''
if ouverture_textes:
infos += '<strong>Informations sur l\'ouverture de l\'établissement : </strong><br>'
infos += '%s<br>' % ouverture_textes[0]['Informationsouverture']
if tarifs:
infos += '<strong>Informations sur les tarifs : </strong><br>'
infos += '%s<br>' % tarifs[0]['Descriptiftarifs']
if tarifs:
for tarif in tarifs:
if tarif['Descriptiftarifs']:
infos += '<br><strong>Informations sur les tarifs2 : </strong> %s<br>' % tarif['Descriptiftarifs']
if capacites:
for capacite in capacites:
if capacite['Nombremaximumdecouverts']:
infos += '<strong>Nombre de couverts : </strong> %s<br>' % capacite['Nombremaximumdecouverts']
if infos[-4:] == '<br>':
infos = infos[:-4]
if not infos or infos == u'<br>':
return None
return infos.replace('\n', '<br>')
class TourinsoftParser64Hebergements(TourinsoftParser64):
"""
On s'occupe du flux tourinsoft pour les Hebergements
"""
label = "Hébergement"
category = "Hébergement"
model = TouristicContent # On indique que l'objet Geotrk qu'on souhaite créer et un object de type "Contenu touristique"
url = "https://api-v3.tourinsoft.com/api/syndications/PRECISER VOTRE URL"
field_options = { # Pour pouvoir créer un Contenu touristique, on indique ici qu'on souhaite à minima que notre objet possède une géométrie et une sous-catégorie (en plus du nom obligatoire par contrainte SQL donc non spécifié ici)
'geom': {'required': True},
'type1': {'required': True},
}
fields = {
'eid': 'SyndicObjectID',
'name': 'SyndicObjectName',
'geom': ('GmapLongitude', 'GmapLatitude'), # Ici on associe deux champs Tourinsoft à un champ Geotrek. Geotrek va donc chercher une méthode "filter_geom" pour savoir que faire avec les deux valeurs provnenant de tourinsoft. La méthode filter_geom est définie L57
'practical_info': (
'CLASs',
'NBREPERS',
'NBRECHAMBRES',
'NBREEMPLACEMENTS',
),
'contact': (
'MOYENSCOMs',
'ADRESSEs',
),
'description': 'DESCRIPTIFSs',
'email': 'MOYENSCOMs', # Ici on pourrait ne pas faire de méthode dédiée, mais comme il s'agit d'un champ Tourinsoft qui est un tableau on doit créer une méthode pour pouvoir parcourir le tableau et trouver l'information qui nous intéresse. Cf L96
'website': 'MOYENSCOMs',
}
m2m_fields = {
'type1': (
'ObjectTypeName',
'TYPEHEBs'
),
'type2': 'LABELSs',
'source': 'LABELs',
}
def start(self):
"""
Méthode d'initialisation de note parser. On va s'occuper ici de créer une catégorie de contenu touristique nommée "Hebergements". Si la cétégorie existe déjà on la récupère afin de pouvoir l'associer aux objets que notr Parser va créer.
"""
super().start()
category, created = TouristicContentCategory.objects.get_or_create(label=self.category)
self.category = category
def filter_practical_info(self, src, val):
classement, capacite_pers, capacite_chambres, capacite_emplacements = val
infos = ''
if classement:
infos += '<strong>Classements : </strong>'
for x in classement:
infos += '%s / ' % x['Classement']['ThesLibelle']
infos = infos[:-3]
if capacite_pers:
infos += '<br><strong>Capacité d\'accueil : </strong> %s<br>' % capacite_pers
if capacite_chambres:
infos += '<strong>Nombre de chambres : </strong> %s<br>' % capacite_chambres
if capacite_emplacements:
infos += '<strong>Nombre d\'emplacements : </strong> %s<br>' % capacite_emplacements
if infos[-4:] == '<br>':
infos = infos[:-4]
if not infos or infos == u'<br>':
return None
return infos.replace('\n', '<br>')
def filter_type2(self, src, val):
"""
Dans cette méthode on s'occupe de parcourir les valeurs contenues dans TYPE2s.
Pour chaque valeur on va soit créer s'il n'existe pas encore, soit récupérer un objet de type TouristicContentType1 correspondant à la valeur rencontrée dans le flux Tourinsoft.
Ensuite on va renvoyer un tableau contenu l'ensemble des objet rencontrés. Ainsi, le contenu touristique créé dans Geotrek sera associé à plusieurs sous-catégories issues de Tourinsoft.
"""
instances = []
if not val:
return instances
labels = val
for element in labels:
instance_label, created = TouristicContentType2.objects.get_or_create(
category_id=self.category.id,
label=element['ThesLibelle']
)
instances.append(instance_label)
return instances
def filter_type1(self, src, val):
"""
Try to match every object with one of the following category :
Chambre d'hôtes / Hébergement atypique / Gîte et hébergement collectif / Hôtel / Camping / Aire de pique-nique (currently no element matching) / Restaurant / Aire de camping-car
"""
instances = []
if not val:
return instances
object_type, type_hebergement = val
if object_type == "Campings":
instance_type, created = TouristicContentType1.objects.get_or_create(
category_id=self.category.id,
label='Camping'
)
instances.append(instance_type)
elif object_type == "Hôtels":
instance_type, created = TouristicContentType1.objects.get_or_create(
category_id=self.category.id,
label='Hôtel'
)
instances.append(instance_type)
elif object_type == "Hébergements collectifs":
instance_type, created = TouristicContentType1.objects.get_or_create(
category_id=self.category.id,
label='Hébergement collectif'
)
instances.append(instance_type)
elif object_type == "Aires de camping-cars":
instance_type, created = TouristicContentType1.objects.get_or_create(
category_id=self.category.id,
label='Aire de camping-car'
)
instances.append(instance_type)
elif object_type == "Villages Vacances":
instance_type, created = TouristicContentType1.objects.get_or_create(
category_id=self.category.id,
label='Village de Vacances'
)
instances.append(instance_type)
elif object_type == "Résidences":
instance_type, created = TouristicContentType1.objects.get_or_create(
category_id=self.category.id,
label='Résidence de Tourisme'
)
instances.append(instance_type)
elif object_type == "Hébergements locatifs (meublés et chambres d'hôtes)":
if type_hebergement:
if type_hebergement[0]['Typedequipement']['ThesLibelle'] == "Chambres d'hôtes":
instance_type, created = TouristicContentType1.objects.get_or_create(
category_id=self.category.id,
label="Chambre d'hôte"
)
instances.append(instance_type)
elif type_hebergement[0]['Typedequipement']['ThesLibelle'] == "Meublés":
instance_type, created = TouristicContentType1.objects.get_or_create(
category_id=self.category.id,
label='Locations et Gîtes'
)
instances.append(instance_type)
else:
raise ValueImportError("An unknown category for 'Type d'hébergement locatif' has been found !")
else:
raise ValueImportError('No category found')
return instances
class TourinsoftParser64LOISIRS1(TourinsoftParser64):
"""
A partir de maintenant une classe = un flux tourinsoft
Dans celle-ci on s'occupe du flux tourinsoft pour les LOI PCU PNA
"""
label = "Sites recommandés"
category = "Sites recommandés"
model = TouristicContent # On indique que l'objet Geotrk qu'on souhaite créer et un object de type "Contenu touristique"
url = "https://api-v3.tourinsoft.com/api/syndications/PRECISER VOTRE URL"
field_options = { # Pour pouvoir créer un Contenu touristique, on indique ici qu'on souhaite à minima que notre objet possède une géométrie et une sous-catégorie (en plus du nom obligatoire par contrainte SQL donc non spécifié ici)
'geom': {'required': True},
'type1': {'required': True},
}
fields = {
'eid': 'SyndicObjectID',
'name': 'SyndicObjectName',
'geom': ('GmapLongitude', 'GmapLatitude'), # Ici on associe deux champs Tourinsoft à un champ Geotrek. Geotrek va donc chercher une méthode "filter_geom" pour savoir que faire avec les deux valeurs provnenant de tourinsoft. La méthode filter_geom est définie L57
'practical_info': (
'OUVERTURETEXTEs',
'TARIFSTEXTEs',
),
'contact': (
'MOYENSCOMs',
'ADRESSEs',
),
'description': 'DESCRIPTIFSs',
'email': 'MOYENSCOMs', # Ici on pourrait ne pas faire de méthode dédiée, mais comme il s'agit d'un champ Tourinsoft qui est un tableau on doit créer une méthode pour pouvoir parcourir le tableau et trouver l'information qui nous intéresse. Cf L96
'website': 'MOYENSCOMs',
}
m2m_fields = {
'type1': 'TYPE', # Ici on associe le champ Tourinsoft "TYPE2s" au champ Many2Many Geotrek "type1". Ce champ correspond à une liste de plusieurs objets de type "Sous-Catégorie de Contenu touristique de niveau 1". Voir L176
}
def start(self):
"""
Méthode d'initialisation de note parser. On va s'occuper ici de créer une catégorie de contenu touristique nommée "Restaurant". Si la cétégorie existe déjà on la récupère afin de pouvoir l'associer aux objets que notr Parser va créer.
"""
super().start()
category, created = TouristicContentCategory.objects.get_or_create(label=self.label)
self.category = category
def filter_type1(self, src, val):
"""
Dans cette méthode on s'occupe de parcourir les valeurs contenues dans TYPE2s.
Pour chaque valeur on va soit créer s'il n'existe pas encore, soit récupérer un objet de type TouristicContentType1 correspondant à la valeur rencontrée dans le flux Tourinsoft.
Ensuite on va renvoyer un tableau contenu l'ensemble des objet rencontrés. Ainsi, le contenu touristique créé dans Geotrek sera associé à plusieurs sous-catégories issues de Tourinsoft.
"""
instances = []
if not val:
return instances
for entry in val.split('#'):
instance_type, created = TouristicContentType1.objects.get_or_create(
category_id=self.category.id,
label=entry
)
instances.append(instance_type)
return instances
def filter_practical_info(self, src, val):
ouverture_textes, tarifs = val
infos = ''
if ouverture_textes:
infos += '<strong>Informations sur l\'ouverture de l\'établissement : </strong><br>'
infos += '%s<br>' % ouverture_textes[0]['Informationsouverture']
if tarifs:
infos += '<strong>Informations sur les tarifs : </strong><br>'
infos += '%s<br>' % tarifs[0]['Descriptiftarifs']
#if tarifs:
# for tarif in tarifs:
# if tarif['Descriptiftarifs']:
# infos += '<br><strong>Informations sur les tarifs2 : </strong> %s<br>' % tarif['Descriptiftarifs']
if infos[-4:] == '<br>':
infos = infos[:-4]
if not infos or infos == u'<br>':
return None
return infos.replace('\n', '<br>')
def filter_description(self, src, val):
descriptions = val
infos = ''
if descriptions:
for desc in descriptions:
if desc['Descriptioncommerciale']:
infos += '%s <br><br>' % desc['Descriptioncommerciale']
if infos[-4:] == '<br>':
infos = infos[:-4]
if not infos or infos == u'<br>':
return None
return infos.replace('\n', '<br>')
class TourinsoftParser64PCU(TourinsoftParser64):
"""
A partir de maintenant une classe = un flux tourinsoft
Dans celle-ci on s'occupe du flux tourinsoft pour les PCU
"""
label = "Patrimoine Culturel"
category = "Patrimoine Culturel"
model = TouristicContent # On indique que l'objet Geotrk qu'on souhaite créer et un object de type "Contenu touristique"
url = "https://api-v3.tourinsoft.com/api/syndications/cdt64.tourinsoft.com/3cfd6199-8c69-41b7-b935-3c2f4dba4b15"
field_options = {
# Pour pouvoir créer un Contenu touristique, on indique ici qu'on souhaite à minima que notre objet possède une géométrie et une sous-catégorie (en plus du nom obligatoire par contrainte SQL donc non spécifié ici)
'geom': {'required': True},
'type1': {'required': True},
}
fields = {
'eid': 'SyndicObjectID',
'name': 'SyndicObjectName',
'geom': ('GmapLongitude', 'GmapLatitude'),
# Ici on associe deux champs Tourinsoft à un champ Geotrek. Geotrek va donc chercher une méthode "filter_geom" pour savoir que faire avec les deux valeurs provnenant de tourinsoft. La méthode filter_geom est définie L57
'practical_info': (
'OUVERTURETEXTE',
'TARIFSTEXTE',
),
'contact': (
'MOYENSCOMs',
'ADRESSEs',
),
'description': 'DESCRIPTIFSs',
'email': 'MOYENSCOMs',
# Ici on pourrait ne pas faire de méthode dédiée, mais comme il s'agit d'un champ Tourinsoft qui est un tableau on doit créer une méthode pour pouvoir parcourir le tableau et trouver l'information qui nous intéresse. Cf L96
'website': 'MOYENSCOMs',
}
m2m_fields = {
'type1': 'TYPEs',
# Ici on associe le champ Tourinsoft "TYPE2s" au champ Many2Many Geotrek "type1". Ce champ correspond à une liste de plusieurs objets de type "Sous-Catégorie de Contenu touristique de niveau 1". Voir L176
}
def start(self):
"""
Méthode d'initialisation de note parser. On va s'occuper ici de créer une catégorie de contenu touristique nommée "Restaurant". Si la cétégorie existe déjà on la récupère afin de pouvoir l'associer aux objets que notr Parser va créer.
"""
super().start()
category, created = TouristicContentCategory.objects.get_or_create(label=self.label)
self.category = category
def filter_type1(self, src, val):
"""
Dans cette méthode on s'occupe de parcourir les valeurs contenues dans TYPE.
Pour chaque valeur on va soit créer s'il n'existe pas encore, soit récupérer un objet de type TouristicContentType1 correspondant à la valeur rencontrée dans le flux Tourinsoft.
Ensuite on va renvoyer un tableau contenu l'ensemble des objet rencontrés. Ainsi, le contenu touristique créé dans Geotrek sera associé à plusieurs sous-catégories issues de Tourinsoft.
"""
# instances = []
# if not val:
# return instances
# for entry in val.split('#'):
# instance_type, created = TouristicContentType1.objects.get_or_create(
# category_id=self.category.id,
# label=entry
# )
# instances.append(instance_type)
# return instances
# instances = []
# if not val:
# return instances
# entry = val
# for entry in val:
# instance_type, created = TouristicContentType1.objects.get_or_create(
# category_id=self.category.id,
# label=entry['ThesLibelle']
# )
# instances.append(instance_type)
# return instances
# instances = []
# if not val:
# return instances
# labels = val
# for element in labels:
# instance_label, created = TouristicContentType1.objects.get_or_create(
# category_id=self.category.id,
# label=element['ThesLibelle']
# )
# instances.append(instance_label)
# return instances
instances = []
if not val:
return instances
for entry in val:
if entry['Typedequipementprincipal']:
for cat in entry['Typedequipementprincipal']:
instance_type, created = TouristicContentType1.objects.get_or_create(
category_id=self.category.id,
label=entry['Typedequipementprincipal']['ThesLibelle']
)
instances.append(instance_type)
return instances
def filter_practical_info(self, src, val):
ouverture_textes, tarifs = val
infos = ''
if ouverture_textes:
infos += '<strong>Informations sur l\'ouverture de l\'établissement : </strong><br>'
infos += '%s<br>' % ouverture_textes
if tarifs:
infos += '<strong>Informations sur les tarifs : </strong><br>'
infos += '%s<br>' % tarifs
# if tarifs:
# for tarif in tarifs:
# if tarif['Descriptiftarifs']:
# infos += '<br><strong>Informations sur les tarifs2 : </strong> %s<br>' % tarif['Descriptiftarifs']
if infos[-4:] == '<br>':
infos = infos[:-4]
if not infos or infos == u'<br>':
return None
return infos.replace('\n', '<br>')
def filter_description(self, src, val):
descriptions = val
infos = ''
if descriptions:
for desc in descriptions:
if desc['Descriptioncommerciale']:
infos += '%s <br><br>' % desc['Descriptioncommerciale']
if infos[-4:] == '<br>':
infos = infos[:-4]
if not infos or infos == u'<br>':
return None
return infos.replace('\n', '<br>') |
Merci pour le retour et le partage. Il y a déjà une partie du parser Tourinsoft qui est partagée dans le code https://github.com/GeotrekCE/Geotrek-admin/blob/master/geotrek/common/parsers.py#L678 et https://github.com/GeotrekCE/Geotrek-admin/blob/master/geotrek/tourism/parsers.py#L585 Mais suite à votre travail, il y en a peut-être d'autres à intégrer dans les parsers par défaut, pour moins avoir de partie spécifique dans votre parser, même si il y a toujours des parties qui doivent être spécifiques au contexte ? Et comme la mise à place d'import depuis des flux Tourinsoft revient souvent, votre mise en place serait certainement l'occasion de mettre quelques lignes sur l'import Tourinsoft dans la documentation d'import (https://github.com/GeotrekCE/Geotrek-admin/blob/master/docs/import.rst) car actuellement on mentionne uniquement Apidae, Esprit Parc et Biodiv'Sports. |
En complément, @AudreyRemy nous partage un parser permettant d'importer des randonnées et leurs POI depuis Tourinsoft vers Geotrek. Code
class TrekTourInSoftParser(TourInSoftParser):
eid = 'eid'
model = Trek
delete = True
themes = None
portal = None
url = 'https://wcf.tourinsoft.com/Syndication/cdt64/xxxxxxxxxxxxxxxxxxxxxxxxxx/Objects'
constant_fields = {
'review': True,
'deleted': False,
}
fields = {
'eid': 'SyndicObjectID',
'name': ('SyndicObjectName', 'NUMEROETAPE'),
'description_teaser': 'TITRE2',
'ambiance': 'DESCRIPTIF',
'departure': 'COMMUNEDEPART',
'arrival': 'COMMUNEARRIVEE',
'description': ('ETAPES', 'COMMUNEDEPART', 'RAISONSOCIALE', 'SyndicObjectID'),
'points_reference': 'ETAPES',
'geom': ('GmapLongitude', 'GmapLatitude'),
'difficulty': 'DIFFICULTE',
'advice': 'ALERTEINFO',
'practice': 'TYPE',
'duration': 'DUREE',
'structure': 'structure',
'route': 'TYPOLOGIE'
}
non_fields = {
'pois': 'POINTDINTERET',
'attachments': 'PHOTO',
}
field_options = {
'geom': {'required': True},
'source': {'create': True},
}
m2m_fields = {
'themes': 'THEMATIQUE', # Not used
'networks': 'BALISAGECOULEUR',
'information_desks': 'structure',
'web_links': ('COMMUNEDEPART', 'RAISONSOCIALE', 'SyndicObjectID'),
'source': 'AUTEUR'
}
natural_keys = {
'category': 'label',
'type1': 'label',
'type2': 'label',
'source': 'name',
'portal': 'name',
}
match_poi_type = {"Panorama": "Point de vue",
"Patrimoine bâti": "Patrimoine bâti",
"Site naturel": "Site naturel",
"Site culturel": "Site culturel",
"Dégustation": "Dégustation",
"Faune / Flore": "Faune / Flore"}
match_practice = {"Equestre": "Equestre",
"VAE": "Vélo"}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.constant_fields = self.constant_fields.copy()
self.m2m_constant_fields = self.m2m_constant_fields.copy()
def next_row(self):
skip = 0
while True:
params = {
'$format': 'json',
'$inlinecount': 'allpages',
'$top': 1000,
'$skip': skip,
}
response = self.request_or_retry(self.url, params=params)
self.root = response.json()
self.nb = self.get_nb()
for row in self.items:
try:
# On ajoute l'info sur les structures dans la reponse avant de traiter l'objet
# On peut ainsi récuperer les informations pour les lieux d'informations et les structures Geotrek
response = self.request_or_retry(row.get('Structure').get('__deferred').get('uri'),
params={'$format': 'json'})
row['structure'] = response.json()['d']
except requests.exceptions.ConnectionError:
# Si on arrive pas a se connceter a l'url de structure on continue sans ajouter la structure
pass
yield {self.normalize_field_name(src): val for src, val in row.items()}
skip += 1000
if skip >= self.nb:
return
def start(self):
kwargs = self.get_to_delete_kwargs()
# On cherche tous les itinéraires qui n'ont que une aggregation et qui viennent de tourinsoft (eid_startwith)
# queryset.filter(**kwargs) filtre également les itinéraires qui ne sont plus en cours de publication reviewed=True
queryset = self.model.objects.annotate(count_aggreg=Count('topo_object__aggregations'))
kwargs['eid__startswith'] = "ITI"
self.to_delete = set(queryset.filter(**kwargs).exclude(count_aggreg__gt=1).values_list('pk', flat=True))
# Ici c'est la gestion des types de fichiers, il ne faut pas changer cette partie.
if settings.PAPERCLIP_ENABLE_LINK is False and self.download_attachments is False:
raise Exception('You need to enable PAPERCLIP_ENABLE_LINK to use this function')
try:
self.filetype = FileType.objects.get(type=self.filetype_name, structure=None)
except FileType.DoesNotExist:
try:
self.filetype = FileType.objects.get(type=self.filetype_name, structure=self.structure)
except FileType.DoesNotExist:
raise GlobalImportError(_("FileType '{name}' does not exists in "
"Geotrek-Admin. Please add it").format(name=self.filetype_name))
# L'auteur des images sera un utilisateur "import"
self.creator, created = get_user_model().objects.get_or_create(username='import', defaults={'is_active': False})
def make_string_to_float(self, value):
# Cela permet de remplacer les données de mauvaise qualité en flottant
return float(value.replace('°', '').replace(',', '.').replace(' ', '').replace('O', ''))
def filter_route(self, src, val):
# En fonction de la valeur dans tourinsoft on remplace la valeur val en nouvelle valeur new_val (Aller-retour ...)
# Certaines condition ne sont pas utiles mais elle existe si on veut changer le nom des types de routes
if not val:
return
val = val.split('#')[0]
if val == "Itinéraire linéaire":
new_val = "Traversée"
elif val == "Boucle":
new_val = "Boucle"
elif val == "Aller-retour":
new_val = "Aller-retour"
else:
return
return Route.objects.get(route=new_val)
def filter_name(self, src, val):
name, num_etape = val
if num_etape and len(num_etape.split('|')) > 1:
final_name = f"{name} étape n°{num_etape.split('|')[1]}"
else:
final_name = name
return final_name
def filter_departure(self, src, val):
# On met en majuscule le départ
return val.capitalize()
def filter_arrival(self, src, val):
# On met en majuscule l'arrivée
return val.capitalize()
def filter_practice(self, src, val):
# Depuis une liste de pratique on récupere la première pratique et on essaye de trouver sa correspondance.
# Toutes les correspondances ne sont pas faites (voir match_practice en haut du parser)
practice_name = val.split('#')[0] # TODO We can have Pédestre#Vélo#VTT what should we do
practice_name_g = self.match_practice.get(practice_name) if self.match_practice.get(practice_name) else practice_name
try:
practice = Practice.objects.get(name=practice_name_g)
except Practice.DoesNotExist:
# Si la pratique n'est pas trouvé sur geotrek, on affiche une erreur en fin d'import de parser. Il arrive également que il n'y ait pas de pratique.
raise RowImportError(f"Missing practice : {practice_name_g}")
return practice
def filter_difficulty(self, src, val):
if not val:
return
val = val.split("#")[0]
# On remplace moyenne par intémediaire
if val == "Moyenne":
val = "Intermédiaire"
return DifficultyLevel.objects.get(difficulty=val)
def filter_description(self, src, val):
steps, commune, social, id_trek = val
if not steps:
description = ""
else:
# On ajoute les étapes
b_steps = steps.split(self.separator)
final_list_steps = []
for step in b_steps:
steps_information = step.split(self.separator2)
if len(steps_information) > 2:
final_list_steps.append(f"""<li>{steps_information[2]}</li>""")
description = "</br>".join(final_list_steps)
description = f'{description}</br></br>'
# On ajoute l'url resa tourisme a partir de la commune l'id du trek dans tourinsoft et la raison social
url = f'https://resa.tourisme64.com/itineraire/{commune.replace(" ", "-")}/{id_trek}-{social.replace(" ", "-")}'
final_description = f'''<ol>{description}</ol><p><strong>Informations touristiques complémentaires : </strong></p></br><p><a href="{url}">{url}</a></p>'''
return final_description
def filter_structure(self, src, val):
if not val or not val.get('Name'):
return Structure.objects.get_or_create(name=settings.DEFAULT_STRUCTURE_NAME)[0]
name_structure = val.get('Name')
# Create or get the structure in structure json
structure, created = Structure.objects.get_or_create(name=name_structure)
return structure
def filter_web_links(self, src, val):
# On ajoute sur tous les liens web "reussir ma rando"
web_link_1 = WebLink.objects.get(name="Reussirmarando")
return [web_link_1]
def filter_information_desks(self, src, val):
# On creer un nouveau lieux d'information a partir des données stockés dans structure
name = val.get('Name')
email = val.get('Email')
url = val.get('Url')
address = val.get('Address1')
address_2 = val.get('Address2')
address_3 = val.get('Address3')
postcode = val.get('Postcode')
city = val.get('City')
phone = val.get('Phone')
type_id = InformationDeskType.objects.get(label="Office du tourisme")
information_desk, created = InformationDesk.objects.get_or_create(name=name,
defaults={'type': type_id,
'phone': phone,
'email': email,
'website': url,
'municipality': f'{city} {postcode}',
'street': f'{address} {address_2} {address_3}'
})
return [information_desk]
def save_pois(self, src, val):
# On genere des pois à partir des données dans POINTDINTERET
# Ici on utilise l'obj courant pour gerer les fichiers attachés (self.obj)
# Durant l'import on passe par Trek_1 puis POI_1_A, POI_1_B Trek_2 puis POI_2_1 etc.
# On stock l'information de l'itinéraire car des actions sont faites sur lui également après l'enregistrement des pois
main_obj = self.obj
if not val:
return
pois_list = val.split(self.separator)
for poi_text in pois_list:
list_val_poi = poi_text.split(self.separator2)
if not list_val_poi or len(list_val_poi) < 8:
# Si entre les separateurs | nous avons moins de 8 elements, on passe au point d'interet suivant (Il faut changer le separateur si il y a le separateur dans la description !)
continue
type_poi = list_val_poi[0]
# On recupere le type de poi correspondant dans match_poi_type
type_poi_g = self.match_poi_type.get(type_poi) if self.match_poi_type.get(
type_poi) else type_poi
try:
poi_type_final = POIType.objects.get(label=type_poi_g)
except POIType.DoesNotExist:
self.add_warning(f"This type of poi doesn't exist {type_poi}")
continue
name_poi = list_val_poi[1]
description_poi = list_val_poi[2]
lng = list_val_poi[3]
lat = list_val_poi[4]
url_img_poi = list_val_poi[5]
legend_img_poi = list_val_poi[6]
credit_img_po = list_val_poi[7]
# Gestion des geometries
if not lng or not lat or not type_poi_g:
continue
try:
serialized = '{"lng": %s, "lat": %s}' % (self.make_string_to_float(lng), self.make_string_to_float(lat))
except ValueError:
continue
try:
topology = Topology.deserialize(serialized)
except GDALException:
self.add_warning(f'poi {name_poi} has bad lat lng format {lat}, {lng} (should be 4326)')
continue
# Move deserialization aggregations to the POI
# We get the poi by it's name if it exist already do not change review/published
#
poi = POI.objects.filter(name=name_poi, type=poi_type_final).first()
if poi:
poi.description = description_poi
poi.save()
else:
poi = POI.objects.create(name=name_poi,
type=poi_type_final,
description=description_poi,
review=True)
# Genere la geometrie avec la segmentation dynamique :
poi.mutate(topology)
# On met le poi en obj courant pour la gestion des fichiers attachés
self.obj = poi
try:
self.save_attachments(src, [url_img_poi, f'{legend_img_poi} {credit_img_po}', ''])
except (requests.exceptions.ConnectionError, ValueImportError):
continue
# On remet l'itineraire courant comme obj courant
self.obj = main_obj
def filter_attachments(self, src, val):
# Gere a la fois les fichiers attachés depuis les itinéraires et les pois si c'est une liste cela vient des pois sinon depuis l'itineraires
# Dans le cas des itineraires nous avons aucune information par rapport a la legende etc
if not val:
return []
if isinstance(val, list):
url_end, legend, author = val
else:
values = val.split('#')
list_urls = []
for value in values:
list_urls.append([f'http://cdt64.media.tourinsoft.eu/upload/{value}', "", ""])
return list_urls
if url_end:
url = f'http://cdt64.media.tourinsoft.eu/upload/{url_end}'
return [[url, legend, author]]
else:
return []
def filter_points_reference(self, src, val):
# Gestion des points de passages
if not val:
return
steps = val.split(self.separator)
steps = [step.split(self.separator2) for step in steps]
geometries = []
for step in steps:
if not step:
continue
if len(step) < 7:
continue
lng = step[5].replace('°', '').replace(',', '.').replace(' ', '').replace('O', '')
lat = step[6].replace('°', '').replace(',', '.').replace(' ', '').replace('O', '')
if lng and lat:
try:
geom = Point(float(lng), float(lat), srid=4326)
try:
geom_srid = geom.transform(settings.SRID, clone=True)
except GDALException:
# Geometry with int value but transformation in srid of project is not possible
continue
geometries.append(geom_srid)
except ValueError:
# lat lng are not numbers
pass
return MultiPoint(geometries, srid=settings.SRID)
def filter_geom(self, src, val):
# Ici on essaye de transformer des lats/long qui peuvent etre ecrit : 43,5 en 43.5
lng, lat = val
if not lng or not lat:
raise ValueImportError("Empty geometry")
lng = lng.replace('°', '').replace(',', '.').replace(' ', '').replace('O', '')
lat = lat.replace('°', '').replace(',', '.').replace(' ', '').replace('N', '')
geom = Point(float(lng), float(lat), srid=4326) # WGS84
geom.transform(settings.SRID)
paths = Path.objects.annotate(distance=Distance('geom', geom))
path = paths.order_by('distance').first()
# pour creer une geometrie il faut l'ecrire dans la forme de l'outil comme ci desous (segmentation dynamique)
# [{distance au troncon, information sur es troncons : le premier (0) entre 0 et 1 sur le troncon "path"]
serialized = [{"offset": 0, "positions": {"0": [0, 1]}, "paths":[path.pk]}]
self.topology = None
try:
self.topology = Topology.deserialize(serialized)
except GDALException:
self.add_warning(f'Wrong geom')
return path.geom
def filter_duration(self, src, val):
# Ici on essaye de transformer la valeur version humaine en entier : 1h30 => 1.5 / 1 jour => 8
all_duration = val
duration = all_duration.split(self.separator2)[0]
duration = duration.replace(' ', '')
duration_number = None
# Type multiple de durée,
for action in ["%Hh%M", "%Hh", '%H', '%Mmn', 'à%Hh', '%Mmin', '%H:00']:
try:
date_duration = datetime.strptime(duration, action)
duration_number = round(date_duration.hour + date_duration.minute / 60, 3)
break
except ValueError:
continue
for action in ["%djour", "%djours"]:
try:
date_duration = datetime.strptime(duration, action)
duration_number = date_duration.day * 8
except ValueError:
continue
if not duration_number:
if duration == '1/2Jour':
return 4
if duration == '1/2jour':
return 4
return duration_number
def filter_networks(self, src, val):
# Nous savons uniquement que Blanc et rouge c'est le GR jaune c'est un PR etc...
# Autres valeurs retrouvés : Blanc et rouge (GR®), Jaune et rouge (GRP®), Bleu, Jaune, Noir, Orange, Rouge, Vert, Violet
network = val
if network == 'Blanc et rouge (GR®)':
return [TrekNetwork.objects.get(network='GR')]
elif network == 'Jaune':
return [TrekNetwork.objects.get(network='PR')]
elif network == 'Orange':
return [TrekNetwork.objects.get(network='Piste équestre')]
return []
def parse_obj(self, row, operation):
try:
update_fields = self.parse_fields(row, self.fields)
update_fields += self.parse_fields(row, self.constant_fields)
# Ici on enleve les champs qui ne seront pas repris lors de la mise a jour d'un objet (review, geom, pois et id)
if 'review' in update_fields:
update_fields.remove('review') # don't want to change review value after creation
if 'geom' in update_fields:
update_fields.remove('geom') # don't want to change geom value after creation
if 'pois' in update_fields:
update_fields.remove('pois') # don't want to change pois value after creation
if 'id' in update_fields:
update_fields.remove('id') # Can't update primary key
except RowImportError as warnings:
self.add_warning(str(warnings))
return
if operation == "created":
self.obj.save()
# Permet de creer la geometrie associé aux informations stocké dans la topologie
# [{distance au troncon, information sur es troncons : le premier (0) entre 0 et 1 sur le troncon "path"]
self.obj.mutate(self.topology)
else:
self.obj.save(update_fields=update_fields)
update_fields += self.parse_fields(row, self.m2m_fields)
update_fields += self.parse_fields(row, self.m2m_constant_fields)
update_fields += self.parse_fields(row, self.non_fields, non_field=True)
if operation == "created":
self.nb_created += 1
elif update_fields:
self.nb_updated += 1
else:
self.nb_unmodified += 1
def download_attachment(self, url):
try:
super().download_attachment(url)
except requests.ConnectionError:
return None
def report(self, output_format='txt'):
report_txt = super().report(output_format)
if self.nb_created > 0:
try:
# On envoit un mail si il y a un nouveau itinéraire
mail_admins(
"[Itinéraire] Un nouvel itinéraire provenant du flux Tourinsoft vient d'être créé",
f"Lors de l'import du flux d'itinéraires provenant de Tourinsoft, un itinéraire a été créer, il est possible de le retrouver avec le champ review dans les filtres et son identifiant est {self.obj.pk}"
)
except Exception:
print("Error while trying to send an email, please configure your mail server")
return report_txt Comme pour créer le tracé d'un itinéraire dans Geotrek, il faut s'appuyer sur une série de tronçons, on ne peut pas importer le tracé depuis Tourinsoft tel quel pour en faire le tracé de la rando dans Geotrek. Le processus est donc celui-ci :
En évolution, je me dis qu'on pourrait voir pour importer la géométrie venant de Tourinsoft dans la géométrie de l'itinéraire, sans lui créer de topologie. Ou plutôt en lui créant une topologie de base comme ça semble déjà être le cas, mais stocker dans la "geom" de l'itinéraire sa géométrie venant de Tourinsoft. D'autant plus que c'est proche d'une fonctionnalité envisagée pour remédier au sujet des modifications de topologies non voulues (#2515) où il a été envisagé :
Ainsi quand on est sur un itinéraire, on pourrait imaginer une fonctionnalité permettant d'afficher la "géométrie préalable" de l'objet en affichant son champs "geom" sur la carte, en plus de sa topologie sur les tronçons. |
De part la structure variable des flux Tourinsofts, il n'est pas possible de créer un parser unique pour importer les itinéraires directement dans Geotrek comme c'est le cas pour Apidae. En effet, un travail d'analyse du flux (de sa structure), et de code est nécéssaire pour faire correspondre les champs Geotrek et les champs Tourinsoft. Ce qui a été effectué pour le département 64 fonctionne bien mais est un cas spécifique car importe des données attributaires mais pas les géométries du fait de la segmentation dynamique. Un autre exemple est disponible ci-après, utilisé pour un territoire sans segmentation dynamique, ce qui signifie que le script importe également les géométries des itinéraires. On notera dans l'exemple qu'un travail de code est nécéssaire pour s'adapter au flux Tourinsoft. Ensuite la classe principale est surchargée pour chaque pratique afin d'adapter les contenus (deux exemples de surcharge en fin de snippet). Code
from datetime import datetime, timedelta
import textwrap
from geotrek.common.models import Attachment, FileType
from geotrek.trekking.parsers import ApidaeTrekParser
from geotrek.common.parsers import TourInSoftParser, RowImportError
from geotrek.trekking.models import DifficultyLevel, Practice, Trek, Route
class TrekParser(TourInSoftParser):
"""Import Treks from Tourinsoft SIT"""
version_tourinsoft = 3
url = "URL DU FLUX"
label = "Itinéraires TourinSoft"
label_fr = ""
label_en = "TourinSoft Treks"
model = Trek
create = True
eid = 'eid'
provider = "Tourinsoft"
fields = {
"eid": "SyndicObjectID",
"name": "NomOffre",
"practice": "TypePrincipals.*.ThesLibelle",
"route": "TypeParcourss.*.Typedetrace.ThesLibelle",
"difficulty": ("TypeParcourss.*.Difficulteduparcours.ThesLibelle", "NiveauDifficultePedestre.ThesLibelle", "NiveauDifficulteVelo.ThesLibelle", "NiveauDifficulteVTT.ThesLibelle", "NiveauDifficulteEquestre.ThesLibelle"),
"duration": ("TypeParcourss.*.Dureeduparcours", "DureePied", "DureeVelo", "DureeVTT", "DureeEquestre"),
"description": ("Description", "PointForts.*.Pointsforts"),
"geom": "Traces.0.TraceGPS.Url",
"departure": ("AdresseDepart", "CPDepart", "CommuneDepart"),
"arrival": ("CPArrivee", "CommuneArrivee"),
"access": ("Access.*.Pointdacces.ThesLibelle", "Access.*.Nomdelacces")
}
natural_keys = {
'difficulty': 'difficulty',
'route': 'route',
'themes': 'label',
'practice': 'name',
'accessibilities': 'name',
'networks': 'network'
}
constant_fields = {
'published': True
}
m2m_fields = {
}
non_fields = {
"attachments": ("Fichierss.*.Fichier", "Photos.*.Photos")
}
def __init__(self, progress_cb=None, user=None, encoding='utf8'):
super().__init__(progress_cb, user, encoding)
def normalize_field_name(self, name):
return name # bypass uppercase
def get_nb(self):
return int(len(self.root['value']))
def filter_geom(self, src, val):
geom_file = self.request_or_retry(url=val).content
geom = ApidaeTrekParser._get_geom_from_gpx(geom_file)
return geom
def filter_route(self, src, val):
route = None
if val and val[0]:
first_val = val[0]
route, _ = Route.objects.get_or_create(route=first_val.capitalize())
return route
def filter_difficulty(self, src, val):
if val:
level, _ = DifficultyLevel.objects.get_or_create(difficulty=val.capitalize())
return level
return None
def filter_duration(self, src, val):
if val:
try:
val_time = datetime.strptime(val, '%H:%M:%S')
except ValueError:
val_time = datetime.strptime(val, '%H:%M')
val_delta = timedelta(hours=val_time.hour, minutes=val_time.minute, seconds=val_time.second)
val_decimal = round(float(val_delta.seconds / 3600), 2)
return val_decimal
return None
def filter_departure(self, src, val):
return ' '.join(val)
def filter_arrival(self, src, val):
return ' '.join(val)
def filter_description(self, src, val):
desc, pois = val
if pois:
return f"{desc}\n\nPoints d'intérêts: {', '.join(pois)}"
return desc
def filter_access(self, src, val):
access_types, access_names = val
value = ""
if access_types and access_names:
for type, name in zip(access_types, access_names):
value = f"{value}{type} {name}\n"
return value
def filter_attachments(self, src, val):
values = []
pdfs, images = val
for pdf in pdfs:
if pdf:
values.append([pdf['Url'], pdf['Titre'], pdf['Credit'], "Topoguide"])
for img in images:
if img:
values.append([img['Url'], img['Titre'], img['Credit'], ''])
return values
def generate_attachment(self, **kwargs):
# No title in data - we use this kwargs to state if we need to use topoguide or regular file
filetype = FileType.objects.get(type="Topoguide") if kwargs.get('title') == "Topoguide" else self.filetype
attachment = Attachment()
attachment.content_object = self.obj
attachment.filetype = filetype
attachment.creator = self.creator
attachment.author = kwargs.get('author')
attachment.legend = textwrap.shorten(kwargs.get('legend'), width=127)
attachment.title = ""
return attachment
class ARCPedestrianTrekParser(ARCTrekParser):
label = "Itinéraires TourinSoft pédestres"
label_fr = "Itinéraires TourinSoft pédestres"
def filter_practice(self, src, val):
if "Pédestre" in val:
return Practice.objects.get(name_fr="Pédestre")
raise RowImportError("Ignored Trek because it is not related to practice 'Pédestre'")
def filter_difficulty(self, src, val):
typeparcours_val, specific_val, _, _, _ = val
if typeparcours_val and typeparcours_val[0]:
actual_val = typeparcours_val[0]
else:
actual_val = specific_val
return super().filter_difficulty(src, actual_val)
def filter_duration(self, src, val):
typeparcours_val, specific_val, _, _, _ = val
if typeparcours_val and typeparcours_val[0]:
actual_val = typeparcours_val[0]
else:
actual_val = specific_val
return super().filter_duration(src, actual_val)
class VeloTrekParser(ARCTrekParser):
label = "Itinéraires TourinSoft Vélo"
label_fr = "Itinéraires TourinSoft Vélo"
def filter_practice(self, src, val):
if "Cyclotourisme" in val:
return Practice.objects.get(name="Vélo")
raise RowImportError("Ignored Trek because it is not related to practice 'Vélo'")
def filter_difficulty(self, src, val):
typeparcours_val, _, specific_val, _, _ = val
if typeparcours_val and typeparcours_val[0]:
actual_val = typeparcours_val[0]
else:
actual_val = specific_val
return super().filter_difficulty(src, actual_val)
def filter_duration(self, src, val):
typeparcours_val, _, specific_val, _, _ = val
if typeparcours_val and typeparcours_val[0]:
actual_val = typeparcours_val[0]
else:
actual_val = specific_val
return super().filter_duration(src, actual_val) Côté code Geotrek, il n'est pas prévu en l'état d'aller plus loin dans l'intégration de cette fonctionnalité au code source de Geotrek, car les flux Tourinsoft sont trop variables. Je laisse ouvert ce ticket et le marque comme "documentation". Il faudra ajouter à la documentation de Geotrek un référence vers ce ticket puis lorsque ce sera fait le clôturer. |
OK merci pour les précisions. Concernant la piste de pouvoir quand même importer les géométries depuis Tourinsoft (ou autre) même en mode segmentation, sujet plus global discuté par ailleurs, notamment sur #2515 (comment) |
Bonjour,
Actuellement de nombreux PLR des Pyrénées-Atlantiques sont saisis dans Tourinsoft par les OT ou les Communautés de Communes (+ de 900 itinéraires).
Nous souhaiterions utiliser une passerelle permettant de récupérer ces itinéraires (tracé, pas à pas, photo, ...) et les importer dans Geotrek.
Avez-vous déjà une procédure permettant cette passerelle ?
Si non, seriez-vous intéressé par cette passerelle ?
Merci d'avance pour vos retours.
Bonne fin de journée.
The text was updated successfully, but these errors were encountered: