From e6cc169cefc419956c4e6846b5e6d4a188d33124 Mon Sep 17 00:00:00 2001
From: Sander Roosendaal
Date: Wed, 18 Dec 2024 20:35:46 +0100
Subject: [PATCH] first working version of exported session to intervals.icu
---
rowers/forms.py | 1 +
rowers/integrations/intervals.py | 34 ++++++++++++++-
rowers/models.py | 16 +++++--
rowers/plannedsessions.py | 36 ++++++++--------
rowers/tasks.py | 53 ++++++++++++++----------
rowers/templates/plannedsessionview.html | 9 ++--
rowers/tests/test_plans.py | 5 ++-
rowers/urls.py | 2 +
rowers/utils.py | 14 +++++++
rowers/views/importviews.py | 1 +
rowers/views/planviews.py | 48 +++++++++++++++++++--
11 files changed, 166 insertions(+), 53 deletions(-)
diff --git a/rowers/forms.py b/rowers/forms.py
index 506f900f..44469278 100644
--- a/rowers/forms.py
+++ b/rowers/forms.py
@@ -103,6 +103,7 @@ class InstantPlanSelectForm(forms.Form):
initial=timezone.now()+datetime.timedelta(days=21),
widget=AdminDateWidget(), # format='%Y-%m-%d'),
label='End Date')
+ plan_past_days = forms.BooleanField(initial=False, required=False, label='Insert sessions for the past')
target = forms.ChoiceField(required=False)
datechoice = forms.ChoiceField(choices=datechoices, initial='enddate', label='Plan by target, start or end date',
widget=forms.RadioSelect)
diff --git a/rowers/integrations/intervals.py b/rowers/integrations/intervals.py
index 5e51049a..8b0828b0 100644
--- a/rowers/integrations/intervals.py
+++ b/rowers/integrations/intervals.py
@@ -370,5 +370,37 @@ class IntervalsIntegration(SyncIntegration):
return data
-
+ def plannedsession_create(self, ps, *args, **kwargs):
+ _ = self.open()
+ r = self.rower
+
+ headers = {
+ 'Authorization': 'Bearer ' + r.intervals_token,
+ }
+
+ stepstext = ps.steps_intervals()
+
+ data = {
+ "start_date_local": ps.preferreddate.strftime('%Y-%m-%dT%H:%M:%S'),
+ "type": mytypes.intervalsmapping[ps.sessionsport],
+ "category": "WORKOUT",
+ "end_date_local": ps.preferreddate.strftime('%Y-%m-%d') + 'T23:59:59',
+ "name": ps.name,
+ "description": stepstext,
+ "indoor": ps.sessionsport in mytypes.ergtypes,
+ }
+
+ url = self.oauth_data['base_url'] + 'athlete/0/events'
+ response = requests.post(url, headers=headers, json=data)
+
+ if response.status_code != 200:
+ dologging('intervals.icu.log', response.text)
+ return 0
+
+ data = response.json()
+ id = data['id']
+ ps.intervalsid = id
+ ps.save()
+
+ return id
diff --git a/rowers/models.py b/rowers/models.py
index 71560594..d5755967 100644
--- a/rowers/models.py
+++ b/rowers/models.py
@@ -5,7 +5,7 @@ from rowers.courseutils import coordinate_in_path
from rowers.utils import (
# workflowleftpanel, workflowmiddlepanel,
defaultleft, defaultmiddle, landingpages, landingpages2,
- steps_read_fit, steps_write_fit, ps_dict_order, uniqify
+ steps_read_fit, steps_write_fit, steps_read_intervals, ps_dict_order, uniqify
)
from rowers.metrics import axlabels
from rowers.utils import geo_distance, move_one_meter
@@ -2000,12 +2000,18 @@ class TrainingPlan(models.Model):
delete_sessions = kwargs.pop('delete_sessions', False)
delete_all_sessions = kwargs.pop('delete_all_sessions', False)
if delete_sessions:
- sessions = PlannedSession.objects.filter(from_plan=self)
+ sessions = PlannedSession.objects.filter(from_plan=self).exclude(
+ sessiontype__in=['race','indoorrace']
+ )
for s in sessions:
s.delete()
if delete_all_sessions:
- sessions = PlannedSession.objects.filter(startdate__gte=self.startdate,enddate__lte=self.enddate)
+ sessions = PlannedSession.objects.filter(
+ startdate__gte=self.startdate,enddate__lte=self.enddate,manager=self.manager.user
+ ).exclude(
+ sessiontype__in=['race','indoorrace']
+ )
for s in sessions:
s.delete()
@@ -2940,6 +2946,10 @@ class PlannedSession(models.Model):
self.save()
+ def steps_intervals(self, *args, **kwargs):
+ s = steps_read_intervals(settings.MEDIA_ROOT+'/'+self.fitfile.name)
+ return s
+
def save(self, *args, **kwargs):
if self.sessionvalue <= 0: # pragma: no cover
self.sessionvalue = 1
diff --git a/rowers/plannedsessions.py b/rowers/plannedsessions.py
index e9298d77..7cadd5d7 100644
--- a/rowers/plannedsessions.py
+++ b/rowers/plannedsessions.py
@@ -1069,6 +1069,7 @@ def get_workouts_session(r, ps):
return ws
def create_sessions_from_json(plansteps, rower, startdate, manager, planbyrscore=False, plan=None,
+ plan_past_days=False,
asynchronous=False, queue=queue):
trainingdays = plansteps['trainingDays']
planstartdate = startdate
@@ -1087,28 +1088,27 @@ def create_sessions_from_json(plansteps, rower, startdate, manager, planbyrscore
if planbyrscore:
sessionmode = 'rScore'
- ps = PlannedSession(
- startdate=preferreddate -
- timedelta(days=preferreddate.weekday()),
- enddate=preferreddate +
- timedelta(days=-preferreddate.weekday()-1, weeks=1),
- preferreddate=preferreddate,
- sessionsport=sessionsport, # change this
- name=workout['workoutName'],
- steps=workout,
- manager=manager,
- sessionmode=sessionmode,
- comment=workout['description'],
- from_plan=plan,
- )
+ if plan_past_days or startdate >= timezone.now().date():
+ ps = PlannedSession(
+ startdate=preferreddate - timedelta(days=preferreddate.weekday()),
+ enddate=preferreddate + timedelta(days=-preferreddate.weekday()-1, weeks=1),
+ preferreddate=preferreddate,
+ sessionsport=sessionsport, # change this
+ name=workout['workoutName'],
+ steps=workout,
+ manager=manager,
+ sessionmode=sessionmode,
+ comment=workout['description'],
+ from_plan=plan,
+ )
+
+ ps.save()
- ps.save()
-
- add_rower_session(rower, ps)
+ add_rower_session(rower, ps)
return
# async version
- _ = myqueue(queue, create_sessions_from_json_async, plansteps, rower, startdate, manager, planbyrscore, plan)
+ _ = myqueue(queue, create_sessions_from_json_async, plansteps, rower, startdate, manager, planbyrscore, plan, plan_past_days)
diff --git a/rowers/tasks.py b/rowers/tasks.py
index 6dc30f32..c26f02fb 100644
--- a/rowers/tasks.py
+++ b/rowers/tasks.py
@@ -374,7 +374,7 @@ def handle_assignworkouts(workouts, rowers, remove_workout, debug=False, **kwarg
return 1
@app.task
-def create_sessions_from_json_async(plansteps, rower, startdate, manager, planbyrscore, plan, debug=False, **kwargs):
+def create_sessions_from_json_async(plansteps, rower, startdate, manager, planbyrscore, plan, plan_past_days, debug=False, **kwargs):
trainingdays = plansteps['trainingDays']
planstartdate = startdate
for day in trainingdays:
@@ -391,32 +391,39 @@ def create_sessions_from_json_async(plansteps, rower, startdate, manager, planby
if planbyrscore:
sessionmode = 'rScore'
- ps = PlannedSession(
- startdate=preferreddate -
- timedelta(days=preferreddate.weekday()),
- enddate=preferreddate +
- timedelta(days=-preferreddate.weekday()-1, weeks=1),
- preferreddate=preferreddate,
- sessionsport=sessionsport, # change this
- name=workout['workoutName'],
- steps=workout,
- manager=manager,
- sessionmode=sessionmode,
- comment=workout['description'],
- from_plan=plan,
- )
+ create_session = False
+ if plan_past_days:
+ create_session = True
+ elif preferreddate >= timezone.now().date():
+ create_session = True
- ps.save()
+ if create_session:
+ ps = PlannedSession(
+ startdate=preferreddate -
+ timedelta(days=preferreddate.weekday()),
+ enddate=preferreddate +
+ timedelta(days=-preferreddate.weekday()-1, weeks=1),
+ preferreddate=preferreddate,
+ sessionsport=sessionsport, # change this
+ name=workout['workoutName'],
+ steps=workout,
+ manager=manager,
+ sessionmode=sessionmode,
+ comment=workout['description'],
+ from_plan=plan,
+ )
- teams = Team.objects.filter(manager=ps.manager)
- members = Rower.objects.filter(team__in=teams).distinct()
- if rower in members and rower.rowerplan != 'freecoach':
- ps.rower.add(rower)
- ps.save()
- elif ps.manager.rower == rower and rower.rowerplan != 'freecoach':
- ps.rower.add(rower)
ps.save()
+ teams = Team.objects.filter(manager=ps.manager)
+ members = Rower.objects.filter(team__in=teams).distinct()
+ if rower in members and rower.rowerplan != 'freecoach':
+ ps.rower.add(rower)
+ ps.save()
+ elif ps.manager.rower == rower and rower.rowerplan != 'freecoach':
+ ps.rower.add(rower)
+ ps.save()
+
return 1
@app.task
diff --git a/rowers/templates/plannedsessionview.html b/rowers/templates/plannedsessionview.html
index 3313c733..8fe9b46c 100644
--- a/rowers/templates/plannedsessionview.html
+++ b/rowers/templates/plannedsessionview.html
@@ -23,6 +23,7 @@
{% else %}
Export to Garmin
{% endif %}
+ Export to intervals.icu
{% endif %}
Session {{ psdict.name.1 }}
@@ -46,10 +47,10 @@
{% endfor %}
{% endfor %}
- {% if steps %}
- Steps
- {{ steps|safe }}
- {% endif %}
+ {% if steps %}
+ Steps
+ {{ steps|safe }}
+ {% endif %}
{% if plannedsession.sessiontype == 'test' or plannedsession.sessiontype == 'coursetest' or plannedsession.sessiontype == 'fastest_distance' or plannedsession.sessiontype == 'fastest_time' %}
diff --git a/rowers/tests/test_plans.py b/rowers/tests/test_plans.py
index b8615a19..79d180f5 100644
--- a/rowers/tests/test_plans.py
+++ b/rowers/tests/test_plans.py
@@ -1852,7 +1852,10 @@ description: ""
response = self.c.get(url)
self.assertEqual(response.status_code,200)
- form = {}
+ form = {
+ 'delete_sessions': 1,
+ 'delete_all_sessions': 0,
+ }
response = self.c.post(url,form)
self.assertEqual(response.status_code,302)
diff --git a/rowers/urls.py b/rowers/urls.py
index 8f0f29b9..aa39292b 100644
--- a/rowers/urls.py
+++ b/rowers/urls.py
@@ -1015,6 +1015,8 @@ urlpatterns = [
name='plannedsession_totemplate_view'),
re_path(r'^sessions/(?P\d+)/togarmin/$', views.plannedsession_togarmin_view,
name='plannedsession_togarmin_view'),
+ re_path(r'^sessions/(?P\d+)/tointervals/$', views.plannedsession_tointervals_view,
+ name='plannedsession_tointervals_view'),
re_path(r'^sessions/(?P\d+)/compare/$',
views.plannedsession_compare_view,
name='plannedsession_compare_view'),
diff --git a/rowers/utils.py b/rowers/utils.py
index d40ba97a..e763564f 100644
--- a/rowers/utils.py
+++ b/rowers/utils.py
@@ -717,6 +717,20 @@ def steps_read_fit(filename, name='', sport='Custom'): # pragma: no cover
return d
+def steps_read_intervals(filename, name='', sport='Custom'): # pragma: no cover
+ authorizationstring = 'Bearer '+settings.WORKOUTS_FIT_TOKEN
+ url = settings.WORKOUTS_FIT_URL+"/tointervals"
+ headers = {'Authorization': authorizationstring}
+
+ response = requests.post(url=url, headers=headers,
+ json={'filename': filename})
+
+ if response.status_code != 200: # pragma: no cover
+ return None
+
+ w = response.text
+
+ return w
def steps_write_fit(steps):
authorizationstring = 'Bearer '+settings.WORKOUTS_FIT_TOKEN
diff --git a/rowers/views/importviews.py b/rowers/views/importviews.py
index 63e49742..c1d05ef9 100644
--- a/rowers/views/importviews.py
+++ b/rowers/views/importviews.py
@@ -7,6 +7,7 @@ from rowers.views.statements import *
from rowers.plannedsessions import get_dates_timeperiod
from rowers.tasks import fetch_strava_workout
from rowers.utils import NoTokenError
+from rowers.models import PlannedSession
import rowers.integrations.strava as strava
from rowers.integrations import importsources
diff --git a/rowers/views/planviews.py b/rowers/views/planviews.py
index e7731516..5ea72dbb 100644
--- a/rowers/views/planviews.py
+++ b/rowers/views/planviews.py
@@ -2052,6 +2052,41 @@ def plannedsession_templateedit_view(request, id=0):
'steps': steps,
})
+@permission_required('plannedsession.change_session', fn=get_session_by_pk, raise_exception=True)
+@user_passes_test(can_plan, login_url="/rowers/paidplans/",
+ message="This functionality requires a Coach or Self-Coach plan",
+ redirect_field_name=None)
+def plannedsession_tointervals_view(request, id=0):
+
+ r = getrequestplanrower(request)
+
+ startdate, enddate = get_dates_timeperiod(request)
+ startdate = startdate.date()
+ enddate = enddate.date()
+
+ ps = get_object_or_404(PlannedSession, pk=id)
+
+ intervals = IntervalsIntegration(request.user)
+ result = intervals.plannedsession_create(ps)
+
+ if not result: # pragma: no cover
+ messages.error(
+ request, 'You failed to export your session to Intervals')
+ else:
+ messages.info(
+ request, 'Session is now on Intervals.')
+
+ url = reverse(plannedsession_view, kwargs={'userid': r.user.id,
+ 'id': ps.id, })
+
+ startdatestring = startdate.strftime('%Y-%m-%d')
+ enddatestring = enddate.strftime('%Y-%m-%d')
+ url += '?when='+startdatestring+'/'+enddatestring
+
+ next = request.GET.get('next', url)
+
+ return HttpResponseRedirect(next)
+
@permission_required('plannedsession.change_session', fn=get_session_by_pk, raise_exception=True)
@user_passes_test(can_plan, login_url="/rowers/paidplans/",
@@ -2486,6 +2521,7 @@ def plannedsession_view(request, id=0, userid=0):
if ps.steps: # pragma: no cover
d = ps.steps
steps = ps_dict_get_description_html(d, short=False)
+ steps_intervals = ps.steps_intervals()
return render(request, 'plannedsessionview.html',
{
@@ -2516,6 +2552,7 @@ def plannedsession_view(request, id=0, userid=0):
'coursediv': coursediv,
'comments': comments,
'steps': steps,
+ 'steps_intervals': steps_intervals,
}
)
@@ -2707,6 +2744,7 @@ def rower_view_instantplan(request, id='', userid=0):
startdate = form.cleaned_data['startdate']
notes = form.cleaned_data['notes']
datechoice = form.cleaned_data['datechoice']
+ plan_past_days = form.cleaned_data['plan_past_days']
status = True
if target and datechoice == 'target': # pragma: no cover
@@ -2726,10 +2764,14 @@ def rower_view_instantplan(request, id='', userid=0):
notes=notes,
)
+ if not plan_past_days:
+ p.startdate = timezone.now().date()
+
p.save()
p.rowers.add(r)
- create_sessions_from_json(plansteps, r, startdate, r.user, planbyrscore=byrscore, plan=p, asynchronous=True)
+ create_sessions_from_json(plansteps, r, startdate, r.user, planbyrscore=byrscore,
+ plan=p, plan_past_days = plan_past_days, asynchronous=True)
messages.info(request, 'Your Sessions have been added')
@@ -3336,8 +3378,8 @@ class TrainingPlanDelete(DeleteView):
success_url = reverse_lazy(rower_create_trainingplan)
def post(self, request, *args, **kwargs):
- delete_sessions = request.POST.get('delete_sessions')
- delete_all_sessions = request.POST.get('delete_all_sessions')
+ delete_sessions = request.POST.get('delete_sessions',0)
+ delete_all_sessions = request.POST.get('delete_all_sessions',0)
self.object = self.get_object()
self.object.delete(delete_sessions=delete_sessions, delete_all_sessions=delete_all_sessions)
return HttpResponseRedirect(self.get_success_url())