force curve comparison
This commit is contained in:
@@ -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']
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
104
rowers/templates/forcecurve_analysis.html
Normal file
104
rowers/templates/forcecurve_analysis.html
Normal 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 %}
|
||||||
@@ -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 %}
|
||||||
|
|||||||
29
rowers/templates/forcecurveanalysis_delete_confirm.html
Normal file
29
rowers/templates/forcecurveanalysis_delete_confirm.html
Normal 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 %}
|
||||||
@@ -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+)/$',
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user