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
  1. \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
  2. \r\n
  3. \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
  4. \r\n
  5. Terminer tout droit
  6. \r\n
\r\n
Description 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", + "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\n

Nous 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\n

1998, qui sont éduqués et câlinés depuis tout petit.

\r\n

Partagez le plaisir de randonnée léger et libre avec nous et permettez aux enfants de découvrir

\r\n

une 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\n

Modeste 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 chevalierscristivomers 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\n

Au 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\n

Source 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
  1. \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
  2. \r\n
  3. \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
  4. \r\n
  5. Terminer tout droit
  6. \r\n
\r\n
Description 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

", + "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')