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 66fdb0d5..042b5eff 100644 --- a/rowers/integrations/intervals.py +++ b/rowers/integrations/intervals.py @@ -65,7 +65,7 @@ class IntervalsIntegration(SyncIntegration): 'base_url': 'https://intervals.icu/api/v1/', 'grant_type': 'refresh_token', 'headers': headers, - 'scope': 'ACTIVITY:WRITE, LIBRARY:READ', + 'scope': 'ACTIVITY:WRITE, LIBRARY:READ, CALENDAR:WRITE', } def get_token(self, code, *args, **kwargs): @@ -315,5 +315,130 @@ class IntervalsIntegration(SyncIntegration): def token_refresh(self, *args, **kwargs): return super(IntervalsIntegration, self).token_refresh(*args, **kwargs) + def get_plannedsessions_list(self, *args, **kwargs): + _ = self.open() + r = self.rower + + headers = { + 'Authorization': 'Bearer ' + r.intervals_token, + } + + # first get the folders - we need the folder id for the next call + oldest = (timezone.now() - timedelta(days=30)).strftime('%Y-%m-%d') + newest = (timezone.now() + timedelta(days=30)).strftime('%Y-%m-%d') + url = self.oauth_data['base_url'] + 'athlete/0/events' #'?category=WORKOUT' + url += '?oldest=' + oldest + '&newest=' + newest + response = requests.get(url, headers=headers) + if response.status_code != 200: + return [] + + data = response.json() + + return data + + def get_plannedsession(self, id, *args, **kwargs): + _ = self.open() + r = self.rower + + url = self.oauth_data['base_url'] + 'athlete/0/events/' + str(id) + headers = { + 'Authorization': 'Bearer ' + r.intervals_token, + } + response = requests.get(url, headers=headers) + + if response.status_code != 200: + dologging('intervals.icu.log', response.text) + return 0 + + data = response.json() + + # get file from athlete/0/events/{id}/downloadfit + if data['category'] == 'WORKOUT': + url = self.oauth_data['base_url'] + 'athlete/0/events/' + str(id) + '/downloadfit' + response = requests.get(url, headers=headers) + if response.status_code != 200: + dologging('intervals.icu.log', response.text) + return 0 + + filename = 'planned_' + str(id) + '.fit' + filename2 = 'media/planned_' + str(id) + '.fit' + with open(filename2, 'wb') as f: + f.write(response.content) + + data['fitfile'] = filename + + + return data + + def plannedsession_create(self, ps, *args, **kwargs): + _ = self.open() + r = self.rower + + headers = { + 'Authorization': 'Bearer ' + r.intervals_token, + } + + stepstext = ps.steps_intervals() + + category = 'WORKOUT' + startdate = ps.preferreddate.strftime('%Y-%m-%dT%H:%M:%S') + enddate = ps.preferreddate.strftime('%Y-%m-%d') + 'T23:59:59' + if ps.sessiontype == 'cycletarget': + category = 'TARGET' + startdate = ps.startdate.strftime('%Y-%m-%dT%H:%M:%S') + enddate = ps.enddate.strftime('%Y-%m-%d') + 'T23:59:59' + + data = { + "start_date_local": startdate, + "type": mytypes.intervalsmapping[ps.sessionsport], + "category": category, + "end_date_local": enddate, + "name": ps.name, + "description": stepstext, + "indoor": ps.sessionsport in mytypes.ergtypes, + } + + if ps.sessiontype == 'cycletarget': + if ps.sessionmode == 'time': + data['time_target'] = ps.sessionvalue*60 + elif ps.sessionmode == 'distance': + data['distance_target'] = ps.sessionvalue + elif ps.sessionmode == 'rScore': + data['load_target'] = ps.sessionvalue + elif ps.sessionmode == 'Trimp': + data['load_target'] = ps.sessionvalue/2. + + 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.intervals_icu_id = id + ps.save() + + return id - + def plannedsession_delete(self, ps, *args, **kwargs): + _ = self.open() + r = self.rower + + headers = { + 'Authorization': 'Bearer ' + r.intervals_token, + } + + url = self.oauth_data['base_url'] + 'athlete/0/events/' + str(ps.intervals_icu_id) + + response = requests.delete(url, headers=headers) + + if response.status_code != 200: + dologging('intervals.icu.log', response.text) + return 0 + + ps.intervals_icu_id = None + ps.save() + + return 1 diff --git a/rowers/models.py b/rowers/models.py index c67ad31c..b19622bc 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -5,7 +5,8 @@ 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, + dologging ) from rowers.metrics import axlabels from rowers.utils import geo_distance, move_one_meter @@ -1174,6 +1175,7 @@ class Rower(models.Model): c2_auto_import = models.BooleanField(default=False) intervals_auto_export = models.BooleanField(default=False) intervals_auto_import = models.BooleanField(default=False) + intervals_delete_plannedsession = models.BooleanField(default=False, verbose_name="Deleting planned session deletes it on intervals.icu") intervals_resample_to_1s = models.BooleanField(default=False, verbose_name='Resample to 1s on export') sporttrackstoken = models.CharField( default='', max_length=200, blank=True, null=True) @@ -1246,6 +1248,7 @@ class Rower(models.Model): intervals_token = models.CharField( default='', max_length=200, blank=True, null=True) intervals_owner_id = models.CharField(default='', max_length=200,blank=True, null=True) + privacychoices = ( ('visible', 'Visible'), @@ -1998,6 +2001,27 @@ class TrainingPlan(models.Model): return stri + def delete(self, *args, **kwargs): + 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).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,manager=self.manager.user + ).exclude( + sessiontype__in=['race','indoorrace'] + ) + for s in sessions: + s.delete() + + super().delete(*args, **kwargs) + def save(self, *args, **kwargs): manager = self.manager if not can_add_plan(manager.user): # pragma: no cover @@ -2675,9 +2699,15 @@ class PlannedSessionStep(models.Model): targettypes = ( ("Speed", "Speed"), + ("SpeedLap", "SpeedLap"), ("HeartRate", "HeartRate"), + ("HeartRateLap", "HeartRateLap"), ("Cadence", "Cadence"), - ("Power", "Power") + ("CadenceLap", "CadenceLap"), + ("Power", "Power"), + ("PowerLap", "PowerLap"), + ("Distance", "Distance"), + ("Duration", "Duration"), ) manager = models.ForeignKey(User, null=True, on_delete=models.CASCADE) @@ -2815,8 +2845,9 @@ class PlannedSession(models.Model): ('None', None), ) - manager = models.ForeignKey(User, on_delete=models.PROTECT) + manager = models.ForeignKey(User, on_delete=models.CASCADE) rojabo_id = models.BigIntegerField(default=0,blank=True) + intervals_icu_id = models.CharField(default=None, blank=True, null=True, max_length=50) course = models.ForeignKey(GeoCourse, blank=True, null=True, verbose_name='OTW Course', on_delete=models.SET_NULL) @@ -2895,6 +2926,8 @@ class PlannedSession(models.Model): garmin_schedule_id = models.BigIntegerField(default=0) tags = TaggableManager(blank=True) + + from_plan = models.ForeignKey(TrainingPlan, on_delete=models.SET_NULL, null=True, blank=True) def __str__(self): @@ -2918,6 +2951,23 @@ 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 delete(self, *args, **kwargs): + r = self.manager.rower + if self.intervals_icu_id and r.intervals_delete_plannedsession: + headers = { + 'Authorization': 'Bearer '+ r.intervals_token + } + url = 'https://intervals.icu/api/v1/athlete/0/events/'+str(self.intervals_icu_id) + response = requests.delete(url, headers=headers) + if response.status_code != 200: + dologging('intervals.icu.log', response.text) + + super(PlannedSession, self).delete(*args, **kwargs) + def save(self, *args, **kwargs): if self.sessionvalue <= 0: # pragma: no cover self.sessionvalue = 1 @@ -3156,7 +3206,7 @@ class VirtualRace(PlannedSession): class RaceLogo(models.Model): filename = models.CharField(default='', max_length=150) creationdatetime = models.DateTimeField() - user = models.ForeignKey(User, on_delete=models.PROTECT) + user = models.ForeignKey(User, on_delete=models.CASCADE) width = models.IntegerField(default=1200) height = models.IntegerField(default=600) race = models.ManyToManyField(VirtualRace, related_name='logos') @@ -4594,6 +4644,7 @@ class RowerExportForm(ModelForm): 'rp3_auto_import', 'intervals_auto_import', 'intervals_auto_export', + 'intervals_delete_plannedsession', 'intervals_resample_to_1s', 'imports_are_private' ] @@ -4622,6 +4673,7 @@ class RowerExportFormIntervals(ModelForm): 'intervals_auto_import', 'intervals_auto_export', 'intervals_resample_to_1s', + 'intervals_delete_plannedsession', ] class RowerExportFormGarmin(ModelForm): diff --git a/rowers/plannedsessions.py b/rowers/plannedsessions.py index 1918d750..7cadd5d7 100644 --- a/rowers/plannedsessions.py +++ b/rowers/plannedsessions.py @@ -9,7 +9,7 @@ from rowers.models import ( from rowers.tasks import ( handle_sendemail_raceregistration, handle_sendemail_racesubmission ) -from rowers.tasks import handle_check_race_course +from rowers.tasks import handle_check_race_course, create_sessions_from_json_async from iso8601 import ParseError import iso8601 import rowers.courses as courses @@ -1068,40 +1068,48 @@ def get_workouts_session(r, ps): return ws -def create_sessions_from_json(plansteps, rower, startdate, manager, planbyrscore=False): +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 - for day in trainingdays: - for workout in day['workouts']: - sessionsport = 'water' - try: - sessionsport = mytypes.fitmappinginv[workout['sport'].lower()] - except KeyError: - pass + if not asynchronous: + for day in trainingdays: + for workout in day['workouts']: + sessionsport = 'water' + try: + sessionsport = mytypes.fitmappinginv[workout['sport'].lower()] + except KeyError: + pass - preferreddate = planstartdate+timedelta(days=day['order']) + preferreddate = planstartdate+timedelta(days=day['order']) - sessionmode = 'time' - if planbyrscore: - sessionmode = 'rScore' + sessionmode = 'time' + 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'] - ) + 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, plan_past_days) + def update_plannedsession(ps, cd): diff --git a/rowers/tasks.py b/rowers/tasks.py index ebadcfee..c26f02fb 100644 --- a/rowers/tasks.py +++ b/rowers/tasks.py @@ -15,7 +15,7 @@ application = get_wsgi_application() from rowers.models import ( Workout, GeoPolygon, GeoPoint, GeoCourse, VirtualRaceResult, CourseTestResult, Rower, - GraphImage + GraphImage, Team, PlannedSession ) from rowers.session_utils import is_session_complete import math @@ -373,6 +373,59 @@ 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, plan_past_days, debug=False, **kwargs): + trainingdays = plansteps['trainingDays'] + planstartdate = startdate + for day in trainingdays: + for workout in day['workouts']: + sessionsport = 'water' + try: + sessionsport = mytypes.fitmappinginv[workout['sport'].lower()] + except KeyError: + pass + + preferreddate = planstartdate+timedelta(days=day['order']) + + sessionmode = 'time' + if planbyrscore: + sessionmode = 'rScore' + + create_session = False + if plan_past_days: + create_session = True + elif preferreddate >= timezone.now().date(): + create_session = True + + 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, + ) + + 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 def handle_post_workout_api(uploadoptions, debug=False, **kwargs): # pragma: no cover session = requests.session() diff --git a/rowers/templates/intervals_list_import.html b/rowers/templates/intervals_list_import.html new file mode 100644 index 00000000..6f1a8a12 --- /dev/null +++ b/rowers/templates/intervals_list_import.html @@ -0,0 +1,45 @@ +{% extends "newbase.html" %} +{% load static %} +{% load rowerfilters %} + +{% block title %}Sessions on intervals.icu{% endblock %} + +{% block main %} +

Sessions on Intervals.icu

+{% if sessions %} +
    +
  • +
    + {% csrf_token %} + + + + + + + + + + + + + + {% for session in sessions %} + + + + + + + + + {% endfor %} + +
    ImportDateNameDescriptionTypeTraining Load
    + + {{ session.start_date_local }}{{ session.name }}{{ session.description }}{{ session.type }}{{ session.icu_training_load }}
    +
    +
  • +
+{% endif %} +{% endblock %} diff --git a/rowers/templates/menu_plan.html b/rowers/templates/menu_plan.html index 4ff3f5bf..a81537e7 100644 --- a/rowers/templates/menu_plan.html +++ b/rowers/templates/menu_plan.html @@ -71,6 +71,11 @@  Import from Rojabo +
  • + +  Import from Intervals + +
  • Plan Microcycle diff --git a/rowers/templates/plannedsessionview.html b/rowers/templates/plannedsessionview.html index 3313c733..78c696e3 100644 --- a/rowers/templates/plannedsessionview.html +++ b/rowers/templates/plannedsessionview.html @@ -23,6 +23,11 @@ {% else %} Export to Garmin {% endif %} + {% if plannedsession.intervals_icu_id %} + Exported to intervals.icu + {% else %} + Export to intervals.icu + {% endif %}

    {% endif %}

    Session {{ psdict.name.1 }}

    @@ -46,10 +51,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/templates/trainingplan_delete.html b/rowers/templates/trainingplan_delete.html index 045411e4..adaa75a7 100644 --- a/rowers/templates/trainingplan_delete.html +++ b/rowers/templates/trainingplan_delete.html @@ -13,7 +13,15 @@
    {% csrf_token %}

    Are you sure you want to delete {{ object }}?

    - +

    + Delete all planned sessions linked to the plan +

    +

    + Delete all planned sessions in the plan time frame +

    +

    + +

    diff --git a/rowers/tests/mocks.py b/rowers/tests/mocks.py index 512b2429..a3a1effc 100644 --- a/rowers/tests/mocks.py +++ b/rowers/tests/mocks.py @@ -1042,6 +1042,10 @@ def mocked_requests(*args, **kwargs): class MockSession: + def __init__(self): + self.status_code = 200 + self.text = "- 20m 200W" + class headers: def __init__(self,*args,**kwargs): # pragma: no cover pass 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/uploads.py b/rowers/uploads.py index a0f76b7f..48f54ede 100644 --- a/rowers/uploads.py +++ b/rowers/uploads.py @@ -167,7 +167,7 @@ def do_sync(w, options, quick=False): #dologging("uploads.log", "do_icu_export: {do_icu_export}".format(do_icu_export=do_icu_export)) try: - if options['intervalsid'] != 0 and options['intervalsid'] != '': # pragma: no cover + if options['intervalsid'] != '': # pragma: no cover w.uploadedtointervals = options['intervalsid'] # upload_to_icu = False do_icu_export = False diff --git a/rowers/urls.py b/rowers/urls.py index 8d673423..aa39292b 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -80,7 +80,7 @@ class WorkoutViewSet(viewsets.ModelViewSet): def get_queryset(self): # pragma: no cover try: r = Rower.objects.get(user=self.request.user) - #return Workout.objects.filter(user=r).order_by("-date", "-starttime") + #return Workout.objects.filter(user=r).exclude(workoutsource='strava').order_by("-date", "-starttime") return Workout.objects.filter(user=r).exclude(workoutsource='strava').order_by("-date", "-starttime") except TypeError: return [] @@ -631,6 +631,8 @@ urlpatterns = [ views.workout_undo_smoothenpace_view, name='workout_undo_smoothenpace_view'), re_path(r'^session/rojaboimport/$', views.workout_rojaboimport_view, name='workout_rojaboimport_view'), + re_path(r'^session/intervalsimport/$', views.plannedsession_intervalsimport_view, + name='plannedsession_intervalsimport_view'), re_path(r'^workout/(?P\w+.*)import/$', views.workout_import_view, name='workout_import_view'), re_path(r'^workout/(?P\w+.*)import/(?P\d+)/$', @@ -1013,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 f403630c..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 @@ -774,7 +788,7 @@ def step_to_time_dist(step, avgspeed=3.2, ftp=200, ftspm=25, ftv=3.7, powerzones rscoreperhour = 100.*veloratio rscore = rscoreperhour*seconds/3600. - if targettype == 'Power': + if targettype in ['Power','PowerLap']: value = step.get('targetValue', 0) valuelow = step.get('targetValueLow', 0) valuehigh = step.get('targetValueHigh', 0) @@ -793,12 +807,19 @@ def step_to_time_dist(step, avgspeed=3.2, ftp=200, ftspm=25, ftv=3.7, powerzones avgpower = targetpower if valuelow != 0 and valuehigh != 0: # pragma: no cover avgpower = (valuelow+valuehigh)/2. - avgspeed = ftv*(avgpower/ftp)**(1./3.) + if avgpower < 10 and avgpower > 0: + targetpower = ftp*0.6 + elif avgpower > 10 and avgpower < 1000: + targetpower = avgpower*ftp/100. + elif avgpower > 1000: + targetpower = avgpower-1000 + + avgspeed = ftv*(targetpower/ftp)**(1./3.) distance = avgspeed*seconds - rscore = 100.*(avgpower/ftp)*seconds/3600. + rscore = 100.*(targetpower/ftp)*seconds/3600. - if targettype == 'Cadence': + if targettype in ['Cadence','CadenceLap']: value = step.get('targetValue', 0) valuelow = step.get('targetValueLow', 0) valuehigh = step.get('targetValueHigh', 0) @@ -820,7 +841,7 @@ def step_to_time_dist(step, avgspeed=3.2, ftp=200, ftspm=25, ftv=3.7, powerzones seconds = distance/avgspeed rscore = 60.*float(seconds)/3600. - if targettype == 'Speed': # pragma: no cover + if targettype in ['Speed', 'SpeedLap']: # pragma: no cover value = step.get('targetValue', 0) valuelow = step.get('targetValueLow', 0) valuehigh = step.get('targetValueHigh', 0) @@ -839,7 +860,7 @@ def step_to_time_dist(step, avgspeed=3.2, ftp=200, ftspm=25, ftv=3.7, powerzones if velomid > 0: seconds = distance/velomid - if targettype == 'Power': # pragma: no cover + if targettype in ['Power','PowerLap']: # pragma: no cover value = step.get('targetValue', 0) valuelow = step.get('targetValueLow', 0) valuehigh = step.get('targetValueHigh', 0) @@ -856,12 +877,20 @@ def step_to_time_dist(step, avgspeed=3.2, ftp=200, ftspm=25, ftv=3.7, powerzones avgpower = targetpower if valuelow != 0 and valuehigh != 0: avgpower = (valuelow+valuehigh)/2. - avgspeed = ftv*(avgpower/ftp)**(1./3.) + if avgpower < 10 and avgpower > 0: + targetpower = ftp*0.6 + elif avgpower > 10 and avgpower < 1000: + targetpower = avgpower*ftp/100. + elif avgpower > 1000: + targetpower = avgpower-1000 + + avgspeed = ftv*(targetpower/ftp)**(1./3.) seconds = distance/avgspeed - rscore = 100.*(avgpower/ftp)*seconds/3600. - if targettype == 'Cadence': # pragma: no cover + rscore = 100.*(targetpower/ftp)*seconds/3600. + + if targettype in ['Cadence','CadenceLap']: # pragma: no cover value = step.get('targetValue', 0) valuelow = step.get('targetValueLow', 0) valuehigh = step.get('targetValueHigh', 0) @@ -1189,7 +1218,7 @@ def step_to_string(step, short=False): except KeyError: targettype = None - if targettype == 'HeartRate': # pragma: no cover + if targettype in ['HeartRate','HeartRateLap']: # pragma: no cover value = step.get('targetValue', 0) valuelow = step.get('targetValueLow', 0) valuehigh = step.get('targetValueHigh', 0) @@ -1207,7 +1236,7 @@ def step_to_string(step, short=False): l=valuelow - 100, h=valuehigh - 100, ) - elif targettype == 'Power': # pragma: no cover + elif targettype in ['Power', 'PowerLap']: # pragma: no cover value = step.get('targetValue', 0) valuelow = step.get('targetValueLow', 0) valuehigh = step.get('targetValueHigh', 0) @@ -1229,7 +1258,7 @@ def step_to_string(step, short=False): l=valuelow-1000, h=valuehigh-1000, ) - elif targettype == 'Speed': # pragma: no cover + elif targettype in ['Speed', 'SpeedLap']: # pragma: no cover value = step.get('targetValue', 0) valuelow = step.get('targetValueLow', 0) @@ -1264,7 +1293,7 @@ def step_to_string(step, short=False): pl=pacestringlow, ph=pacestringhigh, ) - elif targettype == 'Cadence': # pragma: no cover + elif targettype in ['Cadence','CadenceLap']: # pragma: no cover value = step.get('targetValue', 0) valuelow = step.get('targetValueLow', 0) valuehigh = step.get('targetValueHigh', 0) diff --git a/rowers/views/importviews.py b/rowers/views/importviews.py index a02527b0..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 @@ -681,8 +682,73 @@ def rower_process_testcallback(request): # pragma: no cover return HttpResponse(text) +# view to list planned sessions from intervals.icu +@login_required() +@user_passes_test(isplanmember, login_url="/rowers/paidplans/", + message="This functionality requires a Self-coach plan or higher", + redirect_field_name=None) +def plannedsession_intervalsimport_view(request, message="", userid=0): + r = getrequestrower(request, userid=userid) + if r.user != request.user: + messages.error( + request, 'You can only access your own workouts on Intervals.icu, not those of your athletes') + url = reverse('plannedsession_intervalsimport_view', + kwargs={'userid': request.user.id}) + return HttpResponseRedirect(url) + + integration = importsources['intervals'](request.user) + sessions_list = integration.get_plannedsessions_list() + + if request.method == 'POST': # pragma: no cover + tdict = dict(request.POST.lists()) + sessionids = [id for id in tdict['session']] + for sessionid in sessionids: + sessiondata = integration.get_plannedsession(sessionid) + if sessiondata['description'] is None: + sessiondata['description'] = '' + if sessiondata: + timetarget = sessiondata['time_target'] + if timetarget is None: + timetarget = sessiondata['moving_time'] + if timetarget is None: + timetarget = 3600 + timetarget = int(timetarget)/60. + ps = PlannedSession( + name=sessiondata['name'], + comment=sessiondata['description'], + sessionmode='time', + sessionvalue=timetarget, + startdate=arrow.get(sessiondata['start_date_local']).datetime, + enddate=arrow.get(sessiondata['end_date_local']).datetime, + preferreddate=arrow.get(sessiondata['start_date_local']).datetime, + sessionsport=mytypes.intervalsmappinginv[sessiondata['type']], + sessiontype='session', + intervals_icu_id=sessiondata['id'], + manager=request.user, + ) + ps.save() + ps.rower.add(r) + if sessiondata['category'].lower() == 'workout': + ps.fitfile = sessiondata['fitfile'] + ps.save() + ps.update_steps() + if sessiondata['category'].lower() == 'target': + ps.sessiontype = 'cycletarget' + ps.sessionvalue = int(sessiondata['time_target'])/60. + ps.enddate = ps.startdate + datetime.timedelta(days=6) + ps.save() + url = reverse('plannedsessions_view') + return HttpResponseRedirect(url) + + + return render(request, 'intervals_list_import.html', + { + 'sessions': sessions_list, + 'rower': r, + 'active': 'nav-plans', + }) + -# The page where you select which Strava workout to import @login_required() @user_passes_test(isplanmember, login_url="/rowers/paidplans/", message="This functionality requires a Self-coach plan or higher", diff --git a/rowers/views/planviews.py b/rowers/views/planviews.py index eb0db639..b88d29a5 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/", @@ -2483,9 +2518,11 @@ def plannedsession_view(request, id=0, userid=0): plannedsession=ps).order_by("created") steps = '' + steps_intervals = '' 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 +2553,7 @@ def plannedsession_view(request, id=0, userid=0): 'coursediv': coursediv, 'comments': comments, 'steps': steps, + 'steps_intervals': steps_intervals, } ) @@ -2707,6 +2745,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 +2765,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) + 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') @@ -3335,6 +3378,13 @@ class TrainingPlanDelete(DeleteView): template_name = 'trainingplan_delete.html' success_url = reverse_lazy(rower_create_trainingplan) + def post(self, request, *args, **kwargs): + 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()) + def get_object(self, *args, **kwargs): obj = super(TrainingPlanDelete, self).get_object(*args, **kwargs) if not can_delete_plan(self.request.user, obj): # pragma: no cover diff --git a/rowers/views/workoutviews.py b/rowers/views/workoutviews.py index 98257c99..26ec16f8 100644 --- a/rowers/views/workoutviews.py +++ b/rowers/views/workoutviews.py @@ -4976,6 +4976,7 @@ def workout_upload_api(request): # sync related IDs sporttracksid = post_data.get('sporttracksid','') + intervalsid = post_data.get('intervalsid','') c2id = post_data.get('c2id', '') garminid = post_data.get('garminid','') workoutid = post_data.get('id','') diff --git a/templates/newbase.html b/templates/newbase.html index cfcf93b7..206d68c0 100644 --- a/templates/newbase.html +++ b/templates/newbase.html @@ -41,7 +41,7 @@ - + @@ -63,6 +63,7 @@ +