diff --git a/docs/changelog.rst b/docs/changelog.rst
index ec925d6c07..557ecf4c3a 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -5,6 +5,17 @@ CHANGELOG
2.86.0+dev (XXXX-XX-XX)
-----------------------
+**New features**
+
+- Add `provider` field to Trek, POI, Service, Signage, Infrastructure, TouristicContent, TouristicEvent, InformationDesk,
+ Path, Trail, Course, Site, SensitiveArea (#3189)
+- Add parser using api v2 (InformationDesk, TouristicContent, TouristicEvent, POI, Trek, Service, Signage, Infrastructure)
+- Add aggregator parser with a conductor using json file
+
+**Bug fixes**
+
+- Fix filtering on Services List does not filter
+
**Minor improvements**
- Disable debug log in debian package post installation script.
diff --git a/docs/install/advanced-configuration.rst b/docs/install/advanced-configuration.rst
index cb6ed613ab..1432ad2cc1 100644
--- a/docs/install/advanced-configuration.rst
+++ b/docs/install/advanced-configuration.rst
@@ -1717,6 +1717,7 @@ A (nearly?) exhaustive list of attributes available for display and export as co
"min_elevation",
"max_elevation",
"uuid",
+ "provider"
]
COLUMNS_LISTS["workmanagementedge_export"] = [
"eid",
@@ -1742,6 +1743,8 @@ A (nearly?) exhaustive list of attributes available for display and export as co
"maintenance_difficulty",
"published",
"uuid",
+ "eid",
+ "provider"
]
COLUMNS_LISTS["signage_view"] = [
"code",
@@ -1834,6 +1837,9 @@ A (nearly?) exhaustive list of attributes available for display and export as co
"reservation_id",
"portal",
"uuid",
+ "eid",
+ "eid2",
+ "provider"
]
COLUMNS_LISTS["poi_view"] = [
"structure",
@@ -1891,6 +1897,8 @@ A (nearly?) exhaustive list of attributes available for display and export as co
"date_update",
"date_insert",
"uuid",
+ "eid",
+ "provider"
]
COLUMNS_LISTS["touristic_event_view"] = [
"structure",
@@ -1918,6 +1926,8 @@ A (nearly?) exhaustive list of attributes available for display and export as co
"date_update",
"date_insert",
"uuid",
+ "eid",
+ "provider"
]
COLUMNS_LISTS["feedback_view"] = [
"email",
@@ -2163,6 +2173,8 @@ A (nearly?) exhaustive list of attributes available for display and export as co
"usage_difficulty",
"maintenance_difficulty"
"uuid",
+ "eid",
+ "provider"
]
COLUMNS_LISTS["signage_export"] = [
"structure",
@@ -2189,6 +2201,8 @@ A (nearly?) exhaustive list of attributes available for display and export as co
"min_elevation",
"max_elevation",
"uuid",
+ "eid",
+ "provider"
]
COLUMNS_LISTS["intervention_export"] = [
"name",
@@ -2305,6 +2319,7 @@ A (nearly?) exhaustive list of attributes available for display and export as co
"max_elevation",
"slope",
"uuid",
+ "provider"
]
COLUMNS_LISTS["poi_export"] = [
"structure",
@@ -2392,6 +2407,7 @@ A (nearly?) exhaustive list of attributes available for display and export as co
"areas",
"approved",
"uuid",
+ "provider"
]
COLUMNS_LISTS["touristic_event_export"] = [
"structure",
@@ -2428,6 +2444,7 @@ A (nearly?) exhaustive list of attributes available for display and export as co
"areas",
"approved",
"uuid",
+ "provider"
]
COLUMNS_LISTS["feedback_export"] = [
"email",
diff --git a/docs/install/import.rst b/docs/install/import.rst
index 5238dcb4e4..287538b2e8 100644
--- a/docs/install/import.rst
+++ b/docs/install/import.rst
@@ -195,6 +195,45 @@ Start import from Geotrek-admin UI
Open the top right menu and clic on ``imports``.
+Import data from a remote Geotrek instance
+==========================================
+
+Importing from a Geotrek instance works the same way as from SIT.
+A usecase for this is to aggregate data from several Geotrek-admin instance.
+
+.. danger::
+ Importing data from a remote Geotrek instance does not work with dynamic segmentation, your instance where you import data
+ must have dynamic segmentation disabled.
+
+
+For example, to import treks from another instance,
+edit ``/opt/geotrek-admin/var/conf/parsers.py`` file with the following content:
+
+::
+
+ class DemoGeotrekTrekParser(BaseGeotrekTrekParser):
+ url = "https://remote-geotrek-admin.net" # replace url with remote instance url
+ delete = False
+ field_options = {
+ 'difficulty': {'create': True, },
+ 'route': {'create': True, },
+ 'themes': {'create': True},
+ 'practice': {'create': True},
+ 'accessibilities': {'create': True},
+ 'networks': {'create': True},
+ 'geom': {'required': True},
+ 'labels': {'create': True},
+ }
+
+Then run in command line
+
+::
+
+ sudo geotrek import DemoGeotrekTrekParser
+
+Treks are now imported into your own instance.
+
+
Import data from a file
=======================
diff --git a/geotrek/api/tests/test_v2.py b/geotrek/api/tests/test_v2.py
index c34e5b781a..ee0cecbffc 100644
--- a/geotrek/api/tests/test_v2.py
+++ b/geotrek/api/tests/test_v2.py
@@ -66,18 +66,18 @@
'external_id', 'gpx', 'information_desks', 'kml', 'labels', 'length_2d',
'length_3d', 'max_elevation', 'min_elevation', 'name', 'networks',
'next', 'parents', 'parking_location', 'pdf', 'points_reference',
- 'portal', 'practice', 'previous', 'public_transport', 'published', 'ratings', 'ratings_description',
+ 'portal', 'practice', 'previous', 'public_transport', 'provider', 'published', 'ratings', 'ratings_description',
'reservation_system', 'reservation_id', 'route', 'second_external_id', 'source', 'structure',
'themes', 'update_datetime', 'url', 'uuid', 'web_links'
])
-PATH_PROPERTIES_GEOJSON_STRUCTURE = sorted(['comments', 'length_2d', 'length_3d', 'name', 'url', 'uuid'])
+PATH_PROPERTIES_GEOJSON_STRUCTURE = sorted(['comments', 'length_2d', 'length_3d', 'name', 'provider', 'url', 'uuid'])
TOUR_PROPERTIES_GEOJSON_STRUCTURE = sorted(TREK_PROPERTIES_GEOJSON_STRUCTURE + ['count_children', 'steps'])
POI_PROPERTIES_GEOJSON_STRUCTURE = sorted([
'id', 'create_datetime', 'description', 'external_id',
- 'name', 'attachments', 'published', 'type', 'type_label', 'type_pictogram',
+ 'name', 'attachments', 'published', 'provider', 'type', 'type_label', 'type_pictogram',
'update_datetime', 'url', 'uuid'
])
@@ -92,7 +92,7 @@
TOURISTIC_CONTENT_DETAIL_JSON_STRUCTURE = sorted([
'accessibility', 'approved', 'attachments', 'category', 'cities', 'contact', 'create_datetime', 'description',
'description_teaser', 'departure_city', 'email', 'external_id', 'geometry', 'id', 'label_accessibility', 'name', 'pdf',
- 'portal', 'practical_info', 'published', 'reservation_id', 'reservation_system',
+ 'portal', 'practical_info', 'provider', 'published', 'reservation_id', 'reservation_system',
'source', 'structure', 'themes', 'types', 'update_datetime', 'url', 'uuid', 'website',
])
@@ -136,7 +136,7 @@
INFORMATION_DESK_PROPERTIES_JSON_STRUCTURE = sorted([
'id', 'accessibility', 'description', 'email', 'label_accessibility', 'latitude', 'longitude',
- 'municipality', 'name', 'phone', 'photo_url', 'uuid',
+ 'municipality', 'name', 'phone', 'photo_url', 'provider', 'uuid',
'postal_code', 'street', 'type', 'website'
])
@@ -147,7 +147,7 @@
SITE_PROPERTIES_JSON_STRUCTURE = sorted([
'accessibility', 'advice', 'ambiance', 'attachments', 'children', 'cities', 'courses', 'description', 'description_teaser', 'eid',
'geometry', 'id', 'information_desks', 'labels', 'managers', 'name', 'orientation', 'parent', 'period', 'portal',
- 'practice', 'pdf', 'ratings', 'sector', 'source', 'structure', 'themes', 'type', 'url', 'uuid', 'wind', 'web_links',
+ 'practice', 'provider', 'pdf', 'ratings', 'sector', 'source', 'structure', 'themes', 'type', 'url', 'uuid', 'wind', 'web_links',
])
OUTDOORPRACTICE_PROPERTIES_JSON_STRUCTURE = sorted(['id', 'name', 'sector', 'pictogram'])
@@ -158,7 +158,7 @@
SENSITIVE_AREA_PROPERTIES_JSON_STRUCTURE = sorted([
'id', 'contact', 'create_datetime', 'description', 'elevation', 'geometry',
- 'info_url', 'kml_url', 'name', 'period', 'practices', 'published', 'species_id',
+ 'info_url', 'kml_url', 'name', 'period', 'practices', 'provider', 'published', 'species_id',
'structure', 'update_datetime', 'url'
])
@@ -172,7 +172,7 @@
COURSE_PROPERTIES_JSON_STRUCTURE = sorted([
'accessibility', 'advice', 'cities', 'description', 'eid', 'equipment', 'geometry', 'height', 'id',
'length', 'name', 'ratings', 'ratings_description', 'sites', 'structure',
- 'type', 'url', 'attachments', 'max_elevation', 'min_elevation', 'parents',
+ 'type', 'url', 'attachments', 'max_elevation', 'min_elevation', 'parents', 'provider',
'pdf', 'points_reference', 'children', 'duration', 'gear', 'uuid'
])
@@ -181,7 +181,7 @@
ORGANISM_PROPERTIES_JSON_STRUCTURE = sorted(['id', 'name'])
SERVICE_DETAIL_JSON_STRUCTURE = sorted([
- 'id', 'eid', 'geometry', 'structure', 'type', 'uuid'
+ 'id', 'eid', 'geometry', 'provider', 'structure', 'type', 'uuid'
])
SERVICE_TYPE_DETAIL_JSON_STRUCTURE = sorted([
@@ -190,7 +190,7 @@
INFRASTRUCTURE_DETAIL_JSON_STRUCTURE = sorted([
'id', 'accessibility', 'attachments', 'condition', 'description', 'eid', 'geometry',
- 'implantation_year', 'maintenance_difficulty', 'name', 'structure',
+ 'implantation_year', 'maintenance_difficulty', 'name', 'provider', 'structure',
'type', 'usage_difficulty', 'uuid'
])
@@ -214,7 +214,7 @@
'id', 'accessibility', 'approved', 'attachments', 'begin_date', 'booking', 'cities', 'contact', 'create_datetime',
'description', 'description_teaser', 'duration', 'email', 'end_date', 'external_id', 'geometry',
'meeting_point', 'meeting_time', 'name', 'organizer', 'participant_number', 'pdf', 'portal',
- 'practical_info', 'published', 'source', 'speaker', 'structure', 'target_audience', 'themes',
+ 'practical_info', 'provider', 'published', 'source', 'speaker', 'structure', 'target_audience', 'themes',
'type', 'update_datetime', 'url', 'uuid', 'website'
])
@@ -225,7 +225,7 @@
SIGNAGE_DETAIL_JSON_STRUCTURE = sorted([
'id', 'attachments', 'blades', 'code', 'condition', 'description', 'eid',
'geometry', 'implantation_year', 'name', 'printed_elevation', 'sealing',
- 'structure', 'type', 'uuid'
+ 'provider', 'structure', 'type', 'uuid'
])
SIGNAGE_TYPE_DETAIL_JSON_STRUCTURE = sorted([
diff --git a/geotrek/api/v2/serializers.py b/geotrek/api/v2/serializers.py
index 5883ad90c7..c5e53adb00 100644
--- a/geotrek/api/v2/serializers.py
+++ b/geotrek/api/v2/serializers.py
@@ -181,7 +181,7 @@ class ServiceSerializer(DynamicFieldsMixin, serializers.ModelSerializer):
class Meta:
model = trekking_models.Service
- fields = ('id', 'eid', 'geometry', 'structure', 'type', 'uuid')
+ fields = ('id', 'eid', 'geometry', 'provider', 'structure', 'type', 'uuid')
class ReservationSystemSerializer(DynamicFieldsMixin, serializers.ModelSerializer):
@@ -424,7 +424,7 @@ class Meta:
'id', 'accessibility', 'attachments', 'approved', 'category', 'description',
'description_teaser', 'departure_city', 'geometry', 'label_accessibility',
'practical_info', 'url', 'cities', 'create_datetime',
- 'external_id', 'name', 'pdf', 'portal', 'published',
+ 'external_id', 'name', 'pdf', 'portal', 'provider', 'published',
'source', 'structure', 'themes',
'update_datetime', 'types', 'contact', 'email',
'website', 'reservation_system', 'reservation_id', 'uuid'
@@ -461,7 +461,7 @@ class Meta:
'cities', 'contact', 'create_datetime', 'description', 'description_teaser',
'duration', 'email', 'end_date', 'external_id', 'geometry', 'meeting_point',
'meeting_time', 'name', 'organizer', 'participant_number', 'pdf', 'portal',
- 'practical_info', 'published', 'source', 'speaker', 'structure',
+ 'practical_info', 'published', 'provider', 'source', 'speaker', 'structure',
'target_audience', 'themes', 'type', 'update_datetime', 'url', 'uuid', 'website'
)
@@ -500,7 +500,7 @@ class Meta:
fields = (
'id', 'accessibility', 'description', 'email', 'label_accessibility', 'latitude', 'longitude',
'municipality', 'name', 'phone', 'photo_url', 'uuid',
- 'postal_code', 'street', 'type', 'website'
+ 'postal_code', 'provider', 'street', 'type', 'website'
)
@@ -518,7 +518,7 @@ class Meta:
model = core_models.Path
fields = (
'id', 'comments', 'geometry', 'length_2d', 'length_3d',
- 'name', 'url', 'uuid'
+ 'name', 'provider', 'url', 'uuid'
)
@@ -699,7 +699,7 @@ class Meta:
'information_desks', 'kml', 'labels', 'length_2d', 'length_3d',
'max_elevation', 'min_elevation', 'name', 'networks', 'next',
'parents', 'parking_location', 'pdf', 'points_reference',
- 'portal', 'practice', 'ratings', 'ratings_description', 'previous', 'public_transport',
+ 'portal', 'practice', 'provider', 'ratings', 'ratings_description', 'previous', 'public_transport',
'published', 'reservation_system', 'reservation_id', 'route', 'second_external_id',
'source', 'structure', 'themes', 'update_datetime', 'url', 'uuid', 'web_links'
)
@@ -764,9 +764,9 @@ class Meta:
model = trekking_models.POI
fields = (
'id', 'description', 'external_id',
- 'geometry', 'name', 'attachments', 'published', 'type',
+ 'geometry', 'name', 'attachments', 'provider', 'published', 'type',
'type_label', 'type_pictogram', 'url', 'uuid',
- 'create_datetime', 'update_datetime',
+ 'create_datetime', 'update_datetime'
)
class ThemeSerializer(DynamicFieldsMixin, serializers.ModelSerializer):
@@ -845,7 +845,7 @@ class Meta:
fields = (
'id', 'contact', 'create_datetime', 'description', 'elevation',
'geometry', 'info_url', 'kml_url', 'name', 'period',
- 'practices', 'published', 'species_id', 'structure',
+ 'practices', 'published', 'species_id', 'provider', 'structure',
'update_datetime', 'url'
)
@@ -1055,7 +1055,7 @@ class Meta:
fields = (
'id', 'accessibility', 'advice', 'ambiance', 'attachments', 'cities', 'children', 'description',
'description_teaser', 'eid', 'geometry', 'information_desks', 'labels', 'managers',
- 'name', 'orientation', 'pdf', 'period', 'parent', 'portal', 'practice',
+ 'name', 'orientation', 'pdf', 'period', 'parent', 'portal', 'practice', 'provider',
'ratings', 'sector', 'source', 'structure', 'themes',
'type', 'url', 'uuid', 'courses', 'web_links', 'wind',
)
@@ -1115,7 +1115,7 @@ class Meta:
fields = (
'id', 'accessibility', 'advice', 'attachments', 'children', 'cities', 'description', 'duration', 'eid',
'equipment', 'gear', 'geometry', 'height', 'length', 'max_elevation',
- 'min_elevation', 'name', 'parents', 'pdf', 'points_reference', 'ratings', 'ratings_description',
+ 'min_elevation', 'name', 'parents', 'pdf', 'points_reference', 'provider', 'ratings', 'ratings_description',
'sites', 'structure', 'type', 'url', 'uuid'
)
@@ -1209,7 +1209,7 @@ def get_accessibility(self, obj):
class Meta:
model = infrastructure_models.Infrastructure
fields = ('id', 'accessibility', 'attachments', 'condition', 'description', 'eid', 'geometry', 'name',
- 'implantation_year', 'maintenance_difficulty', 'structure', 'type', 'usage_difficulty', 'uuid')
+ 'implantation_year', 'maintenance_difficulty', 'provider', 'structure', 'type', 'usage_difficulty', 'uuid')
class InfrastructureConditionSerializer(DynamicFieldsMixin, serializers.ModelSerializer):
@@ -1254,7 +1254,7 @@ class SignageSerializer(DynamicFieldsMixin, serializers.ModelSerializer):
class Meta:
model = signage_models.Signage
fields = ('id', 'attachments', 'blades', 'code', 'condition', 'description', 'eid',
- 'geometry', 'implantation_year', 'name', 'printed_elevation', 'sealing',
+ 'geometry', 'implantation_year', 'name', 'printed_elevation', 'provider', 'sealing',
'structure', 'type', 'uuid')
class SignageTypeSerializer(DynamicFieldsMixin, serializers.ModelSerializer):
diff --git a/geotrek/common/management/commands/import.py b/geotrek/common/management/commands/import.py
index 30d3581c14..e0d0eadf6f 100644
--- a/geotrek/common/management/commands/import.py
+++ b/geotrek/common/management/commands/import.py
@@ -12,7 +12,7 @@ class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument('parser', help='Parser class name in var/conf/parsers.py (or dotted syntax in python path)')
- parser.add_argument('shapefile', nargs="?")
+ parser.add_argument('filename', nargs="?", help='Optional file used to feed database')
parser.add_argument('-l', dest='limit', type=int, help='Limit number of lines to import')
parser.add_argument('--encoding', '-e', default='utf8')
@@ -41,7 +41,7 @@ def handle(self, *args, **options):
Parser = getattr(module, class_name)
except AttributeError:
raise CommandError("Failed to import parser class '{0}'".format(class_name))
- if not Parser.filename and not Parser.url and not options['shapefile']:
+ if not Parser.filename and not Parser.url and not options['filename']:
raise CommandError("File path missing")
def progress_cb(progress, line, eid):
@@ -52,7 +52,7 @@ def progress_cb(progress, line, eid):
parser = Parser(progress_cb=progress_cb, encoding=encoding)
try:
- parser.parse(options['shapefile'], limit=limit)
+ parser.parse(options['filename'], limit=limit)
except ImportError as e:
raise CommandError(e)
diff --git a/geotrek/common/parsers.py b/geotrek/common/parsers.py
index 8465580716..2726202603 100644
--- a/geotrek/common/parsers.py
+++ b/geotrek/common/parsers.py
@@ -1,4 +1,6 @@
from io import BytesIO
+import importlib
+import json
import os
import re
import requests
@@ -16,11 +18,13 @@
from os.path import dirname
from urllib.parse import urlparse
+from django.contrib.gis.geos import GEOSGeometry, WKBWriter
from django.db import models, connection
from django.db.utils import DatabaseError
from django.contrib.auth import get_user_model
from django.contrib.gis.gdal import DataSource, GDALException, CoordTransform
-from django.contrib.gis.geos import Point
+from django.contrib.gis.geos import Point, Polygon
+from django.core.exceptions import ImproperlyConfigured
from django.core.files.base import ContentFile
from django.template.loader import render_to_string
from django.utils import translation
@@ -30,9 +34,10 @@
from paperclip.models import attachment_upload
from geotrek.authent.models import default_structure
-from geotrek.common.models import FileType, Attachment
+from geotrek.common.models import FileType, Attachment, License
from geotrek.common.utils.translation import get_translated_fields
+
if 'modeltranslation' in settings.INSTALLED_APPS:
from modeltranslation.fields import TranslationField
@@ -60,6 +65,9 @@ class DownloadImportError(ImportError):
class Parser:
+ """
+ provider: Allow to differentiate multiple GeotrekParser for the same model
+ """
label = None
model = None
filename = None
@@ -72,6 +80,7 @@ class Parser:
warn_on_missing_objects = False
separator = '+'
eid = None
+ provider = None
fields = None
m2m_fields = {}
constant_fields = {}
@@ -260,6 +269,8 @@ def parse_obj(self, row, operation):
self.add_warning(str(warnings))
return
if operation == "created":
+ if hasattr(self.model, 'provider') and self.provider is not None and not self.obj.provider:
+ self.obj.provider = self.provider
self.obj.save()
else:
self.obj.save(update_fields=update_fields)
@@ -302,6 +313,8 @@ def parse_row(self, row):
self.add_warning(str(warnings))
return
objects = self.model.objects.filter(**eid_kwargs)
+ if hasattr(self.model, 'provider') and self.provider is not None:
+ objects = objects.filter(provider__exact=self.provider)
if len(objects) == 0 and self.update_only:
if self.warn_on_missing_objects:
self.add_warning(_("Bad value '{eid_val}' for field '{eid_src}'. No object with this identifier").format(eid_val=self.eid_val, eid_src=self.eid_src))
@@ -353,12 +366,16 @@ def get_mapping(self, src, val, mapping, partial):
found = True
break
if not found:
- self.add_warning(_("Bad value '{val}' for field {src}. Should contain {values}").format(val=val, src=src, separator=self.separator, values=', '.join(mapping.keys())))
+ values = [str(key) for key in mapping.keys()]
+ self.add_warning(_("Bad value '{val}' for field {src}. Should contain {values}").format(val=str(val), src=src, separator=self.separator, values=values))
return None
else:
if mapping is not None:
- if val not in mapping.keys():
- self.add_warning(_("Bad value '{val}' for field {src}. Should be {values}").format(val=val, src=src, separator=self.separator, values=', '.join(mapping.keys())))
+ if val and val not in mapping.keys():
+ values = [str(key) for key in mapping.keys()]
+ self.add_warning(_("Bad value '{val}' for field {src}. Should be in {values}").format(val=str(val), src=src, separator=self.separator, values=values))
+ return None
+ if not val:
return None
val = mapping[val]
return val
@@ -388,7 +405,8 @@ def filter_m2m(self, src, val, model, field, mapping=None, partial=False, create
val = val.split(self.separator)
dst = []
for subval in val:
- subval = subval.strip()
+ if isinstance(subval, str):
+ subval = subval.strip()
subval = self.get_mapping(src, subval, mapping, partial)
if subval is None:
continue
@@ -432,6 +450,8 @@ def get_to_delete_kwargs(self):
kwargs[dst] = field.remote_field.model.objects.get(**filters)
except field.remote_field.model.DoesNotExist:
return None
+ if hasattr(self.model, 'provider') and self.provider is not None:
+ kwargs['provider__exact'] = self.provider
return kwargs
def start(self):
@@ -449,7 +469,7 @@ def parse(self, filename=None, limit=None):
if filename:
self.filename = filename
if not self.url and not self.filename:
- raise GlobalImportError(_("Filename is required"))
+ raise GlobalImportError(_("Filename or url is required"))
if self.filename and not os.path.exists(self.filename):
raise GlobalImportError(_("File does not exists at: {filename}").format(filename=self.filename))
self.start()
@@ -594,10 +614,13 @@ def has_size_changed(self, url, attachment):
if parsed_url.scheme == 'http' or parsed_url.scheme == 'https':
try:
response = self.request_or_retry(url, verb='head')
- except DownloadImportError as e:
+ except (requests.exceptions.ConnectionError, DownloadImportError) as e:
raise ValueImportError('Failed to load attachment: {exc}'.format(exc=e))
size = response.headers.get('content-length')
- return size is not None and int(size) != attachment.attachment_file.size
+ try:
+ return size is not None and int(size) != attachment.attachment_file.size
+ except FileNotFoundError:
+ pass
return True
@@ -606,14 +629,14 @@ def download_attachment(self, url):
if parsed_url.scheme == 'ftp':
try:
response = self.request_or_retry(url)
- except DownloadImportError as e:
+ except (DownloadImportError, requests.exceptions.ConnectionError) as e:
raise ValueImportError('Failed to load attachment: {exc}'.format(exc=e))
return response.read()
else:
if self.download_attachments:
try:
response = self.request_or_retry(url)
- except DownloadImportError as e:
+ except (DownloadImportError, requests.exceptions.ConnectionError) as e:
raise ValueImportError('Failed to load attachment: {exc}'.format(exc=e))
if response.status_code != requests.codes.ok:
self.add_warning(_("Failed to download '{url}'").format(url=url))
@@ -621,69 +644,94 @@ def download_attachment(self, url):
return response.content
return None
- def save_attachments(self, src, val):
- updated = False
- attachments_to_delete = list(Attachment.objects.attachments_for_object(self.obj))
+ def check_attachment_updated(self, attachments_to_delete, updated, **kwargs):
+ found = False
+ for attachment in attachments_to_delete:
+ upload_name, ext = os.path.splitext(attachment_upload(attachment, kwargs.get('name')))
+ existing_name = attachment.attachment_file.name
+ if re.search(r"^{name}(_[a-zA-Z0-9]{{7}})?{ext}$".format(
+ name=upload_name, ext=ext), existing_name
+ ) and not self.has_size_changed(kwargs.get('url'), attachment):
+ found = True
+ attachments_to_delete.remove(attachment)
+ if kwargs.get('author') != attachment.author or kwargs.get('legend') != attachment.legend:
+ attachment.author = kwargs.get('author')
+ attachment.legend = textwrap.shorten(kwargs.get('legend'), width=127)
+ attachment.save()
+ updated = True
+ break
+ return found, updated
+
+ def generate_content_attachment(self, attachment, parsed_url, url, updated, name):
+ if (parsed_url.scheme in ('http', 'https') and self.download_attachments) or parsed_url.scheme == 'ftp':
+ content = self.download_attachment(url)
+ if content is None:
+ return False, updated
+ f = ContentFile(content)
+ if settings.PAPERCLIP_MAX_BYTES_SIZE_IMAGE and settings.PAPERCLIP_MAX_BYTES_SIZE_IMAGE < f.size:
+ logger.warning(
+ _(f'{self.obj.__class__.__name__} #{self.obj.pk} - {url} : downloaded file is too large'))
+ return False, updated
+ try:
+ image = Image.open(BytesIO(content))
+ if settings.PAPERCLIP_MIN_IMAGE_UPLOAD_WIDTH and settings.PAPERCLIP_MIN_IMAGE_UPLOAD_WIDTH > image.width:
+ logger.warning(
+ _(f"{self.obj.__class__.__name__} #{self.obj.pk} - {url} : downloaded file is not wide enough"))
+ return False, updated
+ if settings.PAPERCLIP_MIN_IMAGE_UPLOAD_HEIGHT and settings.PAPERCLIP_MIN_IMAGE_UPLOAD_HEIGHT > image.height:
+ logger.warning(
+ _(f"{self.obj.__class__.__name__} #{self.obj.pk} - {url} : downloaded file is not tall enough"))
+ return False, updated
+ except UnidentifiedImageError:
+ pass
+ attachment.attachment_file.save(name, f, save=False)
+ else:
+ attachment.attachment_link = url
+ return True, updated
+
+ def remove_attachments(self, attachments_to_delete):
+ if self.delete_attachments:
+ for att in attachments_to_delete:
+ att.delete()
+
+ def generate_attachment(self, **kwargs):
+ attachment = Attachment()
+ attachment.content_object = self.obj
+ attachment.filetype = self.filetype
+ attachment.creator = self.creator
+ attachment.author = kwargs.get('author')
+ attachment.legend = textwrap.shorten(kwargs.get('legend'), width=127)
+ return attachment
+
+ def generate_attachments(self, src, val, attachments_to_delete, updated):
+ attachments = []
for url, legend, author in self.filter_attachments(src, val):
url = self.base_url + url
legend = legend or ""
author = author or ""
basename, ext = os.path.splitext(os.path.basename(url))
name = '%s%s' % (basename[:128], ext)
- found = False
- for attachment in attachments_to_delete:
- upload_name, ext = os.path.splitext(attachment_upload(attachment, name))
- existing_name = attachment.attachment_file.name
- if re.search(r"^{name}(_[a-zA-Z0-9]{{7}})?{ext}$".format(
- name=upload_name, ext=ext), existing_name
- ) and not self.has_size_changed(url, attachment):
- found = True
- attachments_to_delete.remove(attachment)
- if author != attachment.author or legend != attachment.legend:
- attachment.author = author
- attachment.legend = textwrap.shorten(legend, width=127)
- attachment.save()
- updated = True
- break
+ found, updated = self.check_attachment_updated(attachments_to_delete, updated, name=name, url=url,
+ legend=legend, author=author)
if found:
continue
parsed_url = urlparse(url)
-
- attachment = Attachment()
- attachment.content_object = self.obj
- attachment.filetype = self.filetype
- attachment.creator = self.creator
- attachment.author = author
- attachment.legend = textwrap.shorten(legend, width=127)
-
- if (parsed_url.scheme in ('http', 'https') and self.download_attachments) or parsed_url.scheme == 'ftp':
- content = self.download_attachment(url)
- if content is None:
- continue
- f = ContentFile(content)
- if settings.PAPERCLIP_MAX_BYTES_SIZE_IMAGE and settings.PAPERCLIP_MAX_BYTES_SIZE_IMAGE < f.size:
- logger.warning(_(f'{self.obj.__class__.__name__} #{self.obj.pk} - {url} : downloaded file is too large'))
- return updated
- try:
- image = Image.open(BytesIO(content))
- if settings.PAPERCLIP_MIN_IMAGE_UPLOAD_WIDTH and settings.PAPERCLIP_MIN_IMAGE_UPLOAD_WIDTH > image.width:
- logger.warning(_(f"{self.obj.__class__.__name__} #{self.obj.pk} - {url} : downloaded file is not wide enough"))
- return updated
- if settings.PAPERCLIP_MIN_IMAGE_UPLOAD_HEIGHT and settings.PAPERCLIP_MIN_IMAGE_UPLOAD_HEIGHT > image.height:
- logger.warning(_(f"{self.obj.__class__.__name__} #{self.obj.pk} - {url} : downloaded file is not tall enough"))
- return updated
- except UnidentifiedImageError:
- pass
- attachment.attachment_file.save(name, f, save=False)
- else:
- attachment.attachment_link = url
- attachment.save()
+ attachment = self.generate_attachment(author=author, legend=legend)
+ save, updated = self.generate_content_attachment(attachment, parsed_url, url, updated, name)
+ if not save:
+ continue
+ attachments.append(attachment)
updated = True
+ return updated, attachments
- if self.delete_attachments:
- for att in attachments_to_delete:
- att.delete()
+ def save_attachments(self, src, val):
+ updated = False
+ attachments_to_delete = list(Attachment.objects.attachments_for_object(self.obj))
+ updated, attachments = self.generate_attachments(src, val, attachments_to_delete, updated)
+ Attachment.objects.bulk_create(attachments)
+
+ self.remove_attachments(attachments_to_delete)
return updated
@@ -860,3 +908,283 @@ def next_row(self):
def normalize_field_name(self, name):
return name
+
+
+class GeotrekAggregatorParser:
+ filename = None
+ url = None
+
+ mapping_model_parser = {
+ "Trek": ("geotrek.trekking.parsers", "GeotrekTrekParser"),
+ "POI": ("geotrek.trekking.parsers", "GeotrekPOIParser"),
+ "Service": ("geotrek.trekking.parsers", "GeotrekServiceParser"),
+ "InformationDesk": ("geotrek.tourism.parsers", "GeotrekInformationDeskParser"),
+ "TouristicContent": ("geotrek.tourism.parsers", "GeotrekTouristicContentParser"),
+ "TouristicEvent": ("geotrek.tourism.parsers", "GeotrekTouristicEventParser"),
+ "Signage": ("geotrek.signage.parsers", "GeotrekSignageParser"),
+ "Infrastructure": ("geotrek.infrastructure.parsers", "GeotrekInfrastructureParser"),
+ }
+
+ invalid_model_topology = ['Trek', 'POI', 'Service', 'Signage', 'Infrastructure']
+
+ def __init__(self, progress_cb=None, user=None, encoding='utf8'):
+ self.progress_cb = progress_cb
+ self.user = user
+ self.encoding = encoding
+ self.line = 0
+ self.nb_success = 0
+ self.nb_created = 0
+ self.nb_updated = 0
+ self.nb_unmodified = 0
+ self.progress_cb = progress_cb
+ self.warnings = {}
+ self.report_by_api_v2_by_type = {}
+
+ def add_warning(self, key, msg):
+ warnings = self.warnings.setdefault(key, [])
+ warnings.append(msg)
+
+ def parse(self, filename=None, limit=None):
+ filename = filename if filename else self.filename
+ if not os.path.exists(filename):
+ raise GlobalImportError(_(f"File does not exists at: {filename}"))
+ with open(filename, mode='r') as f:
+ json_aggregator = json.load(f)
+
+ for key, datas in json_aggregator.items():
+ self.report_by_api_v2_by_type[key] = {}
+ models_to_import = datas.get('data_to_import')
+ if not models_to_import:
+ models_to_import = self.mapping_model_parser.keys()
+ for model in models_to_import:
+ Parser = None
+ if settings.TREKKING_TOPOLOGY_ENABLED:
+ if model in self.invalid_model_topology:
+ warning = f"{model}s can't be imported with dynamic segmentation"
+ logger.warning(warning)
+ key_warning = _(f"Model {model}")
+ self.add_warning(key_warning, warning)
+ else:
+ module_name, class_name = self.mapping_model_parser[model]
+ module = importlib.import_module(module_name)
+ parser = getattr(module, class_name)
+ if 'url' not in datas:
+ warning = f"{key} has no url"
+ key_warning = _("Geotrek-admin")
+ self.add_warning(key_warning, warning)
+ else:
+ Parser = parser(progress_cb=self.progress_cb, provider=key, url=datas['url'],
+ portals_filter=datas.get('portals'), mapping=datas.get('mapping'),
+ create_categories=datas.get('create'), all_datas=datas.get('all_datas'))
+ self.progress_cb(0, 0, f'{model} ({key})')
+ Parser.parse()
+
+ self.report_by_api_v2_by_type[key][model] = {
+ 'nb_lines': Parser.line if Parser else 0,
+ 'nb_success': Parser.nb_success if Parser else 0,
+ 'nb_created': Parser.nb_created if Parser else 0,
+ 'nb_updated': Parser.nb_updated if Parser else 0,
+ 'nb_deleted': len(Parser.to_delete) if Parser and Parser.delete else None,
+ 'nb_unmodified': Parser.nb_unmodified if Parser else 0,
+ 'warnings': Parser.warnings if Parser else self.warnings
+ }
+
+ def report(self, output_format='txt'):
+ context = {'report': self.report_by_api_v2_by_type}
+ return render_to_string('common/parser_report_aggregator.{output_format}'.format(output_format=output_format), context)
+
+
+class GeotrekParser(AttachmentParserMixin, Parser):
+ """
+ url_categories: url of the categories in api v2 (example: 'category': '/api/v2/touristiccontent_category/')
+ replace_fields: Replace fields which have not the same name in the api v2 compare to models (geom => geometry in api v2)
+ m2m_replace_fields: Replace m2m fields which have not the same name in the api v2 compare to models (geom => geometry in api v2)
+ categories_keys_api_v2: Key in the route of the category (example: /api/v2/touristiccontent_category/) corresponding to the model field
+ provider: Allow to differentiate multiple GeotrekParser for the same model
+ portals_filter: Portals which will be use for filter in api v2 (default: No portal filter)
+ mapping: Mapping between values in categories (example: /api/v2/touristiccontent_category/) and final values
+ Can be use when you want to change a value from the api/v2
+ create_categories: Create all categories during importation
+ all_datas: Import all datas and do not use updated_after filter
+ """
+ model = None
+ next_url = ''
+ url = None
+ separator = None
+ delete = True
+ eid = 'eid'
+ constant_fields = {}
+ url_categories = {}
+ replace_fields = {}
+ m2m_replace_fields = {}
+ categories_keys_api_v2 = {}
+ non_fields = {
+ 'attachments': "attachments",
+ }
+ field_options = {
+ 'geom': {'required': True},
+ }
+ bbox = None
+ portals_filter = None
+ mapping = {}
+ create_categories = False
+ all_datas = False
+ provider = None
+
+ def __init__(self, all_datas=None, create_categories=None, provider=None, mapping=None, portals_filter=None, url=None, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.bbox = Polygon.from_bbox(settings.SPATIAL_EXTENT)
+ self.bbox.srid = settings.SRID
+ self.bbox.transform(4326) # WGS84
+ self.portals_filter = portals_filter
+ self.url = url if url else self.url
+ self.mapping = mapping if mapping else self.mapping
+ self.provider = provider if provider else self.provider
+ self.all_datas = all_datas if all_datas else self.all_datas
+ self.create_categories = create_categories if create_categories else self.create_categories
+ self.fields = dict((f.name, f.name) for f in self.model._meta.fields if not isinstance(f, TranslationField) and f.name != 'id')
+ self.m2m_fields = {
+ f.name: f.name
+ for f in self.model._meta.many_to_many
+ }
+ # Replace automatics fields and m2m_fields generated above
+ for key, value in self.replace_fields.items():
+ self.fields[key] = value
+
+ for key, value in self.m2m_replace_fields.items():
+ self.m2m_fields[key] = value
+ self.translated_fields = [field for field in get_translated_fields(self.model)]
+ # Generate a mapping dictionnary between id and the related label
+ for category, route in self.url_categories.items():
+ if self.categories_keys_api_v2.get(category):
+ response = self.request_or_retry(f"{self.url}/api/v2/{route}")
+ self.field_options.setdefault(category, {})
+ self.field_options[category]["mapping"] = {}
+ if self.create_categories:
+ self.field_options[category]["create"] = True
+ results = response.json().get('results', [])
+ # for element in category url map the id with its label
+ for result in results:
+ id_result = result['id']
+ label = result[self.categories_keys_api_v2[category]]
+ if isinstance(result[self.categories_keys_api_v2[category]], dict):
+ if label[settings.MODELTRANSLATION_DEFAULT_LANGUAGE]:
+ self.field_options[category]["mapping"][id_result] = self.replace_mapping(label[settings.MODELTRANSLATION_DEFAULT_LANGUAGE], route)
+ else:
+ if label:
+ self.field_options[category]["mapping"][id_result] = self.replace_mapping(label, category)
+ else:
+ raise ImproperlyConfigured(f"{category} is not configured in categories_keys_api_v2")
+ self.creator, created = get_user_model().objects.get_or_create(username='import', defaults={'is_active': False})
+
+ def replace_mapping(self, label, route):
+ for key, list_map in self.mapping.get(route, {}).items():
+ if label in list_map:
+ return key
+ return label
+
+ def generate_attachments(self, src, val, attachments_to_delete, updated):
+ attachments = []
+ for url, legend, author, license in self.filter_attachments(src, val):
+ url = self.base_url + url
+ legend = legend or ""
+ author = author or ""
+ license = License.objects.get_or_create(label=license)[0] if license else None
+ basename, ext = os.path.splitext(os.path.basename(url))
+ name = '%s%s' % (basename[:128], ext)
+ found, updated = self.check_attachment_updated(attachments_to_delete, updated, name=name, url=url,
+ legend=legend, author=author)
+ if found:
+ continue
+
+ parsed_url = urlparse(url)
+ attachment = self.generate_attachment(author=author, legend=legend, license=license)
+ save, updated = self.generate_content_attachment(attachment, parsed_url, url, updated, name)
+ if not save:
+ continue
+ attachments.append(attachment)
+ updated = True
+ return updated, attachments
+
+ def generate_attachment(self, **kwargs):
+ attachment = Attachment()
+ attachment.content_object = self.obj
+ attachment.filetype = self.filetype
+ attachment.creator = self.creator
+ attachment.author = kwargs.get('author')
+ attachment.legend = textwrap.shorten(kwargs.get('legend'), width=127)
+ attachment.license = kwargs.get('license')
+ return attachment
+
+ def start(self):
+ super().start()
+ kwargs = self.get_to_delete_kwargs()
+ json_id_key = self.replace_fields.get('eid', 'id')
+ params = {
+ 'fields': json_id_key,
+ 'page_size': 10000
+ }
+ response = self.request_or_retry(self.next_url, params=params)
+ ids = [f"{element[json_id_key]}" for element in response.json().get('results', [])]
+ self.to_delete = set(self.model.objects.filter(**kwargs).exclude(eid__in=ids).values_list('pk', flat=True))
+
+ def filter_attachments(self, src, val):
+ return [(subval.get('url'), subval.get('legend'), subval.get('author'), subval.get('license')) for subval in val]
+
+ def apply_filter(self, dst, src, val):
+ val = super().apply_filter(dst, src, val)
+ if dst in self.translated_fields:
+ if isinstance(val, dict):
+ for key, final_value in val.items():
+ if key in settings.MODELTRANSLATION_LANGUAGES:
+ self.set_value(f'{dst}_{key}', src, final_value)
+ val = val.get(settings.MODELTRANSLATION_DEFAULT_LANGUAGE)
+ return val
+
+ def normalize_field_name(self, name):
+ return name
+
+ @property
+ def items(self):
+ return self.root['results']
+
+ def filter_geom(self, src, val):
+ geom = GEOSGeometry(json.dumps(val))
+ geom.transform(settings.SRID)
+ geom = WKBWriter().write(geom)
+ geom = GEOSGeometry(geom)
+ return geom
+
+ def next_row(self):
+ """Returns next row.
+ Geotrek API is paginated, run until "next" is empty
+ :returns row
+ """
+ portals = self.portals_filter
+ updated_after = None
+
+ available_fields = [field.name for field in self.model._meta.get_fields()]
+ if not self.all_datas and self.model.objects.filter(provider__exact=self.provider).exists() and 'date_update' in available_fields:
+ updated_after = self.model.objects.filter(provider__exact=self.provider).latest('date_update').date_update.strftime('%Y-%m-%d')
+ params = {
+ 'in_bbox': ','.join([str(coord) for coord in self.bbox.extent]),
+ 'portals': ','.join(portals) if portals else '',
+ 'updated_after': updated_after
+ }
+ response = self.request_or_retry(self.next_url, params=params)
+ self.root = response.json()
+ self.nb = int(self.root['count'])
+
+ for row in self.items:
+ yield row
+ self.next_url = self.root['next']
+
+ while self.next_url:
+ response = self.request_or_retry(self.next_url)
+ self.root = response.json()
+ self.nb = int(self.root['count'])
+
+ for row in self.items:
+ yield row
+
+ self.next_url = self.root['next']
diff --git a/geotrek/common/templates/common/parser_report_aggregator.txt b/geotrek/common/templates/common/parser_report_aggregator.txt
new file mode 100644
index 0000000000..8b4dbd127f
--- /dev/null
+++ b/geotrek/common/templates/common/parser_report_aggregator.txt
@@ -0,0 +1,8 @@
+{% for key, value in report.items %}
+{{ key }} :
+______________________________________________
+{% for model, report_by_model in value.items %}
+{{ model }} :
+{% include "common/parser_report.txt" with nb_success=report_by_model.nb_success nb_created=report_by_model.nb_created nb_deleted=report_by_model.nb_deleted nb_updated=report_by_model.nb_updated nb_lines=report_by_model.nb_lines nb_unmodified=report_by_model.nb_unmodified warnings=report_by_model.warnings %}
+{% endfor %}
+{% endfor %}
\ No newline at end of file
diff --git a/geotrek/common/tests/data/attachment.xls b/geotrek/common/tests/data/attachment.xls
new file mode 100644
index 0000000000..71083411e8
Binary files /dev/null and b/geotrek/common/tests/data/attachment.xls differ
diff --git a/geotrek/common/tests/data/attachment_no_legend.xls b/geotrek/common/tests/data/attachment_no_legend.xls
new file mode 100644
index 0000000000..c0f7f9e620
Binary files /dev/null and b/geotrek/common/tests/data/attachment_no_legend.xls differ
diff --git a/geotrek/common/tests/data/geotrek_parser_v2/config_aggregator.json b/geotrek/common/tests/data/geotrek_parser_v2/config_aggregator.json
new file mode 100644
index 0000000000..9ed9af9877
--- /dev/null
+++ b/geotrek/common/tests/data/geotrek_parser_v2/config_aggregator.json
@@ -0,0 +1,26 @@
+{
+ "URL_1": {
+ "url": "URL_1",
+ "data_to_import": [
+ "Trek",
+ "POI"
+ ],
+ "mapping": {
+ "trek_practice": {
+ "Randonnée Pédestre": [
+ "Pédestre"
+ ],
+ "Vélo": [
+ "VTT",
+ "Vélo"
+ ]
+ },
+ "trek_difficulty": {
+ "Moyen": [
+ "Facile"
+ ]
+ }
+ },
+ "create": true
+ }
+}
\ No newline at end of file
diff --git a/geotrek/common/tests/data/geotrek_parser_v2/config_aggregator_ds.json b/geotrek/common/tests/data/geotrek_parser_v2/config_aggregator_ds.json
new file mode 100644
index 0000000000..1ed8fcd956
--- /dev/null
+++ b/geotrek/common/tests/data/geotrek_parser_v2/config_aggregator_ds.json
@@ -0,0 +1,30 @@
+{
+ "URL_1": {
+ "url": "url_1",
+ "data_to_import": ["Service", "POI"],
+ "portals": null,
+ "mapping": {
+ "trek_practice": {
+ "Randonnée Pédestre": ["Pédestre"],
+ "Vélo": ["VTT", "Vélo"]
+ },
+ "trek_difficulty": {
+ "Moyen": ["Facile"]
+ }
+ }
+ },
+ "URL_2": {
+ "url": "url_2",
+ "data_to_import": ["Trek", "POI"],
+ "portals": null,
+ "mapping": {
+ "trek_practice": {
+ "Randonnée Pédestre": ["Pédestre"],
+ "Vélo": ["VTT", "Vélo"]
+ },
+ "trek_difficulty": {
+ "Moyen": ["Facile"]
+ }
+ }
+ }
+}
diff --git a/geotrek/common/tests/data/geotrek_parser_v2/config_aggregator_multiple_admin.json b/geotrek/common/tests/data/geotrek_parser_v2/config_aggregator_multiple_admin.json
new file mode 100644
index 0000000000..7b79a1e9c5
--- /dev/null
+++ b/geotrek/common/tests/data/geotrek_parser_v2/config_aggregator_multiple_admin.json
@@ -0,0 +1,46 @@
+{
+ "URL_1": {
+ "url": "URL_1",
+ "data_to_import": ["Trek", "POI"],
+ "portals": ["portal a", "portal b"],
+ "mapping": {
+ "trek_practice": {
+ "Randonnée Pédestre": ["Pédestre"],
+ "Vélo": ["VTT", "Vélo"]
+ },
+ "trek_difficulty": {
+ "Moyen": ["Facile"]
+ }
+ }
+ },
+ "URL_2": {
+ "url": "URL_2",
+ "data_to_import": ["Trek", "Service", "POI"],
+ "portals": null,
+ "mapping": {
+ "trek_practice": {
+ "Randonnée Pédestre": ["Pédestre"],
+ "Vélo": ["VTT", "Vélo"]
+ },
+ "trek_difficulty": {
+ "Moyen": ["Facile"]
+ }
+ },
+ "create": true
+ },
+ "URL_3": {
+ "url": "URL_3",
+ "data_to_import": ["POI", "InformationDesk", "TouristicContent"],
+ "portals": null,
+ "mapping": {
+ "trek_practice": {
+ "Randonnée Pédestre": ["Pédestre"],
+ "Vélo": ["VTT", "Vélo"]
+ },
+ "trek_difficulty": {
+ "Moyen": ["Facile"]
+ }
+ },
+ "create": true
+ }
+}
diff --git a/geotrek/common/tests/data/geotrek_parser_v2/config_aggregator_no_data_to_import.json b/geotrek/common/tests/data/geotrek_parser_v2/config_aggregator_no_data_to_import.json
new file mode 100644
index 0000000000..cf64abab46
--- /dev/null
+++ b/geotrek/common/tests/data/geotrek_parser_v2/config_aggregator_no_data_to_import.json
@@ -0,0 +1,22 @@
+{
+ "URL_1": {
+ "url": "URL_1",
+ "mapping": {
+ "trek_practice": {
+ "Randonnée Pédestre": [
+ "Pédestre"
+ ],
+ "Vélo": [
+ "VTT",
+ "Vélo"
+ ]
+ },
+ "trek_difficulty": {
+ "Moyen": [
+ "Facile"
+ ]
+ }
+ },
+ "create": true
+ }
+}
diff --git a/geotrek/common/tests/data/geotrek_parser_v2/config_aggregator_no_url.json b/geotrek/common/tests/data/geotrek_parser_v2/config_aggregator_no_url.json
new file mode 100644
index 0000000000..1fe779a8f0
--- /dev/null
+++ b/geotrek/common/tests/data/geotrek_parser_v2/config_aggregator_no_url.json
@@ -0,0 +1,15 @@
+{
+ "URL_1": {
+ "data_to_import": ["Trek", "POI"],
+ "portals": ["portal a", "portal b"],
+ "mapping": {
+ "trek_practice": {
+ "Randonnée Pédestre": ["Pédestre"],
+ "Vélo": ["VTT", "Vélo"]
+ },
+ "trek_difficulty": {
+ "Moyen": ["Facile"]
+ }
+ }
+ }
+}
diff --git a/geotrek/common/tests/data/geotrek_parser_v2/treks.json b/geotrek/common/tests/data/geotrek_parser_v2/treks.json
new file mode 100644
index 0000000000..ba0d59a5b7
--- /dev/null
+++ b/geotrek/common/tests/data/geotrek_parser_v2/treks.json
@@ -0,0 +1,217 @@
+{
+ "count": 1,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "id": 8702,
+ "access": {
+ "fr": "Accès",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "accessibilities": [],
+ "accessibility_advice": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "accessibility_covering": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "accessibility_exposure": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "accessibility_level": null,
+ "accessibility_signage": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "accessibility_slope": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "accessibility_width": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "advice": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "advised_parking": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "altimetric_profile": "https://foo.fr/api/v2/trek/8702/profile/",
+ "ambiance": {
+ "fr": "Ambiance",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "arrival": {
+ "fr": "Étangs de Picot",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "ascent": 797,
+ "attachments": [],
+ "attachments_accessibility": [],
+ "children": [],
+ "cities": [
+ "09030"
+ ],
+ "create_datetime": "2019-04-01T13:04:06.795861Z",
+ "departure": {
+ "fr": "Barrage de Soulcem",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "departure_city": "09030",
+ "departure_geom": [
+ 1.4526560730280935,
+ 42.677959776888834
+ ],
+ "descent": -65,
+ "description": {
+ "fr": "
Description Lorem ipsum sit dolor amet Description Lorem ipsum sit dolor ametDescription Lorem ipsum sit dolor ametDescription Lorem ipsum sit dolor ametDescription Lorem ipsum sit dolor ametDescription Lorem ipsum sit dolor ametDescription Lorem ipsum sit dolor ametDescription Lorem ipsum sit dolor ametDescription Lorem ipsum sit dolor ametDescription Lorem ipsum sit dolor ametDescription Lorem ipsum sit dolor ametDescription Lorem ipsum sit dolor ametDescription Lorem ipsum sit dolor amet
\r\n\r\n- \r\n
2tape numéro 1 sit dolor ametDescription Lorem ipsum sit dolor ametDescription Lorem ipsum sit dolor ametDescription Lorem ipsum sit dolor ametDescription Lorem ipsum sit dolor ametDescription Lorem ipsum sit dolor ametDesc
\r\n \r\n- \r\n
Prendre à droite ametDescription Lorem ipsum sit dolor ametDescription Lorem ipsum sit dolor ametDescription Lorem ipsum sit dolor ametDescription Lorem ipsum sit dolor ametDescription Lorem ipsum sit dolor ametDescription Lor
\r\n \r\n- Terminer tout droit
\r\n
\r\nDescription Lorem ipsum sit dolor ametDescription Lorem ipsum sit dolor ametDescription Lorem ipsum sit dolor ametDescription Lorem ipsum sit dolor amet
",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "description_teaser": {
+ "fr": "Chapeau",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "difficulty": 3,
+ "disabled_infrastructure": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "duration": 5.0,
+ "elevation_area_url": "https://foo.fr/api/v2/trek/8702/dem/",
+ "elevation_svg_url": "https://foo.fr/api/v2/trek/8702/profile/?language=fr&format=svg",
+ "external_id": null,
+ "gear": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "geometry": {
+ "type": "LineString",
+ "coordinates": [
+ [
+ 1.4526561,
+ 42.6779598,
+ 1552.0
+ ],
+ [
+ 1.4526694,
+ 42.6779712,
+ 1554.0
+ ]
+ ]
+ },
+ "gpx": "https://foo.fr/api/fr/treks/8702/etangs-du-picot.gpx",
+ "information_desks": [],
+ "kml": "https://foo.fr/api/fr/treks/8702/etangs-du-picot.kml",
+ "labels": [],
+ "length_2d": 3347.5,
+ "length_3d": 3536.2,
+ "max_elevation": 2298,
+ "min_elevation": 1537,
+ "name": {
+ "fr": "Étangs du Picot",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "networks": [],
+ "next": {},
+ "parents": [],
+ "parking_location": null,
+ "pdf": {
+ "fr": "https://foo.fr/api/fr/treks/8702/etangs-du-picot.pdf",
+ "en": "https://foo.fr/api/en/treks/8702/etangs-du-picot.pdf",
+ "es": "https://foo.fr/api/es/treks/8702/etangs-du-picot.pdf",
+ "it": "https://foo.fr/api/it/treks/8702/etangs-du-picot.pdf"
+ },
+ "points_reference": {
+ "type": "MultiPoint",
+ "coordinates": [
+ [
+ 1.451697349548341,
+ 42.6817728861541
+ ],
+ [
+ 1.456890106201172,
+ 42.68590558513195
+ ],
+ [
+ 1.465902328491211,
+ 42.68448598671003
+ ]
+ ]
+ },
+ "portal": [],
+ "practice": 4,
+ "ratings": [],
+ "ratings_description": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "previous": {},
+ "public_transport": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "published": {
+ "fr": true,
+ "en": false,
+ "es": false,
+ "it": false
+ },
+ "reservation_system": null,
+ "reservation_id": "",
+ "route": 2,
+ "second_external_id": null,
+ "source": [],
+ "structure": 3,
+ "themes": [],
+ "update_datetime": "2022-04-11T14:52:42.637165Z",
+ "url": "https://foo.fr/api/v2/trek/8702/",
+ "uuid": "58ed4fc1-645d-4bf6-b956-71f0a01a5eec",
+ "web_links": []
+ }
+ ]
+}
diff --git a/geotrek/common/tests/data/organism5.xls b/geotrek/common/tests/data/organism5.xls
new file mode 100644
index 0000000000..9d2244ae8c
Binary files /dev/null and b/geotrek/common/tests/data/organism5.xls differ
diff --git a/geotrek/common/tests/mixins.py b/geotrek/common/tests/mixins.py
index d02b444a87..50693bfbc8 100644
--- a/geotrek/common/tests/mixins.py
+++ b/geotrek/common/tests/mixins.py
@@ -1,3 +1,6 @@
+import json
+import os
+
def dictfetchall(cursor):
"Return all rows from a cursor as a dict"
@@ -6,3 +9,12 @@ def dictfetchall(cursor):
dict(zip(columns, row))
for row in cursor.fetchall()
]
+
+
+class GeotrekParserTestMixin:
+ def mock_json(self):
+ filename = os.path.join('geotrek', self.app_label, 'tests', 'data', 'geotrek_parser_v2',
+ self.mock_json_order[self.mock_time])
+ self.mock_time += 1
+ with open(filename, 'r') as f:
+ return json.load(f)
diff --git a/geotrek/common/tests/test_parsers.py b/geotrek/common/tests/test_parsers.py
index 83eed5104d..33bd69f0fc 100644
--- a/geotrek/common/tests/test_parsers.py
+++ b/geotrek/common/tests/test_parsers.py
@@ -1,27 +1,34 @@
+import json
import os
-from unittest import mock
+import urllib
+from io import StringIO
from shutil import rmtree
from tempfile import mkdtemp
-from io import StringIO
-from requests import Response
-import urllib
+from unittest import mock, skipIf
-from django.test import TestCase
+import requests
from django.conf import settings
+from django.core.exceptions import ImproperlyConfigured
from django.core.management import call_command
from django.core.management.base import CommandError
from django.db.utils import DatabaseError
-from django.test.utils import override_settings
from django.template.exceptions import TemplateDoesNotExist
+from django.test import TestCase
+from django.test.utils import override_settings
+from requests import Response
from geotrek.authent.tests.factories import StructureFactory
-from geotrek.trekking.models import Trek
-from geotrek.common.models import Organism, FileType, Attachment
-from geotrek.common.parsers import (
- ExcelParser, AttachmentParserMixin, TourInSoftParser, ValueImportError, DownloadImportError,
- TourismSystemParser, OpenSystemParser,
-)
+from geotrek.common.models import Attachment, FileType, Organism
+from geotrek.common.parsers import (AttachmentParserMixin, DownloadImportError,
+ ExcelParser, GeotrekAggregatorParser,
+ GeotrekParser, OpenSystemParser,
+ TourInSoftParser, TourismSystemParser,
+ ValueImportError)
+from geotrek.common.tests.mixins import GeotrekParserTestMixin
from geotrek.common.utils.testdata import get_dummy_img
+from geotrek.trekking.models import POI, Trek
+from geotrek.trekking.parsers import GeotrekTrekParser
+from geotrek.trekking.tests.factories import TrekFactory
class OrganismParser(ExcelParser):
@@ -35,10 +42,45 @@ class OrganismEidParser(ExcelParser):
eid = 'organism'
+class StructureExcelParser(ExcelParser):
+ model = Organism
+ fields = {
+ 'organism': 'nOm',
+ 'structure': 'structure'
+ }
+ eid = 'organism'
+
+
+class OrganismNoMappingNoPartialParser(StructureExcelParser):
+ field_options = {
+ "structure": {"mapping": {"foo": "bar", "": "boo"}}
+ }
+ natural_keys = {
+ "structure": "name"
+ }
+
+
+class OrganismNoMappingPartialParser(StructureExcelParser):
+ field_options = {
+ "structure": {"mapping": {"foo": "bar"}, "partial": True}
+ }
+ natural_keys = {
+ "structure": "name"
+ }
+
+
+class OrganismNoNaturalKeysParser(StructureExcelParser):
+ warn_on_missing_fields = True
+
+
class AttachmentParser(AttachmentParserMixin, OrganismEidParser):
non_fields = {'attachments': 'photo'}
+class WarnAttachmentParser(AttachmentParser):
+ warn_on_missing_fields = True
+
+
class AttachmentLegendParser(AttachmentParser):
def filter_attachments(self, src, val):
@@ -126,6 +168,24 @@ def test_databaseerror_except(self, mock_parse_row):
call_command('import', 'geotrek.common.tests.test_parsers.OrganismEidParser', filename, verbosity=2, stdout=output)
self.assertIn('foo bar', output.getvalue())
+ def test_fk_not_in_natural_keys(self):
+ output = StringIO()
+ filename = os.path.join(os.path.dirname(__file__), 'data', 'organism5.xls')
+ call_command('import', 'geotrek.common.tests.test_parsers.OrganismNoNaturalKeysParser', filename, verbosity=2, stdout=output)
+ self.assertIn("Destination field 'structure' not in natural keys configuration", output.getvalue())
+
+ def test_no_mapping_not_partial(self):
+ output = StringIO()
+ filename = os.path.join(os.path.dirname(__file__), 'data', 'organism5.xls')
+ call_command('import', 'geotrek.common.tests.test_parsers.OrganismNoMappingNoPartialParser', filename, verbosity=2, stdout=output)
+ self.assertIn("Bad value 'Structure' for field STRUCTURE. Should be in ['foo', '']", output.getvalue())
+
+ def test_no_mapping_partial(self):
+ output = StringIO()
+ filename = os.path.join(os.path.dirname(__file__), 'data', 'organism5.xls')
+ call_command('import', 'geotrek.common.tests.test_parsers.OrganismNoMappingPartialParser', filename, verbosity=2, stdout=output)
+ self.assertIn("Bad value 'Structure' for field STRUCTURE. Should contain ['foo']", output.getvalue())
+
@override_settings(MEDIA_ROOT=mkdtemp('geotrek_test'))
class AttachmentParserTests(TestCase):
@@ -149,6 +209,18 @@ def test_attachment(self, mocked):
self.assertEqual(attachment.filetype, self.filetype)
self.assertTrue(os.path.exists(attachment.attachment_file.path), True)
+ @mock.patch('requests.get')
+ def test_attachment_connection_error(self, mocked):
+ mocked.return_value.status_code = 200
+ mocked.side_effect = requests.exceptions.ConnectionError("Error connection")
+ filename = os.path.join(os.path.dirname(__file__), 'data', 'organism.xls')
+ output = StringIO()
+ output_3 = StringIO()
+ call_command('import', 'geotrek.common.tests.test_parsers.WarnAttachmentParser', filename, verbosity=2,
+ stdout=output, stderr=output_3)
+ self.assertFalse(Attachment.objects.exists())
+ self.assertIn("Failed to load attachment: Error connection", output.getvalue())
+
@mock.patch('requests.get')
@override_settings(PAPERCLIP_MAX_BYTES_SIZE_IMAGE=20)
def test_attachment_bigger_size(self, mocked):
@@ -260,6 +332,41 @@ def test_attachment_not_updated(self, mocked_head, mocked_get):
self.assertEqual(mocked_get.call_count, 1)
self.assertEqual(Attachment.objects.count(), 1)
+ @mock.patch('requests.get')
+ @mock.patch('requests.head')
+ def test_attachment_not_updated_partially_changed(self, mocked_head, mocked_get):
+ mocked_get.return_value.status_code = 200
+ mocked_get.return_value.content = b''
+ mocked_head.return_value.status_code = 200
+ mocked_head.return_value.headers = {'content-length': 0}
+ filename_no_legend = os.path.join(os.path.dirname(__file__), 'data', 'attachment_no_legend.xls')
+ call_command('import', 'geotrek.common.tests.test_parsers.AttachmentParser', filename_no_legend, verbosity=0)
+ attachment = Attachment.objects.get()
+ self.assertEqual(attachment.legend, '')
+ self.assertEqual(attachment.author, '')
+ filename = os.path.join(os.path.dirname(__file__), 'data', 'attachment.xls')
+ call_command('import', 'geotrek.common.tests.test_parsers.AttachmentLegendParser', filename, verbosity=0)
+ self.assertEqual(mocked_get.call_count, 1)
+ self.assertEqual(Attachment.objects.count(), 1)
+ attachment.refresh_from_db()
+ self.assertEqual(attachment.legend, 'legend')
+ self.assertEqual(attachment.author, 'name')
+
+ @mock.patch('requests.get')
+ @mock.patch('requests.head')
+ def test_attachment_updated_file_not_found(self, mocked_head, mocked_get):
+ mocked_get.return_value.status_code = 200
+ mocked_get.return_value.content = b''
+ mocked_head.return_value.status_code = 200
+ mocked_head.return_value.headers = {'content-length': 0}
+ filename = os.path.join(os.path.dirname(__file__), 'data', 'organism.xls')
+ call_command('import', 'geotrek.common.tests.test_parsers.AttachmentParser', filename, verbosity=0)
+ attachment = Attachment.objects.get()
+ os.remove(attachment.attachment_file.path)
+ call_command('import', 'geotrek.common.tests.test_parsers.AttachmentParser', filename, verbosity=0)
+ self.assertEqual(mocked_get.call_count, 2)
+ self.assertEqual(Attachment.objects.count(), 1)
+
@override_settings(PARSER_RETRY_SLEEP_TIME=0)
@mock.patch('requests.get')
@mock.patch('requests.head')
@@ -291,12 +398,32 @@ def test_attachment_request_except(self, mocked_head, mocked_get):
@mock.patch('geotrek.common.parsers.urlparse')
def test_attachment_download_fail(self, mocked_urlparse, mocked_get):
filename = os.path.join(os.path.dirname(__file__), 'data', 'organism.xls')
- mocked_get.side_effect = DownloadImportError()
+ mocked_get.side_effect = DownloadImportError("DownloadImportError")
mocked_urlparse.return_value = urllib.parse.urlparse('ftp://test.url.com/organism.xls')
+ output = StringIO()
+ call_command('import', 'geotrek.common.tests.test_parsers.WarnAttachmentParser', filename, verbosity=2,
+ stdout=output)
+ self.assertIn("Failed to load attachment: DownloadImportError", output.getvalue())
+ self.assertEqual(mocked_get.call_count, 1)
- call_command('import', 'geotrek.common.tests.test_parsers.AttachmentParser', filename, verbosity=0)
+ @mock.patch('requests.get')
+ def test_attachment_no_content(self, mocked):
+ """
+ It will always take the one without structure first
+ """
+ def mocked_requests_get(*args, **kwargs):
+ response = requests.Response()
+ response.status_code = 200
+ response._content = None
+ return response
- self.assertEqual(mocked_get.call_count, 1)
+ # Mock GET
+ mocked.side_effect = mocked_requests_get
+ structure = StructureFactory.create(name="Structure")
+ FileType.objects.create(type="Photographie", structure=structure)
+ filename = os.path.join(os.path.dirname(__file__), 'data', 'organism.xls')
+ call_command('import', 'geotrek.common.tests.test_parsers.AttachmentParser', filename, verbosity=0)
+ self.assertEqual(Attachment.objects.count(), 0)
class TourInSoftParserTests(TestCase):
@@ -382,3 +509,200 @@ def side_effect():
parser = TestOpenSystemParser()
parser.parse()
self.assertEqual(mocked_get.call_count, 1)
+
+
+class GeotrekTrekTestParser(GeotrekParser):
+ url = "https://test.fr"
+ model = Trek
+ url_categories = {
+ 'foo_field': 'test'
+ }
+
+
+class GeotrekTrekTestProviderParser(GeotrekTrekParser):
+ url = "https://test.fr"
+ provider = "Provider1"
+ delete = True
+ url_categories = {}
+
+
+class GeotrekTrekTestNoProviderParser(GeotrekTrekParser):
+ url = "https://test.fr"
+ delete = True
+ url_categories = {}
+
+
+class GeotrekAggregatorTestParser(GeotrekAggregatorParser):
+ pass
+
+
+class GeotrekParserTest(TestCase):
+ def setUp(self, *args, **kwargs):
+ self.filetype = FileType.objects.create(type="Photographie")
+
+ def test_improperly_configurated_categories(self):
+ with self.assertRaisesRegex(ImproperlyConfigured, 'foo_field is not configured in categories_keys_api_v2'):
+ call_command('import', 'geotrek.common.tests.test_parsers.GeotrekTrekTestParser', verbosity=2)
+
+ def mock_json(self):
+ filename = os.path.join('geotrek', 'common', 'tests', 'data', 'geotrek_parser_v2', 'treks.json')
+ with open(filename, 'r') as f:
+ return json.load(f)
+
+ @mock.patch('requests.get')
+ def test_delete_according_to_provider(self, mocked_get):
+ mocked_get.return_value.status_code = 200
+ mocked_get.return_value.json = self.mock_json
+ self.assertEqual(Trek.objects.count(), 0)
+ call_command('import', 'geotrek.common.tests.test_parsers.GeotrekTrekTestProviderParser', verbosity=0)
+ self.assertEqual(Trek.objects.count(), 1)
+ t = Trek.objects.first()
+ self.assertEqual(t.eid, "58ed4fc1-645d-4bf6-b956-71f0a01a5eec")
+ self.assertEqual(str(t.uuid), "58ed4fc1-645d-4bf6-b956-71f0a01a5eec")
+ self.assertEqual(t.provider, "Provider1")
+ TrekFactory(provider="Provider1", name="I should be deleted", eid="1234")
+ t2 = TrekFactory(provider="Provider2", name="I should not be deleted", eid="1236")
+ t3 = TrekFactory(provider="", name="I should not be deleted", eid="12374")
+ call_command('import', 'geotrek.common.tests.test_parsers.GeotrekTrekTestProviderParser', verbosity=0)
+ self.assertListEqual([t.pk, t2.pk, t3.pk], list(Trek.objects.values_list('pk', flat=True)))
+
+ @mock.patch('requests.get')
+ def test_delete_according_to_no_provider(self, mocked_get):
+ mocked_get.return_value.status_code = 200
+ mocked_get.return_value.json = self.mock_json
+ self.assertEqual(Trek.objects.count(), 0)
+ call_command('import', 'geotrek.common.tests.test_parsers.GeotrekTrekTestNoProviderParser', verbosity=0)
+ self.assertEqual(Trek.objects.count(), 1)
+ t = Trek.objects.first()
+ self.assertEqual(t.provider, "")
+ self.assertEqual(t.eid, "58ed4fc1-645d-4bf6-b956-71f0a01a5eec")
+ self.assertEqual(str(t.uuid), "58ed4fc1-645d-4bf6-b956-71f0a01a5eec")
+ TrekFactory(provider="", name="I should be deleted", eid="12374")
+ call_command('import', 'geotrek.common.tests.test_parsers.GeotrekTrekTestNoProviderParser', verbosity=0)
+ self.assertEqual([t.pk], list(Trek.objects.values_list('pk', flat=True)))
+
+
+class GeotrekAggregatorParserTest(GeotrekParserTestMixin, TestCase):
+ def setUp(self, *args, **kwargs):
+ self.filetype = FileType.objects.create(type="Photographie")
+
+ def test_geotrek_aggregator_no_file(self):
+ with self.assertRaisesRegex(CommandError, "File does not exists at: config_aggregator_does_not_exist.json"):
+ call_command('import', 'geotrek.common.tests.test_parsers.GeotrekAggregatorTestParser',
+ 'config_aggregator_does_not_exist.json', verbosity=0)
+
+ @skipIf(settings.TREKKING_TOPOLOGY_ENABLED, 'Test without dynamic segmentation only')
+ @mock.patch('geotrek.common.parsers.importlib.import_module', return_value=mock.MagicMock())
+ @mock.patch('django.template.loader.render_to_string')
+ @mock.patch('requests.get')
+ def test_geotrek_aggregator_no_data_to_import(self, mocked_get, mocked_render, mocked_import_module):
+ def mocked_json():
+ return {}
+
+ def side_effect_render(file, context):
+ return 'Render'
+
+ mocked_get.json = mocked_json
+ mocked_get.return_value.status_code = 200
+ mocked_get.return_value.content = b''
+ mocked_render.side_effect = side_effect_render
+ output = StringIO()
+ filename = os.path.join(os.path.dirname(__file__), 'data', 'geotrek_parser_v2',
+ 'config_aggregator_no_data_to_import.json')
+ call_command('import', 'geotrek.common.parsers.GeotrekAggregatorParser', filename=filename, verbosity=2,
+ stdout=output)
+ stdout_parser = output.getvalue()
+ self.assertIn('Render\n', stdout_parser)
+ self.assertIn('0000: Trek (URL_1) (00%)', stdout_parser)
+ self.assertIn('0000: InformationDesk (URL_1) (00%)', stdout_parser)
+ self.assertIn('0000: Trek (URL_1) (00%)', stdout_parser)
+ # Trek, POI, Service, InformationDesk, TouristicContent, TouristicEvent, Signage, Infrastructure
+ self.assertEqual(8, mocked_import_module.call_count)
+
+ @skipIf(not settings.TREKKING_TOPOLOGY_ENABLED, 'Test with dynamic segmentation only')
+ def test_geotrek_aggregator_parser_model_dynamic_segmentation(self):
+ output = StringIO()
+ filename = os.path.join(os.path.dirname(__file__), 'data', 'geotrek_parser_v2', 'config_aggregator_ds.json')
+ call_command('import', 'geotrek.common.parsers.GeotrekAggregatorParser', filename=filename, verbosity=2,
+ stdout=output)
+ string_parser = output.getvalue()
+ self.assertIn("Services can't be imported with dynamic segmentation", string_parser)
+ self.assertIn("POIs can't be imported with dynamic segmentation", string_parser)
+ self.assertIn("Treks can't be imported with dynamic segmentation", string_parser)
+
+ @skipIf(settings.TREKKING_TOPOLOGY_ENABLED, 'Test without dynamic segmentation only')
+ @mock.patch('geotrek.common.parsers.importlib.import_module', return_value=mock.MagicMock())
+ @mock.patch('django.template.loader.render_to_string')
+ @mock.patch('requests.get')
+ def test_geotrek_aggregator_parser_multiple_admin(self, mocked_get, mocked_render, mocked_import_module):
+ def mocked_json():
+ return {}
+
+ def side_effect_render(file, context):
+ return 'Render'
+
+ mocked_get.json = mocked_json
+ mocked_get.return_value.status_code = 200
+ mocked_get.return_value.content = b''
+ mocked_render.side_effect = side_effect_render
+ output = StringIO()
+ filename = os.path.join(os.path.dirname(__file__), 'data', 'geotrek_parser_v2',
+ 'config_aggregator_multiple_admin.json')
+ call_command('import', 'geotrek.common.parsers.GeotrekAggregatorParser', filename=filename, verbosity=2,
+ stdout=output)
+ stdout_parser = output.getvalue()
+ self.assertIn('Render\n', stdout_parser)
+ self.assertIn('0000: Trek (URL_1) (00%)', stdout_parser)
+ # "VTT", "Vélo"
+ # "Trek", "Service", "POI"
+ # "POI", "InformationDesk", "TouristicContent"
+ self.assertEqual(8, mocked_import_module.call_count)
+
+ @skipIf(settings.TREKKING_TOPOLOGY_ENABLED, 'Test without dynamic segmentation only')
+ def test_geotrek_aggregator_parser_no_url(self):
+ output = StringIO()
+ filename = os.path.join(os.path.dirname(__file__), 'data', 'geotrek_parser_v2', 'config_aggregator_no_url.json')
+ call_command('import', 'geotrek.common.parsers.GeotrekAggregatorParser', filename=filename, verbosity=2,
+ stdout=output)
+ string_parser = output.getvalue()
+
+ self.assertIn('URL_1 has no url', string_parser)
+
+ @skipIf(settings.TREKKING_TOPOLOGY_ENABLED, 'Test without dynamic segmentation only')
+ @mock.patch('requests.get')
+ @mock.patch('requests.head')
+ @override_settings(MODELTRANSLATION_DEFAULT_LANGUAGE="fr")
+ def test_geotrek_aggregator_parser(self, mocked_head, mocked_get):
+ self.app_label = 'trekking'
+ self.mock_time = 0
+ self.mock_json_order = ['trek_difficulty.json',
+ 'trek_route.json',
+ 'trek_theme.json',
+ 'trek_practice.json',
+ 'trek_accessibility.json',
+ 'trek_network.json',
+ 'trek_label.json',
+ 'trek_ids.json',
+ 'trek.json',
+ 'trek_children.json',
+ 'poi_type.json',
+ 'poi_ids.json',
+ 'poi.json']
+
+ # Mock GET
+ mocked_get.return_value.status_code = 200
+ mocked_get.return_value.json = self.mock_json
+ mocked_get.return_value.content = b''
+ mocked_head.return_value.status_code = 200
+
+ output = StringIO()
+ filename = os.path.join(os.path.dirname(__file__), 'data', 'geotrek_parser_v2', 'config_aggregator.json')
+ call_command('import', 'geotrek.common.parsers.GeotrekAggregatorParser', filename=filename, verbosity=2,
+ stdout=output)
+ string_parser = output.getvalue()
+ self.assertIn('0000: Trek (URL_1) (00%)', string_parser)
+ self.assertIn('0000: POI (URL_1) (00%)', string_parser)
+ self.assertIn('5/5 lines imported.', string_parser)
+ self.assertIn('2/2 lines imported.', string_parser)
+ self.assertEqual(Trek.objects.count(), 5)
+ self.assertEqual(POI.objects.count(), 2)
diff --git a/geotrek/common/tests/test_tasks.py b/geotrek/common/tests/test_tasks.py
index 60a6c73a4a..b9a0916645 100644
--- a/geotrek/common/tests/test_tasks.py
+++ b/geotrek/common/tests/test_tasks.py
@@ -39,7 +39,7 @@ def test_import_datas_from_web_message_exception(self):
def test_import_datas_from_web_other_exception(self):
self.assertRaisesMessage(
GlobalImportError,
- 'Filename is required',
+ 'Filename or url is required',
import_datas_from_web,
name='OrganismParser',
module='geotrek.common.tests.test_tasks'
diff --git a/geotrek/core/filters.py b/geotrek/core/filters.py
index 334b4892d6..d9490ecc70 100644
--- a/geotrek/core/filters.py
+++ b/geotrek/core/filters.py
@@ -1,7 +1,7 @@
from django.conf import settings
from django.db.models import Count, F, Q
from django.utils.translation import gettext_lazy as _
-from django_filters import BooleanFilter, CharFilter, FilterSet, ModelMultipleChoiceFilter
+from django_filters import BooleanFilter, CharFilter, FilterSet, ModelMultipleChoiceFilter, ChoiceFilter
from .models import Topology, Path, Trail, CertificationLabel
@@ -99,11 +99,17 @@ def _topology_filter(self, qs, edges):
class PathFilterSet(AltimetryAllGeometriesFilterSet, ZoningFilterSet, StructureRelatedFilterSet):
name = CharFilter(label=_('Name'), lookup_expr='icontains')
comments = CharFilter(label=_('Comments'), lookup_expr='icontains')
+ provider = ChoiceFilter(
+ field_name='provider',
+ empty_label=_("Provider"),
+ label=_("Provider"),
+ choices=Path.objects.provider_choices()
+ )
class Meta(StructureRelatedFilterSet.Meta):
model = Path
fields = StructureRelatedFilterSet.Meta.fields + \
- ['valid', 'networks', 'usages', 'comfort', 'stake', 'draft', ]
+ ['valid', 'networks', 'usages', 'comfort', 'stake', 'draft', 'provider']
class TrailFilterSet(AltimetryAllGeometriesFilterSet, ValidTopologyFilterSet, ZoningFilterSet, StructureRelatedFilterSet):
@@ -117,11 +123,17 @@ class TrailFilterSet(AltimetryAllGeometriesFilterSet, ValidTopologyFilterSet, Zo
label=_("Certification labels"),
queryset=CertificationLabel.objects.all(),
)
+ provider = ChoiceFilter(
+ field_name='provider',
+ empty_label=_("Provider"),
+ label=_("Provider"),
+ choices=Trail.objects.provider_choices()
+ )
class Meta(StructureRelatedFilterSet.Meta):
model = Trail
fields = StructureRelatedFilterSet.Meta.fields + \
- ['name', 'category', 'departure', 'arrival', 'certification_labels', 'comments']
+ ['name', 'category', 'departure', 'arrival', 'certification_labels', 'comments', 'provider']
class TopologyFilterTrail(TopologyFilter):
diff --git a/geotrek/core/locale/fr/LC_MESSAGES/django.po b/geotrek/core/locale/fr/LC_MESSAGES/django.po
index 112ac5df3a..04d7fc96eb 100644
--- a/geotrek/core/locale/fr/LC_MESSAGES/django.po
+++ b/geotrek/core/locale/fr/LC_MESSAGES/django.po
@@ -177,6 +177,9 @@ msgstr "Interventions"
msgid "Offset"
msgstr "Décalage"
+msgid "Provider"
+msgstr "Fournisseur"
+
msgid "Kind"
msgstr "Type de topologie"
diff --git a/geotrek/core/migrations/0034_auto_20220909_1316.py b/geotrek/core/migrations/0034_auto_20220909_1316.py
new file mode 100644
index 0000000000..6f565a47f5
--- /dev/null
+++ b/geotrek/core/migrations/0034_auto_20220909_1316.py
@@ -0,0 +1,23 @@
+# Generated by Django 3.1.14 on 2022-09-09 13:16
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('core', '0033_auto_20220728_0940'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='path',
+ name='provider',
+ field=models.CharField(blank=True, db_index=True, max_length=1024, verbose_name='Provider'),
+ ),
+ migrations.AddField(
+ model_name='trail',
+ name='provider',
+ field=models.CharField(blank=True, db_index=True, max_length=1024, verbose_name='Provider'),
+ ),
+ ]
diff --git a/geotrek/core/models.py b/geotrek/core/models.py
index a00c468070..d8325fd220 100644
--- a/geotrek/core/models.py
+++ b/geotrek/core/models.py
@@ -47,6 +47,11 @@ def get_queryset(self):
"""
return super().get_queryset().filter(visible=True).annotate(length_2d=Length('geom'))
+ def provider_choices(self):
+ providers = self.get_queryset().exclude(provider__exact='') \
+ .distinct('provider').values_list('provider', 'provider')
+ return providers
+
class PathInvisibleManager(models.Manager):
use_for_related_fields = True
@@ -91,6 +96,7 @@ class Path(ZoningPropertiesMixin, AddPropertyMixin, MapEntityMixin, AltimetryMix
blank=True, related_name="paths",
verbose_name=_("Networks"))
eid = models.CharField(verbose_name=_("External id"), max_length=1024, blank=True, null=True)
+ provider = models.CharField(verbose_name=_("Provider"), db_index=True, max_length=1024, blank=True)
draft = models.BooleanField(default=False, verbose_name=_("Draft"), db_index=True)
uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
@@ -956,6 +962,13 @@ def __str__(self):
return self.network
+class TrailManager(TopologyManager):
+ def provider_choices(self):
+ providers = self.get_queryset().existing().exclude(provider__exact='').order_by('provider') \
+ .distinct('provider').values_list('provider', 'provider')
+ return providers
+
+
class Trail(MapEntityMixin, Topology, StructureRelated):
topo_object = models.OneToOneField(Topology, parent_link=True, on_delete=models.CASCADE)
name = models.CharField(verbose_name=_("Name"), max_length=64)
@@ -970,10 +983,13 @@ class Trail(MapEntityMixin, Topology, StructureRelated):
arrival = models.CharField(verbose_name=_("Arrival"), blank=True, max_length=64)
comments = models.TextField(default="", blank=True, verbose_name=_("Comments"))
eid = models.CharField(verbose_name=_("External id"), max_length=1024, blank=True, null=True)
+ provider = models.CharField(verbose_name=_("Provider"), db_index=True, max_length=1024, blank=True)
certifications_verbose_name = _("Certifications")
geometry_types_allowed = ["LINESTRING"]
+ objects = TrailManager()
+
class Meta:
verbose_name = _("Trail")
verbose_name_plural = _("Trails")
diff --git a/geotrek/core/templates/core/path_detail_attributes.html b/geotrek/core/templates/core/path_detail_attributes.html
index 69262fa51e..6736e81e65 100644
--- a/geotrek/core/templates/core/path_detail_attributes.html
+++ b/geotrek/core/templates/core/path_detail_attributes.html
@@ -45,7 +45,20 @@ {% trans "Attributes" %}
{% if path.source %}{{ path.source }}
{% else %}{% trans "None" %}{% endif %}
|
-
+
+
+ {% trans "External id" %} |
+ {% if path.eid %}{{ path.eid|safe }}
+ {% else %}{% trans "None" %}{% endif %}
+ |
+
+
+ {% trans "Provider" %} |
+ {% if path.provider %}{{ path.provider|safe }}
+ {% else %}{% trans "None" %}{% endif %}
+ |
+
+
{{ path|verbose:"stake" }} |
{% if path.stake %}{{ path.stake }}
{% else %}{% trans "None" %}{% endif %}
diff --git a/geotrek/core/templates/core/sql/post_90_defaults.sql b/geotrek/core/templates/core/sql/post_90_defaults.sql
index 58602d272f..2a3da9be7f 100644
--- a/geotrek/core/templates/core/sql/post_90_defaults.sql
+++ b/geotrek/core/templates/core/sql/post_90_defaults.sql
@@ -25,6 +25,8 @@ ALTER TABLE core_path ALTER COLUMN max_elevation SET DEFAULT 0;
ALTER TABLE core_path ALTER COLUMN slope SET DEFAULT 0.0;
ALTER TABLE core_path ALTER COLUMN date_insert SET DEFAULT now();
ALTER TABLE core_path ALTER COLUMN date_update SET DEFAULT now();
+ALTER TABLE core_path ALTER COLUMN provider SET DEFAULT '';
+
-- structure
@@ -91,4 +93,6 @@ ALTER TABLE core_trail ALTER COLUMN departure SET DEFAULT '';
ALTER TABLE core_trail ALTER COLUMN arrival SET DEFAULT '';
ALTER TABLE core_trail ALTER COLUMN comments SET DEFAULT '';
ALTER TABLE core_trail ALTER COLUMN eid SET DEFAULT '';
+ALTER TABLE core_trail ALTER COLUMN provider SET DEFAULT '';
+
-- structure
diff --git a/geotrek/core/templates/core/trail_detail_attributes.html b/geotrek/core/templates/core/trail_detail_attributes.html
index d534bb9dc5..2b05d58d28 100644
--- a/geotrek/core/templates/core/trail_detail_attributes.html
+++ b/geotrek/core/templates/core/trail_detail_attributes.html
@@ -36,6 +36,18 @@ {% trans "Attributes" %}
|
{% include "altimetry/elevationinfo_fragment.html" %}
{% include "mapentity/trackinfo_fragment.html" %}
+
+ {% trans "External id" %} |
+ {% if trail.eid %}{{ trail.eid|safe }}
+ {% else %}{% trans "None" %}{% endif %}
+ |
+
+
+ {% trans "Provider" %} |
+ {% if trail.provider %}{{ trail.provider|safe }}
+ {% else %}{% trans "None" %}{% endif %}
+ |
+
{{ block.super }}
diff --git a/geotrek/core/tests/test_views.py b/geotrek/core/tests/test_views.py
index 3a437630df..f0e031db88 100644
--- a/geotrek/core/tests/test_views.py
+++ b/geotrek/core/tests/test_views.py
@@ -575,7 +575,7 @@ def test_draft_path_layer_cache(self):
self.modelfactory(draft=True)
# There are 7 queries to get layer without drafts
- with self.assertNumQueries(5):
+ with self.assertNumQueries(6):
response = self.client.get(obj.get_layer_url(), {"_no_draft": "true"})
self.assertEqual(len(response.json()['features']), 1)
@@ -604,7 +604,7 @@ def test_draft_path_layer_cache(self):
self.modelfactory(draft=False)
# Cache was updated, the path was not a draft : we get 7 queries
- with self.assertNumQueries(5):
+ with self.assertNumQueries(6):
self.client.get(obj.get_layer_url(), {"_no_draft": "true"})
def test_path_layer_cache(self):
@@ -618,7 +618,7 @@ def test_path_layer_cache(self):
self.modelfactory(draft=True)
# There are 7 queries to get layer without drafts
- with self.assertNumQueries(5):
+ with self.assertNumQueries(6):
response = self.client.get(obj.get_layer_url())
self.assertEqual(len(response.json()['features']), 2)
@@ -641,13 +641,13 @@ def test_path_layer_cache(self):
self.modelfactory(draft=True)
# Cache is updated when we add a draft path
- with self.assertNumQueries(5):
+ with self.assertNumQueries(6):
self.client.get(obj.get_layer_url())
self.modelfactory(draft=False)
# Cache is updated when we add a path
- with self.assertNumQueries(5):
+ with self.assertNumQueries(6):
self.client.get(obj.get_layer_url())
@@ -700,7 +700,7 @@ def test_denormalized_path_trails(self):
PathFactory.create_batch(size=50)
TrailFactory.create_batch(size=50)
self.login()
- with self.assertNumQueries(6):
+ with self.assertNumQueries(7):
self.client.get(reverse('core:path-drf-list', kwargs={'format': 'datatables'}))
@@ -803,7 +803,7 @@ def test_add_trail_from_existing_topology(self):
def test_perfs_export_csv(self):
self.modelfactory.create()
- with self.assertNumQueries(10):
+ with self.assertNumQueries(13):
self.client.get(self.model.get_format_list_url() + '?format=csv')
diff --git a/geotrek/infrastructure/filters.py b/geotrek/infrastructure/filters.py
index c69e119601..efb8af6787 100644
--- a/geotrek/infrastructure/filters.py
+++ b/geotrek/infrastructure/filters.py
@@ -1,6 +1,6 @@
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext_lazy as _
-from django_filters import CharFilter, MultipleChoiceFilter, ModelMultipleChoiceFilter
+from django_filters import CharFilter, MultipleChoiceFilter, ModelMultipleChoiceFilter, ChoiceFilter
from geotrek.altimetry.filters import AltimetryAllGeometriesFilterSet
from geotrek.authent.filters import StructureRelatedFilterSet
from geotrek.core.filters import ValidTopologyFilterSet, TopologyFilterTrail
@@ -20,12 +20,18 @@ class InfrastructureFilterSet(AltimetryAllGeometriesFilterSet, ValidTopologyFilt
trail = TopologyFilterTrail(label=_('Trail'), required=False)
maintenance_difficulty = ModelMultipleChoiceFilter(queryset=InfrastructureMaintenanceDifficultyLevel.objects.all(), label=_("Maintenance difficulty"))
usage_difficulty = ModelMultipleChoiceFilter(queryset=InfrastructureUsageDifficultyLevel.objects.all(), label=_("Usage difficulty"))
+ provider = ChoiceFilter(
+ field_name='provider',
+ empty_label=_("Provider"),
+ label=_("Provider"),
+ choices=Infrastructure.objects.provider_choices()
+ )
class Meta(StructureRelatedFilterSet.Meta):
model = Infrastructure
fields = StructureRelatedFilterSet.Meta.fields + [
'category', 'type', 'condition', 'implantation_year',
- 'intervention_year', 'published'
+ 'intervention_year', 'published', 'provider'
]
def filter_intervention_year(self, qs, name, value):
diff --git a/geotrek/infrastructure/locale/fr/LC_MESSAGES/django.po b/geotrek/infrastructure/locale/fr/LC_MESSAGES/django.po
index c2030bd454..7aed0e0fcf 100644
--- a/geotrek/infrastructure/locale/fr/LC_MESSAGES/django.po
+++ b/geotrek/infrastructure/locale/fr/LC_MESSAGES/django.po
@@ -117,3 +117,6 @@ msgstr "Aucun(e)"
msgid "Add a new infrastructure"
msgstr "Ajouter un aménagement"
+
+msgid "Provider"
+msgstr "Fournisseur"
diff --git a/geotrek/infrastructure/migrations/0031_infrastructure_provider.py b/geotrek/infrastructure/migrations/0031_infrastructure_provider.py
new file mode 100644
index 0000000000..a6b7576526
--- /dev/null
+++ b/geotrek/infrastructure/migrations/0031_infrastructure_provider.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.1.14 on 2022-09-07 13:43
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('infrastructure', '0030_auto_20220314_1429'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='infrastructure',
+ name='provider',
+ field=models.CharField(blank=True, db_index=True, max_length=1024, verbose_name='Provider'),
+ ),
+ ]
diff --git a/geotrek/infrastructure/models.py b/geotrek/infrastructure/models.py
index 98a0ffe438..6c89156efb 100755
--- a/geotrek/infrastructure/models.py
+++ b/geotrek/infrastructure/models.py
@@ -99,6 +99,7 @@ class BaseInfrastructure(BasePublishableMixin, Topology, StructureRelated):
on_delete=models.SET_NULL)
implantation_year = models.PositiveSmallIntegerField(verbose_name=_("Implantation year"), null=True)
eid = models.CharField(verbose_name=_("External id"), max_length=1024, blank=True, null=True)
+ provider = models.CharField(verbose_name=_("Provider"), db_index=True, max_length=1024, blank=True)
class Meta:
abstract = True
@@ -148,6 +149,11 @@ def implantation_year_choices(self):
.values_list('implantation_year', 'implantation_year')
return all_years
+ def provider_choices(self):
+ providers = self.get_queryset().existing().exclude(provider__exact='') \
+ .distinct('provider').values_list('provider', 'provider')
+ return providers
+
class Infrastructure(MapEntityMixin, BaseInfrastructure):
""" An infrastructure in the park, which is not of type SIGNAGE """
diff --git a/geotrek/infrastructure/parsers.py b/geotrek/infrastructure/parsers.py
new file mode 100644
index 0000000000..c0c68c95db
--- /dev/null
+++ b/geotrek/infrastructure/parsers.py
@@ -0,0 +1,33 @@
+from geotrek.common.parsers import GeotrekParser
+from geotrek.infrastructure.models import Infrastructure
+
+
+class GeotrekInfrastructureParser(GeotrekParser):
+ """Geotrek parser for Infrastructure"""
+
+ url = None
+ model = Infrastructure
+ constant_fields = {
+ "published": True,
+ "deleted": False
+ }
+ replace_fields = {
+ "eid": "uuid",
+ "geom": "geometry"
+ }
+ url_categories = {
+ 'condition': 'infrastructure_condition',
+ 'type': 'infrastructure_type',
+ }
+ categories_keys_api_v2 = {
+ 'condition': 'label',
+ 'type': 'label'
+ }
+ natural_keys = {
+ 'condition': 'label',
+ 'type': 'label'
+ }
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.next_url = f"{self.url}/api/v2/infrastructure"
diff --git a/geotrek/infrastructure/templates/infrastructure/infrastructure_detail_attributes.html b/geotrek/infrastructure/templates/infrastructure/infrastructure_detail_attributes.html
index 95a3675874..dd68c02e19 100644
--- a/geotrek/infrastructure/templates/infrastructure/infrastructure_detail_attributes.html
+++ b/geotrek/infrastructure/templates/infrastructure/infrastructure_detail_attributes.html
@@ -44,6 +44,18 @@ {% trans "Attributes" %}
{{ object|verbose:"maintenance_difficulty" }} |
{{ object.maintenance_difficulty|default:"" }} |
+
+ {% trans "External id" %} |
+ {% if object.eid %}{{ object.eid|safe }}
+ {% else %}{% trans "None" %}{% endif %}
+ |
+
+
+ {% trans "Provider" %} |
+ {% if object.provider %}{{ object.provider|safe }}
+ {% else %}{% trans "None" %}{% endif %}
+ |
+
{% include "altimetry/elevationinfo_fragment.html" %}
{% include "common/publication_info_fragment.html" %}
{% include "mapentity/trackinfo_fragment.html" %}
diff --git a/geotrek/infrastructure/templates/infrastructure/sql/post_90_defaults.sql b/geotrek/infrastructure/templates/infrastructure/sql/post_90_defaults.sql
index f93fb42d43..c03f8a15d4 100644
--- a/geotrek/infrastructure/templates/infrastructure/sql/post_90_defaults.sql
+++ b/geotrek/infrastructure/templates/infrastructure/sql/post_90_defaults.sql
@@ -38,3 +38,4 @@ ALTER TABLE infrastructure_infrastructure ALTER COLUMN description SET DEFAULT '
ALTER TABLE infrastructure_infrastructure ALTER COLUMN published SET DEFAULT FALSE;
-- publication_date
-- structure
+ALTER TABLE infrastructure_infrastructure ALTER COLUMN provider SET DEFAULT '';
\ No newline at end of file
diff --git a/geotrek/infrastructure/tests/data/geotrek_parser_v2/infrastructure.json b/geotrek/infrastructure/tests/data/geotrek_parser_v2/infrastructure.json
new file mode 100644
index 0000000000..da8bf477e7
--- /dev/null
+++ b/geotrek/infrastructure/tests/data/geotrek_parser_v2/infrastructure.json
@@ -0,0 +1,75 @@
+{
+ "count": 2,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "id": 2898,
+ "accessibility": {
+ "fr": "",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "attachments": [
+ {
+ "backend": "",
+ "type": "image",
+ "author": "gutard",
+ "license": null,
+ "thumbnail": "https://foo.fr/media/paperclip/infrastructure_baseinfrastructure/2898/belgique.png.400x0_q85.jpg",
+ "legend": "",
+ "title": "belgique",
+ "url": "https://foo.fr/media/paperclip/infrastructure_baseinfrastructure/2898/belgique.png",
+ "uuid": "20888c5a-0f33-4e22-8999-5fef740a5439"
+ }
+ ],
+ "condition": null,
+ "description": "",
+ "eid": null,
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 1.35208,
+ 42.781068,
+ 840.0
+ ]
+ },
+ "name": "Table pic-nique",
+ "implantation_year": null,
+ "maintenance_difficulty": null,
+ "structure": "MC",
+ "type": 81,
+ "usage_difficulty": null,
+ "uuid": "93747e51-757e-4b39-9c3b-41bb228a7455"
+ },
+ {
+ "id": 8385,
+ "accessibility": {
+ "fr": "",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "attachments": [],
+ "condition": null,
+ "description": "Une belle description",
+ "eid": null,
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 1.1090391,
+ 42.7838115,
+ 1550.0
+ ]
+ },
+ "name": "Cabane d'Aula",
+ "implantation_year": null,
+ "maintenance_difficulty": null,
+ "structure": "DEMO",
+ "type": 45,
+ "usage_difficulty": null,
+ "uuid": "f7d10e86-5bf0-47a5-934e-63dcca8bf9c5"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/geotrek/infrastructure/tests/data/geotrek_parser_v2/infrastructure_condition.json b/geotrek/infrastructure/tests/data/geotrek_parser_v2/infrastructure_condition.json
new file mode 100644
index 0000000000..6d14c903b4
--- /dev/null
+++ b/geotrek/infrastructure/tests/data/geotrek_parser_v2/infrastructure_condition.json
@@ -0,0 +1,32 @@
+{
+ "count": 5,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "id": 6,
+ "label": "Neuf",
+ "structure": null
+ },
+ {
+ "id": 7,
+ "label": "Bon état",
+ "structure": null
+ },
+ {
+ "id": 8,
+ "label": "Dégradé",
+ "structure": null
+ },
+ {
+ "id": 9,
+ "label": "En ruines",
+ "structure": null
+ },
+ {
+ "id": 10,
+ "label": "Tagué",
+ "structure": null
+ }
+ ]
+}
\ No newline at end of file
diff --git a/geotrek/infrastructure/tests/data/geotrek_parser_v2/infrastructure_ids.json b/geotrek/infrastructure/tests/data/geotrek_parser_v2/infrastructure_ids.json
new file mode 100644
index 0000000000..5c5585bdb1
--- /dev/null
+++ b/geotrek/infrastructure/tests/data/geotrek_parser_v2/infrastructure_ids.json
@@ -0,0 +1,13 @@
+{
+ "count": 2,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "uuid": "93747e51-757e-4b39-9c3b-41bb228a7455"
+ },
+ {
+ "uuid": "f7d10e86-5bf0-47a5-934e-63dcca8bf9c5"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/geotrek/infrastructure/tests/data/geotrek_parser_v2/infrastructure_type.json b/geotrek/infrastructure/tests/data/geotrek_parser_v2/infrastructure_type.json
new file mode 100644
index 0000000000..2854eda972
--- /dev/null
+++ b/geotrek/infrastructure/tests/data/geotrek_parser_v2/infrastructure_type.json
@@ -0,0 +1,28 @@
+{
+ "count": 3,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "id": 45,
+ "label": "Aire de stationnement",
+ "pictogram": null,
+ "structure": 3,
+ "type": "Ouvrage"
+ },
+ {
+ "id": 46,
+ "label": "Aire de stationnement Handicapés",
+ "pictogram": null,
+ "structure": null,
+ "type": "Ouvrage"
+ },
+ {
+ "id": 81,
+ "label": "Table",
+ "pictogram": null,
+ "structure": null,
+ "type": "Équipement"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/geotrek/infrastructure/tests/test_parsers.py b/geotrek/infrastructure/tests/test_parsers.py
new file mode 100644
index 0000000000..95ff064b57
--- /dev/null
+++ b/geotrek/infrastructure/tests/test_parsers.py
@@ -0,0 +1,50 @@
+from unittest import mock
+
+from django.core.management import call_command
+from django.test import TestCase
+from django.test.utils import override_settings
+
+from geotrek.common.models import FileType
+from geotrek.common.tests.mixins import GeotrekParserTestMixin
+from geotrek.infrastructure.models import Infrastructure
+from geotrek.infrastructure.parsers import GeotrekInfrastructureParser
+
+
+class TestGeotrekInfrastructureParser(GeotrekInfrastructureParser):
+ url = "https://test.fr"
+
+ field_options = {
+ 'condition': {'create': True, },
+ 'type': {'create': True},
+ 'geom': {'required': True},
+ }
+
+
+class InfrastructureGeotrekParserTests(GeotrekParserTestMixin, TestCase):
+ app_label = 'infrastructure'
+
+ @classmethod
+ def setUpTestData(cls):
+ cls.filetype = FileType.objects.create(type="Photographie")
+
+ @mock.patch('requests.get')
+ @mock.patch('requests.head')
+ @override_settings(MODELTRANSLATION_DEFAULT_LANGUAGE="fr")
+ def test_create(self, mocked_head, mocked_get):
+ self.mock_time = 0
+ self.mock_json_order = ['infrastructure_condition.json', 'infrastructure_type.json', 'infrastructure_ids.json',
+ 'infrastructure.json', ]
+
+ # Mock GET
+ mocked_get.return_value.status_code = 200
+ mocked_get.return_value.json = self.mock_json
+ mocked_get.return_value.content = b''
+ mocked_head.return_value.status_code = 200
+
+ call_command('import', 'geotrek.infrastructure.tests.test_parsers.TestGeotrekInfrastructureParser', verbosity=0)
+ self.assertEqual(Infrastructure.objects.count(), 2)
+ infrastructure = Infrastructure.objects.all().first()
+ self.assertEqual(str(infrastructure.name), 'Table pic-nique')
+ self.assertEqual(str(infrastructure.type), 'Table')
+ self.assertAlmostEqual(infrastructure.geom.x, 565008.6693905985, places=5)
+ self.assertAlmostEqual(infrastructure.geom.y, 6188246.533542466, places=5)
diff --git a/geotrek/outdoor/filters.py b/geotrek/outdoor/filters.py
index 454ace0b6a..baac218c62 100644
--- a/geotrek/outdoor/filters.py
+++ b/geotrek/outdoor/filters.py
@@ -1,6 +1,6 @@
from django.db.models import Q
from django.utils.translation import gettext as _
-from django_filters.filters import MultipleChoiceFilter, ModelMultipleChoiceFilter
+from django_filters.filters import MultipleChoiceFilter, ModelMultipleChoiceFilter, ChoiceFilter
from geotrek.authent.filters import StructureRelatedFilterSet
from geotrek.common.models import Organism
from geotrek.outdoor.models import Site, Practice, Sector, Course
@@ -13,12 +13,18 @@ class SiteFilterSet(ZoningFilterSet, StructureRelatedFilterSet):
practice = ModelMultipleChoiceFilter(queryset=Practice.objects.all(), method='filter_super')
sector = ModelMultipleChoiceFilter(queryset=Sector.objects.all(), method='filter_sector', label=_("Sector"))
managers = ModelMultipleChoiceFilter(queryset=Organism.objects.all(), method='filter_manager', label=_("Manager"))
+ provider = ChoiceFilter(
+ field_name='provider',
+ empty_label=_("Provider"),
+ label=_("Provider"),
+ choices=Site.objects.provider_choices()
+ )
class Meta(StructureRelatedFilterSet.Meta):
model = Site
fields = StructureRelatedFilterSet.Meta.fields + [
'sector', 'practice', 'labels', 'themes', 'portal', 'source', 'information_desks',
- 'web_links', 'type', 'orientation', 'wind',
+ 'web_links', 'type', 'orientation', 'wind', 'provider'
]
def filter_orientation(self, qs, name, values):
@@ -48,13 +54,19 @@ class CourseFilterSet(ZoningFilterSet, StructureRelatedFilterSet):
label=_("Orientation"))
wind = MultipleChoiceFilter(choices=Site.WIND_CHOICES, method='filter_orientation',
label=_("Wind"))
+ provider = ChoiceFilter(
+ field_name='provider',
+ empty_label=_("Provider"),
+ label=_("Provider"),
+ choices=Course.objects.provider_choices()
+ )
class Meta(StructureRelatedFilterSet.Meta):
model = Course
fields = StructureRelatedFilterSet.Meta.fields + [
'parent_sites', 'parent_sites__practice__sector', 'parent_sites__practice', 'parent_sites__labels', 'parent_sites__themes',
'parent_sites__portal', 'parent_sites__source', 'parent_sites__type', 'orientation', 'wind',
- 'height',
+ 'height', 'provider'
]
def filter_orientation(self, qs, name, values):
diff --git a/geotrek/outdoor/locale/fr/LC_MESSAGES/django.po b/geotrek/outdoor/locale/fr/LC_MESSAGES/django.po
index 781c815511..fa8460bbef 100644
--- a/geotrek/outdoor/locale/fr/LC_MESSAGES/django.po
+++ b/geotrek/outdoor/locale/fr/LC_MESSAGES/django.po
@@ -23,6 +23,9 @@ msgstr "Couleur"
msgid "Outdoor"
msgstr "Outdoor"
+msgid "Provider"
+msgstr "Fournisseur"
+
msgid "Sector"
msgstr "Filière"
diff --git a/geotrek/outdoor/migrations/0040_auto_20220909_1327.py b/geotrek/outdoor/migrations/0040_auto_20220909_1327.py
new file mode 100644
index 0000000000..4da4f9b5d6
--- /dev/null
+++ b/geotrek/outdoor/migrations/0040_auto_20220909_1327.py
@@ -0,0 +1,23 @@
+# Generated by Django 3.1.14 on 2022-09-09 13:27
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('outdoor', '0039_auto_20220304_1442'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='course',
+ name='provider',
+ field=models.CharField(blank=True, db_index=True, max_length=1024, verbose_name='Provider'),
+ ),
+ migrations.AddField(
+ model_name='site',
+ name='provider',
+ field=models.CharField(blank=True, db_index=True, max_length=1024, verbose_name='Provider'),
+ ),
+ ]
diff --git a/geotrek/outdoor/models.py b/geotrek/outdoor/models.py
index 8ab6c4d536..bcfb816f94 100644
--- a/geotrek/outdoor/models.py
+++ b/geotrek/outdoor/models.py
@@ -5,10 +5,10 @@
from django.contrib.gis.measure import D
from django.contrib.postgres.indexes import GistIndex
from django.core.validators import MinValueValidator
-from django.db.models import Q
+from django.db.models import Q, Manager
from django.utils.html import escape
from django.utils.translation import gettext_lazy as _
-from mptt.models import MPTTModel, TreeForeignKey
+from mptt.models import MPTTModel, TreeForeignKey, TreeManager
from geotrek.altimetry.models import AltimetryMixin as BaseAltimetryMixin
from geotrek.authent.models import StructureRelated
@@ -110,6 +110,13 @@ def __str__(self):
return self.name
+class SiteManager(TreeManager):
+ def provider_choices(self):
+ providers = self.get_queryset().exclude(provider__exact='').order_by('provider') \
+ .distinct('provider').values_list('provider', 'provider')
+ return providers
+
+
class Site(ZoningPropertiesMixin, AddPropertyMixin, PicturesMixin, PublishableMixin, MapEntityMixin, StructureRelated,
AltimetryMixin, TimeStampedModelMixin, MPTTModel, ExcludedPOIsMixin):
ORIENTATION_CHOICES = (
@@ -171,11 +178,14 @@ class Site(ZoningPropertiesMixin, AddPropertyMixin, PicturesMixin, PublishableMi
type = models.ForeignKey(SiteType, related_name="sites", on_delete=models.PROTECT,
verbose_name=_("Type"), null=True, blank=True)
eid = models.CharField(verbose_name=_("External id"), max_length=1024, blank=True, null=True)
+ provider = models.CharField(verbose_name=_("Provider"), db_index=True, max_length=1024, blank=True)
managers = models.ManyToManyField(Organism, verbose_name=_("Managers"), blank=True)
uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
check_structure_in_forms = False
+ objects = SiteManager()
+
class Meta:
verbose_name = _("Outdoor site")
verbose_name_plural = _("Outdoor sites")
@@ -337,6 +347,13 @@ class Meta:
)
+class CourseManager(Manager):
+ def provider_choices(self):
+ providers = self.get_queryset().exclude(provider__exact='').order_by('provider') \
+ .distinct('provider').values_list('provider', 'provider')
+ return providers
+
+
class Course(ZoningPropertiesMixin, AddPropertyMixin, PublishableMixin, MapEntityMixin, StructureRelated, PicturesMixin,
AltimetryMixin, TimeStampedModelMixin, ExcludedPOIsMixin):
geom = models.GeometryCollectionField(verbose_name=_("Location"), srid=settings.SRID)
@@ -355,6 +372,7 @@ class Course(ZoningPropertiesMixin, AddPropertyMixin, PublishableMixin, MapEntit
ratings = models.ManyToManyField(Rating, related_name='courses', blank=True, verbose_name=_("Ratings"))
height = models.IntegerField(verbose_name=_("Height"), blank=True, null=True)
eid = models.CharField(verbose_name=_("External id"), max_length=1024, blank=True, null=True)
+ provider = models.CharField(verbose_name=_("Provider"), db_index=True, max_length=1024, blank=True)
type = models.ForeignKey(CourseType, related_name="courses", on_delete=models.PROTECT,
verbose_name=_("Type"), null=True, blank=True)
pois_excluded = models.ManyToManyField('trekking.Poi', related_name='excluded_courses', verbose_name=_("Excluded POIs"),
@@ -365,6 +383,8 @@ class Course(ZoningPropertiesMixin, AddPropertyMixin, PublishableMixin, MapEntit
check_structure_in_forms = False
+ objects = CourseManager()
+
class Meta:
verbose_name = _("Outdoor course")
verbose_name_plural = _("Outdoor courses")
diff --git a/geotrek/outdoor/templates/outdoor/course_detail_attributes.html b/geotrek/outdoor/templates/outdoor/course_detail_attributes.html
index 1bdf7c13a3..092f126711 100644
--- a/geotrek/outdoor/templates/outdoor/course_detail_attributes.html
+++ b/geotrek/outdoor/templates/outdoor/course_detail_attributes.html
@@ -87,7 +87,15 @@ {% trans "Attributes" %}
{% trans "External id" %} |
- {% if object.eid %}{{ object.eid }}{% else %}{% trans "None" %}{% endif %} |
+ {% if object.eid %}{{ object.eid|safe }}
+ {% else %}{% trans "None" %}{% endif %}
+ |
+
+
+ {% trans "Provider" %} |
+ {% if object.provider %}{{ object.provider|safe }}
+ {% else %}{% trans "None" %}{% endif %}
+ |
{% include "common/publication_info_fragment.html" %}
{% include "altimetry/elevationinfo_fragment.html" %}
diff --git a/geotrek/outdoor/templates/outdoor/site_detail_attributes.html b/geotrek/outdoor/templates/outdoor/site_detail_attributes.html
index 71a55204b5..23c97bc687 100644
--- a/geotrek/outdoor/templates/outdoor/site_detail_attributes.html
+++ b/geotrek/outdoor/templates/outdoor/site_detail_attributes.html
@@ -159,7 +159,15 @@ {% trans "Attributes" %}
{% trans "External id" %} |
- {% if object.eid %}{{ object.eid }}{% else %}{% trans "None" %}{% endif %} |
+ {% if object.eid %}{{ object.eid|safe }}
+ {% else %}{% trans "None" %}{% endif %}
+ |
+
+
+ {% trans "Provider" %} |
+ {% if object.provider %}{{ object.provider|safe }}
+ {% else %}{% trans "None" %}{% endif %}
+ |
{% include "common/publication_info_fragment.html" %}
{% include "altimetry/elevationinfo_fragment.html" %}
diff --git a/geotrek/outdoor/templates/outdoor/sql/post_90_defaults.sql b/geotrek/outdoor/templates/outdoor/sql/post_90_defaults.sql
index 61d55e3329..b678ee4972 100644
--- a/geotrek/outdoor/templates/outdoor/sql/post_90_defaults.sql
+++ b/geotrek/outdoor/templates/outdoor/sql/post_90_defaults.sql
@@ -74,7 +74,7 @@ ALTER TABLE outdoor_site ALTER COLUMN accessibility SET DEFAULT '';
-- eid
-- managers
ALTER TABLE outdoor_site ALTER COLUMN uuid SET DEFAULT gen_random_uuid();
-
+ALTER TABLE outdoor_site ALTER COLUMN provider SET DEFAULT '';
-- OrderedCourseChild
---------------------
@@ -115,3 +115,4 @@ ALTER TABLE outdoor_course ALTER COLUMN max_elevation SET DEFAULT 0;
ALTER TABLE outdoor_course ALTER COLUMN slope SET DEFAULT 0.0;
ALTER TABLE outdoor_course ALTER COLUMN date_insert SET DEFAULT now();
ALTER TABLE outdoor_course ALTER COLUMN date_update SET DEFAULT now();
+ALTER TABLE outdoor_course ALTER COLUMN provider SET DEFAULT '';
diff --git a/geotrek/sensitivity/filters.py b/geotrek/sensitivity/filters.py
index 1e8fa26a4c..0b631e1b8c 100644
--- a/geotrek/sensitivity/filters.py
+++ b/geotrek/sensitivity/filters.py
@@ -1,5 +1,5 @@
-from django.utils.translation import pgettext_lazy
-from django_filters.filters import ModelMultipleChoiceFilter
+from django.utils.translation import pgettext_lazy, gettext as _
+from django_filters.filters import ModelMultipleChoiceFilter, ChoiceFilter
from geotrek.authent.filters import StructureRelatedFilterSet
from .models import SensitiveArea, Species
@@ -9,9 +9,15 @@ class SensitiveAreaFilterSet(StructureRelatedFilterSet):
label=pgettext_lazy("Singular", "Species"),
queryset=Species.objects.filter(category=Species.SPECIES)
)
+ provider = ChoiceFilter(
+ field_name='provider',
+ empty_label=_("Provider"),
+ label=_("Provider"),
+ choices=SensitiveArea.objects.provider_choices()
+ )
class Meta(StructureRelatedFilterSet.Meta):
model = SensitiveArea
fields = StructureRelatedFilterSet.Meta.fields + [
- 'species', 'species__category',
+ 'species', 'species__category', 'provider'
]
diff --git a/geotrek/sensitivity/locale/fr/LC_MESSAGES/django.po b/geotrek/sensitivity/locale/fr/LC_MESSAGES/django.po
index b0f97fea2b..0992ea36ed 100644
--- a/geotrek/sensitivity/locale/fr/LC_MESSAGES/django.po
+++ b/geotrek/sensitivity/locale/fr/LC_MESSAGES/django.po
@@ -71,6 +71,9 @@ msgstr "Novembre"
msgid "Decembre"
msgstr "Décembre"
+msgid "Provider"
+msgstr "Fournisseur"
+
msgid "Sport practices"
msgstr "Pratiques sportives"
diff --git a/geotrek/sensitivity/migrations/0022_sensitivearea_provider.py b/geotrek/sensitivity/migrations/0022_sensitivearea_provider.py
new file mode 100644
index 0000000000..318960a4f8
--- /dev/null
+++ b/geotrek/sensitivity/migrations/0022_sensitivearea_provider.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.1.14 on 2022-09-09 13:39
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('sensitivity', '0021_auto_20210218_0734'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='sensitivearea',
+ name='provider',
+ field=models.CharField(blank=True, db_index=True, max_length=1024, verbose_name='Provider'),
+ ),
+ ]
diff --git a/geotrek/sensitivity/models.py b/geotrek/sensitivity/models.py
index b21723c3cb..f02b01d2ff 100644
--- a/geotrek/sensitivity/models.py
+++ b/geotrek/sensitivity/models.py
@@ -3,6 +3,7 @@
"""
import datetime
+from geotrek.common.mixins.managers import NoDeleteManager
import simplekml
from django.conf import settings
@@ -72,6 +73,13 @@ def pretty_practices(self):
return ", ".join([str(practice) for practice in self.practices.all()])
+class SensitiveAreaManager(NoDeleteManager):
+ def provider_choices(self):
+ providers = self.get_queryset().existing().exclude(provider__exact='') \
+ .distinct('provider').values_list('provider', 'provider')
+ return providers
+
+
class SensitiveArea(MapEntityMixin, StructureRelated, TimeStampedModelMixin, NoDeleteMixin,
AddPropertyMixin):
geom = models.GeometryField(srid=settings.SRID)
@@ -81,6 +89,9 @@ class SensitiveArea(MapEntityMixin, StructureRelated, TimeStampedModelMixin, NoD
description = models.TextField(verbose_name=_("Description"), blank=True)
contact = models.TextField(verbose_name=_("Contact"), blank=True)
eid = models.CharField(verbose_name=_("External id"), max_length=1024, blank=True, null=True)
+ provider = models.CharField(verbose_name=_("Provider"), db_index=True, max_length=1024, blank=True)
+
+ objects = SensitiveAreaManager()
class Meta:
verbose_name = _("Sensitive area")
diff --git a/geotrek/sensitivity/templates/sensitivity/sensitivearea_detail_attributes.html b/geotrek/sensitivity/templates/sensitivity/sensitivearea_detail_attributes.html
index 98993b7b47..b27c853ce3 100644
--- a/geotrek/sensitivity/templates/sensitivity/sensitivearea_detail_attributes.html
+++ b/geotrek/sensitivity/templates/sensitivity/sensitivearea_detail_attributes.html
@@ -49,6 +49,18 @@ {% trans "Attributes" %}
{% include "common/publication_info_fragment.html" %}
{% include "mapentity/trackinfo_fragment.html" %}
+
+ {% trans "External id" %} |
+ {% if sensitivearea.eid %}{{ sensitivearea.eid|safe }}
+ {% else %}{% trans "None" %}{% endif %}
+ |
+
+
+ {% trans "Provider" %} |
+ {% if sensitivearea.provider %}{{ sensitivearea.provider|safe }}
+ {% else %}{% trans "None" %}{% endif %}
+ |
+
{{ block.super }}
diff --git a/geotrek/sensitivity/templates/sensitivity/sql/post_90_defaults.sql b/geotrek/sensitivity/templates/sensitivity/sql/post_90_defaults.sql
index de9bd22dba..14d93a40a7 100644
--- a/geotrek/sensitivity/templates/sensitivity/sql/post_90_defaults.sql
+++ b/geotrek/sensitivity/templates/sensitivity/sql/post_90_defaults.sql
@@ -38,4 +38,5 @@ ALTER TABLE sensitivity_sensitivearea ALTER COLUMN contact SET DEFAULT '';
-- structure
ALTER TABLE maintenance_project ALTER COLUMN date_insert SET DEFAULT now();
ALTER TABLE maintenance_project ALTER COLUMN date_update SET DEFAULT now();
+ALTER TABLE sensitivity_sensitivearea ALTER COLUMN provider SET DEFAULT '';
-- deleted
diff --git a/geotrek/sensitivity/tests/test_views.py b/geotrek/sensitivity/tests/test_views.py
index 7fafa09971..5cd071e2ba 100644
--- a/geotrek/sensitivity/tests/test_views.py
+++ b/geotrek/sensitivity/tests/test_views.py
@@ -96,6 +96,7 @@ def setUp(self):
"name": self.species.name,
"period": [False, False, False, False, False, True, True, False, False, False, False, False],
'practices': [p.pk for p in self.species.practices.all()],
+ 'provider': '',
'structure': 'My structure',
'published': True,
}
diff --git a/geotrek/signage/filters.py b/geotrek/signage/filters.py
index 5065ffeea7..9b4bffc8f4 100644
--- a/geotrek/signage/filters.py
+++ b/geotrek/signage/filters.py
@@ -1,4 +1,4 @@
-from django_filters import CharFilter, ModelChoiceFilter, MultipleChoiceFilter
+from django_filters import CharFilter, ModelChoiceFilter, MultipleChoiceFilter, ChoiceFilter
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext_lazy as _
@@ -30,12 +30,18 @@ class SignageFilterSet(AltimetryPointFilterSet, ValidTopologyFilterSet, ZoningFi
intervention_year = MultipleChoiceFilter(label=_("Intervention year"), method='filter_intervention_year',
choices=Intervention.objects.year_choices())
trail = TopologyFilterTrail(label=_('Trail'), required=False)
+ provider = ChoiceFilter(
+ field_name='provider',
+ empty_label=_("Provider"),
+ label=_("Provider"),
+ choices=Signage.objects.provider_choices()
+ )
class Meta(StructureRelatedFilterSet.Meta):
model = Signage
fields = StructureRelatedFilterSet.Meta.fields + ['type', 'condition', 'implantation_year', 'intervention_year',
'published', 'code', 'printed_elevation', 'manager',
- 'sealing']
+ 'sealing', 'provider']
def filter_intervention_year(self, qs, name, value):
signage_ct = ContentType.objects.get_for_model(Signage)
diff --git a/geotrek/signage/locale/fr/LC_MESSAGES/django.po b/geotrek/signage/locale/fr/LC_MESSAGES/django.po
index bf7ee7fc92..cbe10f108e 100644
--- a/geotrek/signage/locale/fr/LC_MESSAGES/django.po
+++ b/geotrek/signage/locale/fr/LC_MESSAGES/django.po
@@ -161,3 +161,6 @@ msgstr "Ajouter"
msgid "meters"
msgstr "mètres"
+
+msgid "Provider"
+msgstr "Fournisseur"
\ No newline at end of file
diff --git a/geotrek/signage/migrations/0024_signage_provider.py b/geotrek/signage/migrations/0024_signage_provider.py
new file mode 100644
index 0000000000..90eb791f7b
--- /dev/null
+++ b/geotrek/signage/migrations/0024_signage_provider.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.1.14 on 2022-09-07 13:43
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('signage', '0023_auto_20220314_1441'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='signage',
+ name='provider',
+ field=models.CharField(blank=True, db_index=True, max_length=1024, verbose_name='Provider'),
+ ),
+ ]
diff --git a/geotrek/signage/models.py b/geotrek/signage/models.py
index fcea619b60..ee89343ddd 100755
--- a/geotrek/signage/models.py
+++ b/geotrek/signage/models.py
@@ -63,6 +63,11 @@ def implantation_year_choices(self):
.values_list('implantation_year', 'implantation_year')
return choices
+ def provider_choices(self):
+ providers = self.get_queryset().existing().exclude(provider__exact='') \
+ .distinct('provider').values_list('provider', 'provider')
+ return providers
+
class Signage(MapEntityMixin, BaseInfrastructure):
""" An infrastructure in the park, which is of type SIGNAGE """
diff --git a/geotrek/signage/parsers.py b/geotrek/signage/parsers.py
new file mode 100644
index 0000000000..8ecea2e08c
--- /dev/null
+++ b/geotrek/signage/parsers.py
@@ -0,0 +1,36 @@
+from geotrek.common.parsers import GeotrekParser
+from geotrek.signage.models import Signage
+
+
+class GeotrekSignageParser(GeotrekParser):
+ """Geotrek parser for Signage"""
+
+ url = None
+ model = Signage
+ constant_fields = {
+ "published": True,
+ "deleted": False
+ }
+ replace_fields = {
+ "eid": "uuid",
+ "geom": "geometry"
+ }
+ url_categories = {
+ 'sealing': 'signage_sealing',
+ 'condition': 'infrastructure_condition',
+ 'type': 'signage_type',
+ }
+ categories_keys_api_v2 = {
+ 'condition': 'label',
+ 'sealing': 'label',
+ 'type': 'label'
+ }
+ natural_keys = {
+ 'condition': 'label',
+ 'sealing': 'label',
+ 'type': 'label'
+ }
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.next_url = f"{self.url}/api/v2/signage"
diff --git a/geotrek/signage/templates/signage/signage_detail_attributes.html b/geotrek/signage/templates/signage/signage_detail_attributes.html
index 9d795c1df1..cbc41dbc38 100644
--- a/geotrek/signage/templates/signage/signage_detail_attributes.html
+++ b/geotrek/signage/templates/signage/signage_detail_attributes.html
@@ -51,6 +51,18 @@
{{ object|verbose:"manager" }} |
{{ object.manager|default:"" }} |
+
+ {% trans "External id" %} |
+ {% if object.eid %}{{ object.eid|safe }}
+ {% else %}{% trans "None" %}{% endif %}
+ |
+
+
+ {% trans "Provider" %} |
+ {% if object.provider %}{{ object.provider|safe }}
+ {% else %}{% trans "None" %}{% endif %}
+ |
+
{% include "altimetry/elevationinfo_fragment.html" %}
{% include "common/publication_info_fragment.html" %}
{% include "mapentity/trackinfo_fragment.html" %}
diff --git a/geotrek/signage/templates/signage/sql/post_90_defaults.sql b/geotrek/signage/templates/signage/sql/post_90_defaults.sql
index 640bf4a3cf..749ea89dbc 100644
--- a/geotrek/signage/templates/signage/sql/post_90_defaults.sql
+++ b/geotrek/signage/templates/signage/sql/post_90_defaults.sql
@@ -19,13 +19,16 @@ ALTER TABLE signage_signage ALTER COLUMN code SET DEFAULT '';
--type
-- topo_object
-- name
-ALTER TABLE infrastructure_infrastructure ALTER COLUMN description SET DEFAULT '';
+ALTER TABLE signage_signage ALTER COLUMN description SET DEFAULT '';
-- condition
-- implantation_year
-- eid
-ALTER TABLE infrastructure_infrastructure ALTER COLUMN published SET DEFAULT FALSE;
+ALTER TABLE signage_signage ALTER COLUMN published SET DEFAULT FALSE;
-- publication_date
-- structure
+ALTER TABLE signage_signage ALTER COLUMN provider SET DEFAULT '';
+
+
-- Direction
------------
diff --git a/geotrek/signage/tests/data/geotrek_parser_v2/signage.json b/geotrek/signage/tests/data/geotrek_parser_v2/signage.json
new file mode 100644
index 0000000000..b01ba09978
--- /dev/null
+++ b/geotrek/signage/tests/data/geotrek_parser_v2/signage.json
@@ -0,0 +1,149 @@
+{
+ "count": 2,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "id": 8416,
+ "attachments": [
+ {
+ "backend": "",
+ "type": "image",
+ "author": "gard",
+ "license": null,
+ "thumbnail": "https://foo.fr/media/paperclip/signage_signage/8416/2018-12-13-151001.jpg.400x0_q85.jpg",
+ "legend": "",
+ "title": "2018-12-13 15.10.01",
+ "url": "https://foo.fr/media/paperclip/signage_signage/8416/2018-12-13-151001.jpg",
+ "uuid": "ca59481d-9f99-40fa-acad-1e10ba323697"
+ }
+ ],
+ "blades": [
+ {
+ "id": 2,
+ "number": "2",
+ "color": 2,
+ "direction": 2,
+ "lines": [
+ {
+ "id": 3,
+ "text": "Joli col",
+ "pictogram": null,
+ "distance": "5.0",
+ "time": null
+ },
+ {
+ "id": 4,
+ "text": "Joli sommet",
+ "pictogram": null,
+ "distance": "2.3",
+ "time": null
+ },
+ {
+ "id": 5,
+ "text": "Joli lac",
+ "pictogram": null,
+ "distance": "0.6",
+ "time": null
+ }
+ ]
+ },
+ {
+ "id": 5,
+ "number": "3",
+ "color": 2,
+ "direction": 2,
+ "lines": [
+ {
+ "id": 10,
+ "text": "Toto la-bas",
+ "pictogram": null,
+ "distance": "14.0",
+ "time": null
+ }
+ ]
+ },
+ {
+ "id": 11,
+ "number": "12",
+ "color": 1,
+ "direction": 1,
+ "lines": [
+ {
+ "id": 20,
+ "text": "test",
+ "pictogram": null,
+ "distance": "2.0",
+ "time": null
+ }
+ ]
+ },
+ {
+ "id": 1,
+ "number": "1",
+ "color": 1,
+ "direction": 2,
+ "lines": [
+ {
+ "id": 2,
+ "text": "Les châlet",
+ "pictogram": null,
+ "distance": null,
+ "time": "01:45:00"
+ },
+ {
+ "id": 1,
+ "text": "Trifouilli",
+ "pictogram": "GR6",
+ "distance": "2.7",
+ "time": "00:15:00"
+ }
+ ]
+ }
+ ],
+ "code": "30123/01",
+ "condition": 8,
+ "description": "des",
+ "eid": null,
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 1.4487099,
+ 42.7892878,
+ 1160.0
+ ]
+ },
+ "implantation_year": 2018,
+ "name": "test gard",
+ "printed_elevation": null,
+ "sealing": 1,
+ "structure": "DEMO",
+ "type": 4,
+ "uuid": "6de1205c-8066-4e8b-8f84-f8736db61e95"
+ },
+ {
+ "id": 8455,
+ "attachments": [],
+ "blades": [],
+ "code": "",
+ "condition": null,
+ "description": "",
+ "eid": null,
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 1.4512499,
+ 42.7880983,
+ 1145.0
+ ]
+ },
+ "implantation_year": 2018,
+ "name": "Test picto defaut",
+ "printed_elevation": null,
+ "sealing": null,
+ "structure": "MC",
+ "type": 3,
+ "uuid": "133e9666-5bb5-4690-8034-6171bf4af115"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/geotrek/signage/tests/data/geotrek_parser_v2/signage_condition.json b/geotrek/signage/tests/data/geotrek_parser_v2/signage_condition.json
new file mode 100644
index 0000000000..6d14c903b4
--- /dev/null
+++ b/geotrek/signage/tests/data/geotrek_parser_v2/signage_condition.json
@@ -0,0 +1,32 @@
+{
+ "count": 5,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "id": 6,
+ "label": "Neuf",
+ "structure": null
+ },
+ {
+ "id": 7,
+ "label": "Bon état",
+ "structure": null
+ },
+ {
+ "id": 8,
+ "label": "Dégradé",
+ "structure": null
+ },
+ {
+ "id": 9,
+ "label": "En ruines",
+ "structure": null
+ },
+ {
+ "id": 10,
+ "label": "Tagué",
+ "structure": null
+ }
+ ]
+}
\ No newline at end of file
diff --git a/geotrek/signage/tests/data/geotrek_parser_v2/signage_ids.json b/geotrek/signage/tests/data/geotrek_parser_v2/signage_ids.json
new file mode 100644
index 0000000000..ed3b697135
--- /dev/null
+++ b/geotrek/signage/tests/data/geotrek_parser_v2/signage_ids.json
@@ -0,0 +1,13 @@
+{
+ "count": 2,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "uuid": "6de1205c-8066-4e8b-8f84-f8736db61e95"
+ },
+ {
+ "uuid": "133e9666-5bb5-4690-8034-6171bf4af115"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/geotrek/signage/tests/data/geotrek_parser_v2/signage_sealing.json b/geotrek/signage/tests/data/geotrek_parser_v2/signage_sealing.json
new file mode 100644
index 0000000000..32d464871d
--- /dev/null
+++ b/geotrek/signage/tests/data/geotrek_parser_v2/signage_sealing.json
@@ -0,0 +1,17 @@
+{
+ "count": 2,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "id": 1,
+ "label": "Socle béton",
+ "structure": null
+ },
+ {
+ "id": 2,
+ "label": "Applique",
+ "structure": null
+ }
+ ]
+}
\ No newline at end of file
diff --git a/geotrek/signage/tests/data/geotrek_parser_v2/signage_type.json b/geotrek/signage/tests/data/geotrek_parser_v2/signage_type.json
new file mode 100644
index 0000000000..f8c799d4aa
--- /dev/null
+++ b/geotrek/signage/tests/data/geotrek_parser_v2/signage_type.json
@@ -0,0 +1,37 @@
+{
+ "count": 5,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "id": 3,
+ "label": "Information",
+ "pictogram": null,
+ "structure": null
+ },
+ {
+ "id": 4,
+ "label": "Limite Cœur",
+ "pictogram": null,
+ "structure": null
+ },
+ {
+ "id": 5,
+ "label": "Porte d'entrée du Parc",
+ "pictogram": "https://foo.fr/media/upload/Lieux_culturel.svg",
+ "structure": null
+ },
+ {
+ "id": 6,
+ "label": "Réglementaire",
+ "pictogram": null,
+ "structure": null
+ },
+ {
+ "id": 9,
+ "label": "Temporaire",
+ "pictogram": null,
+ "structure": null
+ }
+ ]
+}
\ No newline at end of file
diff --git a/geotrek/signage/tests/test_parsers.py b/geotrek/signage/tests/test_parsers.py
new file mode 100644
index 0000000000..35163d5b51
--- /dev/null
+++ b/geotrek/signage/tests/test_parsers.py
@@ -0,0 +1,51 @@
+from unittest import mock
+
+from django.core.management import call_command
+from django.test import TestCase
+from django.test.utils import override_settings
+
+from geotrek.common.models import FileType
+from geotrek.common.tests.mixins import GeotrekParserTestMixin
+from geotrek.signage.models import Signage
+from geotrek.signage.parsers import GeotrekSignageParser
+
+
+class TestGeotrekSignageParser(GeotrekSignageParser):
+ url = "https://test.fr"
+
+ field_options = {
+ 'sealing': {'create': True, },
+ 'condition': {'create': True, },
+ 'type': {'create': True},
+ 'geom': {'required': True}
+ }
+
+
+class SignageGeotrekParserTests(GeotrekParserTestMixin, TestCase):
+ app_label = "signage"
+
+ @classmethod
+ def setUpTestData(cls):
+ cls.filetype = FileType.objects.create(type="Photographie")
+
+ @mock.patch('requests.get')
+ @mock.patch('requests.head')
+ @override_settings(MODELTRANSLATION_DEFAULT_LANGUAGE="fr")
+ def test_create(self, mocked_head, mocked_get):
+ self.mock_time = 0
+ self.mock_json_order = ['signage_sealing.json', 'signage_condition.json', 'signage_type.json', 'signage_ids.json',
+ 'signage.json', ]
+ # Mock GET
+ mocked_get.return_value.status_code = 200
+ mocked_get.return_value.json = self.mock_json
+ mocked_get.return_value.content = b''
+ mocked_head.return_value.status_code = 200
+
+ call_command('import', 'geotrek.signage.tests.test_parsers.TestGeotrekSignageParser', verbosity=0)
+ self.assertEqual(Signage.objects.count(), 2)
+ signage = Signage.objects.all().first()
+ self.assertEqual(str(signage.name), 'test gard')
+ self.assertEqual(str(signage.type), 'Limite Cœur')
+ self.assertEqual(str(signage.sealing), 'Socle béton')
+ self.assertAlmostEqual(signage.geom.x, 572941.1308660918, places=5)
+ self.assertAlmostEqual(signage.geom.y, 6189000.155980503, places=5)
diff --git a/geotrek/tourism/filters.py b/geotrek/tourism/filters.py
index 0995e3d0bb..ea9d8e70d2 100644
--- a/geotrek/tourism/filters.py
+++ b/geotrek/tourism/filters.py
@@ -1,6 +1,6 @@
from django.utils.translation import gettext_lazy as _
-from django_filters.filters import ModelMultipleChoiceFilter
+from django_filters.filters import ModelMultipleChoiceFilter, ChoiceFilter
import django_filters.rest_framework
from django.db.models import Q
from geotrek.authent.filters import StructureRelatedFilterSet
@@ -23,12 +23,19 @@ class TypeFilter(ModelMultipleChoiceFilter):
class TouristicContentFilterSet(ZoningFilterSet, StructureRelatedFilterSet):
type1 = TypeFilter(queryset=TouristicContentType1.objects.all())
type2 = TypeFilter(queryset=TouristicContentType2.objects.all())
+ provider = ChoiceFilter(
+ field_name='provider',
+ empty_label=_("Provider"),
+ label=_("Provider"),
+ choices=TouristicContent.objects.provider_choices()
+ )
class Meta(StructureRelatedFilterSet.Meta):
model = TouristicContent
fields = StructureRelatedFilterSet.Meta.fields + [
'published', 'category', 'type1', 'type2', 'themes',
'approved', 'source', 'portal', 'reservation_system',
+ 'provider'
]
@@ -72,12 +79,18 @@ class TouristicEventFilterSet(ZoningFilterSet, StructureRelatedFilterSet):
after = AfterFilter(label=_("After"))
before = BeforeFilter(label=_("Before"))
completed = CompletedFilter(label=_("Completed"))
+ provider = ChoiceFilter(
+ field_name='provider',
+ empty_label=_("Provider"),
+ label=_("Provider"),
+ choices=TouristicEvent.objects.provider_choices()
+ )
class Meta(StructureRelatedFilterSet.Meta):
model = TouristicEvent
fields = StructureRelatedFilterSet.Meta.fields + [
'published', 'type', 'themes', 'after',
- 'before', 'approved', 'source', 'portal'
+ 'before', 'approved', 'source', 'portal', 'provider'
]
diff --git a/geotrek/tourism/locale/fr/LC_MESSAGES/django.po b/geotrek/tourism/locale/fr/LC_MESSAGES/django.po
index 8301f831d6..1c3618b49f 100644
--- a/geotrek/tourism/locale/fr/LC_MESSAGES/django.po
+++ b/geotrek/tourism/locale/fr/LC_MESSAGES/django.po
@@ -368,3 +368,6 @@ msgstr "Date"
msgid "Invalid geometry value."
msgstr "Valeur de géométrie invalide"
+
+msgid "Provider"
+msgstr "Fournisseur"
diff --git a/geotrek/tourism/migrations/0026_auto_20220907_1400.py b/geotrek/tourism/migrations/0026_auto_20220907_1400.py
new file mode 100644
index 0000000000..4d6e9a5e93
--- /dev/null
+++ b/geotrek/tourism/migrations/0026_auto_20220907_1400.py
@@ -0,0 +1,28 @@
+# Generated by Django 3.1.14 on 2022-09-07 14:00
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('tourism', '0025_auto_20220726_0903'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='informationdesk',
+ name='provider',
+ field=models.CharField(blank=True, db_index=True, max_length=1024, verbose_name='Provider'),
+ ),
+ migrations.AddField(
+ model_name='touristiccontent',
+ name='provider',
+ field=models.CharField(blank=True, db_index=True, max_length=1024, verbose_name='Provider'),
+ ),
+ migrations.AddField(
+ model_name='touristicevent',
+ name='provider',
+ field=models.CharField(blank=True, db_index=True, max_length=1024, verbose_name='Provider'),
+ ),
+ ]
diff --git a/geotrek/tourism/models.py b/geotrek/tourism/models.py
index 8efdedfb5c..4cb85eb72a 100644
--- a/geotrek/tourism/models.py
+++ b/geotrek/tourism/models.py
@@ -14,6 +14,7 @@
from extended_choices import Choices
from geotrek.authent.models import StructureRelated
+from geotrek.common.mixins.managers import NoDeleteManager
from geotrek.common.mixins.models import (AddPropertyMixin, NoDeleteMixin, OptionalPictogramMixin, PictogramMixin,
PicturesMixin, PublishableMixin, TimeStampedModelMixin)
from geotrek.common.models import ReservationSystem, Theme
@@ -59,6 +60,7 @@ def __str__(self):
class InformationDesk(models.Model):
eid = models.CharField(verbose_name=_("External id"), max_length=1024, blank=True, null=True)
+ provider = models.CharField(verbose_name=_("Provider"), db_index=True, max_length=1024, blank=True)
name = models.CharField(verbose_name=_("Title"), max_length=256)
type = models.ForeignKey(InformationDeskType, verbose_name=_("Type"), on_delete=models.CASCADE,
related_name='desks')
@@ -274,6 +276,13 @@ class Meta:
verbose_name_plural = _("Second list types")
+class TouristicContentManager(NoDeleteManager):
+ def provider_choices(self):
+ providers = self.get_queryset().existing().exclude(provider__exact='') \
+ .distinct('provider').values_list('provider', 'provider')
+ return providers
+
+
class TouristicContent(ZoningPropertiesMixin, AddPropertyMixin, PublishableMixin, MapEntityMixin, StructureRelated,
TimeStampedModelMixin, PicturesMixin, NoDeleteMixin):
""" A generic touristic content (accomodation, museum, etc.) in the park
@@ -313,6 +322,7 @@ class TouristicContent(ZoningPropertiesMixin, AddPropertyMixin, PublishableMixin
on_delete=models.CASCADE, related_name='contents', blank=True,
null=True)
eid = models.CharField(verbose_name=_("External id"), max_length=1024, blank=True, null=True)
+ provider = models.CharField(verbose_name=_("Provider"), db_index=True, max_length=1024, blank=True)
reservation_system = models.ForeignKey(ReservationSystem, verbose_name=_("Reservation system"),
on_delete=models.CASCADE, blank=True, null=True)
reservation_id = models.CharField(verbose_name=_("Reservation ID"), max_length=1024,
@@ -320,6 +330,7 @@ class TouristicContent(ZoningPropertiesMixin, AddPropertyMixin, PublishableMixin
approved = models.BooleanField(verbose_name=_("Approved"), default=False,
help_text=_("Indicates whether the content has a label or brand"))
uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
+ objects = TouristicContentManager()
class Meta:
verbose_name = _("Touristic content")
@@ -385,6 +396,13 @@ def __str__(self):
return self.type
+class TouristicEventManager(NoDeleteManager):
+ def provider_choices(self):
+ providers = self.get_queryset().existing().order_by('provider').exclude(provider__exact='') \
+ .distinct('provider').values_list('provider', 'provider')
+ return providers
+
+
class TouristicEvent(ZoningPropertiesMixin, AddPropertyMixin, PublishableMixin, MapEntityMixin, StructureRelated,
PicturesMixin, TimeStampedModelMixin, NoDeleteMixin):
""" A touristic event (conference, workshop, etc.) in the park
@@ -426,9 +444,10 @@ class TouristicEvent(ZoningPropertiesMixin, AddPropertyMixin, PublishableMixin,
blank=True, related_name='touristicevents',
verbose_name=_("Portal"))
eid = models.CharField(verbose_name=_("External id"), max_length=1024, blank=True, null=True)
+ provider = models.CharField(verbose_name=_("Provider"), db_index=True, max_length=1024, blank=True)
approved = models.BooleanField(verbose_name=_("Approved"), default=False)
uuid = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
-
+ objects = TouristicEventManager()
id_prefix = 'E'
class Meta:
diff --git a/geotrek/tourism/parsers.py b/geotrek/tourism/parsers.py
index c390c7ba8b..d8b4b80e08 100644
--- a/geotrek/tourism/parsers.py
+++ b/geotrek/tourism/parsers.py
@@ -14,7 +14,7 @@
from django.core.files.uploadedfile import UploadedFile
from geotrek.common.parsers import (AttachmentParserMixin, Parser,
- TourInSoftParser)
+ TourInSoftParser, GeotrekParser)
from geotrek.tourism.models import (InformationDesk, TouristicContent, TouristicEvent,
TouristicContentType1, TouristicContentType2)
@@ -888,3 +888,160 @@ def filter_end_date(self, src, val):
class TouristicEventTourInSoftParserV3(TouristicEventTourInSoftParser):
version_tourinsoft = 3
+
+
+class GeotrekTouristicContentParser(GeotrekParser):
+ """Geotrek parser for TouristicContent"""
+
+ url = None
+ model = TouristicContent
+ constant_fields = {
+ 'published': True,
+ 'deleted': False,
+ }
+
+ replace_fields = {
+ "eid": "uuid",
+ "geom": "geometry",
+ }
+
+ m2m_replace_fields = {
+ "type1": "types",
+ "type2": "types"
+ }
+
+ url_categories = {
+ "category": "touristiccontent_category",
+ "themes": "theme",
+ }
+
+ categories_keys_api_v2 = {
+ 'category': 'label',
+ 'themes': 'label',
+ }
+
+ natural_keys = {
+ 'category': 'label',
+ 'themes': 'label',
+ 'type1': 'label',
+ 'type2': 'label'
+ }
+
+ field_options = {
+ 'type1': {'fk': 'category'},
+ 'type2': {'fk': 'category'},
+ 'geom': {'required': True},
+ }
+
+ def __init__(self, *args, **kwargs):
+ """Initialize parser with mapping for type1 and type2"""
+ super().__init__(*args, **kwargs)
+ response = self.request_or_retry(f"{self.url}/api/v2/touristiccontent_category/", )
+ self.field_options.setdefault("type1", {})
+ self.field_options.setdefault("type2", {})
+ self.field_options["type1"]["mapping"] = {}
+ self.field_options["type2"]["mapping"] = {}
+ for r in response.json()['results']:
+ for type_category in r['types']:
+ values = type_category["values"]
+ id_category = type_category["id"]
+ if self.create_categories:
+ self.field_options['type1']["create"] = True
+ self.field_options['type2']["create"] = True
+ for value in values:
+ if id_category % 10 == 1:
+ self.field_options['type1']["mapping"][value['id']] = self.replace_mapping(
+ value['label'][settings.MODELTRANSLATION_DEFAULT_LANGUAGE], 'type1'
+ )
+ if id_category % 10 == 2:
+ self.field_options['type2']["mapping"][value['id']] = self.replace_mapping(
+ value['label'][settings.MODELTRANSLATION_DEFAULT_LANGUAGE], 'type2'
+ )
+ self.next_url = f"{self.url}/api/v2/touristiccontent"
+
+ def filter_type1(self, src, val):
+ type1_result = []
+ for key, value in val.items():
+ if int(key) % 10 == 1:
+ type1_result.extend(value)
+ return self.apply_filter('type1', src, type1_result)
+
+ def filter_type2(self, src, val):
+ type2_result = []
+ for key, value in val.items():
+ if int(key) % 10 == 2:
+ type2_result.extend(value)
+ return self.apply_filter('type2', src, type2_result)
+
+
+class GeotrekTouristicEventParser(GeotrekParser):
+ """Geotrek parser for TouristicEvent"""
+
+ url = None
+ model = TouristicEvent
+ constant_fields = {
+ 'published': True,
+ 'deleted': False,
+ }
+ replace_fields = {
+ "eid": "uuid",
+ "geom": "geometry"
+ }
+ url_categories = {
+ "type": "touristicevent_type",
+ }
+ categories_keys_api_v2 = {
+ 'type': 'type',
+ }
+ natural_keys = {
+ 'type': 'type',
+ }
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.next_url = f"{self.url}/api/v2/touristicevent"
+
+
+class GeotrekInformationDeskParser(GeotrekParser):
+ """Geotrek parser for InformationDesk"""
+ url = None
+ model = InformationDesk
+ constant_fields = {}
+ replace_fields = {
+ "eid": "uuid",
+ "geom": ["latitude", "longitude"],
+ "photo": "photo_url"
+ }
+ url_categories = {}
+ categories_keys_api_v2 = {}
+ natural_keys = {
+ 'type': 'label',
+ }
+
+ field_options = {
+ "type": {"create": True},
+ 'geom': {'required': True},
+ }
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.next_url = f"{self.url}/api/v2/informationdesk"
+
+ def filter_geom(self, src, val):
+ lat, lng = val
+ return Point(lng, lat, srid=settings.API_SRID).transform(settings.SRID, clone=True)
+
+ def filter_type(self, src, val):
+ return self.apply_filter('type', src, val["label"][settings.MODELTRANSLATION_DEFAULT_LANGUAGE])
+
+ def filter_photo(self, src, val):
+ if not val:
+ return None
+ content = self.download_attachment(val)
+ if content is None:
+ return None
+ f = ContentFile(content)
+ basename, ext = os.path.splitext(os.path.basename(val))
+ name = '%s%s' % (basename[:128], ext)
+ file = UploadedFile(f, name=name)
+ return file
diff --git a/geotrek/tourism/templates/tourism/sql/post_90_defaults.sql b/geotrek/tourism/templates/tourism/sql/post_90_defaults.sql
index 64d3438db1..0f7b538320 100644
--- a/geotrek/tourism/templates/tourism/sql/post_90_defaults.sql
+++ b/geotrek/tourism/templates/tourism/sql/post_90_defaults.sql
@@ -27,6 +27,7 @@ ALTER TABLE tourism_informationdesk ALTER COLUMN accessibility SET DEFAULT '';
-- geom
-- eid
ALTER TABLE tourism_informationdesk ALTER COLUMN uuid SET DEFAULT gen_random_uuid();
+ALTER TABLE tourism_informationdesk ALTER COLUMN provider SET DEFAULT '';
-- TouristicContentCategory
@@ -77,6 +78,7 @@ ALTER TABLE tourism_touristiccontent ALTER COLUMN published SET DEFAULT FALSE;
-- structure
ALTER TABLE tourism_touristiccontent ALTER COLUMN date_insert SET DEFAULT now();
ALTER TABLE tourism_touristiccontent ALTER COLUMN date_update SET DEFAULT now();
+ALTER TABLE tourism_touristiccontent ALTER COLUMN provider SET DEFAULT '';
-- deleted
@@ -121,3 +123,4 @@ ALTER TABLE tourism_touristicevent ALTER COLUMN published SET DEFAULT FALSE;
ALTER TABLE tourism_touristiccontent ALTER COLUMN date_insert SET DEFAULT now();
ALTER TABLE tourism_touristiccontent ALTER COLUMN date_update SET DEFAULT now();
-- deleted
+ALTER TABLE tourism_touristicevent ALTER COLUMN provider SET DEFAULT '';
diff --git a/geotrek/tourism/templates/tourism/touristiccontent_detail_attributes.html b/geotrek/tourism/templates/tourism/touristiccontent_detail_attributes.html
index 67b54105da..48b510e9de 100644
--- a/geotrek/tourism/templates/tourism/touristiccontent_detail_attributes.html
+++ b/geotrek/tourism/templates/tourism/touristiccontent_detail_attributes.html
@@ -78,7 +78,15 @@ {% trans "Attributes" %}
{% trans "External id" %} |
- {{ object.eid }} |
+ {% if object.eid %}{{ object.eid|safe }}
+ {% else %}{% trans "None" %}{% endif %}
+ |
+
+
+ {% trans "Provider" %} |
+ {% if object.provider %}{{ object.provider|safe }}
+ {% else %}{% trans "None" %}{% endif %}
+ |
{% trans "Reservation system" %} |
diff --git a/geotrek/tourism/templates/tourism/touristicevent_detail_attributes.html b/geotrek/tourism/templates/tourism/touristicevent_detail_attributes.html
index 8128ee25f8..4bb19bbb20 100644
--- a/geotrek/tourism/templates/tourism/touristicevent_detail_attributes.html
+++ b/geotrek/tourism/templates/tourism/touristicevent_detail_attributes.html
@@ -108,7 +108,15 @@ {% trans "Attributes" %}
{% trans "External id" %} |
- {{ object.eid }} |
+ {% if object.eid %}{{ object.eid|safe }}
+ {% else %}{% trans "None" %}{% endif %}
+ |
+
+
+ {% trans "Provider" %} |
+ {% if object.provider %}{{ object.provider|safe }}
+ {% else %}{% trans "None" %}{% endif %}
+ |
{% trans "Thumbnail" %} |
diff --git a/geotrek/tourism/tests/data/geotrek_parser_v2/informationdesk.json b/geotrek/tourism/tests/data/geotrek_parser_v2/informationdesk.json
new file mode 100644
index 0000000000..c4c40ee382
--- /dev/null
+++ b/geotrek/tourism/tests/data/geotrek_parser_v2/informationdesk.json
@@ -0,0 +1,133 @@
+{
+ "count": 3,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "id": 1,
+ "accessibility": {
+ "fr": "",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "description": {
+ "fr": "Informations diverses",
+ "en": "Various information",
+ "es": "Informaciones diversas",
+ "it": null
+ },
+ "email": "",
+ "label_accessibility": null,
+ "latitude": 42.86365970075902,
+ "longitude": 1.2016725540161124,
+ "municipality": "Seix",
+ "name": {
+ "fr": "Office de Tourisme de Seix",
+ "en": "Seix Tourism Office",
+ "es": "Officio de Turismo de Seix",
+ "it": null
+ },
+ "phone": "09 64 46 55 12",
+ "photo_url": "https://foo.fr/media/upload/office-seix.jpg.150x150_q85.jpg",
+ "uuid": "576198ee-b407-4e3c-9f3f-0b5efef02016",
+ "postal_code": "09140",
+ "street": "33 Place Champ de Mars",
+ "type": {
+ "id": 1,
+ "label": {
+ "fr": "Maisons du parc",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "pictogram": "https://foo.fr/media/upload/desktype-info.svg"
+ },
+ "website": "https://www.google.fr/maps/place/Office+Tourisme/@42.8772264,1.207959,15z/data=!4m2!3m1!1s0x12a8caa4377c469d:0xf8f00e9b653bb18f"
+ },
+ {
+ "id": 2,
+ "accessibility": {
+ "fr": "lkkll",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "description": {
+ "fr": "Test description avec\r\n\r\n- des éléments HTML
\r\n- et des listes à
\r\n- puces
\r\n
",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "email": null,
+ "label_accessibility": null,
+ "latitude": 43.58039085560773,
+ "longitude": 1.4282226562500002,
+ "municipality": "Toulouse",
+ "name": {
+ "fr": "Foo",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "phone": null,
+ "photo_url": "https://foo.fr/media/upload/office-seix.jpg.150x150_q85.jpg",
+ "uuid": "576198ee-b407-4e3c-9f3f-0b5efef02014",
+ "postal_code": "31300",
+ "street": "52 rue Jacques Babinet",
+ "type": {
+ "id": 2,
+ "label": {
+ "fr": "Relais d'information",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "pictogram": "https://foo.fr/media/upload/desktype-info.svg"
+ },
+ "website": null
+ },
+ {
+ "id": 3,
+ "accessibility": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "description": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "email": null,
+ "label_accessibility": null,
+ "latitude": null,
+ "longitude": null,
+ "municipality": null,
+ "name": {
+ "fr": "Test fso sans carte",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "phone": null,
+ "photo_url": "",
+ "uuid": "576198ee-b407-4e3c-9f3f-0b5efef02015",
+ "postal_code": null,
+ "street": null,
+ "type": {
+ "id": 3,
+ "label": {
+ "fr": "Office du tourisme",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "pictogram": "https://foo.fr/media/upload/desktype-info.svg"
+ },
+ "website": null
+ }
+ ]
+}
\ No newline at end of file
diff --git a/geotrek/tourism/tests/data/geotrek_parser_v2/informationdesk_ids.json b/geotrek/tourism/tests/data/geotrek_parser_v2/informationdesk_ids.json
new file mode 100644
index 0000000000..e113fcd438
--- /dev/null
+++ b/geotrek/tourism/tests/data/geotrek_parser_v2/informationdesk_ids.json
@@ -0,0 +1,13 @@
+{
+ "count": 2,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "uuid": "576198ee-b407-4e3c-9f3f-0b5efef02016"
+ },
+ {
+ "uuid": "576198ee-b407-4e3c-9f3f-0b5efef02014"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/geotrek/tourism/tests/data/geotrek_parser_v2/touristiccontent.json b/geotrek/tourism/tests/data/geotrek_parser_v2/touristiccontent.json
new file mode 100644
index 0000000000..e4feb8401b
--- /dev/null
+++ b/geotrek/tourism/tests/data/geotrek_parser_v2/touristiccontent.json
@@ -0,0 +1,198 @@
+{
+ "count": 2,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "id": 10,
+ "accessibility": {
+ "fr": "",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "attachments": [
+ {
+ "backend": "",
+ "type": "image",
+ "author": "Wikimedia Common",
+ "license": null,
+ "thumbnail": "https://foo.fr/media/paperclip/tourism_touristiccontent/10/anes-01.jpg.400x0_q85.jpg",
+ "legend": "Ânes",
+ "title": "anes-01",
+ "url": "https://foo.fr/media/paperclip/tourism_touristiccontent/10/anes-01.jpg",
+ "uuid": "1f9887b3-dc75-46a3-a93e-e2c1a8ef7105"
+ },
+ {
+ "backend": "",
+ "type": "image",
+ "author": "Wikimedia Commons",
+ "license": null,
+ "thumbnail": "https://foo.fr/media/paperclip/tourism_touristiccontent/10/anes-02.jpg.400x0_q85.jpg",
+ "legend": "Ânes",
+ "title": "anes-02",
+ "url": "https://foo.fr/media/paperclip/tourism_touristiccontent/10/anes-02.jpg",
+ "uuid": "36d83840-ed78-4d2e-9937-d259930f053e"
+ },
+ {
+ "backend": "Attachment",
+ "type": "video",
+ "author": "",
+ "license": null,
+ "thumbnail": "",
+ "legend": "",
+ "title": "",
+ "url": "https://soundcloud.com/festivalnumerozero/radio-petit-zero-3-sortir-balade-jaune",
+ "uuid": "1918257f-19fb-4387-b117-d5ceb4a0f4ab"
+ }
+ ],
+ "approved": false,
+ "category": 3,
+ "description": {
+ "fr": "Vacances à la ferme, Activités en pleine nature dans les Pyrénées.
\r\nNous vous accueillons en pleine montagne entre le massif des Trois Seigneurs et l’Étang de Lers en Ariège, Midi-Pyrénées. Passionnés par les ânes et la randonnée, nous élevons nos ânes depuis
\r\n1998, qui sont éduqués et câlinés depuis tout petit.
\r\nPartagez le plaisir de randonnée léger et libre avec nous et permettez aux enfants de découvrir
\r\nune autre monde au milieu de la nature.
",
+ "en": "",
+ "es": "",
+ "it": null
+ },
+ "description_teaser": {
+ "fr": "Élevage et Éducation d'ânes, Séjours nature et montagne en Ariège, Accueil Cavaliers.
",
+ "en": "",
+ "es": "",
+ "it": null
+ },
+ "departure_city": "09231",
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 1.3877974,
+ 42.8597065
+ ]
+ },
+ "label_accessibility": null,
+ "practical_info": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": null
+ },
+ "url": "https://foo.fr/api/v2/touristiccontent/10/",
+ "cities": [
+ "09231"
+ ],
+ "create_datetime": "2015-12-30T14:12:50.965428+01:00",
+ "external_id": "",
+ "name": {
+ "fr": "Balad'âne",
+ "en": "",
+ "es": "",
+ "it": null
+ },
+ "pdf": {
+ "fr": "https://foo.fr/api/fr/touristiccontents/10/baladane.pdf",
+ "en": "https://foo.fr/api/en/touristiccontents/10/baladane.pdf",
+ "es": "https://foo.fr/api/es/touristiccontents/10/baladane.pdf",
+ "it": "https://foo.fr/api/it/touristiccontents/10/baladane.pdf"
+ },
+ "portal": [],
+ "published": true,
+ "source": [],
+ "structure": 1,
+ "themes": [
+ 1,
+ 6
+ ],
+ "update_datetime": "2015-12-30T14:17:31.483686+01:00",
+ "types": {
+ "301": [
+ 24
+ ],
+ "302": [
+ 38
+ ]
+ },
+ "contact": "Balad'âne - Salbis - 09320 Le Port - Tel : 33 (0)5 61 04 41 35
",
+ "email": "",
+ "website": "http://www.baladane.com/fr/",
+ "reservation_system": null,
+ "reservation_id": "",
+ "uuid": "921f9859-94b5-447a-ad20-946697323569"
+ },
+ {
+ "id": 22,
+ "accessibility": {
+ "fr": "",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "attachments": [],
+ "approved": false,
+ "category": 5,
+ "description": {
+ "fr": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. ",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "description_teaser": {
+ "fr": "Ce magnifique bar a requin est au beau milieu des pyrénées.",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "departure_city": "09322",
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 1.2808534,
+ 42.735508
+ ]
+ },
+ "label_accessibility": null,
+ "practical_info": {
+ "fr": "Attention à l'épilepsie",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "url": "https://foo.fr/api/v2/touristiccontent/22/",
+ "cities": [
+ "09322"
+ ],
+ "create_datetime": "2019-09-17T14:55:46.308511+02:00",
+ "external_id": null,
+ "name": {
+ "fr": "Bar à requin",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "pdf": {
+ "fr": "https://foo.fr/api/fr/touristiccontents/22/bar-a-requin.pdf",
+ "en": "https://foo.fr/api/en/touristiccontents/22/bar-a-requin.pdf",
+ "es": "https://foo.fr/api/es/touristiccontents/22/bar-a-requin.pdf",
+ "it": "https://foo.fr/api/it/touristiccontents/22/bar-a-requin.pdf"
+ },
+ "portal": [],
+ "published": true,
+ "source": [],
+ "structure": 3,
+ "themes": [
+ 1
+ ],
+ "update_datetime": "2019-09-17T15:04:54.043628+02:00",
+ "types": {
+ "501": [
+ 43
+ ],
+ "502": []
+ },
+ "contact": "Contacter Jean",
+ "email": null,
+ "website": "http://make-everything-ok.com/",
+ "reservation_system": null,
+ "reservation_id": "",
+ "uuid": "a9bf823a-a472-4eb2-b273-3db1355602d0"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/geotrek/tourism/tests/data/geotrek_parser_v2/touristiccontent_category.json b/geotrek/tourism/tests/data/geotrek_parser_v2/touristiccontent_category.json
new file mode 100644
index 0000000000..67e843aa1a
--- /dev/null
+++ b/geotrek/tourism/tests/data/geotrek_parser_v2/touristiccontent_category.json
@@ -0,0 +1,385 @@
+{
+ "count": 7,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "id": 1,
+ "label": {
+ "fr": "Hébergement",
+ "en": "Accommodation",
+ "es": null,
+ "it": null
+ },
+ "order": 20,
+ "pictogram": "https://foo.fr/media/upload/touristiccontent-accommodation.svg",
+ "types": [
+ {
+ "id": 101,
+ "label": {
+ "fr": "Type d'usage",
+ "en": "Type of use",
+ "es": null,
+ "it": null
+ },
+ "values": [
+ {
+ "id": 4,
+ "label": {
+ "fr": "Gite d'étape",
+ "en": "Stopover",
+ "es": null,
+ "it": null
+ },
+ "pictogram": null
+ },
+ {
+ "id": 6,
+ "label": {
+ "fr": "Village de vacances",
+ "en": "Holiday village",
+ "es": null,
+ "it": null
+ },
+ "pictogram": "/media/upload/Lieux_culturel_0rT7C2L.svg"
+ }
+ ]
+ },
+ {
+ "id": 102,
+ "label": {
+ "fr": "Label",
+ "en": "Label",
+ "es": null,
+ "it": null
+ },
+ "values": []
+ }
+ ]
+ },
+ {
+ "id": 2,
+ "label": {
+ "fr": "Pleine Nature",
+ "en": "Outdoor",
+ "es": null,
+ "it": null
+ },
+ "order": 13,
+ "pictogram": "https://foo.fr/media/upload/touristiccontent-outdoor.svg",
+ "types": [
+ {
+ "id": 201,
+ "label": {
+ "fr": "Type d'usage",
+ "en": "Type of use",
+ "es": null,
+ "it": null
+ },
+ "values": [
+ {
+ "id": 14,
+ "label": {
+ "fr": "Canyoning",
+ "en": "Canyoning",
+ "es": null,
+ "it": null
+ },
+ "pictogram": "/media/upload/noun_canyon_3005142.png"
+ },
+ {
+ "id": 17,
+ "label": {
+ "fr": "Course d'orientation",
+ "en": "Orienteering",
+ "es": null,
+ "it": null
+ },
+ "pictogram": "/media/upload/noun_Running_65866.png"
+ }
+ ]
+ },
+ {
+ "id": 202,
+ "label": {
+ "fr": "",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "values": []
+ }
+ ]
+ },
+ {
+ "id": 3,
+ "label": {
+ "fr": "Sorties",
+ "en": "Visits",
+ "es": null,
+ "it": null
+ },
+ "order": 28,
+ "pictogram": "https://foo.fr/media/upload/touristiccontent-visits.svg",
+ "types": [
+ {
+ "id": 301,
+ "label": {
+ "fr": "Type d'usage",
+ "en": "Type of use",
+ "es": null,
+ "it": null
+ },
+ "values": [
+ {
+ "id": 24,
+ "label": {
+ "fr": "Ane",
+ "en": "Donkey",
+ "es": null,
+ "it": null
+ },
+ "pictogram": null
+ },
+ {
+ "id": 30,
+ "label": {
+ "fr": "Canoë-Kayak",
+ "en": "Canoe-Kayak",
+ "es": null,
+ "it": null
+ },
+ "pictogram": null
+ },
+ {
+ "id": 29,
+ "label": {
+ "fr": "Canyoning",
+ "en": "Canyoning",
+ "es": null,
+ "it": null
+ },
+ "pictogram": null
+ },
+ {
+ "id": 32,
+ "label": {
+ "fr": "Vol libre",
+ "en": "Free fly",
+ "es": null,
+ "it": null
+ },
+ "pictogram": null
+ }
+ ]
+ },
+ {
+ "id": 302,
+ "label": {
+ "fr": "Service",
+ "en": "Prestation",
+ "es": null,
+ "it": null
+ },
+ "values": [
+ {
+ "id": 38,
+ "label": {
+ "fr": "Accompagnée",
+ "en": "Guided",
+ "es": null,
+ "it": null
+ },
+ "pictogram": null
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "id": 5,
+ "label": {
+ "fr": "Restaurants",
+ "en": "Restaurants",
+ "es": null,
+ "it": null
+ },
+ "order": 30,
+ "pictogram": "https://foo.fr/media/upload/touristiccontent-restaurants.svg",
+ "types": [
+ {
+ "id": 501,
+ "label": {
+ "fr": "Type d'usage",
+ "en": "Type of use",
+ "es": null,
+ "it": null
+ },
+ "values": [
+ {
+ "id": 43,
+ "label": {
+ "fr": "Restaurant",
+ "en": "Restaurant",
+ "es": null,
+ "it": null
+ },
+ "pictogram": null
+ }
+ ]
+ },
+ {
+ "id": 502,
+ "label": {
+ "fr": "",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "values": []
+ }
+ ]
+ },
+ {
+ "id": 7,
+ "label": {
+ "fr": "Séjours",
+ "en": "Destinations",
+ "es": null,
+ "it": null
+ },
+ "order": 25,
+ "pictogram": "https://foo.fr/media/upload/touristiccontent-destination.svg",
+ "types": [
+ {
+ "id": 701,
+ "label": {
+ "fr": "Thématique",
+ "en": "Theme",
+ "es": null,
+ "it": null
+ },
+ "values": [
+ {
+ "id": 49,
+ "label": {
+ "fr": "Bien être",
+ "en": "Wellness",
+ "es": null,
+ "it": null
+ },
+ "pictogram": null
+ },
+ {
+ "id": 50,
+ "label": {
+ "fr": "Nature",
+ "en": "Nature",
+ "es": null,
+ "it": null
+ },
+ "pictogram": null
+ }
+ ]
+ },
+ {
+ "id": 702,
+ "label": {
+ "fr": "",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "values": []
+ }
+ ]
+ },
+ {
+ "id": 8,
+ "label": {
+ "fr": "Musée",
+ "en": "Museum",
+ "es": null,
+ "it": null
+ },
+ "order": 40,
+ "pictogram": "https://foo.fr/media/upload/touristiccontent-museum.svg",
+ "types": [
+ {
+ "id": 801,
+ "label": {
+ "fr": "Type d'usage",
+ "en": "Type of use",
+ "es": null,
+ "it": null
+ },
+ "values": [
+ {
+ "id": 58,
+ "label": {
+ "fr": "Lieu de visite",
+ "en": "Visit site",
+ "es": null,
+ "it": null
+ },
+ "pictogram": null
+ },
+ {
+ "id": 57,
+ "label": {
+ "fr": "Musée",
+ "en": "Museum",
+ "es": null,
+ "it": null
+ },
+ "pictogram": null
+ }
+ ]
+ },
+ {
+ "id": 802,
+ "label": {
+ "fr": "",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "values": []
+ }
+ ]
+ },
+ {
+ "id": 9,
+ "label": {
+ "fr": "Course d'orientation",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "order": 12,
+ "pictogram": "https://foo.fr/media/upload/touristiccontent-sites_2.svg",
+ "types": [
+ {
+ "id": 901,
+ "label": {
+ "fr": "Genre",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "values": []
+ },
+ {
+ "id": 902,
+ "label": {
+ "fr": "Label",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "values": []
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/geotrek/tourism/tests/data/geotrek_parser_v2/touristiccontent_ids.json b/geotrek/tourism/tests/data/geotrek_parser_v2/touristiccontent_ids.json
new file mode 100644
index 0000000000..5252af41e0
--- /dev/null
+++ b/geotrek/tourism/tests/data/geotrek_parser_v2/touristiccontent_ids.json
@@ -0,0 +1,13 @@
+{
+ "count": 2,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "uuid": "921f9859-94b5-447a-ad20-946697323569"
+ },
+ {
+ "uuid": "a9bf823a-a472-4eb2-b273-3db1355602d0"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/geotrek/tourism/tests/data/geotrek_parser_v2/touristiccontent_themes.json b/geotrek/tourism/tests/data/geotrek_parser_v2/touristiccontent_themes.json
new file mode 100644
index 0000000000..cc77967cf7
--- /dev/null
+++ b/geotrek/tourism/tests/data/geotrek_parser_v2/touristiccontent_themes.json
@@ -0,0 +1,107 @@
+{
+ "count": 10,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "id": 1,
+ "label": {
+ "fr": "Faune",
+ "en": "Fauna",
+ "es": null,
+ "it": null
+ },
+ "pictogram": "https://foo.fr/media/upload/theme-fauna.png"
+ },
+ {
+ "id": 2,
+ "label": {
+ "fr": "Flore",
+ "en": "Flora",
+ "es": null,
+ "it": null
+ },
+ "pictogram": "https://foo.fr/media/upload/theme-flora.png"
+ },
+ {
+ "id": 4,
+ "label": {
+ "fr": "Point de vue",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "pictogram": "https://foo.fr/media/upload/theme-panorama.png"
+ },
+ {
+ "id": 5,
+ "label": {
+ "fr": "Architecture",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "pictogram": "https://foo.fr/media/upload/theme-architecture.png"
+ },
+ {
+ "id": 6,
+ "label": {
+ "fr": "Pastoralisme",
+ "en": "Pastoralism",
+ "es": null,
+ "it": null
+ },
+ "pictogram": "https://foo.fr/media/upload/theme-pastoral.png"
+ },
+ {
+ "id": 7,
+ "label": {
+ "fr": "Géologie",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "pictogram": "https://foo.fr/media/upload/theme-geology.png"
+ },
+ {
+ "id": 8,
+ "label": {
+ "fr": "Lac et glacier",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "pictogram": "https://foo.fr/media/upload/theme-lake.png"
+ },
+ {
+ "id": 9,
+ "label": {
+ "fr": "Sommet",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "pictogram": "https://foo.fr/media/upload/theme-peak.png"
+ },
+ {
+ "id": 10,
+ "label": {
+ "fr": "Refuge",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "pictogram": "https://foo.fr/media/upload/theme-refugee.png"
+ },
+ {
+ "id": 11,
+ "label": {
+ "fr": "Archéologie et histoire",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "pictogram": "https://foo.fr/media/upload/theme-history.png"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/geotrek/tourism/tests/data/geotrek_parser_v2/touristicevent.json b/geotrek/tourism/tests/data/geotrek_parser_v2/touristicevent.json
new file mode 100644
index 0000000000..d733bd5822
--- /dev/null
+++ b/geotrek/tourism/tests/data/geotrek_parser_v2/touristicevent.json
@@ -0,0 +1,160 @@
+{
+ "count": 2,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "id": 3,
+ "accessibility": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": null
+ },
+ "approved": false,
+ "attachments": [],
+ "begin_date": "2025-07-31",
+ "booking": "",
+ "cities": [],
+ "contact": "Autrefois le Couserans
Place Alphonse Sentein
09200 Saint-Girons
05 34 14 62 69
09 67 11 26 69
",
+ "create_datetime": "2015-03-25T14:09:44.346826+01:00",
+ "description": {
+ "fr": "Ainsi défile dans les rues et sur les boulevards de la ville, la longue caravane constituée de plus d'une cinquantaine de vieux tracteurs, en parfait état de fonctionnement, mais également de vieilles machines agricoles , faucheuses, batteuses « qui dans la journée avaleront goulûment leurs gerbes de blés ». Tous ces matériels et engins, sans oublier les charrettes, chars et tombereaux, sont guidés par des hommes, des femmes et des enfants en costumes d'autrefois : boeufs, chevaux, ânes et mules tirent les attelages chargés de bois, de fourrage, de fumier...plus loin suivent d'autres animaux, moutons, dindons, oies et canards, chiens et derrière le cortège des lavandières, des couturières, des gardeuses d'oies, le livreur de lait, le facteur, le rémouleur, le marchand de pain, les porteurs d'eau, les marchandes des quatre saisons, la classe 1900, Monsieur le Curé et tant d'autres..
",
+ "en": "",
+ "es": "",
+ "it": null
+ },
+ "description_teaser": {
+ "fr": "Cette manifestation qui s’inscrit dorénavant dans le patrimoine Ariégeois, mobilise 800 participants et attire chaque premier week end d’Août 25 000 personnes venues de tout le Sud Ouest et de plus loin encore.
",
+ "en": "",
+ "es": "",
+ "it": null
+ },
+ "duration": "3",
+ "email": "contact@autrefois-le-couserans.com",
+ "end_date": "2025-08-02",
+ "external_id": "",
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 1.1496792,
+ 42.9637089
+ ]
+ },
+ "meeting_point": "Centre ville de Saint Girons",
+ "meeting_time": null,
+ "name": {
+ "fr": "Autrefois le Couserans",
+ "en": "",
+ "es": "",
+ "it": null
+ },
+ "organizer": "",
+ "participant_number": "",
+ "pdf": {
+ "fr": "https://foo.fr/api/fr/touristicevents/3/autrefois-le-couserans.pdf",
+ "en": "https://foo.fr/api/en/touristicevents/3/autrefois-le-couserans.pdf",
+ "es": "https://foo.fr/api/es/touristicevents/3/autrefois-le-couserans.pdf",
+ "it": "https://foo.fr/api/it/touristicevents/3/autrefois-le-couserans.pdf"
+ },
+ "portal": [],
+ "practical_info": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": null
+ },
+ "published": true,
+ "source": [],
+ "speaker": "",
+ "structure": 1,
+ "target_audience": "",
+ "themes": [],
+ "type": 6,
+ "update_datetime": "2016-03-16T16:50:37.409679+01:00",
+ "url": "https://foo.fr/api/v2/touristicevent/3/",
+ "uuid": "7382c443-8206-4ef2-bf43-f471cdc5073d",
+ "website": "http://www.autrefois-le-couserans.com/"
+ },
+ {
+ "id": 1,
+ "accessibility": {
+ "fr": "accessible aux handicapés moteurs",
+ "en": "",
+ "es": "",
+ "it": null
+ },
+ "approved": false,
+ "attachments": [],
+ "begin_date": "2025-01-16",
+ "booking": "Pour les réservations, voir l'encars contact
",
+ "cities": [
+ "09100"
+ ],
+ "contact": "Contactez moi, ou elle, ou lui !
06 12 12 12 12 12
",
+ "create_datetime": "2015-01-17T14:44:58.483513+01:00",
+ "description": {
+ "fr": "Ceci est un test evenement tourristique
",
+ "en": "",
+ "es": "",
+ "it": null
+ },
+ "description_teaser": {
+ "fr": "Les evenements tourristiques sont supers
",
+ "en": "",
+ "es": "",
+ "it": null
+ },
+ "duration": "premier semestre",
+ "email": "test@test.fr",
+ "end_date": "2025-04-16",
+ "external_id": "",
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 1.1946237,
+ 42.7666746
+ ]
+ },
+ "meeting_point": "Au bout du monde",
+ "meeting_time": "15:45:00",
+ "name": {
+ "fr": "Festival du test",
+ "en": "",
+ "es": "",
+ "it": null
+ },
+ "organizer": "Mr Test",
+ "participant_number": "12",
+ "pdf": {
+ "fr": "https://foo.fr/api/fr/touristicevents/1/festival-du-test.pdf",
+ "en": "https://foo.fr/api/en/touristicevents/1/festival-du-test.pdf",
+ "es": "https://foo.fr/api/es/touristicevents/1/festival-du-test.pdf",
+ "it": "https://foo.fr/api/it/touristicevents/1/festival-du-test.pdf"
+ },
+ "portal": [],
+ "practical_info": {
+ "fr": "Venez en bonne compagnie et avec le sourire !
",
+ "en": "",
+ "es": "",
+ "it": null
+ },
+ "published": true,
+ "source": [],
+ "speaker": "Batman",
+ "structure": 1,
+ "target_audience": "Tout public",
+ "themes": [
+ 5,
+ 7,
+ 4,
+ 10
+ ],
+ "type": 6,
+ "update_datetime": "2016-03-16T16:50:09.774431+01:00",
+ "url": "https://foo.fr/api/v2/touristicevent/1/",
+ "uuid": "20fb113f-07f7-4deb-94b4-8445c85d5598",
+ "website": "http://test.fr/"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/geotrek/tourism/tests/data/geotrek_parser_v2/touristicevent_ids.json b/geotrek/tourism/tests/data/geotrek_parser_v2/touristicevent_ids.json
new file mode 100644
index 0000000000..ab17c78dff
--- /dev/null
+++ b/geotrek/tourism/tests/data/geotrek_parser_v2/touristicevent_ids.json
@@ -0,0 +1,13 @@
+{
+ "count": 2,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "uuid": "7382c443-8206-4ef2-bf43-f471cdc5073d"
+ },
+ {
+ "uuid": "20fb113f-07f7-4deb-94b4-8445c85d5598"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/geotrek/tourism/tests/data/geotrek_parser_v2/touristicevent_type.json b/geotrek/tourism/tests/data/geotrek_parser_v2/touristicevent_type.json
new file mode 100644
index 0000000000..3911e20712
--- /dev/null
+++ b/geotrek/tourism/tests/data/geotrek_parser_v2/touristicevent_type.json
@@ -0,0 +1,17 @@
+{
+ "count": 1,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "id": 6,
+ "pictogram": null,
+ "type": {
+ "fr": "Spectacle",
+ "en": "Show",
+ "es": null,
+ "it": null
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/geotrek/tourism/tests/test_parsers.py b/geotrek/tourism/tests/test_parsers.py
index 05905a248c..50702f7f16 100644
--- a/geotrek/tourism/tests/test_parsers.py
+++ b/geotrek/tourism/tests/test_parsers.py
@@ -9,6 +9,7 @@
from django.core.management import call_command
from django.core.management.base import CommandError
+from geotrek.common.tests.mixins import GeotrekParserTestMixin
from geotrek.common.tests.factories import RecordSourceFactory, TargetPortalFactory
from geotrek.common.models import Attachment, FileType
from geotrek.common.tests import TranslationResetMixin
@@ -19,7 +20,8 @@
from geotrek.tourism.parsers import (TouristicContentApidaeParser, TouristicEventApidaeParser, EspritParcParser,
TouristicContentTourInSoftParserV3, TouristicContentTourInSoftParserV3withMedias,
TouristicContentTourInSoftParser, TouristicEventTourInSoftParser,
- InformationDeskApidaeParser)
+ InformationDeskApidaeParser, GeotrekTouristicContentParser,
+ GeotrekTouristicEventParser, GeotrekInformationDeskParser)
class ApidaeConstantFieldContentParser(TouristicContentApidaeParser):
@@ -647,3 +649,167 @@ def mocked_json():
information_desk_2 = InformationDesk.objects.get(eid=2)
self.assertEqual(information_desk_2.website, None)
+
+
+class TestGeotrekTouristicContentParser(GeotrekTouristicContentParser):
+ url = "https://test.fr"
+
+ field_options = {
+ "category": {'create': True},
+ 'themes': {'create': True},
+ 'type1': {'create': True, 'fk': 'category'},
+ 'type2': {'create': True, 'fk': 'category'},
+ 'geom': {'required': True},
+ }
+
+
+class TestGeotrekTouristicContentCreateCategoriesParser(GeotrekTouristicContentParser):
+ url = "https://test.fr"
+ create_categories = True
+
+
+class TestGeotrekTouristicEventParser(GeotrekTouristicEventParser):
+ url = "https://test.fr"
+
+ field_options = {
+ 'type': {'create': True, },
+ 'geom': {'required': True},
+ }
+
+
+class TestGeotrekInformationDeskParser(GeotrekInformationDeskParser):
+ url = "https://test.fr"
+
+ field_options = {
+ 'type': {'create': True, },
+ 'geom': {'required': True},
+ }
+
+
+class TouristicContentGeotrekParserTests(GeotrekParserTestMixin, TestCase):
+ app_label = "tourism"
+
+ @classmethod
+ def setUpTestData(cls):
+ cls.filetype = FileType.objects.create(type="Photographie")
+
+ @mock.patch('requests.get')
+ @mock.patch('requests.head')
+ @override_settings(MODELTRANSLATION_DEFAULT_LANGUAGE="fr")
+ def test_create(self, mocked_head, mocked_get):
+ self.mock_time = 0
+ self.mock_json_order = ['touristiccontent_category.json',
+ 'touristiccontent_themes.json',
+ 'touristiccontent_category.json',
+ 'touristiccontent_ids.json',
+ 'touristiccontent.json']
+
+ # Mock GET
+ mocked_get.return_value.status_code = 200
+ mocked_get.return_value.json = self.mock_json
+ mocked_get.return_value.content = b''
+ mocked_head.return_value.status_code = 200
+
+ call_command('import', 'geotrek.tourism.tests.test_parsers.TestGeotrekTouristicContentCreateCategoriesParser', verbosity=0)
+ self.assertEqual(TouristicContent.objects.count(), 2)
+ touristic_content = TouristicContent.objects.all().first()
+ self.assertEqual(str(touristic_content.category), 'Sorties')
+ self.assertEqual(str(touristic_content.type1.first()), 'Ane')
+ self.assertEqual(str(touristic_content.name), "Balad'âne")
+ self.assertAlmostEqual(touristic_content.geom.x, 568112.6362873032, places=5)
+ self.assertAlmostEqual(touristic_content.geom.y, 6196929.676669887, places=5)
+ self.assertEqual(Attachment.objects.count(), 3)
+
+ @mock.patch('requests.get')
+ @mock.patch('requests.head')
+ @override_settings(MODELTRANSLATION_DEFAULT_LANGUAGE="fr")
+ def test_create_create_categories(self, mocked_head, mocked_get):
+ self.mock_time = 0
+ self.mock_json_order = ['touristiccontent_category.json',
+ 'touristiccontent_themes.json',
+ 'touristiccontent_category.json',
+ 'touristiccontent_ids.json',
+ 'touristiccontent.json']
+
+ # Mock GET
+ mocked_get.return_value.status_code = 200
+ mocked_get.return_value.json = self.mock_json
+ mocked_get.return_value.content = b''
+ mocked_head.return_value.status_code = 200
+
+ call_command('import', 'geotrek.tourism.tests.test_parsers.TestGeotrekTouristicContentParser', verbosity=0)
+ self.assertEqual(TouristicContent.objects.count(), 2)
+ touristic_content = TouristicContent.objects.all().first()
+ self.assertEqual(str(touristic_content.category), 'Sorties')
+ self.assertEqual(str(touristic_content.name), "Balad'âne")
+ self.assertAlmostEqual(touristic_content.geom.x, 568112.6362873032, places=5)
+ self.assertAlmostEqual(touristic_content.geom.y, 6196929.676669887, places=5)
+ self.assertEqual(Attachment.objects.count(), 3)
+
+
+class TouristicEventGeotrekParserTests(GeotrekParserTestMixin, TestCase):
+ app_label = "tourism"
+
+ @classmethod
+ def setUpTestData(cls):
+ cls.filetype = FileType.objects.create(type="Photographie")
+
+ @mock.patch('requests.get')
+ @mock.patch('requests.head')
+ @override_settings(MODELTRANSLATION_DEFAULT_LANGUAGE="fr")
+ def test_create(self, mocked_head, mocked_get):
+ self.mock_time = 0
+ self.mock_json_order = ['touristicevent_type.json',
+ 'touristicevent_ids.json',
+ 'touristicevent.json']
+
+ # Mock GET
+ mocked_get.return_value.status_code = 200
+ mocked_get.return_value.json = self.mock_json
+ mocked_get.return_value.content = b''
+ mocked_head.return_value.status_code = 200
+
+ call_command('import', 'geotrek.tourism.tests.test_parsers.TestGeotrekTouristicEventParser', verbosity=0)
+ self.assertEqual(TouristicEvent.objects.count(), 2)
+ touristic_event = TouristicEvent.objects.all().first()
+ self.assertEqual(str(touristic_event.type), 'Spectacle')
+ self.assertEqual(str(touristic_event.name), "Autrefois le Couserans")
+ self.assertAlmostEqual(touristic_event.geom.x, 548907.5259389633, places=5)
+ self.assertAlmostEqual(touristic_event.geom.y, 6208918.713349126, places=5)
+
+
+class InformationDeskGeotrekParserTests(GeotrekParserTestMixin, TestCase):
+ app_label = "tourism"
+
+ @classmethod
+ def setUpTestData(cls):
+ cls.filetype = FileType.objects.create(type="Photographie")
+
+ @mock.patch('requests.get')
+ @mock.patch('geotrek.common.parsers.AttachmentParserMixin.download_attachment')
+ @override_settings(MODELTRANSLATION_DEFAULT_LANGUAGE="fr")
+ def test_create(self, mocked_download_attachment, mocked_get):
+ self.mock_time = 0
+ self.mock_json_order = ['informationdesk_ids.json',
+ 'informationdesk.json', ]
+ self.mock_download = 0
+
+ def mocked_download(*args, **kwargs):
+ if self.mock_download > 0:
+ return None
+ self.mock_download += 1
+ return b'boo'
+
+ # Mock GET
+ mocked_get.return_value.status_code = 200
+ mocked_get.return_value.json = self.mock_json
+ mocked_download_attachment.side_effect = mocked_download
+ call_command('import', 'geotrek.tourism.tests.test_parsers.TestGeotrekInformationDeskParser', verbosity=0)
+ self.assertEqual(InformationDesk.objects.count(), 3)
+ information_desk = InformationDesk.objects.all().first()
+ self.assertEqual(str(information_desk.type), "Relais d'information")
+ self.assertEqual(str(information_desk.name), "Foo")
+ self.assertAlmostEqual(information_desk.geom.x, 573013.9272605104, places=5)
+ self.assertAlmostEqual(information_desk.geom.y, 6276967.321705549, places=5)
+ self.assertEqual(str(information_desk.photo), '')
+ self.assertEqual(InformationDesk.objects.exclude(photo='').first().photo.read(), b'boo')
diff --git a/geotrek/trekking/filters.py b/geotrek/trekking/filters.py
index 3d10d261d2..bdf58e7d3a 100644
--- a/geotrek/trekking/filters.py
+++ b/geotrek/trekking/filters.py
@@ -1,4 +1,5 @@
from django.utils.translation import gettext_lazy as _
+from django_filters import ChoiceFilter
from geotrek.authent.filters import StructureRelatedFilterSet
from geotrek.core.filters import TopologyFilter, ValidTopologyFilterSet
from geotrek.altimetry.filters import AltimetryPointFilterSet, AltimetryAllGeometriesFilterSet
@@ -8,13 +9,19 @@
class TrekFilterSet(AltimetryAllGeometriesFilterSet, ValidTopologyFilterSet, ZoningFilterSet, StructureRelatedFilterSet):
+ provider = ChoiceFilter(
+ field_name='provider',
+ empty_label=_("Provider"),
+ label=_("Provider"),
+ choices=Trek.objects.provider_choices()
+ )
class Meta(StructureRelatedFilterSet.Meta):
model = Trek
fields = StructureRelatedFilterSet.Meta.fields + [
'published', 'difficulty', 'duration', 'themes', 'networks',
'practice', 'accessibilities', 'accessibility_level', 'route', 'labels',
- 'source', 'portal', 'reservation_system',
+ 'source', 'portal', 'reservation_system', 'provider'
]
@@ -24,15 +31,28 @@ class POITrekFilter(TopologyFilter):
class POIFilterSet(AltimetryPointFilterSet, ValidTopologyFilterSet, ZoningFilterSet, StructureRelatedFilterSet):
trek = POITrekFilter(label=_("Trek"), required=False)
+ provider = ChoiceFilter(
+ field_name='provider',
+ empty_label=_("Provider"),
+ label=_("Provider"),
+ choices=POI.objects.provider_choices()
+ )
class Meta(StructureRelatedFilterSet.Meta):
model = POI
fields = StructureRelatedFilterSet.Meta.fields + [
- 'published', 'type', 'trek',
+ 'published', 'type', 'trek', 'provider'
]
class ServiceFilterSet(AltimetryPointFilterSet, ValidTopologyFilterSet, ZoningFilterSet, StructureRelatedFilterSet):
- class Meta:
+ provider = ChoiceFilter(
+ field_name='provider',
+ empty_label=_("Provider"),
+ label=_("Provider"),
+ choices=Service.objects.provider_choices()
+ )
+
+ class Meta(StructureRelatedFilterSet.Meta):
model = Service
- fields = StructureRelatedFilterSet.Meta.fields + ['type']
+ fields = StructureRelatedFilterSet.Meta.fields + ['type', 'provider']
diff --git a/geotrek/trekking/locale/fr/LC_MESSAGES/django.po b/geotrek/trekking/locale/fr/LC_MESSAGES/django.po
index 3af3fe60e1..f4b21037ce 100644
--- a/geotrek/trekking/locale/fr/LC_MESSAGES/django.po
+++ b/geotrek/trekking/locale/fr/LC_MESSAGES/django.po
@@ -147,6 +147,9 @@ msgstr "A bref résumé (info bulle sur la carte)"
msgid "Description"
msgstr "Description"
+msgid "Provider"
+msgstr "Fournisseur"
+
msgid "Complete description"
msgstr "Description complète"
diff --git a/geotrek/trekking/migrations/0042_auto_20220907_1253.py b/geotrek/trekking/migrations/0042_auto_20220907_1253.py
new file mode 100644
index 0000000000..88064e7a5a
--- /dev/null
+++ b/geotrek/trekking/migrations/0042_auto_20220907_1253.py
@@ -0,0 +1,28 @@
+# Generated by Django 3.1.14 on 2022-09-07 12:53
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('trekking', '0041_auto_20220304_1442'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='poi',
+ name='provider',
+ field=models.CharField(blank=True, db_index=True, max_length=1024, verbose_name='Provider'),
+ ),
+ migrations.AddField(
+ model_name='service',
+ name='provider',
+ field=models.CharField(blank=True, db_index=True, max_length=1024, verbose_name='Provider'),
+ ),
+ migrations.AddField(
+ model_name='trek',
+ name='provider',
+ field=models.CharField(blank=True, db_index=True, max_length=1024, verbose_name='Provider'),
+ ),
+ ]
diff --git a/geotrek/trekking/models.py b/geotrek/trekking/models.py
index 02bbba6c38..8d6f06a1d8 100755
--- a/geotrek/trekking/models.py
+++ b/geotrek/trekking/models.py
@@ -17,7 +17,7 @@
from mapentity.serializers import plain_text
from geotrek.authent.models import StructureRelated
-from geotrek.core.models import Path, Topology, simplify_coords
+from geotrek.core.models import Path, Topology, TopologyManager, simplify_coords
from geotrek.common.utils import intersecting, classproperty
from geotrek.common.mixins.models import PicturesMixin, PublishableMixin, PictogramMixin, OptionalPictogramMixin
from geotrek.common.mixins.managers import NoDeleteManager
@@ -111,6 +111,13 @@ class Meta:
ordering = ('order', 'name')
+class TrekManager(TopologyManager):
+ def provider_choices(self):
+ providers = self.get_queryset().existing().order_by('provider').distinct('provider') \
+ .exclude(provider__exact='').values_list('provider', 'provider')
+ return providers
+
+
class Trek(Topology, StructureRelated, PicturesMixin, PublishableMixin, MapEntityMixin):
topo_object = models.OneToOneField(Topology, parent_link=True, on_delete=models.CASCADE)
departure = models.CharField(verbose_name=_("Departure"), max_length=128, blank=True,
@@ -202,6 +209,7 @@ class Trek(Topology, StructureRelated, PicturesMixin, PublishableMixin, MapEntit
verbose_name=_("Labels"),
blank=True)
eid = models.CharField(verbose_name=_("External id"), max_length=1024, blank=True, null=True)
+ provider = models.CharField(verbose_name=_("Provider"), db_index=True, max_length=1024, blank=True)
eid2 = models.CharField(verbose_name=_("Second external id"), max_length=1024, blank=True, null=True)
pois_excluded = models.ManyToManyField('Poi', related_name='excluded_treks', verbose_name=_("Excluded POIs"),
blank=True)
@@ -214,6 +222,7 @@ class Trek(Topology, StructureRelated, PicturesMixin, PublishableMixin, MapEntit
capture_map_image_waitfor = '.poi_enum_loaded.services_loaded.info_desks_loaded.ref_points_loaded'
geometry_types_allowed = ["LINESTRING"]
+ objects = TrekManager()
class Meta:
verbose_name = _("Trek")
@@ -717,11 +726,19 @@ def __str__(self):
return "%s" % self.label
+class POIManager(NoDeleteManager):
+ def provider_choices(self):
+ providers = self.get_queryset().existing().exclude(provider__exact='') \
+ .distinct('provider').values_list('provider', 'provider')
+ return providers
+
+
class POI(StructureRelated, PicturesMixin, PublishableMixin, MapEntityMixin, Topology):
topo_object = models.OneToOneField(Topology, parent_link=True, on_delete=models.CASCADE)
description = models.TextField(verbose_name=_("Description"), blank=True, help_text=_("History, details, ..."))
type = models.ForeignKey('POIType', related_name='pois', verbose_name=_("Type"), on_delete=models.CASCADE)
eid = models.CharField(verbose_name=_("External id"), max_length=1024, blank=True, null=True)
+ provider = models.CharField(verbose_name=_("Provider"), db_index=True, max_length=1024, blank=True)
geometry_types_allowed = ["POINT"]
@@ -730,7 +747,7 @@ class Meta:
verbose_name_plural = _("POI")
# Override default manager
- objects = NoDeleteManager()
+ objects = POIManager()
# Do no check structure when selecting POIs to exclude
check_structure_in_forms = False
@@ -847,12 +864,18 @@ class ServiceManager(NoDeleteManager):
def get_queryset(self):
return super().get_queryset().select_related('type')
+ def provider_choices(self):
+ providers = self.get_queryset().existing().exclude(provider__exact='') \
+ .distinct('provider').values_list('provider', 'provider')
+ return providers
+
class Service(StructureRelated, MapEntityMixin, Topology):
topo_object = models.OneToOneField(Topology, parent_link=True,
on_delete=models.CASCADE)
type = models.ForeignKey('ServiceType', related_name='services', verbose_name=_("Type"), on_delete=models.CASCADE)
eid = models.CharField(verbose_name=_("External id"), max_length=1024, blank=True, null=True)
+ provider = models.CharField(verbose_name=_("Provider"), db_index=True, max_length=1024, blank=True)
class Meta:
verbose_name = _("Service")
diff --git a/geotrek/trekking/parsers.py b/geotrek/trekking/parsers.py
index f466fcc345..8448182a32 100644
--- a/geotrek/trekking/parsers.py
+++ b/geotrek/trekking/parsers.py
@@ -1,8 +1,10 @@
-from django.contrib.gis.geos import Point
+import json
+from django.conf import settings
+from django.contrib.gis.geos import Point, GEOSGeometry
from django.utils.translation import gettext as _
-from geotrek.common.parsers import ShapeParser, AttachmentParserMixin
-from geotrek.trekking.models import Trek
+from geotrek.common.parsers import ShapeParser, AttachmentParserMixin, GeotrekParser
+from geotrek.trekking.models import OrderedTrekChild, POI, Service, Trek
class DurationParserMixin:
@@ -60,3 +62,149 @@ def filter_geom(self, src, val):
self.add_warning(_("Invalid geometry type for field '{src}'. Should be LineString, not {geom_type}").format(src=src, geom_type=val.geom_type))
return None
return val
+
+
+class GeotrekTrekParser(GeotrekParser):
+ """Geotrek parser for Trek"""
+
+ url = None
+ model = Trek
+ constant_fields = {
+ 'published': True,
+ 'deleted': False,
+ }
+ replace_fields = {
+ "eid": "uuid",
+ "eid2": "second_external_id",
+ "geom": "geometry"
+ }
+ url_categories = {
+ "difficulty": "trek_difficulty",
+ "route": "trek_route",
+ "themes": "theme",
+ "practice": "trek_practice",
+ "accessibilities": "trek_accessibility",
+ "networks": "trek_network",
+ 'labels': 'label'
+ }
+ categories_keys_api_v2 = {
+ 'difficulty': 'label',
+ 'route': 'route',
+ 'themes': 'label',
+ 'practice': 'name',
+ 'accessibilities': 'name',
+ 'networks': 'label',
+ 'labels': 'name'
+ }
+ natural_keys = {
+ 'difficulty': 'difficulty',
+ 'route': 'route',
+ 'themes': 'label',
+ 'practice': 'name',
+ 'accessibilities': 'name',
+ 'networks': 'network',
+ 'labels': 'name'
+ }
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.next_url = f"{self.url}/api/v2/trek"
+
+ def filter_parking_location(self, src, val):
+ if val:
+ return Point(val[0], val[1], srid=settings.API_SRID)
+
+ def filter_points_reference(self, src, val):
+ if val:
+ geom = GEOSGeometry(json.dumps(val))
+ return geom.transform(settings.SRID, clone=True)
+
+ def end(self):
+ """Add children after all treks imported are created in database."""
+ super().end()
+ self.next_url = f"{self.url}/api/v2/tour"
+ try:
+ params = {
+ 'in_bbox': ','.join([str(coord) for coord in self.bbox.extent]),
+ 'fields': 'steps,uuid'
+ }
+ response = self.request_or_retry(f"{self.next_url}", params=params)
+ results = response.json()['results']
+ final_children = {}
+ for result in results:
+ final_children[result['uuid']] = [step['uuid'] for step in result['steps']]
+
+ for key, value in final_children.items():
+ if value:
+ trek_parent_instance = Trek.objects.filter(eid=key)
+ if not trek_parent_instance:
+ self.add_warning(_(f"Trying to retrieve children for missing trek : could not find trek with UUID {key}"))
+ return
+ order = 0
+ for child in value:
+ try:
+ trek_child_instance = Trek.objects.get(eid=child)
+ except Trek.DoesNotExist:
+ self.add_warning(_(f"One trek has not be generated for {trek_parent_instance[0].name} : could not find trek with UUID {child}"))
+ continue
+ OrderedTrekChild.objects.get_or_create(parent=trek_parent_instance[0],
+ child=trek_child_instance,
+ order=order)
+ order += 1
+ except Exception as e:
+ self.add_warning(_(f"An error occured in children generation : {getattr(e, 'message', repr(e))}"))
+
+
+class GeotrekServiceParser(GeotrekParser):
+ """Geotrek parser for Service"""
+
+ url = None
+ model = Service
+ constant_fields = {
+ 'deleted': False,
+ }
+ replace_fields = {
+ "eid": "uuid",
+ "geom": "geometry"
+ }
+ url_categories = {
+ "type": "service_type",
+ }
+ categories_keys_api_v2 = {
+ 'type': 'name',
+ }
+ natural_keys = {
+ 'type': 'name'
+ }
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.next_url = f"{self.url}/api/v2/service"
+
+
+class GeotrekPOIParser(GeotrekParser):
+ """Geotrek parser for GeotrekPOI"""
+
+ url = None
+ model = POI
+ constant_fields = {
+ 'published': True,
+ 'deleted': False,
+ }
+ replace_fields = {
+ "eid": "uuid",
+ "geom": "geometry"
+ }
+ url_categories = {
+ "type": "poi_type",
+ }
+ categories_keys_api_v2 = {
+ 'type': 'label',
+ }
+ natural_keys = {
+ 'type': 'label',
+ }
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.next_url = f"{self.url}/api/v2/poi"
diff --git a/geotrek/trekking/serializers.py b/geotrek/trekking/serializers.py
index aa7529d25f..b6b0c9e4e4 100644
--- a/geotrek/trekking/serializers.py
+++ b/geotrek/trekking/serializers.py
@@ -363,7 +363,7 @@ class POIAPISerializer(PublishableSerializerMixin, PicturesSerializerMixin, Zoni
structure = StructureSerializer()
class Meta:
- model = trekking_models.Trek
+ model = trekking_models.POI
id_field = 'id' # By default on this model it's topo_object = OneToOneField(parent_link=True)
fields = (
'id', 'description', 'type', 'min_elevation', 'max_elevation', 'structure'
diff --git a/geotrek/trekking/templates/trekking/poi_detail_attributes.html b/geotrek/trekking/templates/trekking/poi_detail_attributes.html
index 42226cbc2e..048af1f957 100644
--- a/geotrek/trekking/templates/trekking/poi_detail_attributes.html
+++ b/geotrek/trekking/templates/trekking/poi_detail_attributes.html
@@ -28,7 +28,14 @@ {% trans "Attributes" %}
{% trans "External id" %} |
- {{ poi.eid }} |
+ {% if poi.eid %}{{ poi.eid|safe }}
+ {% else %}{% trans "None" %}{% endif %}
+ |
+
+ {% trans "Provider" %} |
+ {% if poi.provider %}{{ poi.provider|safe }}
+ {% else %}{% trans "None" %}{% endif %}
+ |
{% include "altimetry/elevationinfo_fragment.html" %}
diff --git a/geotrek/trekking/templates/trekking/service_detail_attributes.html b/geotrek/trekking/templates/trekking/service_detail_attributes.html
index 3ade42bb24..fb0a448f12 100644
--- a/geotrek/trekking/templates/trekking/service_detail_attributes.html
+++ b/geotrek/trekking/templates/trekking/service_detail_attributes.html
@@ -15,9 +15,16 @@ {% trans "Attributes" %}
{% trans "External id" %} |
- {{ service.eid }} |
+ {% if service.eid %}{{ service.eid|safe }}
+ {% else %}{% trans "None" %}{% endif %}
+ |
+
+
+ {% trans "Provider" %} |
+ {% if service.provider %}{{ service.provider|safe }}
+ {% else %}{% trans "None" %}{% endif %}
+ |
-
{% include "altimetry/elevationinfo_fragment.html" %}
{% include "mapentity/trackinfo_fragment.html" %}
diff --git a/geotrek/trekking/templates/trekking/sql/post_90_defaults.sql b/geotrek/trekking/templates/trekking/sql/post_90_defaults.sql
index c34f394a36..971c231b76 100644
--- a/geotrek/trekking/templates/trekking/sql/post_90_defaults.sql
+++ b/geotrek/trekking/templates/trekking/sql/post_90_defaults.sql
@@ -61,6 +61,7 @@ ALTER TABLE trekking_trek ALTER COLUMN accessibility_infrastructure SET DEFAULT
ALTER TABLE trekking_trek ALTER COLUMN accessibility_signage SET DEFAULT '';
ALTER TABLE trekking_trek ALTER COLUMN accessibility_slope SET DEFAULT '';
ALTER TABLE trekking_trek ALTER COLUMN accessibility_width SET DEFAULT '';
+ALTER TABLE trekking_trek ALTER COLUMN provider SET DEFAULT '';
-- route
-- difficulty
-- web_links
@@ -147,6 +148,8 @@ ALTER TABLE trekking_poi ALTER COLUMN description SET DEFAULT '';
-- name
ALTER TABLE trekking_poi ALTER COLUMN review SET DEFAULT FALSE;
ALTER TABLE trekking_poi ALTER COLUMN published SET DEFAULT FALSE;
+ALTER TABLE trekking_poi ALTER COLUMN provider SET DEFAULT '';
+
-- publication_date
@@ -173,3 +176,4 @@ ALTER TABLE trekking_poi ALTER COLUMN published SET DEFAULT FALSE;
-- type
--eid
--structure
+ALTER TABLE trekking_service ALTER COLUMN provider SET DEFAULT '';
diff --git a/geotrek/trekking/templates/trekking/trek_detail_attributes.html b/geotrek/trekking/templates/trekking/trek_detail_attributes.html
index da3b6817c8..56e59853f1 100644
--- a/geotrek/trekking/templates/trekking/trek_detail_attributes.html
+++ b/geotrek/trekking/templates/trekking/trek_detail_attributes.html
@@ -180,6 +180,12 @@ {% trans "Attributes" %}
{% else %}{% trans "None" %}{% endif %}
+
+ {% trans "Provider" %} |
+ {% if trek.provider %}{{ trek.provider|safe }}
+ {% else %}{% trans "None" %}{% endif %}
+ |
+
{% trans "Second external id" %} |
{% if trek.eid2 %}{{ trek.eid2|safe }}
diff --git a/geotrek/trekking/tests/data/geotrek_parser_v2/poi.json b/geotrek/trekking/tests/data/geotrek_parser_v2/poi.json
new file mode 100644
index 0000000000..798a3999e2
--- /dev/null
+++ b/geotrek/trekking/tests/data/geotrek_parser_v2/poi.json
@@ -0,0 +1,139 @@
+{
+ "count": 2,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "id": 2867,
+ "description": {
+ "fr": " Le pic des Trois Seigneurs est situé au point de rencontre des trois vallées de la Courbière, du Vicdessos et de l'Arac (Couserans). \r\nModeste sommet bien individualisé, il offre néanmoins un panorama exceptionnel sur les montagnes ariégeoises. ",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "external_id": null,
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 1.4397635,
+ 42.8303552,
+ 2166.0
+ ]
+ },
+ "name": {
+ "fr": "Pic des Trois Seigneurs",
+ "en": "Peak of the Three Lords",
+ "es": "",
+ "it": "Picco dei Tre Signori"
+ },
+ "attachments": [
+ {
+ "backend": "",
+ "type": "file",
+ "author": "gutard",
+ "license": null,
+ "thumbnail": "",
+ "legend": "",
+ "title": "",
+ "url": "https://foo.fr/media/paperclip/trekking_poi/2867/dep-a5-chartreuse-bat1.pdf",
+ "uuid": "004c3484-d018-4706-996a-fba6e89abd96"
+ },
+ {
+ "backend": "Attachment",
+ "type": "video",
+ "author": "",
+ "license": null,
+ "thumbnail": "",
+ "legend": "",
+ "title": "",
+ "url": "https://soundcloud.com/user-14604138/chemin-de-memoires-annie-carriere",
+ "uuid": "fd30f548-d870-402b-bbaf-31f895416662"
+ }
+ ],
+ "published": {
+ "fr": true,
+ "en": false,
+ "es": false,
+ "it": false
+ },
+ "type": 7,
+ "type_label": {
+ "fr": "Sommet",
+ "en": "Peak",
+ "es": null,
+ "it": null
+ },
+ "type_pictogram": "https://foo.fr/media/upload/poi-peak.png",
+ "url": "https://foo.fr/api/v2/poi/2867/",
+ "uuid": "d4f534b5-74af-40a0-9c6c-825c385b4e93",
+ "create_datetime": "2013-12-19T10:53:07.061717+01:00",
+ "update_datetime": "2020-11-24T10:52:46.696698+01:00"
+ },
+ {
+ "id": 2869,
+ "description": {
+ "fr": "Ce lac très accessible est un site prisé par les pêcheurs. On y observe truites fario, saumons des fontaines, ombles chevaliers, cristivomers et vairons. ",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "external_id": null,
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 1.4358006,
+ 42.8201023,
+ 1750.0
+ ]
+ },
+ "name": {
+ "fr": "Étang d'Arbu",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "attachments": [
+ {
+ "backend": "",
+ "type": "image",
+ "author": "ads",
+ "license": null,
+ "thumbnail": "https://foo.fr/media/paperclip/trekking_poi/2869/sensitivearea.png.400x0_q85.png",
+ "legend": "",
+ "title": "sensitivearea",
+ "url": "https://foo.fr/media/paperclip/trekking_poi/2869/sensitivearea.png",
+ "uuid": "542afd02-487a-4df7-afaf-2082b24e8b6e"
+ },
+ {
+ "backend": "Attachment",
+ "type": "video",
+ "author": "",
+ "license": null,
+ "thumbnail": "",
+ "legend": "",
+ "title": "",
+ "url": "https://www.youtube.com/watch?v=CdWChQqZMZc",
+ "uuid": "0d0542b7-b6e9-4fbd-b7c2-1438de41dfb1"
+ }
+ ],
+ "published": {
+ "fr": true,
+ "en": false,
+ "es": false,
+ "it": false
+ },
+ "type": 6,
+ "type_label": {
+ "fr": "Lac",
+ "en": "Lake",
+ "es": null,
+ "it": null
+ },
+ "type_pictogram": "https://foo.fr/media/upload/poi-lake.png",
+ "url": "https://foo.fr/api/v2/poi/2869/",
+ "uuid": "3a48fd35-654a-442d-be37-cc4c43f620bb",
+ "create_datetime": "2013-12-19T10:59:43.060709+01:00",
+ "update_datetime": "2019-04-25T11:05:26.169812+02:00"
+ }
+ ]
+}
diff --git a/geotrek/trekking/tests/data/geotrek_parser_v2/poi_ids.json b/geotrek/trekking/tests/data/geotrek_parser_v2/poi_ids.json
new file mode 100644
index 0000000000..20ee702b48
--- /dev/null
+++ b/geotrek/trekking/tests/data/geotrek_parser_v2/poi_ids.json
@@ -0,0 +1,13 @@
+{
+ "count": 2,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "uuid": "fd30f548-d870-402b-bbaf-31f895416662"
+ },
+ {
+ "uuid": "542afd02-487a-4df7-afaf-2082b24e8b6e"
+ }
+ ]
+}
diff --git a/geotrek/trekking/tests/data/geotrek_parser_v2/poi_type.json b/geotrek/trekking/tests/data/geotrek_parser_v2/poi_type.json
new file mode 100644
index 0000000000..b70131103a
--- /dev/null
+++ b/geotrek/trekking/tests/data/geotrek_parser_v2/poi_type.json
@@ -0,0 +1,167 @@
+{
+ "count": 16,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "id": 9,
+ "label": {
+ "fr": "Archéologie",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "pictogram": "https://foo.fr/media/upload/poi-archeology.png"
+ },
+ {
+ "id": 11,
+ "label": {
+ "fr": "Architecture",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "pictogram": "https://foo.fr/media/upload/poi-architecture.png"
+ },
+ {
+ "id": 16,
+ "label": {
+ "fr": "Barre attache",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "pictogram": "https://foo.fr/media/upload/POIE_point_attache.png"
+ },
+ {
+ "id": 15,
+ "label": {
+ "fr": "Col",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "pictogram": "https://foo.fr/media/upload/poi-pass.png"
+ },
+ {
+ "id": 3,
+ "label": {
+ "fr": "Faune",
+ "en": "Fauna",
+ "es": null,
+ "it": null
+ },
+ "pictogram": "https://foo.fr/media/upload/poi-fauna.png"
+ },
+ {
+ "id": 2,
+ "label": {
+ "fr": "Flore",
+ "en": "Flora",
+ "es": null,
+ "it": null
+ },
+ "pictogram": "https://foo.fr/media/upload/poi-flora.png"
+ },
+ {
+ "id": 8,
+ "label": {
+ "fr": "Géologie",
+ "en": "Geology",
+ "es": null,
+ "it": null
+ },
+ "pictogram": "https://foo.fr/media/upload/poi-geology.png"
+ },
+ {
+ "id": 12,
+ "label": {
+ "fr": "Glacier",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "pictogram": "https://foo.fr/media/upload/poi-glacier.png"
+ },
+ {
+ "id": 10,
+ "label": {
+ "fr": "Histoire",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "pictogram": "https://foo.fr/media/upload/poi-history.png"
+ },
+ {
+ "id": 6,
+ "label": {
+ "fr": "Lac",
+ "en": "Lake",
+ "es": null,
+ "it": null
+ },
+ "pictogram": "https://foo.fr/media/upload/poi-lake.png"
+ },
+ {
+ "id": 14,
+ "label": {
+ "fr": "Pastoralisme",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "pictogram": "https://foo.fr/media/upload/poi-pastoral.png"
+ },
+ {
+ "id": 1,
+ "label": {
+ "fr": "Petit patrimoine",
+ "en": "Small patrimony",
+ "es": null,
+ "it": null
+ },
+ "pictogram": "https://foo.fr/media/upload/poi-patrimony.png"
+ },
+ {
+ "id": 4,
+ "label": {
+ "fr": "Point de vue",
+ "en": "Panorama",
+ "es": null,
+ "it": null
+ },
+ "pictogram": "https://foo.fr/media/upload/poi-panorama.png"
+ },
+ {
+ "id": 5,
+ "label": {
+ "fr": "Refuge",
+ "en": "Refuge",
+ "es": null,
+ "it": null
+ },
+ "pictogram": "https://foo.fr/media/upload/poi-refuge.png"
+ },
+ {
+ "id": 13,
+ "label": {
+ "fr": "Savoir-faire",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "pictogram": "https://foo.fr/media/upload/poi-knowhow.png"
+ },
+ {
+ "id": 7,
+ "label": {
+ "fr": "Sommet",
+ "en": "Peak",
+ "es": null,
+ "it": null
+ },
+ "pictogram": "https://foo.fr/media/upload/poi-peak.png"
+ }
+ ]
+}
diff --git a/geotrek/trekking/tests/data/geotrek_parser_v2/service.json b/geotrek/trekking/tests/data/geotrek_parser_v2/service.json
new file mode 100644
index 0000000000..94210bcc29
--- /dev/null
+++ b/geotrek/trekking/tests/data/geotrek_parser_v2/service.json
@@ -0,0 +1,37 @@
+{
+ "count": 2,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "id": 4886,
+ "eid": null,
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 1.4375926,
+ 42.8190752,
+ 1725.0
+ ]
+ },
+ "structure": "MC",
+ "type": 1,
+ "uuid": "1c67978b-9833-4f31-a384-c76a05eb2fbf"
+ },
+ {
+ "id": 10393,
+ "eid": null,
+ "geometry": {
+ "type": "Point",
+ "coordinates": [
+ 1.3985067,
+ 42.7560827,
+ 2100.0
+ ]
+ },
+ "structure": "CCCCC",
+ "type": 1,
+ "uuid": "d2acb966-d01c-4eb1-81f9-a9f490c8484a"
+ }
+ ]
+}
diff --git a/geotrek/trekking/tests/data/geotrek_parser_v2/service_ids.json b/geotrek/trekking/tests/data/geotrek_parser_v2/service_ids.json
new file mode 100644
index 0000000000..a0f136a53b
--- /dev/null
+++ b/geotrek/trekking/tests/data/geotrek_parser_v2/service_ids.json
@@ -0,0 +1,13 @@
+{
+ "count": 2,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "uuid": "1c67978b-9833-4f31-a384-c76a05eb2fbf"
+ },
+ {
+ "uuid": "d2acb966-d01c-4eb1-81f9-a9f490c8484a"
+ }
+ ]
+}
diff --git a/geotrek/trekking/tests/data/geotrek_parser_v2/service_type.json b/geotrek/trekking/tests/data/geotrek_parser_v2/service_type.json
new file mode 100644
index 0000000000..4a5d79616b
--- /dev/null
+++ b/geotrek/trekking/tests/data/geotrek_parser_v2/service_type.json
@@ -0,0 +1,39 @@
+{
+ "count": 2,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "id": 1,
+ "name": {
+ "fr": "Eau potable",
+ "en": "Drinking water",
+ "es": null,
+ "it": null
+ },
+ "practices": [
+ 4,
+ 2,
+ 1,
+ 3
+ ],
+ "pictogram": "https://foo.fr/media/upload/drinking_water.svg"
+ },
+ {
+ "id": 2,
+ "name": {
+ "fr": "Descente pentue",
+ "en": "Steep descent",
+ "es": null,
+ "it": null
+ },
+ "practices": [
+ 4,
+ 2,
+ 1,
+ 3
+ ],
+ "pictogram": "https://foo.fr/media/upload/steep_descent.svg"
+ }
+ ]
+}
diff --git a/geotrek/trekking/tests/data/geotrek_parser_v2/trek.json b/geotrek/trekking/tests/data/geotrek_parser_v2/trek.json
new file mode 100644
index 0000000000..daa3f5e8f9
--- /dev/null
+++ b/geotrek/trekking/tests/data/geotrek_parser_v2/trek.json
@@ -0,0 +1,1257 @@
+{
+ "count": 5,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "id": 2,
+ "access": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "accessibilities": [
+ 1
+ ],
+ "accessibility_advice": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "accessibility_covering": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "accessibility_exposure": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "accessibility_level": null,
+ "accessibility_signage": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "accessibility_slope": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "accessibility_width": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "advice": {
+ "fr": "Attention en cas d'orage. Fortement déconseillé par mauvais temps! ",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "advised_parking": {
+ "fr": "Avant le Port de Lers",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "altimetric_profile": "https://foo.fr/api/v2/trek/2/profile/",
+ "ambiance": {
+ "fr": "Le nom de ce pic est issu de la légende selon laquelle les trois seigneurs des vallées de Massat, Vicdessos et Rabat-les-Trois-Seigneurs, se rencontraient sur la dalle plate en son sommet afin de débattre des droits des différentes vallées qu'ils administraient. \r\n\r\nÀ partir du xviie siècle, de grandes caravanes d'ânes et de mulets transportaient le charbon de bois entre les forêts du Couserans et les forges à la catalane de la vallée de Rabat via le col de la Pourtanelle sur l'épaulement nord du pic. \r\n\r\nAu xixe siècle, des porteurs de glace venaient y chercher leur butin sur le flanc nord du pic au glacier d'Ambans pour le transporter ensuite vers Toulouse. Ce glacier a totalement disparu au début du xxie siècle. \r\n\r\n\r\nSource Wikipedia \r\n",
+ "en": "Ambiance en",
+ "es": "",
+ "it": ""
+ },
+ "arrival": {
+ "fr": "Sur la route",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "ascent": 666,
+ "attachments": [
+ {
+ "backend": "",
+ "type": "image",
+ "author": "Grégory Tonon",
+ "license": "License",
+ "thumbnail": "https://foo.fr/media/paperclip/trekking_trek/2/1024px-les_pyrenees_depuis_le_pic_des_3_seigneurs.jpg.400x0_q85.jpg",
+ "legend": "Vue sur la chaîne des Pyrénées, depuis le pics des 3 Seigneurs",
+ "title": "1024px-Les_Pyrénées_depuis_le_pic_des_3_Seigneurs",
+ "url": "https://foo.fr/media/paperclip/trekking_trek/2/1024px-les_pyrenees_depuis_le_pic_des_3_seigneurs.jpg",
+ "uuid": "8c62ae5f-9533-4de6-a863-3e33cd42d16c"
+ },
+ {
+ "backend": "Attachment",
+ "type": "video",
+ "author": "Ariège Pyrénées",
+ "license": null,
+ "thumbnail": "",
+ "legend": "Grands site Occitanie - collection Ariège",
+ "title": "",
+ "url": "https://www.youtube.com/watch?v=O5fwnNceuks",
+ "uuid": "aeffbba4-821a-4f7b-af74-e47ed628679c"
+ },
+ {
+ "backend": "",
+ "type": "file",
+ "author": "",
+ "license": null,
+ "thumbnail": "",
+ "legend": "",
+ "title": "file_example_MP3_700KB",
+ "url": "https://foo.fr/media/paperclip/trekking_trek/2/file_example_mp3_700kb.mp3",
+ "uuid": "529e754b-e5a8-4fe7-944d-161b4900a62e"
+ }
+ ],
+ "attachments_accessibility": [],
+ "children": [
+ "c9567576-2934-43ab-979e-e13d02c671a9"
+ ],
+ "cities": [
+ "09231",
+ "09302"
+ ],
+ "create_datetime": "2013-12-12T16:20:01.405056Z",
+ "departure": {
+ "fr": "Port de Lers",
+ "en": "Lers bridge",
+ "es": "",
+ "it": ""
+ },
+ "departure_city": "09231",
+ "departure_geom": [
+ 1.411681003952174,
+ 42.80641572814877
+ ],
+ "descent": -777,
+ "description": {
+ "fr": "https://foo.fr/media/paperclip/trekking_trek/2/file_example_mp3_700kb.mp3",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "description_teaser": {
+ "fr": "Un très joli point de vue, en retrait de la chaîne des Pyrénées. ",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "difficulty": 1,
+ "disabled_infrastructure": {
+ "fr": "Accessibilité aménagement",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "duration": 24.0,
+ "elevation_area_url": "https://foo.fr/api/v2/trek/2/dem/",
+ "elevation_svg_url": "https://foo.fr/api/v2/trek/2/profile/?language=fr&format=svg",
+ "external_id": null,
+ "gear": {
+ "fr": "Il faut de la corde",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "geometry": {
+ "type": "LineString",
+ "coordinates": [
+ [
+ 1.411681,
+ 42.8064157,
+ 1516.0
+ ],
+ [
+ 1.4117038,
+ 42.8065905,
+ 1516.0
+ ]
+ ]
+ },
+ "gpx": "https://foo.fr/api/fr/treks/2/boucle-du-pic-des-trois-seigneurs.gpx",
+ "information_desks": [
+ 2,
+ 1,
+ 3
+ ],
+ "kml": "https://foo.fr/api/fr/treks/2/boucle-du-pic-des-trois-seigneurs.kml",
+ "labels": [
+ 1,
+ 2,
+ 3
+ ],
+ "length_2d": 7606.3,
+ "length_3d": 7811.9,
+ "max_elevation": 2042,
+ "min_elevation": 1358,
+ "name": {
+ "fr": "Boucle du Pic des Trois Seigneurs",
+ "en": "Loop of the pic of 3 lords",
+ "es": "",
+ "it": "Foo bar"
+ },
+ "networks": [
+ 2
+ ],
+ "next": {},
+ "parents": [],
+ "parking_location": [
+ 1.412816,
+ 42.8063269
+ ],
+ "pdf": {
+ "fr": "https://foo.fr/api/fr/treks/2/boucle-du-pic-des-trois-seigneurs.pdf",
+ "en": "https://foo.fr/api/en/treks/2/boucle-du-pic-des-trois-seigneurs.pdf",
+ "es": "https://foo.fr/api/es/treks/2/boucle-du-pic-des-trois-seigneurs.pdf",
+ "it": "https://foo.fr/api/it/treks/2/boucle-du-pic-des-trois-seigneurs.pdf"
+ },
+ "points_reference": {
+ "type": "MultiPoint",
+ "coordinates": [
+ [
+ 1.411417008494028,
+ 42.81070319713575
+ ],
+ [
+ 1.415279389475474,
+ 42.81693648180148
+ ],
+ [
+ 1.403348923777236,
+ 42.825246550724195
+ ],
+ [
+ 1.428325654123916,
+ 42.8286458024494
+ ],
+ [
+ 1.437852860544806,
+ 42.81611800550653
+ ],
+ [
+ 1.423759460449225,
+ 42.807239989743145
+ ]
+ ]
+ },
+ "portal": [],
+ "practice": 3,
+ "ratings": [],
+ "ratings_description": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "previous": {},
+ "public_transport": {
+ "fr": "Test",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "published": {
+ "fr": true,
+ "en": true,
+ "es": true,
+ "it": false
+ },
+ "reservation_system": null,
+ "reservation_id": "",
+ "route": 1,
+ "second_external_id": null,
+ "source": [],
+ "structure": 1,
+ "themes": [
+ 8,
+ 4
+ ],
+ "update_datetime": "2022-05-16T12:10:59.927409Z",
+ "url": "https://foo.fr/api/v2/trek/2/",
+ "uuid": "9e70b294-1134-4c50-9c56-d722720cacf1",
+ "web_links": [
+ {
+ "name": {
+ "fr": "Camping",
+ "en": "Camping",
+ "es": "Camping",
+ "it": null
+ },
+ "url": "http://camping.com/",
+ "category": {
+ "label": {
+ "fr": "A lire",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "id": 4,
+ "pictogram": "https://foo.fr/media/upload/weblink-book.png"
+ }
+ },
+ {
+ "name": {
+ "fr": "Makina Corpus",
+ "en": "Makina Corpus",
+ "es": "Makina Corpus",
+ "it": null
+ },
+ "url": "http://makina-corpus.com/",
+ "category": {
+ "label": {
+ "fr": "A lire",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "id": 4,
+ "pictogram": "https://foo.fr/media/upload/weblink-book.png"
+ }
+ },
+ {
+ "name": {
+ "fr": "Test",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "url": "http://toto.com",
+ "category": {
+ "label": {
+ "fr": "A lire",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "id": 4,
+ "pictogram": "https://foo.fr/media/upload/weblink-book.png"
+ }
+ }
+ ]
+ },
+ {
+ "id": 10443,
+ "access": {
+ "fr": "Bonjour",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "accessibilities": [],
+ "accessibility_advice": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "accessibility_covering": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "accessibility_exposure": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "accessibility_level": null,
+ "accessibility_signage": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "accessibility_slope": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "accessibility_width": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "advice": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "advised_parking": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "altimetric_profile": "https://foo.fr/api/v2/trek/10443/profile/",
+ "ambiance": {
+ "fr": "Test ambiance",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "arrival": {
+ "fr": "col de la Core",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "ascent": 722,
+ "attachments": [
+ {
+ "backend": "",
+ "type": "image",
+ "author": "Samrong01 - CC BY-SA 4.0",
+ "license": null,
+ "thumbnail": "https://foo.fr/media/paperclip/trekking_trek/10443/1083px-arrien-en-bethmale_general_view.JPG.400x0_q85.jpg",
+ "legend": "Arrien-en-Bethmale, vue du village",
+ "title": "",
+ "url": "https://foo.fr/media/paperclip/trekking_trek/10443/1083px-arrien-en-bethmale_general_view.JPG",
+ "uuid": "8605f89a-48fa-40ba-a9b0-9a48e5b0f310"
+ }
+ ],
+ "attachments_accessibility": [],
+ "children": [],
+ "cities": [
+ "09055",
+ "09291"
+ ],
+ "create_datetime": "2019-07-23T09:15:06.776780Z",
+ "departure": {
+ "fr": "Bethmale",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "departure_city": "09055",
+ "departure_geom": [
+ 1.0853185207621108,
+ 42.86403321156401
+ ],
+ "descent": -69,
+ "description": {
+ "fr": "La belle description de la crête.",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "description_teaser": {
+ "fr": "Au revoir",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "difficulty": 2,
+ "disabled_infrastructure": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "duration": 2.0,
+ "elevation_area_url": "https://foo.fr/api/v2/trek/10443/dem/",
+ "elevation_svg_url": "https://foo.fr/api/v2/trek/10443/profile/?language=fr&format=svg",
+ "external_id": null,
+ "gear": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "geometry": {
+ "type": "LineString",
+ "coordinates": [
+ [
+ 1.0853185,
+ 42.8640332,
+ 1057.0
+ ],
+ [
+ 1.0852891,
+ 42.8640109,
+ 1061.0
+ ]
+ ]
+ },
+ "gpx": "https://foo.fr/api/fr/treks/10443/de-bethmale-au-col-de-la-core.gpx",
+ "information_desks": [],
+ "kml": "https://foo.fr/api/fr/treks/10443/de-bethmale-au-col-de-la-core.kml",
+ "labels": [],
+ "length_2d": 3063.4,
+ "length_3d": 3245.4,
+ "max_elevation": 1711,
+ "min_elevation": 1057,
+ "name": {
+ "fr": "De Bethmale au col de la Core",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "networks": [],
+ "next": {
+ "10445": null
+ },
+ "parents": [
+ 10445
+ ],
+ "parking_location": null,
+ "pdf": {
+ "fr": "https://foo.fr/api/fr/treks/10443/de-bethmale-au-col-de-la-core.pdf",
+ "en": "https://foo.fr/api/en/treks/10443/de-bethmale-au-col-de-la-core.pdf",
+ "es": "https://foo.fr/api/es/treks/10443/de-bethmale-au-col-de-la-core.pdf",
+ "it": "https://foo.fr/api/it/treks/10443/de-bethmale-au-col-de-la-core.pdf"
+ },
+ "points_reference": null,
+ "portal": [],
+ "practice": 4,
+ "ratings": [],
+ "ratings_description": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "previous": {
+ "10445": 10441
+ },
+ "public_transport": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "published": {
+ "fr": true,
+ "en": false,
+ "es": false,
+ "it": false
+ },
+ "reservation_system": null,
+ "reservation_id": "",
+ "route": 3,
+ "second_external_id": null,
+ "source": [],
+ "structure": 3,
+ "themes": [],
+ "update_datetime": "2022-07-26T08:18:06.997582Z",
+ "url": "https://foo.fr/api/v2/trek/10443/",
+ "uuid": "1ba24605-aff2-4b16-bf30-6de1ebfb2a12",
+ "web_links": []
+ },
+ {
+ "id": 2849,
+ "access": {
+ "fr": "Depuis le village d'Aulus-les-Bains. ",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "accessibilities": [],
+ "accessibility_advice": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "accessibility_covering": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "accessibility_exposure": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "accessibility_level": null,
+ "accessibility_signage": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "accessibility_slope": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "accessibility_width": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "advice": {
+ "fr": "Sentier parfois escarpé. Camping interdit. Feu interdit.",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "advised_parking": {
+ "fr": "Dans le bas du village",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "altimetric_profile": "https://foo.fr/api/v2/trek/2849/profile/",
+ "ambiance": {
+ "fr": "Une des plus belles cascades des Pyrénées pour toute la famille ! C'est une cascade naturelle des Pyrénées située dans le Couserans en Ariège, à 1 380 m d'altitude. C'est une des plus belles et imposantes des Pyrénées. Le comte Henry Russell, célèbre pyrénéistes du XIXe siècle, la plaçait en tête de toutes ses rivales. ",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "arrival": {
+ "fr": "Aulus-les-Bains (sud)",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "ascent": 1130,
+ "attachments": [
+ {
+ "backend": "",
+ "type": "image",
+ "author": "Pierre Gouget",
+ "license": null,
+ "thumbnail": "https://foo.fr/media/paperclip/trekking_trek/2849/cascade_dars_a_aulus-les-bains-cc-by-sa-pierre-gouget.jpg.400x0_q85.jpg",
+ "legend": "Cascade d'Ars au mois de Mai (CC-By-SA)",
+ "title": "",
+ "url": "https://foo.fr/media/paperclip/trekking_trek/2849/cascade_dars_a_aulus-les-bains-cc-by-sa-pierre-gouget.jpg",
+ "uuid": "7dfdc2cd-69f8-4ed6-9ed1-eab178390aca"
+ },
+ {
+ "backend": "Attachment",
+ "type": "video",
+ "author": "Ariège Pyrénées",
+ "license": null,
+ "thumbnail": "",
+ "legend": "",
+ "title": "",
+ "url": "https://www.youtube.com/watch?v=O5fwnNceuks",
+ "uuid": "9fd25104-f758-4d8a-a7fa-241bc1b598dc"
+ },
+ {
+ "backend": "",
+ "type": "image",
+ "author": "Tongatahu",
+ "license": null,
+ "thumbnail": "",
+ "legend": "Panneau (CC-By-SA)",
+ "title": "",
+ "url": "https://upload.wikimedia.org/wikipedia/commons/e/e3/Aulus-les-Bains_Panneau_d%27information_sur_le_chemin_de_cascade_d%27_ars.jpg",
+ "uuid": "ca7e611f-6656-42db-85f4-01f51131a5cf"
+ },
+ {
+ "backend": "",
+ "type": "file",
+ "author": "Département d'Ariège",
+ "license": null,
+ "thumbnail": "",
+ "legend": "Activités pédestres",
+ "title": "activite_Pedestre",
+ "url": "https://foo.fr/media/paperclip/trekking_trek/2849/activite_pedestre.PDF",
+ "uuid": "d150f3f0-0c96-4f6d-8a54-3bbbe0dbe9d5"
+ },
+ {
+ "backend": "",
+ "type": "image",
+ "author": "Mathieu MD",
+ "license": null,
+ "thumbnail": "https://foo.fr/media/paperclip/trekking_trek/2849/14212843221_3cfb859ec4_o.jpg.400x0_q85.jpg",
+ "legend": "Cascade d'Ars - CC-By-SA",
+ "title": "14212843221_3cfb859ec4_o",
+ "url": "https://foo.fr/media/paperclip/trekking_trek/2849/14212843221_3cfb859ec4_o.jpg",
+ "uuid": "5ebbb31a-6a85-4377-9144-11130788a930"
+ }
+ ],
+ "attachments_accessibility": [],
+ "children": [],
+ "cities": [
+ "09029"
+ ],
+ "create_datetime": "2013-12-13T10:51:27.121271Z",
+ "departure": {
+ "fr": "Aulus-les-Bains",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "departure_city": "09029",
+ "departure_geom": [
+ 1.342102928593241,
+ 42.78978331370928
+ ],
+ "descent": -1033,
+ "description": {
+ "fr": "On accède à la cascade en 90 mn de marche à partir de la D 8, au départ d'Aulus-les-Bains vers le col de Latrappe. Au premier virage en épingle (1) une piste forestière s'élève entre forêt et pâturage. On franchit ensuite la passerelle de l'Artigou (1 060 m - 2) puis on longe la rive droite de l'Ars jusqu'au pied de la chute d'eau (3). ",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "description_teaser": {
+ "fr": "Haute d'environ 246m, elle est a son apogée à la fonte des neiges. ",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "difficulty": 1,
+ "disabled_infrastructure": {
+ "fr": "Aucune infrastructure spécifique ",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "duration": 4.0,
+ "elevation_area_url": "https://foo.fr/api/v2/trek/2849/dem/",
+ "elevation_svg_url": "https://foo.fr/api/v2/trek/2849/profile/?language=fr&format=svg",
+ "external_id": null,
+ "gear": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "geometry": {
+ "type": "LineString",
+ "coordinates": [
+ [
+ 1.3421029,
+ 42.7897833,
+ 760.0
+ ],
+ [
+ 1.3422439,
+ 42.7896888,
+ 760.0
+ ]
+ ]
+ },
+ "gpx": "https://foo.fr/api/fr/treks/2849/decouverte-de-la-cascade-dars.gpx",
+ "information_desks": [],
+ "kml": "https://foo.fr/api/fr/treks/2849/decouverte-de-la-cascade-dars.kml",
+ "labels": [
+ 1
+ ],
+ "length_2d": 12218.2,
+ "length_3d": 12531.1,
+ "max_elevation": 1607,
+ "min_elevation": 760,
+ "name": {
+ "fr": "Découverte de la Cascade d'Ars",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "networks": [
+ 2
+ ],
+ "next": {},
+ "parents": [
+ 4904
+ ],
+ "parking_location": [
+ 1.3386154,
+ 42.790173
+ ],
+ "pdf": {
+ "fr": "https://foo.fr/api/fr/treks/2849/decouverte-de-la-cascade-dars.pdf",
+ "en": "https://foo.fr/api/en/treks/2849/decouverte-de-la-cascade-dars.pdf",
+ "es": "https://foo.fr/api/es/treks/2849/decouverte-de-la-cascade-dars.pdf",
+ "it": "https://foo.fr/api/it/treks/2849/decouverte-de-la-cascade-dars.pdf"
+ },
+ "points_reference": {
+ "type": "MultiPoint",
+ "coordinates": [
+ [
+ 1.352026462554938,
+ 42.78377952689919
+ ],
+ [
+ 1.349644660949709,
+ 42.77618839945826
+ ],
+ [
+ 1.351146697998048,
+ 42.76437462843613
+ ]
+ ]
+ },
+ "portal": [],
+ "practice": 4,
+ "ratings": [],
+ "ratings_description": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "previous": {},
+ "public_transport": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "published": {
+ "fr": true,
+ "en": true,
+ "es": false,
+ "it": false
+ },
+ "reservation_system": null,
+ "reservation_id": "",
+ "route": 2,
+ "second_external_id": null,
+ "source": [],
+ "structure": 1,
+ "themes": [
+ 7
+ ],
+ "update_datetime": "2022-05-18T10:32:44.170710Z",
+ "url": "https://foo.fr/api/v2/trek/2849/",
+ "uuid": "6761143f-9244-41d0-b1af-21114408f769",
+ "web_links": []
+ },
+ {
+ "id": 10439,
+ "access": {
+ "fr": "Bonnac Irazein",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "accessibilities": [],
+ "accessibility_advice": {
+ "fr": "",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "accessibility_covering": {
+ "fr": "",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "accessibility_exposure": {
+ "fr": "",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "accessibility_level": null,
+ "accessibility_signage": {
+ "fr": "",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "accessibility_slope": {
+ "fr": "",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "accessibility_width": {
+ "fr": "",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "advice": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "advised_parking": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "altimetric_profile": "https://foo.fr/api/v2/trek/10439/profile/",
+ "ambiance": {
+ "fr": "Je suis une bonne ambiance.",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "arrival": {
+ "fr": "Ourjout",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "ascent": 203,
+ "attachments": [
+ {
+ "backend": "",
+ "type": "image",
+ "author": "Borvan53, CC-By-SA 4.0",
+ "license": null,
+ "thumbnail": "https://foo.fr/media/paperclip/trekking_trek/10439/bocard_deylie_2013_01.JPG.400x0_q85.jpg",
+ "legend": "Vue du site du bocard d'Eylie",
+ "title": "",
+ "url": "https://foo.fr/media/paperclip/trekking_trek/10439/bocard_deylie_2013_01.JPG",
+ "uuid": "2ed7ebc0-39ed-482c-bae3-0b1182f27d3d"
+ },
+ {
+ "backend": "",
+ "type": "image",
+ "author": "",
+ "license": null,
+ "thumbnail": "https://foo.fr/media/paperclip/trekking_trek/10439/eeaio.jpeg.400x0_q85.jpg",
+ "legend": "",
+ "title": "éèàïô",
+ "url": "https://foo.fr/media/paperclip/trekking_trek/10439/eeaio.jpeg",
+ "uuid": "05c0c4ad-69bd-4b86-a8dc-3cbc74ecc4b6"
+ }
+ ],
+ "attachments_accessibility": [],
+ "children": [],
+ "cities": [
+ "09290",
+ "09059",
+ "09317",
+ "09062"
+ ],
+ "create_datetime": "2019-07-23T09:09:53.090318Z",
+ "departure": {
+ "fr": "Eylé",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "departure_city": "09290",
+ "departure_geom": [
+ 0.9377712652787816,
+ 42.83789927043632
+ ],
+ "descent": -564,
+ "description": {
+ "fr": "Dede est au bar.",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "description_teaser": {
+ "fr": "Chapeau a fleur.",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "difficulty": 2,
+ "disabled_infrastructure": {
+ "fr": "",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "duration": 6.0,
+ "elevation_area_url": "https://foo.fr/api/v2/trek/10439/dem/",
+ "elevation_svg_url": "https://foo.fr/api/v2/trek/10439/profile/?language=fr&format=svg",
+ "external_id": null,
+ "gear": {
+ "fr": "",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "geometry": {
+ "type": "LineString",
+ "coordinates": [
+ [
+ 0.9377713,
+ 42.8378993,
+ 912.0
+ ],
+ [
+ 0.9377673,
+ 42.8379281,
+ 913.0
+ ]
+ ]
+ },
+ "gpx": "https://foo.fr/api/fr/treks/10439/deyle-a-ourjout.gpx",
+ "information_desks": [],
+ "kml": "https://foo.fr/api/fr/treks/10439/deyle-a-ourjout.kml",
+ "labels": [],
+ "length_2d": 12856.3,
+ "length_3d": 12904.9,
+ "max_elevation": 927,
+ "min_elevation": 551,
+ "name": {
+ "fr": "Foo",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "networks": [],
+ "next": {
+ "2": null,
+ "10445": 10441
+ },
+ "parents": [
+ 2,
+ 10445
+ ],
+ "parking_location": null,
+ "pdf": {
+ "fr": "https://foo.fr/api/fr/treks/10439/deyle-a-ourjout.pdf",
+ "en": "https://foo.fr/api/en/treks/10439/deyle-a-ourjout.pdf",
+ "es": "https://foo.fr/api/es/treks/10439/deyle-a-ourjout.pdf",
+ "it": "https://foo.fr/api/it/treks/10439/deyle-a-ourjout.pdf"
+ },
+ "points_reference": null,
+ "portal": [],
+ "practice": 4,
+ "ratings": [],
+ "ratings_description": {
+ "fr": "",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "previous": {
+ "2": null,
+ "10445": null
+ },
+ "public_transport": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "published": {
+ "fr": true,
+ "en": false,
+ "es": false,
+ "it": false
+ },
+ "reservation_system": null,
+ "reservation_id": "",
+ "route": 3,
+ "second_external_id": null,
+ "source": [],
+ "structure": 3,
+ "themes": [],
+ "update_datetime": "2021-05-17T13:54:07.091500Z",
+ "url": "https://foo.fr/api/v2/trek/10439/",
+ "uuid": "c9567576-2934-43ab-979e-e13d02c671a9",
+ "web_links": []
+ },
+ {
+ "id": 8700,
+ "access": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "accessibilities": [],
+ "accessibility_advice": {
+ "fr": "",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "accessibility_covering": {
+ "fr": "",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "accessibility_exposure": {
+ "fr": "",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "accessibility_level": null,
+ "accessibility_signage": {
+ "fr": "",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "accessibility_slope": {
+ "fr": "",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "accessibility_width": {
+ "fr": "",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "advice": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "advised_parking": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "altimetric_profile": "https://foo.fr/api/v2/trek/8700/profile/",
+ "ambiance": {
+ "fr": "Ambience",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "arrival": {
+ "fr": "Étang",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "ascent": 1051,
+ "attachments": [],
+ "attachments_accessibility": [],
+ "children": [],
+ "cities": [
+ "09322"
+ ],
+ "create_datetime": "2019-04-01T12:25:12.319886Z",
+ "departure": {
+ "fr": "Île aux moussures",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "departure_city": "09322",
+ "departure_geom": [
+ 1.28743696523673,
+ 42.75618193887717
+ ],
+ "descent": -133,
+ "description": {
+ "fr": "Description",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "description_teaser": {
+ "fr": "Chapeau",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "difficulty": 3,
+ "disabled_infrastructure": {
+ "fr": "",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "duration": 4.0,
+ "elevation_area_url": "https://foo.fr/api/v2/trek/8700/dem/",
+ "elevation_svg_url": "https://foo.fr/api/v2/trek/8700/profile/?language=fr&format=svg",
+ "external_id": null,
+ "gear": {
+ "fr": "",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "geometry": {
+ "type": "LineString",
+ "coordinates": [
+ [
+ 1.287437,
+ 42.7561819,
+ 1010.0
+ ],
+ [
+ 1.2873813,
+ 42.756201,
+ 1007.0
+ ]
+ ]
+ },
+ "gpx": "https://foo.fr/api/fr/treks/8700/etang-dalet.gpx",
+ "information_desks": [],
+ "kml": "https://foo.fr/api/fr/treks/8700/etang-dalet.kml",
+ "labels": [
+ 2
+ ],
+ "length_2d": 4844.7,
+ "length_3d": 5073.4,
+ "max_elevation": 1936,
+ "min_elevation": 979,
+ "name": {
+ "fr": "Étang d'Alet",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "networks": [],
+ "next": {},
+ "parents": [],
+ "parking_location": null,
+ "pdf": {
+ "fr": "https://foo.fr/api/fr/treks/8700/etang-dalet.pdf",
+ "en": "https://foo.fr/api/en/treks/8700/etang-dalet.pdf",
+ "es": "https://foo.fr/api/es/treks/8700/etang-dalet.pdf",
+ "it": "https://foo.fr/api/it/treks/8700/etang-dalet.pdf"
+ },
+ "points_reference": null,
+ "portal": [],
+ "practice": 4,
+ "ratings": [],
+ "ratings_description": {
+ "fr": "",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "previous": {},
+ "public_transport": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "published": {
+ "fr": true,
+ "en": false,
+ "es": false,
+ "it": false
+ },
+ "reservation_system": null,
+ "reservation_id": "",
+ "route": 2,
+ "second_external_id": null,
+ "source": [],
+ "structure": 3,
+ "themes": [],
+ "update_datetime": "2021-04-28T14:45:50.070810Z",
+ "url": "https://foo.fr/api/v2/trek/8700/",
+ "uuid": "b2aea892-5e6e-4daa-a750-7d2ee52d3fe1",
+ "web_links": []
+ }
+ ]
+}
diff --git a/geotrek/trekking/tests/data/geotrek_parser_v2/trek_2.json b/geotrek/trekking/tests/data/geotrek_parser_v2/trek_2.json
new file mode 100644
index 0000000000..639fdd9da5
--- /dev/null
+++ b/geotrek/trekking/tests/data/geotrek_parser_v2/trek_2.json
@@ -0,0 +1,217 @@
+{
+ "count": 1,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "id": 8702,
+ "access": {
+ "fr": "Accès",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "accessibilities": [],
+ "accessibility_advice": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "accessibility_covering": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "accessibility_exposure": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "accessibility_level": null,
+ "accessibility_signage": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "accessibility_slope": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "accessibility_width": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "advice": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "advised_parking": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "altimetric_profile": "https://foo.fr/api/v2/trek/8702/profile/",
+ "ambiance": {
+ "fr": "Ambiance",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "arrival": {
+ "fr": "Étangs de Picot",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "ascent": 797,
+ "attachments": [],
+ "attachments_accessibility": [],
+ "children": [],
+ "cities": [
+ "09030"
+ ],
+ "create_datetime": "2019-04-01T13:04:06.795861Z",
+ "departure": {
+ "fr": "Barrage de Soulcem",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "departure_city": "09030",
+ "departure_geom": [
+ 1.4526560730280935,
+ 42.677959776888834
+ ],
+ "descent": -65,
+ "description": {
+ "fr": "Description Lorem ipsum sit dolor amet Description Lorem ipsum sit dolor ametDescription Lorem ipsum sit dolor ametDescription Lorem ipsum sit dolor ametDescription Lorem ipsum sit dolor ametDescription Lorem ipsum sit dolor ametDescription Lorem ipsum sit dolor ametDescription Lorem ipsum sit dolor ametDescription Lorem ipsum sit dolor ametDescription Lorem ipsum sit dolor ametDescription Lorem ipsum sit dolor ametDescription Lorem ipsum sit dolor ametDescription Lorem ipsum sit dolor amet
\r\n\r\n- \r\n
2tape numéro 1 sit dolor ametDescription Lorem ipsum sit dolor ametDescription Lorem ipsum sit dolor ametDescription Lorem ipsum sit dolor ametDescription Lorem ipsum sit dolor ametDescription Lorem ipsum sit dolor ametDesc \r\n \r\n- \r\n
Prendre à droite ametDescription Lorem ipsum sit dolor ametDescription Lorem ipsum sit dolor ametDescription Lorem ipsum sit dolor ametDescription Lorem ipsum sit dolor ametDescription Lorem ipsum sit dolor ametDescription Lor \r\n \r\n- Terminer tout droit
\r\n \r\nDescription Lorem ipsum sit dolor ametDescription Lorem ipsum sit dolor ametDescription Lorem ipsum sit dolor ametDescription Lorem ipsum sit dolor amet ",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "description_teaser": {
+ "fr": "Chapeau",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "difficulty": 3,
+ "disabled_infrastructure": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "duration": 5.0,
+ "elevation_area_url": "https://foo.fr/api/v2/trek/8702/dem/",
+ "elevation_svg_url": "https://foo.fr/api/v2/trek/8702/profile/?language=fr&format=svg",
+ "external_id": null,
+ "gear": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "geometry": {
+ "type": "LineString",
+ "coordinates": [
+ [
+ 1.4526561,
+ 42.6779598,
+ 1552.0
+ ],
+ [
+ 1.4526694,
+ 42.6779712,
+ 1554.0
+ ]
+ ]
+ },
+ "gpx": "https://foo.fr/api/fr/treks/8702/etangs-du-picot.gpx",
+ "information_desks": [],
+ "kml": "https://foo.fr/api/fr/treks/8702/etangs-du-picot.kml",
+ "labels": [],
+ "length_2d": 3347.5,
+ "length_3d": 3536.2,
+ "max_elevation": 2298,
+ "min_elevation": 1537,
+ "name": {
+ "fr": "Étangs du Picot",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "networks": [],
+ "next": {},
+ "parents": [],
+ "parking_location": null,
+ "pdf": {
+ "fr": "https://foo.fr/api/fr/treks/8702/etangs-du-picot.pdf",
+ "en": "https://foo.fr/api/en/treks/8702/etangs-du-picot.pdf",
+ "es": "https://foo.fr/api/es/treks/8702/etangs-du-picot.pdf",
+ "it": "https://foo.fr/api/it/treks/8702/etangs-du-picot.pdf"
+ },
+ "points_reference": {
+ "type": "MultiPoint",
+ "coordinates": [
+ [
+ 1.451697349548341,
+ 42.6817728861541
+ ],
+ [
+ 1.456890106201172,
+ 42.68590558513195
+ ],
+ [
+ 1.465902328491211,
+ 42.68448598671003
+ ]
+ ]
+ },
+ "portal": [],
+ "practice": 4,
+ "ratings": [],
+ "ratings_description": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "previous": {},
+ "public_transport": {
+ "fr": "",
+ "en": "",
+ "es": "",
+ "it": ""
+ },
+ "published": {
+ "fr": true,
+ "en": false,
+ "es": false,
+ "it": false
+ },
+ "reservation_system": null,
+ "reservation_id": "",
+ "route": 2,
+ "second_external_id": null,
+ "source": [],
+ "structure": 3,
+ "themes": [],
+ "update_datetime": "2022-04-11T14:52:42.637165Z",
+ "url": "https://foo.fr/api/v2/trek/8702/",
+ "uuid": "58ed4fc1-645d-4bf6-b956-71f0a01a5eec",
+ "web_links": []
+ }
+ ]
+}
diff --git a/geotrek/trekking/tests/data/geotrek_parser_v2/trek_accessibility.json b/geotrek/trekking/tests/data/geotrek_parser_v2/trek_accessibility.json
new file mode 100644
index 0000000000..1040952cd6
--- /dev/null
+++ b/geotrek/trekking/tests/data/geotrek_parser_v2/trek_accessibility.json
@@ -0,0 +1,37 @@
+{
+ "count": 3,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "id": 1,
+ "name": {
+ "fr": "Fauteuil roulant",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "pictogram": "https://foo.fr/media/upload/accessibility-wheelchair.png"
+ },
+ {
+ "id": 2,
+ "name": {
+ "fr": "Poussette",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "pictogram": "https://foo.fr/media/upload/accessibility-troller.png"
+ },
+ {
+ "id": 3,
+ "name": {
+ "fr": "Joelette",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "pictogram": "https://foo.fr/media/upload/accessibility-joelette.png"
+ }
+ ]
+}
diff --git a/geotrek/trekking/tests/data/geotrek_parser_v2/trek_children.json b/geotrek/trekking/tests/data/geotrek_parser_v2/trek_children.json
new file mode 100644
index 0000000000..16226a3287
--- /dev/null
+++ b/geotrek/trekking/tests/data/geotrek_parser_v2/trek_children.json
@@ -0,0 +1,31 @@
+{
+ "count": 5,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "uuid": "9e70b294-1134-4c50-9c56-d722720cacf1",
+ "steps": [
+ {
+ "uuid": "c9567576-2934-43ab-979e-e13d02c671a9"
+ }
+ ]
+ },
+ {
+ "uuid": "1ba24605-aff2-4b16-bf30-6de1ebfb2a12",
+ "steps": []
+ },
+ {
+ "uuid": "6761143f-9244-41d0-b1af-21114408f769",
+ "steps": []
+ },
+ {
+ "uuid": "c9567576-2934-43ab-979e-e13d02c671a9",
+ "steps": []
+ },
+ {
+ "uuid": "b2aea892-5e6e-4daa-a750-7d2ee52d3fe1",
+ "steps": []
+ }
+ ]
+}
diff --git a/geotrek/trekking/tests/data/geotrek_parser_v2/trek_children_do_not_exist.json b/geotrek/trekking/tests/data/geotrek_parser_v2/trek_children_do_not_exist.json
new file mode 100644
index 0000000000..ed743a54b3
--- /dev/null
+++ b/geotrek/trekking/tests/data/geotrek_parser_v2/trek_children_do_not_exist.json
@@ -0,0 +1,42 @@
+{
+ "count": 5,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "uuid": "9e70b294-1134-4c50-9c56-d722720cacf1",
+ "steps": [
+ {
+ "uuid": "c9567576-2934-43ab-979e-e13d02c671a9"
+ },
+ {
+ "uuid": "c9567576-2934-43ab-666e-e13d02c671a9"
+ }
+ ]
+ },
+ {
+ "uuid": "1ba24605-aff2-4b16-bf30-6de1ebfb2a12",
+ "steps": []
+ },
+ {
+ "uuid": "6761143f-9244-41d0-b1af-21114408f769",
+ "steps": []
+ },
+ {
+ "uuid": "c9567576-2934-43ab-979e-e13d02c671a9",
+ "steps": []
+ },
+ {
+ "uuid": "b2aea892-5e6e-4daa-a750-7d2ee52d3fe1",
+ "steps": []
+ },
+ {
+ "uuid": "b2aea666-5e6e-4daa-a750-7d2ee52d3fe1",
+ "steps": [
+ {
+ "uuid": "c9567576-2934-43ab-979e-e13d02c671a9"
+ }
+ ]
+ }
+ ]
+}
diff --git a/geotrek/trekking/tests/data/geotrek_parser_v2/trek_difficulty.json b/geotrek/trekking/tests/data/geotrek_parser_v2/trek_difficulty.json
new file mode 100644
index 0000000000..da62db310b
--- /dev/null
+++ b/geotrek/trekking/tests/data/geotrek_parser_v2/trek_difficulty.json
@@ -0,0 +1,62 @@
+{
+ "count": 5,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "id": 1,
+ "cirkwi_level": 1,
+ "label": {
+ "fr": "Très facile",
+ "en": "Very easy",
+ "es": null,
+ "it": null
+ },
+ "pictogram": "https://foo.fr/media/upload/difficulty-1.svg"
+ },
+ {
+ "id": 2,
+ "cirkwi_level": 2,
+ "label": {
+ "fr": "Facile",
+ "en": "Easy",
+ "es": null,
+ "it": null
+ },
+ "pictogram": "https://foo.fr/media/upload/difficulty-2.svg"
+ },
+ {
+ "id": 3,
+ "cirkwi_level": 3,
+ "label": {
+ "fr": "Intermédiaire",
+ "en": "Medium",
+ "es": null,
+ "it": null
+ },
+ "pictogram": "https://foo.fr/media/upload/difficulty-3.svg"
+ },
+ {
+ "id": 4,
+ "cirkwi_level": 4,
+ "label": {
+ "fr": "Difficile",
+ "en": "Hard",
+ "es": null,
+ "it": null
+ },
+ "pictogram": "https://foo.fr/media/upload/difficulty-4.svg"
+ },
+ {
+ "id": 5,
+ "cirkwi_level": 5,
+ "label": {
+ "fr": "Très difficile",
+ "en": "Very hard",
+ "es": null,
+ "it": null
+ },
+ "pictogram": "https://foo.fr/media/upload/difficulty-5.svg"
+ }
+ ]
+}
diff --git a/geotrek/trekking/tests/data/geotrek_parser_v2/trek_ids.json b/geotrek/trekking/tests/data/geotrek_parser_v2/trek_ids.json
new file mode 100644
index 0000000000..a6ab1d4325
--- /dev/null
+++ b/geotrek/trekking/tests/data/geotrek_parser_v2/trek_ids.json
@@ -0,0 +1,22 @@
+{
+ "count": 5,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "uuid": "9e70b294-1134-4c50-9c56-d722720cacf1"
+ },
+ {
+ "uuid": "1ba24605-aff2-4b16-bf30-6de1ebfb2a12"
+ },
+ {
+ "uuid": "6761143f-9244-41d0-b1af-21114408f769"
+ },
+ {
+ "uuid": "c9567576-2934-43ab-979e-e13d02c671a9"
+ },
+ {
+ "uuid": "b2aea892-5e6e-4daa-a750-7d2ee52d3fe1"
+ }
+ ]
+}
diff --git a/geotrek/trekking/tests/data/geotrek_parser_v2/trek_ids_2.json b/geotrek/trekking/tests/data/geotrek_parser_v2/trek_ids_2.json
new file mode 100644
index 0000000000..35c48d9a9b
--- /dev/null
+++ b/geotrek/trekking/tests/data/geotrek_parser_v2/trek_ids_2.json
@@ -0,0 +1,13 @@
+{
+ "count": 2,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "uuid": "58ed4fc1-645d-4bf6-b956-71f0a01a5eec"
+ },
+ {
+ "uuid": "9e70b294-1134-4c50-9c56-d722720cacf1"
+ }
+ ]
+}
diff --git a/geotrek/trekking/tests/data/geotrek_parser_v2/trek_label.json b/geotrek/trekking/tests/data/geotrek_parser_v2/trek_label.json
new file mode 100644
index 0000000000..67f0fa464a
--- /dev/null
+++ b/geotrek/trekking/tests/data/geotrek_parser_v2/trek_label.json
@@ -0,0 +1,58 @@
+{
+ "count": 3,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "id": 1,
+ "advice": {
+ "fr": "La divagation des chiens est autorisée sur le sentier. \r\n![\"Chient](\"http://www.mercantour-parcnational.fr/sites/mercantour.eu/files/styles/slide_750x500/public/22259_pnm-800px.jpg?itok=S1nrGDX_\") ",
+ "en": "The national park is an unrestricted natural area but subjected to regulations which must be known by all visitors.",
+ "es": "El parque nacional es un área natural sin restricciones pero sometido a regulaciones que deben ser conocidas por todos los visitantes.",
+ "it": "Il Parco Nazionale è un territorio naturale, aperto a tutti, ma soggetto ad un regolamento che è utile conoscere per preparare il vostro soggiorno."
+ },
+ "filter": true,
+ "name": {
+ "fr": "Chien autorisé",
+ "en": "Is in the midst of the park",
+ "es": "En coeur de parc",
+ "it": "Nel cuore del parco"
+ },
+ "pictogram": "https://foo.fr/media/upload/dog.png"
+ },
+ {
+ "id": 2,
+ "advice": {
+ "fr": "Le Parc national est un territoire naturel, ouvert à tous, mais soumis à une réglementation qu’il est utile de connaître pour préparer son séjour",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "filter": true,
+ "name": {
+ "fr": "En coeur de parc",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "pictogram": null
+ },
+ {
+ "id": 3,
+ "advice": {
+ "fr": "Cette randonnée se déroule sur un territoire où les patous sont présents.",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "filter": true,
+ "name": {
+ "fr": "Information Patou",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "pictogram": null
+ }
+ ]
+}
diff --git a/geotrek/trekking/tests/data/geotrek_parser_v2/trek_network.json b/geotrek/trekking/tests/data/geotrek_parser_v2/trek_network.json
new file mode 100644
index 0000000000..cdff494575
--- /dev/null
+++ b/geotrek/trekking/tests/data/geotrek_parser_v2/trek_network.json
@@ -0,0 +1,27 @@
+{
+ "count": 2,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "id": 2,
+ "label": {
+ "fr": "PR",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "pictogram": "https://foo.fr/media/upload/network-PR.svg"
+ },
+ {
+ "id": 4,
+ "label": {
+ "fr": "VTT",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "pictogram": "https://foo.fr/media/upload/network-VTT.svg"
+ }
+ ]
+}
diff --git a/geotrek/trekking/tests/data/geotrek_parser_v2/trek_practice.json b/geotrek/trekking/tests/data/geotrek_parser_v2/trek_practice.json
new file mode 100644
index 0000000000..ac28443b96
--- /dev/null
+++ b/geotrek/trekking/tests/data/geotrek_parser_v2/trek_practice.json
@@ -0,0 +1,40 @@
+{
+ "count": 3,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "id": 1,
+ "name": {
+ "fr": "VTT",
+ "en": "Mountain Bike",
+ "es": null,
+ "it": null
+ },
+ "order": 3,
+ "pictogram": "https://foo.fr/media/upload/practice-mountainbike.svg"
+ },
+ {
+ "id": 3,
+ "name": {
+ "fr": "Cheval",
+ "en": "Horse",
+ "es": null,
+ "it": null
+ },
+ "order": 4,
+ "pictogram": "https://foo.fr/media/upload/practice-horse.svg"
+ },
+ {
+ "id": 4,
+ "name": {
+ "fr": "Pédestre",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "order": 1,
+ "pictogram": "https://foo.fr/media/upload/practice-foot.svg"
+ }
+ ]
+}
diff --git a/geotrek/trekking/tests/data/geotrek_parser_v2/trek_route.json b/geotrek/trekking/tests/data/geotrek_parser_v2/trek_route.json
new file mode 100644
index 0000000000..17e5a389db
--- /dev/null
+++ b/geotrek/trekking/tests/data/geotrek_parser_v2/trek_route.json
@@ -0,0 +1,37 @@
+{
+ "count": 3,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "id": 1,
+ "pictogram": "https://foo.fr/media/upload/route-loop.svg",
+ "route": {
+ "fr": "Boucle",
+ "en": null,
+ "es": null,
+ "it": null
+ }
+ },
+ {
+ "id": 2,
+ "pictogram": "https://foo.fr/media/upload/route-return.svg",
+ "route": {
+ "fr": "Aller-retour",
+ "en": null,
+ "es": null,
+ "it": null
+ }
+ },
+ {
+ "id": 3,
+ "pictogram": "https://foo.fr/media/upload/route-cross.svg",
+ "route": {
+ "fr": "Traversée",
+ "en": null,
+ "es": null,
+ "it": null
+ }
+ }
+ ]
+}
diff --git a/geotrek/trekking/tests/data/geotrek_parser_v2/trek_theme.json b/geotrek/trekking/tests/data/geotrek_parser_v2/trek_theme.json
new file mode 100644
index 0000000000..bb6570f322
--- /dev/null
+++ b/geotrek/trekking/tests/data/geotrek_parser_v2/trek_theme.json
@@ -0,0 +1,107 @@
+{
+ "count": 10,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "id": 1,
+ "label": {
+ "fr": "Faune",
+ "en": "Fauna",
+ "es": null,
+ "it": null
+ },
+ "pictogram": "https://foo.fr/media/upload/theme-fauna.png"
+ },
+ {
+ "id": 2,
+ "label": {
+ "fr": "Flore",
+ "en": "Flora",
+ "es": null,
+ "it": null
+ },
+ "pictogram": "https://foo.fr/media/upload/theme-flora.png"
+ },
+ {
+ "id": 4,
+ "label": {
+ "fr": "Point de vue",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "pictogram": "https://foo.fr/media/upload/theme-panorama.png"
+ },
+ {
+ "id": 5,
+ "label": {
+ "fr": "Architecture",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "pictogram": "https://foo.fr/media/upload/theme-architecture.png"
+ },
+ {
+ "id": 6,
+ "label": {
+ "fr": "Pastoralisme",
+ "en": "Pastoralism",
+ "es": null,
+ "it": null
+ },
+ "pictogram": "https://foo.fr/media/upload/theme-pastoral.png"
+ },
+ {
+ "id": 7,
+ "label": {
+ "fr": "Géologie",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "pictogram": "https://foo.fr/media/upload/theme-geology.png"
+ },
+ {
+ "id": 8,
+ "label": {
+ "fr": "Lac et glacier",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "pictogram": "https://foo.fr/media/upload/theme-lake.png"
+ },
+ {
+ "id": 9,
+ "label": {
+ "fr": "Sommet",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "pictogram": "https://foo.fr/media/upload/theme-peak.png"
+ },
+ {
+ "id": 10,
+ "label": {
+ "fr": "Refuge",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "pictogram": "https://foo.fr/media/upload/theme-refugee.png"
+ },
+ {
+ "id": 11,
+ "label": {
+ "fr": "Archéologie et histoire",
+ "en": null,
+ "es": null,
+ "it": null
+ },
+ "pictogram": "https://foo.fr/media/upload/theme-history.png"
+ }
+ ]
+}
diff --git a/geotrek/trekking/tests/data/geotrek_parser_v2/trek_wrong_children.json b/geotrek/trekking/tests/data/geotrek_parser_v2/trek_wrong_children.json
new file mode 100644
index 0000000000..a6ab1d4325
--- /dev/null
+++ b/geotrek/trekking/tests/data/geotrek_parser_v2/trek_wrong_children.json
@@ -0,0 +1,22 @@
+{
+ "count": 5,
+ "next": null,
+ "previous": null,
+ "results": [
+ {
+ "uuid": "9e70b294-1134-4c50-9c56-d722720cacf1"
+ },
+ {
+ "uuid": "1ba24605-aff2-4b16-bf30-6de1ebfb2a12"
+ },
+ {
+ "uuid": "6761143f-9244-41d0-b1af-21114408f769"
+ },
+ {
+ "uuid": "c9567576-2934-43ab-979e-e13d02c671a9"
+ },
+ {
+ "uuid": "b2aea892-5e6e-4daa-a750-7d2ee52d3fe1"
+ }
+ ]
+}
diff --git a/geotrek/trekking/tests/test_parsers.py b/geotrek/trekking/tests/test_parsers.py
index 4865227649..4080c78ec6 100644
--- a/geotrek/trekking/tests/test_parsers.py
+++ b/geotrek/trekking/tests/test_parsers.py
@@ -1,12 +1,19 @@
+from io import StringIO
+from unittest import mock
+import json
import os
+from unittest import skipIf
+from django.conf import settings
from django.contrib.gis.geos import Point, LineString, MultiLineString, WKTWriter
from django.core.management import call_command
from django.test import TestCase
+from django.test.utils import override_settings
-from geotrek.common.models import Theme, FileType
-from geotrek.trekking.models import Trek, DifficultyLevel, Route
-from geotrek.trekking.parsers import TrekParser
+from geotrek.common.models import Theme, FileType, Attachment
+from geotrek.common.tests.mixins import GeotrekParserTestMixin
+from geotrek.trekking.models import POI, Service, Trek, DifficultyLevel, Route
+from geotrek.trekking.parsers import TrekParser, GeotrekPOIParser, GeotrekServiceParser, GeotrekTrekParser
class TrekParserFilterDurationTests(TestCase):
@@ -124,3 +131,363 @@ def test_create(self):
self.assertEqual(trek.route, self.route)
self.assertQuerysetEqual(trek.themes.all(), [repr(t) for t in self.themes], ordered=False)
self.assertEqual(WKTWriter(precision=4).write(trek.geom), WKT)
+
+
+class TestGeotrekTrekParser(GeotrekTrekParser):
+ url = "https://test.fr"
+ provider = 'geotrek1'
+ field_options = {
+ 'difficulty': {'create': True, },
+ 'route': {'create': True, },
+ 'themes': {'create': True},
+ 'practice': {'create': True},
+ 'accessibilities': {'create': True},
+ 'networks': {'create': True},
+ 'geom': {'required': True},
+ 'labels': {'create': True},
+ }
+
+
+class TestGeotrek2TrekParser(GeotrekTrekParser):
+ url = "https://test.fr"
+
+ field_options = {
+ 'geom': {'required': True},
+ }
+ provider = 'geotrek2'
+
+
+class TestGeotrekPOIParser(GeotrekPOIParser):
+ url = "https://test.fr"
+
+ field_options = {
+ 'type': {'create': True, },
+ 'geom': {'required': True},
+ }
+
+
+class TestGeotrekServiceParser(GeotrekServiceParser):
+ url = "https://test.fr"
+
+ field_options = {
+ 'type': {'create': True, },
+ 'geom': {'required': True},
+ }
+
+
+@override_settings(MODELTRANSLATION_DEFAULT_LANGUAGE="fr")
+@skipIf(settings.TREKKING_TOPOLOGY_ENABLED, 'Test without dynamic segmentation only')
+class TrekGeotrekParserTests(GeotrekParserTestMixin, TestCase):
+ app_label = 'trekking'
+
+ @classmethod
+ def setUpTestData(cls):
+ cls.filetype = FileType.objects.create(type="Photographie")
+
+ @mock.patch('requests.get')
+ @mock.patch('requests.head')
+ def test_create(self, mocked_head, mocked_get):
+ self.mock_time = 0
+ self.mock_json_order = ['trek_difficulty.json', 'trek_route.json', 'trek_theme.json', 'trek_practice.json',
+ 'trek_accessibility.json', 'trek_network.json', 'trek_label.json', 'trek_ids.json',
+ 'trek.json', 'trek_children.json', ]
+
+ # Mock GET
+ mocked_get.return_value.status_code = 200
+ mocked_get.return_value.json = self.mock_json
+ mocked_get.return_value.content = b''
+ mocked_head.return_value.status_code = 200
+
+ call_command('import', 'geotrek.trekking.tests.test_parsers.TestGeotrekTrekParser', verbosity=0)
+ self.assertEqual(Trek.objects.count(), 5)
+ trek = Trek.objects.all().first()
+ self.assertEqual(trek.name, "Boucle du Pic des Trois Seigneurs")
+ self.assertEqual(trek.name_it, "Foo bar")
+ self.assertEqual(str(trek.difficulty), 'Très facile')
+ self.assertEqual(str(trek.practice), 'Cheval')
+ self.assertAlmostEqual(trek.geom[0][0], 569946.9850365581, places=5)
+ self.assertAlmostEqual(trek.geom[0][1], 6190964.893167565, places=5)
+ self.assertEqual(trek.children.first().name, "Foo")
+ self.assertEqual(trek.labels.count(), 3)
+ self.assertEqual(trek.labels.first().name, "Chien autorisé")
+ self.assertEqual(Attachment.objects.filter(object_id=trek.pk).count(), 3)
+ self.assertEqual(Attachment.objects.get(object_id=trek.pk, license__isnull=False).license.label, "License")
+
+ @mock.patch('requests.get')
+ @mock.patch('requests.head')
+ def test_create_multiple_page(self, mocked_head, mocked_get):
+ class MockResponse:
+ mock_json_order = ['trek_difficulty.json', 'trek_route.json', 'trek_theme.json', 'trek_practice.json',
+ 'trek_accessibility.json', 'trek_network.json', 'trek_label.json', 'trek_ids.json',
+ 'trek.json', 'trek_children.json', 'trek_children.json']
+ mock_time = 0
+ total_mock_response = 1
+
+ def __init__(self, status_code):
+ self.status_code = status_code
+
+ def json(self):
+ filename = os.path.join(os.path.dirname(__file__), 'data', 'geotrek_parser_v2',
+ self.mock_json_order[self.mock_time])
+ with open(filename, 'r') as f:
+ data_json = json.load(f)
+ if self.mock_json_order[self.mock_time] == 'trek.json':
+ data_json['count'] = 10
+ if self.total_mock_response == 1:
+ self.total_mock_response += 1
+ data_json['next'] = "foo"
+ self.mock_time += 1
+ return data_json
+
+ @property
+ def content(self):
+ return b''
+
+ # Mock GET
+ mocked_get.return_value = MockResponse(200)
+ mocked_head.return_value.status_code = 200
+
+ call_command('import', 'geotrek.trekking.tests.test_parsers.TestGeotrekTrekParser', verbosity=0)
+ self.assertEqual(Trek.objects.count(), 5)
+ trek = Trek.objects.all().first()
+ self.assertEqual(trek.name, "Boucle du Pic des Trois Seigneurs")
+ self.assertEqual(trek.name_it, "Foo bar")
+ self.assertEqual(str(trek.difficulty), 'Très facile')
+ self.assertEqual(str(trek.practice), 'Cheval')
+ self.assertAlmostEqual(trek.geom[0][0], 569946.9850365581, places=5)
+ self.assertAlmostEqual(trek.geom[0][1], 6190964.893167565, places=5)
+ self.assertEqual(trek.children.first().name, "Foo")
+ self.assertEqual(trek.labels.count(), 3)
+ self.assertEqual(trek.labels.first().name, "Chien autorisé")
+ self.assertEqual(Attachment.objects.filter(object_id=trek.pk).count(), 3)
+ self.assertEqual(Attachment.objects.get(object_id=trek.pk, license__isnull=False).license.label, "License")
+
+ @override_settings(PAPERCLIP_MAX_BYTES_SIZE_IMAGE=1)
+ @mock.patch('requests.get')
+ @mock.patch('requests.head')
+ def test_create_attachment_max_size(self, mocked_head, mocked_get):
+ self.mock_time = 0
+ self.mock_json_order = ['trek_difficulty.json', 'trek_route.json', 'trek_theme.json', 'trek_practice.json',
+ 'trek_accessibility.json', 'trek_network.json', 'trek_label.json', 'trek_ids.json',
+ 'trek.json', 'trek_children.json', ]
+
+ # Mock GET
+ mocked_get.return_value.status_code = 200
+ mocked_get.return_value.json = self.mock_json
+ mocked_get.return_value.content = b'11'
+ mocked_head.return_value.status_code = 200
+
+ call_command('import', 'geotrek.trekking.tests.test_parsers.TestGeotrekTrekParser', verbosity=0)
+ self.assertEqual(Trek.objects.count(), 5)
+ self.assertEqual(Attachment.objects.count(), 0)
+
+ @mock.patch('requests.get')
+ @mock.patch('requests.head')
+ def test_update_attachment(self, mocked_head, mocked_get):
+
+ class MockResponse:
+ mock_json_order = ['trek_difficulty.json', 'trek_route.json', 'trek_theme.json', 'trek_practice.json',
+ 'trek_accessibility.json', 'trek_network.json', 'trek_label.json', 'trek_ids.json',
+ 'trek.json', 'trek_children.json', ]
+ mock_time = 0
+ a = 0
+
+ def __init__(self, status_code):
+ self.status_code = status_code
+
+ def json(self):
+ if len(self.mock_json_order) <= self.mock_time:
+ self.mock_time = 0
+ filename = os.path.join(os.path.dirname(__file__), 'data', 'geotrek_parser_v2',
+ self.mock_json_order[self.mock_time])
+
+ self.mock_time += 1
+ with open(filename, 'r') as f:
+ return json.load(f)
+
+ @property
+ def content(self):
+ # We change content of attachment every time
+ self.a += 1
+ return bytes(f'{self.a}', 'utf-8')
+
+ # Mock GET
+ mocked_get.return_value.status_code = 200
+ mocked_get.return_value = MockResponse(200)
+ mocked_head.return_value.status_code = 200
+
+ call_command('import', 'geotrek.trekking.tests.test_parsers.TestGeotrekTrekParser', verbosity=0)
+ self.assertEqual(Trek.objects.count(), 5)
+ trek = Trek.objects.all().first()
+ self.assertEqual(Attachment.objects.filter(object_id=trek.pk).count(), 3)
+ self.assertEqual(Attachment.objects.first().attachment_file.read(), b'11')
+ call_command('import', 'geotrek.trekking.tests.test_parsers.TestGeotrekTrekParser', verbosity=0)
+ self.assertEqual(Trek.objects.count(), 5)
+ trek.refresh_from_db()
+ self.assertEqual(Attachment.objects.filter(object_id=trek.pk).count(), 3)
+ self.assertEqual(Attachment.objects.first().attachment_file.read(), b'13')
+
+ @mock.patch('requests.get')
+ @mock.patch('requests.head')
+ def test_create_multiple(self, mocked_head, mocked_get):
+ self.mock_time = 0
+ self.mock_json_order = ['trek_difficulty.json', 'trek_route.json', 'trek_theme.json', 'trek_practice.json',
+ 'trek_accessibility.json', 'trek_network.json', 'trek_label.json', 'trek_ids.json', 'trek.json',
+ 'trek_children.json', 'trek_difficulty.json', 'trek_route.json', 'trek_theme.json',
+ 'trek_practice.json', 'trek_accessibility.json', 'trek_network.json', 'trek_label.json',
+ 'trek_ids_2.json', 'trek_2.json', 'trek_children.json', ]
+
+ # Mock GET
+ mocked_get.return_value.status_code = 200
+ mocked_get.return_value.json = self.mock_json
+ mocked_get.return_value.content = b''
+ mocked_head.return_value.status_code = 200
+
+ call_command('import', 'geotrek.trekking.tests.test_parsers.TestGeotrekTrekParser', verbosity=0)
+ self.assertEqual(Trek.objects.count(), 5)
+ trek = Trek.objects.all().first()
+ self.assertEqual(trek.name, "Boucle du Pic des Trois Seigneurs")
+ self.assertEqual(trek.name_en, "Boucle du Pic des Trois Seigneurs")
+ self.assertEqual(str(trek.difficulty), 'Très facile')
+ self.assertEqual(str(trek.practice), 'Cheval')
+ self.assertAlmostEqual(trek.geom[0][0], 569946.9850365581, places=5)
+ self.assertAlmostEqual(trek.geom[0][1], 6190964.893167565, places=5)
+ self.assertEqual(trek.children.first().name, "Foo")
+ self.assertEqual(trek.labels.count(), 3)
+ self.assertEqual(trek.labels.first().name, "Chien autorisé")
+ call_command('import', 'geotrek.trekking.tests.test_parsers.TestGeotrek2TrekParser', verbosity=0)
+ self.assertEqual(Trek.objects.count(), 6)
+
+ @mock.patch('requests.get')
+ @mock.patch('requests.head')
+ def test_children_do_not_exist(self, mocked_head, mocked_get):
+ self.mock_time = 0
+ self.mock_json_order = ['trek_difficulty.json', 'trek_route.json', 'trek_theme.json', 'trek_practice.json',
+ 'trek_accessibility.json', 'trek_network.json', 'trek_label.json', 'trek_ids.json',
+ 'trek.json', 'trek_children_do_not_exist.json', ]
+
+ # Mock GET
+ mocked_get.return_value.status_code = 200
+ mocked_get.return_value.json = self.mock_json
+ mocked_get.return_value.content = b''
+ mocked_head.return_value.status_code = 200
+ output = StringIO()
+ call_command('import', 'geotrek.trekking.tests.test_parsers.TestGeotrekTrekParser', verbosity=2,
+ stdout=output)
+ self.assertIn("One trek has not be generated for Boucle du Pic des Trois Seigneurs : could not find trek with UUID c9567576-2934-43ab-666e-e13d02c671a9,\n", output.getvalue())
+ self.assertIn("Trying to retrieve children for missing trek : could not find trek with UUID b2aea666-5e6e-4daa-a750-7d2ee52d3fe1", output.getvalue())
+
+ @mock.patch('requests.get')
+ @mock.patch('requests.head')
+ def test_wrong_children_error(self, mocked_head, mocked_get):
+ self.mock_time = 0
+ self.mock_json_order = ['trek_difficulty.json', 'trek_route.json', 'trek_theme.json', 'trek_practice.json',
+ 'trek_accessibility.json', 'trek_network.json', 'trek_label.json', 'trek_ids.json',
+ 'trek.json', 'trek_wrong_children.json', ]
+
+ # Mock GET
+ mocked_get.return_value.status_code = 200
+ mocked_get.return_value.json = self.mock_json
+ mocked_get.return_value.content = b''
+ mocked_head.return_value.status_code = 200
+ output = StringIO()
+
+ call_command('import', 'geotrek.trekking.tests.test_parsers.TestGeotrekTrekParser', verbosity=2,
+ stdout=output)
+ self.assertIn("An error occured in children generation : KeyError('steps'", output.getvalue())
+
+ @mock.patch('requests.get')
+ @mock.patch('requests.head')
+ def test_updated(self, mocked_head, mocked_get):
+ self.mock_time = 0
+ self.mock_json_order = ['trek_difficulty.json', 'trek_route.json', 'trek_theme.json', 'trek_practice.json',
+ 'trek_accessibility.json', 'trek_network.json', 'trek_label.json', 'trek_ids.json', 'trek.json',
+ 'trek_children.json', 'trek_difficulty.json', 'trek_route.json', 'trek_theme.json',
+ 'trek_practice.json', 'trek_accessibility.json', 'trek_network.json', 'trek_label.json',
+ 'trek_ids_2.json', 'trek_2.json', 'trek_children.json', ]
+
+ # Mock GET
+ mocked_get.return_value.status_code = 200
+ mocked_get.return_value.json = self.mock_json
+ mocked_get.return_value.content = b''
+ mocked_head.return_value.status_code = 200
+
+ call_command('import', 'geotrek.trekking.tests.test_parsers.TestGeotrekTrekParser', verbosity=0)
+ self.assertEqual(Trek.objects.count(), 5)
+ trek = Trek.objects.all().first()
+ self.assertEqual(trek.name, "Boucle du Pic des Trois Seigneurs")
+ self.assertEqual(trek.name_en, "Boucle du Pic des Trois Seigneurs")
+ self.assertEqual(str(trek.difficulty), 'Très facile')
+ self.assertEqual(str(trek.practice), 'Cheval')
+ self.assertAlmostEqual(trek.geom[0][0], 569946.9850365581, places=5)
+ self.assertAlmostEqual(trek.geom[0][1], 6190964.893167565, places=5)
+ self.assertEqual(trek.children.first().name, "Foo")
+ self.assertEqual(trek.labels.count(), 3)
+ self.assertEqual(trek.labels.first().name, "Chien autorisé")
+ call_command('import', 'geotrek.trekking.tests.test_parsers.TestGeotrekTrekParser', verbosity=0)
+ # Trek 2 is still in ids (trek_ids_2) => it's not removed
+ self.assertEqual(Trek.objects.count(), 2)
+ trek = Trek.objects.all().first()
+ self.assertEqual(trek.name, "Boucle du Pic des Trois Seigneurs")
+
+
+@skipIf(settings.TREKKING_TOPOLOGY_ENABLED, 'Test without dynamic segmentation only')
+class POIGeotrekParserTests(GeotrekParserTestMixin, TestCase):
+ app_label = "trekking"
+
+ @classmethod
+ def setUpTestData(cls):
+ cls.filetype = FileType.objects.create(type="Photographie")
+
+ @mock.patch('requests.get')
+ @mock.patch('requests.head')
+ @override_settings(MODELTRANSLATION_DEFAULT_LANGUAGE="en")
+ def test_create(self, mocked_head, mocked_get):
+ self.mock_time = 0
+ self.mock_json_order = ['poi_type.json', 'poi_ids.json', 'poi.json']
+
+ # Mock GET
+ mocked_get.return_value.status_code = 200
+ mocked_get.return_value.json = self.mock_json
+ mocked_get.return_value.content = b''
+ mocked_head.return_value.status_code = 200
+
+ call_command('import', 'geotrek.trekking.tests.test_parsers.TestGeotrekPOIParser', verbosity=0)
+ self.assertEqual(POI.objects.count(), 2)
+ poi = POI.objects.all().first()
+ self.assertEqual(poi.name, "Peak of the Three Lords")
+ self.assertEqual(poi.name_fr, "Pic des Trois Seigneurs")
+ self.assertEqual(poi.name_en, "Peak of the Three Lords")
+ self.assertEqual(poi.name_it, "Picco dei Tre Signori")
+ self.assertEqual(str(poi.type), 'Peak')
+ self.assertAlmostEqual(poi.geom.x, 572298.7056448072, places=5)
+ self.assertAlmostEqual(poi.geom.y, 6193580.839504813, places=5)
+
+
+@skipIf(settings.TREKKING_TOPOLOGY_ENABLED, 'Test without dynamic segmentation only')
+class ServiceGeotrekParserTests(GeotrekParserTestMixin, TestCase):
+ app_label = "trekking"
+
+ @classmethod
+ def setUpTestData(cls):
+ cls.filetype = FileType.objects.create(type="Photographie")
+
+ @mock.patch('requests.get')
+ @mock.patch('requests.head')
+ @override_settings(MODELTRANSLATION_DEFAULT_LANGUAGE="fr")
+ def test_create(self, mocked_head, mocked_get):
+ self.mock_time = 0
+ self.mock_json_order = ['service_type.json', 'service_ids.json', 'service.json']
+
+ # Mock GET
+ mocked_get.return_value.status_code = 200
+ mocked_get.return_value.json = self.mock_json
+ mocked_get.return_value.content = b''
+ mocked_head.return_value.status_code = 200
+
+ call_command('import', 'geotrek.trekking.tests.test_parsers.TestGeotrekServiceParser', verbosity=0)
+ self.assertEqual(Service.objects.count(), 2)
+ service = Service.objects.all().first()
+ self.assertEqual(str(service.type), 'Eau potable')
+ self.assertAlmostEqual(service.geom.x, 572096.2266745908, places=5)
+ self.assertAlmostEqual(service.geom.y, 6192330.15779677, places=5)
diff --git a/geotrek/trekking/tests/test_views.py b/geotrek/trekking/tests/test_views.py
index fd59e05e8e..07ce885560 100755
--- a/geotrek/trekking/tests/test_views.py
+++ b/geotrek/trekking/tests/test_views.py
@@ -172,10 +172,10 @@ def test_listing_number_queries(self):
self.modelfactory.build_batch(1000)
DistrictFactory.build_batch(10)
- with self.assertNumQueries(6):
+ with self.assertNumQueries(7):
self.client.get(self.model.get_datatablelist_url())
- with self.assertNumQueries(9):
+ with self.assertNumQueries(11):
self.client.get(self.model.get_format_list_url())
def test_list_in_csv(self):
@@ -1479,11 +1479,11 @@ def test_listing_number_queries(self):
DistrictFactory.build_batch(10)
# 1) session, 2) user, 3) user perms, 4) group perms, 5) last modified, 6) list
- with self.assertNumQueries(6):
+ with self.assertNumQueries(7):
self.client.get(self.model.get_datatablelist_url())
# 1) session, 2) user, 3) user perms, 4) group perms, 5) list
- with self.assertNumQueries(5):
+ with self.assertNumQueries(8):
self.client.get(self.model.get_format_list_url())
def test_services_on_treks_do_not_exist(self):
diff --git a/geotrek/trekking/views.py b/geotrek/trekking/views.py
index 0dbd14d200..c9796580a0 100755
--- a/geotrek/trekking/views.py
+++ b/geotrek/trekking/views.py
@@ -500,6 +500,7 @@ class ServiceViewSet(GeotrekMapentityViewSet):
model = Service
serializer_class = ServiceSerializer
geojson_serializer_class = ServiceGeojsonSerializer
+ filterset_class = ServiceFilterSet
def get_queryset(self):
qs = self.model.objects.existing().select_related('type')
|