Skip to content

Commit

Permalink
Merge pull request #2797 from GeotrekCE/display_ref_points_on_course
Browse files Browse the repository at this point in the history
Display ref points on course
  • Loading branch information
babastienne committed Oct 20, 2021
2 parents 705150c + 59de06e commit e305c35
Show file tree
Hide file tree
Showing 23 changed files with 172 additions and 38 deletions.
14 changes: 12 additions & 2 deletions docs/advanced-configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1032,6 +1032,14 @@ Points of reference are enabled on form of treks.

|
::

OUTDOOR_COURSE_POINTS_OF_REFERENCE_ENABLED = True

Points of reference are enabled on form of otudoor courses.

|
::

TOPOLOGY_STATIC_OFFSETS = {'land': -5, 'physical': 0, 'competence': 5, 'signagemanagement': -10, 'workmanagement': 10}
Expand Down Expand Up @@ -1711,7 +1719,8 @@ A (nearly?) exhaustive list of attributes available for display and export as co
"gear",
"duration"
"ratings_description",
"type"
"type",
"points_reference",
],
COLUMNS_LISTS["path_export"] = [
"structure",
Expand Down Expand Up @@ -2106,7 +2115,8 @@ A (nearly?) exhaustive list of attributes available for display and export as co
"gear",
"duration"
"ratings_description",
"type"
"type",
"points_reference",
]

**Hideable form fields**
Expand Down
1 change: 1 addition & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ CHANGELOG
**New features**

- Link an Outdoor Course to multiple parent Sites instead of one
- Added notion of points of reference for Outdoor Courses. (Can be disabled with ``OUTDOOR_COURSE_POINTS_OF_REFERENCE_ENABLED = False``)

**Breaking change**

Expand Down
Binary file not shown.
6 changes: 4 additions & 2 deletions geotrek/api/tests/test_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,8 @@
COURSE_PROPERTIES_JSON_STRUCTURE = sorted([
'advice', 'description', 'eid', 'equipment', 'geometry', 'height', 'id',
'length', 'name', 'ratings', 'ratings_description', 'sites', 'structure',
'type', 'url', 'attachments', 'max_elevation', 'min_elevation', 'parents', 'children', 'duration', 'gear'
'type', 'url', 'attachments', 'max_elevation', 'min_elevation', 'parents',
'points_reference', 'children', 'duration', 'gear'
])

COURSETYPE_PROPERTIES_JSON_STRUCTURE = sorted(['id', 'name', 'practice'])
Expand Down Expand Up @@ -329,7 +330,8 @@ def setUpTestData(cls):
cls.nb_treks += 4 # add parent, 1 child published and treks with a multilinestring/point geom
cls.coursetype = outdoor_factory.CourseTypeFactory()
cls.course = outdoor_factory.CourseFactory(
type=cls.coursetype
type=cls.coursetype,
points_reference=MultiPoint(Point(12, 12))
)
cls.course.parent_sites.set([cls.site])
# create a reference point for distance filter (in 4326, Cahors city)
Expand Down
9 changes: 8 additions & 1 deletion geotrek/api/v2/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -992,6 +992,7 @@ class CourseSerializer(DynamicFieldsMixin, serializers.ModelSerializer):
gear = serializers.SerializerMethodField(read_only=True)
ratings_description = serializers.SerializerMethodField(read_only=True)
sites = serializers.SerializerMethodField(read_only=True)
points_reference = serializers.SerializerMethodField(read_only=True)

def get_gear(self, obj):
return get_translation_or_dict('gear', self, obj)
Expand All @@ -1013,12 +1014,18 @@ def get_sites(self, obj):
sites.append(site.pk)
return sites

def get_points_reference(self, obj):
if not obj.points_reference:
return None
geojson = obj.points_reference.transform(settings.API_SRID, clone=True).geojson
return json.loads(geojson)

class Meta:
model = outdoor_models.Course
fields = (
'id', 'advice', 'attachments', 'children', 'description', 'duration', 'eid',
'equipment', 'gear', 'geometry', 'height', 'length', 'max_elevation',
'min_elevation', 'name', 'parents', 'ratings', 'ratings_description',
'min_elevation', 'name', 'parents', 'points_reference', 'ratings', 'ratings_description',
'sites', 'structure', 'type', 'url',
)

Expand Down
18 changes: 18 additions & 0 deletions geotrek/common/static/common/css/points_reference.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
.leaflet-marker-icon.point-reference {
background-color: red;
border-radius: 50px;
width: 20px !important;
height: 20px !important;
margin-left: -10px;
margin-top: -10px;
color: white;
font-size: 14px;
font-weight: bold;
text-align: center;
}

.leaflet-draw.id_points_reference .leaflet-draw-draw-marker {
background-image: url(../point_reference_widget.png);
background-position: 0px 0px, center;
background-size: cover;
}
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 10 additions & 1 deletion geotrek/outdoor/forms.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from django.conf import settings
from crispy_forms.layout import Div
from django import forms
from django.core.exceptions import ValidationError
Expand Down Expand Up @@ -105,6 +106,7 @@ class CourseForm(CommonForm):

fieldslayout = [
Div(
'points_reference',
'structure',
'name',
'parent_sites',
Expand All @@ -127,7 +129,7 @@ class CourseForm(CommonForm):

class Meta:
fields = ['geom', 'structure', 'name', 'parent_sites', 'type', 'review', 'published', 'description', 'ratings_description', 'duration', 'pois_excluded',
'advice', 'gear', 'equipment', 'height', 'eid', 'children_course', 'hidden_ordered_children']
'points_reference', 'advice', 'gear', 'equipment', 'height', 'eid', 'children_course', 'hidden_ordered_children']
model = Course

def __init__(self, parent_sites=None, *args, **kwargs):
Expand Down Expand Up @@ -160,6 +162,13 @@ def __init__(self, parent_sites=None, *args, **kwargs):
self.fields['pois_excluded'].queryset = self.instance.all_pois.all()
else:
self.fieldslayout[0].remove('pois_excluded')
if not settings.OUTDOOR_COURSE_POINTS_OF_REFERENCE_ENABLED:
self.fields.pop('points_reference')
else:
# Edit points of reference with custom edition JavaScript class
self.fields['points_reference'].label = ''
self.fields['points_reference'].widget.target_map = 'geom'
self.fields['points_reference'].widget.geometry_field_class = 'PointsReferenceField'

def clean_children_course(self):
"""
Expand Down
20 changes: 20 additions & 0 deletions geotrek/outdoor/migrations/0032_course_points_reference.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Generated by Django 3.1.13 on 2021-10-13 13:05

from django.conf import settings
import django.contrib.gis.db.models.fields
from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('outdoor', '0031_course_parent_sites'),
]

operations = [
migrations.AddField(
model_name='course',
name='points_reference',
field=django.contrib.gis.db.models.fields.MultiPointField(blank=True, null=True, spatial_index=False, srid=settings.SRID, verbose_name='Points of reference'),
),
]
2 changes: 2 additions & 0 deletions geotrek/outdoor/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,8 @@ class Course(ZoningPropertiesMixin, AddPropertyMixin, PublishableMixin, MapEntit
verbose_name=_("Type"), null=True, blank=True)
pois_excluded = models.ManyToManyField('trekking.Poi', related_name='excluded_courses', verbose_name=_("Excluded POIs"),
blank=True)
points_reference = models.MultiPointField(verbose_name=_("Points of reference"),
srid=settings.SRID, spatial_index=False, blank=True, null=True)

check_structure_in_forms = False

Expand Down
24 changes: 19 additions & 5 deletions geotrek/outdoor/serializers.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import json

from django.conf import settings
from rest_framework import serializers as rest_serializers
from rest_framework.serializers import ModelSerializer, ReadOnlyField
from rest_framework_gis.fields import GeometryField
from rest_framework_gis.serializers import GeoFeatureModelSerializer

from geotrek.authent.serializers import StructureSerializer
from geotrek.common.serializers import (PublishableSerializerMixin, TranslatedModelSerializer,
LabelSerializer, ThemeSerializer, TargetPortalSerializer,
RecordSourceSerializer)
from geotrek.outdoor.models import Practice, Site, Course
from geotrek.common.serializers import (LabelSerializer,
PublishableSerializerMixin,
RecordSourceSerializer,
TargetPortalSerializer,
ThemeSerializer,
TranslatedModelSerializer)
from geotrek.outdoor.models import Course, Practice, Site
from geotrek.tourism.serializers import InformationDeskSerializer
from geotrek.trekking.serializers import WebLinkSerializer
from geotrek.zoning.serializers import ZoningSerializerMixin
Expand Down Expand Up @@ -57,14 +64,21 @@ class Meta(SiteSerializer.Meta):

class CourseSerializer(PublishableSerializerMixin, ZoningSerializerMixin, TranslatedModelSerializer):
structure = StructureSerializer()
points_reference = rest_serializers.SerializerMethodField()

class Meta:
model = Course
fields = ('id', 'structure', 'name', 'parent_sites', 'description', 'duration', 'advice',
fields = ('id', 'structure', 'name', 'parent_sites', 'description', 'duration', 'advice', 'points_reference',
'equipment', 'height', 'eid', 'ratings', 'ratings_description', 'gear', 'type') + \
ZoningSerializerMixin.Meta.fields + \
PublishableSerializerMixin.Meta.fields

def get_points_reference(self, obj):
if not obj.points_reference:
return None
geojson = obj.points_reference.transform(settings.API_SRID, clone=True).geojson
return json.loads(geojson)


class CourseGeojsonSerializer(GeoFeatureModelSerializer, CourseSerializer):
# Annotated geom field with API_SRID
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
{% extends "mapentity/mapentity_detail_attributes.html" %}
{% load i18n geotrek_tags mapentity_tags outdoor_tags %}

{% block attributes %}
<h3>{% trans "Tree view" %}</h3>
{% include "outdoor/recursive_courses_tree.html" with sites_at_level=course.all_hierarchy_roots original_course=object %}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{% load i18n outdoor_tags %}
{% load i18n geojson_tags outdoor_tags %}

{% is_outdoor_enabled as enabled %}
{% if enabled %}
Expand All @@ -23,6 +23,32 @@
{% course_sites as sites %}
{{ sites|safe }}
</script>
<script type="text/javascript">
$(window).on('detailmap:ready', function (e, data) {
var map = data.map;
//
// Points of reference
(function (map) {
var data = {{ object.points_reference|geojsonfeature|safe }};
L.geoJson(data, {
pointToLayer: (function () {
var counter = 1;
return function (featureData, latlng) {
var icon = L.divIcon({html: counter++,
className: 'point-reference'});
return L.marker(latlng, {
clickable: false,
icon: icon,
zIndexOffset: 9999
}).addTo(map);
};
})()
}).addTo(map);
$('.map-panel').addClass('ref_points_loaded');
})(map);

});
</script>
{% endif %}
<script type="application/json" id="all-ratings-scales">
{% all_ratings_scales as scales %}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{% load static %}
{# loaded by MapEntity, used to inject custom CSS at project level #}
{# Extract reference points styling #}
<link rel="stylesheet" href="{% static "common/css/style.css" %}" />
24 changes: 24 additions & 0 deletions geotrek/outdoor/tests/test_forms.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from django.core.exceptions import ValidationError
from django.test import TestCase
from django.test.utils import override_settings
from geotrek.authent.factories import UserFactory
from geotrek.outdoor.factories import SiteFactory, RatingFactory, CourseFactory
from geotrek.outdoor.forms import SiteForm, CourseForm
Expand Down Expand Up @@ -52,13 +53,36 @@ def test_no_rating_save(self):
form.save()
self.assertQuerysetEqual(self.course.ratings.all(), [])

def test_points_reference(self):
form = CourseForm(user=self.user, instance=self.course, data={
'name_en': 'Course',
'geom': '{"type": "GeometryCollection", "geometries": [{"type": "Point", "coordinates": [3, 45]}]}',
'points_reference': "{\"type\":\"MultiPoint\",\"coordinates\":[[5.82618713378906,43.767622885160975]]}",
'parent_sites': [str(self.course.parent_sites.first().pk)],
})
self.assertTrue(form.is_valid())
created = form.save()
self.assertEqual(len(created.points_reference), 1)

def test_form_init(self):
site = SiteFactory()
course = CourseFactory()
course.parent_sites.set([site])
form = CourseForm(user=self.user, instance=course, parent_sites=site.pk)
self.assertEqual(form.initial['parent_sites'], [site])

@override_settings(OUTDOOR_COURSE_POINTS_OF_REFERENCE_ENABLED=False)
def test_no_points_reference(self):
form = CourseForm(user=self.user, instance=self.course, data={
'name_en': 'Course',
'geom': '{"type": "GeometryCollection", "geometries": [{"type": "Point", "coordinates": [3, 45]}]}',
'points_reference': "{\"type\":\"MultiPoint\",\"coordinates\":[[5.82618713378906,43.767622885160975]]}",
'parent_sites': [str(self.course.parent_sites.first().pk)],
})
self.assertTrue(form.is_valid())
created = form.save()
self.assertIsNone(created.points_reference)


class CourseItinerancyTestCase(TestCase):

Expand Down
1 change: 1 addition & 0 deletions geotrek/outdoor/tests/test_functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ def get_expected_json_attrs(self):
'map_image_url': '/image/course-{}.png'.format(self.obj.pk),
'name': 'Course',
'parent_sites': [self.obj.parent_sites.first().pk],
'points_reference': None,
'printable': '/api/en/courses/{}/course.pdf'.format(self.obj.pk),
'publication_date': '2020-03-17',
'published': True,
Expand Down
19 changes: 17 additions & 2 deletions geotrek/outdoor/tests/test_views.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
from unittest import mock

from django.contrib.gis.geos.collections import MultiPoint
from django.contrib.gis.geos.point import Point
from django.test import TestCase
from django.test.utils import override_settings

from geotrek.common.factories import RecordSourceFactory, TargetPortalFactory
from geotrek.outdoor.factories import SiteFactory, CourseFactory
from geotrek.outdoor.factories import CourseFactory, SiteFactory
from geotrek.tourism.tests.test_views import PNG_BLACK_PIXEL
from unittest import mock


class SiteCustomViewTests(TestCase):
Expand All @@ -19,6 +24,7 @@ def test_api_filters(self):
SiteFactory.create(name='site1', published=False)
SiteFactory.create(name='site2', published=True)
site3 = SiteFactory.create(name='site3', published=True)

site3.source.add(RecordSourceFactory.create(name='source1'))
site3.portal.add(TargetPortalFactory.create(name='portal1'))

Expand Down Expand Up @@ -71,3 +77,12 @@ def test_api_filters(self):
response4 = self.client.get('/api/en/courses.json?portal=portalX')
self.assertEqual(len(response4.json()), 1)
self.assertEqual(response4.json()[0]['name'], 'course2')

@override_settings(API_SRID=2154)
def test_serialize_ref_points(self):
CourseFactory.create(name='course_with_ref_points', published=True, points_reference=MultiPoint(Point(12, 12)))
response = self.client.get('/api/en/courses.json')
self.assertEqual(len(response.json()), 1)
self.assertEqual(response.json()[0]['name'], 'course_with_ref_points')
data = "{'type': 'MultiPoint', 'coordinates': [[12.0, 12.0]]}"
self.assertEqual(str(response.json()[0]['points_reference']), data)
2 changes: 1 addition & 1 deletion geotrek/outdoor/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,5 +202,5 @@ class CourseFormatList(MapEntityFormat, CourseList):
mandatory_columns = ['id']
default_extra_columns = [
'structure', 'name', 'parent_sites', 'description',
'advice', 'equipment', 'eid', 'height', 'ratings'
'advice', 'equipment', 'eid', 'height', 'ratings', 'points_reference'
]
3 changes: 2 additions & 1 deletion geotrek/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,7 @@ def api_bbox(bbox, buffer):
'PLUGINS': {
'geotrek': {'js': ['core/leaflet.lineextremities.js',
'core/leaflet.textpath.js',
'trekking/points_reference.js',
'common/points_reference.js',
'trekking/parking_location.js']},
'topofields': {'js': ['core/geotrek.forms.snap.js',
'core/geotrek.forms.topology.js',
Expand Down Expand Up @@ -553,6 +553,7 @@ def api_bbox(bbox, buffer):
SIGNAGE_LINE_ENABLED = False

TREK_POINTS_OF_REFERENCE_ENABLED = True
OUTDOOR_COURSE_POINTS_OF_REFERENCE_ENABLED = True
TREK_EXPORT_POI_LIST_LIMIT = 14
TREK_EXPORT_INFORMATION_DESK_LIST_LIMIT = 2

Expand Down

0 comments on commit e305c35

Please sign in to comment.