diff --git a/rowers/dataprep.py b/rowers/dataprep.py index eeaa2b64..ba78268e 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): + goodids = [] + for id in ids: + w = Workout.objects.get(id=id) + if int(w.user.user.id) == int(userid): + goodids.append(id) + + return [Workout.objects.get(id=id) for id in goodids] + def filter_df(datadf,fieldname,value,largerthan=True): try: 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/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 diff --git a/rowers/models.py b/rowers/models.py index fa0c839d..528c28b9 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=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") 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/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/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 %} +
+ Import all NEW +
+
+

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/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): +

+

+

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/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/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/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 56c90669..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), @@ -211,9 +212,12 @@ 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/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), 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), @@ -274,6 +278,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'), 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 fb8ce778..2bead2fc 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 @@ -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): @@ -1310,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, @@ -1499,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: @@ -3111,7 +3150,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'] @@ -3124,7 +3163,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 @@ -4344,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'] @@ -4412,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'] = { @@ -5248,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, @@ -5256,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), } @@ -5306,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, @@ -5314,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), } @@ -5364,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, @@ -5372,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), } @@ -5419,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, @@ -5427,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), } @@ -5476,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, @@ -5484,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), } @@ -5531,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, @@ -5539,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), } @@ -5588,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, @@ -5596,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), } @@ -5640,10 +5720,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), }) @@ -5753,15 +5856,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', @@ -5812,7 +5925,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=""): @@ -5833,6 +5997,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'] @@ -5841,8 +6011,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) @@ -5948,6 +6122,69 @@ 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) + if id==0: + messages.error(request,message) + + else: + w = Workout.objects.get(id=id) + w.uploadedtosporttracks=sporttracksid + 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) + return HttpResponseRedirect(url) + + + # Imports a workout from Concept2 @login_required() def workout_getc2workout_view(request,c2id): @@ -6180,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, @@ -6187,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), } @@ -6424,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, @@ -6431,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), } @@ -6633,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: @@ -6744,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: @@ -7065,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, @@ -7072,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 @@ -7079,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) @@ -7428,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