From b569fb2c46d63bd2ab09a229b5c0655fdeac25fb Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Tue, 16 May 2017 20:23:45 +0200 Subject: [PATCH 01/10] small improvement --- rowers/management/commands/getexpired.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rowers/management/commands/getexpired.py b/rowers/management/commands/getexpired.py index b5b24e41..e6f68fc0 100644 --- a/rowers/management/commands/getexpired.py +++ b/rowers/management/commands/getexpired.py @@ -22,6 +22,9 @@ def getexpired(): if r.planexpires < datetime.datetime.now().date(): if r.rowerplan == 'pro' or r.rowerplan == 'coach': lijst.append(r) + if r.teamplanexpires < datetime.datetime.now().date(): + if r.rowerplan == 'pro' or r.rowerplan == 'coach': + lijst.append(r) return lijst From 791d537b8df7ccd82acae4d009ad423e45f64bd8 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 17 May 2017 09:02:14 +0200 Subject: [PATCH 02/10] added user name to paypal button field --- rowers/templates/about_us.html | 12 ++++--- rowers/templates/paypaltest.html | 55 +++++++++++++++++++++++++++++ rowers/templates/promembership.html | 11 +++--- rowers/urls.py | 1 + 4 files changed, 70 insertions(+), 9 deletions(-) create mode 100644 rowers/templates/paypaltest.html diff --git a/rowers/templates/about_us.html b/rowers/templates/about_us.html index b89f6881..5defb350 100644 --- a/rowers/templates/about_us.html +++ b/rowers/templates/about_us.html @@ -123,12 +123,10 @@ of a rowers technique.

Click on the PayPal button to pay for your Premium membership. - It will be valid for one year with automatic renewal which you can stop at any time. + Before you pay, please register for the free Basic membership and log in. + In this way, your user name will be added to the payment details. + Your payment will be valid for one year with automatic renewal which you can stop at any time. You will be taken to the secure PayPal payment site. -

After you do the payment, we will manually change your membership to "Pro". Depending on our availability, this may take some time @@ -138,6 +136,10 @@ You will be taken to the secure PayPal payment site.

+{% if user.is_authenticated %} + + +{% endif %}
diff --git a/rowers/templates/paypaltest.html b/rowers/templates/paypaltest.html new file mode 100644 index 00000000..2183fdcf --- /dev/null +++ b/rowers/templates/paypaltest.html @@ -0,0 +1,55 @@ + + {% extends "base.html" %} + {% block title %}About us{% endblock title %} + {% block content %} +

PayPal Test Button

+ +
+ +

This is a test page for testing the paypal functionality. Do not use it.

+ +

Donations are welcome to keep this web site going. To help cover the hosting +costs, I have created a Pro membership option (for only 15 EURO per year). Once I process your +donation, I will give you access to some special features on this +website.

+ +

Currently, the Pro membership will give you the following extra functionality (and more will follow): +

    +
  • More stroke metrics plots
  • +
  • Power curves for OTW rowing
  • +
  • Add weather information to OTW rowing sessions
  • +
  • Power histogram
  • +
+

+

Click on the PayPal button to pay for your Pro membership. Before you pay, please register for the free Basic membership and log in. + In this way, your user name will be added to the payment details. +Your payment will be valid for one year with automatic renewal which you can stop at any time. +You will be taken to the secure PayPal payment site. +

+
+ +
+
+ + +{% if user.is_authenticated %} + + +{% endif %} + + +
+ + + +

After you do the payment, we will manually change your membership to + "Pro". Depending on our availability, this may take some time + (typically one working day). Don't hesitate to contact us + if you have any questions at this stage.

+ +

If, for any reason, you are not happy with your Pro membership, please let me know through the contact form. I will contact you as soon as possible to discuss how we can make things better.

+ +
+ + + {% endblock content %} diff --git a/rowers/templates/promembership.html b/rowers/templates/promembership.html index 76e228aa..4339a5d6 100644 --- a/rowers/templates/promembership.html +++ b/rowers/templates/promembership.html @@ -19,11 +19,10 @@ website.

  • Power histogram
  • -

    Click on the PayPal button to pay for your Pro membership. It will be valid for one year with automatic renewal which you can stop at any time. +

    Click on the PayPal button to pay for your Pro membership. Before you pay, please register for the free Basic membership and log in. + In this way, your user name will be added to the payment details. +Your payment will be valid for one year with automatic renewal which you can stop at any time. You will be taken to the secure PayPal payment site. -

    @@ -31,6 +30,10 @@ You will be taken to the secure PayPal payment site.
    +{% if user.is_authenticated %} + + +{% endif %}
    diff --git a/rowers/urls.py b/rowers/urls.py index 56c90669..dbb5fcd9 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -274,6 +274,7 @@ urlpatterns = [ url(r'^videos', TemplateView.as_view(template_name='videos.html'),name='videos'), url(r'^analysis', TemplateView.as_view(template_name='analysis.html'),name='analysis'), url(r'^promembership', TemplateView.as_view(template_name='promembership.html'),name='promembership'), + url(r'^paypaltest', TemplateView.as_view(template_name='paypaltest.html'),name='paypaltest'), url(r'^legal', TemplateView.as_view(template_name='legal.html'),name='legal'), url(r'^register$',views.rower_register_view), url(r'^register/thankyou/$', TemplateView.as_view(template_name='registerthankyou.html'), name='registerthankyou'), From b7d7d5cc83442b5e424cd614385a3126dd2de6d2 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 17 May 2017 09:31:56 +0200 Subject: [PATCH 03/10] redirects to boxchart workout picker when ids messed up --- rowers/dataprep.py | 9 +++++++++ rowers/views.py | 13 ++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/rowers/dataprep.py b/rowers/dataprep.py index eeaa2b64..e25bc255 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -95,6 +95,15 @@ from scipy.signal import savgol_filter import datetime +def get_workouts(ids,userid): + workouts = [] + for id in ids: + w = Workout.objects.get(id=id) + if w.user.user.id == userid: + workouts.append[w] + + return workouts + def filter_df(datadf,fieldname,value,largerthan=True): try: diff --git a/rowers/views.py b/rowers/views.py index fb8ce778..7e6461b8 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -3124,7 +3124,18 @@ def boxplot_view(request,userid=0, workstrokesonly = not includereststrokes ids = request.session['ids'] request.session['ids'] = ids - workouts = [Workout.objects.get(id=id) for id in ids] + workouts = dataprep.get_workouts(ids,userid) + if not workouts: + message = 'Error: Workouts in session storage do not belong to this user.' + messages.error(request,message) + url = reverse(user_boxplot_select, + kwargs={ + 'userid':userid, + } + ) + return HttpResponseRedirect(url) + + # workouts = [Workout.objects.get(id=id) for id in ids] labeldict = { int(w.id): w.__unicode__() for w in workouts From bb2283635e07ec93ecaeca0b68b432f0dbdb0568 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 17 May 2017 09:44:47 +0200 Subject: [PATCH 04/10] bug fix --- rowers/dataprep.py | 8 ++++---- rowers/views.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/rowers/dataprep.py b/rowers/dataprep.py index e25bc255..ba78268e 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -96,13 +96,13 @@ from scipy.signal import savgol_filter import datetime def get_workouts(ids,userid): - workouts = [] + goodids = [] for id in ids: w = Workout.objects.get(id=id) - if w.user.user.id == userid: - workouts.append[w] + if int(w.user.user.id) == int(userid): + goodids.append(id) - return workouts + return [Workout.objects.get(id=id) for id in goodids] def filter_df(datadf,fieldname,value,largerthan=True): diff --git a/rowers/views.py b/rowers/views.py index 7e6461b8..376fa0d1 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -3111,7 +3111,7 @@ def boxplot_view(request,userid=0, }) else: return HttpResponse("Form is not valid") - if request.method == 'POST' and 'ids' in request.session: + elif request.method == 'POST' and 'ids' in request.session: chartform = BoxPlotChoiceForm(request.POST) if chartform.is_valid(): plotfield = chartform.cleaned_data['yparam'] From 12d95cfe43889b1cef61a553ed767cbb10350de0 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 17 May 2017 11:54:02 +0200 Subject: [PATCH 05/10] C2 and ST button to import all new workouts --- rowers/dataprepnodjango.py | 2 + rowers/templates/c2_list_import2.html | 78 +++++++----- rowers/templates/sporttracks_list_import.html | 73 ++++++----- rowers/urls.py | 2 + rowers/utils.py | 15 +++ rowers/views.py | 117 ++++++++++++++++-- 6 files changed, 218 insertions(+), 69 deletions(-) diff --git a/rowers/dataprepnodjango.py b/rowers/dataprepnodjango.py index 8872d4e7..af17d87a 100644 --- a/rowers/dataprepnodjango.py +++ b/rowers/dataprepnodjango.py @@ -18,6 +18,7 @@ from rowsandall_app.settings import DATABASES from utils import lbstoN + user = DATABASES['default']['USER'] password = DATABASES['default']['PASSWORD'] database_name = DATABASES['default']['NAME'] @@ -335,6 +336,7 @@ def handle_nonpainsled(f2,fileformat,summary=''): return (f2,summary,oarlength,inboard) + # Create new workout from file and store it in the database # This routine should be used everywhere in views.py and mailprocessing.py # Currently there is code duplication diff --git a/rowers/templates/c2_list_import2.html b/rowers/templates/c2_list_import2.html index 03f2d8e7..886542be 100644 --- a/rowers/templates/c2_list_import2.html +++ b/rowers/templates/c2_list_import2.html @@ -6,36 +6,50 @@ {% block content %}

    Available on C2 Logbook

    - {% if workouts %} - - - - - - - - - - - - - - {% for workout in workouts %} - - - - - - - - - - - {% endfor %} - -
    Import Date/Time Duration Total Distance Type Source Comment
    -Import{{ workout|lookup:'starttime' }}{{ workout|lookup:'duration' }}{{ workout|lookup:'distance' }}{{ workout|lookup:'rowtype' }}{{ workout|lookup:'source' }}{{ workout|lookup:'comment' }}
    - {% else %} -

    No workouts found

    - {% endif %} + +{% if workouts %} + +
    +

    This imports all workouts that have not been imported to rowsandall.com. + The action may take a longer time to process, so please be patient. Click on Import in the list below to import an individual workout. +

    +
    + +
    + + + + + + + + + + + + + + + {% for workout in workouts %} + + + + + + + + + + + + {% endfor %} + +
    Import Date/Time Duration Total Distance Type Source Comment New
    + Import{{ workout|lookup:'starttime' }}{{ workout|lookup:'duration' }}{{ workout|lookup:'distance' }}{{ workout|lookup:'rowtype' }}{{ workout|lookup:'source' }}{{ workout|lookup:'comment' }}{{ workout|lookup:'new' }}
    +
    +{% else %} +

    No workouts found

    +{% endif %} {% endblock %} diff --git a/rowers/templates/sporttracks_list_import.html b/rowers/templates/sporttracks_list_import.html index 96a2aa2d..b547efc8 100644 --- a/rowers/templates/sporttracks_list_import.html +++ b/rowers/templates/sporttracks_list_import.html @@ -6,34 +6,47 @@ {% block content %}

    Available on SportTracks

    - {% if workouts %} - - - - - - - - - - - - - {% for workout in workouts %} - - - - - - - - - - {% endfor %} - -
    Import Name Date/Time Duration Total Distance Type
    -Import{{ workout|lookup:'name' }}{{ workout|lookup:'starttime' }}{{ workout|lookup:'duration' }} {{ workout|lookup:'distance' }} m{{ workout|lookup:'type' }}
    - {% else %} -

    No workouts found

    - {% endif %} +{% if workouts %} + +
    +

    This imports all workouts that have not been imported to rowsandall.com. + The action may take a longer time to process, so please be patient. Click on Import in the list below to import an individual workout. +

    +
    + +
    + + + + + + + + + + + + + + {% for workout in workouts %} + + + + + + + + + + + {% endfor %} + +
    Import Name Date/Time Duration Total Distance Type New
    + Import{{ workout|lookup:'name' }}{{ workout|lookup:'starttime' }}{{ workout|lookup:'duration' }} {{ workout|lookup:'distance' }} m{{ workout|lookup:'type' }}{{ workout|lookup:'new' }}
    +
    +{% else %} +

    No workouts found

    +{% endif %} {% endblock %} diff --git a/rowers/urls.py b/rowers/urls.py index dbb5fcd9..ec5d73a4 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -211,9 +211,11 @@ urlpatterns = [ url(r'^workout/c2import/$',views.workout_c2import_view), url(r'^workout/stravaimport/$',views.workout_stravaimport_view), url(r'^workout/c2import/(?P\d+)/$',views.workout_getc2workout_view), + url(r'^workout/c2import/all/$',views.workout_getc2workout_all), url(r'^workout/stravaimport/(?P\d+)/$',views.workout_getstravaworkout_view), url(r'^workout/sporttracksimport/$',views.workout_sporttracksimport_view), url(r'^workout/sporttracksimport/(?P\d+)/$',views.workout_getsporttracksworkout_view), + url(r'^workout/sporttracksimport/all/$',views.workout_getsporttracksworkout_all), url(r'^workout/runkeeperimport/$',views.workout_runkeeperimport_view), url(r'^workout/runkeeperimport/(?P\d+)/$',views.workout_getrunkeeperworkout_view), url(r'^workout/underarmourimport/$',views.workout_underarmourimport_view), diff --git a/rowers/utils.py b/rowers/utils.py index b700cba3..b78310cd 100644 --- a/rowers/utils.py +++ b/rowers/utils.py @@ -4,6 +4,21 @@ import numpy as np lbstoN = 4.44822 +def uniqify(seq, idfun=None): + # order preserving + if idfun is None: + def idfun(x): return x + seen = {} + result = [] + for item in seq: + marker = idfun(item) + # in old Python versions: + # if seen.has_key(marker) + # but in new ones: + if marker in seen: continue + seen[marker] = 1 + result.append(item) + return result def serialize_list(value,token=','): assert(isinstance(value, list) or isinstance(value, tuple) or isinstance(value,np.ndarray)) diff --git a/rowers/views.py b/rowers/views.py index 376fa0d1..460ef645 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -267,7 +267,7 @@ def splitstdata(lijst): return [np.array(t),np.array(latlong)] -from utils import geo_distance,serialize_list,deserialize_list +from utils import geo_distance,serialize_list,deserialize_list,uniqify from rowers.models import checkworkoutuser # Check if a user is a Coach member @@ -5764,15 +5764,25 @@ def workout_sporttracksimport_view(request,message=""): return HttpResponseRedirect(url) else: workouts = [] + r = Rower.objects.get(user=request.user) + stids = [int(getidfromsturi(item['uri'])) for item in res.json()['items']] + knownstids = uniqify([ + w.uploadedtosporttracks for w in Workout.objects.filter(user=r) + ]) + newids = [stid for stid in stids if not stid in knownstids] for item in res.json()['items']: d = int(float(item['total_distance'])) - i = getidfromsturi(item['uri']) + i = int(getidfromsturi(item['uri'])) + if i in knownstids: + nnn = '' + else: + nnn = 'NEW' n = item['name'] ttot = str(datetime.timedelta(seconds=int(float(item['duration'])))) s = item['start_time'] r = item['type'] - keys = ['id','distance','duration','starttime','type','name'] - values = [i,d,ttot,s,r,n] + keys = ['id','distance','duration','starttime','type','name','new'] + values = [i,d,ttot,s,r,n,nnn] res = dict(zip(keys,values)) workouts.append(res) return render(request,'sporttracks_list_import.html', @@ -5823,7 +5833,58 @@ def c2listdebug_view(request,message=""): {'workouts':workouts, 'teams':get_my_teams(request.user), }) - + +# Import all unknown workouts available on Concept2 logbook +@login_required() +def workout_getc2workout_all(request,message=""): + try: + thetoken = c2_open(request.user) + except C2NoTokenError: + return HttpResponseRedirect("/rowers/me/c2authorize/") + + res = c2stuff.get_c2_workout_list(request.user) + + if (res.status_code != 200): + message = "Something went wrong in workout_c2import_view (C2 token refresh)" + messages.error(request,message) + else: + r = Rower.objects.get(user=request.user) + c2ids = [item['id'] for item in res.json()['data']] + knownc2ids = uniqify([ + w.uploadedtoc2 for w in Workout.objects.filter(user=r) + ]) + newids = [c2id for c2id in c2ids if not c2id in knownc2ids] + for c2id in newids: + res = c2stuff.get_c2_workout(request.user,c2id) + if (res.status_code == 200): + data = res.json()['data'] + splitdata = None + if 'workout' in data: + if 'splits' in data['workout']: + splitdata = data['workout']['splits'] + if 'intervals' in data['workout']: + splitdata = data['workout']['intervals'] + + # Check if workout has stroke data, and get the stroke data + if data['stroke_data']: + res2 = c2stuff.get_c2_workout_strokes(request.user,c2id) + # We have stroke data + if res2.status_code == 200: + strokedata = pd.DataFrame.from_dict(res2.json()['data']) + # create the workout + id,message = add_workout_from_strokedata( + request.user,c2id,data,strokedata, + source='c2') + w = Workout.objects.get(id=id) + w.uploadedtoc2=c2id + w.save() + if message: + messages.error(request,message) + + url = reverse(workouts_view) + return HttpResponseRedirect(url) + + # List of workouts available on Concept2 logbook - for import @login_required() def workout_c2import_view(request,message=""): @@ -5844,6 +5905,12 @@ def workout_c2import_view(request,message=""): return HttpResponseRedirect(url) else: workouts = [] + r = Rower.objects.get(user=request.user) + c2ids = [item['id'] for item in res.json()['data']] + knownc2ids = uniqify([ + w.uploadedtoc2 for w in Workout.objects.filter(user=r) + ]) + newids = [c2id for c2id in c2ids if not c2id in knownc2ids] for item in res.json()['data']: d = item['distance'] i = item['id'] @@ -5852,8 +5919,12 @@ def workout_c2import_view(request,message=""): r = item['type'] s2 = item['source'] c = item['comments'] - keys = ['id','distance','duration','starttime','rowtype','source','comment'] - values = [i,d,ttot,s,r,s2,c] + if i in knownc2ids: + nnn = '' + else: + nnn = 'NEW' + keys = ['id','distance','duration','starttime','rowtype','source','comment','new'] + values = [i,d,ttot,s,r,s2,c,nnn] res = dict(zip(keys,values)) workouts.append(res) @@ -5959,6 +6030,38 @@ def workout_getsporttracksworkout_view(request,sporttracksid): }) return HttpResponseRedirect(url) +# Imports all new workouts from SportTracks +@login_required() +def workout_getsporttracksworkout_all(request): + res = sporttracksstuff.get_sporttracks_workout_list(request.user) + if (res.status_code == 200): + r = Rower.objects.get(user=request.user) + stids = [int(getidfromsturi(item['uri'])) for item in res.json()['items']] + knownstids = uniqify([ + w.uploadedtosporttracks for w in Workout.objects.filter(user=r) + ]) + newids = [stid for stid in stids if not stid in knownstids] + for sporttracksid in newids: + res = sporttracksstuff.get_sporttracks_workout( + request.user,sporttracksid) + data = res.json() + + id,message = add_workout_from_stdata(request.user,sporttracksid,data) + print sporttracksid,id + if id==0: + messages.error(request,message) + + else: + w = Workout.objects.get(id=id) + w.uploadedtosporttracks=sporttracksid + print 'saved' + w.save() + + url = reverse(workouts_view) + return HttpResponseRedirect(url) + + + # Imports a workout from Concept2 @login_required() def workout_getc2workout_view(request,c2id): From be13626fbc854b6cd28331fed5c6917873a73bae Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 17 May 2017 13:34:01 +0200 Subject: [PATCH 06/10] strava done --- rowers/templates/strava_list_import.html | 86 ++++++++++++------------ rowers/urls.py | 1 + rowers/views.py | 62 +++++++++++++++-- 3 files changed, 102 insertions(+), 47 deletions(-) diff --git a/rowers/templates/strava_list_import.html b/rowers/templates/strava_list_import.html index a99ea834..b384cd5c 100644 --- a/rowers/templates/strava_list_import.html +++ b/rowers/templates/strava_list_import.html @@ -6,47 +6,47 @@ {% block content %}

    Available on Strava

    - {% if data %} - - - - - - - - - - - - - {% for workout in data %} - - {% for key,value in workout.items %} - {% if key == "start_date" %} - - {% endif %} - {% if key == "name" %} - - {% endif %} - {% if key == "type" %} - - {% endif %} - {% if key == "distance" %} - - {% endif %} - {% if key == "elapsed_time" %} - - {% endif %} - {% if key == "id" %} - - {% endif %} - {% endfor %} - - - {% endfor %} - -
    Import Duration Type Date Distance Name
    {{ value }}{{ value }}{{ value }}{{ value }}m{{ value }}Import
    - {% else %} -

    No workouts found

    - {% endif %} +{% if workouts %} + +
    +

    This imports all workouts that have not been imported to rowsandall.com. + The action may take a longer time to process, so please be patient. Click on Import in the list below to import an individual workout. +

    +
    + +
    + + + + + + + + + + + + + + {% for workout in workouts %} + + + + + + + + + + + {% endfor %} + +
    Import Name Date Duration Distance Type New
    + Import{{ workout|lookup:'name' }}{{ workout|lookup:'starttime' }}{{ workout|lookup:'duration' }} {{ workout|lookup:'distance' }} m{{ workout|lookup:'type' }}{{ workout|lookup:'new' }}
    +
    +{% else %} +

    No workouts found

    +{% endif %} {% endblock %} diff --git a/rowers/urls.py b/rowers/urls.py index ec5d73a4..b9848e61 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -213,6 +213,7 @@ urlpatterns = [ url(r'^workout/c2import/(?P\d+)/$',views.workout_getc2workout_view), url(r'^workout/c2import/all/$',views.workout_getc2workout_all), url(r'^workout/stravaimport/(?P\d+)/$',views.workout_getstravaworkout_view), + url(r'^workout/stravaimport/all/$',views.workout_getstravaworkout_all), url(r'^workout/sporttracksimport/$',views.workout_sporttracksimport_view), url(r'^workout/sporttracksimport/(?P\d+)/$',views.workout_getsporttracksworkout_view), url(r'^workout/sporttracksimport/all/$',views.workout_getsporttracksworkout_all), diff --git a/rowers/views.py b/rowers/views.py index 460ef645..685e69d8 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -5651,10 +5651,33 @@ def workout_stravaimport_view(request,message=""): url = reverse(workouts_view) return HttpResponseRedirect(url) else: - data = res.json() + workouts = [] + r = Rower.objects.get(user=request.user) + stravaids = [int(item['id']) for item in res.json()] + knownstravaids = uniqify([ + w.uploadedtostrava for w in Workout.objects.filter(user=r) + ]) + newids = [stravaid for stravaid in stravaids if not stravaid in knownstravaids] + + for item in res.json(): + d = int(float(item['distance'])) + i = item['id'] + if i in knownstravaids: + nnn = '' + else: + nnn = 'NEW' + n = item['name'] + ttot = str(datetime.timedelta(seconds=int(float(item['elapsed_time'])))) + s = item['start_date'] + r = item['type'] + keys = ['id','distance','duration','starttime','type','name','new'] + values = [i,d,ttot,s,r,n,nnn] + res = dict(zip(keys,values)) + workouts.append(res) + return render(request,'strava_list_import.html', - {'data':data, + {'workouts':workouts, 'teams':get_my_teams(request.user), }) @@ -6047,14 +6070,45 @@ def workout_getsporttracksworkout_all(request): data = res.json() id,message = add_workout_from_stdata(request.user,sporttracksid,data) - print sporttracksid,id if id==0: messages.error(request,message) else: w = Workout.objects.get(id=id) w.uploadedtosporttracks=sporttracksid - print 'saved' + w.save() + + url = reverse(workouts_view) + return HttpResponseRedirect(url) + + +# Imports all new workouts from SportTracks +@login_required() +def workout_getstravaworkout_all(request): + res = stravastuff.get_strava_workout_list(request.user) + if (res.status_code == 200): + r = Rower.objects.get(user=request.user) + stravaids = [int(item['id']) for item in res.json()] + knownstravaids = uniqify([ + w.uploadedtostrava for w in Workout.objects.filter(user=r) + ]) + newids = [stravaid for stravaid in stravaids if not stravaid in knownstravaids] + + for stravaid in newids: + res = stravastuff.get_strava_workout(request.user,stravaid) + strokedata = res[1] + data = res[0] + + id,message = add_workout_from_strokedata(request.user,stravaid,data,strokedata, + source='strava', + workoutsource='strava') + + if id==0: + messages.error(request,message) + + else: + w = Workout.objects.get(id=id) + w.uploadedtostrava=stravaid w.save() url = reverse(workouts_view) From ba38a0747a013397cd92793815cfc2cae91a2e3c Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 17 May 2017 16:30:05 +0200 Subject: [PATCH 07/10] csv to admin functionality --- rowers/urls.py | 1 + rowers/views.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/rowers/urls.py b/rowers/urls.py index b9848e61..33f953f7 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -170,6 +170,7 @@ urlpatterns = [ url(r'^workout/(?P\d+)/comment$',views.workout_comment_view), url(r'^workout/(?P\d+)/emailtcx$',views.workout_tcxemail_view), url(r'^workout/(?P\d+)/emailcsv$',views.workout_csvemail_view), + url(r'^workout/(?P\d+)/csvtoadmin$',views.workout_csvtoadmin_view), url(r'^workout/compare/(?P\d+)/$',views.workout_comparison_list), url(r'^workout/compare2/(?P\d+)/(?P\d+)/(?P\w+.*)/(?P\w+.*)/$',views.workout_comparison_view), url(r'^workout/compare/(?P\d+)/(?P\d+-\d+-\d+)/(?P\w+.*)$',views.workout_comparison_list), diff --git a/rowers/views.py b/rowers/views.py index 685e69d8..7a48b6ae 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -1150,6 +1150,41 @@ def workout_csvemail_view(request,id=0): return response +# Get Workout CSV file and send it to user's email address +@login_required() +def workout_csvtoadmin_view(request,id=0): + message = "" + r = Rower.objects.get(user=request.user) + try: + w = Workout.objects.get(id=id) + except Workout.DoesNotExist: + raise Http404("Workout doesn't exist") + + csvfile = w.csvfilename + if settings.DEBUG: + res = handle_sendemailcsv.delay( + 'Sander', + 'Roosendaal', + 'roosendaalsander@gmail.com',csvfile) + + else: + res = queuehigh.enqueue( + handle_sendemailcsv, + 'Sander', + 'Roosendaal', + 'roosendaalsander@gmail.com', + csvfile) + + successmessage = "The CSV file was sent to the site admin per email" + messages.info(request,successmessage) + url = reverse(workout_export_view, + kwargs = { + 'id':str(w.id), + }) + response = HttpResponseRedirect(url) + + return response + # Send workout to TP @login_required() def workout_tp_upload_view(request,id=0): From 9578be77c0715a84bad8807dc0f1120bacaf8971 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Thu, 18 May 2017 10:19:39 +0200 Subject: [PATCH 08/10] some small improvement (export messages) --- rowers/views.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/rowers/views.py b/rowers/views.py index 7a48b6ae..1b421e1f 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -1345,8 +1345,10 @@ def workout_c2_upload_view(request,id=0): except C2NoTokenError: return HttpResponseRedirect("/rowers/me/c2authorize/") - if message: + if message and c2id <=0: messages.error(request,message) + elif message: + messages.info(request,message) url = reverse(workout_export_view, @@ -1534,6 +1536,8 @@ def workout_sporttracks_upload_view(request,id=0): sporttracksid = sporttracksstuff.getidfromresponse(response) w.uploadedtosporttracks = sporttracksid w.save() + message = "Upload to SportTracks was successful" + messages.info(message) url = "/rowers/workout/"+str(w.id)+"/export" return HttpResponseRedirect(url) else: From 40313f9680469af96276bac19754c2f71c4197d9 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Thu, 18 May 2017 15:23:09 +0200 Subject: [PATCH 09/10] adding OTW power slack --- rowers/models.py | 5 +- rowers/templates/rower_form.html | 12 ++++- rowers/views.py | 88 +++++++++++++++++++++++++------- 3 files changed, 83 insertions(+), 22 deletions(-) diff --git a/rowers/models.py b/rowers/models.py index fa0c839d..15046a7a 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -173,13 +173,14 @@ class Rower(models.Model): # Power Zone Data ftp = models.IntegerField(default=226,verbose_name="Functional Threshold Power") + otwslack = models.IntegerField(default=15,verbose_name="OTW Power slack") pw_ut2 = models.IntegerField(default=124,verbose_name="UT2 Power") pw_ut1 = models.IntegerField(default=171,verbose_name="UT1 Power") pw_at = models.IntegerField(default=203,verbose_name="AT Power") pw_tr = models.IntegerField(default=237,verbose_name="TR Power") pw_an = models.IntegerField(default=273,verbose_name="AN Power") - + powerzones = PowerZonesField(default=['Rest', 'Pwr UT2', 'Pwr UT1', @@ -576,7 +577,7 @@ class AdvancedWorkoutForm(ModelForm): class RowerPowerForm(ModelForm): class Meta: model = Rower - fields = ['ftp'] + fields = ['ftp','otwslack'] # Form to set rower's Power zones, including test routines # to enable consistency diff --git a/rowers/templates/rower_form.html b/rowers/templates/rower_form.html index c495cb23..9fc7b380 100644 --- a/rowers/templates/rower_form.html +++ b/rowers/templates/rower_form.html @@ -7,6 +7,7 @@

    Heart Rate Zones

    +

    Set your heart rate zones with this form.

    {% if form.errors %}

    Please correct the error{{ form.errors|pluralize }} below. @@ -27,6 +28,8 @@

    Power Zones

    +

    The power zones are defined relative to power as measured by the + indoor rower.

    {% if powerzonesform.errors %}

    @@ -127,10 +130,15 @@

    -

    Functional Threshold Power

    +

    Functional Threshold Power and OTW Slack

    Use this form to quickly change your zones based on the power of a recent - full out 60 minutes effort. It will update all zones defined above.

    + full out 60 minutes effort on the ergometer. + It will update all zones defined above.

    +

    The OTW Power Slack is the percentage drop of your On-the-water + rowing power + vs the erg power. Typical values are around 15%. This will lower + the power zones for your OTW workouts.

    {{ powerform.as_table }} diff --git a/rowers/views.py b/rowers/views.py index 1b421e1f..2bead2fc 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -4394,7 +4394,11 @@ def cumstats(request,theuser=0, def workout_stats_view(request,id=0,message="",successmessage=""): r = Rower.objects.get(user=request.user) - + try: + w = Workout.objects.get(id=id) + except Workout.DoesNotExist: + raise Http404("Workout doesn't exist") + workstrokesonly = True if request.method == 'POST' and 'workstrokesonly' in request.POST: workstrokesonly = request.POST['workstrokesonly'] @@ -4462,9 +4466,13 @@ def workout_stats_view(request,id=0,message="",successmessage=""): pwr4 = datadf['power']**(4) normp = (pwr4.mean())**(0.25) if not np.isnan(normp): - intensityfactor = datadf['power'].mean()/float(r.ftp) - intensityfactor = normp/float(r.ftp) - tss = 100.*((duration*normp*intensityfactor)/(3600.*r.ftp)) + ftp = float(r.ftp) + if w.workouttype == 'water': + ftp = ftp*(100.-r.otwslack)/100. + + intensityfactor = datadf['power'].mean()/float(ftp) + intensityfactor = normp/float(ftp) + tss = 100.*((duration*normp*intensityfactor)/(3600.*ftp)) if not np.isnan(tss): otherstats['tss'] = { @@ -5298,7 +5306,11 @@ def workout_add_otw_powerplot_view(request,id): r.pw_at, r.pw_tr,r.pw_an])/r.ftp - + + ftp = r.ftp + if w.workouttype == 'water': + ftp = ftp*(100.-r.otwslack)/100. + hrpwrdata = { 'hrmax':r.max, 'hrut2':r.ut2, @@ -5306,7 +5318,7 @@ def workout_add_otw_powerplot_view(request,id): 'hrat':r.at, 'hrtr':r.tr, 'hran':r.an, - 'ftp':r.ftp, + 'ftp':ftp, 'powerperc':serialize_list(powerperc), 'powerzones':serialize_list(r.powerzones), } @@ -5356,6 +5368,9 @@ def workout_add_piechart_view(request,id): r.pw_at, r.pw_tr,r.pw_an])/r.ftp + ftp = float(r.ftp) + if w.workouttype == 'water': + ftp = ftp*(100.-r.otwslack)/100. hrpwrdata = { 'hrmax':r.max, @@ -5364,7 +5379,7 @@ def workout_add_piechart_view(request,id): 'hrat':r.at, 'hrtr':r.tr, 'hran':r.an, - 'ftp':r.ftp, + 'ftp':ftp, 'powerperc':serialize_list(powerperc), 'powerzones':serialize_list(r.powerzones), } @@ -5414,6 +5429,9 @@ def workout_add_power_piechart_view(request,id): r.pw_at, r.pw_tr,r.pw_an])/r.ftp + ftp = float(r.ftp) + if w.workouttype == 'water': + ftp = ftp*(100.-r.otwslack)/100. hrpwrdata = { 'hrmax':r.max, @@ -5422,7 +5440,7 @@ def workout_add_power_piechart_view(request,id): 'hrat':r.at, 'hrtr':r.tr, 'hran':r.an, - 'ftp':r.ftp, + 'ftp':ftp, 'powerperc':serialize_list(powerperc), 'powerzones':serialize_list(r.powerzones), } @@ -5469,6 +5487,9 @@ def workout_add_timeplot_view(request,id): r.pw_at, r.pw_tr,r.pw_an])/r.ftp + ftp = float(r.ftp) + if w.workouttype == 'water': + ftp = ftp*(100.-r.otwslack)/100. hrpwrdata = { 'hrmax':r.max, @@ -5477,7 +5498,7 @@ def workout_add_timeplot_view(request,id): 'hrat':r.at, 'hrtr':r.tr, 'hran':r.an, - 'ftp':r.ftp, + 'ftp':ftp, 'powerperc':serialize_list(powerperc), 'powerzones':serialize_list(r.powerzones), } @@ -5526,6 +5547,9 @@ def workout_add_distanceplot_view(request,id): r.pw_at, r.pw_tr,r.pw_an])/r.ftp + ftp = float(r.ftp) + if w.workouttype == 'water': + ftp = ftp*(100.-r.otwslack)/100. hrpwrdata = { 'hrmax':r.max, @@ -5534,7 +5558,7 @@ def workout_add_distanceplot_view(request,id): 'hrat':r.at, 'hrtr':r.tr, 'hran':r.an, - 'ftp':r.ftp, + 'ftp':ftp, 'powerperc':serialize_list(powerperc), 'powerzones':serialize_list(r.powerzones), } @@ -5581,6 +5605,9 @@ def workout_add_distanceplot2_view(request,id): r.pw_at, r.pw_tr,r.pw_an])/r.ftp + ftp = float(r.ftp) + if w.workouttype == 'water': + ftp = ftp*(100.-r.otwslack)/100. hrpwrdata = { 'hrmax':r.max, @@ -5589,7 +5616,7 @@ def workout_add_distanceplot2_view(request,id): 'hrat':r.at, 'hrtr':r.tr, 'hran':r.an, - 'ftp':r.ftp, + 'ftp':ftp, 'powerperc':serialize_list(powerperc), 'powerzones':serialize_list(r.powerzones), } @@ -5638,6 +5665,9 @@ def workout_add_timeplot2_view(request,id): r.pw_at, r.pw_tr,r.pw_an])/r.ftp + ftp = float(r.ftp) + if w.workouttype == 'water': + ftp = ftp*(100.-r.otwslack)/100. hrpwrdata = { 'hrmax':r.max, @@ -5646,7 +5676,7 @@ def workout_add_timeplot2_view(request,id): 'hrat':r.at, 'hrtr':r.tr, 'hran':r.an, - 'ftp':r.ftp, + 'ftp':ftp, 'powerperc':serialize_list(powerperc), 'powerzones':serialize_list(r.powerzones), } @@ -6387,6 +6417,10 @@ def workout_upload_view(request, r.pw_at, r.pw_tr,r.pw_an])/r.ftp + ftp = float(r.ftp) + if w.workouttype == 'water': + ftp = ftp*(100.-r.otwslack)/100. + hrpwrdata = { 'hrmax':r.max, 'hrut2':r.ut2, @@ -6394,7 +6428,7 @@ def workout_upload_view(request, 'hrat':r.at, 'hrtr':r.tr, 'hran':r.an, - 'ftp':r.ftp, + 'ftp':ftp, 'powerperc':serialize_list(powerperc), 'powerzones':serialize_list(r.powerzones), } @@ -6631,6 +6665,10 @@ def team_workout_upload_view(request,message="", r.pw_at, r.pw_tr,r.pw_an])/r.ftp + ftp = float(r.ftp) + if w.workouttype == 'water': + ftp = ftp*(100.-r.otwslack)/100. + hrpwrdata = { 'hrmax':r.max, 'hrut2':r.ut2, @@ -6638,7 +6676,7 @@ def team_workout_upload_view(request,message="", 'hrat':r.at, 'hrtr':r.tr, 'hran':r.an, - 'ftp':r.ftp, + 'ftp':ftp, 'powerperc':serialize_list(powerperc), 'powerzones':serialize_list(r.powerzones), } @@ -6840,9 +6878,13 @@ def workout_summary_restore_view(request,id,message="",successmessage=""): r.pw_at, r.pw_tr,r.pw_an])/r.ftp + ftp = float(r.ftp) + if row.workouttype == 'water': + ftp = ftp*(100.-r.otwslack)/100. + rr = rrower(hrmax=r.max,hrut2=r.ut2, hrut1=r.ut1,hrat=r.at, - hrtr=r.tr,hran=r.an,ftp=r.ftp, + hrtr=r.tr,hran=r.an,ftp=ftp, powerperc=powerperc,powerzones=r.powerzones) rowdata = rdata(f1,rower=rr) if rowdata == 0: @@ -6951,9 +6993,13 @@ def workout_summary_edit_view(request,id,message="",successmessage="" r.pw_at, r.pw_tr,r.pw_an])/r.ftp + ftp = float(r.ftp) + if row.workouttype == 'water': + ftp = ftp*(100.-r.otwslack)/100. + rr = rrower(hrmax=r.max,hrut2=r.ut2, hrut1=r.ut1,hrat=r.at, - hrtr=r.tr,hran=r.an,ftp=r.ftp, + hrtr=r.tr,hran=r.an,ftp=ftp, powerperc=powerperc,powerzones=r.powerzones) rowdata = rdata(f1,rower=rr) if rowdata == 0: @@ -7272,6 +7318,7 @@ def rower_edit_view(request,message=""): if powerform.is_valid(): cd = powerform.cleaned_data ftp = cd['ftp'] + otwslack = cd['otwslack'] try: r = Rower.objects.get(user=request.user) powerfrac = 100*np.array([r.pw_ut2, @@ -7279,6 +7326,7 @@ def rower_edit_view(request,message=""): r.pw_at, r.pw_tr,r.pw_an])/r.ftp r.ftp = max(min(ftp,650),50) + r.otwslack = max(min(otwslack,50),0) ut2,ut1,at,tr,an = (r.ftp*powerfrac/100.).astype(int) r.pw_ut2 = ut2 r.pw_ut1 = ut1 @@ -7286,7 +7334,7 @@ def rower_edit_view(request,message=""): r.pw_tr = tr r.pw_an = an r.save() - message = "Functional Threshold Value Changed" + message = "FTP and/or OTW slack values changed." messages.info(request,message) url = reverse(rower_edit_view) response = HttpResponseRedirect(url) @@ -7635,9 +7683,13 @@ def strokedatajson(request,id): r.pw_at, r.pw_tr,r.pw_an])/r.ftp + ftp = float(r.ftp) + if row.workouttype == 'water': + ftp = ftp*(100.-r.otwslack)/100. + rr = rrower(hrmax=r.max,hrut2=r.ut2, hrut1=r.ut1,hrat=r.at, - hrtr=r.tr,hran=r.an,ftp=r.ftp, + hrtr=r.tr,hran=r.an,ftp=ftp, powerperc=powerperc,powerzones=r.powerzones) rowdata = rdata(row.csvfilename,rower=rr).df From eb9c7ed33f992f6d1d703c5660c91199ee2e4690 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Thu, 18 May 2017 18:45:17 +0200 Subject: [PATCH 10/10] default OTW slack set to 0 --- rowers/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rowers/models.py b/rowers/models.py index 15046a7a..528c28b9 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -173,7 +173,7 @@ class Rower(models.Model): # Power Zone Data ftp = models.IntegerField(default=226,verbose_name="Functional Threshold Power") - otwslack = models.IntegerField(default=15,verbose_name="OTW Power slack") + otwslack = models.IntegerField(default=0,verbose_name="OTW Power slack") pw_ut2 = models.IntegerField(default=124,verbose_name="UT2 Power") pw_ut1 = models.IntegerField(default=171,verbose_name="UT1 Power")