Private
Public Access
1
0

Merge branch 'release/v12.22'

This commit is contained in:
Sander Roosendaal
2020-05-05 19:17:18 +02:00
11 changed files with 375 additions and 17 deletions
+24
View File
@@ -143,6 +143,30 @@ class DisqualificationForm(forms.Form):
message = forms.CharField(required=True,widget=forms.Textarea) message = forms.CharField(required=True,widget=forms.Textarea)
class HistorySelectForm(forms.Form):
typeselectchoices = [("All","All")]
for wtype,verbose in mytypes.workouttypes_ordered.items():
typeselectchoices.append((wtype,verbose))
startdate = forms.DateField(
initial=timezone.now()-datetime.timedelta(days=15),
# widget=SelectDateWidget(years=range(1990,2050)),
widget=AdminDateWidget(), #format='%Y-%m-%d'),
label='Start Date')
enddate = forms.DateField(
initial=timezone.now(),
widget=AdminDateWidget(), #format='%Y-%m-%d'),
label='End Date')
workouttype = forms.ChoiceField(initial='All',choices=typeselectchoices)
class Meta:
fields = ['startdate','enddate']
input_formats=("%Y-%m-%d")
dateTimeOptions = {
'format': '%Y-%m-%d',
'autoclose': True,
}
class MetricsForm(forms.Form): class MetricsForm(forms.Form):
avghr = forms.IntegerField(required=False,label='Average Heart Rate') avghr = forms.IntegerField(required=False,label='Average Heart Rate')
avgpwr = forms.IntegerField(required=False,label='Average Power') avgpwr = forms.IntegerField(required=False,label='Average Power')
+77
View File
@@ -169,6 +169,83 @@ def tailwind(bearing,vwind,winddir):
from rowers.dataprep import nicepaceformat,niceformat from rowers.dataprep import nicepaceformat,niceformat
from rowers.dataprep import timedeltaconv from rowers.dataprep import timedeltaconv
from math import pi
def interactive_hr_piechart(df,rower,title):
if df.empty:
return "","Not enough data to make a chart"
df.sort_values(by='hr',inplace=True)
qry = 'hr < {ut2}'.format(ut2=rower.ut2)
frac_lut2 = len(df.query(qry))/len(df)
qry = 'hr < {ut1}'.format(ut1=rower.ut1,ut2=rower.ut2)
frac_ut2 = len(df.query(qry))/len(df)
qry = 'hr < {at}'.format(ut1=rower.ut1,at=rower.at)
frac_ut1 = len(df.query(qry))/len(df)
qry = 'hr < {tr}'.format(at=rower.at,tr=rower.tr)
frac_at = len(df.query(qry))/len(df)
qry = 'hr < {an}'.format(tr=rower.tr,an=rower.an)
frac_tr = len(df.query(qry))/len(df)
frac_an = 1.
source_starts = 2*pi*pd.Series([
0,
frac_lut2,
frac_ut2,
frac_ut1,
frac_at,
frac_tr,
])
source_ends = 2*pi*pd.Series([
frac_lut2,
frac_ut2,
frac_ut1,
frac_at,
frac_tr,
frac_an,
])
source_legends = [
'<ut2',
'ut2',
'ut1',
'at',
'tr',
'an',
]
colors = ['gray','yellow','lime','blue','purple','red']
size=350
TOOLS = 'save'
z = figure(title="HR "+title, x_range=(-1,1), y_range=(-1,1), width=size, height=size,
tools=TOOLS,
)
for start, end , legend, color in zip(source_starts, source_ends, source_legends, colors[0:len(source_starts)]):
z.wedge(x=0, y=0, radius=1, start_angle=start, end_angle=end, color=color, legend=legend)
z.toolbar_location = 'right'
z.legend.location = 'top_right'
#z.legend.visible = False
z.axis.visible = False
z.xgrid.grid_line_color = None
z.ygrid.grid_line_color = None
z.outline_line_color = None
return components(z)
def interactive_boxchart(datadf,fieldname,extratitle='', def interactive_boxchart(datadf,fieldname,extratitle='',
spmmin=0,spmmax=0,workmin=0,workmax=0): spmmin=0,spmmax=0,workmin=0,workmax=0):
+1 -1
View File
@@ -889,7 +889,7 @@ class Rower(models.Model):
verbose_name='Show Notes for Favorite Charts') verbose_name='Show Notes for Favorite Charts')
# Static chart settings # Static chart settings
staticgrids = models.CharField(default=None,choices=gridtypes,null=True,max_length=50, staticgrids = models.CharField(default='both',choices=gridtypes,null=True,max_length=50,
verbose_name='Chart Grid') verbose_name='Chart Grid')
ergpaceslow = datetime.timedelta(seconds=160) ergpaceslow = datetime.timedelta(seconds=160)
+97
View File
@@ -0,0 +1,97 @@
{% extends "newbase.html" %}
{% load staticfiles %}
{% load rowerfilters %}
{% block title %}Rowsandall {% endblock %}
{% block main %}
<h1>History</h1>
<ul class="main-content">
<script async="true" src="https://cdn.pydata.org/bokeh/release/bokeh-1.0.4.min.js"></script>
<li class="grid_2">
<p>
<form enctype="multipart/form-data" method="post">
{% csrf_token %}
<table>
{{ form.as_table }}
</table>
<input type="submit" value="Submit">
</form>
</p>
<h2>All workouts</h2>
<p>
<table class="listtable shortpadded">
<tbody>
<tr>
<td>Total Distance</td><td>{{ totalsdict|lookup:"distance"}} meters</td>
</tr>
<tr>
<td>Total Duration</td><td>{{ totalsdict|lookup:"duration"}} hours</td>
</tr>
<tr>
<td>Number of workouts</td><td>{{ totalsdict|lookup:"nrworkouts"}}</td>
</tr>
<tr>
<td>Average heart rate</td><td>{{ totalsdict|lookup:"hrmean"}} bpm</td>
</tr>
<tr>
<td>Maximum heart rate</td><td>{{ totalsdict|lookup:"hrmax"}} bpm</td>
</tr>
<tr>
<td>Average power</td><td>{{ totalsdict|lookup:"powermean"}} W</td>
</tr>
<tr>
<td>Maximum power</td><td>{{ totalsdict|lookup:"powermax"}} W</td>
</tr>
</tbody>
</table>
</p>
</li>
<li class="grid_2">
<div>{{ totalscript|safe }}{{ totaldiv|safe }}</div>
</li>
{% for ddict in typedicts %}
<li class="grid_1">
<h2>{{ ddict|lookup:"wtype"}}</h2>
<p>
<table class="listtable shortpadded">
<tbody>
<tr>
<td>Total Distance</td><td>{{ ddict|lookup:"distance"}} meters</td>
</tr>
<tr>
<td>Total Duration</td><td>{{ ddict|lookup:"duration"}} hours</td>
</tr>
<tr>
<td>Number of workouts</td><td>{{ ddict|lookup:"nrworkouts"}}</td>
</tr>
<tr>
<td>Average heart rate</td><td>{{ ddict|lookup:"hrmean"}} bpm</td>
</tr>
<tr>
<td>Maximum heart rate</td><td>{{ ddict|lookup:"hrmax"}} bpm</td>
</tr>
<tr>
<td>Average power</td><td>{{ ddict|lookup:"powermean"}} W</td>
</tr>
<tr>
<td>Maximum power</td><td>{{ ddict|lookup:"powermax"}} W</td>
</tr>
</tbody>
</table>
</p>
</li>
{% endfor %}
</ul>
{% endblock %}
{% block sidebar %}
{% include 'menu_analytics.html' %}
{% endblock %}
+13 -9
View File
@@ -59,10 +59,14 @@
<ul class="main-content"> <ul class="main-content">
<li class="grid_2" style="min-height:200px;"> <li class="grid_2" style="min-height:200px;">
<p>
Total meters: {{ totalmeters }}. Total time {{ totalhours }}:{{ totalminutes }}h.
<a href="/rowers/history/">Dig deeper</a>.
</p>
<script async="true" src="https://cdn.pydata.org/bokeh/release/bokeh-1.0.4.min.js"></script> <script async="true" src="https://cdn.pydata.org/bokeh/release/bokeh-1.0.4.min.js"></script>
{{ interactiveplot |safe }} {{ interactiveplot |safe }}
{{ the_div |safe }} {{ the_div |safe }}
</li> </li>
<li class="grid_2"> <li class="grid_2">
@@ -112,11 +116,11 @@
</a> </a>
{% endif %} {% endif %}
{% endif %} {% endif %}
<span> <span>
Page {{ workouts.number }} of {{ workouts.paginator.num_pages }}. Page {{ workouts.number }} of {{ workouts.paginator.num_pages }}.
</span> </span>
{% if workouts.has_next %} {% if workouts.has_next %}
{% if request.GET.q %} {% if request.GET.q %}
<a href="{{ request.path }}?page={{ workouts.next_page_number }}&q={{ request.GET.q }}&when={{ timeperiod }}"> <a href="{{ request.path }}?page={{ workouts.next_page_number }}&q={{ request.GET.q }}&when={{ timeperiod }}">
@@ -139,7 +143,7 @@
</p> </p>
</li> </li>
<li class="maxheight grid_4"> <li class="maxheight grid_4">
{% if workouts %} {% if workouts %}
<table width="100%" class="listtable shortpadded"> <table width="100%" class="listtable shortpadded">
<thead> <thead>
@@ -189,7 +193,7 @@
{% else %} {% else %}
<td> <td>
<a href={% url rower.defaultlandingpage id=workout.id|encode %}>No Name <a href={% url rower.defaultlandingpage id=workout.id|encode %}>No Name
</a></td> </a></td>
{% endif %} {% endif %}
{% else %} {% else %}
{% if workout.name != '' %} {% if workout.name != '' %}
@@ -251,9 +255,9 @@
&nbsp; &nbsp;
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
@@ -277,7 +281,7 @@
{% endfor %} {% endfor %}
{% endif %} {% endif %}
</ul> </ul>
{% endblock %} {% endblock %}
{% block sidebar %} {% block sidebar %}
+5
View File
@@ -90,6 +90,11 @@
<i class="fas fa-bell fa-fw"></i>&nbsp;Alerts <i class="fas fa-bell fa-fw"></i>&nbsp;Alerts
</a> </a>
</li> </li>
<li>
<a href="/rowers/history/">
<i class="fas fa-history fa-fw"></i>&nbsp;History
</a>
</li>
</ul> </ul>
</li> </li>
<li> <li>
+3 -3
View File
@@ -982,9 +982,9 @@ class PermissionsViewTests(TestCase):
self.assertEqual(response.status_code,200) self.assertEqual(response.status_code,200)
self.assertRedirects(response, #self.assertRedirects(response,
expected_url = url, # expected_url = url,
status_code=302,target_status_code=200) # status_code=302,target_status_code=200)
aantal2 = len(Workout.objects.filter(user=self.rbasic)) aantal2 = len(Workout.objects.filter(user=self.rbasic))
+1
View File
@@ -752,6 +752,7 @@ urlpatterns = [
re_path(r'^workout/api/upload/',views.workout_upload_api,name='workout_upload_api'), re_path(r'^workout/api/upload/',views.workout_upload_api,name='workout_upload_api'),
re_path(r'^access/share/$',views.createShareURL, name="sharedURL"), re_path(r'^access/share/$',views.createShareURL, name="sharedURL"),
re_path(r'^access/(?P<key>\w+)/$', views.sharedPage, name="sharedPage"), re_path(r'^access/(?P<key>\w+)/$', views.sharedPage, name="sharedPage"),
re_path(r'^history/$',views.history_view,name="history_view"),
] ]
if settings.DEBUG: if settings.DEBUG:
+132
View File
@@ -4650,3 +4650,135 @@ class AlertDelete(DeleteView):
# some checks # some checks
return obj return obj
@login_required()
def history_view(request,userid=0):
r = getrequestrower(request,userid=userid)
form = HistorySelectForm()
usertimezone = pytz.timezone(r.defaulttimezone)
activity_enddate = timezone.now()
activity_enddate = activity_enddate.replace(hour=23,minute=59,second=59).astimezone(usertimezone)
activity_startdate = activity_enddate-datetime.timedelta(days=15)
activity_startdate = activity_startdate.replace(hour=0,minute=0,second=0)
typeselect = 'All'
if request.method=='POST':
form = HistorySelectForm(request.POST)
if form.is_valid():
startdate = form.cleaned_data['startdate']
enddate = form.cleaned_data['enddate']
typeselect = form.cleaned_data['workouttype']
activity_startdate = datetime.datetime(
startdate.year,startdate.month,startdate.day
).replace(hour=0,minute=0,second=0).astimezone(usertimezone)
activity_enddate = datetime.datetime(
enddate.year,enddate.month,enddate.day
).replace(hour=23,minute=59,second=59).astimezone(usertimezone)
g_workouts = Workout.objects.filter(
user=r,
startdatetime__gte=activity_startdate,
startdatetime__lte=activity_enddate,
duplicate=False,
privacy='visible'
).order_by("-startdatetime")
ids = [w.id for w in g_workouts]
columns = ['hr','power']
df = getsmallrowdata_db(columns,ids=ids)
totalmeters,totalhours, totalminutes = get_totals(g_workouts)
# meters, duration per workout type
wtypes = list(set([w.workouttype for w in g_workouts]))
typechoices = [("All","All")]
for wtype in wtypes:
typechoices.append((wtype,mytypes.workouttypes_ordered[wtype]))
form.fields['workouttype'].choices = typechoices
listofdicts = []
for wtype in wtypes:
a_workouts = g_workouts.filter(workouttype=wtype)
wmeters, whours, wminutes = get_totals(a_workouts)
ddict = {}
ddict['wtype'] = mytypes.workouttypes_ordered[wtype]
ddict['distance'] = wmeters
ddict['duration'] = "{whours}:{wminutes:02d}".format(
whours=whours,
wminutes=wminutes
)
ddf = getsmallrowdata_db(columns,ids=[w.id for w in a_workouts])
ddict['hrmean'] = ddf['hr'].mean().astype(int)
ddict['hrmax'] = ddf['hr'].max().astype(int)
ddict['powermean'] = ddf['power'].mean().astype(int)
ddict['powermax'] = ddf['power'].max().astype(int)
ddict['nrworkouts'] = a_workouts.count()
listofdicts.append(ddict)
# interactive hr pie chart
if typeselect == 'All':
totalscript,totaldiv = interactive_hr_piechart(df,r,'All Workouts')
else:
a_workouts = g_workouts.filter(workouttype=typeselect)
ddf = getsmallrowdata_db(columns,ids=[w.id for w in a_workouts])
totalscript, totaldiv = interactive_hr_piechart(ddf,r,mytypes.workouttypes_ordered[typeselect])
# interactive power pie chart
totalsdict = {}
totalsdict['duration'] = "{totalhours}:{totalminutes}".format(
totalhours=totalhours,
totalminutes=totalminutes
)
totalsdict['distance'] = totalmeters
try:
totalsdict['powermean'] = df['power'].mean().astype(int)
totalsdict['powermax'] = df['power'].max().astype(int)
except KeyError:
totalsdict['powermean'] = 0
totalsdict['powermax'] = 0
try:
totalsdict['hrmean'] = df['hr'].mean().astype(int)
totalsdict['hrmax'] = df['hr'].max().astype(int)
except KeyError:
totalsdict['hrmean'] = 0
totalsdict['hrmax'] = 0
totalsdict['nrworkouts'] = g_workouts.count()
breadcrumbs = [
{
'url':'rowers/analysis',
'name':'Analysis',
},
{
'url':reverse('history_view'),
'name': 'History',
},
]
return render(request,'history.html',
{
'rower':r,
'breadcrumbs':breadcrumbs,
'active':'nav-analysis',
'totalsdict':totalsdict,
'typedicts':listofdicts,
'totalscript':totalscript,
'totaldiv':totaldiv,
'form':form,
})
+14 -2
View File
@@ -74,7 +74,7 @@ from rowers.forms import (
MetricsForm,DisqualificationForm,disqualificationreasons, MetricsForm,DisqualificationForm,disqualificationreasons,
disqualifiers,SearchForm,BillingForm,PlanSelectForm, disqualifiers,SearchForm,BillingForm,PlanSelectForm,
VideoAnalysisCreateForm,WorkoutSingleSelectForm, VideoAnalysisCreateForm,WorkoutSingleSelectForm,
VideoAnalysisMetricsForm,SurveyForm, VideoAnalysisMetricsForm,SurveyForm,HistorySelectForm,
) )
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
@@ -259,6 +259,18 @@ from django_mailbox.models import Message,Mailbox,MessageAttachment
from rules.contrib.views import permission_required, objectgetter from rules.contrib.views import permission_required, objectgetter
def get_totals(workouts):
totalminutes = 0
totalmeters = 0
for w in workouts:
totalmeters += w.distance
totalminutes += w.duration.hour*60+w.duration.minute
totalhour, totalminutes = divmod(totalminutes,60)
return totalmeters,totalhour, totalminutes
# creating shareable views # creating shareable views
def allow_shares(view_func): def allow_shares(view_func):
def sharify(request, *args, **kwargs): def sharify(request, *args, **kwargs):
@@ -506,7 +518,7 @@ def getrequestplanrower(request,rowerid=0,userid=0,notpermanent=False):
def getrower(user): def getrower(user):
try: try:
if user.is_anonymous: if user is None or user.is_anonymous:
return None return None
except AttributeError: except AttributeError:
if User.objects.get(id=user).is_anonymous: if User.objects.get(id=user).is_anonymous:
+8 -2
View File
@@ -1918,7 +1918,7 @@ def workouts_view(request,message='',successmessage='',
g_workouts = Workout.objects.filter( g_workouts = Workout.objects.filter(
team=theteam,user=r, team=theteam,user=r,
startdatetime__gte=activity_startdate, startdatetime__gte=activity_startdate,
enddatetime__lte=activity_enddate, startdatetime__lte=activity_enddate,
duplicate=False, duplicate=False,
privacy='visible').order_by("-startdatetime") privacy='visible').order_by("-startdatetime")
@@ -2009,6 +2009,8 @@ def workouts_view(request,message='',successmessage='',
g_enddate, g_enddate,
stack=stack) stack=stack)
totalmeters,totalhours, totalminutes = get_totals(g_workouts)
messages.info(request,successmessage) messages.info(request,successmessage)
messages.error(request,message) messages.error(request,message)
@@ -2036,6 +2038,9 @@ def workouts_view(request,message='',successmessage='',
'interactiveplot':script, 'interactiveplot':script,
'the_div':div, 'the_div':div,
'timeperiod':timeperiod, 'timeperiod':timeperiod,
'totalmeters':totalmeters,
'totalminutes':totalminutes,
'totalhours':totalhours,
}) })
@@ -5085,7 +5090,8 @@ def team_workout_upload_view(request,message="",
rowers = rowers.exclude(rowerplan='basic') rowers = rowers.exclude(rowerplan='basic')
rowerform.fields['user'].queryset = User.objects.filter(rower__in=rowers).distinct() rowerform.fields['user'].queryset = User.objects.filter(rower__in=rowers).distinct()
if form.is_valid(): rowerform.fields['user'].required = True
if form.is_valid() and rowerform.is_valid():
f = request.FILES.get('file',False) f = request.FILES.get('file',False)
if f: if f:
res = handle_uploaded_file(f) res = handle_uploaded_file(f)