From a80c523c3bd8f5499d238ec1b0c2553642e461bc Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Thu, 31 Oct 2024 09:06:37 +0100 Subject: [PATCH 1/3] changes --- rowers/dataroutines.py | 6 +++++- rowers/tests/testdata/testdata.tcx.gz | Bin 4000 -> 4000 bytes rowers/views/analysisviews.py | 2 ++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/rowers/dataroutines.py b/rowers/dataroutines.py index 6dc3268b..422226bd 100644 --- a/rowers/dataroutines.py +++ b/rowers/dataroutines.py @@ -37,6 +37,7 @@ from polars.exceptions import ( ColumnNotFoundError, SchemaError, ComputeError, InvalidOperationError, ShapeError ) +from pandas.errors import IntCastingNaNError from rowingdata import ( KinoMapParser, @@ -257,7 +258,10 @@ def get_video_data(w, groups=['basic'], mode='water'): df2 = pd.concat([df2, coordinates], axis=1) latitude = df2['latitude'] longitude = df2['longitude'] - boatspeed = (100*df2['velo'].fillna(method='ffill').fillna(method='bfill')).astype(int)/100. + try: + boatspeed = (100*df2['velo'].fillna(method='ffill').fillna(method='bfill')).astype(int)/100. + except IntCastingNaNError: + boatspeed = 0.0*df2['longitude'] # bundle data data = { 'boatspeed': boatspeed.values.tolist(), diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz index 654adcaedc233e73eae89b6b82380d30b627db18..96de089261eaba1ffcf28004d7b41f4b18171ba8 100644 GIT binary patch delta 36 scmZ1=zd)W{zMF%?=!(on_LY2WcZHOW+viT+#`lP0-?fy6BMpoU0OF?&SpWb4 delta 36 scmZ1=zd)W{zMF&NguCEI_LY2W^=@&yl+z|}<9o#6D{)cfNCP7S0NC&h8vp Date: Thu, 31 Oct 2024 10:30:23 +0100 Subject: [PATCH 2/3] adding functionality to make it easier to do timed courses --- rowers/templates/course_view.html | 19 ++++++ rowers/templates/workout_form.html | 12 ++++ rowers/templatetags/rowerfilters.py | 12 ++++ rowers/tests/testdata/testdata.tcx.gz | Bin 4000 -> 4000 bytes rowers/urls.py | 4 ++ rowers/views/racesviews.py | 3 +- rowers/views/workoutviews.py | 89 +++++++++++++++++++++----- 7 files changed, 123 insertions(+), 16 deletions(-) diff --git a/rowers/templates/course_view.html b/rowers/templates/course_view.html index decc0089..4a610e83 100644 --- a/rowers/templates/course_view.html +++ b/rowers/templates/course_view.html @@ -111,6 +111,15 @@

+ {% if workoutid %} +
  • +

    + Suggested Workout: {{ workoutid|getworkoutname }} +
    Measure time on course + +

    +
  • + {% endif %} {% if form %}
  • @@ -136,6 +145,16 @@

  • {% endif %} + {% else %} + {% if workoutid %} +
  • +

    + Suggested Workout: {{ workoutid|getworkoutname }} +
    Measure time on course + +

    +
  • + {% endif %} {% endif %} {% if ownrecords %}
  • diff --git a/rowers/templates/workout_form.html b/rowers/templates/workout_form.html index ac019062..98034e21 100644 --- a/rowers/templates/workout_form.html +++ b/rowers/templates/workout_form.html @@ -213,6 +213,18 @@
  • {% endif %} + {% if suggested_courses %} +
  • +

    Suggested Measured Courses

    + {% for course in suggested_courses %} +

    + {{ course }} +
    See course +
    Measure time on course +

    + {% endfor %} +
  • +{% endif %} {% for graph in graphs %}
  • diff --git a/rowers/templatetags/rowerfilters.py b/rowers/templatetags/rowerfilters.py index 2d4af95e..60a97c29 100644 --- a/rowers/templatetags/rowerfilters.py +++ b/rowers/templatetags/rowerfilters.py @@ -48,6 +48,16 @@ from six import string_types register = template.Library() +@register.filter +def getworkoutname(id): + try: + w = Workout.objects.get(id=encoder.decode_hex(id)) + return w + except Workout.DoesNotExist: + return '' + + return '' + @register.filter def workoutdate(id): # pragma: no cover try: @@ -56,6 +66,8 @@ def workoutdate(id): # pragma: no cover except Workout.DoesNotExist: return 'unknown' + return 'unknown' + @register.filter def usermessages(rower): try: diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz index 96de089261eaba1ffcf28004d7b41f4b18171ba8..5cf8d9a455ab06f2eb612d9828f15a01404b6104 100644 GIT binary patch delta 3914 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 Y<;BDFG+v}jAKv*N+UkM11jK*=0GY7rX#fBK delta 3914 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 YFCL|*@khG!;hp~hzSd-b1jK*=0Ke$+@&Et; diff --git a/rowers/urls.py b/rowers/urls.py index afe89f63..18b28d66 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -366,6 +366,8 @@ urlpatterns = [ views.course_follow_view, name='course_follow_view'), re_path(r'^courses/(?P\d+)/unfollow/', views.course_unfollow_view, name='course_unfollow_view'), + re_path(r'^courses/(?P\d+)/workout/(?P\b[0-9A-Fa-f]+\b)', + views.course_view, name='course_view'), re_path(r'^standards/upload/$', views.standards_upload_view, name='standards_upload_view'), re_path(r'^standards/upload/(?P\d+)/$', @@ -385,6 +387,8 @@ urlpatterns = [ re_path( r'^user-analysis-select/(?P\w.*)/team/(?P\d+)/workout/(?P\b[0-9A-Fa-f]+\b)/$', views.analysis_new, name='analysis_new'), + re_path(r'workout/(?P\b[0-9A-Fa-f]+\b)/submit/(?P\d+)/$', + views.workout_submit_course_view, name='workout_submit_course_view'), re_path(r'^workouts-dupes-select/user/(?P\d+)/$', views.workouts_duplicates_select_view, name='workouts_duplicates_select_view'), re_path(r'^workouts-dupes-select/$', diff --git a/rowers/views/racesviews.py b/rowers/views/racesviews.py index 006de323..c233a985 100644 --- a/rowers/views/racesviews.py +++ b/rowers/views/racesviews.py @@ -356,7 +356,7 @@ def course_unfollow_view(request, id=0): url = reverse("courses_view") return HttpResponseRedirect(url) -def course_view(request, id=0): +def course_view(request, id=0, workoutid=0): try: course = GeoCourse.objects.get(id=id) except GeoCourse.DoesNotExist: # pragma: no cover @@ -477,6 +477,7 @@ def course_view(request, id=0): 'rower': r, 'form': form, 'onlyme': onlyme, + 'workoutid': workoutid, } ) diff --git a/rowers/views/workoutviews.py b/rowers/views/workoutviews.py index 73719594..fe0724b3 100644 --- a/rowers/views/workoutviews.py +++ b/rowers/views/workoutviews.py @@ -4540,24 +4540,13 @@ def workout_edit_view(request, id=0, message="", successmessage=""): hascoordinates = 1 courses = [] - if rowdata != 0: - try: - latitude = rowdata.df[' latitude'] - longitude = rowdata.df[' longitude'] - if not latitude.std(): # pragma: no cover - hascoordinates = 0 - if not longitude.std(): - hascoordinates = 0 - except (KeyError, AttributeError): - hascoordinates = 0 - - else: # pragma: no cover - hascoordinates = 0 + suggested_courses = [] + has_latlon, lat_mean, lon_mean = dataprep.workout_has_latlon(row.id) mapscript = "" mapdiv = "" - if hascoordinates: + if has_latlon: try: mapscript, mapdiv = leaflet_chart( rowdata.df[' latitude'], @@ -4570,6 +4559,12 @@ def workout_edit_view(request, id=0, message="", successmessage=""): workoutid=row.id, userid=row.user.user.id, coursecompleted=True) if records.count() > 0: # pragma: no cover courses = list(set([record.course for record in records])) + suggested_courses = getnearestcourses([lat_mean, lon_mean], GeoCourse.objects.all(), whatisnear=25, + strict=True) + + suggested_courses = list(set(courses) ^ set(suggested_courses)) + + breadcrumbs = [ { @@ -4608,6 +4603,7 @@ def workout_edit_view(request, id=0, message="", successmessage=""): 'mapdiv': mapdiv, 'rower': r, 'courses': courses, + 'suggested_courses': suggested_courses, }) @@ -6102,8 +6098,71 @@ def workout_fusion_view(request, id1=0, id2=1): 'workout2': w2, }) -# See attached courses +# See attached courses / attaching courses +@login_required() +@permission_required('workout.change_workout', fn=get_workout_by_opaqueid, raise_exception=True) +def workout_submit_course_view(request, id, courseid): + row = get_workout_by_opaqueid(request, id) + r = getrower(request.user) + try: + course = GeoCourse.objects.get(id=courseid) + except GeoCourse.DoesNotExist: + url = reverse('workout_edit_view', kwargs={'id': encoder.encode_hex(row.id)}) + return HttpResponseRedirect(url) + + # got a course + records = VirtualRaceResult.objects.filter( + userid = r.id, + course=course, + workoutid=row.id + ) + if records: + record = records[0] + else: + # create record + record = VirtualRaceResult( + userid=r.id, + username=r.user.first_name+' '+r.user.last_name, + workoutid=row.id, + weightcategory=r.weightcategory, + adaptiveclass=r.adaptiveclass, + course=course, + distance=course.distance, + boatclass=row.workouttype, + boattype=row.boattype, + sex=r.sex, + age=calculate_age(r.birthdate), + ) + record.save() + + job = myqueue( + queuehigh, + handle_check_race_course, + row.csvfilename, + row.id, + course.id, + record.id, + r.user.email, + r.user.first_name, + summary=True, + successemail=True, + ) + + try: + request.session['async_tasks'] += [ + (job.id, 'check_race_course')] + except KeyError: # pragma: no cover + request.session['async_tasks'] = [ + (job.id, 'check_race_course')] + + messages.info(request, 'We are checking your time on the course in the background." \ + " You will receive an email when the check is complete." \ + " You can check the status here') + + url = reverse('workout_edit_view', kwargs={'id': encoder.encode_hex(row.id)}) + return HttpResponseRedirect(url) + @login_required() @permission_required('workout.change_workout', fn=get_workout_by_opaqueid, raise_exception=True) From 84fda80fa5db8fa1f285e67ec11f15c32f2f1f81 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Thu, 31 Oct 2024 11:33:16 +0100 Subject: [PATCH 3/3] new functionality tested again --- rowers/tests/testdata/testdata.tcx.gz | Bin 4000 -> 4001 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz index 5cf8d9a455ab06f2eb612d9828f15a01404b6104..457398eb969b6e8b4da2525f4ff451b30ea42127 100644 GIT binary patch literal 4001 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 GfB^s+SR+dS