Private
Public Access
1
0

Merge branch 'feature/instroke' into develop

This commit is contained in:
Sander Roosendaal
2022-10-10 20:45:18 +02:00
16 changed files with 468 additions and 47 deletions
+12 -1
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 PaidPlan, InStrokeAnalysis
) )
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
@@ -150,6 +150,7 @@ class InstantPlanSelectForm(forms.Form):
# Instroke Metrics interactive chart form # Instroke Metrics interactive chart form
class InstrokeForm(forms.Form): class InstrokeForm(forms.Form):
name = forms.CharField(initial="", max_length=200,required=False)
metric = forms.ChoiceField(label='metric',choices=(('a','a'),('b','b'))) metric = forms.ChoiceField(label='metric',choices=(('a','a'),('b','b')))
individual_curves = forms.BooleanField(label='individual curves',initial=False, individual_curves = forms.BooleanField(label='individual curves',initial=False,
required=False) required=False)
@@ -159,6 +160,9 @@ class InstrokeForm(forms.Form):
required=False, initial=0, widget=forms.HiddenInput()) required=False, initial=0, widget=forms.HiddenInput())
activeminutesmax = forms.IntegerField( activeminutesmax = forms.IntegerField(
required=False, initial=0, widget=forms.HiddenInput()) required=False, initial=0, widget=forms.HiddenInput())
notes = forms.CharField(required=False,
max_length=200, label='Notes',
widget=forms.Textarea)
def __init__(self, *args, **kwargs): # pragma: no cover def __init__(self, *args, **kwargs): # pragma: no cover
choices = kwargs.pop('choices', []) choices = kwargs.pop('choices', [])
@@ -1246,6 +1250,13 @@ class WorkoutSingleSelectForm(forms.Form):
self.fields['workout'].queryset = workouts self.fields['workout'].queryset = workouts
class InStrokeMultipleCompareForm(forms.Form):
analyses = forms.ModelMultipleChoiceField(
queryset=InStrokeAnalysis.objects.all(),
widget=forms.CheckboxSelectMultiple()
)
class WorkoutMultipleCompareForm(forms.Form): class WorkoutMultipleCompareForm(forms.Form):
workouts = forms.ModelMultipleChoiceField( workouts = forms.ModelMultipleChoiceField(
queryset=Workout.objects.filter(), queryset=Workout.objects.filter(),
+96 -2
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 from rowers.models import course_spline, VirtualRaceResult, InStrokeAnalysis
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
@@ -4078,9 +4078,85 @@ def interactive_streamchart(id=0, promember=0):
return [script, div] return [script, div]
def instroke_multi_interactive_chart(selected):
df_plot = pd.DataFrame()
ids = [analysis.id for analysis in selected]
for analysis in selected:
#start_second, end_second, spm_min, spm_max, name
activeminutesmin = int(analysis.start_second/60.)
activeminutesmax = int(analysis.end_second/60.)
rowdata = rrdata(csvfile=analysis.workout.csvfilename)
data = rowdata.get_instroke_data(
analysis.metric,
spm_min=analysis.spm_min,
spm_max=analysis.spm_max,
activeminutesmin=activeminutesmin,
activeminutesmax=activeminutesmax,
)
mean_vals = data.mean()
xvals = np.arange(len(mean_vals))
xname = 'x_'+str(analysis.id)
yname = 'y_'+str(analysis.id)
df_plot[xname] = xvals
df_plot[yname] = mean_vals
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 = InStrokeAnalysis.objects.get(id=id)
plot.line(xname,yname,source=source,legend_label=analysis.name,
line_width=2, color=color)
script, div = components(plot)
return (script, div)
def instroke_interactive_chart(df,metric, workout, spm_min, spm_max, def instroke_interactive_chart(df,metric, workout, spm_min, spm_max,
activeminutesmin, activeminutesmax, activeminutesmin, activeminutesmax,
individual_curves): individual_curves,
name='',notes=''):
df_pos = (df+abs(df))/2. df_pos = (df+abs(df))/2.
@@ -4184,6 +4260,24 @@ def instroke_interactive_chart(df,metric, workout, spm_min, spm_max,
plot.add_layout(label) plot.add_layout(label)
plot.add_layout(label2) plot.add_layout(label2)
if name:
namelabel = Label(x=50, y=480, x_units='screen', y_units='screen',
text=name,
background_fill_alpha=0.7,
background_fill_color='white',
text_color='black',
)
plot.add_layout(namelabel)
if notes:
noteslabel = Label(x=50, y=50, x_units='screen', y_units='screen',
text=notes,
background_fill_alpha=0.7,
background_fill_color='white',
text_color='black',
)
plot.add_layout(noteslabel)
if individual_curves: if individual_curves:
for index,row in df.iterrows(): for index,row in df.iterrows():
plot.line(xvals,row,color='lightgray',line_width=1) plot.line(xvals,row,color='lightgray',line_width=1)
+10 -2
View File
@@ -4969,10 +4969,18 @@ class ShareKey(models.Model):
class InStrokeAnalysis(models.Model): class InStrokeAnalysis(models.Model):
workout = models.ForeignKey(Workout, on_delete=models.CASCADE) workout = models.ForeignKey(Workout, on_delete=models.CASCADE)
rower = models.ForeignKey(Rower, on_delete=models.CASCADE)
metric = models.CharField(max_length=140, blank=True, null=True)
name = models.CharField(max_length=150, blank=True, null=True) name = models.CharField(max_length=150, blank=True, null=True)
date = models.DateField(blank=True, null=True) date = models.DateField(blank=True, null=True)
notes = models.TextField(blank=True) notes = models.TextField(blank=True)
start_second = models.IntegerField(default=0) start_second = models.IntegerField(default=0)
end_second = models.IntegerField(default=3600) end_second = models.IntegerField(default=3600)
min_spm = models.IntegerField(default=10) spm_min = models.IntegerField(default=10)
max_spm = models.IntegerField(default=45) spm_max = models.IntegerField(default=45)
def __str__(self):
s = 'In-Stroke Analysis {name} ({date})'.format(name = self.name,
date = self.date)
return s
+2 -1
View File
@@ -106,6 +106,7 @@ NK_API_LOCATION = CFG["nk_api_location"]
TP_CLIENT_ID = CFG["tp_client_id"] TP_CLIENT_ID = CFG["tp_client_id"]
TP_CLIENT_SECRET = CFG["tp_client_secret"] TP_CLIENT_SECRET = CFG["tp_client_secret"]
from requests_oauthlib import OAuth1, OAuth1Session from requests_oauthlib import OAuth1, OAuth1Session
import pandas as pd import pandas as pd
@@ -386,7 +387,7 @@ def instroke_static(w, metric, debug=False, **kwargs):
@app.task @app.task
def handle_request_post(url, data, debug=False, **kwargs): # pragma: no cover def handle_request_post(url, data, debug=False, **kwargs): # pragma: no cover
if 'localhost' in url: if 'localhost' in url:
url = 'http'+url[5:] url = 'http'+url[4:]
response = requests.post(url, data, verify=False) response = requests.post(url, data, verify=False)
dologging('upload_api.log', data) dologging('upload_api.log', data)
dologging('upload_api.log', response.status_code) dologging('upload_api.log', response.status_code)
-12
View File
@@ -131,18 +131,6 @@
<p> <p>
Need to monitor a metric? Set up automatic alerting and see the reports for your workouts. Need to monitor a metric? Set up automatic alerting and see the reports for your workouts.
</p> </p>
</li>
<li class="rounder">
<h2>Ranking Pieces</h2>
<a href="/rowers/ote-bests2/">
<div class="vignet">
<img src="/static/img/rankingpiece.png"
alt="Ranking Piece">
</div>
</a>
<p>
Analyze your Concept2 ranking pieces over a date range and predict your pace on other pieces.
</p>
</li> </li>
<li class="rounder"> <li class="rounder">
<h2>Histogram</h2> <h2>Histogram</h2>
+94
View File
@@ -0,0 +1,94 @@
{% 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>In-Stroke 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">
<input type="checkbox" name="analyses" value="{{ analysis.id }}" id="analyses_{{ analysis.id }}">
</div>
<div class="workoutelement">
<a class="small" href="/rowers/workout/{{ analysis.workout.id|encode }}/instroke/interactive/{{ analysis.id }}/"
title="Edit">
<i class="fas fa-pencil-alt fa-fw"></i>
</a>
</div>
<div class="workoutelement">
<a class="small" href="/rowers/analysis/instrokeanalysis/{{ 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>
{{ analysis.workout }}
</div>
<div class="workoutelement">
<span style="color:#555">Metric</span><br>
{{ analysis.metric }}
</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">Time</span><br>
{{ analysis.start_second|secondstotimestring }} - {{ analysis.end_second|secondstotimestring }}
</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 %}
+2 -1
View File
@@ -143,7 +143,8 @@ $( function() {
<label for="amount">Active Range:</label> <label for="amount">Active Range:</label>
<input type="text" id="amount" readonly style="border:0; color:#1c75bc; font-weight:bold;"> <input type="text" id="amount" readonly style="border:0; color:#1c75bc; font-weight:bold;">
</p> </p>
<input name='form' class="button" type="submit" value="Submit"> <p><input name='_form' class="button" type="submit" value="Submit"></p>
<p><input name='_save' class="button" type="submit" value="Save"></p>
</form> </form>
</li> </li>
<li class="grid_4"> <li class="grid_4">
@@ -0,0 +1,29 @@
{% extends "newbase.html" %}
{% load static %}
{% block title %}In-Stroke 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 %}
+1 -1
View File
@@ -12,7 +12,7 @@
<p>Rower: {{ rower.user.first_name }}</p> <p>Rower: {{ rower.user.first_name }}</p>
<p> <p>
<a href="/rowers/alerts/">Try out Alerts</a> <a href="/rowers/analysis/instrokeanalysis/">Try out In-Stroke Analysis</a>
</p> </p>
{% endblock %} {% endblock %}
-5
View File
@@ -51,11 +51,6 @@
<a href="/rowers/goldmedalscores/"> <a href="/rowers/goldmedalscores/">
<i class="far fa-medal fa-fw"></i>&nbsp;Marker Workouts <i class="far fa-medal fa-fw"></i>&nbsp;Marker Workouts
</a> </a>
</li>
<li id="fitness-ranking">
<a href="/rowers/ote-bests2/">
<i class="fas fa-star fa-fw"></i>&nbsp;Ranking Pieces
</a>
</li> </li>
<li id="fitness-zones"> <li id="fitness-zones">
<a href="/rowers/trainingzones/"> <a href="/rowers/trainingzones/">
+8
View File
@@ -252,6 +252,10 @@ urlpatterns = [
path('403/', TemplateView.as_view(template_name='403.html'), name='403'), path('403/', TemplateView.as_view(template_name='403.html'), name='403'),
re_path(r'^workout/(?P<id>\b[0-9A-Fa-f]+\b)/instroke/interactive/$', re_path(r'^workout/(?P<id>\b[0-9A-Fa-f]+\b)/instroke/interactive/$',
views.instroke_chart_interactive, name='instroke_chart_interactive'), views.instroke_chart_interactive, name='instroke_chart_interactive'),
re_path(r'^workout/(?P<id>\b[0-9A-Fa-f]+\b)/instroke/interactive/(?P<analysis>\d+)/$',
views.instroke_chart_interactive, name='instroke_chart_interactive'),
re_path(r'^workout/(?P<id>\b[0-9A-Fa-f]+\b)/instroke/interactive/(?P<analysis>\d+)/user/(?P<userid>\d+)/$',
views.instroke_chart_interactive, name='instroke_chart_interactive'),
re_path(r'^exportallworkouts/?/$', views.workouts_summaries_email_view, re_path(r'^exportallworkouts/?/$', views.workouts_summaries_email_view,
name='workouts_summaries_email_view'), name='workouts_summaries_email_view'),
path('failedjobs/', views.failed_queue_view, name='failed_queue_view'), path('failedjobs/', views.failed_queue_view, name='failed_queue_view'),
@@ -828,6 +832,10 @@ urlpatterns = [
re_path(r'^errormessage/(?P<errormessage>[\w\ ]+.*)/$', re_path(r'^errormessage/(?P<errormessage>[\w\ ]+.*)/$',
views.errormessage_view, name='errormessage_view'), views.errormessage_view, name='errormessage_view'),
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,
name='instrokeanalysis_view'),
re_path(r'^analysis/instrokeanalysis/(?P<pk>\d+)/delete/$',
views.InStrokeAnalysisDelete.as_view(), name='instroke_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+)/$',
+99 -4
View File
@@ -1845,6 +1845,105 @@ def agegrouprecordview(request, sex='male', weightcategory='hwt',
}) })
@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 instrokeanalysis_view(request, userid=0):
r = getrequestrower(request, userid=userid)
analyses = InStrokeAnalysis.objects.filter(rower=r).order_by("-date","-id")
script = ""
div = ""
if request.method == 'POST':
form = InStrokeMultipleCompareForm(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 = instroke_multi_interactive_chart(selected)
breadcrumbs = [
{
'url': '/rowers/analysis',
'name': 'Analysis'
},
{
'url': reverse('instrokeanalysis_view'),
'name': 'In-Stroke Analysis',
},
]
return render(request, 'instroke_analysis.html',
{
'breadcrumbs': breadcrumbs,
'analyses': analyses,
'rower': r,
'the_script': script,
'the_div': div,
})
#instroke analysis delete view
class InStrokeAnalysisDelete(DeleteView):
login_required = True
model = InStrokeAnalysis
template_name = 'instrokeanalysis_delete_confirm.html'
# extra parameters
def get_context_data(self, **kwargs):
context = super(InStrokeAnalysisDelete, 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('instrokeanalysis_view'),
'name': 'In-Stroke Analysis',
},
{
'url': reverse('instroke_chart_interactive',
kwargs={'userid': userid,
'id': encoder.encode_hex(self.object.workout.id),
'analysis': self.object.pk}),
'name': self.object.name,
},
{
'url': reverse('instroke_analysis_delete_view', kwargs={'pk': self.object.pk}),
'name': 'Delete'
}
]
context['breadcrumbs'] = breadcrumbs
return context
def get_success_url(self):
return reverse('instrokeanalysis_view')
def get_object(self, *args, **kwargs):
obj = super(InStrokeAnalysisDelete, self).get_object(*args, **kwargs)
if obj.rower != self.request.user.rower:
raise PermissionDenied("You are not allowed to delete this Analysis")
return obj
@login_required @login_required
@permission_required('rower.is_coach', fn=get_user_by_userid, raise_exception=True) @permission_required('rower.is_coach', fn=get_user_by_userid, raise_exception=True)
def alerts_view(request, userid=0): def alerts_view(request, userid=0):
@@ -1877,8 +1976,6 @@ def alerts_view(request, userid=0):
}) })
# alert create view # alert create view
@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."
" 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",
@@ -2133,8 +2230,6 @@ def alert_edit_view(request, id=0, userid=0):
}) })
# alert delete view # alert delete view
class AlertDelete(DeleteView): class AlertDelete(DeleteView):
login_required = True login_required = True
model = Alert model = Alert
+11 -7
View File
@@ -1901,13 +1901,17 @@ def virtualevent_addboat_view(request, id=0):
registeredname = r.user.first_name+' '+r.user.last_name registeredname = r.user.first_name+' '+r.user.last_name
email = follower.emailaddress email = follower.emailaddress
if follower.user.id not in registereduserids: try:
_ = myqueue( if follower.user.id not in registereduserids:
queue, _ = myqueue(
handle_sendemail_raceregistration, queue,
email, othername, handle_sendemail_raceregistration,
registeredname, race.name, race.id, email, othername,
) registeredname, race.name, race.id,
)
registereduserids.append(follower.user.id)
except AttributeError:
pass
url = reverse('virtualevent_view', url = reverse('virtualevent_view',
kwargs={ kwargs={
+2 -2
View File
@@ -111,7 +111,7 @@ from rowers.forms import (
VideoAnalysisMetricsForm, SurveyForm, HistorySelectForm, VideoAnalysisMetricsForm, SurveyForm, HistorySelectForm,
StravaChartForm, FitnessFitForm, PerformanceManagerForm, StravaChartForm, FitnessFitForm, PerformanceManagerForm,
TrainingPlanBillingForm, InstantPlanSelectForm, TrainingPlanBillingForm, InstantPlanSelectForm,
TrainingZonesForm, InstrokeForm TrainingZonesForm, InstrokeForm, InStrokeMultipleCompareForm
) )
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
@@ -153,7 +153,7 @@ from rowers.models import (
VideoAnalysis, ShareKey, VideoAnalysis, ShareKey,
StandardCollection, CourseStandard, StandardCollection, CourseStandard,
VirtualRaceFollower, TombStone, InstantPlan, VirtualRaceFollower, TombStone, InstantPlan,
PlannedSessionStep, PlannedSessionStep,InStrokeAnalysis,
) )
from rowers.models import ( from rowers.models import (
RowerPowerForm, RowerHRZonesForm, RowerForm, RowerCPForm, GraphImage, AdvancedWorkoutForm, RowerPowerForm, RowerHRZonesForm, RowerForm, RowerCPForm, GraphImage, AdvancedWorkoutForm,
+93 -8
View File
@@ -2915,11 +2915,17 @@ def instroke_chart(request, id=0, metric=''): # pragma: no cover
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
@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('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 instroke_chart_interactive(request, id=0): def instroke_chart_interactive(request, id=0, analysis=0, userid=0):
is_ajax = request_is_ajax(request) is_ajax = request_is_ajax(request)
r = getrequestrower(request, userid=userid)
w = get_workoutuser(id, request) w = get_workoutuser(id, request)
rowdata = rrdata(csvfile=w.csvfilename) rowdata = rrdata(csvfile=w.csvfilename)
@@ -2975,17 +2981,64 @@ def instroke_chart_interactive(request, id=0):
'maxminutes': maxminutes, 'maxminutes': maxminutes,
}) })
if analysis:
try:
instroke_analysis = InStrokeAnalysis.objects.get(id=analysis)
if instroke_analysis.rower != r:
analysis = 0
messages.error(request,'Access to this saved analysis denied')
raise ValueError
if instroke_analysis.workout != w:
messages.error(request,'This saved analysis belongs to a different workout')
form = InstrokeForm(
choices=instrokemetrics,
initial={
'metric':instroke_analysis.metric,
'name': instroke_analysis.name,
'notes': instroke_analysis.notes,
'activeminutesmin':int(instroke_analysis.start_second/60.),
'activeminutesmax':int(instroke_analysis.end_second/60.),
'spm_min': instroke_analysis.spm_min,
'spm_max': instroke_analysis.spm_max,
}
)
metric = instroke_analysis.metric
name = instroke_analysis.name
notes = instroke_analysis.notes
activeminutesmin = int(instroke_analysis.start_second/60.)
activeminutesmax = int(instroke_analysis.end_second/60.)
spm_min = instroke_analysis.spm_min
spm_max = instroke_analysis.spm_max
except (InStrokeAnalysis.DoesNotExist, ValueError):
metric = instrokemetrics[0]
spm_min = 15
spm_max = 45
name = ''
notes = ''
activeminutesmax = int(rowdata.duration/60.)
activeminutesmin = 0
else:
metric = instrokemetrics[0]
spm_min = 15
spm_max = 45
name = ''
notes = ''
activeminutesmax = int(rowdata.duration/60.)
activeminutesmin = 0
maxminutes = int(rowdata.duration/60.)
individual_curves = False
script = '' script = ''
div = get_call() div = get_call()
metric = instrokemetrics[0]
spm_min = 15
spm_max = 45
activeminutesmax = int(rowdata.duration/60.)
activeminutesmin = 0
maxminutes = activeminutesmax
individual_curves = False
if request.method == 'POST': if request.method == 'POST':
form = InstrokeForm(request.POST,choices=instrokemetrics) form = InstrokeForm(request.POST,choices=instrokemetrics)
@@ -2996,6 +3049,37 @@ def instroke_chart_interactive(request, id=0):
activeminutesmin = form.cleaned_data['activeminutesmin'] activeminutesmin = form.cleaned_data['activeminutesmin']
activeminutesmax = form.cleaned_data['activeminutesmax'] activeminutesmax = form.cleaned_data['activeminutesmax']
individual_curves = form.cleaned_data['individual_curves'] individual_curves = form.cleaned_data['individual_curves']
notes = form.cleaned_data['notes']
name = form.cleaned_data['name']
if "_save" in request.POST:
if not analysis:
instroke_analysis = InStrokeAnalysis(
workout = w,
metric = metric,
name = name,
date = timezone.now().date(),
notes = notes,
start_second = 60*activeminutesmin,
end_second = 60*activeminutesmax,
spm_min = spm_min,
spm_max = spm_max,
rower=w.user,
)
else:
instroke_analysis.workout = w
instroke_analysis.metric = metric
instroke_analysis.name = name
instroke_analysis.date = timezone.now().date()
instroke_analysis.notes = notes
instroke_analysis.start_second = 60*activeminutesmin
instroke_analysis.end_second = 60*activeminutesmax
instroke_analysis.spm_min = spm_min
instroke_analysis.spm_max = spm_max
instroke_analysis.rower=w.user
instroke_analysis.save()
messages.info(request,'In-Stroke Analysis saved')
activesecondsmin = 60.*activeminutesmin activesecondsmin = 60.*activeminutesmin
@@ -3016,6 +3100,7 @@ def instroke_chart_interactive(request, id=0):
activeminutesmin, activeminutesmin,
activeminutesmax, activeminutesmax,
individual_curves, individual_curves,
name=name,notes=notes,
) )
# change to range spm_min to spm_max # change to range spm_min to spm_max
+8
View File
@@ -395,6 +395,14 @@ th.rotate > div > span {
margin: 0px; margin: 0px;
} }
.analysiscontainer {
display: grid;
grid-template-columns: 50px repeat(auto-fit, minmax(calc((100% - 100px)/7), 1fr));
/* grid-template-columns: 50px repeat(auto-fit, minmax(100px, 1fr)) 50px; ????*/
padding: 5px;
margin: 0px;
}
.workoutelement { .workoutelement {
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;