diff --git a/rowers/dataprep.py b/rowers/dataprep.py index f99a9468..3261c14f 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -158,6 +158,72 @@ def filter_df(datadf, fieldname, value, largerthan=True): return datadf +# joins workouts +def join_workouts(r,ids,title='Joined Workout', + parent=None, + setprivate=False, + forceunit='lbs'): + + message = None + + summary = '' + if parent: + oarlength = parent.oarlength + inboard = parent.inboard + workouttype = parent.workouttype + notes = parent.notes + summary = parent.summary + if parent.privacy == 'hidden': + makeprivate = True + else: + makeprivate = False + + startdatetime = parent.startdatetime + else: + oarlength = 2.89 + inboard = 0.88 + workouttype = 'rower' + notes = '' + summary = '' + makeprivate = False + startdatetime = timezone.now() + + if setprivate == True and makeprivate == False: + makeprivate = True + elif setprivate == False and makeprivate == True: + makeprivate = False + + + # reorder in chronological order + ws = Workout.objects.filter(id__in=ids).order_by("date", "starttime") + files = [w.csvfilename for w in ws] + + row = rdata(files[0]) + + files = files[1:] + + while len(files): + row2 = rdata(files[0]) + if row2 != 0: + row = row+row2 + files = files[1:] + + timestr = strftime("%Y%m%d-%H%M%S") + csvfilename = 'media/df_' + timestr + '.csv' + + row.write_csv(csvfilename,gzip=True) + id, message = save_workout_database(csvfilename, r, + workouttype=workouttype, + title=title, + notes=notes, + oarlength=oarlength, + inboard=inboard, + makeprivate=makeprivate, + dosmooth=False, + consistencychecks=False) + + return (id, message) + def df_resample(datadf): # time stamps must be in seconds @@ -801,6 +867,7 @@ def save_workout_database(f2, r, dosmooth=True, workouttype='rower', pytz.timezone(timezone_str) ).strftime('%H:%M:%S') + if makeprivate: privacy = 'hidden' else: diff --git a/rowers/forms.py b/rowers/forms.py index bad60f81..aa8cf8ed 100644 --- a/rowers/forms.py +++ b/rowers/forms.py @@ -521,7 +521,11 @@ class ChartParamChoiceForm(forms.Form): formaxlabels.pop('time') metricchoices = list(sorted(formaxlabels.items(), key = lambda x:x[1])) - + +class WorkoutJoinParamForm(forms.Form): + workout_name = forms.CharField(required = True, initial = 'Joined Workout') + set_private = forms.BooleanField(initial=False, required = False) + class FusionMetricChoiceForm(ModelForm): class Meta: model = Workout diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index 29edb78c..de097e7a 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -2475,9 +2475,12 @@ def interactive_flex_chart2(id=0,promember=0, slider_work_max = Slider(start=0.0, end=1500,value=1500.0, step=10, title="Max Work per Stroke",callback=callback) callback.args["maxwork"] = slider_work_max - - distmax = 100+100*int(rowdata['distance'].max()/100.) - + + try: + distmax = 100+100*int(rowdata['distance'].max()/100.) + except KeyError: + distmax = 100 + slider_dist_min = Slider(start=0,end=distmax,value=0,step=1, title="Min Distance",callback=callback) callback.args["mindist"] = slider_dist_min diff --git a/rowers/management/commands/processemail.py b/rowers/management/commands/processemail.py index cae0ccb0..f5b6c8f5 100644 --- a/rowers/management/commands/processemail.py +++ b/rowers/management/commands/processemail.py @@ -4,7 +4,7 @@ import sys import os import zipfile - +import re import time from time import strftime @@ -95,6 +95,24 @@ def processattachment(rower, fileobj, title, uploadoptions,testing=False): return workoutid +def get_from_address(message): + + from_address = message.from_address[0].lower() + + if message.encoded: + body = message.text.splitlines() + else: + body = message.get_body().splitlines() + + first_line = body[0].lower() + + if "quiske" in first_line: + match = re.search(r'[\w\.-]+@[\w\.-]+', first_line) + return match.group(0) + + return from_address + + class Command(BaseCommand): def add_arguments(self, parser): parser.add_argument( @@ -131,7 +149,9 @@ class Command(BaseCommand): uploadoptions = uploads.upload_options(body) - from_address = message.from_address[0].lower() + + from_address = get_from_address(message) + name = message.subject # get a list of users # theusers = User.objects.filter(email=from_address) diff --git a/rowers/templates/advancededit.html b/rowers/templates/advancededit.html index 0df1fff3..a43e0c04 100644 --- a/rowers/templates/advancededit.html +++ b/rowers/templates/advancededit.html @@ -6,198 +6,217 @@ {% block content %}
- - {% if form.errors %} -

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

- {% endif %} - -

{{ workout.name }} - Advanced

- {% if user.rower.rowerplan == 'basic' %} -

This is a preview of the page with advanced functionality for Pro users. - See - the page about Pro membership for more information and to sign up for Pro Membership -{% endif %} -

-

- Edit Workout -

-
-
-

- Workflow View -

-
-
-

- Export -

- -
-
- - - - - - - - - - - - -
Date:{{ workout.date }}
Time:{{ workout.starttime }}
Distance:{{ workout.distance }}m
Duration:{{ workout.duration |durationprint:"%H:%M:%S.%f" }}
Public link to this workout - https://rowsandall.com/rowers/workout/{{ workout.id }} - -
-
- -
-
- {% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %} - Compare Workouts - {% else %} - Compare Workouts +
+ {% if form.errors %} +

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

+ {% endif %} + +

{{ workout.name }} - Advanced

+ {% if user.rower.rowerplan == 'basic' %} +

This is a preview of the page with advanced functionality for Pro users. + See + the page about Pro membership + for more information and to sign up for Pro Membership

{% endif %} -

- Compare this workout to other workouts. Plot HR, SPM, or pace vs time or distance for the two workouts. -

-
- - Flexible Interactive Plot - -

- Flexible Interactive plot. Pick your own X and Y axis parameters. -

+
+
+

+ Edit Workout +

+
+
+

+ Workflow View +

+
+
+

+ Export +

+
-
-

+

+ + + + + + + + + + + + +
Date:{{ workout.date }}
Time:{{ workout.starttime }}
Distance:{{ workout.distance }}m
Duration:{{ workout.duration |durationprint:"%H:%M:%S.%f" }}
Public link to this workout + https://rowsandall.com/rowers/workout/{{ workout.id }} + +
+
+ +
+
{% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %} - Edit Intervals + Compare Workouts {% else %} - Edit Intervals + Compare Workouts {% endif %} -

- Enter or change the interval and summary data for your workout - -

- Enter or change the interval and summary data for your workout -

+

+ Compare this workout to other workouts. Plot HR, SPM, or pace vs time or distance for the two workouts. +

+
+
+ + Flexible Interactive Plot + +

+ Flexible Interactive plot. Pick your own X and Y axis parameters. +

+
+ +
+

+ {% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %} + Edit Intervals + {% else %} + Edit Intervals + {% endif %} +

+ Enter or change the interval and summary data for your workout + +

+ Enter or change the interval and summary data for your workout +

+
-
-
- -
-

- {% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %} - Dist Metrics Plot - {% else %} - Dist Metrics Plot - {% endif %} -

-

- Various advanced stroke metrics plotted versus distance. -

+
+ +
+

+ {% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %} + Dist Metrics Plot + {% else %} + Dist Metrics Plot + {% endif %} +

+

+ Various advanced stroke metrics plotted versus distance. +

+
+
+

+ {% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %} + Time Metrics Plot + {% else %} + Time Metrics Plot + {% endif %} +

+

+ Various advanced stroke metrics plotted versus time. +

+
+
+

+ {% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %} + Power Histogram + {% else %} + Power Histogram + {% endif %} +

+

+ Plot the Power Histogram of this workout +

+
-
-

- {% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %} - Time Metrics Plot - {% else %} - Time Metrics Plot - {% endif %} -

-

- Various advanced stroke metrics plotted versus time. -

+
+
+

+ {% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %} + Glue + {% else %} + Glue + {% endif %} +

+

+ Glue multiple workouts together (into one workout). For example, to combine separately recorded intervals. +

+
+ +
+

+ {% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %} + Sensor Fusion + {% else %} + Sensor Fusion + {% endif %} +

+

+ Merge data from another source into this workout +

+
+
+

+ {% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %} + Split Workout + {% else %} + Split Workout + {% endif %} +

+

+ Split workout into two seperate workouts +

+
-
-

- Big Interactive Plot -

-

- See (and save) the big interactive plot -

+
+
+

+ Big Interactive Plot +

+

+ See (and save) the big interactive plot +

+
-
- -
-

- {% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %} - Power Histogram - {% else %} - Dist Metrics Plot - {% endif %} -

-

- Plot the Power Histogram of this workout -

-
-
-

- {% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %} - Sensor Fusion - {% else %} - Sensor Fusion - {% endif %} -

-

- Merge data from another source into this workout -

-
-
-

- {% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %} - Split Workout - {% else %} - Split Workout - {% endif %} -

-

- Split workout into two seperate workouts -

-
-
-
-
- - - - - {{ interactiveplot |safe }} - +
+ + + + + {{ interactiveplot |safe }} + - - -
- {{ the_div |safe }} -
+ + +
+ {{ the_div |safe }} +
+
{% endblock %} diff --git a/rowers/templates/advancedotw.html b/rowers/templates/advancedotw.html index 3c4999fc..0f54ff2b 100644 --- a/rowers/templates/advancedotw.html +++ b/rowers/templates/advancedotw.html @@ -187,14 +187,18 @@
-
-

- Big Interactive Plot -

-

- See (and save) the big interactive plot -

-
+
+

+ {% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %} + Glue + {% else %} + Glue + {% endif %} +

+

+ Glue multiple workouts together (into one workout). For example, to combine separately recorded intervals. +

+

{% if user.rower.rowerplan == 'pro' or user.rower.rowerplan == 'coach' %} @@ -220,6 +224,17 @@

+
+
+
+

+ Big Interactive Plot +

+

+ See (and save) the big interactive plot +

+
+
diff --git a/rowers/templates/list_workouts.html b/rowers/templates/list_workouts.html index c04b7727..e051f0b6 100644 --- a/rowers/templates/list_workouts.html +++ b/rowers/templates/list_workouts.html @@ -195,7 +195,7 @@
-
+
{% if rankingonly and not team %}
All Workouts @@ -205,6 +205,9 @@ Ranking Pieces Only
{% endif %} +

 

{% if team %}
-
+
{% if workouts.has_previous %} {% if request.GET.q %} diff --git a/rowers/templates/team_compare_select.html b/rowers/templates/team_compare_select.html index 3a7dd211..176f5c69 100644 --- a/rowers/templates/team_compare_select.html +++ b/rowers/templates/team_compare_select.html @@ -146,7 +146,7 @@

Warning: You are on an experimental part of the site. Use at your own risk.

Select two or more workouts on the left, set your plot settings below, - and press submit"

+ and press submit

{% csrf_token %} {{ chartform.as_table }} diff --git a/rowers/templates/workout_join_select.html b/rowers/templates/workout_join_select.html new file mode 100644 index 00000000..ece4ea83 --- /dev/null +++ b/rowers/templates/workout_join_select.html @@ -0,0 +1,167 @@ +{% extends "base.html" %} +{% load staticfiles %} +{% load rowerfilters %} + +{% block title %}Workouts{% endblock %} + +{% block content %} + + + + + + +
+ {% include "teambuttons.html" with teamid=team.id team=team %} +
+
+

{{ team.name }} Team Workouts

+
+
+
+ + {% if team %} +
+ {% else %} + + {% endif %} +
+
+ {{ dateform.as_table }} +
+ {% csrf_token %} +
+
+ +
+ + {% if team %} +
+ {% else %} + + {% endif %} +
+ + {{ modalityform.as_table }} +
+ {% csrf_token %} +
+
+ +
+
+
+
+ {% if team %} +
+ {% else %} + +{% endif %} +
+ +
+
+ +
+
+
+ +
+ + +
+
+ + + {% if workouts %} + + Toggle All
+ + + {{ form.as_table }} +
+ +{% else %} +

No workouts found

+{% endif %} +
+
+

Warning: You are on an experimental part of the site. Use at your own risk.

+

Select two or more workouts on the left, + and press submit

+ + {{ joinparamform.as_table }} +
+
+

+ {% csrf_token %} + +

+
+
+

You can use the date and search forms above to search through all + workouts from this team.

+
+
+
+ + +{% endblock %} diff --git a/rowers/urls.py b/rowers/urls.py index 46406980..193b2a2e 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -131,6 +131,11 @@ urlpatterns = [ url(r'^team-compare-select/team/(?P\d+)/$',views.team_comparison_select), url(r'^team-compare-select/(?P\w+.*)/(?P\w+.*)$',views.team_comparison_select), url(r'^team-compare-select/$',views.team_comparison_select), + url(r'^workouts-join-select/team/(?P\d+)/(?P\w+.*)/(?P\w+.*)$',views.workouts_join_select), + url(r'^workouts-join$',views.workouts_join_view), + url(r'^workouts-join-select/team/(?P\d+)/$',views.workouts_join_select), + url(r'^workouts-join-select/(?P\w+.*)/(?P\w+.*)$',views.workouts_join_select), + url(r'^workouts-join-select/$',views.workouts_join_select), url(r'^user-boxplot-select/user/(?P\d+)/(?P\w+.*)/(?P\w+.*)$',views.user_boxplot_select), url(r'^user-boxplot-select/user/(?P\d+)/$',views.user_boxplot_select), url(r'^user-boxplot-select/(?P\w+.*)/(?P\w+.*)$',views.user_boxplot_select), diff --git a/rowers/views.py b/rowers/views.py index 58cbdee0..97315c9a 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -46,7 +46,7 @@ from rowers.forms import ( RegistrationFormUniqueEmail,CNsummaryForm,UpdateWindForm, UpdateStreamForm,WorkoutMultipleCompareForm,ChartParamChoiceForm, FusionMetricChoiceForm,BoxPlotChoiceForm,MultiFlexChoiceForm, - TrendFlexModalForm,WorkoutSplitForm, + TrendFlexModalForm,WorkoutSplitForm,WorkoutJoinParamForm, ) from rowers.models import Workout, User, Rower, WorkoutForm,FavoriteChart from rowers.models import ( @@ -3940,6 +3940,7 @@ def oterankings_view(request,theuser=0, paulintercept = 1 message = res[4] else: + ratio = 1 script = '' div = '

No ranking pieces found.

' paulslope = 1 @@ -4033,7 +4034,10 @@ def oterankings_view(request,theuser=0, pwr2 = p1[0]/(1+t/p1[2]) pwr2 += p1[1]/(1+t/p1[3]) - pwr2 *= ratio + try: + pwr2 *= ratio + except UnboundLocalError: + pass if pwr2 <= 0: pwr2 = 50. @@ -4190,6 +4194,209 @@ def workout_setprivate_view(request,id, }) return HttpResponseRedirect(url) +# Joining workout +@user_passes_test(ispromember,login_url="/",redirect_field_name=None) +def workouts_join_view(request): + promember=0 + if not request.user.is_anonymous(): + r = getrower(request.user) + result = request.user.is_authenticated() and ispromember(request.user) + if result: + promember=1 + + + if request.method == 'POST' and 'workouts' in request.POST: + form = WorkoutMultipleCompareForm(request.POST) + paramform = WorkoutJoinParamForm(request.POST) + if form.is_valid() and paramform.is_valid(): + workout_name = paramform.cleaned_data['workout_name'] + set_private = paramform.cleaned_data['set_private'] + + cd = form.cleaned_data + workouts = cd['workouts'] + ids = [int(w.id) for w in workouts] + request.session['ids'] = ids + + + id,message = dataprep.join_workouts(r,ids, + title=workout_name, + setprivate=set_private) + + if message: + messages.error(request,message) + + url = reverse(workout_edit_view, + kwargs = { + 'id':int(id), + }) + + return HttpResponseRedirect(url) + + else: + return HttpResponse("form is not valid") + + else: + url = reverse(workouts_join_select) + return HttpResponseRedirect(url) + +@user_passes_test(ispromember,login_url="/",redirect_field_name=None) +def workouts_join_select(request, + startdatestring="", + enddatestring="", + message='', + successmessage='', + startdate=timezone.now()-datetime.timedelta(days=30), + enddate=timezone.now()+datetime.timedelta(days=1), + teamid=0): + + try: + r = getrower(request.user) + except Rower.DoesNotExist: + raise Http404("Rower doesn't exist") + + if 'startdate' in request.session: + startdate = iso8601.parse_date(request.session['startdate']) + + + if 'enddate' in request.session: + enddate = iso8601.parse_date(request.session['enddate']) + + + if 'waterboattype' in request.session: + waterboattype = request.session['waterboattype'] + else: + waterboattype = ['1x','2x','2-','4x','4-','8+'] + + + if 'modalities' in request.session: + modalities = request.session['modalities'] + if len(modalities) > 1: + modality = 'all' + else: + modality = modalities[0] + else: + modalities = [m[0] for m in types.workouttypes] + modality = 'all' + + if request.method == 'POST' and 'daterange' in request.POST: + dateform = DateRangeForm(request.POST) + if dateform.is_valid(): + startdate = dateform.cleaned_data['startdate'] + enddate = dateform.cleaned_data['enddate'] + startdatestring = startdate.strftime('%Y-%m-%d') + enddatestring = enddate.strftime('%Y-%m-%d') + request.session['startdate'] = startdatestring + request.session['enddate'] = enddatestring + else: + dateform = DateRangeForm(initial={ + 'startdate':startdate, + 'enddate':enddate, + }) + + + if request.method == 'POST' and 'modality' in request.POST: + modalityform = TrendFlexModalForm(request.POST) + if modalityform.is_valid(): + modality = modalityform.cleaned_data['modality'] + waterboattype = modalityform.cleaned_data['waterboattype'] + if modality == 'all': + modalities = [m[0] for m in types.workouttypes] + else: + modalities = [modality] + + if modality != 'water': + waterboattype = [b[0] for b in types.boattypes] + + + request.session['modalities'] = modalities + request.session['waterboattype'] = waterboattype + + negtypes = [] + for b in types.boattypes: + if b[0] not in waterboattype: + negtypes.append(b[0]) + + startdate = datetime.datetime.combine(startdate,datetime.time()) + enddate = datetime.datetime.combine(enddate,datetime.time(23,59,59)) + enddate = enddate+datetime.timedelta(days=1) + + if startdatestring: + startdate = iso8601.parse_date(startdatestring) + if enddatestring: + enddate = iso8601.parse_date(enddatestring) + + if enddate < startdate: + s = enddate + enddate = startdate + startdate = s + + try: + theteam = Team.objects.get(id=teamid) + except Team.DoesNotExist: + theteam = 0 + + if r.rowerplan == 'basic' and theteam==0: + raise Http404("Not allowed") + + if theteam and (theteam.viewing == 'allmembers' or theteam.manager == request.user): + workouts = Workout.objects.filter(team=theteam, + startdatetime__gte=startdate, + startdatetime__lte=enddate, + workouttype__in=modalities).order_by("-date", "-starttime").exclude(boattype__in=negtypes) + elif theteam and theteam.viewing == 'coachonly': + workouts = Workout.objects.filter(team=theteam,user=r, + startdatetime__gte=startdate, + startdatetime__lte=enddate, + workouttype__in=modalities).order_by("-date","-starttime").exclude(boattype__in=negtypes) + + + else: + theteam = None + workouts = Workout.objects.filter(user=r, + startdatetime__gte=startdate, + startdatetime__lte=enddate, + workouttype__in=modalities).order_by("-date", "-starttime").exclude(boattype__in=negtypes) + + query = request.GET.get('q') + if query: + query_list = query.split() + workouts = workouts.filter( + reduce(operator.and_, + (Q(name__icontains=q) for q in query_list)) | + reduce(operator.and_, + (Q(notes__icontains=q) for q in query_list)) + ) + + form = WorkoutMultipleCompareForm() + form.fields["workouts"].queryset = workouts + + if theteam: + theid = theteam.id + else: + theid = 0 + + joinparamform = WorkoutJoinParamForm() + modalityform = TrendFlexModalForm(initial={ + 'modality':modality, + 'waterboattype':waterboattype + }) + + + messages.info(request,successmessage) + messages.error(request,message) + + return render(request, 'workout_join_select.html', + {'workouts': workouts, + 'dateform':dateform, + 'startdate':startdate, + 'enddate':enddate, + 'team':theteam, + 'form':form, + 'joinparamform':joinparamform, + 'modalityform':modalityform, + 'teams':get_my_teams(request.user), + }) + # Team comparison @login_required() def team_comparison_select(request, @@ -5563,7 +5770,7 @@ def workout_fusion_list(request,id=0,message='',successmessage='', }) except Rower.DoesNotExist: raise Http404("User has no rower instance") - + # Basic 'EDIT' view of workout def workout_view(request,id=0): request.session['referer'] = absolute(request)['PATH'] @@ -8657,7 +8864,8 @@ def workout_upload_view(request, optionsform = UploadOptionsForm(request.POST) if form.is_valid(): - f = request.FILES['file'] +# f = request.FILES['file'] + f = form.cleaned_data['file'] res = handle_uploaded_file(f) t = form.cleaned_data['title']