Private
Public Access
1
0
This commit is contained in:
Sander Roosendaal
2019-12-02 08:24:34 +01:00
27 changed files with 2060 additions and 349 deletions
+132 -3
View File
@@ -10,6 +10,7 @@ from __future__ import unicode_literals, absolute_import
from rowers.models import Workout, Team
import pytz
import collections
from rowingdata import rowingdata as rrdata
@@ -55,7 +56,7 @@ from rowingdata import (
from rowingdata.csvparsers import HumonParser
from rowers.metrics import axes,calc_trimp,rowingmetrics,dtypes
from rowers.metrics import axes,calc_trimp,rowingmetrics,dtypes,metricsgroups
from rowers.models import strokedatafields
#allowedcolumns = [item[0] for item in rowingmetrics]
@@ -122,6 +123,100 @@ from scipy.signal import savgol_filter
import datetime
def get_video_data(w,groups=['basic'],mode='water'):
modes = [mode,'both','basic']
columns = ['time','velo','spm']
columns += [name for name,d in rowingmetrics if d['group'] in groups and d['mode'] in modes]
columns = list(set(columns))
df = getsmallrowdata_db(columns,ids=[w.id],
workstrokesonly=False,doclean=False,compute=False)
df['time'] = (df['time']-df['time'].min())/1000.
df.sort_values(by='time',inplace=True)
df.set_index(pd.to_timedelta(df['time'],unit='s'),inplace=True)
df2 = df.resample('1s').first().fillna(method='ffill')
if 'pace' in columns:
df2['pace'] = df2['pace']/1000.
p = df2['pace']
p = p.apply(lambda x:timedeltaconv(x))
p = nicepaceformat(p)
df2['pace'] = p
#mask = df2['time'] < delay
#df2 = df2.mask(mask).dropna()
df2['time'] = (df2['time']-df2['time'].min())
df2 = df2.round(decimals=2)
boatspeed = (100*df2['velo']).astype(int)/100.
try:
coordinates = get_latlon_time(w.id)
except KeyError:
nulseries = df['time']*0
coordinates = pd.DataFrame({
'time': df['time'],
'latitude': nulseries,
'longitude': nulseries,
})
coordinates.set_index(pd.to_timedelta(coordinates['time'],unit='s'),inplace=True)
coordinates = coordinates.resample('1s').mean().interpolate()
#mask = coordinates['time'] < delay
#coordinates = coordinates.mask(mask).dropna()
coordinates['time'] = coordinates['time']-coordinates['time'].min()
latitude = coordinates['latitude']
longitude = coordinates['longitude']
# bundle data
data = {
'boatspeed':boatspeed.values.tolist(),
'latitude':latitude.values.tolist(),
'longitude':longitude.values.tolist(),
}
# metrics = {
# 'boatspeed': {
# 'unit': 'm/s',
# 'metric': 'boatspeed',
# 'name': 'Boat Speed'
# },
# }
metrics = {}
for c in columns:
if c != 'time':
try:
if dict(rowingmetrics)[c]['numtype'] == 'integer':
data[c] = df2[c].astype(int).tolist()
else:
data[c] = df2[c].values.tolist()
metrics[c] = {
'name': dict(rowingmetrics)[c]['verbose_name'],
'metric': c,
'unit': ''
}
except KeyError:
pass
metrics['boatspeed'] = metrics.pop('velo')
# metrics['workperstroke'] = metrics.pop('driveenergy')
metrics = collections.OrderedDict(sorted(metrics.items()))
maxtime = coordinates['time'].max()
return data, metrics, maxtime
def polarization_index(df,rower):
df['dt'] = df['time'].diff()/6.e4
# remove rest (spm<15)
@@ -172,6 +267,36 @@ def get_latlon(id):
return [pd.Series([]), pd.Series([])]
def get_latlon_time(id):
try:
w = Workout.objects.get(id=id)
except Workout.DoesNotExist:
return False
rowdata = rdata(w.csvfilename)
if rowdata.df.empty:
return [pd.Series([]), pd.Series([])]
try:
try:
latitude = rowdata.df.loc[:, ' latitude']
longitude = rowdata.df.loc[:, ' longitude']
except KeyError:
latitude = 0 * rowdata.df.loc[:, 'TimeStamp (sec)']
longitude = 0 * rowdata.df.loc[:, 'TimeStamp (sec)']
except AttributeError:
return pd.DataFrame()
df = pd.DataFrame({
'time': rowdata.df[' ElapsedTime (sec)'],
'latitude': rowdata.df[' latitude'],
'longitude': rowdata.df[' longitude']
})
return df
def workout_summary_to_df(
rower,
startdate=datetime.datetime(1970,1,1),
@@ -1693,8 +1818,12 @@ def getrowdata_db(id=0, doclean=False, convertnewtons=True,
if not data.empty and data['efficiency'].mean() == 0 and data['power'].mean() != 0 and checkefficiency == True:
data = add_efficiency(id=id)
if checkefficiency==True and not data.empty:
try:
if data['efficiency'].mean() == 0 and data['power'].mean() != 0:
data = add_efficiency(id=id)
except KeyError:
data = add_efficiency(id=id)
if doclean:
data = clean_df_stats(data, ignorehr=True)
+3
View File
@@ -13,6 +13,9 @@ from rowingdata import empower_bug_correction,get_empower_rigging
from time import strftime
from pandas import DataFrame,Series
import shutil
from shutil import copyfile
import pandas as pd
import numpy as np
import itertools
+123 -82
View File
@@ -24,7 +24,7 @@ import rowers.mytypes as mytypes
import datetime
from django.forms import formset_factory
from rowers.utils import landingpages
from rowers.metrics import axes
from rowers.metrics import axes, metricsgroups,rowingmetrics
from rowers.metrics import axlabels
formaxlabels = axlabels.copy()
@@ -51,6 +51,40 @@ class FlexibleDecimalField(forms.DecimalField):
value = value.replace('.', '').replace(',', '.')
return super(FlexibleDecimalField, self).to_python(value)
# Video Analysis creation form
class VideoAnalysisCreateForm(forms.Form):
name = forms.CharField(max_length=255,label='Analysis Name',required=False)
url = forms.CharField(max_length=255,required=True,label='YouTube Video URL')
delay = forms.IntegerField(initial=0,label='Delay (seconds)')
def get_metricschoices(mode='water'):
modes = [mode,'both','basic']
metricsdescriptions = {}
for m in metricsgroups:
metricsdescriptions[m] = m+' ('
for name,d in rowingmetrics:
if d['group']==m and d['mode'] in modes:
metricsdescriptions[m]+=d['verbose_name']+', '
metricsdescriptions[m]=metricsdescriptions[m][0:-2]+')'
metricsgroupschoices = ((m,metricsdescriptions[m]) for m in metricsgroups)
return metricsgroupschoices
class VideoAnalysisMetricsForm(forms.Form):
groups = forms.MultipleChoiceField(label='Metrics Groups',
choices=get_metricschoices(mode='water'),
widget=forms.CheckboxSelectMultiple,)
class Meta:
mode = 'water'
def __init__(self, *args, **kwargs):
mode = kwargs.pop('mode','water')
super(VideoAnalysisMetricsForm, self).__init__(*args, **kwargs)
self.fields['groups'].choices = get_metricschoices(mode=mode)
# BillingForm form
class BillingForm(forms.Form):
amount = FlexibleDecimalField(required=True,decimal_places=2,
@@ -59,7 +93,7 @@ class BillingForm(forms.Form):
payment_method_nonce = forms.CharField(max_length=255,required=True)
paymenttype = forms.CharField(max_length=255,required=True)
tac= forms.BooleanField(required=True,initial=False)
# login form
class LoginForm(forms.Form):
@@ -73,8 +107,8 @@ class SearchForm(forms.Form):
attrs={'placeholder': 'keyword or leave empty'}),
label='Filter by Keyword')
# simple form for Contact page. Sends email to info@rowsandall.com
class EmailForm(forms.Form):
firstname = forms.CharField(max_length=255)
@@ -107,7 +141,7 @@ class MetricsForm(forms.Form):
avghr = forms.IntegerField(required=False,label='Average Heart Rate')
avgpwr = forms.IntegerField(required=False,label='Average Power')
avgspm = forms.FloatField(required=False,label='Average SPM')
# Upload the CrewNerd Summary CSV
class CNsummaryForm(forms.Form):
file = forms.FileField(required=True,validators=[must_be_csv])
@@ -120,7 +154,7 @@ class SummaryStringForm(forms.Form):
class TeamInviteCodeForm(forms.Form):
code = forms.CharField(max_length=10,label='Team Code',
)
# Used for testing the POST API for StrokeData
class StrokeDataForm(forms.Form):
strokedata = forms.CharField(label='payload',widget=forms.Textarea)
@@ -133,7 +167,7 @@ class ImageForm(forms.Form):
def __init__(self, *args, **kwargs):
from django.forms.widgets import HiddenInput
super(ImageForm, self).__init__(*args, **kwargs)
# The form used for uploading images
class CourseForm(forms.Form):
@@ -147,9 +181,9 @@ class CourseForm(forms.Form):
def __init__(self, *args, **kwargs):
from django.forms.widgets import HiddenInput
super(CourseForm, self).__init__(*args, **kwargs)
# The form used for uploading files
# The form used for uploading files
class DocumentsForm(forms.Form):
title = forms.CharField(required=False)
file = forms.FileField(required=False,
@@ -157,7 +191,7 @@ class DocumentsForm(forms.Form):
workouttype = forms.ChoiceField(required=True,
choices=Workout.workouttypes)
boattype = forms.ChoiceField(required=True,
choices=mytypes.boattypes,
label = "Boat Type")
@@ -181,7 +215,7 @@ from rowers.utils import (
defaultleft,defaultmiddle
)
# Form to change Workflow page layout
class WorkFlowLeftPanelForm(forms.Form):
@@ -212,14 +246,14 @@ class WorkFlowLeftPanelForm(forms.Form):
self.base_fields['leftpanel'].initial=panels
else:
panels = defaultleft
super(WorkFlowLeftPanelForm,self).__init__(*args, **kwargs)
class WorkFlowMiddlePanelForm(forms.Form):
panels = defaultmiddle
middlepanel = forms.MultipleChoiceField(
label='',
@@ -237,7 +271,7 @@ class WorkFlowMiddlePanelForm(forms.Form):
# 'css/uid-manage-form.css'],
}
js = ['/admin/jsi18n/']
def __init__(self, *args, **kwargs):
if 'instance' in kwargs:
r = kwargs.pop('instance')
@@ -245,7 +279,7 @@ class WorkFlowMiddlePanelForm(forms.Form):
self.base_fields['middlepanel'].initial=panels
else:
panels = defaultmiddle
super(WorkFlowMiddlePanelForm,self).__init__(*args, **kwargs)
class WorkFlowLeftPanelElement(forms.Form):
@@ -265,8 +299,8 @@ class WorkFlowMiddlePanelElement(forms.Form):
choices=panelchoices,
initial='None',
)
# The form to indicate additional actions to be performed immediately
# after a successful upload
@@ -359,7 +393,7 @@ class TeamUploadOptionsForm(forms.Form):
class Meta:
fields = ['make_plot','plottype']
# This form is used on the Workout Split page
class WorkoutSplitForm(forms.Form):
splitchoices = (
@@ -386,7 +420,7 @@ class WorkoutSplitForm(forms.Form):
label='Split Mode',
choices=splitchoices,
widget = forms.CheckboxSelectMultiple())
# This form is used on the Analysis page to add a custom distance/time
# trial and predict the pace
from rowers.utils import rankingdistances,rankingdurations
@@ -427,14 +461,14 @@ class PredictedPieceForm(forms.Form):
choices=rankingdistancechoices,initial=rankingdistances,
label='Ranking Distances'
)
trankingdurations = forms.MultipleChoiceField(
required=True,
choices=rankingdurationchoices,
initial=[a for a,b in rankingdurationchoices],
label='Ranking Durations'
)
value = forms.FloatField(initial=10,label='Free ranking piece (minutes)')
pieceunit = forms.ChoiceField(required=True,choices=unitchoices,
initial='t',label='Unit')
@@ -451,17 +485,17 @@ class PredictedPieceFormNoDistance(forms.Form):
thetuple = (timestr,timestr)
rankingdurationchoices.append(thetuple)
trankingdurations = forms.MultipleChoiceField(
required=True,
choices=rankingdurationchoices,
initial=[a for a,b in rankingdurationchoices],
label='Ranking Durations'
)
value = forms.FloatField(initial=10,label='Free ranking piece')
# On the Geeky side, to update stream information for river dwellers
class UpdateStreamForm(forms.Form):
unitchoices = (
@@ -545,16 +579,16 @@ class FitnessMetricForm(forms.Form):
('rower','indoor rower'),
('water','on the water')
)
mode = forms.ChoiceField(required=True,
choices=modechoices,
initial='rower',
label='Workout Mode'
)
class Meta:
fields = ['startdate','enddate','mode']
class SessionDateShiftForm(forms.Form):
shiftstartdate = forms.DateField(
initial=timezone.now(),
@@ -563,7 +597,7 @@ class SessionDateShiftForm(forms.Form):
class Meta:
fields = ['shiftstartdate']
# Form used to select workouts for the past N days
class DeltaDaysForm(forms.Form):
deltadays = forms.IntegerField(initial=7,required=False,label='')
@@ -599,7 +633,7 @@ class RegistrationFormTermsOfService(RegistrationForm):
tos = forms.BooleanField(widget=forms.CheckboxInput,
label='I have read and agree to the Terms of Service',
error_messages={'required': "You must agree to the terms to register"})
class RegistrationFormUniqueEmail(RegistrationFormTermsOfService):
@@ -631,20 +665,20 @@ class RegistrationFormSex(RegistrationFormUniqueEmail):
adaptivecategories = mytypes.adaptivetypes
thisyear = timezone.now().year
birthdate = forms.DateTimeField(
widget=SelectDateWidget(years=range(1900,thisyear)),
initial = datetime.date(year=1970,
month=4,
day=15))
def clean_birthdate(self):
dob = self.cleaned_data['birthdate']
age = (timezone.now() - dob).days/365
if age < 16:
raise forms.ValidationError('Must be at least 16 years old to register')
return self.cleaned_data['birthdate']
sex = forms.ChoiceField(required=False,
choices=sexcategories,
initial='not specified',
@@ -655,10 +689,10 @@ class RegistrationFormSex(RegistrationFormUniqueEmail):
adaptiveclass = forms.ChoiceField(label='Adaptive Classification',
choices=adaptivecategories,initial='None',required=False)
# def __init__(self, *args, **kwargs):
# self.fields['sex'].initial = 'not specified'
# Time field supporting microseconds. Not used, I believe.
class MyTimeField(forms.TimeField):
@@ -673,7 +707,7 @@ class PowerIntervalUpdateForm(forms.Form):
('pace','Pace'),
('work','Work per Stroke'),
)
pace = forms.DurationField(required=False,label='Pace (/500m)')
power = forms.IntegerField(required=False,label='Power (W)')
work = forms.IntegerField(required=False,label='Work per Stroke (J)')
@@ -681,10 +715,10 @@ class PowerIntervalUpdateForm(forms.Form):
required=True,
initial='power',
label='Use')
# Form used to update interval stats
class IntervalUpdateForm(forms.Form):
def __init__(self, *args, **kwargs):
typechoices = (
(1,'single time'),
@@ -733,7 +767,7 @@ class HistoForm(forms.Form):
includereststrokes = forms.BooleanField(initial=False,label='Include Rest Strokes',required=False)
histoparam = forms.ChoiceField(choices=parchoices,initial='power',
label='Metric')
class AnalysisOptionsForm(forms.Form):
modality = forms.ChoiceField(choices=workouttypes,
label='Workout Type',
@@ -744,8 +778,8 @@ class AnalysisOptionsForm(forms.Form):
rankingonly = forms.BooleanField(initial=False,
label='Only Ranking Pieces',
required=False)
# form to select modality and boat type for trend flex
class TrendFlexModalForm(forms.Form):
modality = forms.ChoiceField(choices=workouttypes,
@@ -759,7 +793,7 @@ class TrendFlexModalForm(forms.Form):
required=False)
# This form sets options for the summary stats page
class StatsOptionsForm(forms.Form):
includereststrokes = forms.BooleanField(initial=False,label='Include Rest Strokes',required=False)
@@ -772,13 +806,13 @@ class StatsOptionsForm(forms.Form):
initial = mytypes.waterboattype)
def __init__(self, *args, **kwargs):
super(StatsOptionsForm, self).__init__(*args,**kwargs)
for type in mytypes.checktypes:
self.fields[type] = forms.BooleanField(initial=True,required=False)
class PlanSelectForm(forms.Form):
plan = forms.ModelChoiceField(queryset=PaidPlan.objects.all(),
widget=forms.RadioSelect,required=True)
@@ -812,10 +846,21 @@ class PlanSelectForm(forms.Form):
"price","shortname"
)
class CourseSelectForm(forms.Form):
course = forms.ModelChoiceField(queryset=GeoCourse.objects.all())
class WorkoutSingleSelectForm(forms.Form):
workout = forms.ModelChoiceField(
queryset=Workout.objects.filter(),
widget=forms.RadioSelect
)
def __init__(self, *args, **kwargs):
workouts = kwargs.pop('workouts',Workout.objects.filter().order_by('-date'))
super(WorkoutSingleSelectForm,self).__init__(*args,**kwargs)
self.fields['workout'].queryset = workouts
class WorkoutMultipleCompareForm(forms.Form):
workouts = forms.ModelMultipleChoiceField(
queryset=Workout.objects.filter(),
@@ -824,8 +869,8 @@ class WorkoutMultipleCompareForm(forms.Form):
def __init__(self, *args, **kwargs):
super(WorkoutMultipleCompareForm,self).__init__(*args,**kwargs)
self.fields['workouts'].queryset = Workout.objects.filter()
class PlannedSessionMultipleCloneForm(forms.Form):
plannedsessions = forms.ModelMultipleChoiceField(
queryset=PlannedSession.objects.all(),
@@ -867,7 +912,7 @@ class AnalysisChoiceForm(forms.Form):
)
axchoices = dict((x,y) for x,y in axchoices)
axchoices = list(sorted(axchoices.items(), key = lambda x:x[1]))
yaxchoices = list((ax[0],ax[1]) for ax in axes if ax[0] not in ['cumdist','distance','time'])
yaxchoices = dict((x,y) for x,y in yaxchoices)
@@ -877,7 +922,7 @@ class AnalysisChoiceForm(forms.Form):
('line','Line Plot'),
('scatter','Scatter Plot'),
)
yaxchoices2 = list(
(ax[0],ax[1]) for ax in axes if ax[0] not in ['cumdist','distance','time']
)
@@ -914,7 +959,7 @@ class AnalysisChoiceForm(forms.Form):
palette = forms.ChoiceField(choices=palettechoices,
label = 'Color Scheme',
initial='monochrome_blue')
spmmin = forms.FloatField(initial=15,
required=False,label = 'Min SPM')
spmmax = forms.FloatField(initial=55,
@@ -927,7 +972,7 @@ class AnalysisChoiceForm(forms.Form):
includereststrokes = forms.BooleanField(initial=False,
required=False,
label='Include Rest Strokes')
class BoxPlotChoiceForm(forms.Form):
yparam = forms.ChoiceField(choices=parchoices,initial='spm',
label='Metric')
@@ -971,7 +1016,7 @@ class MultiFlexChoiceForm(forms.Form):
palette = forms.ChoiceField(choices=palettechoices,
label = 'Color Scheme',
initial='monochrome_blue')
class ChartParamChoiceForm(forms.Form):
plotchoices = (
('line','Line Plot'),
@@ -993,7 +1038,7 @@ class FusionMetricChoiceForm(ModelForm):
class Meta:
model = Workout
fields = []
posneg = (
('pos','Workout 2 starts after Workout 1'),
('neg','Workout 2 starts before Workout 1'),
@@ -1021,14 +1066,14 @@ class FusionMetricChoiceForm(ModelForm):
formaxlabels2.pop(label)
except KeyError:
pass
metricchoices = list(sorted(formaxlabels2.items(), key = lambda x:x[1]))
self.fields['columns'].choices = metricchoices
class PlannedSessionSelectForm(forms.Form):
def __init__(self, sessionchoices, *args, **kwargs):
initialsession = kwargs.pop('initialsession',None)
initialsession = kwargs.pop('initialsession',None)
super(PlannedSessionSelectForm, self).__init__(*args,**kwargs)
self.fields['plannedsession'] = forms.ChoiceField(
@@ -1040,7 +1085,7 @@ class PlannedSessionSelectForm(forms.Form):
class WorkoutSessionSelectForm(forms.Form):
def __init__(self, workoutdata, *args, **kwargs):
super(WorkoutSessionSelectForm, self).__init__(*args, **kwargs)
@@ -1053,7 +1098,7 @@ class WorkoutSessionSelectForm(forms.Form):
)
class RaceResultFilterForm(forms.Form):
boatclasses = (type for type in mytypes.workouttypes if type[0] in mytypes.rowtypes)
boatclassinitial = [t for t in mytypes.rowtypes]
@@ -1069,7 +1114,7 @@ class RaceResultFilterForm(forms.Form):
)
adaptivecategories = mytypes.adaptivetypes
sex = forms.MultipleChoiceField(
choices=sexchoices,
initial=['male','female','mixed'],
@@ -1081,7 +1126,7 @@ class RaceResultFilterForm(forms.Form):
initial=boatclassinitial,
label='Boat/Erg Class',
widget=forms.CheckboxSelectMultiple())
boattype = forms.MultipleChoiceField(
choices=boattypes,
label='Boat Type',
@@ -1106,7 +1151,7 @@ class RaceResultFilterForm(forms.Form):
def __init__(self, *args, **kwargs):
if 'records' in kwargs:
records = kwargs.pop('records',None)
super(RaceResultFilterForm,self).__init__(*args,**kwargs)
if records:
@@ -1135,7 +1180,7 @@ class RaceResultFilterForm(forms.Form):
if choice[0] in theboatclasses:
boatclasschoices.append(choice)
self.fields['boatclass'].choices = boatclasschoices
# boattype
try:
theboattypees = [record.boattype for record in records]
@@ -1151,7 +1196,7 @@ class RaceResultFilterForm(forms.Form):
if choice[0] in theboattypees:
boattypechoices.append(choice)
self.fields['boattype'].choices = boattypechoices
# weightcategory
theweightcategoryes = [record.weightcategory for record in records]
theweightcategoryes = list(set(theweightcategoryes))
@@ -1177,7 +1222,7 @@ class RaceResultFilterForm(forms.Form):
if choice[0] in theadaptivecategoryes:
adaptivecategorychoices.append(choice)
self.fields['adaptivecategory'].choices = adaptivecategorychoices
class WorkoutRaceSelectForm(forms.Form):
# evaluate_after = forms.TimeField(
# input_formats=['%H:%M:%S.%f',
@@ -1188,7 +1233,7 @@ class WorkoutRaceSelectForm(forms.Form):
# '%M'],
# label = 'Only Evaluate After:',
# required=False)
def __init__(self, workoutdata,entries, *args, **kwargs):
super(WorkoutRaceSelectForm, self).__init__(*args, **kwargs)
@@ -1206,7 +1251,7 @@ class WorkoutRaceSelectForm(forms.Form):
initial = entries['initial'],
)
# self.fields['evaluate_after'] =
# self.fields['evaluate_after'] =
# form to send messages to team members
class TeamMessageForm(forms.Form):
@@ -1256,7 +1301,7 @@ class PlannedSessionTeamMemberForm(forms.Form):
members = forms.ModelMultipleChoiceField(
queryset=Rower.objects.all(),
widget=forms.CheckboxSelectMultiple())
def __init__(self, thesession, *args, **kwargs):
super(PlannedSessionTeamMemberForm,self).__init__(*args,**kwargs)
@@ -1283,13 +1328,13 @@ class VirtualRaceSelectForm(forms.Form):
('my','My Races'),
('all','All Races'),
)
regattatype = forms.ChoiceField(
label='Type',
choices = regattatypechoices,
initial = 'upcoming',
)
country = forms.ChoiceField(
label='Country',
choices = get_countries()
@@ -1311,7 +1356,7 @@ class FlexOptionsForm(forms.Form):
)
plottype = forms.ChoiceField(choices=plotchoices,initial='line',
label='Chart Type')
class ForceCurveOptionsForm(forms.Form):
includereststrokes = forms.BooleanField(initial=False, required = False,
label='Include Rest Strokes')
@@ -1322,15 +1367,15 @@ class ForceCurveOptionsForm(forms.Form):
)
plottype = forms.ChoiceField(choices=plotchoices,initial='line',
label='Individual Stroke Chart Type')
class FlexAxesForm(forms.Form):
axchoices = list(
(ax[0],ax[1]) for ax in axes if ax[0] not in ['cumdist','None']
)
axchoices = dict((x,y) for x,y in axchoices)
axchoices = list(sorted(axchoices.items(), key = lambda x:x[1]))
yaxchoices = list((ax[0],ax[1]) for ax in axes if ax[0] not in ['cumdist','distance','time'])
yaxchoices = dict((x,y) for x,y in yaxchoices)
@@ -1355,9 +1400,9 @@ class FlexAxesForm(forms.Form):
super(FlexAxesForm, self).__init__(*args, **kwargs)
rower = Rower.objects.get(user=request.user)
axchoicespro = (
('',ax[1]) if ax[4] == 'pro' and ax[0] else (ax[0],ax[1]) for ax in axes
('',ax[1]) if ax[4] == 'pro' and ax[0] else (ax[0],ax[1]) for ax in axes
)
axchoicesbasicx = []
@@ -1375,12 +1420,8 @@ class FlexAxesForm(forms.Form):
if ax[0] not in ['cumdist','distance','time']:
axchoicesbasicy.insert(0,('None',ax[1]+' (PRO)'))
if rower.rowerplan == 'basic':
self.fields['xaxis'].choices = axchoicesbasicx
self.fields['yaxis1'].choices = axchoicesbasicy
self.fields['yaxis2'].choices = axchoicesbasicy
+229
View File
@@ -1809,6 +1809,127 @@ def leaflet_chart2(lat,lon,name=""):
return script,div
def leaflet_chart_video(lat,lon,name=""):
if not len(lat) or not len(lon):
return [0,"invalid coordinate data"]
# Throw out 0,0
df = pd.DataFrame({
'lat':lat,
'lon':lon
})
df = df.replace(0,np.nan)
df = df.loc[(df!=0).any(axis=1)]
df.fillna(method='bfill',axis=0,inplace=True)
df.fillna(method='ffill',axis=0,inplace=True)
lat = df['lat']
lon = df['lon']
if lat.empty or lon.empty:
return [0,"invalid coordinate data"]
latmean = lat.mean()
lonmean = lon.mean()
latbegin = lat[lat.index[0]]
longbegin = lon[lon.index[0]]
latend = lat[lat.index[-1]]
longend = lon[lon.index[-1]]
coordinates = zip(lat,lon)
scoordinates = "["
for x,y in coordinates:
scoordinates += """[{x},{y}],
""".format(
x=x,
y=y
)
scoordinates += "]"
script = """
var streets = L.tileLayer('https://api.tiles.mapbox.com/v4/{{id}}/{{z}}/{{x}}/{{y}}.png?access_token=pk.eyJ1Ijoic2FuZGVycm9vc2VuZGFhbCIsImEiOiJjajY3aTRkeWQwNmx6MzJvMTN3andlcnBlIn0.MFG8Xt0kDeSA9j7puZQ9hA', {{
maxZoom: 18,
id: 'mapbox.streets'
}}),
satellite = L.tileLayer('https://api.tiles.mapbox.com/v4/{{id}}/{{z}}/{{x}}/{{y}}.png?access_token=pk.eyJ1Ijoic2FuZGVycm9vc2VuZGFhbCIsImEiOiJjajY3aTRkeWQwNmx6MzJvMTN3andlcnBlIn0.MFG8Xt0kDeSA9j7puZQ9hA', {{
maxZoom: 18,
id: 'mapbox.satellite'
}}),
outdoors = L.tileLayer('https://api.tiles.mapbox.com/v4/{{id}}/{{z}}/{{x}}/{{y}}.png?access_token=pk.eyJ1Ijoic2FuZGVycm9vc2VuZGFhbCIsImEiOiJjajY3aTRkeWQwNmx6MzJvMTN3andlcnBlIn0.MFG8Xt0kDeSA9j7puZQ9hA', {{
maxZoom: 18,
id: 'mapbox.outdoors'
}});
var mymap = L.map('map_canvas', {{
center: [{latmean}, {lonmean}],
zoom: 13,
layers: [streets, satellite]
}}).setView([{latmean},{lonmean}], 13);
var navionics = new JNC.Leaflet.NavionicsOverlay({{
navKey: 'Navionics_webapi_03205',
chartType: JNC.NAVIONICS_CHARTS.NAUTICAL,
isTransparent: true,
zIndex: 1
}});
var osmUrl2='http://tiles.openseamap.org/seamark/{{z}}/{{x}}/{{y}}.png';
var osmUrl='http://{{s}}.tile.openstreetmap.org/{{z}}/{{x}}/{{y}}.png';
//create two TileLayer
var nautical=new L.TileLayer(osmUrl,{{
maxZoom:18}});
L.control.layers({{
"Streets": streets,
"Satellite": satellite,
"Outdoors": outdoors,
"Nautical": nautical,
}},{{
"Navionics":navionics,
}},
{{
position:'topleft'
}}).addTo(mymap);
var marker = L.marker([{latbegin}, {longbegin}]).addTo(mymap);
marker.bindPopup("<b>Start</b>");
var latlongs = {scoordinates}
var polyline = L.polyline(latlongs, {{color:'red'}}).addTo(mymap)
mymap.fitBounds(polyline.getBounds())
""".format(
latmean=latmean,
lonmean=lonmean,
latbegin = latbegin,
latend=latend,
longbegin=longbegin,
longend=longend,
scoordinates=scoordinates,
)
div = """
<div id="map_canvas" style="width: 640px; height: 390px;"><p>&nbsp;</p></div>
"""
return script,div
def interactive_agegroupcpchart(age,normalized=False):
durations = [1,4,30,60]
distances = [100,500,1000,2000,5000,6000,10000,21097,42195]
@@ -2828,6 +2949,114 @@ def interactive_chart(id=0,promember=0,intervaldata = {}):
return [script,div]
def interactive_chart_video(videodata):
spm = videodata['spm']
time = range(len(spm))
data = zip(time,spm)
data2 = "["
for t,s in data:
data2 += "{x: %s, y: %s}, " % (t,s)
data2 = data2[:-2] + "]"
markerpoint = {
'x': time[0],
'y': spm[0],
'r': 10,
}
div = """
<canvas id="myChart">
</canvas>
"""
script = """
var ctx = document.getElementById("myChart").getContext('2d');
var data = %s
var myChart = new Chart(ctx, {
type: 'scatter',
label: 'SPM',
animationSteps: 10,
options: {
legend: {
display: false,
},
animation: {
duration: 100,
},
scales: {
yAxes: [{
scaleLabel: {
display: true,
labelString: 'Stroke Rate'
}
}],
xAxes: [{
scaleLabel: {
type: 'linear',
display: true,
labelString: 'Time (seconds)'
}
}],
}
},
data:
{
datasets: [
{
type: 'bubble',
label: 'now',
data: [ %s ],
backgroundColor: '#36a2eb',
},
{
label: 'spm',
data: data,
backgroundColor: "#ff0000",
borderColor: "#ff0000",
fill: false,
borderDash: [0, 0],
pointRadius: 1,
pointHoverRadius: 1,
showLine: true,
tension: 0,
},
]
},
});
var marker = {
datapoint: %s ,
setLatLng: function (LatLng) {
var lat = LatLng.lat;
var lng = LatLng.lng;
this.datapoint = {
'x': lat,
'y': lng,
'r': 10,
}
myChart.data.datasets[0].data[0] = this.datapoint;
myChart.update();
}
}
marker.setLatLng({
'lat': data[0]['x'],
'lng': data[0]['y']
})
""" % (data2, markerpoint, markerpoint)
return [script,div]
def interactive_multiflex(datadf,xparam,yparam,groupby,extratitle='',
ploterrorbars=False,
title=None,binsize=1,colorlegend=[],
+69 -45
View File
@@ -34,7 +34,6 @@ nometrics = [
'workoutid',
'totalangle',
'hr_bottom',
'x_right',
'Position',
'Extensions',
'GPS Speed',
@@ -56,8 +55,9 @@ rowingmetrics = (
'ax_min': 0,
'ax_max': 1e5,
'mode':'both',
'type': 'basic'}),
'type': 'basic',
'group':'basic'}),
('hr',{
'numtype':'integer',
'null':True,
@@ -65,7 +65,8 @@ rowingmetrics = (
'ax_min': 100,
'ax_max': 200,
'mode':'both',
'type': 'basic'}),
'type': 'basic',
'group':'athlete'}),
('pace',{
'numtype':'float',
@@ -74,7 +75,8 @@ rowingmetrics = (
'ax_min': 1.0e3*210,
'ax_max': 1.0e3*75,
'mode':'both',
'type': 'basic'}),
'type': 'basic',
'group': 'basic'}),
('velo',{
'numtype':'float',
@@ -84,7 +86,8 @@ rowingmetrics = (
'ax_max': 8,
'default': 0,
'mode':'both',
'type':'pro'}),
'type':'pro',
'group': 'basic'}),
# ('powerhr',{
# 'numtype':'float',
@@ -93,8 +96,9 @@ rowingmetrics = (
# 'ax_min':0,
# 'ax_max':300,
# 'mode':'both',
# 'type':'pro'}),
# 'type':'pro',
# 'group':'athlete'}),
('spm',{
'numtype':'float',
'null':True,
@@ -102,8 +106,9 @@ rowingmetrics = (
'ax_min': 15,
'ax_max': 45,
'mode':'both',
'type': 'basic'}),
'type': 'basic',
'group':'basic'}),
('driveenergy',{
'numtype':'float',
'null':True,
@@ -111,8 +116,9 @@ rowingmetrics = (
'ax_min': 0,
'ax_max': 1000,
'mode':'both',
'type': 'pro'}),
'type': 'pro',
'group':'forcepower'}),
('power',{
'numtype':'float',
'null':True,
@@ -120,8 +126,9 @@ rowingmetrics = (
'ax_min': 0,
'ax_max': 600,
'mode':'both',
'type': 'basic'}),
'type': 'basic',
'group':'forcepower'}),
('averageforce',{
'numtype':'float',
'null':True,
@@ -129,8 +136,9 @@ rowingmetrics = (
'ax_min': 0,
'ax_max': 1200,
'mode':'both',
'type': 'pro'}),
'type': 'pro',
'group':'forcepower'}),
('peakforce',{
'numtype':'float',
'null':True,
@@ -138,16 +146,18 @@ rowingmetrics = (
'ax_min': 0,
'ax_max': 1500,
'mode':'both',
'type': 'pro'}),
'type': 'pro',
'group':'forcepower'}),
('drivelength',{
'numtype':'float',
'null':True,
'verbose_name': 'Drive Length (m)',
'ax_min': 0,
'ax_max': 2.5,
'mode':'both',
'type': 'pro'}),
'mode':'rower',
'type': 'pro',
'group':'stroke'}),
('forceratio',{
'numtype':'float',
@@ -156,16 +166,18 @@ rowingmetrics = (
'ax_min': 0,
'ax_max': 1,
'mode':'both',
'type': 'pro'}),
'type': 'pro',
'group': 'forcepower'}),
('distance',{
'numtype':'float',
'null':True,
'verbose_name': 'Distance (m)',
'verbose_name': 'Interval Distance (m)',
'ax_min': 0,
'ax_max': 1e5,
'mode':'both',
'type': 'basic'}),
'type': 'basic',
'group':'basic'}),
('cumdist',{
'numtype':'float',
@@ -174,7 +186,8 @@ rowingmetrics = (
'ax_min': 0,
'ax_max': 1e5,
'mode':'both',
'type': 'basic'}),
'type': 'basic',
'group':'basic'}),
('drivespeed',{
'numtype':'float',
@@ -184,7 +197,8 @@ rowingmetrics = (
'ax_max': 4,
'default': 0,
'mode':'both',
'type': 'pro'}),
'type': 'pro',
'group': 'stroke'}),
('catch',{
'numtype':'float',
@@ -194,7 +208,8 @@ rowingmetrics = (
'ax_max': -75,
'default': 0,
'mode':'water',
'type': 'pro'}),
'type': 'pro',
'group': 'stroke'}),
('slip',{
'numtype':'float',
@@ -204,7 +219,8 @@ rowingmetrics = (
'ax_max': 20,
'default': 0,
'mode':'water',
'type': 'pro'}),
'type': 'pro',
'group': 'stroke'}),
('finish',{
'numtype':'float',
@@ -214,7 +230,8 @@ rowingmetrics = (
'ax_max': 55,
'default': 0,
'mode':'water',
'type': 'pro'}),
'type': 'pro',
'group': 'stroke'}),
('wash',{
'numtype':'float',
@@ -224,7 +241,8 @@ rowingmetrics = (
'ax_max': 30,
'default': 0,
'mode':'water',
'type': 'pro'}),
'type': 'pro',
'group': 'stroke'}),
('peakforceangle',{
'numtype':'float',
@@ -234,9 +252,10 @@ rowingmetrics = (
'ax_max': 50,
'default': 0,
'mode':'water',
'type': 'pro'}),
'type': 'pro',
'group':'stroke'}),
('totalangle',{
'numtype':'float',
'null':True,
@@ -245,9 +264,10 @@ rowingmetrics = (
'ax_max': 140,
'default': 0,
'mode':'water',
'type': 'pro'}),
'type': 'pro',
'group':'stroke'}),
('effectiveangle',{
'numtype':'float',
'null':True,
@@ -256,7 +276,8 @@ rowingmetrics = (
'ax_max': 140,
'default': 0,
'mode':'water',
'type': 'pro'}),
'type': 'pro',
'group':'stroke'}),
('rhythm',{
'numtype':'float',
@@ -266,7 +287,8 @@ rowingmetrics = (
'ax_max': 55,
'default': 1.0,
'mode':'erg',
'type': 'pro'}),
'type': 'pro',
'group':'stroke'}),
('efficiency',{
'numtype':'float',
@@ -276,7 +298,8 @@ rowingmetrics = (
'ax_max': 110,
'default': 0,
'mode':'water',
'type': 'pro'}),
'type': 'pro',
'group':'forcepower'}),
('distanceperstroke',{
'numtype':'float',
@@ -286,10 +309,13 @@ rowingmetrics = (
'ax_max': 15,
'default': 0,
'mode':'both',
'type': 'basic'}),
'type': 'basic',
'group':'basic'}),
)
metricsgroups = list(set([d['group'] for n,d in rowingmetrics]))
dtypes = {}
for name,d in rowingmetrics:
@@ -297,11 +323,11 @@ for name,d in rowingmetrics:
dtypes[name] = float
elif d['numtype'] == 'int':
dtypes[name] = int
axesnew = [
(name,d['verbose_name'],d['ax_min'],d['ax_max'],d['type']) for name,d in rowingmetrics
]
axes = tuple(axesnew+[('None','<None>',0,1,'basic')])
axlabels = {ax[0]:ax[1] for ax in axes}
@@ -328,7 +354,7 @@ defaultfavoritecharts = (
'workouttype':'both',
'reststrokes':True,
'notes':"""This chart shows your pace and heart rate versus time.
Heart rate values will be shown only when it is in your data, i.e.
Heart rate values will be shown only when it is in your data, i.e.
in case you recorded your heart rate during your workout""",
},
{
@@ -358,9 +384,9 @@ plotted versus time. """,
'plottype':'scatter',
'workouttype':'otw',
'reststrokes':True,
'notes':"""This chart shows the Distance per Stroke versus stroke
'notes':"""This chart shows the Distance per Stroke versus stroke
stroke rate. You should see a steady decline of the Distance per Stroke
as you increase stroke rate. Typical values are > 10m for steady state
as you increase stroke rate. Typical values are > 10m for steady state
dropping to 8m for race pace in the single.""",
},
{
@@ -395,5 +421,3 @@ def calc_trimp(df,sex,hrmax,hrmin,hrftp):
hrtss = 100*trimp/trimp1hr
return trimp,hrtss
+15
View File
@@ -3720,3 +3720,18 @@ class BlogPost(models.Model):
title = models.TextField(max_length=300)
link = models.TextField(max_length=300)
date = models.DateField()
defaultgroups = ['basic']
class VideoAnalysis(models.Model):
name = models.CharField(default='', max_length=150,blank=True,null=True)
video_id = models.CharField(default='',max_length=150)
delay = models.IntegerField(default=0)
workout = models.ForeignKey(Workout, on_delete=models.CASCADE)
metricsgroups = TemplateListField(default=defaultgroups)
class Meta:
unique_together = ('video_id','workout')
def __str__(self):
return self.name
+360
View File
@@ -0,0 +1,360 @@
{% extends "newbase.html" %}
{% load staticfiles %}
{% load rowerfilters %}
{% load i18n %}
{% load leaflet_tags %}
{% block title %}Workout Video{% endblock %}
{% block meta %}
{% leaflet_js %}
{% leaflet_css %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.2/Chart.js"></script>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<script>
$(document).ready(function() {
$('form').submit(function(e) {
$(':disabled').each(function(e) {
$(this).removeAttr('disabled');
});
});
});
</script>
{% endblock %}
{% block main %}
<style>
.slidecontainer {
width: 100%; /* Width of the outside container */
}
.bold { font-weight: bold; }
/* The slider itself */
.slider {
-webkit-appearance: none; /* Override default CSS styles */
appearance: none;
width: 100%; /* Full-width */
height: 25px; /* Specified height */
background: #d3d3d3; /* Grey background */
outline: none; /* Remove outline */
opacity: 0.7; /* Set transparency (for mouse-over effects on hover) */
-webkit-transition: .2s; /* 0.2 seconds transition on hover */
transition: opacity .2s;
}
/* Mouse-over effects */
.slider:hover {
opacity: 1; /* Fully shown on mouse-over */
}
/* The slider handle (use -webkit- (Chrome, Opera, Safari, Edge) and -moz- (Firefox) to override default look) */
.slider::-webkit-slider-thumb {
-webkit-appearance: none; /* Override default look */
appearance: none;
width: 25px; /* Set a specific slider handle width */
height: 25px; /* Slider handle height */
background: #4CAF50; /* Green background */
cursor: pointer; /* Cursor on hover */
}
.slider::-moz-range-thumb {
width: 25px; /* Set a specific slider handle width */
height: 25px; /* Slider handle height */
background: #4CAF50; /* Green background */
cursor: pointer; /* Cursor on hover */
}
</style>
{% language 'en' %}
<h1>Video Analysis for {{ workout.name }}</h1>
<ul class="main-content">
{% if user.is_authenticated and user == workout.user.user and not locked %}
<li class="grid_2">
<p>Paste link to you tube video below</p>
<p>Use the slider to locate start point for video on workout map</p>
<p>Playing the video will advance the data in synchonization. Use the regular YouTube
controls
to move around in the video and play it.</p>
<p>You can make manual adjustments to the delay to fine tune the alignment.
Once you are finished, check "Lock Video and Data" to lock the video and the data.</p>
<p>Once you are happy with the alignment, you can save the analysis, and share with other people.</p>
</li>
{% else %}
<li class="grid_2">
<p>Playing the video will advance the data in synchronization. Use the regular
YouTube controls to move around in the video and play it.</p>
{% endif %}
</ul>
<ul class="main-content">
<li class="grid_2">
<div id="theplot" class="flexplot mapdiv">
{{ mapdiv | safe}}
</div>
<div class="slidecontainer">
<input type="range" min="0" max="{{ maxtime }}" value="{{ analysis.delay }}"
id="myRange">
</div>
</li>
<li class="grid_2">
<div id="player"></div>
<script>
// 1. Code for the map
{{ mapscript | safe }}
// 2. This code loads the IFrame Player API code asynchronously.
var tag = document.createElement('script');
tag.src = "https://www.youtube.com/iframe_api";
var firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
// 3. This function creates an <iframe> (and YouTube player)
// after the API code downloads.
var player;
var playing;
var videotime = 0;
var data = JSON.parse('{{ data|safe }}');
{% for id, metric in metrics.items %}
var {{ id }}_values = data["{{ id }}"];
// console.log("{{ id }}_values",{{ id }}_values);
{% endfor %}
// var boatspeed = data["boatspeed"];
var latitude = data["latitude"];
var longitude = data["longitude"];
// var spm = data["spm"];
// var ctch = data["catch"];
function onYouTubeIframeAPIReady() {
player = new YT.Player('player', {
height: '315',
width: '560',
modestbranding: '1',
videoId: '{{ video_id }}',
events: {
'onReady': onPlayerReady,
'onStateChange': onPlayerStateChange
}
});
}
// 4. The API will call this function when the video player is ready.
function onPlayerReady(event) {
// event.target.playVideo();
}
function updateTime() {
var oldTime = videotime;
var slider = document.getElementById("myRange");
var lock = document.getElementById("lock");
if(player && player.getCurrentTime) {
videotime = player.getCurrentTime();
var delay = document.getElementById("id_delay").value;
sliderpos = Math.round(videotime) + Math.round(delay);
slider.value = sliderpos;
var datatime = parseFloat(videotime)+parseFloat(delay);
// velo = boatspeed[Math.round(datatime)];
lat = latitude[Math.round(datatime)];
lon = longitude[Math.round(datatime)];
// strokerate = spm[Math.round(datatime)];
// catchangle = ctch[Math.round(datatime)];
{% for id, metric in metrics.items %}
{{ id }}_now = {{ id }}_values[Math.round(datatime)];
// console.log(datatime,{{ id }}_now, "{{ metric.name }}")
{% endfor %}
document.getElementById("time").innerHTML = Math.round(videotime);
document.getElementById("datatime").innerHTML = Math.round(datatime);
// document.getElementById("speed").innerHTML = velo;
// document.getElementById("spm").innerHTML = strokerate;
// document.getElementById("catch").innerHTML = catchangle;
{% for id, metric in metrics.items %}
document.getElementById("{{ id }}").innerHTML = {{ id }}_now;
document.getElementById("{{ id }}").className = 'bold';
{% endfor %}
{% for group in metricsgroups %}
try {
set_{{ group }}();
} catch (e) {}
{% endfor %}
// gauge.set(catch_now);
try {
var newLatLng = new L.LatLng(lat, lon);
// console.log(newLatLng);
marker.setLatLng(newLatLng);
} catch (e) {}
}
if(videotime !== oldTime) {
onProgress(videotime);
}
}
timeupdater = setInterval(updateTime, 1000);
// when the time changes, this will be called.
function onProgress(currentTime) {
var slider = document.getElementById("myRange");
var lock = document.getElementById("lock");
videotime = player.getCurrentTime();
var delay = document.getElementById("id_delay").value;
sliderpos = Math.round(videotime) + Math.round(delay);
slider.value = sliderpos;
}
function stopVideo() {
player.stopVideo();
}
// call this function when player state changes
function onPlayerStateChange(event) {
if (event.data == YT.PlayerState.PLAYING) {
playing = false;
} else {
playing = true;
}
}
</script>
</li>
{% if user.is_authenticated and user == workout.user.user %}
<li class="grid_4">
<input type="checkbox" name="lock" id="lock" value="Lock">Lock Data and Video
</li>
{% else %}
<li class="grid_4">
<input type="hidden" name="lock" id="lock" value="Lock">
</li>
{% endif %}
<li>
Data Time
<span id="datatime">
</span> seconds
</li>
<li>
Video Time
<span id="time">
</span> seconds
</li>
{% for id, metric in metrics.items %}
<li>
{{ metric.name }}
<span id="{{ id }}">
</span> {{ metric.unit }}
</li>
{% endfor %}
{% for group in metricsgroups %}
<li class="grid">
<canvas id="{{ group }}"></canvas>
</li>
{% endfor %}
</ul>
<p>&nbsp;</p>
<form enctype="multipart/form-data" action="" method="post">
<ul class="main-content">
{% if form and user.is_authenticated and user == workout.user.user %}
<li class="grid_2">
<table>
{{ form.as_table }}
</table>
{% csrf_token %}
{% if not analysis.id %}
<input type="submit" name="reload_button" value="Reload">
{% endif %}
<input type="submit" name="save_button" value="Save">
</li>
<li class="grid_2">
<table>
{{ metricsform.as_table }}
</table>
</li>
{% else %}
<input type="hidden" id="id_delay" value="{{ analysis.delay }}">
{% endif %}
<li>
{% if analysis and user.is_authenticated and user == analysis.workout.user.user %}
<p>
<a href="/rowers/video/{{ analysis.id }}/delete/">Delete Analysis</a>
</p>
{% endif %}
</li>
</ul>
</form>
<script>
$(document).ready( function() {
// lock
var lock = document.getElementById("lock");
var output = document.getElementById("id_delay");
{% if locked %}
lock.checked = true;
output.disabled = true;
{% endif %}
// slider
var slider = document.getElementById("myRange");
{% if user.is_authenticated and user == workout.user.user %}
document.getElementById("myRange").style.display = "block";
{% else %}
document.getElementById("myRange").style.display = "none";
{% endif %}
var output = document.getElementById("id_delay");
try {
output.value = Math.round(slider.value)-Math.round(player.getCurrentTime()); // Display the default slider value
}
catch(err) {
output.value = Math.round(slider.value);
}
// Update the current slider value (each time you drag the slider handle)
slider.oninput = function() {
clearInterval(timeupdater)
if (lock.checked) {
if (this.value-output.value > 0) {
player.seekTo(this.value-output.value);
} else {
if (playing) {
player.seekTo(0);
player.playVideo();
}
else {
player.seekTo(0);
player.pauseVideo();
}
}
} else {
// console.log('changing');
output.value = this.value-Math.round(player.getCurrentTime());
}
timeupdater = setInterval(updateTime, 1000)
}
output.oninput = function() {
slider.value = this.value+Math.round(player.getCurrentTime());
}
// lock delay form field if checkbox checked
lock.oninput = function() {
if (this.checked) {
output.disabled = true;
} else {
output.disabled = false;
}
}
});
</script>
<script type="text/javascript" src="https://bernii.github.io/gauge.js/dist/gauge.js"></script>
<script type="text/javascript" src="{% static 'js/videogauges.js' %}"></script>
{% endlanguage %}
{% endblock %}
{% block sidebar %}
{% include 'menu_workout.html' %}
{% endblock %}
+8 -7
View File
@@ -12,14 +12,15 @@
<h1>Recent Graphs</h1>
<ul class="main-content">
{% if graphs %}
<li class="grid_2">
<li class="grid_2">
<form id="searchform" action="."
method="get" accept-charset="utf-8">
{{ searchform }}
<input type="submit" value="GO"></input>
</form>
</li>
{% if graphs %}
<li class="grid_2">
<p>
<span>
@@ -28,7 +29,7 @@
<a href="?page=1&q={{ request.GET.q }}">
<i class="fas fa-arrow-alt-to-left"></i>
</a>
<a href="?page={{ workouts.previous_page_number }}&q={{ request.GET.q }}">
<a href="?page={{ graphs.previous_page_number }}&q={{ request.GET.q }}">
<i class="fas fa-arrow-alt-left"></i>
</a>
{% else %}
@@ -40,11 +41,11 @@
</a>
{% endif %}
{% endif %}
<span>
Page {{ graphs.number }} of {{ graphs.paginator.num_pages }}.
</span>
{% if graphs.has_next %}
{% if request.GET.q %}
<a href="?page={{ graphs.next_page_number }}&q={{ request.GET.q }}">
@@ -69,7 +70,7 @@
<li>
<p class="caption">
<a href="/rowers/graph/{{ graph.id }}/">
<img src="/{{ graph.filename }}"
<img src="/{{ graph.filename }}"
onerror="this.src='/static/img/rowingtimer.gif'"
alt="{{ graph.filename }}" width="120" height="100">
</a>
@@ -90,4 +91,4 @@
{% block sidebar %}
{% include 'menu_workouts.html' %}
{% endblock %}
{% endblock %}
+90
View File
@@ -0,0 +1,90 @@
{% extends "newbase.html" %}
{% load staticfiles %}
{% load rowerfilters %}
{% block title %}Rowsandall Recent Video Analyses{% endblock %}
{% block scripts %}
{% include "monitorjobs.html" %}
{% endblock %}
{% block main %}
<h1>Recent videos</h1>
<ul class="main-content">
<li class="grid_2">
<form id="searchform" action="."
method="get" accept-charset="utf-8">
{{ searchform }}
<input type="submit" value="GO"></input>
</form>
</li>
{% if analyses %}
<li class="grid_2">
<p>
<span>
{% if analyses.has_previous %}
{% if request.GET.q %}
<a href="?page=1&q={{ request.GET.q }}">
<i class="fas fa-arrow-alt-to-left"></i>
</a>
<a href="?page={{ analyses.previous_page_number }}&q={{ request.GET.q }}">
<i class="fas fa-arrow-alt-left"></i>
</a>
{% else %}
<a href="?page=1">
<i class="fas fa-arrow-alt-to-left"></i>
</a>
<a href="?page={{ analyses.previous_page_number }}">
<i class="fas fa-arrow-alt-left"></i>
</a>
{% endif %}
{% endif %}
<span>
Page {{ analyses.number }} of {{ analyses.paginator.num_pages }}.
</span>
{% if analyses.has_next %}
{% if request.GET.q %}
<a href="?page={{ analyses.next_page_number }}&q={{ request.GET.q }}">
<i class="fas fa-arrow-alt-right"></i>
</a>
<a href="?page={{ analyses.paginator.num_pages }}&q={{ request.GET.q }}">
<i class="fas fa-arrow-alt-to-right">
</a>
{% else %}
<a href="?page={{ analyses.next_page_number }}">
<i class="fas fa-arrow-alt-right"></i>
</a>
<a href="?page={{ analyses.paginator.num_pages }}">
<i class="fas fa-arrow-alt-to-right"></i>
</a>
{% endif %}
{% endif %}
</span>
</p>
</li>
{% for video in analyses %}
<li>
<div>{{ video.name }}</div>
<a href="/rowers/video/{{ video.id|encode }}/">
<img src="https://img.youtube.com/vi/{{ video.video_id }}/hqdefault.jpg"/>
</a>
</li>
{% endfor %}
{% else %}
<li class="grid_4">
<p>
No videos found
</p>
</li>
{% endif %}
</ul>
{% endblock %}
{% block sidebar %}
{% include 'menu_analytics.html' %}
{% endblock %}
+91 -73
View File
@@ -2,6 +2,24 @@
{% load rowerfilters %}
<h1><a href="/rowers/analysis/">Analysis</a></h1>
<ul class="cd-accordion-menu animated">
<li class="has-children" id="videos">
<input type="checkbox" name="group-videos" id="group-videos" checked>
<label for="group-videos">
<i class="fas fa-film fa-fw"></i>&nbsp;Video Analysis
</label>
<ul>
<li id="videos-analyses">
<a href="/rowers/videos/">
<i class="fas fa-film fa-fw"></i>&nbsp;Video Analyses
</a>
</li>
<li id="video-add">
<a href="/rowers/add-video/">
<i class="fas fa-video-plus fa-fw"></i>&nbsp;New Video Analysis
</a>
</li>
</ul>
</li>
<li class="has-children" id="fitness">
<input type="checkbox" name="group-fitness" id="group-fitness" checked>
<label for="group-fitness">
@@ -18,7 +36,7 @@
<i class="fas fa-balance-scale fa-fw"></i>&nbsp;Compare
</a>
{% endif %}
</li>
</li>
<li id="fitness-powerprogress">
<a href="/rowers/fitness-progress/">
<i class="far fa-watch-fitness fa-fw"></i>&nbsp;Power Progress
@@ -61,79 +79,79 @@
<a href="/rowers/user-analysis-select/stats/">
<i class="fal fa-table fa-fw"></i>&nbsp;Statistics
</a>
</li>
<li id="stats-histo">
<a href="/rowers/user-analysis-select/histo/">
<i class="fas fa-chart-bar fa-fw"></i>&nbsp;Histogram
</a>
</li>
<li>
<a href="/rowers/alerts/">
<i class="fas fa-bell fa-fw"></i>&nbsp;Alerts
</a>
</li>
</ul>
</li>
<li>
<a href="/rowers/user-analysis-select/flexall/">
<i class="fas fa-chart-line fa-fw"></i>&nbsp;Cumulative Flex Chart
</a>
</li>
<li>
<a href="/rowers/laboratory/">
<i class="fas fa-flask fa-fw"></i>&nbsp;Laboratory</a>
</li>
</ul><!-- cd-accordion-menu -->
</li>
<li id="stats-histo">
<a href="/rowers/user-analysis-select/histo/">
<i class="fas fa-chart-bar fa-fw"></i>&nbsp;Histogram
</a>
</li>
<li>
<a href="/rowers/alerts/">
<i class="fas fa-bell fa-fw"></i>&nbsp;Alerts
</a>
</li>
</ul>
</li>
<li>
<a href="/rowers/user-analysis-select/flexall/">
<i class="fas fa-chart-line fa-fw"></i>&nbsp;Cumulative Flex Chart
</a>
</li>
<li>
<a href="/rowers/laboratory/">
<i class="fas fa-flask fa-fw"></i>&nbsp;Laboratory</a>
</li>
</ul><!-- cd-accordion-menu -->
{% if user.is_authenticated and user|is_manager and rower %}
<p>&nbsp;</p>
{% if user.is_authenticated and user|is_manager and rower %}
<p>&nbsp;</p>
{% if user|coach_rowers %}
<ul class="cd-accordion-menu animated">
<li class="has-children" id="athletes">
<input type="checkbox" name="athlete-selector" id="athlete-selector">
<label for="athlete-selector"><i class="fas fa-users fa-fw"></i>&nbsp;Athletes</label>
<ul>
{% for member in user|coach_rowers %}
<a href={{ request.path|userurl:member.user }}>
<i class="fas fa-user fa-fw"></i>
{% if member.user == rower.user %}
&bull;
{% else %}
&nbsp;
{% endif %}
{{ member.user.first_name }} {{ member.user.last_name }}
</a>
{% endfor %}
</ul>
</li>
</ul>
{% endif %}
{% endif %}
{% if user|coach_rowers %}
<ul class="cd-accordion-menu animated">
<li class="has-children" id="athletes">
<input type="checkbox" name="athlete-selector" id="athlete-selector">
<label for="athlete-selector"><i class="fas fa-users fa-fw"></i>&nbsp;Athletes</label>
<ul>
{% for member in user|coach_rowers %}
<a href={{ request.path|userurl:member.user }}>
<i class="fas fa-user fa-fw"></i>
{% if member.user == rower.user %}
&bull;
{% else %}
&nbsp;
{% endif %}
{{ member.user.first_name }} {{ member.user.last_name }}
</a>
{% endfor %}
</ul>
</li>
</ul>
{% endif %}
{% endif %}
{% if user|user_teams %}
<p>&nbsp;</p>
<ul class="cd-accordion-menu animated">
<li class="has-children" id="teams">
<input type="checkbox" name="team-selector" id="team-selector">
<label for="team-selector"><i class="fas fa-bullhorn fa-fw"></i>&nbsp;Groups</label>
<ul>
{% for tteam in teams %}
<li>
<a href={{ request.path|teamurl:tteam }}>
<i class="fas fa-users fa-fw"></i>
{% if tteam == team %}
&bull;
{% else %}
&nbsp;
{% endif %}
{{ tteam.name }}
</a>
</li>
{% endfor %}
</ul>
</li>
</ul>
{% endif %}
{% if user|user_teams %}
<p>&nbsp;</p>
<ul class="cd-accordion-menu animated">
<li class="has-children" id="teams">
<input type="checkbox" name="team-selector" id="team-selector">
<label for="team-selector"><i class="fas fa-bullhorn fa-fw"></i>&nbsp;Groups</label>
<ul>
{% for tteam in teams %}
<li>
<a href={{ request.path|teamurl:tteam }}>
<i class="fas fa-users fa-fw"></i>
{% if tteam == team %}
&bull;
{% else %}
&nbsp;
{% endif %}
{{ tteam.name }}
</a>
</li>
{% endfor %}
</ul>
</li>
</ul>
{% endif %}
{% include 'menuscript.html' %}
{% include 'menuscript.html' %}
+7 -2
View File
@@ -45,6 +45,11 @@
<i class="fas fa-balance-scale fa-fw"></i>&nbsp;Compare
</a>
</li>
<li id="video-analysis">
<a href="/rowers/workout/{{ workout.id|encode }}/video/">
<i class="fas fa-video-plus fa-fw"></i>&nbsp;Video Analysis
</a>
</li>
{% if user.is_authenticated and workout|may_edit:request %}
<li id="chart-image">
<a href="/rowers/workout/{{ workout.id|encode }}/image/">
@@ -106,7 +111,7 @@
<a href="/rowers/workout/{{ workout.id|encode }}/addstatic/13/">
<i class="far fa-chart-pie fa-fw"></i>&nbsp;Power (Pie)
</a>
</li>
</li>
<li id="chart-hrpie">
<a href="/rowers/workout/{{ workout.id|encode }}/addstatic/3/">
<i class="fas fa-heartbeat fa-fw"></i>&nbsp;Heart Rate (Pie)
@@ -266,7 +271,7 @@
<input type="checkbox" name="group-advanced" id="group-advanced">
<label for="group-advanced">Advanced</label>
<ul>
{% if workout|water %}
{% if workout|water %}
<li id="advanced-wind">
<a href="/rowers/workout/{{ workout.id|encode }}/wind/">
<i class="fas fa-pennant fa-fw"></i>&nbsp;Wind
+2 -5
View File
@@ -2,12 +2,9 @@
<li class="grid_2">
<div class="mapdiv">
{{ mapdiv|safe }}
{{ mapscript|safe }}
</div>
</li>
</ul>
+1
View File
@@ -1,4 +1,5 @@
{% load rowerfilters %}
{% load leaflet_tags %}
<div class="grid_2 alpha">
<p>
<a class="button gray small" href="/rowers/workout/{{ workout.id|encode }}/map">Map View</a>
@@ -0,0 +1,30 @@
{% extends "newbase.html" %}
{% load staticfiles %}
{% load rowerfilters %}
{% block title %}Delete Graph Image {% endblock %}
{% block main %}
<ul class="main-content">
<li class="grid_2">
<form action="" method="post">
{% csrf_token %}
<p>Are you sure you want to delete this video analysis?</p>
<p>
<input class="button red" type="submit" value="Confirm">
</p>
</form>
</li>
<li class="grid_2">
<a href="/rowers/video/{{ object.id|encode }}">
<img src="https://img.youtube.com/vi/{{ object.video_id }}/hqdefault.jpg"/>
</a>
</li>
</ul>
{% endblock %}
{% block sidebar %}
{% include 'menu_workouts.html' %}
{% endblock %}
+117
View File
@@ -0,0 +1,117 @@
{% extends "newbase.html" %}
{% load staticfiles %}
{% load rowerfilters %}
{% block title %}Workouts{% endblock %}
{% block main %}
<script>
function toggle(source) {
checkboxes = document.querySelectorAll("input[name='workouts']");
for(var i=0, n=checkboxes.length;i<n;i++) {
checkboxes[i].checked = source.checked;
}
}
</script>
<script src="https://code.jquery.com/jquery-1.9.1.min.js"></script>
<script>
$(function() {
// Get the form fields and hidden div
var modality = $("#id_modality");
var hidden = $("#id_waterboattype");
// Hide the fields.
// Use JS to do this in case the user doesn't have JS
// enabled.
hidden.hide();
if (modality.val() == 'water') {
hidden.show();
}
// Setup an event listener for when the state of the
// checkbox changes.
modality.change(function() {
// Check to see if the checkbox is checked.
// If it is, show the fields and populate the input.
// If not, hide the fields.
var Value = modality.val();
var otwtypes = ['water','coastal','c-boat','churchboat']
if (otwtypes.includes(Value)) {
// Show the hidden fields.
hidden.show();
} else {
// Make sure that the hidden fields are indeed
// hidden.
hidden.hide();
// You may also want to clear the value of the
// hidden fields here. Just in case somebody
// shows the fields, enters data to them and then
// unticks the checkbox.
//
// This would do the job:
//
// $("#hidden_field").val("");
}
});
});
</script>
<ul class="main-content">
<li class="grid_4">
<p>Select the workout you want to add a video to</p>
</li>
<li class="grid_2 maxheight">
<form id="searchform" action=""
method="get" accept-charset="utf-8">
{{ searchform }}
<input type="submit" value="GO"></input>
</form>
<form enctype="multipart/form-data" action="" method="post">
{% if workouts %}
<table width="100%" class="listtable">
{{ form.as_table }}
</table>
{% else %}
<p> No workouts found </p>
{% endif %}
</li>
<li class="grid_2">
<form enctype="multipart/form-data" method="post">
<table>
{{ dateform.as_table }}
</table>
{% csrf_token %}
<input name='optionsform' type="submit" value="Submit">
<input name='workoutform' type='submit' value="Create Video">
</form>
</li>
</ul>
{% endblock %}
{% block scripts %}
{% if request.method == 'POST' %}
<script type='text/javascript'
src='https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js'>
</script>
{% endif %}
{% endblock %}
{% block sidebar %}
{% include 'menu_analytics.html' %}
{% endblock %}
+19 -11
View File
@@ -35,9 +35,9 @@ $('#id_workouttype').on('change', function(){
|| $(this).val() == 'churchboat'
) {
$('#id_boattype').toggle(true);
} else {
} else {
$('#id_boattype').toggle(false);
$('#id_boattype').val('1x');
$('#id_boattype').val('1x');
}
if (
$(this).val() == 'rower'
@@ -45,13 +45,13 @@ $('#id_workouttype').on('change', function(){
|| $(this).val() == 'slides'
) {
$('#id_dragfactor').toggle(true);
} else {
} else {
$('#id_dragfactor').toggle(false);
$('#id_dragfactor').val('0');
$('#id_dragfactor').val('0');
}
});
$('#id_workouttype').change();
$('#id_workouttype').change();
});
</script>
{% endblock %}
@@ -114,7 +114,7 @@ $('#id_workouttype').change();
Please correct the error{{ form.errors|pluralize }} below.
</p>
{% endif %}
<form id="importantform"
enctype="multipart/form-data" action="" method="post">
@@ -126,9 +126,9 @@ $('#id_workouttype').change();
</form>
</li>
<li class="grid_2">
<h1>Workout Summary</h1>
<p>
<pre>
{{ workout.summary }}
@@ -151,13 +151,13 @@ $('#id_workouttype').change();
<script async="true" type="text/javascript">
Bokeh.set_log_level("info");
</script>
<div class="mapdiv">
{{ mapdiv|safe }}
</div>
{{ mapscript|safe }}
</li>
@@ -165,12 +165,20 @@ $('#id_workouttype').change();
{% for graph in graphs %}
<li>
<a href="/rowers/graph/{{ graph.id }}/">
<img src="/{{ graph.filename }}"
<img src="/{{ graph.filename }}"
onerror="this.src='/static/img/rowingtimer.gif'"
alt="{{ graph.filename }}" width="120" height="100">
</a>
</li>
{% endfor %}
{% for video in videos %}
<li>
<div>{{ video.name }}</div>
<a href="/rowers/video/{{ video.id|encode }}/">
<img src="https://img.youtube.com/vi/{{ video.video_id }}/hqdefault.jpg"/>
</a>
</li>
{% endfor %}
</ul>
{% endblock %}
+10
View File
@@ -141,6 +141,16 @@ def mocked_read_df_sql(id):
return df
def mocked_get_video_data(*args, **kwargs):
with open('rowers/tests/testdata/videodata.json','r') as infile:
data = json.load(infile)
maxtime = 4135.
with open('rowers/tests/testdata/videometrics.json','r') as infile:
metrics = json.load(infile)
return data,metrics,maxtime
def mocked_getrowdata_db(*args, **kwargs):
df = pd.read_csv('rowers/tests/testdata/getrowdata_mock.csv')
+13 -13
View File
@@ -13,7 +13,7 @@ tested = [
'/rowers/me/delete/'
]
#@pytest.mark.django_db
#@pytest.mark.django_db
@override_settings(TESTING=True)
class URLTests(TestCase):
def setUp(self):
@@ -25,7 +25,7 @@ class URLTests(TestCase):
r = Rower.objects.create(user=u,rowerplan='coach',gdproptin=True,
gdproptindate=timezone.now())
self.c = Client()
self.nu = datetime.datetime.now()
filename = 'rowers/tests/testdata/testdata.csv'
self.wotw = Workout.objects.create(name='testworkout',
@@ -34,20 +34,20 @@ class URLTests(TestCase):
starttime=self.nu.strftime('%H:%M:%S'),
duration="0:55:00",distance=8000,
csvfilename=filename)
self.wote = Workout.objects.create(name='testworkout',
workouttype='Indoor Rower',
user=r,date=self.nu.strftime('%Y-%m-%d'),
starttime=self.nu.strftime('%H:%M:%S'),
duration="0:55:00",distance=8000,
csvfilename=filename)
powerperc = 100*np.array([r.pw_ut2,
r.pw_ut1,
r.pw_at,
r.pw_tr,r.pw_an])/r.ftp
self.hrdata = {
'hrmax':r.max,
'hrut2':r.ut2,
@@ -103,7 +103,7 @@ class URLTests(TestCase):
'/rowers/help/',
'/rowers/histo/',
'/rowers/histo/user/1/',
# '/rowers/histo/user/1/2016-01-01/2016-12-31/',
# '/rowers/histo/user/1/2016-01-01/2016-12-31/',
'/rowers/histodata/',
# '/rowers/job-kill/1/',
# '/rowers/jobs-status/',
@@ -238,7 +238,7 @@ class URLTests(TestCase):
'/rowers/workout/upload/team/',
'/rowers/workouts-join/',
'/rowers/workouts-join-select/',
# '/rowers/workouts-join-select/2016-01-01/2016-12-31/',
# '/rowers/workouts-join-select/2016-01-01/2016-12-31/',
]
# urlstotest = ['/rowers/createplan/user/1/']
@@ -249,19 +249,21 @@ class URLTests(TestCase):
(url,200)
)
@parameterized.expand(lijst)
@patch('rowers.dataprep.create_engine')
@patch('rowers.dataprep.read_df_sql')
@patch('rowers.dataprep.getsmallrowdata_db')
@patch('requests.get',side_effect=mocked_requests)
@patch('requests.post',side_effect=mocked_requests)
@patch('rowers.dataprep.get_video_data',side_effect=mocked_get_video_data)
def test_url_generator(self,url,expected,
mocked_sqlalchemy,
mocked_read_df_sql,
mocked_getsmallrowdata_db,
mock_get,
mock_post):
mock_post,
mocked_get_video_data):
if url not in tested:
login = self.c.login(username='john',password='koeinsloot')
@@ -276,10 +278,10 @@ class URLTests(TestCase):
html = BeautifulSoup(response.content,'html.parser')
urls = [a['href'] for a in html.find_all('a')]
for u in urls:
if u not in tested and 'rowers' in u and 'http' not in u and 'authorize' not in u and 'import' not in u and 'logout' not in u:
response2 = self.c.get(u)
@@ -294,5 +296,3 @@ class URLTests(TestCase):
[200,302,301])
else:
tested.append(u)
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
{"boatspeed": {"name": "Boat Speed (m/s)", "metric": "velo", "unit": ""}, "cumdist": {"name": "Cumulative Distance (m)", "metric": "cumdist", "unit": ""}, "distance": {"name": "Distance (m)", "metric": "distance", "unit": ""}, "distanceperstroke": {"name": "Distance per Stroke (m)", "metric": "distanceperstroke", "unit": ""}, "pace": {"name": "Pace (/500m)", "metric": "pace", "unit": ""}, "spm": {"name": "Stroke Rate (spm)", "metric": "spm", "unit": ""}}
+28 -27
View File
@@ -48,12 +48,12 @@ def cleanbody(body):
body = body.decode('utf-8')
except AttributeError:
pass
regex = r".*---\n([\s\S]*?)\.\.\..*"
matches = re.finditer(regex,body)
for m in matches:
if m != None:
body = m.group(0)
@@ -112,7 +112,7 @@ def matchrace(line):
return None
return None
def matchsync(line):
results = []
tester = '((sync)|(synchronization)|(export))'
@@ -175,7 +175,7 @@ def gettypeoptions_body2(uploadoptions,body):
break
return uploadoptions
def getprivateoptions_body2(uploadoptions,body):
tester = re.compile('^(priva)')
for line in body.splitlines():
@@ -206,7 +206,7 @@ def getplotoptions_body2(uploadoptions,body):
if chart:
uploadoptions['make_plot'] = True
uploadoptions['plottype'] = chart
return uploadoptions
def getuseroptions_body2(uploadoptions,body):
@@ -236,7 +236,7 @@ def getsyncoptions_body2(uploadoptions,body):
uploadoptions[r] = True
return uploadoptions
def getsyncoptions(uploadoptions,values):
try:
value = values.lower()
@@ -247,7 +247,7 @@ def getsyncoptions(uploadoptions,values):
for v in values:
try:
v = v.lower()
if v in ['c2','concept2','logbook']:
uploadoptions['upload_to_C2'] = True
if v in ['tp','trainingpeaks']:
@@ -262,7 +262,7 @@ def getsyncoptions(uploadoptions,values):
uploadoptions['upload_to_MapMyFitness'] = True
except AttributeError:
pass
return uploadoptions
def getplotoptions(uploadoptions,value):
@@ -437,7 +437,7 @@ def make_plot(r,w,f1,f2,plottype,title,imagename='',plotnr=0):
if plotnr == 0:
plotnr = plotnrs[plottype]
if w.workouttype in otwtypes:
plotnr = plotnr+3
@@ -445,20 +445,20 @@ def make_plot(r,w,f1,f2,plottype,title,imagename='',plotnr=0):
job = myqueue(queuehigh,handle_makeplot,f1,f2,
title,hrpwrdata,
plotnr,imagename)
try:
width,height = Image.open(fullpathimagename).size
except:
width = 1200
height = 600
imgs = GraphImage.objects.filter(workout=w)
if len(imgs) < 7:
i = GraphImage(workout=w,
creationdatetime=timezone.now(),
filename=fullpathimagename,
width=width,height=height)
i.save()
else:
return 0,'You have reached the maximum number of static images for this workout. Delete an image first'
@@ -501,7 +501,7 @@ def make_private(w,options):
return 1
from rowers.utils import isprorower
def do_sync(w,options):
try:
if options['stravaid'] != 0:
@@ -524,20 +524,21 @@ def do_sync(w,options):
if ('upload_to_Strava' in options and not options['upload_to_Strava']):
pass
else:
try:
message,id = stravastuff.workout_strava_upload(
if options['stravaid'] != 0:
try:
message,id = stravastuff.workout_strava_upload(
w.user.user,w
)
except NoTokenError:
id = 0
message = "Please connect to Strava first"
)
except NoTokenError:
id = 0
message = "Please connect to Strava first"
if ('upload_to_SportTracks' in options and options['upload_to_SportTracks']) or (w.user.sporttracks_auto_export and isprorower(w.user)):
if ('upload_to_SportTracks' in options and not options['upload_to_SportTracks']):
pass
else:
try:
message,id = sporttracksstuff.workout_sporttracks_upload(
w.user.user,w
@@ -545,13 +546,13 @@ def do_sync(w,options):
except NoTokenError:
message = "Please connect to SportTracks first"
id = 0
if ('upload_to_RunKeeper' in options and options['upload_to_RunKeeper']) or (w.user.runkeeper_auto_export and isprorower(w.user)):
if ('upload_to_RunKeeper' in options and not options['upload_to_RunKeeper']):
pass
else:
try:
message,id = runkeeperstuff.workout_runkeeper_upload(
w.user.user,w
@@ -559,11 +560,11 @@ def do_sync(w,options):
except NoTokenError:
message = "Please connect to Runkeeper first"
id = 0
if ('upload_to_MapMyFitness' in options and options['upload_to_MapMyFitness']) or (w.user.mapmyfitness_auto_export and isprorower(w.user)):
if ('upload_to_MapMyFitness' in options and not options['upload_to_MapMyFitness']):
pass
else:
else:
try:
message,id = underarmourstuff.workout_ua_upload(
w.user.user,w
@@ -572,7 +573,7 @@ def do_sync(w,options):
message = "Please connect to MapMyFitness first"
id = 0
if ('upload_to_TrainingPeaks' in options and options['upload_to_TrainingPeaks']) or (w.user.trainingpeaks_auto_export and isprorower(w.user)):
if ('upload_to_TrainingPeaks' in options and not options['upload_to_TrainingPeaks']):
pass
+14 -8
View File
@@ -45,8 +45,8 @@ class WorkoutViewSet(viewsets.ModelViewSet):
except TypeError:
return []
permission_classes = (
#DjangoModelPermissions,
IsOwnerOrNot,
@@ -56,7 +56,7 @@ class RowerViewSet(viewsets.ModelViewSet):
model = Rower
serializer_class = RowerSerializer
#queryset = Rower.objects.all()
def get_queryset(self):
try:
r = Rower.objects.filter(user=self.request.user)
@@ -70,7 +70,7 @@ class RowerViewSet(viewsets.ModelViewSet):
http_method_names = ['get','patch']
class FavoriteChartViewSet(viewsets.ModelViewSet):
model = FavoriteChart
serializer_class = FavoriteChartSerializer
@@ -88,10 +88,10 @@ class FavoriteChartViewSet(viewsets.ModelViewSet):
)
http_method_names = ['get','put','patch','delete']
class StrokeDataViewSet(viewsets.ModelViewSet):
serializer_class = StrokeDataSerializer
# Routers provide an easy way of automatically determining the URL conf.
router = routers.DefaultRouter()
router.register(r'api/workouts',WorkoutViewSet, 'workout')
@@ -104,7 +104,6 @@ def permissiondenied_view(request):
def filenotfound_view(request):
print('aapje')
return rowers.views.error403_view(request)
def response_error_handler(request, exception=None):
@@ -344,6 +343,13 @@ urlpatterns = [
re_path(r'^workout/(?P<id>\b[0-9A-Fa-f]+\b)/split/$',views.workout_split_view,name='workout_split_view'),
# re_path(r'^workout/(?P<id>\d+)/interactiveplot/$',views.workout_biginteractive_view),
re_path(r'^workout/(?P<id>\b[0-9A-Fa-f]+\b)/view/$',views.workout_view,name='workout_view'),
re_path(r'^workout/(?P<id>\b[0-9A-Fa-f]+\b)/video/$',views.workout_video_create_view,
name='workout_video_create_view'),
re_path(r'^video/(?P<pk>\d+)/delete/$',views.VideoDelete.as_view(),name='video_delete'),
re_path(r'^video/(?P<id>\w.+)/$',views.workout_video_view,
name='workout_video_view'),
re_path(r'^videos/',views.list_videos,name='list_videos'),
re_path(r'^add-video/',views.video_selectworkout,name='video_selectworkout'),
# re_path(r'^workout/(?P<id>\d+)/$',views.workout_view,name='workout_view'),
re_path(r'^workout/(?P<id>\b[0-9A-Fa-f]+\b)/$',views.workout_view,name='workout_view'),
re_path(r'^workout/fusion/(?P<id1>\b[0-9A-Fa-f]+\b)/(?P<id2>\b[0-9A-Fa-f]+\b)/$',views.workout_fusion_view,name='workout_fusion_view'),
@@ -475,7 +481,7 @@ urlpatterns = [
re_path(r'^me/coachrequest/(?P<code>\w+.*)/accept/$',views.coach_accept_coachrequest_view,
name='coach_accept_coachrequest_view'),
re_path(r'^me/coachoffer/(?P<code>\w+.*)/accept/$',views.rower_accept_coachoffer_view,
name='rower_accept_coachoffer_view'),
name='rower_accept_coachoffer_view'),
re_path(r'^team/(?P<id>\d+)/delete/$',views.team_delete_view,name='team_delete_view'),
re_path(r'^team/create/$',views.team_create_view,name='team_create_view'),
re_path(r'^me/team/(?P<teamid>\d+)/drop/(?P<userid>\d+)/$',views.manager_member_drop_view,name='manager_member_drop_view'),
+68 -67
View File
@@ -58,7 +58,9 @@ from rowers.forms import (
RaceResultFilterForm,PowerIntervalUpdateForm,FlexAxesForm,
FlexOptionsForm,DataFrameColumnsForm,OteWorkoutTypeForm,
MetricsForm,DisqualificationForm,disqualificationreasons,
disqualifiers,SearchForm,BillingForm,PlanSelectForm
disqualifiers,SearchForm,BillingForm,PlanSelectForm,
VideoAnalysisCreateForm,WorkoutSingleSelectForm,
VideoAnalysisMetricsForm,
)
from django.urls import reverse, reverse_lazy
@@ -80,7 +82,7 @@ from rowers.forms import (
UpdateStreamForm,WorkoutMultipleCompareForm,ChartParamChoiceForm,
FusionMetricChoiceForm,BoxPlotChoiceForm,MultiFlexChoiceForm,
TrendFlexModalForm,WorkoutSplitForm,WorkoutJoinParamForm,
AnalysisOptionsForm, AnalysisChoiceForm,
AnalysisOptionsForm, AnalysisChoiceForm,
PlannedSessionMultipleCloneForm,SessionDateShiftForm,RowerTeamForm,
)
from rowers.models import (
@@ -95,7 +97,8 @@ from rowers.models import (
TrainingMesoCycleForm, TrainingMicroCycleForm,
RaceLogo,RowerBillingAddressForm,PaidPlan,
AlertEditForm, ConditionEditForm,
PlannedSessionComment,CoachRequest,CoachOffer,checkaccessplanuser
PlannedSessionComment,CoachRequest,CoachOffer,checkaccessplanuser,
VideoAnalysis
)
from rowers.models import (
RowerPowerForm,RowerForm,GraphImage,AdvancedWorkoutForm,
@@ -115,7 +118,7 @@ from rowers.models import (
FavoriteForm,BaseFavoriteFormSet,SiteAnnouncement,BasePlannedSessionFormSet,
get_course_timezone,BaseConditionFormSet,
)
from rowers.metrics import rowingmetrics,defaultfavoritecharts,nometrics
from rowers.metrics import rowingmetrics,defaultfavoritecharts,nometrics,metricsgroups
from rowers import metrics as metrics
from rowers import courses as courses
import rowers.uploads as uploads
@@ -255,7 +258,7 @@ def getfavorites(r,row):
workoutsource = row.workoutsource
if 'speedcoach2' in row.workoutsource:
workoutsource = 'speedcoach2'
favorites = FavoriteChart.objects.filter(user=r,
workouttype__in=matchworkouttypes).order_by("id")
favorites2 = FavoriteChart.objects.filter(user=r,
@@ -264,12 +267,12 @@ def getfavorites(r,row):
favorites = favorites | favorites2
maxfav = len(favorites)-1
return favorites,maxfav
def get_workout_default_page(request,id):
if request.user.is_anonymous:
return reverse('workout_view',kwargs={'id':id})
@@ -279,7 +282,7 @@ def get_workout_default_page(request,id):
return reverse('workout_edit_view',kwargs={'id':id})
else:
return reverse('workout_workflow_view',kwargs={'id':id})
def getrequestrower(request,rowerid=0,userid=0,notpermanent=False):
userid = int(userid)
@@ -288,12 +291,12 @@ def getrequestrower(request,rowerid=0,userid=0,notpermanent=False):
if notpermanent == False:
if rowerid == 0 and 'rowerid' in request.session:
rowerid = request.session['rowerid']
if userid != 0:
rowerid = 0
try:
if rowerid != 0:
r = Rower.objects.get(id=rowerid)
elif userid != 0:
@@ -301,7 +304,7 @@ def getrequestrower(request,rowerid=0,userid=0,notpermanent=False):
r = getrower(u)
else:
r = getrower(request.user)
except Rower.DoesNotExist:
raise Http404("Rower doesn't exist")
@@ -321,12 +324,12 @@ def getrequestplanrower(request,rowerid=0,userid=0,notpermanent=False):
if notpermanent == False:
if rowerid == 0 and 'rowerid' in request.session:
rowerid = request.session['rowerid']
if userid != 0:
rowerid = 0
try:
if rowerid != 0:
r = Rower.objects.get(id=rowerid)
elif userid != 0:
@@ -334,7 +337,7 @@ def getrequestplanrower(request,rowerid=0,userid=0,notpermanent=False):
r = getrower(u)
else:
r = getrower(request.user)
except Rower.DoesNotExist:
raise Http404("Rower doesn't exist")
@@ -374,20 +377,20 @@ def get_workout(id):
def get_workout_permitted(user,id):
w = get_workout(id)
if (checkworkoutuser(user,w)==False):
raise PermissionDenied("Access denied")
return w
def get_workout_permittedview(user,id):
w = get_workout(id)
if (checkworkoutuserview(user,w)==False):
raise PermissionDenied("Access denied")
return w
def getvalue(data):
perc = 0
total = 1
@@ -412,7 +415,7 @@ class SessionTaskListener(threading.Thread):
self.redis = r
self.pubsub = self.redis.pubsub()
self.pubsub.subscribe(channels)
def work(self, item):
try:
@@ -420,10 +423,10 @@ class SessionTaskListener(threading.Thread):
total,done,id,session_key = getvalue(data)
perc = int(100.*done/total)
cache.set(id,perc,3600)
except TypeError:
pass
def run(self):
for item in self.pubsub.listen():
if item['data'] == "KILL":
@@ -497,14 +500,14 @@ def remove_asynctask(request,id):
oldtasks = request.session['async_tasks']
except KeyError:
oldtasks = []
newtasks = []
for task in oldtasks:
if id not in task[0]:
newtasks += [(task[0],task[1])]
request.session['async_tasks'] = newtasks
def get_job_result(jobid):
if settings.TESTING:
return None
@@ -522,7 +525,7 @@ def get_job_result(jobid):
result = job.result
except NoSuchJobError:
return None
return result
verbose_job_status = {
@@ -551,7 +554,7 @@ def get_job_status(jobid):
job = celery_result.AsyncResult(jobid)
jobresult = job.result
if 'fail' in job.status.lower():
jobresult = '0'
summary = {
@@ -566,7 +569,7 @@ def get_job_status(jobid):
status = job.status
except AttributeError:
status = job.get_status()
summary = {
'status':status,
'result':job.result,
@@ -611,7 +614,7 @@ def kill_async_job(request,id='aap'):
cancel_job(id,connection=redis_connection)
except NoSuchJobError:
pass
remove_asynctask(request,id)
cache.delete(id)
url = reverse(session_jobs_status)
@@ -637,7 +640,7 @@ def raise_500(request):
# try:
# request.session['async_tasks'] += [(job.id,'long_test_task')]
# except KeyError:
# request.session['async_tasks'] = [(job.id,'long_test_task')]
# request.session['async_tasks'] = [(job.id,'long_test_task')]
#
# url = reverse(session_jobs_status)
#
@@ -654,7 +657,7 @@ def raise_500(request):
# try:
# request.session['async_tasks'] += [(job.id,'long_test_task2')]
# except KeyError:
# request.session['async_tasks'] = [(job.id,'long_test_task2')]
# request.session['async_tasks'] = [(job.id,'long_test_task2')]
#
# url = reverse(session_jobs_status)
#
@@ -689,7 +692,7 @@ def post_progress(request,id=None,value=0):
else: # request method is not POST
return HttpResponse('GET method not allowed',status=405)
def get_all_queued_jobs(userid=0):
r = StrictRedis()
@@ -709,13 +712,13 @@ def get_all_queued_jobs(userid=0):
'function':'',
'meta':job.info,
}))
ids = [j.id for j in queue.jobs]
ids += [j.id for j in queuehigh.jobs]
ids += [j.id for j in queuelow.jobs]
ids += [j.id for j in queuefailed.jobs]
for id in ids:
job = Job.fetch(id,connection=redis_connection)
jobs.append(
@@ -766,14 +769,14 @@ def get_stored_tasks_status(request):
taskstatus.append(this_task_status)
return taskstatus
@login_required()
def get_thumbnails(request,id):
row = get_workout_permitted(request.user,id)
r = getrower(request.user)
result = request.user.is_authenticated and ispromember(request.user)
if result:
@@ -788,15 +791,15 @@ def get_thumbnails(request,id):
favorites,maxfav = getfavorites(r,row)
charts = []
charts = thumbnails_set(r,encoder.decode_hex(id),favorites)
try:
if charts[0]['script'] == '':
if charts[0]['script'] == '':
charts = []
except IndexError:
charts = []
return JSONResponse(charts)
def get_blog_posts(request):
@@ -810,7 +813,7 @@ def get_blog_posts(request):
'title':blogpost.title,
'link':blogpost.link,
}
jsondata.append(thedict)
return JSONResponse(jsondata)
@@ -827,12 +830,12 @@ def get_blog_posts_old(request):
blogs_json = []
except ConnectionError:
pass
blogposts = []
for postdata in blogs_json[0:3]:
try:
title = postdata['title']['rendered'].encode(
'ascii','xmlcharrefreplace')
@@ -841,7 +844,7 @@ def get_blog_posts_old(request):
title = postdata['title']['rendered'].encode(
'ascii','xmlcharrefreplace').decode('utf-8')
thedict = {
'title': title,
# 'author': '',
@@ -874,7 +877,7 @@ Hoi
"""
}
return JSONResponse([object,object])
@login_required()
@@ -935,7 +938,7 @@ def get_my_teams(user):
teams1 = therower.team.all()
except AttributeError:
teams1 = []
teams2 = Team.objects.filter(manager=user)
teams = list(set(teams1).union(set(teams2)))
except TypeError:
@@ -960,10 +963,10 @@ def get_time(second):
hours = int((second-24.*3600.*days)/3600.) % 24
minutes = int((second-3600.*(hours+24.*days))/60.) % 60
sec = int(second-3600.*(hours+24.*days)-60.*minutes) % 60
microsecond = int(1.0e6*(second-3600.*(hours+24.*days)-60.*minutes-sec))
microsecond = int(1.0e6*(second-3600.*(hours+24.*days)-60.*minutes-sec))
return datetime.time(hours,minutes,sec,microsecond)
# get the workout ID from the SportTracks URI
def getidfromsturi(uri,length=8):
return uri[len(uri)-length:]
@@ -975,7 +978,7 @@ def getidfromuri(uri):
return m.group(2)
from rowers.utils import (
geo_distance,serialize_list,deserialize_list,uniqify,
str2bool,range_to_color_hex,absolute,myqueue,get_call,
@@ -997,11 +1000,11 @@ def iscoachmember(user):
except Rower.DoesNotExist:
r = Rower(user=user)
r.save()
result = user.is_authenticated and ('coach' in r.rowerplan)
else:
result = False
return result
def cancreateteam(user):
@@ -1092,7 +1095,7 @@ def sendmail(request):
messages.error(request,'You have to answer YES to the question')
return HttpResponseRedirect('/rowers/email/')
else:
return HttpResponseRedirect('/rowers/email/')
return HttpResponseRedirect('/rowers/email/')
else:
return HttpResponseRedirect('/rowers/email/')
@@ -1106,7 +1109,7 @@ def add_workout_from_strokedata(user,importid,data,strokedata,
workouttype = data['type']
except KeyError:
workouttype = 'rower'
if workouttype not in [x[0] for x in Workout.workouttypes]:
workouttype = 'other'
try:
@@ -1128,14 +1131,14 @@ def add_workout_from_strokedata(user,importid,data,strokedata,
rowdatetime = iso8601.parse_date(data['start_date'])
except ParseError:
rowdatetime = iso8601.parse_date(data['date'])
try:
c2intervaltype = data['workout_type']
except KeyError:
c2intervaltype = ''
try:
title = data['name']
except KeyError:
@@ -1177,7 +1180,7 @@ def add_workout_from_strokedata(user,importid,data,strokedata,
spm = strokedata.loc[:,'spm']
except KeyError:
spm = 0*dist2
try:
hr = strokedata.loc[:,'hr']
except KeyError:
@@ -1189,7 +1192,7 @@ def add_workout_from_strokedata(user,importid,data,strokedata,
velo = 500./pace
power = 2.8*velo**3
# save csv
# Create data frame with all necessary data to write to csv
df = pd.DataFrame({'TimeStamp (sec)':unixtime,
@@ -1211,9 +1214,9 @@ def add_workout_from_strokedata(user,importid,data,strokedata,
' ElapsedTime (sec)':seconds
})
df.sort_values(by='TimeStamp (sec)',ascending=True)
timestr = strftime("%Y%m%d-%H%M%S")
@@ -1249,11 +1252,11 @@ def add_workout_from_strokedata(user,importid,data,strokedata,
dosummary=True
)
return id,message
def keyvalue_get_default(key,options,def_options):
@@ -1288,5 +1291,3 @@ def trydf(df,aantal,column):
import rowers.teams as teams
from rowers.models import C2WorldClassAgePerformance
+553
View File
@@ -5,6 +5,267 @@ from __future__ import unicode_literals
from rowers.views.statements import *
import rowers.teams as teams
import rowers.mytypes as mytypes
import numpy
from urllib.parse import urlparse, parse_qs
def default(o):
if isinstance(o, numpy.int64): return int(o)
if isinstance(o, numpy.int32): return int(o)
raise TypeError
def get_video_id(url):
"""Returns Video_ID extracting from the given url of Youtube
Examples of URLs:
Valid:
'http://youtu.be/_lOT2p_FCvA',
'www.youtube.com/watch?v=_lOT2p_FCvA&feature=feedu',
'http://www.youtube.com/embed/_lOT2p_FCvA',
'http://www.youtube.com/v/_lOT2p_FCvA?version=3&amp;hl=en_US',
'https://www.youtube.com/watch?v=rTHlyTphWP0&index=6&list=PLjeDyYvG6-40qawYNR4juzvSOg-ezZ2a6',
'youtube.com/watch?v=_lOT2p_FCvA',
Invalid:
'youtu.be/watch?v=_lOT2p_FCvA',
"""
if url.startswith(('youtu', 'www')):
url = 'http://' + url
elif 'http' not in url:
return url
query = urlparse(url)
if 'youtube' in query.hostname:
if query.path == '/watch':
return parse_qs(query.query)['v'][0]
elif query.path.startswith(('/embed/', '/v/')):
return query.path.split('/')[2]
elif 'youtu.be' in query.hostname:
return query.path[1:]
else:
raise ValueError
# Show a video compared with data
def workout_video_view(request,id=''):
try:
id = encoder.decode_hex(id)
analysis = VideoAnalysis.objects.get(id=id)
except VideoAnalysis.DoesNotExist:
raise Http404("Video Analysis does not exist")
w = analysis.workout
delay = analysis.delay
if w.workouttype in mytypes.otwtypes:
mode = 'water'
else:
mode = 'erg'
if request.user.is_authenticated:
mayedit = checkworkoutuser(request.user,w) and isprorower(request.user.rower)
rower = request.user.rower
else:
mayedit = False
rower = None
# get video ID and offset
if mayedit and request.method == 'POST':
form = VideoAnalysisCreateForm(request.POST)
metricsform = VideoAnalysisMetricsForm(request.POST,mode=mode)
if form.is_valid() and metricsform.is_valid():
video_id = form.cleaned_data['url']
try:
video_id = get_video_id(form.cleaned_data['url'])
except (TypeError,ValueError):
pass
delay = form.cleaned_data['delay']
metricsgroups = metricsform.cleaned_data['groups']
if 'save_button' in request.POST:
analysis.name = form.cleaned_data['name']
analysis.video_id = video_id
analysis.delay = delay
analysis.metricsgroups = metricsgroups
analysis.save()
else:
video_id = id
delay = 0
elif mayedit:
form = VideoAnalysisCreateForm(
initial = {
'name':analysis.name,
'delay': analysis.delay,
'url': analysis.video_id,
}
)
metricsform = VideoAnalysisMetricsForm(initial={'groups':analysis.metricsgroups},
mode=mode)
metricsgroups = analysis.metricsgroups
video_id = analysis.video_id
else:
form = None
metricsform = None
metricsgroups = analysis.metricsgroups
data, metrics, maxtime = dataprep.get_video_data(w,groups=metricsgroups,mode=mode)
hascoordinates = pd.Series(data['latitude']).std() > 0
# create map
if hascoordinates:
mapscript, mapdiv = leaflet_chart_video(data['latitude'],data['longitude'],
w.name)
else:
mapscript, mapdiv = interactive_chart_video(data)
data['longitude'] = data['spm']
data['latitude'] = list(range(len(data['spm'])))
breadcrumbs = [
{
'url':'/rowers/list-workouts/',
'name':'Workouts'
},
{
'url':get_workout_default_page(request,encoder.encode_hex(w.id)),
'name': w.name
},
{
'url':reverse('workout_video_view',kwargs={'id':encoder.encode_hex(analysis.id)}),
'name': 'Video Analysis'
}
]
return render(request,
'embedded_video.html',
{
'workout':w,
'rower':rower,
'data': json.dumps(data,default=default),
'mapscript': mapscript,
'mapdiv': mapdiv,
'video_id': analysis.video_id,
'form':form,
'breadcrumbs':breadcrumbs,
'analysis':analysis,
'maxtime':maxtime,
'metrics':metrics,
'locked': True,
'metricsform':metricsform,
'metricsgroups': metricsgroups,
})
# Create a video compared with data
@user_passes_test(ispromember,login_url="/rowers/paidplans/",
message="This functionality requires a Pro plan or higher",
redirect_field_name=None)
def workout_video_create_view(request,id=0):
# get workout
w = get_workout_permitted(request.user,id)
if w.workouttype in mytypes.otwtypes:
mode = 'water'
else:
mode = 'erg'
# get video ID and offset
if request.method == 'POST':
form = VideoAnalysisCreateForm(request.POST)
metricsform = VideoAnalysisMetricsForm(request.POST,mode=mode)
if form.is_valid() and metricsform.is_valid():
url = form.cleaned_data['url']
delay = form.cleaned_data['delay']
metricsgroups = metricsform.cleaned_data['groups']
video_id = get_video_id(url)
if 'save_button' in request.POST:
analysis = VideoAnalysis(
workout=w,
name=form.cleaned_data['name'],
video_id = video_id,
delay=delay,
metricsgroups=metricsgroups
)
try:
analysis.save()
url = reverse('workout_video_view',
kwargs={'id':encoder.encode_hex(analysis.id)})
return HttpResponseRedirect(url)
except IntegrityError:
messages.error(request,'You cannot save two video analysis with the same YouTube video and Workout. Redirecting to your existing analysis')
analysis = VideoAnalysis.objects.filter(workout=w,video_id=video_id)
if analysis:
url = reverse('workout_video_view',
kwargs={'id':encoder.encode_hex(analysis[0].id)})
else:
url = reverse('workout_video_create_view',
kwargs={'id':encoder.encode_hex(w.id)})
return HttpResponseRedirect(url)
else:
video_id = None
delay = 0
metricsgroups = ['basic']
else:
form = VideoAnalysisCreateForm()
metricsform = VideoAnalysisMetricsForm(initial={'groups':['basic']},mode=mode)
video_id = None
delay = 0
metricsgroups = ['basic']
# get data
data, metrics, maxtime = dataprep.get_video_data(w,groups=metricsgroups,mode=mode)
hascoordinates = pd.Series(data['latitude']).std() > 0
# create map
if hascoordinates:
mapscript, mapdiv = leaflet_chart_video(data['latitude'],data['longitude'],
w.name)
else:
mapscript, mapdiv = interactive_chart_video(data)
data['longitude'] = data['spm']
data['latitude'] = list(range(len(data['spm'])))
breadcrumbs = [
{
'url':'/rowers/list-workouts/',
'name':'Workouts'
},
{
'url':get_workout_default_page(request,encoder.encode_hex(w.id)),
'name': w.name
},
{
'url':reverse('workout_video_create_view',kwargs={'id':encoder.encode_hex(w.id)}),
'name': 'Video Analysis'
}
]
analysis = {'delay':delay}
template = 'embedded_video.html'
return render(request,
template,
{
'workout':w,
'rower':request.user.rower,
'data': json.dumps(data,default=default),
'mapscript': mapscript,
'mapdiv': mapdiv,
'video_id': video_id,
'form':form,
'metricsform':metricsform,
'analysis':analysis,
'breadcrumbs':breadcrumbs,
'maxtime':maxtime,
'metrics':metrics,
'metricsgroups': metricsgroups,
'locked': False,
})
# Show the EMpower Oarlock generated Stroke Profile
@user_passes_test(ispromember,login_url="/rowers/paidplans/",
@@ -446,6 +707,205 @@ def workouts_join_view(request):
url = reverse('workouts_join_select')
return HttpResponseRedirect(url)
defaultoptions = {
'includereststrokes': False,
'workouttypes':['rower','dynamic','slides'],
'waterboattype': mytypes.waterboattype,
'rankingonly': False,
'function':'boxplot'
}
@user_passes_test(ispromember, login_url="/rowers/paidplans",
message="This functionality requires a Pro plan or higher",
redirect_field_name=None)
def video_selectworkout(request,userid=0,teamid=0):
r = getrequestrower(request, userid=userid)
user = r.user
userid = user.id
workouts = Workout.objects.filter(user=r).order_by('-date')
try:
theteam = Team.objects.get(id=teamid)
except Team.DoesNotExist:
theteam = None
if 'options' in request.session:
options = request.session['options']
else:
options=defaultoptions
options['userid'] = userid
try:
workouttypes = options['workouttypes']
except KeyError:
workouttypes = ['rower','dynamic','slides']
try:
modalities = options['modalities']
modality = modalities[0]
except KeyError:
modalities = [m[0] for m in mytypes.workouttypes_ordered.items()]
modality = 'all'
try:
rankingonly = options['rankingonly']
except KeyError:
rankingonly = False
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))
)
searchform = SearchForm(initial={'q':query})
else:
searchform = SearchForm()
if 'startdate' in request.session:
startdate = iso8601.parse_date(request.session['startdate'])
else:
startdate=timezone.now()-datetime.timedelta(days=42)
if 'enddate' in request.session:
enddate = iso8601.parse_date(request.session['enddate'])
else:
enddate = timezone.now()
workouts = workouts.filter(date__gte=startdate,date__lte=enddate)
waterboattype = mytypes.waterboattype
if request.method == 'POST':
thediv = get_call()
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
form = WorkoutSingleSelectForm(request.POST)
if form.is_valid():
cd = form.cleaned_data
selectedworkout = cd['workout']
url = reverse('workout_video_create_view',
kwargs={'id':encoder.encode_hex(selectedworkout.id)})
return HttpResponseRedirect(url)
else:
id = 0
else:
form = WorkoutSingleSelectForm(workouts=workouts)
thediv = ''
dateform = DateRangeForm(initial={
'startdate':startdate,
'enddate':enddate,
})
negtypes = []
for b in mytypes.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))
if enddate < startdate:
s = enddate
enddate = startdate
startdate = s
negtypes = []
for b in mytypes.boattypes:
if b[0] not in waterboattype:
negtypes.append(b[0])
if theteam is not None and (theteam.viewing == 'allmembers' or theteam.manager == request.user):
workouts = Workout.objects.filter(team=theteam,
startdatetime__gte=startdate,
startdatetime__lte=enddate,
workouttype__in=modalities,
)
elif theteam is not None and theteam.viewing == 'coachonly':
workouts = Workout.objects.filter(team=theteam,user=r,
startdatetime__gte=startdate,
startdatetime__lte=enddate,
workouttype__in=modalities,
)
else:
workouts = Workout.objects.filter(user=r,
startdatetime__gte=startdate,
startdatetime__lte=enddate,
workouttype__in=modalities,
)
workouts = workouts.order_by(
"-date", "-starttime"
).exclude(boattype__in=negtypes)
if rankingonly:
workouts = workouts.exclude(rankingpiece=False)
optionsform = AnalysisOptionsForm(initial={
'modality':modality,
'waterboattype':waterboattype,
'rankingonly':rankingonly,
})
startdatestring = startdate.strftime('%Y-%m-%d')
enddatestring = enddate.strftime('%Y-%m-%d')
request.session['startdate'] = startdatestring
request.session['enddate'] = enddatestring
request.session['options'] = options
breadcrumbs = [
{
'url':'/rowers/analysis',
'name':'Analysis'
},
{
'url':reverse('analysis_new',kwargs={'userid':userid}),
'name': 'Analysis Select'
},
]
return render(request, 'video_selectworkout.html',
{'workouts': workouts,
'dateform':dateform,
'startdate':startdate,
'enddate':enddate,
'rower':r,
'breadcrumbs':breadcrumbs,
'theuser':user,
'the_div':thediv,
'form':form,
'active':'nav-analysis',
'searchform':searchform,
'teams':get_my_teams(request.user),
})
@user_passes_test(ispromember,login_url="/rowers/paidplans",
message="This functionality requires a Pro plan or higher. If you are already a Pro user, please log in to access this functionality. If you are already a Pro user, please log in to access this functionality",
redirect_field_name=None)
@@ -3530,6 +3990,7 @@ def workout_edit_view(request,id=0,message="",successmessage=""):
except:
pass
videos = VideoAnalysis.objects.filter(workout=row)
# create interactive plot
f1 = row.csvfilename
@@ -3593,6 +4054,7 @@ def workout_edit_view(request,id=0,message="",successmessage=""):
'workout':row,
'teams':get_my_teams(request.user),
'graphs':g,
'videos':videos,
'breadcrumbs':breadcrumbs,
'rower':r,
'indoorraces':indoorraces,
@@ -4495,6 +4957,45 @@ def team_workout_upload_view(request,message="",
# A page with all the recent graphs (searchable on workout name)
@login_required()
def list_videos(request):
r = getrequestrower(request)
workouts = Workout.objects.filter(user=r).order_by("-date", "-starttime")
query = request.GET.get('q')
if query:
query_list = query.split()
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))
)
searchform = SearchForm(initial={'q':query})
else:
searchform = SearchForm()
g = VideoAnalysis.objects.filter(workout__in=workouts).order_by("-workout__date")
paginator = Paginator(g,8)
page = request.GET.get('page')
try:
g = paginator.page(page)
except PageNotAnInteger:
g = paginator.page(1)
except EmptyPage:
g = paginator.page(paginator.num_pages)
return render(request, 'list_videos.html',
{'analyses': g,
'searchform':searchform,
'active':'nav-analysis',
'teams':get_my_teams(request.user),
})
@login_required()
def graphs_view(request):
request.session['referer'] = reverse('graphs_view')
@@ -5254,6 +5755,58 @@ def workout_summary_edit_view(request,id,message="",successmessage=""
'formvalues':formvalues,
})
class VideoDelete(DeleteView):
login_required = True
model = VideoAnalysis
template_name = 'video_delete_confirm.html'
# extra parameters
def get_context_data(self, **kwargs):
context = super(VideoDelete, self).get_context_data(**kwargs)
breadcrumbs = [
{
'url':'/rowers/list-workouts/',
'name':'Workouts'
},
{
'url':get_workout_default_page(
self.request,
encoder.encode_hex(self.object.workout.id)),
'name': self.object.workout.name
},
{
'url':reverse('workout_video_view',kwargs={'id':encoder.encode_hex(self.object.id)}),
'name': 'Video Analysis'
},
{ 'url':reverse('video_delete',kwargs={'pk':str(self.object.pk)}),
'name': 'Delete'
}
]
context['active'] = 'nav-workouts'
context['rower'] = getrower(self.request.user)
context['breadcrumbs'] = breadcrumbs
return context
def get_success_url(self):
w = self.object.workout
try:
w = Workout.objects.get(id=w.id)
except Workout.DoesNotExist:
return reverse('workouts_view')
return reverse('workout_edit_view',kwargs={'id':encoder.encode_hex(w.id)})
def get_object(self, *args, **kwargs):
obj = super(VideoDelete, self).get_object(*args, **kwargs)
if not checkaccessuser(self.request.user,obj.workout.user):
raise PermissionDenied('You are not allowed to delete this analysis')
return obj
class GraphDelete(DeleteView):
login_required = True
+5 -1
View File
@@ -77,8 +77,12 @@ CACHES = {
STATIC_URL = '/static/'
#STATIC_ROOT = 'C:/python/rowsandallapp'
STATIC_ROOT = BASE_DIR
#STATIC_ROOT = os.path.join(BASE_DIR, 'static')
STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static'),
os.path.join(BASE_DIR, 'static/plots'),]
INTERNAL_IPS = ['127.0.0.1']
+11 -5
View File
@@ -30,9 +30,9 @@ import django.views.i18n
urlpatterns = [
re_path('^', include('django.contrib.auth.urls')),
re_path('^', include('django.contrib.auth.urls')),
re_path(r'^password_change_done/$',auth_views.PasswordChangeDoneView.as_view(),name='password_change_done'),
re_path(r'^password_change/$',auth_views.PasswordChangeView.as_view(),name='password_change'),
re_path(r'^password_change/$',auth_views.PasswordChangeView.as_view(),name='password_change'),
re_path(r'^password_reset/$',
auth_views.PasswordResetView.as_view(),
{'template_name': 'rowers/templates/registration/password_reset.html'},
@@ -96,10 +96,16 @@ django.views.i18n.javascript_catalog = None
if settings.DEBUG:
import debug_toolbar
import django
urlpatterns += [
re_path(r'^static/plots/(?P<path>.*)$',
django.views.static.serve,
kwargs={'document_root': settings.STATIC_ROOT+'plots/'})
]
urlpatterns += [
# re_path(r'^__debug__/','debug_toolbar.urls'),
re_path(r'^static/(?P<path>.*)$',
django.views.static.serve,
kwargs={'document_root': settings.STATIC_ROOT,}
re_path(r'^static/(?P<path>.*)$',
django.views.static.serve,
kwargs={'document_root': settings.STATIC_ROOT,
'show_indexes': True}
)
]
+60
View File
@@ -0,0 +1,60 @@
// var opts = {
// lines: 12,
// angle: 0.15,
// lineWidth: 0.44,
// pointer: {
// length: 0.9,
// strokeWidth: 0.035,
// color: '#000000'
// },
// limitMax: 'false',
// // percentColors: [[0.0, "#a9d70b" ], [0.50, "#a9d70b"], [1.0, "#a9d70b"]], // !!!!
// strokeColor: '#E0E0E0',
// generateGradient: true
// };
// var target = document.getElementById('angles');
// var gauge = new Gauge(target).setOptions(opts);
// gauge.maxValue = 90;
// gauge.minValue = -90;
// gauge.animationSpeed = 5;
// gauge.set(-75);
// https://github.com/bernii/gauge.js/issues/193
var opts = {
angle: 0, // The span of the gauge arc
lineWidth: 0.2, // The line thickness
radiusScale: 0.89, // Relative radius
pointer: {
length: 0.54, // // Relative to gauge radius
strokeWidth: 0.053, // The thickness
color: '#000000' // Fill color
},
limitMax: false, // If false, max value increases automatically if value > maxValue
limitMin: false, // If true, the min value of the gauge will be fixed
colorStart: '#6FADCF', // Colors
colorStop: '#8FC0DA', // just experiment with them
strokeColor: '#E0E0E0', // to see which ones work best for you
generateGradient: true,
highDpiSupport: true, // High resolution support
staticZones: [
{strokeStyle: "#00FF00", min: 0, max: 2}, // Red from 100 to 60
{strokeStyle: "#0000FF", min: 2, max: 3}, // Yellow
{strokeStyle: "#00FFFF", min: 3, max: 4}, // Green
{strokeStyle: "#FFDD00", min: 4, max: 5}, // Yellow
{strokeStyle: "#FF0000", min: 5, max: 6} // Red
],
};
var target = document.getElementById('basic'); // your canvas element
var gaugeboatspeed = new Gauge(target).setOptions(opts); // create sexy gauge!
gaugeboatspeed.maxValue = 6; // set max gauge value
gaugeboatspeed.setMinValue(0); // Prefer setter over gauge.minValue = 0
gaugeboatspeed.animationSpeed = 10; // set animation speed (32 is default value)
gaugeboatspeed.set(0); // set actual value
// Define set_basic(values) so that gauges can be set by metricsgroups
function set_basic() {
gaugeboatspeed.set(boatspeed_now);
}