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 }}

+ +
+
    + {% if the_div %} +
  • +
    + {{ the_div|safe }} +
    +
  • + {% endif %} + {% if analyses %} + {% for analysis in analyses %} +
  • + {{ analysis.date }} +

    {{ analysis.name }}

    +
    +
    + {% if analysis in selected %} + + {% else %} + + {% endif %} +
    +
    + + + +
    +
    + + + +
    +
    + Workout
    + {{ analysis.workout.name }}
    + {{ analysis.workout.date }}, {{ analysis.workout.distance }}m +
    +
    + Notes
    + {{ analysis.notes }} +
    +
    + SPM
    + {{ analysis.spm_min }} - {{ analysis.spm_max }} +
    +
    + Distance
    + {{ analysis.dist_min }} - {{ analysis.dist_max }} +
    +
    + Work
    + {{ analysis.work_min }} - {{ analysis.work_max }} +
    +
    + Avg Pace
    + {{ analysis.average_boatspeed|velotopace }} +
    + +
    +
  • + {% endfor %} + {% else %} +
  • +

    You have not saved any analyses for {{ rower.user.first_name }}

    +
  • + {% endif %} +
+{% csrf_token %} + +
+ + + +{% 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 @@ {{ form.as_table }}
-

- + -

+ + +

+ 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,