Private
Public Access
1
0

force curve comparison

This commit is contained in:
Sander Roosendaal
2022-10-26 23:49:28 +02:00
parent cb8aeb1cf6
commit 7ebdf21c25
10 changed files with 590 additions and 27 deletions

View File

@@ -10,7 +10,7 @@ from django.contrib.admin.widgets import FilteredSelectMultiple
from rowers.models import ( from rowers.models import (
Workout, Rower, Team, PlannedSession, GeoCourse, Workout, Rower, Team, PlannedSession, GeoCourse,
VirtualRace, VirtualRaceResult, IndoorVirtualRaceResult, VirtualRace, VirtualRaceResult, IndoorVirtualRaceResult,
PaidPlan, InStrokeAnalysis PaidPlan, InStrokeAnalysis, ForceCurveAnalysis
) )
from rowers.rows import validate_file_extension, must_be_csv, validate_image_extension, validate_kml from rowers.rows import validate_file_extension, must_be_csv, validate_image_extension, validate_kml
from django.contrib.auth.forms import UserCreationForm from django.contrib.auth.forms import UserCreationForm
@@ -1256,6 +1256,12 @@ class InStrokeMultipleCompareForm(forms.Form):
widget=forms.CheckboxSelectMultiple() widget=forms.CheckboxSelectMultiple()
) )
class ForceCurveMultipleCompareForm(forms.Form):
analyses = forms.ModelMultipleChoiceField(
queryset=ForceCurveAnalysis.objects.all(),
widget=forms.CheckboxSelectMultiple()
)
class WorkoutMultipleCompareForm(forms.Form): class WorkoutMultipleCompareForm(forms.Form):
workouts = forms.ModelMultipleChoiceField( workouts = forms.ModelMultipleChoiceField(
@@ -1829,6 +1835,16 @@ class FlexOptionsForm(forms.Form):
class ForceCurveOptionsForm(forms.Form): class ForceCurveOptionsForm(forms.Form):
includereststrokes = forms.BooleanField(initial=False, required=False, includereststrokes = forms.BooleanField(initial=False, required=False,
label='Include Rest Strokes') label='Include Rest Strokes')
spm_min = forms.FloatField(initial=15.0,label='SPM Min',widget=HiddenInput,required=False)
spm_max = forms.FloatField(initial=55.0,label='SPM Max',widget=HiddenInput,required=False)
dist_min = forms.IntegerField(initial=0,label='Dist Min',widget=HiddenInput,required=False)
dist_max = forms.IntegerField(initial=0,label='Dist Max',widget=HiddenInput,required=False)
work_min = forms.IntegerField(initial=0,label='Work Min',widget=HiddenInput,required=False)
work_max = forms.IntegerField(initial=1500,label='Work Max',widget=HiddenInput,required=False)
notes = forms.CharField(initial="", label='notes', widget=HiddenInput, required=False)
plotchoices = ( plotchoices = (
('line', 'Force Curve Collection Plot'), ('line', 'Force Curve Collection Plot'),
('scatter', 'Peak Force Scatter Plot'), ('scatter', 'Peak Force Scatter Plot'),
@@ -1837,6 +1853,8 @@ class ForceCurveOptionsForm(forms.Form):
plottype = forms.ChoiceField(choices=plotchoices, initial='line', plottype = forms.ChoiceField(choices=plotchoices, initial='line',
label='Individual Stroke Chart Type') label='Individual Stroke Chart Type')
name = forms.CharField(initial="", label='Name',required=False)
axchoices = list( axchoices = list(
(ax[0], ax[1]) for ax in axes if ax[0] not in ['cumdist', 'None'] (ax[0], ax[1]) for ax in axes if ax[0] not in ['cumdist', 'None']

View File

@@ -5,7 +5,7 @@ from rowers.metrics import rowingmetrics, metricsdicts
from scipy.spatial import ConvexHull, Delaunay from scipy.spatial import ConvexHull, Delaunay
from scipy.stats import linregress, percentileofscore from scipy.stats import linregress, percentileofscore
from pytz import timezone as tz, utc from pytz import timezone as tz, utc
from rowers.models import course_spline, VirtualRaceResult, InStrokeAnalysis from rowers.models import course_spline, VirtualRaceResult, InStrokeAnalysis, ForceCurveAnalysis
from bokeh.palettes import Category20c, Category10 from bokeh.palettes import Category20c, Category10
from bokeh.layouts import layout, widgetbox from bokeh.layouts import layout, widgetbox
from bokeh.resources import CDN, INLINE from bokeh.resources import CDN, INLINE
@@ -813,7 +813,11 @@ def interactive_activitychart2(workouts, startdate, enddate, stack='type', toolb
return script, div return script, div
def interactive_forcecurve(theworkouts, workstrokesonly=True, plottype='scatter'): def interactive_forcecurve(theworkouts, workstrokesonly=True, plottype='scatter',
spm_min=15, spm_max=45,
notes='',
dist_min=0,dist_max=0,
work_min=0,work_max=1500):
TOOLS = 'save,pan,box_zoom,wheel_zoom,reset,tap,hover,crosshair' TOOLS = 'save,pan,box_zoom,wheel_zoom,reset,tap,hover,crosshair'
ids = [int(w.id) for w in theworkouts] ids = [int(w.id) for w in theworkouts]
@@ -1413,9 +1417,18 @@ def interactive_forcecurve(theworkouts, workstrokesonly=True, plottype='scatter'
lengthlabel.text = 'Length: '+length.toFixed(2) lengthlabel.text = 'Length: '+length.toFixed(2)
efflengthlabel.text = 'Effective Length: '+efflength.toFixed(2) efflengthlabel.text = 'Effective Length: '+efflength.toFixed(2)
console.log(count); // console.log(count);
console.log(multilines['x'].length); // console.log(multilines['x'].length);
console.log(multilines['y'].length); // console.log(multilines['y'].length);
// change DOM elements
document.getElementById("id_spm_min").value = minspm;
document.getElementById("id_spm_max").value = maxspm;
document.getElementById("id_dist_min").value = mindist;
document.getElementById("id_dist_max").value = maxdist;
document.getElementById("id_notes").value = annotation;
document.getElementById("id_work_min").value = minwork;
document.getElementById("id_work_max").value = maxwork;
// source.trigger('change'); // source.trigger('change');
source.change.emit(); source.change.emit();
@@ -1425,40 +1438,43 @@ def interactive_forcecurve(theworkouts, workstrokesonly=True, plottype='scatter'
""") """)
annotation = TextInput( annotation = TextInput(
width=140, title="Type your plot notes here", value="") width=140, title="Type your plot notes here", value="", name="annotation")
annotation.js_on_change('value', callback) annotation.js_on_change('value', callback)
callback.args["annotation"] = annotation callback.args["annotation"] = annotation
slider_spm_min = Slider(width=140, start=15.0, end=55, value=15.0, step=.1, slider_spm_min = Slider(width=140, start=15.0, end=55, value=15, step=.1,
title="Min SPM") title="Min SPM", name="min_spm_slider")
slider_spm_min.js_on_change('value', callback) slider_spm_min.js_on_change('value', callback)
callback.args["minspm"] = slider_spm_min callback.args["minspm"] = slider_spm_min
slider_spm_max = Slider(width=140, start=15.0, end=55, value=55.0, step=.1, slider_spm_max = Slider(width=140, start=15.0, end=55, value=55, step=.1,
title="Max SPM") title="Max SPM", name="max_spm_slider")
slider_spm_max.js_on_change('value', callback) slider_spm_max.js_on_change('value', callback)
callback.args["maxspm"] = slider_spm_max callback.args["maxspm"] = slider_spm_max
slider_work_min = Slider(width=140, start=0, end=1500, value=0, step=10, slider_work_min = Slider(width=140, start=0, end=1500, value=0, step=10,
title="Min Work per Stroke") title="Min Work per Stroke", name="min_work_slider")
slider_work_min.js_on_change('value', callback) slider_work_min.js_on_change('value', callback)
callback.args["minwork"] = slider_work_min callback.args["minwork"] = slider_work_min
slider_work_max = Slider(width=140, start=0, end=1500, value=1500, step=10, slider_work_max = Slider(width=140, start=0, end=1500, value=1500, step=10,
title="Max Work per Stroke") title="Max Work per Stroke", name="max_work_slider")
slider_work_max.js_on_change('value', callback) slider_work_max.js_on_change('value', callback)
callback.args["maxwork"] = slider_work_max callback.args["maxwork"] = slider_work_max
distmax = 100+100*int(rowdata['distance'].max()/100.) distmax = 100+100*int(rowdata['distance'].max()/100.)
slider_dist_min = Slider(width=140, start=0, end=distmax, value=0, step=50, slider_dist_min = Slider(width=140, start=0, end=distmax, value=0, step=50,
title="Min Distance") title="Min Distance", name="min_dist_slider")
slider_dist_min.js_on_change('value', callback) slider_dist_min.js_on_change('value', callback)
callback.args["mindist"] = slider_dist_min callback.args["mindist"] = slider_dist_min
if dist_max == 0:
dist_max = distmax
slider_dist_max = Slider(width=140, start=0, end=distmax, value=distmax, slider_dist_max = Slider(width=140, start=0, end=distmax, value=distmax,
step=50, step=50,
title="Max Distance") title="Max Distance", name="max_dist_slider")
slider_dist_max.js_on_change('value', callback) slider_dist_max.js_on_change('value', callback)
callback.args["maxdist"] = slider_dist_max callback.args["maxdist"] = slider_dist_max
@@ -4078,6 +4094,120 @@ def interactive_streamchart(id=0, promember=0):
return [script, div] return [script, div]
def forcecurve_multi_interactive_chart(selected):
df_plot = pd.DataFrame()
ids = [analysis.id for analysis in selected]
columns = ['catch', 'slip', 'wash', 'finish', 'averageforce',
'peakforceangle', 'peakforce', 'spm', 'distance',
'workoutstate', 'driveenergy']
for analysis in selected:
workstrokesonly = not analysis.include_rest_strokes
spm_min = analysis.spm_min
spm_max = analysis.spm_max
dist_min = analysis.dist_min
dist_max = analysis.dist_max
work_min = analysis.work_min
work_max = analysis.work_max
rowdata = dataprep.getsmallrowdata_db(columns, ids=[analysis.workout.id],
workstrokesonly=workstrokesonly)
rowdata = rowdata[rowdata['spm']>spm_min]
rowdata = rowdata[rowdata['spm']<spm_max]
rowdata = rowdata[rowdata['driveenergy']>work_min]
rowdata = rowdata[rowdata['driveenergy']<work_max]
rowdata = rowdata[rowdata['distance']<dist_max]
rowdata = rowdata[rowdata['distance']>dist_min]
catchav = rowdata['catch'].median()
finishav = rowdata['finish'].median()
washav = (rowdata['finish']-rowdata['wash']).median()
slipav = (rowdata['slip']+rowdata['catch']).median()
peakforceav = rowdata['peakforce'].median()
peakforceangleav = rowdata['peakforceangle'].median()
thresholdforce = 100 if 'x' in analysis.workout.boattype else 200
x = [catchav,
slipav,
peakforceangleav,
washav,
finishav]
y = [0, thresholdforce,
peakforceav,
thresholdforce, 0]
xname = 'x_'+str(analysis.id)
yname = 'y_'+str(analysis.id)
df_plot[xname] = x
df_plot[yname] = y
source = ColumnDataSource(
df_plot
)
TOOLS = 'save,pan,box_zoom,wheel_zoom,reset,tap,crosshair'
plot = Figure(plot_width=920,tools=TOOLS,
toolbar_location='above',
toolbar_sticky=False)
plot.sizing_mode = 'stretch_both'
# add watermark
watermarkurl = "/static/img/logo7.png"
watermarkrange = Range1d(start=0, end=1)
watermarkalpha = 0.6
watermarkx = 0.99
watermarky = 0.01
watermarkw = 184
watermarkh = 35
watermarkanchor = 'bottom_right'
plot.extra_y_ranges = {"watermark": watermarkrange}
plot.extra_x_ranges = {"watermark": watermarkrange}
plot.image_url([watermarkurl], watermarkx, watermarky,
watermarkw, watermarkh,
global_alpha=watermarkalpha,
w_units='screen',
h_units='screen',
anchor=watermarkanchor,
dilate=True,
x_range_name="watermark",
y_range_name="watermark",
)
colors = itertools.cycle(palette)
try:
items = itertools.izip(ids, colors)
except AttributeError:
items = zip(ids, colors)
for id, color in items:
xname = 'x_'+str(id)
yname = 'y_'+str(id)
analysis = ForceCurveAnalysis.objects.get(id=id)
legendlabel = '{name}'.format(
name = analysis.name,
)
if analysis.notes:
legendlabel = '{name} - {notes}'.format(
name = analysis.name,
notes = analysis.notes
)
plot.line(xname,yname,source=source,legend_label=legendlabel,
line_width=2, color=color)
plot.legend.location = "top_left"
plot.xaxis.axis_label = "Angle"
plot.yaxis.axis_label = "Force (N)"
script, div = components(plot)
return (script, div)
def instroke_multi_interactive_chart(selected): def instroke_multi_interactive_chart(selected):
df_plot = pd.DataFrame() df_plot = pd.DataFrame()
ids = [analysis.id for analysis in selected] ids = [analysis.id for analysis in selected]

View File

@@ -4992,3 +4992,25 @@ class InStrokeAnalysis(models.Model):
date = self.date) date = self.date)
return s return s
class ForceCurveAnalysis(models.Model):
workout = models.ForeignKey(Workout, on_delete=models.CASCADE)
rower = models.ForeignKey(Rower, on_delete=models.SET_NULL, null=True)
name = models.CharField(max_length=150, blank=True, null=True)
date = models.DateField(blank=True, null=True)
notes = models.TextField(blank=True)
dist_min = models.IntegerField(default=0)
dist_max = models.IntegerField(default=3600)
spm_min = models.FloatField(default=15)
spm_max = models.FloatField(default=55)
work_min = models.IntegerField(default=0)
work_max = models.IntegerField(default=1500)
average_spm = models.FloatField(default=23)
average_boatspeed = models.FloatField(default=4.0)
include_rest_strokes = models.BooleanField(default=False)
def __str__(self):
s = 'Force Curve Analysis {name} ({date})'.format(name = self.name,
date = self.date)
return s

View File

@@ -0,0 +1,104 @@
{% extends "newbase.html" %}
{% load static %}
{% load rowerfilters %}
{% block title %}Rowsandall - Analysis {% endblock %}
{% block main %}
{{ js_res | safe }}
{{ css_res| safe }}
<script src="https://cdn.pydata.org/bokeh/release/bokeh-2.2.3.min.js"></script>
<script src="https://cdn.pydata.org/bokeh/release/bokeh-widgets-2.2.3.min.js"></script>
<script async="true" type="text/javascript">
Bokeh.set_log_level("info");
</script>
{{ the_script |safe }}
<h1>Force Curve Analysis for {{ rower.user.first_name }} {{ rower.user.last_name }}</h1>
<form enctype="multipart/form-data" method="post">
<ul class="main-content">
{% if the_div %}
<li class="grid_4">
<div id="theplot" class="flexplot">
{{ the_div|safe }}
</div>
</li>
{% endif %}
{% if analyses %}
{% for analysis in analyses %}
<li class="grid_4 divlines" id="analysis_{{ analysis.id }}">
{{ analysis.date }}
<div><h3>{{ analysis.name }}</h3></div>
<div class="analysiscontainer">
<div class="workoutelement">
{% if analysis in selected %}
<input type="checkbox" name="analyses" value="{{ analysis.id }}" id="analyses_{{ analysis.id }}" checked>
{% else %}
<input type="checkbox" name="analyses" value="{{ analysis.id }}" id="analyses_{{ analysis.id }}">
{% endif %}
</div>
<div class="workoutelement">
<a class="small" href="/rowers/workout/{{ analysis.workout.id|encode }}/forcecurve/{{ analysis.id }}/"
title="Edit">
<i class="fas fa-pencil-alt fa-fw"></i>
</a>
</div>
<div class="workoutelement">
<a class="small" href="/rowers/analysis/forcecurveanalysis/{{ analysis.id }}/delete/"
title="Delete">
<i class="fas fa-trash-alt fa-fw"></i>
</a>
</div>
<div class="workoutelement">
<span style="color:#555">Workout</span><br>
<span>{{ analysis.workout.name }}</span><br>
<span>{{ analysis.workout.date }}, {{ analysis.workout.distance }}m</span>
</div>
<div class="workoutelement">
<span style="color:#555">Notes</span><br>
{{ analysis.notes }}
</div>
<div class="workoutelement">
<span style="color:#555">SPM</span><br>
{{ analysis.spm_min }} - {{ analysis.spm_max }}
</div>
<div class="workoutelement">
<span style="color:#555">Distance</span><br>
{{ analysis.dist_min }} - {{ analysis.dist_max }}
</div>
<div class="workoutelement">
<span style="color:#555">Work</span><br>
{{ analysis.work_min }} - {{ analysis.work_max }}
</div>
<div class="workoutelement">
<span style="color:#555">Avg Pace</span><br>
{{ analysis.average_boatspeed|velotopace }}
</div>
</div>
</li>
{% endfor %}
{% else %}
<li class="grid_4">
<p>You have not saved any analyses for {{ rower.user.first_name }}</p>
</li>
{% endif %}
</ul>
{% csrf_token %}
<input name='instroke_compare' type="submit" value="Compare Selected">
</form>
{% endblock %}
{% block scripts %}
{% endblock %}
{% block sidebar %}
{% include 'menu_analytics.html' %}
{% endblock %}

View File

@@ -34,14 +34,47 @@
<table> <table>
{{ form.as_table }} {{ form.as_table }}
</table> </table>
<p> <div class="buttoncontainer">
<input name="chartform" type="submit" <input name="chartform" type="submit" class="button"
value="Update Chart"> value="Update Chart">
</p> <input name='_save' class="button" type="submit" value="Save">
<input name='_save_as_new' class="button" type="submit" value="Save as New">
<p>
With the Save buttons, you can save your analysis for future use and to compare
multiple analyses to each other. You can find the saved analyses under the Analysis
tab (<a href="/rowers/analysis/forcecurveanalysis/">force curve analysis</a>).
</p>
</div>
</form> </form>
</li> </li>
</ul> </ul>
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
<script>
$(document).ready(function() {
var slider_min_spm = Bokeh.documents[0].get_model_by_name('min_spm_slider')
slider_min_spm.value = {{ spm_min }}
console.log('min spm value after:', slider_min_spm.value)
var slider_max_spm = Bokeh.documents[0].get_model_by_name('max_spm_slider')
slider_max_spm.value = {{ spm_max }}
console.log('max spm value after:', slider_max_spm.value)
var slider_min_work = Bokeh.documents[0].get_model_by_name('min_work_slider')
slider_min_work.value = {{ work_min }}
console.log('min work value after:', slider_min_work.value)
var slider_max_work = Bokeh.documents[0].get_model_by_name('max_work_slider')
slider_max_work.value = {{ work_max }}
console.log('max work value after:', slider_max_work.value)
var slider_min_dist = Bokeh.documents[0].get_model_by_name('min_dist_slider')
slider_min_dist.value = {{ dist_min }}
console.log('min dist value after:', slider_min_dist.value)
var slider_max_dist = Bokeh.documents[0].get_model_by_name('max_dist_slider')
slider_max_dist.value = {{ dist_max }}
console.log('max dist value after:', slider_max_dist.value)
var annotation = Bokeh.documents[0].get_model_by_name('annotation')
annotation.value = "{{ annotation }}"
console.log('Annotation set to ', annotation.value)
});
</script>
{% endblock %} {% endblock %}
{% endlocaltime %} {% endlocaltime %}

View File

@@ -0,0 +1,29 @@
{% extends "newbase.html" %}
{% load static %}
{% block title %}Force Curve Analysis{% endblock %}
{% block main %}
<h1>Confirm Delete</h1>
<p>This will permanently delete the analysis</p>
<ul class="main-content">
<li class="grid_2">
<p>
<form action="" method="post">
{% csrf_token %}
<p>Are you sure you want to delete <em>{{ object }}</em>?</p>
<input class="button" type="submit" value="Confirm">
</form>
</p>
</li>
</ul>
{% endblock %}
{% block sidebar %}
{% include 'menu_analytics.html' %}
{% endblock %}

View File

@@ -452,6 +452,10 @@ urlpatterns = [
name='workout_histo_view'), name='workout_histo_view'),
re_path(r'^workout/(?P<id>\b[0-9A-Fa-f]+\b)/forcecurve/$', views.workout_forcecurve_view, re_path(r'^workout/(?P<id>\b[0-9A-Fa-f]+\b)/forcecurve/$', views.workout_forcecurve_view,
name='workout_forcecurve_view'), name='workout_forcecurve_view'),
re_path(r'^workout/(?P<id>\b[0-9A-Fa-f]+\b)/forcecurve/(?P<analysis>\d+)/$', views.workout_forcecurve_view,
name='workout_forcecurve_view'),
re_path(r'^workout/(?P<id>\b[0-9A-Fa-f]+\b)/forcecurve/(?P<analysis>\d+)/user/(?P<userid>\d+)/$', views.workout_forcecurve_view,
name='workout_forcecurve_view'),
re_path(r'^workout/(?P<id>\b[0-9A-Fa-f]+\b)/unsubscribe/$', views.workout_unsubscribe_view, re_path(r'^workout/(?P<id>\b[0-9A-Fa-f]+\b)/unsubscribe/$', views.workout_unsubscribe_view,
name='workout_unsubscribe_view'), name='workout_unsubscribe_view'),
re_path(r'^workout/(?P<id>\b[0-9A-Fa-f]+\b)/comment/$', views.workout_comment_view, re_path(r'^workout/(?P<id>\b[0-9A-Fa-f]+\b)/comment/$', views.workout_comment_view,
@@ -834,8 +838,12 @@ urlpatterns = [
re_path(r'^analysis/$', views.analysis_view, name='analysis'), re_path(r'^analysis/$', views.analysis_view, name='analysis'),
re_path(r'^analysis/instrokeanalysis/$', views.instrokeanalysis_view, re_path(r'^analysis/instrokeanalysis/$', views.instrokeanalysis_view,
name='instrokeanalysis_view'), name='instrokeanalysis_view'),
re_path(r'^analysis/forcecurveanalysis/$', views.forcecurveanalysis_view,
name='forcecurveanalysis_view'),
re_path(r'^analysis/instrokeanalysis/(?P<pk>\d+)/delete/$', re_path(r'^analysis/instrokeanalysis/(?P<pk>\d+)/delete/$',
views.InStrokeAnalysisDelete.as_view(), name='instroke_analysis_delete_view'), views.InStrokeAnalysisDelete.as_view(), name='instroke_analysis_delete_view'),
re_path(r'^analysis/forcecurveanalysis/(?P<pk>\d+)/delete/$',
views.ForceCurveAnalysisDelete.as_view(), name='forcecurve_analysis_delete_view'),
re_path(r'^promembership', TemplateView.as_view( re_path(r'^promembership', TemplateView.as_view(
template_name='promembership.html'), name='promembership'), template_name='promembership.html'), name='promembership'),
re_path(r'^checkout/(?P<planid>\d+)/$', re_path(r'^checkout/(?P<planid>\d+)/$',

View File

@@ -1844,6 +1844,108 @@ def agegrouprecordview(request, sex='male', weightcategory='hwt',
'the_div': div, 'the_div': div,
}) })
@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",
redirect_field_name=None)
@permission_required('rower.is_coach', fn=get_user_by_userid, raise_exception=True)
def forcecurveanalysis_view(request, userid=0):
r = getrequestrower(request, userid=userid)
analyses = ForceCurveAnalysis.objects.filter(rower=r).order_by("-date","-id")
selected = []
div = ""
script = ""
if request.method == 'POST':
form = ForceCurveMultipleCompareForm(request.POST)
if form.is_valid():
cd = form.cleaned_data
selected = cd['analyses']
request.session['analyses'] = [a.id for a in selected]
# now should redirect to analysis
script, div = forcecurve_multi_interactive_chart(selected)
breadcrumbs = [
{
'url': '/rowers/analysis',
'name': 'Analysis'
},
{
'url': reverse('instrokeanalysis_view'),
'name': 'In-Stroke Analysis',
},
]
return render(request, 'forcecurve_analysis.html',
{
'breadcrumbs': breadcrumbs,
'analyses': analyses,
'rower': r,
'the_script': script,
'the_div': div,
'selected': selected,
})
#instroke analysis delete view
class ForceCurveAnalysisDelete(DeleteView):
login_required = True
model = ForceCurveAnalysis
template_name = 'forcecurveanalysis_delete_confirm.html'
# extra parameters
def get_context_data(self, **kwargs):
context = super(ForceCurveAnalysisDelete, self).get_context_data(**kwargs)
if 'userid' in kwargs: # pragma: no cover
userid = kwargs['userid']
else:
userid = 0
context['rower'] = getrequestrower(self.request, userid=userid)
context['alert'] = self.object
breadcrumbs = [
{
'url': '/rowers/analysis',
'name': 'Analysis'
},
{
'url': reverse('forcecurveanalysis_view'),
'name': 'Force Curve Analysis',
},
{
'url': reverse('workout_forcecurve_view',
kwargs={'userid': userid,
'id': encoder.encode_hex(self.object.workout.id),
'analysis': self.object.pk}),
'name': self.object.name,
},
{
'url': reverse('forcecurve_analysis_delete_view', kwargs={'pk': self.object.pk}),
'name': 'Delete'
}
]
context['breadcrumbs'] = breadcrumbs
return context
def get_success_url(self):
return reverse('forcecurveanalysis_view')
def get_object(self, *args, **kwargs):
obj = super(ForceCurveAnalysisDelete, self).get_object(*args, **kwargs)
if obj.rower != self.request.user.rower:
raise PermissionDenied("You are not allowed to delete this Analysis")
return obj
@user_passes_test(ispromember, login_url="/rowers/paidplans", @user_passes_test(ispromember, login_url="/rowers/paidplans",
message="This functionality requires a Pro plan or higher." message="This functionality requires a Pro plan or higher."
@@ -1856,8 +1958,9 @@ def instrokeanalysis_view(request, userid=0):
analyses = InStrokeAnalysis.objects.filter(rower=r).order_by("-date","-id") analyses = InStrokeAnalysis.objects.filter(rower=r).order_by("-date","-id")
selected = [] selected = []
script = ""
div = "" div = ""
script = ""
if request.method == 'POST': if request.method == 'POST':
form = InStrokeMultipleCompareForm(request.POST) form = InStrokeMultipleCompareForm(request.POST)

View File

@@ -111,7 +111,8 @@ from rowers.forms import (
VideoAnalysisMetricsForm, SurveyForm, HistorySelectForm, VideoAnalysisMetricsForm, SurveyForm, HistorySelectForm,
StravaChartForm, FitnessFitForm, PerformanceManagerForm, StravaChartForm, FitnessFitForm, PerformanceManagerForm,
TrainingPlanBillingForm, InstantPlanSelectForm, TrainingPlanBillingForm, InstantPlanSelectForm,
TrainingZonesForm, InstrokeForm, InStrokeMultipleCompareForm TrainingZonesForm, InstrokeForm, InStrokeMultipleCompareForm,
ForceCurveMultipleCompareForm
) )
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
@@ -153,7 +154,7 @@ from rowers.models import (
VideoAnalysis, ShareKey, VideoAnalysis, ShareKey,
StandardCollection, CourseStandard, StandardCollection, CourseStandard,
VirtualRaceFollower, TombStone, InstantPlan, VirtualRaceFollower, TombStone, InstantPlan,
PlannedSessionStep,InStrokeAnalysis, PlannedSessionStep,InStrokeAnalysis, ForceCurveAnalysis
) )
from rowers.models import ( from rowers.models import (
RowerPowerForm, RowerHRZonesForm, RowerForm, RowerCPForm, GraphImage, AdvancedWorkoutForm, RowerPowerForm, RowerHRZonesForm, RowerForm, RowerCPForm, GraphImage, AdvancedWorkoutForm,

View File

@@ -408,33 +408,138 @@ def workout_video_create_view(request, id=0):
" 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) redirect_field_name=None)
@permission_required('workout.change_workout', fn=get_workout_by_opaqueid, raise_exception=True) @permission_required('workout.change_workout', fn=get_workout_by_opaqueid, raise_exception=True)
def workout_forcecurve_view(request, id=0, workstrokesonly=False): def workout_forcecurve_view(request, id=0, analysis=0, userid=0, workstrokesonly=False):
row = get_workoutuser(id, request) row = get_workoutuser(id, request)
mayedit = 0 mayedit = 0
r = getrequestrower(request) r = getrequestrower(request, userid=userid)
if r == row.user: if r == row.user:
mayedit = 1 mayedit = 1
if analysis:
try:
forceanalysis = ForceCurveAnalysis.objects.get(id=analysis)
dist_min = forceanalysis.dist_min
dist_max = forceanalysis.dist_max
spm_min = forceanalysis.spm_min
spm_max = forceanalysis.spm_max
work_min = forceanalysis.work_min
work_max = forceanalysis.work_max
notes = forceanalysis.notes
name = forceanalysis.name
includereststrokes = forceanalysis.include_rest_strokes
except (ForceCurveAnalysis.DoesNotExist, ValueError):
pass
else:
dist_min = 0
dist_max = 0
spm_min = 15
spm_max = 55
work_min = 0
work_max = 1500
notes = ''
includereststrokes = False
name = ''
form = ForceCurveOptionsForm(initial={
'spm_min': spm_min,
'spm_max': spm_max,
'dist_min': dist_min,
'dist_max': dist_max,
'work_min': work_min,
'work_max': work_max,
'notes': notes,
'plottype': 'line',
'name': name,
})
plottype = 'line'
if request.method == 'POST': if request.method == 'POST':
form = ForceCurveOptionsForm(request.POST) form = ForceCurveOptionsForm(request.POST)
if form.is_valid(): if form.is_valid():
spm_min = form.cleaned_data['spm_min']
spm_max = form.cleaned_data['spm_max']
dist_min = form.cleaned_data['dist_min']
dist_max = form.cleaned_data['dist_max']
work_min = form.cleaned_data['work_min']
work_max = form.cleaned_data['work_max']
notes = form.cleaned_data['notes']
name = form.cleaned_data['name']
if not name:
name = row.name
includereststrokes = form.cleaned_data['includereststrokes'] includereststrokes = form.cleaned_data['includereststrokes']
plottype = form.cleaned_data['plottype'] plottype = form.cleaned_data['plottype']
workstrokesonly = not includereststrokes workstrokesonly = not includereststrokes
if "_save" in request.POST and "new" not in request.POST:
if not analysis:
forceanalysis = ForceCurveAnalysis(
workout = row,
name = name,
date = timezone.now().date(),
notes = notes,
dist_min = dist_min,
dist_max = dist_max,
work_min = work_min,
work_max = work_max,
spm_min = spm_min,
spm_max = spm_max,
rower=row.user,
include_rest_strokes = includereststrokes,
)
else:
forceanalysis.workout = row
forceanalysis.name = name
forceanalysis.date = timezone.now().date()
forceanalysis.notes = notes
forceanalysis.dist_min = dist_min
forceanalysis.dist_max = dist_max
forceanalysis.work_min = work_min
forceanalysis.work_max = work_max
forceanalysis.spm_min = spm_min
forceanalysis.spm_max = spm_max
forceanalysis.include_rest_strokes = includereststrokes
forceanalysis.save()
dosave = True
messages.info(request,'Force Curve analysis saved')
if "_save_as_new" in request.POST:
forceanalysis = ForceCurveAnalysis(
workout = row,
name = name,
date = timezone.now().date(),
notes = notes,
dist_min = dist_min,
dist_max = dist_max,
spm_min = spm_min,
spm_max = spm_max,
work_min = work_min,
work_max = work_max,
rower=row.user,
include_rest_strokes = includereststrokes,
)
forceanalysis.save()
dosave = True
messages.info(request,'Force Curve analysis saved')
else: # pragma: no cover else: # pragma: no cover
workstrokesonly = True workstrokesonly = True
plottype = 'line' plottype = 'line'
else:
form = ForceCurveOptionsForm()
plottype = 'line'
script, div, js_resources, css_resources = interactive_forcecurve( script, div, js_resources, css_resources = interactive_forcecurve(
[row], [row],
workstrokesonly=workstrokesonly, workstrokesonly=workstrokesonly,
plottype=plottype, plottype=plottype,
dist_min = dist_min,
dist_max = dist_max,
spm_min = spm_min,
spm_max = spm_max,
work_min = work_min,
work_max = work_max,
notes=notes,
) )
breadcrumbs = [ breadcrumbs = [
@@ -455,6 +560,9 @@ def workout_forcecurve_view(request, id=0, workstrokesonly=False):
r = getrower(request.user) r = getrower(request.user)
if dist_max == 0:
dist_max = row.distance+100
return render(request, return render(request,
'forcecurve_single.html', 'forcecurve_single.html',
{ {
@@ -464,6 +572,13 @@ def workout_forcecurve_view(request, id=0, workstrokesonly=False):
'workout': row, 'workout': row,
'breadcrumbs': breadcrumbs, 'breadcrumbs': breadcrumbs,
'active': 'nav-workouts', 'active': 'nav-workouts',
'spm_min': spm_min,
'spm_max': spm_max,
'dist_min': dist_min,
'dist_max': dist_max,
'work_min': work_min,
'work_max': work_max,
'annotation': notes,
'the_div': div, 'the_div': div,
'js_res': js_resources, 'js_res': js_resources,
'css_res': css_resources, 'css_res': css_resources,
@@ -3052,7 +3167,7 @@ def instroke_chart_interactive(request, id=0, analysis=0, userid=0):
notes = form.cleaned_data['notes'] notes = form.cleaned_data['notes']
name = form.cleaned_data['name'] name = form.cleaned_data['name']
if "_save" in request.POST: if "_save" in request.POST and "new" not in request.POST:
if not analysis: if not analysis:
instroke_analysis = InStrokeAnalysis( instroke_analysis = InStrokeAnalysis(
workout = w, workout = w,