Merge branch 'develop' into feature/idoklad
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
45
rowers/templates/intervals_list_import.html
Normal file
45
rowers/templates/intervals_list_import.html
Normal 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 %}
|
||||
@@ -71,6 +71,11 @@
|
||||
<i class="fas fa-cloud-download fa-fw"></i> Import from Rojabo
|
||||
</a>
|
||||
</li>
|
||||
<li id="import-intervals">
|
||||
<a href="/rowers/session/intervalsimport/">
|
||||
<i class="fa-solid fa-wave-pulse fa-fw"></i> 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
|
||||
|
||||
@@ -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' %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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','')
|
||||
|
||||
Reference in New Issue
Block a user