diff --git a/rowers/c2stuff.py b/rowers/c2stuff.py index aa29acb3..9cd176d1 100644 --- a/rowers/c2stuff.py +++ b/rowers/c2stuff.py @@ -449,6 +449,7 @@ def get_username(access_token): try: res = me_json['data']['username'] + id = me_json['data']['id'] except KeyError: res = None @@ -482,7 +483,7 @@ def process_callback(request): access_token = get_token(code) - username = get_username(access_token) + username,id = get_username(access_token) return HttpResponse("got a user name: %s" % username) diff --git a/rowers/forms.py b/rowers/forms.py index 23d67fb4..6f251a2e 100644 --- a/rowers/forms.py +++ b/rowers/forms.py @@ -252,3 +252,23 @@ class StatsOptionsForm(forms.Form): paddle = forms.BooleanField(initial=False,required=False) snow = forms.BooleanField(initial=False,required=False) other = forms.BooleanField(initial=False,required=False) + +class WorkoutMultipleCompareForm(forms.Form): + workouts = forms.ModelMultipleChoiceField(queryset=Workout.objects.all(), + widget=forms.CheckboxSelectMultiple()) + +from rowers.interactiveplots import axlabels + +axlabels.pop('None') +axlabels = list(axlabels.items()) + + +class ChartParamChoiceForm(forms.Form): + plotchoices = ( + ('line','Line Plot'), + ('scatter','Scatter Plot'), + ) + xparam = forms.ChoiceField(choices=axlabels,initial='distance') + yparam = forms.ChoiceField(choices=axlabels,initial='hr') + plottype = forms.ChoiceField(choices=plotchoices,initial='scatter') + teamid = forms.IntegerField(widget=forms.HiddenInput()) diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index c59b4124..6789d0ce 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -8,6 +8,8 @@ from rowingdata import rowingdata as rrdata from django.utils import timezone +from bokeh.palettes import Dark2_8 as palette +import itertools from bokeh.plotting import figure, ColumnDataSource, Figure,curdoc from bokeh.models import CustomJS,Slider from bokeh.charts import Histogram,HeatMap @@ -52,7 +54,7 @@ import rowers.dataprep as dataprep axlabels = { 'time': 'Time', 'distance': 'Distance (m)', - 'cumdist': 'Distance (m)', + 'cumdist': 'Cumulative Distance (m)', 'hr': 'Heart Rate (bpm)', 'spm': 'Stroke Rate (spm)', 'pace': 'Pace (/500m)', @@ -1579,6 +1581,139 @@ def interactive_bar_chart(id=0,promember=0): return [script,div] +def interactive_multiple_compare_chart(ids,xparam,yparam,plottype='line', + promember=0, + labeldict=None): + columns = [xparam,yparam, + 'ftime','distance','fpace', + 'power','hr','spm', + 'time','pace','workoutstate', + 'workoutid'] + + datadf = dataprep.getsmallrowdata_db(columns,ids=ids) + + yparamname = axlabels[yparam] + + #datadf = datadf[datadf[yparam] > 0] + + #datadf = datadf[datadf[xparam] > 0] + + # check if dataframe not empty + if datadf.empty: + return ['','

No non-zero data in selection

','',''] + + + + if xparam=='distance': + xaxmax = datadf['distance'].max() + xaxmin = datadf['distance'].min() + else: + xaxmax = yaxmaxima[xparam] + xaxmin = yaxminima[xparam] + + + x_axis_type = 'linear' + y_axis_type = 'linear' + + # Add hover to this comma-separated string and see what changes + if (promember==1): + TOOLS = 'save,pan,box_zoom,wheel_zoom,reset,tap,resize,crosshair' + else: + TOOLS = 'pan,box_zoom,wheel_zoom,reset,tap,crosshair' + + if yparam == 'pace': + y_axis_type = 'datetime' + yaxmax = 90. + yaxmin = 150. + + if xparam == 'time': + x_axis_type = 'datetime' + + if xparam != 'time': + xvals = xaxmin+np.arange(100)*(xaxmax-xaxmin)/100. + else: + xvals = np.arange(100) + + plot = Figure(x_axis_type=x_axis_type,y_axis_type=y_axis_type, + tools=TOOLS, + toolbar_location="above", + plot_width=920, + toolbar_sticky=False) + + colors = itertools.cycle(palette) + + cntr = 0 + + for id,color in itertools.izip(ids,colors): + group = datadf[datadf['workoutid']==int(id)].copy() + group.sort_values(by='time',ascending=True,inplace=True) + group['x'] = group[xparam] + group['y'] = group[yparam] + + ymean = group['y'].mean() + ylabel = Label(x=100,y=70+20*cntr, + x_units='screen',y_units='screen', + text=yparam+": {ymean:6.2f}".format(ymean=ymean), + background_fill_alpha=.7, + text_color=color, + ) + if yparam != 'time' and yparam != 'pace': + plot.add_layout(ylabel) + + print cntr,id,len(group),ymean + + source = ColumnDataSource( + group + ) + + if labeldict: + legend=labeldict[id] + else: + legend=str(id) + + if plottype=='line': + plot.line('x','y',source=source,color=color,legend=legend) + else: + plot.scatter('x','y',source=source,color=color,legend=legend, + fill_alpha=0.4,line_color=None) + + cntr += 1 + + plot.legend.location='bottom_right' + plot.xaxis.axis_label = axlabels[xparam] + plot.yaxis.axis_label = axlabels[yparam] + + if (xparam != 'time') and (xparam != 'distance') and (xparam != 'cumdist'): + xrange1 = Range1d(start=yaxminima[xparam],end=yaxmaxima[xparam]) + plot.x_range = xrange1 + + if xparam == 'time': + xrange1 = Range1d(start=xaxmin,end=xaxmax) + plot.x_range = xrange1 + plot.xaxis[0].formatter = DatetimeTickFormatter( + hours = ["%H"], + minutes = ["%M"], + seconds = ["%S"], + days = ["0"], + months = [""], + years = [""] + ) + + + if yparam == 'pace': + plot.yaxis[0].formatter = DatetimeTickFormatter( + seconds = ["%S"], + minutes = ["%M"] + ) + + script, div = components(plot) + + + + return [script,div] + + + def interactive_comparison_chart(id1=0,id2=0,xparam='distance',yparam='spm', promember=0,plottype='line'): diff --git a/rowers/models.py b/rowers/models.py index c1dd3bbb..b78740e6 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -398,17 +398,23 @@ class Workout(models.Model): privacy = models.CharField(default='visible',max_length=30, choices=privacychoices) - def __str__(self): + def __unicode__(self): date = self.date name = self.name + distance = str(self.distance) + ownerfirst = self.user.user.first_name + ownerlast = self.user.user.last_name + duration = self.duration - try: - stri = date.strftime('%Y-%m-%d')+'_'+name - except AttributeError: - stri = str(date)+'_'+name - - + stri = u'{d} {n} {dist}m {duration:%H:%M:%S} {ownerfirst} {ownerlast}'.format( + d = date.strftime('%Y-%m-%d'), + n = name, + dist = distance, + duration = duration, + ownerfirst = ownerfirst, + ownerlast = ownerlast, + ) return stri @@ -890,7 +896,7 @@ class WorkoutComment(models.Model): def __unicode__(self): return u'Comment to: {w} by {u1} {u2}'.format( - w=self.workout.name, + w=self.workout, u1 = self.user.first_name, u2 = self.user.last_name, ) diff --git a/rowers/stravastuff.py b/rowers/stravastuff.py index 144787f2..67780a93 100644 --- a/rowers/stravastuff.py +++ b/rowers/stravastuff.py @@ -39,15 +39,18 @@ from rowsandall_app.settings import C2_CLIENT_ID, C2_REDIRECT_URI, C2_CLIENT_SEC def ewmovingaverage(interval,window_size): # Experimental code using Exponential Weighted moving average - intervaldf = pd.DataFrame({'v':interval}) - idf_ewma1 = intervaldf.ewm(span=window_size) - idf_ewma2 = intervaldf[::-1].ewm(span=window_size) + try: + intervaldf = pd.DataFrame({'v':interval}) + idf_ewma1 = intervaldf.ewm(span=window_size) + idf_ewma2 = intervaldf[::-1].ewm(span=window_size) + + i_ewma1 = idf_ewma1.mean().ix[:,'v'] + i_ewma2 = idf_ewma2.mean().ix[:,'v'] - i_ewma1 = idf_ewma1.mean().ix[:,'v'] - i_ewma2 = idf_ewma2.mean().ix[:,'v'] - - interval2 = np.vstack((i_ewma1,i_ewma2[::-1])) - interval2 = np.mean( interval2, axis=0) # average + interval2 = np.vstack((i_ewma1,i_ewma2[::-1])) + interval2 = np.mean( interval2, axis=0) # average + except ValueError: + interval2 = interval return interval2 @@ -234,7 +237,7 @@ def handle_stravaexport(f2,workoutname,stravatoken,description=''): act = client.upload_activity(f2,'tcx',name=workoutname) try: - res = act.wait(poll_interval=5.0) + res = act.wait(poll_interval=5.0,timeout=30) message = 'Workout successfully synchronized to Strava' except: res = 0 @@ -246,6 +249,7 @@ def handle_stravaexport(f2,workoutname,stravatoken,description=''): act = client.update_activity(res.id,activity_type='Rowing',description=description) else: message = 'Strava upload timed out.' + return (0,message) return (res.id,message) diff --git a/rowers/templates/biginteractive1.html b/rowers/templates/biginteractive1.html index 3613da98..97061e31 100644 --- a/rowers/templates/biginteractive1.html +++ b/rowers/templates/biginteractive1.html @@ -6,57 +6,57 @@ {% block content %} - - + + - {{ interactiveplot |safe }} +{{ interactiveplot |safe }} - - + +
- - -

Interactive Plot

- - {% if user.is_authenticated and mayedit %} -
-

- Edit Workout -

-
-
-

- Advanced Edit -

- -
- {% endif %} - -
+ + +

Interactive Plot

+ + {% if user.is_authenticated and mayedit %} +
+

+ Edit Workout +

+
+
+

+ Advanced Edit +

+ +
+ {% endif %} + +
{{ the_div|safe }} +
+
-
- -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/rowers/templates/export.html b/rowers/templates/export.html index 910cbd10..b2cf83e9 100644 --- a/rowers/templates/export.html +++ b/rowers/templates/export.html @@ -6,88 +6,91 @@ {% block content %}
+ + +

Export Workout

+
+

+ Edit Workout +

+
+
+

+ Advanced Edit +

+ +
+ +
+

+ Click on the icon to upload this workout to your site of choice. A checkmark indicates that the workout has already been uploaded. If the button is grayed out, click it to authorize the connection to that site. Use TCX or CSV export to email a TCX or CSV file of your workout to yourself. +

+ +
-

Export Workout

- -
-

- Edit Workout -

-
-
-

- Advanced Edit -

- -
- -
-

-Click on the icon to upload this workout to your site of choice. A checkmark indicates that the workout has already been uploaded. If the button is grayed out, click it to authorize the connection to that site. Use TCX or CSV export to email a TCX or CSV file of your workout to yourself. -

- + {% if workout.uploadedtoc2 == 0 %} + {% if user.rower.c2token == None or user.rower.c2token == '' %} +
+ + C2 icon +
+ {% else %} +
+ Concept2 icon +
+ {% endif %} + {% else %} +
+ + Concept2 icon
- -{% if workout.uploadedtoc2 == 0 %} - {% if user.rower.c2token == None or user.rower.c2token == '' %} -
- - C2 icon -
- {% else %} -
- Concept2 icon -
- {% endif %} -{% else %} -
- Concept2 icon -
{% endif %} {% if workout.uploadedtostrava == 0 %} - {% if user.rower.stravatoken == None or user.rower.stravatoken == '' %} -
- - Strava icon -
- {% else %} -
- Strava icon -
- {% endif %} +{% if user.rower.stravatoken == None or user.rower.stravatoken == '' %} +
+ + Strava icon +
{% else %} -
- Concept2 icon -
+
+ Strava icon +
+{% endif %} +{% else %} +
+ + Concept2 icon +
{% endif %} {% if workout.uploadedtosporttracks == 0 %} - {% if user.rower.sporttrackstoken == None or user.rower.sporttrackstoken == '' %} -
- - SportTracks icon -
- {% else %} -
- - SportTracks icon -
- {% endif %} +{% if user.rower.sporttrackstoken == None or user.rower.sporttrackstoken == '' %} +
+ + SportTracks icon +
{% else %} -
- Concept2 icon -
+
+ + SportTracks icon +
{% endif %} -
- - TCX Export -
+{% else %} +
+ + Concept2 icon +
+{% endif %} +
+ + TCX Export +
-
- - CSV Export -
+
+ + CSV Export +
@@ -100,16 +103,16 @@ You only need to do this once. After that, the site will have access until you revoke the authorization for the "rowingdata" app.

-

connect with strava

+

connect with strava

-

connect with Concept2

+

connect with Concept2

-

connect with SportTracks

+

connect with SportTracks

@@ -117,4 +120,4 @@ revoke the authorization for the "rowingdata" app.

-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/rowers/templates/list_workouts.html b/rowers/templates/list_workouts.html index 891e596b..33408fe0 100644 --- a/rowers/templates/list_workouts.html +++ b/rowers/templates/list_workouts.html @@ -112,6 +112,13 @@
+ {% if team %} +
+ +
+ {% endif %}
{% if announcements %}

What's New?

diff --git a/rowers/templates/multicompare.html b/rowers/templates/multicompare.html new file mode 100644 index 00000000..c5724579 --- /dev/null +++ b/rowers/templates/multicompare.html @@ -0,0 +1,58 @@ +{% extends "base.html" %} +{% load staticfiles %} +{% load rowerfilters %} + +{% block title %}View Comparison {% endblock %} + +{% block content %} + + + + +{{ interactiveplot |safe }} + + + + + +
+

Interactive Comparison

+ + +
+ Team Page +
+
+ + + +
+ {{ the_div|safe }} +
+
+
+ +{% endblock %} diff --git a/rowers/templates/team_compare_select.html b/rowers/templates/team_compare_select.html new file mode 100644 index 00000000..d855a82b --- /dev/null +++ b/rowers/templates/team_compare_select.html @@ -0,0 +1,94 @@ +{% extends "base.html" %} +{% load staticfiles %} +{% load rowerfilters %} + +{% block title %}Workouts{% endblock %} + +{% block content %} + + + + +
+ +
+ + {% if team %} +
+ {% else %} + + {% endif %} + + + {{ dateform.as_table }} +
+ {% csrf_token %} +
+
+ +
+
+ {% if team %} +
+ {% else %} + +{% endif %} +
+ +
+
+ +
+
+
+ +
+ +
+

{{ team.name }} Team Workouts

+
+ +
+
+ + + {% if workouts %} + + + + {{ 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, set your plot settings below, + and press submit"

+ {% csrf_token %} + + {{ chartform.as_table }} +
+
+

+ +

+
+
+

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

+

TIP: Agree with your team members to put tags (e.g. '8x500m') in the notes section of + your workouts. That makes it easy to search.

+
+
+
+ + +{% endblock %} diff --git a/rowers/urls.py b/rowers/urls.py index e54bbac2..0b1f90c8 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -113,6 +113,13 @@ urlpatterns = [ url(r'^list-workouts/team/(?P\d+)/$',views.workouts_view), url(r'^list-workouts/(?P\w+.*)/(?P\w+.*)$',views.workouts_view), url(r'^list-workouts/$',views.workouts_view), + url(r'^team-compare-select/c/(?P\w+.*)/$',views.team_comparison_select), + url(r'^team-compare-select/s/(?P\w+.*)/$',views.team_comparison_select), + url(r'^team-compare-select/c/(?P\w+.*)/s/(?P\w+.*)$',views.team_comparison_select), + url(r'^team-compare-select/team/(?P\d+)/(?P\w+.*)/(?P\w+.*)$',views.team_comparison_select), + 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'^list-graphs/$',views.graphs_view), url(r'^(?P\d+)/ote-bests/(?P\w+.*)/(?P\w+.*)$',views.rankings_view), url(r'^(?P\d+)/ote-bests/(?P\d+)$',views.rankings_view), @@ -204,6 +211,7 @@ urlpatterns = [ url(r'^workout/(\d+)/stravauploadw/$',views.workout_strava_upload_view), url(r'^workout/(\d+)/recalcsummary/$',views.workout_recalcsummary_view), url(r'^workout/(\d+)/sporttracksuploadw/$',views.workout_sporttracks_upload_view), + url(r'^multi-compare$',views.multi_compare_view), url(r'^me/teams/c/(?P\w+.*)/s/(?P\w+.*)$',views.rower_teams_view), url(r'^me/teams/s/(?P\w+.*)$',views.rower_teams_view), url(r'^me/teams/c/(?P\w+.*)$',views.rower_teams_view), diff --git a/rowers/views.py b/rowers/views.py index c0b48523..cd06ba03 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -26,7 +26,7 @@ from rowers.forms import ( StatsOptionsForm,PredictedPieceForm,DateRangeForm,DeltaDaysForm, EmailForm, RegistrationForm, RegistrationFormTermsOfService, RegistrationFormUniqueEmail,CNsummaryForm,UpdateWindForm, - UpdateStreamForm + UpdateStreamForm,WorkoutMultipleCompareForm,ChartParamChoiceForm ) from rowers.models import Workout, User, Rower, WorkoutForm,FavoriteChart from rowers.models import ( @@ -810,7 +810,10 @@ def workout_strava_upload_view(request,id=0): message = mes w.uploadedtostrava = -1 w.save() - os.remove(tcxfile) + try: + os.remove(tcxfile) + except WindowsError: + pass url = reverse(workout_export_view, kwargs = { 'id':str(w.id), @@ -1958,6 +1961,137 @@ def workout_setprivate_view(request,id, }) return HttpResponseRedirect(url) +# Team comparison +@login_required() +def team_comparison_select(request, + startdatestring="", + enddatestring="", + message='', + successmessage='', + startdate=timezone.now()-datetime.timedelta(days=30), + enddate=timezone.now()+datetime.timedelta(days=1), + teamid=0): + + try: + r = Rower.objects.get(user=request.user) + except Rower.DoesNotExist: + raise Http404("Rower doesn't exist") + + if request.method == 'POST': + dateform = DateRangeForm(request.POST) + if dateform.is_valid(): + startdate = dateform.cleaned_data['startdate'] + enddate = dateform.cleaned_data['enddate'] + else: + dateform = DateRangeForm(initial={ + 'startdate':startdate, + 'enddate':enddate, + }) + + 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: + raise Http404("Team doesn't exist") + + if theteam.viewing == 'allmembers' or theteam.manager == request.user: + workouts = Workout.objects.filter(team=theteam, + startdatetime__gte=startdate, + startdatetime__lte=enddate).order_by("-date", "-starttime") + elif theteam.viewing == 'coachonly': + workouts = Workout.objects.filter(team=theteam,user=r, + startdatetime__gte=startdate, + startdatetime__lte=enddate).order_by("-date","-starttime") + + + else: + theteam = None + workouts = Workout.objects.filter(user=r, + startdatetime__gte=startdate, + startdatetime__lte=enddate).order_by("-date", "-starttime") + + 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 + + chartform = ChartParamChoiceForm(initial={'teamid':theteam.id}) + + return render(request, 'team_compare_select.html', + {'workouts': workouts, + 'message': message, + 'successmessage':successmessage, + 'dateform':dateform, + 'startdate':startdate, + 'enddate':enddate, + 'team':theteam, + 'form':form, + 'chartform':chartform, + }) + +@login_required() +def multi_compare_view(request): + promember=0 + if not request.user.is_anonymous(): + r = Rower.objects.get(user=request.user) + result = request.user.is_authenticated() and ispromember(request.user) + if result: + promember=1 + + if request.method == 'POST': + form = WorkoutMultipleCompareForm(request.POST) + chartform = ChartParamChoiceForm(request.POST) + if form.is_valid() and chartform.is_valid(): + cd = form.cleaned_data + workouts = cd['workouts'] + xparam = chartform.cleaned_data['xparam'] + yparam = chartform.cleaned_data['yparam'] + plottype = chartform.cleaned_data['plottype'] + teamid = chartform.cleaned_data['teamid'] + ids = [w.id for w in workouts] + labeldict = { + w.id: w.__unicode__() for w in workouts + } + + res = interactive_multiple_compare_chart(ids,xparam,yparam, + promember=promember, + plottype=plottype, + labeldict=labeldict) + script = res[0] + div = res[1] + + return render(request,'multicompare.html', + {'interactiveplot':script, + 'the_div':div, + 'promember':promember, + 'teamid':teamid, + }) + else: + return HttpResponse("Form is not valid") + else: + url = reverse(workouts_view) + return HttpResponseRedirect(url) # List Workouts @login_required() @@ -3302,6 +3436,16 @@ def workout_export_view(request,id=0, message="", successmessage=""): except Workout.DoesNotExist: raise Http404("Workout doesn't exist") + try: + thetoken = c2_open(request.user) + except C2NoTokenError: + thetoken = 0 + + if (checkworkoutuser(request.user,row)) and thetoken: + c2userid = c2stuff.get_userid(thetoken) + else: + c2userid = 0 + form = WorkoutForm(instance=row) g = GraphImage.objects.filter(workout=row).order_by("-creationdatetime") # check if user is owner of this workout @@ -3318,6 +3462,7 @@ def workout_export_view(request,id=0, message="", successmessage=""): {'workout':row, 'message':message, 'successmessage':successmessage, + 'c2userid':c2userid, }) # list of comments to a workout