From 3f56bff2fa574fbb047343358f122ff25554af78 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Fri, 16 Feb 2018 14:03:18 +0100 Subject: [PATCH 1/9] import courses from kml --- rowers/admin.py | 13 ++++++++++- rowers/courses.py | 56 ++++++++++++++++++++++++++++++++++++++++++++++- rowers/models.py | 1 + 3 files changed, 68 insertions(+), 2 deletions(-) diff --git a/rowers/admin.py b/rowers/admin.py index e1094553..14599dc5 100644 --- a/rowers/admin.py +++ b/rowers/admin.py @@ -6,6 +6,7 @@ from .models import ( Rower, Workout,GraphImage,FavoriteChart,SiteAnnouncement, Team,TeamInvite,TeamRequest, WorkoutComment,C2WorldClassAgePerformance,PlannedSession, + GeoCourse,GeoPolygon,GeoPoint, ) # Register your models here so you can use them in the Admin module @@ -50,6 +51,16 @@ class PlannedSessionAdmin(admin.ModelAdmin): class GraphImageAdmin(admin.ModelAdmin): list_display = ('creationdatetime','workout','filename') +class GeoPolygonInline(admin.StackedInline): + model = GeoPolygon + + +class GeoCourseAdmin(admin.ModelAdmin): + list_display = ('manager','name') + inlines = (GeoPolygonInline,) + + + admin.site.unregister(User) admin.site.register(User,UserAdmin) admin.site.register(Workout,WorkoutAdmin) @@ -63,4 +74,4 @@ admin.site.register(WorkoutComment,WorkoutCommentAdmin) admin.site.register(C2WorldClassAgePerformance, C2WorldClassAgePerformanceAdmin) admin.site.register(PlannedSession,PlannedSessionAdmin) - +admin.site.register(GeoCourse, GeoCourseAdmin) diff --git a/rowers/courses.py b/rowers/courses.py index 2d35363c..71ccaec1 100644 --- a/rowers/courses.py +++ b/rowers/courses.py @@ -12,6 +12,9 @@ from django.conf import settings from utils import myqueue from matplotlib import path +import xml.etree.ElementTree as et + +ns = {'opengis': 'http://www.opengis.net/kml/2.2'} import django_rq queue = django_rq.get_queue('default') @@ -44,7 +47,7 @@ def polygon_to_path(polygon): def coordinate_in_polygon(latitude,longitude, polygon): p = polygon_to_path(polygon) - retun p.contains_points([(latitude,longitude)])[0] + return p.contains_points([(latitude,longitude)])[0] @@ -71,3 +74,54 @@ def time_in_polygon(df,polygon,maxmin='max'): return time + + +def kmltocourse(f): + doc = et.parse(f) + polygonpms = doc.findall('.//opengis:Placemark[opengis:Polygon]',ns) + polygons = [] + for pm in polygonpms: + name = pm.findall('.//opengis:name',ns)[0].text + coordinates = pm.findall('.//opengis:coordinates',ns) + if coordinates: + cc = coordinates[0].text + else: + cc = '' + + pointstring = cc.split() + + points = [] + for s in pointstring: + coordinates = s.split(',') + points.append({ + 'longitude':float(coordinates[0]), + 'latitude':float(coordinates[1]), + }) + + polygons.append({ + 'name':name, + 'points':points + }) + + return polygons + +def createcourse(manager,name,polygons): + c = GeoCourse(manager=manager,name=name) + c.save() + + i = 0 + for p in polygons: + pp = GeoPolygon(course=c,order_in_course=i,name=p['name']) + pp.save() + j = 0 + for point in p['points']: + obj = GeoPoint( + latitude = point['latitude'], + longitude = point['longitude'], + polygon = pp, + order_in_poly = j + ) + obj.save() + j += 1 + i += 1 + diff --git a/rowers/models.py b/rowers/models.py index 6d380cc2..f90131f2 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -664,6 +664,7 @@ class GeoCourse(models.Model): class GeoPolygon(models.Model): + name = models.CharField(max_length=150,blank=True) course = models.ForeignKey(GeoCourse, blank=True) order_in_course = models.IntegerField(default=0) From 6ec66da974fa381980fdee85891c298c4ce2e56f Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sun, 18 Feb 2018 13:48:37 +0100 Subject: [PATCH 2/9] small cosmetic bug fix --- rowers/templates/plannedsessions_multiclone_select.html | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/rowers/templates/plannedsessions_multiclone_select.html b/rowers/templates/plannedsessions_multiclone_select.html index 773afaa0..c9624212 100644 --- a/rowers/templates/plannedsessions_multiclone_select.html +++ b/rowers/templates/plannedsessions_multiclone_select.html @@ -68,12 +68,8 @@
{% include "planningbuttons.html" %}
-
- {% if theteam %} -

Coach Overview. Team {{ theteam.name }}

- {% else %} -

Coach Overview

- {% endif %} +
+

Clone Multiple Sessions

+ + +
+ {% if rankingonly and not team %} + + {% elif not team %} + + {% endif %} + +

 

+ {% if team %} +
+ {% else %} + + {% endif %} +
+ +
+
+ +
+
+
+
+ + {% if courses.has_previous %} + {% if request.GET.q %} + < + {% else %} + < + {% endif %} + {% endif %} + + + Page {{ courses.number }} of {{ courses.paginator.num_pages }}. + + + {% if courses.has_next %} + {% if request.GET.q %} + > + {% else %} + > + {% endif %} + {% endif %} + + + {% endblock %} diff --git a/rowers/urls.py b/rowers/urls.py index e2772640..e980ffd8 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -143,6 +143,7 @@ urlpatterns = [ url(r'^u/(?P\d+)/list-workouts/(?P\w+.*)/(?P\w+.*)$',views.workouts_view), url(r'^list-workouts/(?P\w+.*)/(?P\w+.*)$',views.workouts_view), url(r'^list-workouts/$',views.workouts_view), + url(r'^list-courses/$',views.courses_view), url(r'^addmanual/$',views.addmanual_view), url(r'^team-compare-select/team/(?P\d+)/(?P\w+.*)/(?P\w+.*)$',views.team_comparison_select), url(r'^team-compare-select/team/(?P\d+)/$',views.team_comparison_select), diff --git a/rowers/views.py b/rowers/views.py index 16d1b947..f57602a0 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -6045,6 +6045,19 @@ def boxplot_view(request,userid=0, }) +# List Courses +@login_required() +def courses_view(request): + r = getrower(request.user) + + courses = GeoCourse.objects.all().order_by("country") + + return render(request,'list_courses.html', + {'courses':courses, + 'rower':r, + }) + + # List Workouts @login_required() def workouts_view(request,message='',successmessage='', From 78c488efe75e42729963e94b16c0c34a3863a8a7 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Mon, 19 Feb 2018 17:28:59 +0100 Subject: [PATCH 5/9] list course view --- rowers/courses.py | 55 +++++++++++- rowers/interactiveplots.py | 127 ++++++++++++++++++++++++++- rowers/templates/.#workout_view.html | 1 + rowers/templates/course_view.html | 24 +++++ rowers/templates/list_courses.html | 10 ++- rowers/urls.py | 2 + rowers/views.py | 20 ++++- 7 files changed, 234 insertions(+), 5 deletions(-) create mode 100644 rowers/templates/.#workout_view.html create mode 100644 rowers/templates/course_view.html diff --git a/rowers/courses.py b/rowers/courses.py index 34a7b379..25a23d11 100644 --- a/rowers/courses.py +++ b/rowers/courses.py @@ -14,6 +14,8 @@ from utils import myqueue from matplotlib import path import xml.etree.ElementTree as et +import pandas as pd + ns = {'opengis': 'http://www.opengis.net/kml/2.2'} import django_rq @@ -34,8 +36,59 @@ class InvalidTrajectoryError(Exception): def __str__(self): return repr(self.value) +def polygon_coord_center(polygon): + + points = GeoPoint.objects.filter(polygon=polygon).order_by("order_in_poly") + + latitudes = pd.Series([p.latitude for p in points]) + longitudes = pd.Series([p.longitude for p in points]) + + return latitudes.mean(), longitudes.mean() + +def course_coord_center(course): + + polygons = GeoPolygon.objects.filter(course=course).order_by("order_in_course") + + latitudes = [] + longitudes = [] + + for p in polygons: + latitude,longitude = polygon_coord_center(p) + latitudes.append(latitude) + longitudes.append(longitude) + + latitude = pd.Series(latitudes).median() + longitude = pd.Series(longitudes).median() + + coordinates = pd.DataFrame({ + 'latitude':latitudes, + 'longitude':longitudes, + }) + + return latitude,longitude,coordinates + +def course_coord_maxmin(course): + + polygons = GeoPolygon.objects.filter(course=course).order_by("order_in_course") + + latitudes = [] + longitudes = [] + + for p in polygons: + latitude,longitude = polygon_coord_center(p) + latitudes.append(latitude) + longitudes.append(longitude) + + lat_min = pd.Series(latitudes).min() + lat_max = pd.Series(latitudes).max() + long_min = pd.Series(longitudes).min() + long_max = pd.Series(longitudes).max() + + + return lat_min,lat_max,long_min,long_max + def polygon_to_path(polygon): - points = GeoPoint.objects.filter(polygon==polygon).order_by(order_in_polygon) + points = GeoPoint.objects.filter(polygon==polygon).order_by("order_in_polygon") s = [] for point in points: s.append([point.latitude,point.longitude]) diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index cbe08d1e..9ec4834e 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -1,5 +1,8 @@ import colorsys -from rowers.models import Workout, User, Rower, WorkoutForm,RowerForm,GraphImage +from rowers.models import ( + Workout, User, Rower, WorkoutForm,RowerForm, + GraphImage,GeoPolygon,GeoCourse,GeoPoint + ) from rowingdata import rower as rrower from rowingdata import main as rmain from rowingdata import cumcpdata,histodata @@ -35,6 +38,10 @@ from bokeh.core.properties import value from collections import OrderedDict from django.conf import settings +from courses import ( + course_coord_center,course_coord_maxmin, + ) + import datetime import math import numpy as np @@ -707,6 +714,124 @@ def interactive_histoall(theworkouts): script, div = components(plot) return [script,div] +def course_map(course): + latmean,lonmean,coordinates = course_coord_center(course) + lat_min, lat_max, long_min, long_max = course_coord_maxmin(course) + + scoordinates = "[" + + for index,row in coordinates.iterrows(): + scoordinates += """[{x},{y}], + """.format( + x=row['latitude'], + y=row['longitude'] + ) + + scoordinates +="]" + + polygons = GeoPolygon.objects.filter(course=course).order_by("order_in_course") + + + pcoordinates = """[ + """ + + for p in polygons: + pcoordinates += """[ + [""" + + points = GeoPoint.objects.filter(polygon=p).order_by("order_in_poly") + + for pt in points: + pcoordinates += "[{x},{y}],".format( + x = pt.latitude, + y = pt.longitude + ) + + # remove last comma + pcoordinates = pcoordinates[:-1] + pcoordinates += """] + ], + """ + + pcoordinates += """ + ]""" + + + print pcoordinates + + script = """ + + """.format( + latmean=latmean, + lonmean=lonmean, + scoordinates=scoordinates, + pcoordinates=pcoordinates + ) + + div = """ +

 

+ """ + + return script,div def leaflet_chart(lat,lon,name=""): if lat.empty or lon.empty: diff --git a/rowers/templates/.#workout_view.html b/rowers/templates/.#workout_view.html new file mode 100644 index 00000000..e5eeb942 --- /dev/null +++ b/rowers/templates/.#workout_view.html @@ -0,0 +1 @@ +E408191@CZ27LT9RCGN72.9092:1519040360 \ No newline at end of file diff --git a/rowers/templates/course_view.html b/rowers/templates/course_view.html new file mode 100644 index 00000000..68884203 --- /dev/null +++ b/rowers/templates/course_view.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} +{% load staticfiles %} +{% load rowerfilters %} +{% block scripts %} +{% include "monitorjobs.html" %} +{% endblock %} + +{% block title %}{{ course.name }} {% endblock %} +{% block og_title %}{{ course.name }} {% endblock %} +{% block content %} +
+ + +

{{ course.name }}

+ + {{ mapdiv|safe }} + + + {{ mapscript|safe }} + + +
+ +{% endblock %} diff --git a/rowers/templates/list_courses.html b/rowers/templates/list_courses.html index 4f9db35e..54795b69 100644 --- a/rowers/templates/list_courses.html +++ b/rowers/templates/list_courses.html @@ -32,8 +32,14 @@ {% for course in courses %} - {{ course.country }} - {{ course.name }} + {{ course.country }} + + {% if course.manager.user == user %} + {{ course.name }} + {% else %} + {{ course.name }} + {% endif %} + diff --git a/rowers/urls.py b/rowers/urls.py index e980ffd8..21a12f56 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -485,6 +485,8 @@ urlpatterns = [ url(r'^sessions/rower/(?P\d+)$',views.plannedsessions_view), url(r'^sessions/(?P[\w\ ]+.*)/rower/(?P\d+)$',views.plannedsessions_view), url(r'^sessions/(?P[\w\ ]+.*)$',views.plannedsessions_view), + url(r'^courses/(?P\d+)/edit$',views.course_edit_view, + name='course_edit_view'), ] if settings.DEBUG: diff --git a/rowers/views.py b/rowers/views.py index 432dd9c8..fdc9fd43 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -8404,7 +8404,25 @@ def workout_comment_view(request,id=0): 'comments':comments, 'form':form, }) - + +@login_required() +def course_edit_view(request,id=0): + try: + course = GeoCourse.objects.get(id=id) + except GeoCourse.DoesNotExist: + return Http404("Course doesn't exist") + + + script,div = course_map(course) + + return render(request, 'course_view.html', + { + 'course':course, + 'mapscript':script, + 'mapdiv':div, + } + ) + # The basic edit page @login_required() From 323843d64a244d53ec90778c094c9758203614b6 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Mon, 19 Feb 2018 17:29:42 +0100 Subject: [PATCH 6/9] remove temp files --- rowers/templates/.#workout_view.html | 1 - 1 file changed, 1 deletion(-) delete mode 100644 rowers/templates/.#workout_view.html diff --git a/rowers/templates/.#workout_view.html b/rowers/templates/.#workout_view.html deleted file mode 100644 index e5eeb942..00000000 --- a/rowers/templates/.#workout_view.html +++ /dev/null @@ -1 +0,0 @@ -E408191@CZ27LT9RCGN72.9092:1519040360 \ No newline at end of file From e361825889e8e122fc3b8fa78f83f5e079bdcbe7 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Tue, 20 Feb 2018 10:45:40 +0100 Subject: [PATCH 7/9] bug fix addition error checking on METAR --- rowers/weather.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/rowers/weather.py b/rowers/weather.py index d01d0fed..8f5ba3f5 100644 --- a/rowers/weather.py +++ b/rowers/weather.py @@ -45,7 +45,12 @@ def get_metar_data(airportcode,unixtime): url += str(unixtime+3600) url += "&stationString="+airportcode - s = requests.get(url) + try: + s = requests.get(url) + except: + message = 'Failed to download METAR data' + return [0,0,message,'',''] + if s.ok: doc = etree.fromstring(s.content) From d3e3925625bcf99295af61b4d9ffa930db6beaa5 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Tue, 20 Feb 2018 22:50:34 +0100 Subject: [PATCH 8/9] adding courses upload and course delete --- rowers/courses.py | 9 +- rowers/forms.py | 18 ++- rowers/interactiveplots.py | 1 - rowers/models.py | 2 +- rowers/rows.py | 7 + rowers/templates/course_form.html | 252 +++++++++++++++++++++++++++++ rowers/templates/course_view.html | 8 +- rowers/templates/list_courses.html | 42 +---- rowers/urls.py | 2 + rowers/views.py | 88 +++++++++- 10 files changed, 382 insertions(+), 47 deletions(-) create mode 100644 rowers/templates/course_form.html diff --git a/rowers/courses.py b/rowers/courses.py index 25a23d11..060ab246 100644 --- a/rowers/courses.py +++ b/rowers/courses.py @@ -162,8 +162,10 @@ from geopy.geocoders import Nominatim geolocator = Nominatim() -def createcourse(manager,name,polygons): - c = GeoCourse(manager=manager,name=name) +def createcourse( + manager,name,polygons,notes=''): + + c = GeoCourse(manager=manager,name=name,notes=notes) c.save() i = 0 @@ -186,4 +188,5 @@ def createcourse(manager,name,polygons): obj.save() j += 1 i += 1 - + + return c diff --git a/rowers/forms.py b/rowers/forms.py index 3d5281d5..c5ffa552 100644 --- a/rowers/forms.py +++ b/rowers/forms.py @@ -1,7 +1,7 @@ from django import forms from django.contrib.admin.widgets import FilteredSelectMultiple from rowers.models import Workout,Rower,Team,PlannedSession -from rowers.rows import validate_file_extension,must_be_csv,validate_image_extension +from rowers.rows import validate_file_extension,must_be_csv,validate_image_extension,validate_kml from django.contrib.auth.forms import UserCreationForm from django.contrib.auth.models import User from django.contrib.admin.widgets import AdminDateWidget @@ -55,7 +55,21 @@ class ImageForm(forms.Form): from django.forms.widgets import HiddenInput super(ImageForm, self).__init__(*args, **kwargs) - + +# The form used for uploading images +class CourseForm(forms.Form): + name = forms.CharField(max_length=150,label='Course Name') + file = forms.FileField(required=False, + validators=[validate_kml]) + notes = forms.CharField(required=False, + max_length=200,label='Course Notes', + widget=forms.Textarea) + + def __init__(self, *args, **kwargs): + from django.forms.widgets import HiddenInput + super(CourseForm, self).__init__(*args, **kwargs) + + # The form used for uploading files class DocumentsForm(forms.Form): title = forms.CharField(required=False) diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index 9ec4834e..cf9bbdd1 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -757,7 +757,6 @@ def course_map(course): ]""" - print pcoordinates script = """ + + +{% endblock %} + +{% block content %} + +
+
+
+

Upload KML Course File

+ {% if form.errors %} +

+ Please correct the error{{ form.errors|pluralize }} below. +

+ {% endif %} + + + {{ form.as_table }} +
+ {% csrf_token %} +
+ +
+
+ + + + +
+
+{% endblock %} + + {% block scripts %} + + + + {% endblock %} diff --git a/rowers/templates/course_view.html b/rowers/templates/course_view.html index 68884203..88c74081 100644 --- a/rowers/templates/course_view.html +++ b/rowers/templates/course_view.html @@ -9,7 +9,13 @@ {% block og_title %}{{ course.name }} {% endblock %} {% block content %}
- + {% if nosessions %} +
+ Delete +
+ {% endif %} +
+

{{ course.name }}

diff --git a/rowers/templates/list_courses.html b/rowers/templates/list_courses.html index 54795b69..84255ebd 100644 --- a/rowers/templates/list_courses.html +++ b/rowers/templates/list_courses.html @@ -85,26 +85,12 @@
- {% if rankingonly and not team %} - {% elif not team %} - - {% endif %} -

 

- {% if team %} -
- {% else %} - - {% endif %}
@@ -116,26 +102,6 @@
- - {% if courses.has_previous %} - {% if request.GET.q %} - < - {% else %} - < - {% endif %} - {% endif %} - - - Page {{ courses.number }} of {{ courses.paginator.num_pages }}. - - - {% if courses.has_next %} - {% if request.GET.q %} - > - {% else %} - > - {% endif %} - {% endif %} - - +   +
{% endblock %} diff --git a/rowers/urls.py b/rowers/urls.py index 21a12f56..acf5dfa2 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -144,6 +144,7 @@ urlpatterns = [ url(r'^list-workouts/(?P\w+.*)/(?P\w+.*)$',views.workouts_view), url(r'^list-workouts/$',views.workouts_view), url(r'^list-courses/$',views.courses_view), + url(r'^courses/upload$',views.course_upload_view), url(r'^addmanual/$',views.addmanual_view), url(r'^team-compare-select/team/(?P\d+)/(?P\w+.*)/(?P\w+.*)$',views.team_comparison_select), url(r'^team-compare-select/team/(?P\d+)/$',views.team_comparison_select), @@ -487,6 +488,7 @@ urlpatterns = [ url(r'^sessions/(?P[\w\ ]+.*)$',views.plannedsessions_view), url(r'^courses/(?P\d+)/edit$',views.course_edit_view, name='course_edit_view'), + url(r'^courses/(?P\d+)/delete$',views.course_delete_view), ] if settings.DEBUG: diff --git a/rowers/views.py b/rowers/views.py index fdc9fd43..c9578471 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -27,7 +27,7 @@ from django.http import ( ) from django.contrib.auth import authenticate, login, logout from rowers.forms import ( - LoginForm,DocumentsForm,UploadOptionsForm,ImageForm, + LoginForm,DocumentsForm,UploadOptionsForm,ImageForm,CourseForm, TeamUploadOptionsForm,WorkFlowLeftPanelForm,WorkFlowMiddlePanelForm, WorkFlowLeftPanelElement,WorkFlowMiddlePanelElement, LandingPageForm,PlannedSessionSelectForm,WorkoutSessionSelectForm, @@ -69,6 +69,7 @@ from rowers.models import ( ) from rowers.metrics import rowingmetrics,defaultfavoritecharts from rowers import metrics +from rowers import courses import rowers.uploads as uploads from django.forms.formsets import formset_factory from django.forms import modelformset_factory @@ -6052,6 +6053,8 @@ def courses_view(request): courses = GeoCourse.objects.all().order_by("country") + # add search processing + return render(request,'list_courses.html', {'courses':courses, 'rower':r, @@ -8405,6 +8408,28 @@ def workout_comment_view(request,id=0): 'form':form, }) +@login_required() +def course_delete_view(request,id=0): + try: + course = GeoCourse.objects.get(id=id) + except GeoCourse.DoesNotExist: + return Http404("Course doesn't exist") + + r = getrower(request.user) + + if course.manager != r: + raise PermissionDenied("Access denied") + + ps = PlannedSession.objects.filter(course=course) + nosessions = len(ps) == 0 + + if nosessions: + course.delete() + + url = reverse(courses_view) + + return HttpResponseRedirect(url) + @login_required() def course_edit_view(request,id=0): try: @@ -8412,7 +8437,15 @@ def course_edit_view(request,id=0): except GeoCourse.DoesNotExist: return Http404("Course doesn't exist") + r = getrower(request.user) + + if course.manager != r: + raise PermissionDenied("Access denied") + ps = PlannedSession.objects.filter(course=course) + nosessions = len(ps) == 0 + + script,div = course_map(course) return render(request, 'course_view.html', @@ -8420,6 +8453,7 @@ def course_edit_view(request,id=0): 'course':course, 'mapscript':script, 'mapdiv':div, + 'nosessions':nosessions, } ) @@ -8892,6 +8926,58 @@ def workout_uploadimage_view(request,id): else: return {'result':0} +# Image upload +@login_required() +def course_upload_view(request): + is_ajax = False + if request.is_ajax(): + is_ajax = True + + r = getrower(request.user) + + if request.method == 'POST': + form = CourseForm(request.POST,request.FILES) + + if form.is_valid(): + f = form.cleaned_data['file'] + name = form.cleaned_data['name'] + notes = form.cleaned_data['notes'] + if f is not None: + filename,path_and_filename = handle_uploaded_file(f) + + polygons = courses.kmltocourse(path_and_filename) + + course = courses.createcourse(r,name,polygons,notes=notes) + os.remove(path_and_filename) + + url = reverse(courses_view) + if is_ajax: + return JSONResponse({'result':1,'url':url}) + else: + return HttpResponseRedirect(url) + else: + messages.error(request,'Something went wrong - no file attached') + url = reverse(course_upload_view) + + if is_ajax: + return JSONResponse({'result':0,'url':0}) + else: + return HttpResponseRedirect(url) + else: + messages.error(request,'Form is not valid') + return render(request,'course_form.html', + {'form':form, + }) + + else: + if not is_ajax: + form = CourseForm() + return render(request,'course_form.html', + {'form':form, + }) + else: + return {'result':0} + # Generic chart creation From 289d3fc2cc3227af358b0257a1acfebf19494f0f Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 21 Feb 2018 12:57:46 +0100 Subject: [PATCH 9/9] courses functionality --- rowers/courses.py | 41 +++++-- rowers/interactiveplots.py | 20 +++- rowers/models.py | 16 ++- rowers/templates/course_edit_view.html | 54 +++++++++ rowers/templates/course_form.html | 2 +- rowers/templates/course_view.html | 32 +++++- rowers/templates/list_courses.html | 121 ++++++++++++++------- rowers/templates/plannedsessioncreate.html | 4 + rowers/templates/plannedsessionedit.html | 3 + rowers/templates/plannedsessions.html | 4 +- rowers/templates/plannedsessionview.html | 17 +++ rowers/urls.py | 1 + rowers/views.py | 69 +++++++++++- 13 files changed, 322 insertions(+), 62 deletions(-) create mode 100644 rowers/templates/course_edit_view.html diff --git a/rowers/courses.py b/rowers/courses.py index 060ab246..9857b230 100644 --- a/rowers/courses.py +++ b/rowers/courses.py @@ -9,7 +9,6 @@ from django.db import IntegrityError import uuid from django.conf import settings -from utils import myqueue from matplotlib import path import xml.etree.ElementTree as et @@ -18,10 +17,6 @@ import pandas as pd ns = {'opengis': 'http://www.opengis.net/kml/2.2'} -import django_rq -queue = django_rq.get_queue('default') -queuelow = django_rq.get_queue('low') -queuehigh = django_rq.get_queue('low') from rowers.models import ( Rower, Workout, @@ -128,10 +123,27 @@ def time_in_polygon(df,polygon,maxmin='max'): return time +def crewnerdcourse(doc): + courses = [] + for course in doc: + name = course.findall('.//opengis:name',ns)[0].text + try: + description = course.findall('.//opengis:description',ns)[0].text + except IndexError: + description = '' + + polygonpms = course.findall('.//opengis:Placemark[opengis:Polygon]',ns) + polygons = get_polygons(polygonpms) -def kmltocourse(f): - doc = et.parse(f) - polygonpms = doc.findall('.//opengis:Placemark[opengis:Polygon]',ns) + courses.append({ + 'name':name, + 'description':description, + 'polygons':polygons + }) + + return courses + +def get_polygons(polygonpms): polygons = [] for pm in polygonpms: name = pm.findall('.//opengis:name',ns)[0].text @@ -157,6 +169,19 @@ def kmltocourse(f): }) return polygons + + +def kmltocourse(f): + doc = et.parse(f) + courses = doc.findall('.//opengis:Folder[opengis:Placemark]',ns) + + if courses: + return crewnerdcourse(courses) + + polygonpms = doc.findall('.//opengis:Placemark[opengis:Polygon]',ns) + + return get_polygons(polygonpms) + from geopy.geocoders import Nominatim geolocator = Nominatim() diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index cf9bbdd1..6b3ae2e4 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -40,6 +40,7 @@ from django.conf import settings from courses import ( course_coord_center,course_coord_maxmin, + polygon_coord_center ) import datetime @@ -731,6 +732,20 @@ def course_map(course): polygons = GeoPolygon.objects.filter(course=course).order_by("order_in_course") + plabels = '' + + for p in polygons: + coords = polygon_coord_center(p) + + plabels += """ + var marker = L.marker([{latbegin}, {longbegin}]).addTo(mymap); + marker.bindPopup("{name}"); + + """.format( + latbegin = coords[0], + longbegin = coords[1], + name = p.name + ) pcoordinates = """[ """ @@ -818,12 +833,15 @@ def course_map(course): var platlongs = {pcoordinates} var polygons = L.polygon(platlongs, {{color:'blue'}}).addTo(mymap) + {plabels} + """.format( latmean=latmean, lonmean=lonmean, scoordinates=scoordinates, - pcoordinates=pcoordinates + pcoordinates=pcoordinates, + plabels = plabels ) div = """ diff --git a/rowers/models.py b/rowers/models.py index 2c3f7e08..ab4f613f 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -667,8 +667,22 @@ class GeoCourse(models.Model): notes = models.CharField(blank=True,max_length=200,verbose_name='Course Notes') def __unicode__(self): name = self.name - return u'{name}'.format(name=name) + country = self.country + + return u'{country} - {name}'.format( + name=name, + country=country + ) +class GeoCourseEditForm(ModelForm): + class Meta: + model = GeoCourse + fields = ['name','notes'] + + widgets = { + 'notes': forms.Textarea, + } + class GeoPolygon(models.Model): name = models.CharField(max_length=150,blank=True) course = models.ForeignKey(GeoCourse, blank=True) diff --git a/rowers/templates/course_edit_view.html b/rowers/templates/course_edit_view.html new file mode 100644 index 00000000..64c46e59 --- /dev/null +++ b/rowers/templates/course_edit_view.html @@ -0,0 +1,54 @@ +{% extends "base.html" %} +{% load staticfiles %} +{% load rowerfilters %} +{% block scripts %} +{% include "monitorjobs.html" %} +{% endblock %} + +{% block title %}{{ course.name }} {% endblock %} +{% block og_title %}{{ course.name }} {% endblock %} +{% block content %} +
+
+ {% if nosessions %} + Delete + {% else %} +   + {% endif %} +
+
+ {% if course.manager == rower %} + View Course + {% else %} +   + {% endif %} +
+
+ Courses +
+
+
+ +

{{ course.name }}

+ +
+
+ + {{ form.as_table }} +
+ {% csrf_token %} +
+ +
+
+
+
+ {{ mapdiv|safe }} + + + {{ mapscript|safe }} +
+ +
+ +{% endblock %} diff --git a/rowers/templates/course_form.html b/rowers/templates/course_form.html index 957e181d..de1d0db7 100644 --- a/rowers/templates/course_form.html +++ b/rowers/templates/course_form.html @@ -44,7 +44,7 @@ -
+ {% endblock %} {% block scripts %} diff --git a/rowers/templates/course_view.html b/rowers/templates/course_view.html index 88c74081..89b8b03d 100644 --- a/rowers/templates/course_view.html +++ b/rowers/templates/course_view.html @@ -9,21 +9,47 @@ {% block og_title %}{{ course.name }} {% endblock %} {% block content %}
- {% if nosessions %}
+ {% if nosessions %} Delete -
+ {% else %} +   {% endif %} +
+
+ {% if course.manager == rower %} + Edit + {% else %} +   + {% endif %} +
+
+ Courses +

{{ course.name }}

+
+ + + + + + + + + + +
Name{{ course.name }}
Country{{ course.country }}
Notes{{ course.notes }}
+
+
{{ mapdiv|safe }} {{ mapscript|safe }} - +
diff --git a/rowers/templates/list_courses.html b/rowers/templates/list_courses.html index 84255ebd..6f486448 100644 --- a/rowers/templates/list_courses.html +++ b/rowers/templates/list_courses.html @@ -20,8 +20,8 @@
-

Courses

- +

Courses

+ {% if courses %} @@ -29,27 +29,52 @@ - + {% for course in courses %} - - + + + - + {% endfor %}
Country Name
{{ course.country }} - {% if course.manager.user == user %} - {{ course.name }} - {% else %} - {{ course.name }} - {% endif %} -
{{ course.country }} + {% if course.manager.user == user %} + {{ course.name }} + {% else %} + {{ course.name }} + {% endif %} +
{% else %}

No courses found

{% endif %} + +
+ +

 

+
+
+ +
+
+ +
+
+
+   +
+ + +
+
@@ -68,40 +93,52 @@ {% endif %}
-

About

-

This site is a beta site, pioneering rowing data visualization and analysis. No warranties. The site's author is - Sander Roosendaal. A Masters rower. - - Read his blog +

How-to

+

+ Courses allow you to mark the start & finish lines of your + test pieces and measure the time spent on the course (as opposed + to the total duration of a workout). This allows you to row and rank + marked courses. + + To create a course, you use Google Earth + to mark the start and finish lines using polygons. The process is identical + to creating custom courses for the + CrewNerd + app. +

-
- - -
+ +

CrewNerd has published a nice video tutorial of the process. + Click here to see the video. The part + we're interested in starts at 2:05. +

+ +

+ In addition to start and finish areas, on rowsandall.com you can add additional + polygons to mark areas that you must pass (in that order). This allows for + courses with turns around buoys, respecting buoy lines, or respecting traffic + patterns on rivers and lakes. +

+ +

+

    +
  • Open Google Earth
  • +
  • Create Start polygon
  • +
  • Optional: Create First "must row through" polygon
  • +
  • Optional: Create subsequent "must row through" polygons
  • +
  • Create Finish polygon
  • +
  • Save Place as KML file
  • +
  • Upload the file to rowsandall.com using the "Add Courses" button
  • +
+

+ +

You are allowed to have multiple courses in one KML file. + Your CrewNerd "courses.kml" file works out of the box

+ +

The site doesn't test for duplicate courses.

-
- -
- -

 

-
-
- -
-
- -
-
-
-   -
{% endblock %} diff --git a/rowers/templates/plannedsessioncreate.html b/rowers/templates/plannedsessioncreate.html index 6c1e8f58..4b3830e8 100644 --- a/rowers/templates/plannedsessioncreate.html +++ b/rowers/templates/plannedsessioncreate.html @@ -57,7 +57,11 @@ {% endfor %} + {% endif %} +
+ Courses +
diff --git a/rowers/templates/plannedsessionedit.html b/rowers/templates/plannedsessionedit.html index 28847759..fe759a50 100644 --- a/rowers/templates/plannedsessionedit.html +++ b/rowers/templates/plannedsessionedit.html @@ -53,6 +53,9 @@
{% endif %} +
+ Courses +
{% endif %} - +
+ Courses +
{% if plannedsessions %}

diff --git a/rowers/templates/plannedsessionview.html b/rowers/templates/plannedsessionview.html index 2ee8c3d6..9234b030 100644 --- a/rowers/templates/plannedsessionview.html +++ b/rowers/templates/plannedsessionview.html @@ -22,6 +22,13 @@   {% endif %}

+
+ {% if plannedsession.sessiontype == 'coursetest' %} + Courses + {% else %} +   + {% endif %} +
@@ -128,6 +135,16 @@
+
+
+ {% if coursescript %} +

Course

+ {{ coursediv|safe }} + + {{ coursescript|safe }} + {% endif %} +
+
diff --git a/rowers/urls.py b/rowers/urls.py index acf5dfa2..27d9f7cf 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -489,6 +489,7 @@ urlpatterns = [ url(r'^courses/(?P\d+)/edit$',views.course_edit_view, name='course_edit_view'), url(r'^courses/(?P\d+)/delete$',views.course_delete_view), + url(r'^courses/(?P\d+)$',views.course_view), ] if settings.DEBUG: diff --git a/rowers/views.py b/rowers/views.py index c9578471..4f7e770f 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -62,7 +62,7 @@ from rowers.models import ( Team,TeamForm,TeamInviteForm,TeamInvite,TeamRequest, WorkoutComment,WorkoutCommentForm,RowerExportForm, CalcAgePerformance,PowerTimeFitnessMetric,PlannedSessionForm, - PlannedSessionFormSmall, + PlannedSessionFormSmall,GeoCourseEditForm, ) from rowers.models import ( FavoriteForm,BaseFavoriteFormSet,SiteAnnouncement,BasePlannedSessionFormSet @@ -6051,9 +6051,20 @@ def boxplot_view(request,userid=0, def courses_view(request): r = getrower(request.user) - courses = GeoCourse.objects.all().order_by("country") + courses = GeoCourse.objects.all().order_by("country","name") # add search processing + query = request.GET.get('q') + if query: + query_list = query.split() + courses = GeoCourse.objects.filter( + reduce(operator.and_, + (Q(name__icontains=q) for q in query_list)) | + reduce(operator.and_, + (Q(country__icontains=q) for q in query_list)) | + reduce(operator.and_, + (Q(notes__icontains=q) for q in query_list)) + ) return render(request,'list_courses.html', {'courses':courses, @@ -8444,7 +8455,40 @@ def course_edit_view(request,id=0): ps = PlannedSession.objects.filter(course=course) nosessions = len(ps) == 0 + + script,div = course_map(course) + if request.method == 'POST': + form = GeoCourseEditForm(request.POST) + if form.is_valid(): + name = form.cleaned_data['name'] + notes = form.cleaned_data['notes'] + + course.name = name + course.notes = notes + course.save() + + form = GeoCourseEditForm(instance=course) + + return render(request, 'course_edit_view.html', + { + 'course':course, + 'mapscript':script, + 'mapdiv':div, + 'nosessions':nosessions, + 'rower':r, + 'form':form, + } + ) + +@login_required() +def course_view(request,id=0): + try: + course = GeoCourse.objects.get(id=id) + except GeoCourse.DoesNotExist: + return Http404("Course doesn't exist") + + r = getrower(request.user) script,div = course_map(course) @@ -8453,7 +8497,8 @@ def course_edit_view(request,id=0): 'course':course, 'mapscript':script, 'mapdiv':div, - 'nosessions':nosessions, + 'nosessions':False, + 'rower':r, } ) @@ -8945,9 +8990,15 @@ def course_upload_view(request): if f is not None: filename,path_and_filename = handle_uploaded_file(f) - polygons = courses.kmltocourse(path_and_filename) + cs = courses.kmltocourse(path_and_filename) - course = courses.createcourse(r,name,polygons,notes=notes) + for course in cs: + cname = name+' - '+course['name'] + cnotes = notes+'\n\n'+course['description'] + polygons = course['polygons'] + + course = courses.createcourse(r,cname,polygons,notes=cnotes) + os.remove(path_and_filename) url = reverse(courses_view) @@ -12571,6 +12622,12 @@ def plannedsession_view(request,id=0,rowerid=0, raise Http404("Planned Session does not exist") + if ps.course: + coursescript,coursediv = course_map(ps.course) + else: + coursescript = '' + coursediv = '' + if ps.manager != request.user and r not in ps.rower.all(): raise PermissionDenied("You do not have access to this session") @@ -12623,6 +12680,8 @@ def plannedsession_view(request,id=0,rowerid=0, 'plannedsession':ps, 'timeperiod':timeperiod, 'ranking':ranking, + 'coursescript': coursescript, + 'coursediv': coursediv } )