Private
Public Access
1
0

Merge branch 'develop' into feature/idoklad

This commit is contained in:
2024-12-22 16:51:14 +01:00
18 changed files with 520 additions and 60 deletions

View File

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

View File

@@ -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

View File

@@ -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):

View File

@@ -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):

View File

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

View File

@@ -0,0 +1,45 @@
{% extends "newbase.html" %}
{% load static %}
{% load rowerfilters %}
{% block title %}Sessions on intervals.icu{% endblock %}
{% block main %}
<h1>Sessions on Intervals.icu</h1>
{% if sessions %}
<ul class="main-content">
<li class="grid_4">
<form enctype="multipart/form-data" method="post">
{% csrf_token %}
<input type="submit" name="action" value="Import selected sessions">
<table width="70%" class="listtable">
<thead>
<tr>
<th>Import</th>
<th>Date</th>
<th>Name</th>
<th>Description</th>
<th>Type</th>
<th>Training Load</th>
</tr>
</thead>
<tbody>
{% for session in sessions %}
<tr>
<td>
<input type="checkbox" name="session" value="{{ session.id }}">
</td>
<td>{{ session.start_date_local }}</td>
<td>{{ session.name }}</td>
<td>{{ session.description }}</td>
<td>{{ session.type }}</td>
<td>{{ session.icu_training_load }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</form>
</li>
</ul>
{% endif %}
{% endblock %}

View File

@@ -71,6 +71,11 @@
<i class="fas fa-cloud-download fa-fw"></i>&nbsp;Import from Rojabo
</a>
</li>
<li id="import-intervals">
<a href="/rowers/session/intervalsimport/">
<i class="fa-solid fa-wave-pulse fa-fw"></i>&nbsp;Import from Intervals
</a>
</li>
<li id="plan-microcycle">
<a href="/rowers/sessions/multicreate/?when={{ timeperiod }}">
<i class="fas fa-expand fa-fw"></i>Plan Microcycle

View File

@@ -23,6 +23,11 @@
{% else %}
<a href="/rowers/sessions/{{ psdict.id.1 }}/togarmin/?next={{ request.path|urlencode }}"><i class="fas fa-watch-fitness fa-fw"></i> Export to Garmin</a>
{% endif %}
{% if plannedsession.intervals_icu_id %}
<a href="https://intervals.icu/?w={{ plannedsession.preferreddate }}"><i class="fa-solid fa-wave-pulse"></i> Exported to intervals.icu</a>
{% else %}
<a href="/rowers/sessions/{{ psdict.id.1 }}/tointervals/?next={{ request.path|urlencode }}"><i class="fa-solid fa-wave-pulse fa-fw"></i> Export to intervals.icu</a>
{% endif %}
</p>
{% endif %}
<h1>Session {{ psdict.name.1 }}</h1>
@@ -46,10 +51,10 @@
{% endfor %}
{% endfor %}
</table>
{% if steps %}
<h2>Steps</h2>
<p>{{ steps|safe }}</p>
{% endif %}
{% if steps %}
<h2>Steps</h2>
<p>{{ steps|safe }}</p>
{% endif %}
</li>
<li class="grid_2">
{% if plannedsession.sessiontype == 'test' or plannedsession.sessiontype == 'coursetest' or plannedsession.sessiontype == 'fastest_distance' or plannedsession.sessiontype == 'fastest_time' %}

View File

@@ -13,7 +13,15 @@
<form action="" method="post">
{% csrf_token %}
<p>Are you sure you want to delete <em>{{ object }}</em>?</p>
<input class="button red" type="submit" value="Confirm">
<p>
<input type="checkbox" name="delete_sessions" value="1"> Delete all planned sessions linked to the plan
</p>
<p>
<input type="checkbox" name="delete_all_sessions" value="1">Delete all planned sessions in the plan time frame
</p>
<p>
<input class="button red" type="submit" value="Confirm">
</p>
</form>

View File

@@ -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

View File

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

View File

@@ -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

View File

@@ -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<source>\w+.*)import/$',
views.workout_import_view, name='workout_import_view'),
re_path(r'^workout/(?P<source>\w+.*)import/(?P<externalid>\d+)/$',
@@ -1013,6 +1015,8 @@ urlpatterns = [
name='plannedsession_totemplate_view'),
re_path(r'^sessions/(?P<id>\d+)/togarmin/$', views.plannedsession_togarmin_view,
name='plannedsession_togarmin_view'),
re_path(r'^sessions/(?P<id>\d+)/tointervals/$', views.plannedsession_tointervals_view,
name='plannedsession_tointervals_view'),
re_path(r'^sessions/(?P<id>\d+)/compare/$',
views.plannedsession_compare_view,
name='plannedsession_compare_view'),

View File

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

View File

@@ -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",

View File

@@ -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

View File

@@ -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','')