Private
Public Access
1
0

better more intuitive course update

This commit is contained in:
Sander Roosendaal
2020-07-11 17:05:21 +02:00
parent 7b552f8da6
commit 6372da689c
10 changed files with 570 additions and 54 deletions

View File

@@ -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')

View File

@@ -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):

View File

@@ -24,7 +24,7 @@
<p>Drag and drop files here </p>
</div>
<div id="id_drop-files" class="grid_12 alpha drop-files">
<form id="file_form" enctype="multipart/form-data" action="{{ formloc }}" method="post">
<form id="file_form" enctype="multipart/form-data" method="post">
{% if form.errors %}
<p style="color: red;">
Please correct the error{{ form.errors|pluralize }} below.
@@ -242,6 +242,8 @@
$("#id_waiting").replaceWith(
'<div id="id_failed" class="grid_12 alpha message">Your upload failed</div>'
);
console.log(data);
setTimeout(1000);
setTimeout(function() {
location.reload();
},1000);

View File

@@ -0,0 +1,319 @@
{% extends "newbase.html" %}
{% load staticfiles %}
{% load rowerfilters %}
{% block title %}File loading{% endblock %}
{% block meta %}
<script type='text/javascript'
src='https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js'>
</script>
<script type='text/javascript'
src='https://ajax.aspnetcdn.com/ajax/jquery.validate/1.14.0/jquery.validate.min.js'>
</script>
<script>
</script>
{% endblock %}
{% block main %}
<h1>Upload KML Course File</h1>
<ul class="main-content">
<li class="grid_4">
<div id="id_dropregion watermark invisible">
<p>Drag and drop files here </p>
</div>
<div id="id_drop-files" class="grid_12 alpha drop-files">
<form id="file_form" enctype="multipart/form-data" method="post">
{% if form.errors %}
<p style="color: red;">
Please correct the error{{ form.errors|pluralize }} below.
</p>
{% endif %}
<table>
{{ form.as_table }}
</table>
{% csrf_token %}
<p>
<input class="button" type="submit" value="Submit">
</p>
</form>
</div>
</li>
<li class="grid_4">
<h2>How-to</h2>
<p>
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 <a href="https://www.google.com/earth/">Google Earth</a>
to mark the start and finish lines using polygons. The process is identical
to creating custom courses for the
<a href="http://performancephones.com/crewnerd/">CrewNerd</a>
app.
</p>
<p>CrewNerd has published a nice video tutorial of the process.
<a href="https://youtu.be/whhWFmMJbhM">Click here</a> to see the video. The part
we're interested in starts at 2:05.
</p>
<p>
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.
</p>
<p>
<ul>
<li>Open Google Earth</li>
<li>Create a folder "Courses" under "Temporary Places" or under "My Places"</li>
<li>Create a folder for each Course under "Courses", and for each course:</li>
<li>Create Start polygon</li>
<li>Optional: Create First "must row through" polygon</li>
<li>Optional: Create subsequent "must row through" polygons</li>
<li>Create Finish polygon</li>
<li>Save "Courses" as KML file</li>
<li>Upload the file to rowsandall.com using the "Add Courses" button</li>
</ul>
</p>
<p>You are allowed to have multiple courses in one KML file.
Your CrewNerd "courses.kml" file works out of the box</p>
<p>The site doesn't test for duplicate courses.</p>
</li>
</ul>
{% endblock %}
{% block sidebar %}
{% include 'menu_racing.html' %}
{% endblock %}
{% block scripts %}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<script>
var td = new FormData();
var formdatasetok = false;
try {
td.set('aap','noot');
formdatasetok = true;
console.log('FormData.set OK');
}
catch(err) {
console.log('FormData.set not OK');
formdatasetok = false;
}
if (!formdatasetok) {
$("#id_dropregion").remove();
}
if (formdatasetok) {
$(document).ready(function() {
var csrftoken = jQuery("[name=csrfmiddlewaretoken]").val();
console.log("CSRF token",csrftoken);
function csrfSafeMethod(method) {
// these HTTP methods do not require CSRF protection
return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
}
$.ajaxSetup({
beforeSend: function(xhr, settings) {
if (!csrfSafeMethod(settings.type) && !this.crossDomain) {
xhr.setRequestHeader("X-CSRFToken", csrftoken);
}
}
});
console.log("Loading dropper");
jQuery.event.props.push('dataTransfer');
$(window).on('dragenter', function() {
$("#id_drop-files").css("background-color","#E9E9E4");
$("#id_dropregion").addClass("watermark").removeClass("invisible");})
$(window).on('dragleave', function() {
$("#id_drop-files").css("background-color","#FFFFFF");
$("#id_dropregion").removeClass("watermark").addClass("invisible");})
var frm = $("#file_form");
if( window.FormData === undefined ) {
console.log('no formdata');
alert("No FormData");
} else {
console.log('we have formdata');
}
var data = new FormData(frm[0]);
$('#id_file').on('change', function(evt) {
var f = this.files[0];
console.log(f);
var istcx = false;
var isgzip = false;
var size1 = 10485760;
var size2 = 1048576;
if ((/\.(tcx|TCX)/i).test(f.name)) {
istcx = true;
console.log('tcx');
if ((/\.(gz|GZ)/i).test(f.name)) {
isgzip = true;
console.log('gzip');
size1 /= 5;
size2 /= 5;
}
}
console.log(size1)
console.log(size2)
if (f.size > size1) {
alert("File Size must be smaller than 10 MB");
this.value = null;
} else {
if (f.size > size2) {
$('#id_offline').val('True');
$('#id_offline').prop('checked','True');
data.set($('#id_offline').attr('name'),$('#id_offline').prop('checked'));
console.log("Set offline to True");
}
}
});
$('input').each(function( i ) {
$(this).change(function() {
if ($(this).attr('type') == 'checkbox') {
data.set($(this).attr('name'),$(this).prop('checked'));
console.log($(this).attr('id'),$(this).attr('name'),$(this).prop('checked'));
} else {
data.set($(this).attr('name'),$(this).val());
if ($(this).attr('id') == 'id_file') {
data.set("file",this.files[0]);
}
console.log($(this).attr('name'),$(this).val());
console.log(data)
};
});});
$('textarea').each(function( i ) {
$(this).change(function() {
data.set($(this).attr('name'),$(this).val());
console.log($(this).attr('name'),$(this).val());
});
});
$('select').each(function( i ) {
console.log($(this).attr('name'),$(this).val());
$(this).change(function() {
data.set($(this).attr('name'),$(this).val());
console.log($(this).attr('id'),$(this).attr('name'),$(this).val());
});
});
frm.submit(function() {
console.log(data)
console.log("Form submission");
$(data.values()).each(function(value) {
console.log(value);
});
$("#id_drop-files").replaceWith(
'<div id="id_waiting"><img src="/static/img/rowingtimer.gif" width="60" height="50" style="width:60px">'
);
$.ajax({
data: data,
type: $(this).attr('method'),
url: '/rowers/courses/{{ course.id }}/update/',
contentType: false,
processData: false,
error: function(result) {
$("#id_waiting").replaceWith(
'<div id="id_failed" class="grid_12 alpha message">Your upload failed</div>'
);
console.log(data);
setTimeout(1000);
setTimeout(function() {
location.reload();
},1000);
},
success: function(result) {
console.log('got something back');
console.log(result);
if (result.result == 1) {
setTimeout(1000);
window.location.href = result.url;
} else {
console.log(result," reloading");
location.reload();
};
}
});
return false;
});
$('#id_drop-files').bind({
drop: function(e) {
e.preventDefault();
console.log("you dropped something");
var files = e.dataTransfer.files;
console.log(files[0]);
var f = files[0];
var istcx = false;
var isgzip = false;
var size1 = 10485760;
var size2 = 1048576;
if ((/\.(tcx|TCX)/i).test(f.name)) {
istcx = true;
console.log('tcx');
if ((/\.(gz|GZ)/i).test(f.name)) {
isgzip = true;
console.log('gzip');
size1 /= 5;
size2 /= 5;
}
}
console.log(f);
console.log(size1)
console.log(size2)
if (f.size > size1) {
alert("File Size must be smaller than 10 MB");
$("#id_file").value = 0;
return false;
}
data.set("file",f);
// data.append("file",f);
$("#id_file").replaceWith('<div id="id_file">'+files[0].name+'&nbsp; <a class="remove" href="javascript:void(0);"><b><font color="red">X</font></b></a></div>');
},
mouseenter:function(){$("#id_drop-files").css("background-color","#E9E9E4");},
mouseleave:function(){$("#id_drop-files").css("background-color","#FFFFFF");},
dragover:function(e){
e.preventDefault();
$("#id_drop-files").css("background-color","#E9E9E4");},
dragleave:function(e){ e.preventDefault();},
});
$(document).on("click", "a.remove", function() {
$(this).parent().replaceWith('<td><input id="id_file" name="file" type="file" /></td>');
});
});
};
</script>
{% endblock %}

View File

@@ -30,7 +30,7 @@
{{ form.as_table }}
</table>
{% csrf_token %}
<input class="button green" type="submit" value="Submit">
<input class="button" type="submit" value="Submit">
</form>
</li>
<li class="grid_2">

View File

@@ -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 %}
<h1>Replace {{ course.name }}</h1>
<ul class="main-content">
<li class="grid_2">
<p>
This updates the course {{ course.name }} with the course markers as shown on
the map below.
</p>
<form id="course_form" method="post">
<form id="course_form" method="post">
<table>
{{ form.as_table }}
</table>
{% csrf_token %}
<input class="button" type="submit" value="Submit">
</form>
</li>
<li class="grid_2">
<div class="mapdiv">
{{ mapdiv|safe }}
{{ mapscript|safe }}
</div>
</li>
<li class="grid_4">
<div class="mapdiv">
{{ mapdiv|safe }}
{{ mapscript|safe }}
</div>
</li>
</ul>
{% endblock %}
{% block sidebar %}
{% include 'menu_racing.html' %}
{% endblock %}

View File

@@ -128,7 +128,7 @@
</li>
{% endif %}
<li id="course-view">
<a href="/rowers/courses/{{ course.id }}/replace/">
<a href="/rowers/courses/{{ course.id }}/update/">
<i class="fas fa-map-marked-alt fa-fw"></i>&nbsp;Update Markers</a>
</li>
{% endif %}

View File

@@ -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<id>\d+)/update/(?P<newid>\d+)/',views.course_update_confirm,name='course_update_confirm'),
re_path(r'^courses/(?P<id>\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<id>\d+)/$',views.standards_upload_view,name='standards_upload_view'),
re_path(r'^workout/addmanual/(?P<raceid>\d+)/$',views.addmanual_view,name='addmanual_view'),

View File

@@ -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):

View File

@@ -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,