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())