From 7ebdf21c2559768b0ddecb49f38018e0a3e22670 Mon Sep 17 00:00:00 2001
From: Sander Roosendaal
Date: Wed, 26 Oct 2022 23:49:28 +0200
Subject: [PATCH] force curve comparison
---
rowers/forms.py | 20 ++-
rowers/interactiveplots.py | 158 ++++++++++++++++--
rowers/models.py | 22 +++
rowers/templates/forcecurve_analysis.html | 104 ++++++++++++
rowers/templates/forcecurve_single.html | 39 ++++-
.../forcecurveanalysis_delete_confirm.html | 29 ++++
rowers/urls.py | 8 +
rowers/views/analysisviews.py | 105 +++++++++++-
rowers/views/statements.py | 5 +-
rowers/views/workoutviews.py | 127 +++++++++++++-
10 files changed, 590 insertions(+), 27 deletions(-)
create mode 100644 rowers/templates/forcecurve_analysis.html
create mode 100644 rowers/templates/forcecurveanalysis_delete_confirm.html
diff --git a/rowers/forms.py b/rowers/forms.py
index 76e6d66d..02f970a6 100644
--- a/rowers/forms.py
+++ b/rowers/forms.py
@@ -10,7 +10,7 @@ from django.contrib.admin.widgets import FilteredSelectMultiple
from rowers.models import (
Workout, Rower, Team, PlannedSession, GeoCourse,
VirtualRace, VirtualRaceResult, IndoorVirtualRaceResult,
- PaidPlan, InStrokeAnalysis
+ PaidPlan, InStrokeAnalysis, ForceCurveAnalysis
)
from rowers.rows import validate_file_extension, must_be_csv, validate_image_extension, validate_kml
from django.contrib.auth.forms import UserCreationForm
@@ -1256,6 +1256,12 @@ class InStrokeMultipleCompareForm(forms.Form):
widget=forms.CheckboxSelectMultiple()
)
+class ForceCurveMultipleCompareForm(forms.Form):
+ analyses = forms.ModelMultipleChoiceField(
+ queryset=ForceCurveAnalysis.objects.all(),
+ widget=forms.CheckboxSelectMultiple()
+ )
+
class WorkoutMultipleCompareForm(forms.Form):
workouts = forms.ModelMultipleChoiceField(
@@ -1829,6 +1835,16 @@ class FlexOptionsForm(forms.Form):
class ForceCurveOptionsForm(forms.Form):
includereststrokes = forms.BooleanField(initial=False, required=False,
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 = (
('line', 'Force Curve Collection Plot'),
('scatter', 'Peak Force Scatter Plot'),
@@ -1837,6 +1853,8 @@ class ForceCurveOptionsForm(forms.Form):
plottype = forms.ChoiceField(choices=plotchoices, initial='line',
label='Individual Stroke Chart Type')
+ name = forms.CharField(initial="", label='Name',required=False)
+
axchoices = list(
(ax[0], ax[1]) for ax in axes if ax[0] not in ['cumdist', 'None']
diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py
index 1bb56563..1dcca83f 100644
--- a/rowers/interactiveplots.py
+++ b/rowers/interactiveplots.py
@@ -5,7 +5,7 @@ from rowers.metrics import rowingmetrics, metricsdicts
from scipy.spatial import ConvexHull, Delaunay
from scipy.stats import linregress, percentileofscore
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.layouts import layout, widgetbox
from bokeh.resources import CDN, INLINE
@@ -813,7 +813,11 @@ def interactive_activitychart2(workouts, startdate, enddate, stack='type', toolb
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'
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)
efflengthlabel.text = 'Effective Length: '+efflength.toFixed(2)
- console.log(count);
- console.log(multilines['x'].length);
- console.log(multilines['y'].length);
+ // console.log(count);
+ // console.log(multilines['x'].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.change.emit();
@@ -1425,40 +1438,43 @@ def interactive_forcecurve(theworkouts, workstrokesonly=True, plottype='scatter'
""")
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)
callback.args["annotation"] = annotation
- slider_spm_min = Slider(width=140, start=15.0, end=55, value=15.0, step=.1,
- title="Min SPM")
+ slider_spm_min = Slider(width=140, start=15.0, end=55, value=15, step=.1,
+ title="Min SPM", name="min_spm_slider")
slider_spm_min.js_on_change('value', callback)
callback.args["minspm"] = slider_spm_min
- slider_spm_max = Slider(width=140, start=15.0, end=55, value=55.0, step=.1,
- title="Max SPM")
+ slider_spm_max = Slider(width=140, start=15.0, end=55, value=55, step=.1,
+ title="Max SPM", name="max_spm_slider")
slider_spm_max.js_on_change('value', callback)
callback.args["maxspm"] = slider_spm_max
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)
callback.args["minwork"] = slider_work_min
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)
callback.args["maxwork"] = slider_work_max
distmax = 100+100*int(rowdata['distance'].max()/100.)
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)
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,
step=50,
- title="Max Distance")
+ title="Max Distance", name="max_dist_slider")
slider_dist_max.js_on_change('value', callback)
callback.args["maxdist"] = slider_dist_max
@@ -4078,6 +4094,120 @@ def interactive_streamchart(id=0, promember=0):
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']work_min]
+ rowdata = rowdata[rowdata['driveenergy']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):
df_plot = pd.DataFrame()
ids = [analysis.id for analysis in selected]
diff --git a/rowers/models.py b/rowers/models.py
index 6c03718a..1ecccfac 100644
--- a/rowers/models.py
+++ b/rowers/models.py
@@ -4992,3 +4992,25 @@ class InStrokeAnalysis(models.Model):
date = self.date)
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
diff --git a/rowers/templates/forcecurve_analysis.html b/rowers/templates/forcecurve_analysis.html
new file mode 100644
index 00000000..4582a95b
--- /dev/null
+++ b/rowers/templates/forcecurve_analysis.html
@@ -0,0 +1,104 @@
+{% extends "newbase.html" %}
+{% load static %}
+{% load rowerfilters %}
+
+{% block title %}Rowsandall - Analysis {% endblock %}
+
+{% block main %}
+
+{{ js_res | safe }}
+{{ css_res| safe }}
+
+
+
+
+
+{{ the_script |safe }}
+
+Force Curve Analysis for {{ rower.user.first_name }} {{ rower.user.last_name }}
+
+
+
+
+
+{% endblock %}
+
+{% block scripts %}
+{% endblock %}
+
+{% block sidebar %}
+{% include 'menu_analytics.html' %}
+{% endblock %}
diff --git a/rowers/templates/forcecurve_single.html b/rowers/templates/forcecurve_single.html
index 547b2bc5..56844a45 100644
--- a/rowers/templates/forcecurve_single.html
+++ b/rowers/templates/forcecurve_single.html
@@ -34,14 +34,47 @@
-
-
+
-
+
+
+
+ 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 (force curve analysis ).
+
+
+
+
{% endblock %}
{% endlocaltime %}
diff --git a/rowers/templates/forcecurveanalysis_delete_confirm.html b/rowers/templates/forcecurveanalysis_delete_confirm.html
new file mode 100644
index 00000000..39f7682a
--- /dev/null
+++ b/rowers/templates/forcecurveanalysis_delete_confirm.html
@@ -0,0 +1,29 @@
+{% extends "newbase.html" %}
+{% load static %}
+
+{% block title %}Force Curve Analysis{% endblock %}
+
+{% block main %}
+Confirm Delete
+This will permanently delete the analysis
+
+
+
+
+
+
+
+
+{% endblock %}
+
+{% block sidebar %}
+{% include 'menu_analytics.html' %}
+{% endblock %}
diff --git a/rowers/urls.py b/rowers/urls.py
index e73b05a7..63aab31d 100644
--- a/rowers/urls.py
+++ b/rowers/urls.py
@@ -452,6 +452,10 @@ urlpatterns = [
name='workout_histo_view'),
re_path(r'^workout/(?P\b[0-9A-Fa-f]+\b)/forcecurve/$', views.workout_forcecurve_view,
name='workout_forcecurve_view'),
+ re_path(r'^workout/(?P\b[0-9A-Fa-f]+\b)/forcecurve/(?P\d+)/$', views.workout_forcecurve_view,
+ name='workout_forcecurve_view'),
+ re_path(r'^workout/(?P\b[0-9A-Fa-f]+\b)/forcecurve/(?P\d+)/user/(?P\d+)/$', views.workout_forcecurve_view,
+ name='workout_forcecurve_view'),
re_path(r'^workout/(?P\b[0-9A-Fa-f]+\b)/unsubscribe/$', views.workout_unsubscribe_view,
name='workout_unsubscribe_view'),
re_path(r'^workout/(?P\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/instrokeanalysis/$', views.instrokeanalysis_view,
name='instrokeanalysis_view'),
+ re_path(r'^analysis/forcecurveanalysis/$', views.forcecurveanalysis_view,
+ name='forcecurveanalysis_view'),
re_path(r'^analysis/instrokeanalysis/(?P\d+)/delete/$',
views.InStrokeAnalysisDelete.as_view(), name='instroke_analysis_delete_view'),
+ re_path(r'^analysis/forcecurveanalysis/(?P\d+)/delete/$',
+ views.ForceCurveAnalysisDelete.as_view(), name='forcecurve_analysis_delete_view'),
re_path(r'^promembership', TemplateView.as_view(
template_name='promembership.html'), name='promembership'),
re_path(r'^checkout/(?P\d+)/$',
diff --git a/rowers/views/analysisviews.py b/rowers/views/analysisviews.py
index b8ebf271..2b67f9dd 100644
--- a/rowers/views/analysisviews.py
+++ b/rowers/views/analysisviews.py
@@ -1844,6 +1844,108 @@ def agegrouprecordview(request, sex='male', weightcategory='hwt',
'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",
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")
selected = []
- script = ""
div = ""
+ script = ""
+
if request.method == 'POST':
form = InStrokeMultipleCompareForm(request.POST)
diff --git a/rowers/views/statements.py b/rowers/views/statements.py
index f52cfac7..09c5e3b2 100644
--- a/rowers/views/statements.py
+++ b/rowers/views/statements.py
@@ -111,7 +111,8 @@ from rowers.forms import (
VideoAnalysisMetricsForm, SurveyForm, HistorySelectForm,
StravaChartForm, FitnessFitForm, PerformanceManagerForm,
TrainingPlanBillingForm, InstantPlanSelectForm,
- TrainingZonesForm, InstrokeForm, InStrokeMultipleCompareForm
+ TrainingZonesForm, InstrokeForm, InStrokeMultipleCompareForm,
+ ForceCurveMultipleCompareForm
)
from django.urls import reverse, reverse_lazy
@@ -153,7 +154,7 @@ from rowers.models import (
VideoAnalysis, ShareKey,
StandardCollection, CourseStandard,
VirtualRaceFollower, TombStone, InstantPlan,
- PlannedSessionStep,InStrokeAnalysis,
+ PlannedSessionStep,InStrokeAnalysis, ForceCurveAnalysis
)
from rowers.models import (
RowerPowerForm, RowerHRZonesForm, RowerForm, RowerCPForm, GraphImage, AdvancedWorkoutForm,
diff --git a/rowers/views/workoutviews.py b/rowers/views/workoutviews.py
index 9b4658de..dbc5e220 100644
--- a/rowers/views/workoutviews.py
+++ b/rowers/views/workoutviews.py
@@ -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",
redirect_field_name=None)
@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)
mayedit = 0
- r = getrequestrower(request)
+ r = getrequestrower(request, userid=userid)
if r == row.user:
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':
form = ForceCurveOptionsForm(request.POST)
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']
plottype = form.cleaned_data['plottype']
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
workstrokesonly = True
plottype = 'line'
- else:
- form = ForceCurveOptionsForm()
- plottype = 'line'
+
script, div, js_resources, css_resources = interactive_forcecurve(
[row],
workstrokesonly=workstrokesonly,
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 = [
@@ -455,6 +560,9 @@ def workout_forcecurve_view(request, id=0, workstrokesonly=False):
r = getrower(request.user)
+ if dist_max == 0:
+ dist_max = row.distance+100
+
return render(request,
'forcecurve_single.html',
{
@@ -464,6 +572,13 @@ def workout_forcecurve_view(request, id=0, workstrokesonly=False):
'workout': row,
'breadcrumbs': breadcrumbs,
'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,
'js_res': js_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']
name = form.cleaned_data['name']
- if "_save" in request.POST:
+ if "_save" in request.POST and "new" not in request.POST:
if not analysis:
instroke_analysis = InStrokeAnalysis(
workout = w,