Merge branch 'feature/instroke' into develop
This commit is contained in:
+12
-1
@@ -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(),
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 %}
|
||||||
@@ -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 %}
|
||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -51,11 +51,6 @@
|
|||||||
<a href="/rowers/goldmedalscores/">
|
<a href="/rowers/goldmedalscores/">
|
||||||
<i class="far fa-medal fa-fw"></i> Marker Workouts
|
<i class="far fa-medal fa-fw"></i> Marker Workouts
|
||||||
</a>
|
</a>
|
||||||
</li>
|
|
||||||
<li id="fitness-ranking">
|
|
||||||
<a href="/rowers/ote-bests2/">
|
|
||||||
<i class="fas fa-star fa-fw"></i> Ranking Pieces
|
|
||||||
</a>
|
|
||||||
</li>
|
</li>
|
||||||
<li id="fitness-zones">
|
<li id="fitness-zones">
|
||||||
<a href="/rowers/trainingzones/">
|
<a href="/rowers/trainingzones/">
|
||||||
|
|||||||
@@ -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+)/$',
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1901,6 +1901,7 @@ 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
|
||||||
|
try:
|
||||||
if follower.user.id not in registereduserids:
|
if follower.user.id not in registereduserids:
|
||||||
_ = myqueue(
|
_ = myqueue(
|
||||||
queue,
|
queue,
|
||||||
@@ -1908,6 +1909,9 @@ def virtualevent_addboat_view(request, id=0):
|
|||||||
email, othername,
|
email, othername,
|
||||||
registeredname, race.name, race.id,
|
registeredname, race.name, race.id,
|
||||||
)
|
)
|
||||||
|
registereduserids.append(follower.user.id)
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
url = reverse('virtualevent_view',
|
url = reverse('virtualevent_view',
|
||||||
kwargs={
|
kwargs={
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,18 +2981,65 @@ def instroke_chart_interactive(request, id=0):
|
|||||||
'maxminutes': maxminutes,
|
'maxminutes': maxminutes,
|
||||||
})
|
})
|
||||||
|
|
||||||
script = ''
|
if analysis:
|
||||||
div = get_call()
|
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]
|
metric = instrokemetrics[0]
|
||||||
spm_min = 15
|
spm_min = 15
|
||||||
spm_max = 45
|
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.)
|
activeminutesmax = int(rowdata.duration/60.)
|
||||||
activeminutesmin = 0
|
activeminutesmin = 0
|
||||||
maxminutes = activeminutesmax
|
|
||||||
|
maxminutes = int(rowdata.duration/60.)
|
||||||
individual_curves = False
|
individual_curves = False
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
script = ''
|
||||||
|
div = get_call()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
form = InstrokeForm(request.POST,choices=instrokemetrics)
|
form = InstrokeForm(request.POST,choices=instrokemetrics)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user