From 04aa1a146d3102bbb9af64cb1b0514d4b3a8ad15 Mon Sep 17 00:00:00 2001
From: Sander Roosendaal
Date: Fri, 7 Oct 2022 09:28:16 +0200
Subject: [PATCH 1/9] adding condition description
---
rowers/models.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/rowers/models.py b/rowers/models.py
index 2eeefe77..d4af5780 100644
--- a/rowers/models.py
+++ b/rowers/models.py
@@ -1481,6 +1481,9 @@ class Alert(models.Model):
value1=self.measured.value1,
)
+ for condition in self.filter.all():
+ description += ' '+str(condition)+';'
+
return description
def shortdescription(self): # pragma: no cover
From fba2d9e4c09d7802ca794a0eebeb9653f4dad7bb Mon Sep 17 00:00:00 2001
From: Sander Roosendaal
Date: Fri, 7 Oct 2022 11:43:45 +0200
Subject: [PATCH 2/9] updating filter str
---
rowers/models.py | 12 ++++++++++--
1 file changed, 10 insertions(+), 2 deletions(-)
diff --git a/rowers/models.py b/rowers/models.py
index d4af5780..221e11f9 100644
--- a/rowers/models.py
+++ b/rowers/models.py
@@ -1378,12 +1378,20 @@ class Condition(models.Model):
max_length=20, choices=conditionchoices, null=True)
def __str__(self):
- str = 'Condition: {metric} {condition} {value1} {value2}'.format(
+ str = 'Condition: {metric} {condition} {value1}'.format(
metric=self.metric,
condition=self.condition,
value1 = self.value1,
- value2 = self.value2,
)
+ if self.condition == 'between':
+ str = 'Condition: {metric} between {value1} and {value2}'.format(
+ metric=self.metric,
+ condition=self.condition,
+ value1 = self.value1,
+ value2 = self.value2,
+ )
+
+
return str
From ee2d2120a41d37c2dbe1d418eb5a5a48d110f1d5 Mon Sep 17 00:00:00 2001
From: Sander Roosendaal
Date: Fri, 7 Oct 2022 12:01:44 +0200
Subject: [PATCH 3/9] better breakthrough email
---
rowers/models.py | 10 ++++++++++
rowers/tasks.py | 1 +
rowers/templates/breakthroughemail.html | 2 +-
rowers/tests/test_async_tasks.py | 1 +
4 files changed, 13 insertions(+), 1 deletion(-)
diff --git a/rowers/models.py b/rowers/models.py
index 221e11f9..8e6cfba1 100644
--- a/rowers/models.py
+++ b/rowers/models.py
@@ -4966,3 +4966,13 @@ class ShareKey(models.Model):
@property
def expiration_date(self): # pragma: no cover
return self.creation_date + datetime.timedelta(self.expiration_seconds)
+
+class InStrokeAnalysis(models.Model):
+ workout = models.ForeignKey(Workout, on_delete=models.CASCADE)
+ name = models.CharField(max_length=150, blank=True, null=True)
+ date = models.DateField(blank=True, null=True)
+ notes = models.TextField(blank=True)
+ start_second = models.IntegerField(default=0)
+ end_second = models.IntegerField(default=3600)
+ min_spm = models.IntegerField(default=10)
+ max_spm = models.IntegerField(default=45)
diff --git a/rowers/tasks.py b/rowers/tasks.py
index bff93d3e..5cf8866b 100644
--- a/rowers/tasks.py
+++ b/rowers/tasks.py
@@ -1715,6 +1715,7 @@ def handle_sendemail_breakthrough(workoutid, useremail,
tablevalues = [
{'delta': t.delta,
+ 'time': str(timedelta(seconds=t.delta)),
'cpvalue': t.cpvalues,
'pwr': t.pwr
} for t in btvalues.itertuples()
diff --git a/rowers/templates/breakthroughemail.html b/rowers/templates/breakthroughemail.html
index beadc132..d4280b5c 100644
--- a/rowers/templates/breakthroughemail.html
+++ b/rowers/templates/breakthroughemail.html
@@ -40,7 +40,7 @@
{% for set in btvalues %}
- | {{ set["delta"] }} |
+ {{ set["time"] }} |
{{ set["cpvalue"] }} |
{{ set["pwr"] }} |
diff --git a/rowers/tests/test_async_tasks.py b/rowers/tests/test_async_tasks.py
index daa642e6..43718b36 100644
--- a/rowers/tests/test_async_tasks.py
+++ b/rowers/tests/test_async_tasks.py
@@ -306,6 +306,7 @@ class AsyncTaskTests(TestCase):
btvalues = pd.DataFrame({
'delta':[3,1,3],
+ 'time': str(timedelta(seconds=t) for t in [3,1,3]),
'cpvalues':[100,200,300],
'pwr':[100,200,300]
}).to_json()
From cf7d6c7c7970b15983809d70661162195128d233 Mon Sep 17 00:00:00 2001
From: Sander Roosendaal
Date: Fri, 7 Oct 2022 13:23:15 +0200
Subject: [PATCH 4/9] first version analyses collection
---
rowers/forms.py | 4 ++
rowers/interactiveplots.py | 21 ++++++++-
rowers/models.py | 6 ++-
rowers/templates/instroke_analysis.html | 55 ++++++++++++++++++++++
rowers/templates/instroke_interactive.html | 3 +-
rowers/templates/laboratory.html | 2 +-
rowers/urls.py | 2 +
rowers/views/analysisviews.py | 26 ++++++++++
rowers/views/statements.py | 2 +-
rowers/views/workoutviews.py | 21 +++++++++
10 files changed, 136 insertions(+), 6 deletions(-)
create mode 100644 rowers/templates/instroke_analysis.html
diff --git a/rowers/forms.py b/rowers/forms.py
index 8610dab5..bad4b1b6 100644
--- a/rowers/forms.py
+++ b/rowers/forms.py
@@ -150,6 +150,7 @@ class InstantPlanSelectForm(forms.Form):
# Instroke Metrics interactive chart form
class InstrokeForm(forms.Form):
+ name = forms.CharField(initial="", max_length=200,required=False)
metric = forms.ChoiceField(label='metric',choices=(('a','a'),('b','b')))
individual_curves = forms.BooleanField(label='individual curves',initial=False,
required=False)
@@ -159,6 +160,9 @@ class InstrokeForm(forms.Form):
required=False, initial=0, widget=forms.HiddenInput())
activeminutesmax = forms.IntegerField(
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
choices = kwargs.pop('choices', [])
diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py
index 063ff6f0..93366b79 100644
--- a/rowers/interactiveplots.py
+++ b/rowers/interactiveplots.py
@@ -4080,7 +4080,8 @@ def interactive_streamchart(id=0, promember=0):
def instroke_interactive_chart(df,metric, workout, spm_min, spm_max,
activeminutesmin, activeminutesmax,
- individual_curves):
+ individual_curves,
+ name='',notes=''):
df_pos = (df+abs(df))/2.
@@ -4184,6 +4185,24 @@ def instroke_interactive_chart(df,metric, workout, spm_min, spm_max,
plot.add_layout(label)
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:
for index,row in df.iterrows():
plot.line(xvals,row,color='lightgray',line_width=1)
diff --git a/rowers/models.py b/rowers/models.py
index 8e6cfba1..3881e269 100644
--- a/rowers/models.py
+++ b/rowers/models.py
@@ -4969,10 +4969,12 @@ class ShareKey(models.Model):
class InStrokeAnalysis(models.Model):
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)
date = models.DateField(blank=True, null=True)
notes = models.TextField(blank=True)
start_second = models.IntegerField(default=0)
end_second = models.IntegerField(default=3600)
- min_spm = models.IntegerField(default=10)
- max_spm = models.IntegerField(default=45)
+ spm_min = models.IntegerField(default=10)
+ spm_max = models.IntegerField(default=45)
diff --git a/rowers/templates/instroke_analysis.html b/rowers/templates/instroke_analysis.html
new file mode 100644
index 00000000..5e98c069
--- /dev/null
+++ b/rowers/templates/instroke_analysis.html
@@ -0,0 +1,55 @@
+{% extends "newbase.html" %}
+{% load static %}
+{% load rowerfilters %}
+
+{% block title %}Rowsandall - Analysis {% endblock %}
+
+{% block main %}
+
+In-Stroke Analysis for {{ rower.user.first_name }} {{ rower.user.last_name }}
+
+
+
+ {% if analyses %}
+ {% for analysis in analyses %}
+ -
+
{{ analysis.name }}
+
+
+
+
+
+ {{ analysis.notes }}
+
+
+ Workout: {{ analysis.workout }}
+
+
+ {{ analysis.spm_min }} - {{ analysis.spm_max }} SPM,
+ {{ analysis.start_second|secondstotimestring }} - {{ analysis.end_second|secondstotimestring }}
+
+
+
+ {{ analysis.date }}
+
+
+
+ {% endfor %}
+ {% else %}
+ -
+
You have not saved any analyses for {{ rower.user.first_name }}
+
+ {% endif %}
+
+
+
+
+{% endblock %}
+
+{% block scripts %}
+{% endblock %}
+
+{% block sidebar %}
+{% include 'menu_analytics.html' %}
+{% endblock %}
diff --git a/rowers/templates/instroke_interactive.html b/rowers/templates/instroke_interactive.html
index db6aaa5d..7eec7665 100644
--- a/rowers/templates/instroke_interactive.html
+++ b/rowers/templates/instroke_interactive.html
@@ -143,7 +143,8 @@ $( function() {
-
+
+
diff --git a/rowers/templates/laboratory.html b/rowers/templates/laboratory.html
index 15b27e02..8eeab9c3 100644
--- a/rowers/templates/laboratory.html
+++ b/rowers/templates/laboratory.html
@@ -12,7 +12,7 @@
Rower: {{ rower.user.first_name }}
- Try out Alerts
+ Try out In-Stroke Analysis
{% endblock %}
diff --git a/rowers/urls.py b/rowers/urls.py
index 51364854..a43558f1 100644
--- a/rowers/urls.py
+++ b/rowers/urls.py
@@ -828,6 +828,8 @@ urlpatterns = [
re_path(r'^errormessage/(?P[\w\ ]+.*)/$',
views.errormessage_view, name='errormessage_view'),
re_path(r'^analysis/$', views.analysis_view, name='analysis'),
+ re_path(r'^analysis/instrokeanalysis/$', views.instrokeanalysis_view,
+ name='instrokeanalysis_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 66f96a62..6251ead6 100644
--- a/rowers/views/analysisviews.py
+++ b/rowers/views/analysisviews.py
@@ -1845,6 +1845,32 @@ def agegrouprecordview(request, sex='male', weightcategory='hwt',
})
+@login_required
+@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")
+
+ 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,
+ })
+
+
@login_required
@permission_required('rower.is_coach', fn=get_user_by_userid, raise_exception=True)
def alerts_view(request, userid=0):
diff --git a/rowers/views/statements.py b/rowers/views/statements.py
index 01709aec..628087b1 100644
--- a/rowers/views/statements.py
+++ b/rowers/views/statements.py
@@ -153,7 +153,7 @@ from rowers.models import (
VideoAnalysis, ShareKey,
StandardCollection, CourseStandard,
VirtualRaceFollower, TombStone, InstantPlan,
- PlannedSessionStep,
+ PlannedSessionStep,InStrokeAnalysis,
)
from rowers.models import (
RowerPowerForm, RowerHRZonesForm, RowerForm, RowerCPForm, GraphImage, AdvancedWorkoutForm,
diff --git a/rowers/views/workoutviews.py b/rowers/views/workoutviews.py
index a3155e52..1e9f7cda 100644
--- a/rowers/views/workoutviews.py
+++ b/rowers/views/workoutviews.py
@@ -2981,6 +2981,8 @@ def instroke_chart_interactive(request, id=0):
metric = instrokemetrics[0]
spm_min = 15
spm_max = 45
+ name = ''
+ notes = ''
activeminutesmax = int(rowdata.duration/60.)
activeminutesmin = 0
@@ -2996,6 +2998,24 @@ def instroke_chart_interactive(request, id=0):
activeminutesmin = form.cleaned_data['activeminutesmin']
activeminutesmax = form.cleaned_data['activeminutesmax']
individual_curves = form.cleaned_data['individual_curves']
+ notes = form.cleaned_data['notes']
+ name = form.cleaned_data['name']
+
+ if "_save" in request.POST:
+ 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,
+ )
+ instroke_analysis.save()
+ messages.info(request,'In-Stroke Analysis saved')
activesecondsmin = 60.*activeminutesmin
@@ -3016,6 +3036,7 @@ def instroke_chart_interactive(request, id=0):
activeminutesmin,
activeminutesmax,
individual_curves,
+ name=name,notes=notes,
)
# change to range spm_min to spm_max
From 1e5b6f6cd860c059185e09efef0602f276350db6 Mon Sep 17 00:00:00 2001
From: Sander Roosendaal
Date: Fri, 7 Oct 2022 13:46:36 +0200
Subject: [PATCH 5/9] better analysis list view
---
rowers/templates/instroke_analysis.html | 49 ++++++++++++++++---------
rowers/views/analysisviews.py | 2 +-
static/css/rowsandall2.css | 8 ++++
3 files changed, 41 insertions(+), 18 deletions(-)
diff --git a/rowers/templates/instroke_analysis.html b/rowers/templates/instroke_analysis.html
index 5e98c069..ff533b4a 100644
--- a/rowers/templates/instroke_analysis.html
+++ b/rowers/templates/instroke_analysis.html
@@ -12,28 +12,43 @@
{% if analyses %}
{% for analysis in analyses %}
- -
-
{{ analysis.name }}
-
+ {{ analysis.date }}
+ {{ analysis.name }}
+
+
+
+
+ Workout
+ {{ analysis.workout }}
+
+
+ Metric
+ {{ analysis.metric }}
+
+
+
Notes
{{ analysis.notes }}
-
-
- Workout: {{ analysis.workout }}
-
-
- {{ analysis.spm_min }} - {{ analysis.spm_max }} SPM,
+
+
+ SPM
+ {{ analysis.spm_min }} - {{ analysis.spm_max }}
+
+
+
Time
{{ analysis.start_second|secondstotimestring }} - {{ analysis.end_second|secondstotimestring }}
-
-
-
- {{ analysis.date }}
-
-
+
+
{% endfor %}
{% else %}
diff --git a/rowers/views/analysisviews.py b/rowers/views/analysisviews.py
index 6251ead6..01e942c5 100644
--- a/rowers/views/analysisviews.py
+++ b/rowers/views/analysisviews.py
@@ -1850,7 +1850,7 @@ def agegrouprecordview(request, sex='male', weightcategory='hwt',
def instrokeanalysis_view(request, userid=0):
r = getrequestrower(request, userid=userid)
- analyses = InStrokeAnalysis.objects.filter(rower=r).order_by("-date")
+ analyses = InStrokeAnalysis.objects.filter(rower=r).order_by("-date","-id")
breadcrumbs = [
{
diff --git a/static/css/rowsandall2.css b/static/css/rowsandall2.css
index a157f4ae..a9086cc1 100644
--- a/static/css/rowsandall2.css
+++ b/static/css/rowsandall2.css
@@ -395,6 +395,14 @@ th.rotate > div > span {
margin: 0px;
}
+.analysiscontainer {
+ display: grid;
+ grid-template-columns: 50px repeat(auto-fit, minmax(calc((100% - 100px)/6), 1fr));
+ /* grid-template-columns: 50px repeat(auto-fit, minmax(100px, 1fr)) 50px; ????*/
+ padding: 5px;
+ margin: 0px;
+}
+
.workoutelement {
margin-left: auto;
margin-right: auto;
From e6160ae82e4ed317abd3a88ed9d30adbbde162b4 Mon Sep 17 00:00:00 2001
From: Sander Roosendaal
Date: Fri, 7 Oct 2022 15:43:13 +0200
Subject: [PATCH 6/9] passing tests
---
rowers/tasks.py | 3 +-
rowers/templates/analysis.html | 12 ---
rowers/templates/instroke_analysis.html | 2 +-
rowers/templates/menu_analytics.html | 5 --
rowers/urls.py | 2 +
rowers/views/workoutviews.py | 104 +++++++++++++++++++-----
6 files changed, 87 insertions(+), 41 deletions(-)
diff --git a/rowers/tasks.py b/rowers/tasks.py
index 5cf8866b..01f5bc7b 100644
--- a/rowers/tasks.py
+++ b/rowers/tasks.py
@@ -106,6 +106,7 @@ NK_API_LOCATION = CFG["nk_api_location"]
TP_CLIENT_ID = CFG["tp_client_id"]
TP_CLIENT_SECRET = CFG["tp_client_secret"]
+
from requests_oauthlib import OAuth1, OAuth1Session
import pandas as pd
@@ -386,7 +387,7 @@ def instroke_static(w, metric, debug=False, **kwargs):
@app.task
def handle_request_post(url, data, debug=False, **kwargs): # pragma: no cover
if 'localhost' in url:
- url = 'http'+url[5:]
+ url = 'http'+url[4:]
response = requests.post(url, data, verify=False)
dologging('upload_api.log', data)
dologging('upload_api.log', response.status_code)
diff --git a/rowers/templates/analysis.html b/rowers/templates/analysis.html
index b1e8ea47..555f58bf 100644
--- a/rowers/templates/analysis.html
+++ b/rowers/templates/analysis.html
@@ -131,18 +131,6 @@
Need to monitor a metric? Set up automatic alerting and see the reports for your workouts.
-
-
- Ranking Pieces
-
-
-

-
-
-
- Analyze your Concept2 ranking pieces over a date range and predict your pace on other pieces.
-
Histogram
diff --git a/rowers/templates/instroke_analysis.html b/rowers/templates/instroke_analysis.html
index ff533b4a..3669a531 100644
--- a/rowers/templates/instroke_analysis.html
+++ b/rowers/templates/instroke_analysis.html
@@ -17,7 +17,7 @@
{{ analysis.name }}