From 6372da689c18a0c4ecec838f784995c65da1f17d Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sat, 11 Jul 2020 17:05:21 +0200 Subject: [PATCH] better more intuitive course update --- rowers/forms.py | 9 +- rowers/rows.py | 100 +++--- rowers/templates/course_form.html | 4 +- rowers/templates/course_form_update.html | 319 +++++++++++++++++++ rowers/templates/course_replace.html | 2 +- rowers/templates/course_replace_confirm.html | 58 ++++ rowers/templates/menu_racing.html | 2 +- rowers/urls.py | 2 + rowers/views/racesviews.py | 127 ++++++++ rowers/views/statements.py | 1 + 10 files changed, 570 insertions(+), 54 deletions(-) create mode 100644 rowers/templates/course_form_update.html create mode 100644 rowers/templates/course_replace_confirm.html diff --git a/rowers/forms.py b/rowers/forms.py index 440e38cb..27dc4158 100644 --- a/rowers/forms.py +++ b/rowers/forms.py @@ -203,7 +203,7 @@ class ImageForm(forms.Form): # The form used for uploading images class CourseForm(forms.Form): - name = forms.CharField(max_length=150,label='Course Name') + name = forms.CharField(max_length=150,label='Course Name',required=False) file = forms.FileField(required=False, validators=[validate_kml]) notes = forms.CharField(required=False, @@ -214,6 +214,13 @@ class CourseForm(forms.Form): from django.forms.widgets import HiddenInput super(CourseForm, self).__init__(*args, **kwargs) +class CourseConfirmForm(forms.Form): + BOOL_CHOICES = ((True, 'Yes'), (False, 'No')) + doupdate = forms.TypedChoiceField( + initial=False, + coerce=lambda x: x =='True', choices=((False, 'No'), (True, 'Yes')), widget=forms.RadioSelect, + label='Update Course with new markers?') + # The form used for uploading files class StandardsForm(forms.Form): name = forms.CharField(max_length=150,label='Course Name') diff --git a/rowers/rows.py b/rowers/rows.py index fa34cf00..9a70073f 100644 --- a/rowers/rows.py +++ b/rowers/rows.py @@ -15,18 +15,18 @@ import uuid from django.core.exceptions import ValidationError def format_pace_tick(x,pos=None): - minu=int(x/60) - sec=int(x-minu*60.) - sec_str=str(sec).zfill(2) - template='%d:%s' - return template % (minu,sec_str) + minu=int(x/60) + sec=int(x-minu*60.) + sec_str=str(sec).zfill(2) + template='%d:%s' + return template % (minu,sec_str) def format_time_tick(x,pos=None): - hour=int(x/3600) - min=int((x-hour*3600.)/60) - min_str=str(min).zfill(2) - template='%d:%s' - return template % (hour,min_str) + hour=int(x/3600) + min=int((x-hour*3600.)/60) + min_str=str(min).zfill(2) + template='%d:%s' + return template % (hour,min_str) def format_pace(x,pos=None): if isinf(x) or isnan(x): @@ -56,12 +56,12 @@ def format_time(x,pos=None): return str1 def validate_image_extension(value): - import os - ext = os.path.splitext(value.name)[1].lower() - valid_extension = ['.jpg','.jpeg','.png','.gif'] + import os + ext = os.path.splitext(value.name)[1].lower() + valid_extension = ['.jpg','.jpeg','.png','.gif'] - if not ext in valid_extension: - raise ValidationError(u'File not supported') + if not ext in valid_extension: + raise ValidationError(u'File not supported') def validate_file_extension(value): import os @@ -88,53 +88,53 @@ def validate_kml(value): def handle_uploaded_image(i): - from io import StringIO, BytesIO - from PIL import Image, ImageOps, ExifTags - import os - from django.core.files import File - image_str = b'' - for chunk in i.chunks(): - image_str += chunk + from io import StringIO, BytesIO + from PIL import Image, ImageOps, ExifTags + import os + from django.core.files import File + image_str = b'' + for chunk in i.chunks(): + image_str += chunk - imagefile = BytesIO(image_str) + imagefile = BytesIO(image_str) - image = Image.open(i) + image = Image.open(i) - try: - for orientation in ExifTags.TAGS.keys(): - if ExifTags.TAGS[orientation]=='Orientation': + try: + for orientation in ExifTags.TAGS.keys(): + if ExifTags.TAGS[orientation]=='Orientation': break - exif=dict(image._getexif().items()) + exif=dict(image._getexif().items()) - except (AttributeError, KeyError, IndexError): - # cases: image don't have getexif - exif = {'orientation':0} + except (AttributeError, KeyError, IndexError): + # cases: image don't have getexif + exif = {'orientation':0} - if image.mode not in ("L", "RGB"): - image = image.convert("RGB") + if image.mode not in ("L", "RGB"): + image = image.convert("RGB") - basewidth = 600 - wpercent = (basewidth/float(image.size[0])) - hsize = int((float(image.size[1])*float(wpercent))) - image = image.resize((basewidth,hsize), Image.ANTIALIAS) + basewidth = 600 + wpercent = (basewidth/float(image.size[0])) + hsize = int((float(image.size[1])*float(wpercent))) + image = image.resize((basewidth,hsize), Image.ANTIALIAS) - try: - if exif[orientation] == 3: - image=image.rotate(180, expand=True) - elif exif[orientation] == 6: - image=image.rotate(270, expand=True) - elif exif[orientation] == 8: - image=image.rotate(90, expand=True) - except KeyError: - pass + try: + if exif[orientation] == 3: + mage=image.rotate(180, expand=True) + elif exif[orientation] == 6: + image=image.rotate(270, expand=True) + elif exif[orientation] == 8: + image=image.rotate(90, expand=True) + except KeyError: + pass - filename = hashlib.md5(imagefile.getvalue()).hexdigest()+'.jpg' + filename = hashlib.md5(imagefile.getvalue()).hexdigest()+'.jpg' - filename2 = os.path.join('static/plots/',filename) - image.save(filename2,'JPEG') + filename2 = os.path.join('static/plots/',filename) + image.save(filename2,'JPEG') - return filename,filename2 + return filename,filename2 def handle_uploaded_file(f): diff --git a/rowers/templates/course_form.html b/rowers/templates/course_form.html index aa1bb20f..1c5b25b6 100644 --- a/rowers/templates/course_form.html +++ b/rowers/templates/course_form.html @@ -24,7 +24,7 @@

Drag and drop files here

-
+ {% if form.errors %}

Please correct the error{{ form.errors|pluralize }} below. @@ -242,6 +242,8 @@ $("#id_waiting").replaceWith( '

Your upload failed
' ); + console.log(data); + setTimeout(1000); setTimeout(function() { location.reload(); },1000); diff --git a/rowers/templates/course_form_update.html b/rowers/templates/course_form_update.html new file mode 100644 index 00000000..ce63e459 --- /dev/null +++ b/rowers/templates/course_form_update.html @@ -0,0 +1,319 @@ +{% extends "newbase.html" %} +{% load staticfiles %} +{% load rowerfilters %} + +{% block title %}File loading{% endblock %} + +{% block meta %} + + + +{% endblock %} + +{% block main %} +

Upload KML Course File

+ +
    +
  • +
    +

    Drag and drop files here

    +
    +
    + + {% if form.errors %} +

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

    + {% endif %} + + + {{ form.as_table }} +
    + {% csrf_token %} +

    + +

    + +
    +
  • +
  • +

    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 a folder "Courses" under "Temporary Places" or under "My Places"
    • +
    • Create a folder for each Course under "Courses", and for each course:
    • +
    • Create Start polygon
    • +
    • Optional: Create First "must row through" polygon
    • +
    • Optional: Create subsequent "must row through" polygons
    • +
    • Create Finish polygon
    • +
    • Save "Courses" 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 %} + +{% block sidebar %} +{% include 'menu_racing.html' %} +{% endblock %} + + +{% block scripts %} + + + + +{% endblock %} diff --git a/rowers/templates/course_replace.html b/rowers/templates/course_replace.html index 278b472c..17ad92c4 100644 --- a/rowers/templates/course_replace.html +++ b/rowers/templates/course_replace.html @@ -30,7 +30,7 @@ {{ form.as_table }} {% csrf_token %} - +
  • diff --git a/rowers/templates/course_replace_confirm.html b/rowers/templates/course_replace_confirm.html new file mode 100644 index 00000000..b0b010c4 --- /dev/null +++ b/rowers/templates/course_replace_confirm.html @@ -0,0 +1,58 @@ +{% extends "newbase.html" %} +{% load staticfiles %} +{% load rowerfilters %} +{% load leaflet_tags %} + +{% block meta %} +{% leaflet_js %} +{% leaflet_css %} +{% endblock %} + +{% block scripts %} +{% include "monitorjobs.html" %} +{% endblock %} + +{% block title %}{{ course.name }} {% endblock %} +{% block og_title %}{{ course.name }} {% endblock %} +{% block main %} + +

    Replace {{ course.name }}

    + +
      + +
    • +

      + This updates the course {{ course.name }} with the course markers as shown on + the map below. +

      +
      + + + {{ form.as_table }} +
      + {% csrf_token %} + +
      +
    • +
    • +
      + {{ mapdiv|safe }} + + + {{ mapscript|safe }} +
      +
    • +
    • +
      + {{ mapdiv|safe }} + {{ mapscript|safe }} +
      +
    • +
    + + +{% endblock %} + +{% block sidebar %} +{% include 'menu_racing.html' %} +{% endblock %} diff --git a/rowers/templates/menu_racing.html b/rowers/templates/menu_racing.html index 9d115550..4ebd5e26 100644 --- a/rowers/templates/menu_racing.html +++ b/rowers/templates/menu_racing.html @@ -128,7 +128,7 @@
  • {% endif %}
  • - +  Update Markers
  • {% endif %} diff --git a/rowers/urls.py b/rowers/urls.py index 68109a49..6e74b0df 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -205,6 +205,8 @@ urlpatterns = [ re_path(r'^list-courses/$',views.courses_view,name='courses_view'), re_path(r'^list-standards/$',views.standards_view,name='standards_view'), re_path(r'^courses/upload/$',views.course_upload_view,name='course_upload_view'), + re_path(r'^courses/(?P\d+)/update/(?P\d+)/',views.course_update_confirm,name='course_update_confirm'), + re_path(r'^courses/(?P\d+)/update/',views.course_upload_replace_view,name='course_upload_replace_view'), re_path(r'^standards/upload/$',views.standards_upload_view,name='standards_upload_view'), re_path(r'^standards/upload/(?P\d+)/$',views.standards_upload_view,name='standards_upload_view'), re_path(r'^workout/addmanual/(?P\d+)/$',views.addmanual_view,name='addmanual_view'), diff --git a/rowers/views/racesviews.py b/rowers/views/racesviews.py index 8fd04b04..455ab0f5 100644 --- a/rowers/views/racesviews.py +++ b/rowers/views/racesviews.py @@ -8,6 +8,7 @@ from rowsandall_app.settings import SITE_URL from rowers.scoring import * from django.contrib.gis.geoip2 import GeoIP2 +from django import forms # distance of course from lat_lon in km def howfaris(lat_lon,course): @@ -559,6 +560,132 @@ def virtualevent_uploadimage_view(request,id=0): }) +@login_required() +@permission_required('course.change_course',fn=get_course_by_pk,raise_exception=True) +def course_upload_replace_view(request,id=0): + is_ajax = False + if request.is_ajax(): + is_ajax = True + + r = getrower(request.user) + + course = get_object_or_404(GeoCourse,pk=id) + + + if request.method == 'POST': + form = CourseForm(request.POST,request.FILES) + + if form.is_valid(): + f = form.cleaned_data['file'] + notes = form.cleaned_data['notes'] + if f is not None: + filename, path_and_filename = handle_uploaded_file(f) + + cs = courses.kmltocourse(path_and_filename) + os.remove(path_and_filename) + if cs and len(cs) > 1: + messages.info(request,'File contained multiple courses. We use the first one.') + if cs: + course = cs[0] + cname = course['name'] + cnotes = notes+'\n\n'+course['description'] + polygons = course['polygons'] + + course = courses.createcourse(r,cname,polygons,notes=cnotes) + + url = reverse(course_update_confirm, + kwargs = { + 'newid':course.id, + 'id':id, + } + ) + if is_ajax: + return JSONResponse({'result':1,'url':url}) + else: + return HttpResponseRedirect(url) + else: + messages.error(request,"File does not contain a course") + else: + messages.error(request,"No file attached") + else: + messages.error(request,"Form is not valid") + else: + form = CourseForm() + + form.fields['name'].widget = forms.HiddenInput() + + if not is_ajax: + return render(request,'course_form_update.html', + {'form':form, + 'course':course, + 'active':'nav-racing', + }) + else: + return {'result':0} + + +@login_required() +@permission_required('course.change_course',fn=get_course_by_pk,raise_exception=True) +def course_update_confirm(request,id=0,newid=0): + course = get_object_or_404(GeoCourse,pk=id) + course2 = get_object_or_404(GeoCourse,pk=newid) + r = getrower(request.user) + if request.method == 'POST': + form = CourseConfirmForm(request.POST) + if form.is_valid(): + doupdate = form.cleaned_data['doupdate'] + if doupdate: + res = courses.replacecourse(course,course2) + messages.info(request,'All challenges with this course are updated') + url = reverse(course_view, + kwargs = { + 'id':course2.id, + }) + return HttpResponseRedirect(url) + else: + course2.delete() + url = reverse(course_view, + kwargs = { + 'id':course.id, + }) + return HttpResponseRedirect(url) + else: + form = CourseConfirmForm() + # GET call or invalid form + script, div = course_map(course2) + + + breadcrumbs = [ + { + 'url': reverse('virtualevents_view'), + 'name': 'Challenges' + }, + { + 'url': reverse(courses_view), + 'name': 'Courses' + }, + { + 'url': reverse(course_view,kwargs={'id':course.id}), + 'name': course.name + }, + { + 'url': reverse(course_replace_view,kwargs={'id':course.id}), + 'name': 'Replace Markers' + } + ] + + return render(request, + 'course_replace_confirm.html', + {'course':course, + 'form':form, + 'active':'nav-racing', + 'breadcrumbs':breadcrumbs, + 'rower':r, + 'mapdiv':div, + 'mapscript':script, + }) + + # Course upload @login_required() def course_upload_view(request): diff --git a/rowers/views/statements.py b/rowers/views/statements.py index 6003cb4b..2b34446f 100644 --- a/rowers/views/statements.py +++ b/rowers/views/statements.py @@ -64,6 +64,7 @@ from django.contrib.auth import authenticate, login, logout from rowers.forms import ( ForceCurveOptionsForm,HistoForm,TeamMessageForm, LoginForm,DocumentsForm,UploadOptionsForm,ImageForm,CourseForm, + CourseConfirmForm, TeamUploadOptionsForm,WorkFlowLeftPanelForm,WorkFlowMiddlePanelForm, WorkFlowLeftPanelElement,WorkFlowMiddlePanelElement, LandingPageForm,PlannedSessionSelectForm,WorkoutSessionSelectForm,