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 %}
-
-
- {% endif %}
-
-
+
+
+
Interactive Plot
+
+ {% if user.is_authenticated and mayedit %}
+
+
+ {% 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 %}
@@ -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
+
+
+
+
+
+
+
+
+ {{ 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 %}
+
+
+
+
+
+
+
+
{{ team.name }} Team Workouts
+
+
+
+
+
+{% 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