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 %}
+
+{% 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 @@
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 @@
+