Merge branch 'release/v23.5.0'
This commit is contained in:
@@ -0,0 +1,112 @@
|
|||||||
|
import polars as pl
|
||||||
|
import datetime
|
||||||
|
import arrow
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from rowers.dataprep import rscore_approx
|
||||||
|
|
||||||
|
from rowers.models import Rower, Workout
|
||||||
|
|
||||||
|
def weightfromrecord(row,metricchoice):
|
||||||
|
vv = row[metricchoice][0]
|
||||||
|
if vv > 0:
|
||||||
|
return vv
|
||||||
|
if metricchoice == 'rscore': # pragma: no cover
|
||||||
|
return rscore_approx(row)
|
||||||
|
|
||||||
|
return 0 # pragma: no cover
|
||||||
|
|
||||||
|
|
||||||
|
def getfatigues(df, fatigues, fitnesses, dates, impulses, startdate, kfatigue, kfitness):
|
||||||
|
fatigue = 0
|
||||||
|
fitness = 0
|
||||||
|
impulses = []
|
||||||
|
|
||||||
|
for f in fatigues:
|
||||||
|
impulses.append(0)
|
||||||
|
|
||||||
|
lambda_a = 2/(kfatigue+1)
|
||||||
|
lambda_c = 2/(kfitness+1)
|
||||||
|
|
||||||
|
enddate = datetime.date.today()
|
||||||
|
nrdays = (enddate - startdate).days
|
||||||
|
|
||||||
|
for i in range(nrdays+1):
|
||||||
|
date = startdate + datetime.timedelta(days=i)
|
||||||
|
datekey = date.strftime('%Y-%m-%d')
|
||||||
|
weight = 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
df2 = df.filter(pl.col('date') == date)
|
||||||
|
if type(df2) == pl.Series:
|
||||||
|
weight += weightfromrecord(df2, 'rscore')
|
||||||
|
else:
|
||||||
|
for row in df2.iter_slices(n_rows=1):
|
||||||
|
weight += weightfromrecord(row, 'rscore')
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
impulses.append(weight)
|
||||||
|
|
||||||
|
fatigue = (1 - lambda_a) * fatigue + lambda_a * weight
|
||||||
|
fitness = (1 - lambda_c) * fitness + lambda_c * weight
|
||||||
|
|
||||||
|
fatigues.append(fatigue)
|
||||||
|
fitnesses.append(fitness)
|
||||||
|
dates.append(arrow.get(date).datetime)
|
||||||
|
|
||||||
|
return fatigues, fitnesses, dates, impulses
|
||||||
|
|
||||||
|
def calculate_fitness(rower):
|
||||||
|
workouts = Workout.objects.filter(user=rower, date__gte=datetime.date.today() - datetime.timedelta(days=90),
|
||||||
|
duplicate=False).order_by('date')
|
||||||
|
|
||||||
|
kfitness = rower.kfit
|
||||||
|
kfatigue = rower.kfatigue
|
||||||
|
|
||||||
|
|
||||||
|
records = []
|
||||||
|
|
||||||
|
for w in workouts:
|
||||||
|
dd = {
|
||||||
|
'date': w.date,
|
||||||
|
'trimp': w.trimp,
|
||||||
|
'rscore': w.rscore,
|
||||||
|
'hrtss':w.hrtss,
|
||||||
|
'duration': w.duration,
|
||||||
|
'id': w.id,
|
||||||
|
'rpe': w.rpe,
|
||||||
|
}
|
||||||
|
records.append(dd)
|
||||||
|
|
||||||
|
df = pl.from_records(records)
|
||||||
|
if df.is_empty():
|
||||||
|
return 0, 0
|
||||||
|
|
||||||
|
dates = [arrow.get(w.date).datetime for w in workouts]
|
||||||
|
impulses = [np.nan for w in workouts]
|
||||||
|
fitnesses = [np.nan for w in workouts]
|
||||||
|
fatigues = [np.nan for w in workouts]
|
||||||
|
|
||||||
|
fatigues, fitnesses, dates, impulses = getfatigues(
|
||||||
|
df,
|
||||||
|
fatigues,
|
||||||
|
fitnesses,
|
||||||
|
dates,
|
||||||
|
impulses,
|
||||||
|
datetime.date.today()-datetime.timedelta(days=90),
|
||||||
|
kfatigue, kfitness
|
||||||
|
)
|
||||||
|
|
||||||
|
df = pl.DataFrame({
|
||||||
|
'date': dates,
|
||||||
|
'fatigue': fatigues,
|
||||||
|
'fitness': fitnesses,
|
||||||
|
'impulse': impulses,
|
||||||
|
})
|
||||||
|
|
||||||
|
endfitness = fitnesses[-2]
|
||||||
|
endfatigue = fatigues[-2]
|
||||||
|
|
||||||
|
return endfitness, endfatigue
|
||||||
|
|
||||||
+10
-1
@@ -316,8 +316,17 @@ class MetricsForm(forms.Form):
|
|||||||
class CNsummaryForm(forms.Form):
|
class CNsummaryForm(forms.Form):
|
||||||
file = forms.FileField(required=True, validators=[must_be_csv])
|
file = forms.FileField(required=True, validators=[must_be_csv])
|
||||||
|
|
||||||
|
class CurrentWeekJsonForm(forms.Form):
|
||||||
|
plan = forms.CharField(required=True, label="Plan Code", initial="zwolsche")
|
||||||
|
secret = forms.CharField(required=True, label="Plan Secret", initial="noot")
|
||||||
|
delete_existing = forms.BooleanField(required=False, initial=False, label="Remove any existing sessions")
|
||||||
|
|
||||||
class NextWeekJsonForm(forms.Form):
|
class NextWeekJsonForm(forms.Form):
|
||||||
file = forms.FileField(required=True)
|
plan = forms.CharField(required=True, label="Plan Code", initial="zwolsche")
|
||||||
|
secret = forms.CharField(required=True, label="Plan Secret", initial="noot")
|
||||||
|
fitness = forms.IntegerField(required=False, label='Fitness', initial=0)
|
||||||
|
fatigue = forms.IntegerField(required=False, label='Fatigue', initial=0)
|
||||||
|
delete_existing = forms.BooleanField(required=False, initial=False, label="Remove any existing sessions")
|
||||||
|
|
||||||
# The little window to type '4x2000m/500m' to update the workout summary
|
# The little window to type '4x2000m/500m' to update the workout summary
|
||||||
|
|
||||||
|
|||||||
@@ -1067,6 +1067,14 @@ def get_workouts_session(r, ps):
|
|||||||
|
|
||||||
return ws
|
return ws
|
||||||
|
|
||||||
|
def strcapitalize(s):
|
||||||
|
if s is None:
|
||||||
|
return None
|
||||||
|
if isinstance(s, str):
|
||||||
|
return s[0].upper() + s[1:]
|
||||||
|
|
||||||
|
return s
|
||||||
|
|
||||||
def correct_intensity(workout):
|
def correct_intensity(workout):
|
||||||
# reads the steps and if the intensity is an integer, converts it to a string
|
# reads the steps and if the intensity is an integer, converts it to a string
|
||||||
steps = workout['steps']
|
steps = workout['steps']
|
||||||
@@ -1074,15 +1082,53 @@ def correct_intensity(workout):
|
|||||||
if 'intensity' in step:
|
if 'intensity' in step:
|
||||||
if isinstance(step['intensity'], int):
|
if isinstance(step['intensity'], int):
|
||||||
step['intensity'] = intensitymap[step['intensity']]
|
step['intensity'] = intensitymap[step['intensity']]
|
||||||
|
step['durationType'] = strcapitalize(step['durationType'])
|
||||||
|
step['targetType'] = strcapitalize(step['targetType'])
|
||||||
|
step['intensity'] = strcapitalize(step['intensity'])
|
||||||
|
|
||||||
return workout
|
return workout
|
||||||
|
|
||||||
|
def remove_next_week_sessions(rower):
|
||||||
def create_next_week_from_json(plansteps, rower, planbyrscore=False, plan=None,):
|
# remove all sessions in the next week
|
||||||
trainingdays = plansteps['cycles']
|
|
||||||
# start date is the first day of the following week
|
|
||||||
today = timezone.now()
|
today = timezone.now()
|
||||||
startdate = today - timezone.timedelta(days=today.weekday())+timezone.timedelta(days=7)
|
startdate = today - timezone.timedelta(days=today.weekday())+timezone.timedelta(days=7)
|
||||||
|
enddate = startdate + timezone.timedelta(days=6)
|
||||||
|
|
||||||
|
sps = PlannedSession.objects.filter(
|
||||||
|
rower__in=[rower],
|
||||||
|
startdate__gte=startdate,
|
||||||
|
enddate__lte=enddate,
|
||||||
|
is_template=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
for ps in sps:
|
||||||
|
ps.delete()
|
||||||
|
|
||||||
|
return 1
|
||||||
|
|
||||||
|
def remove_this_week_sessions(rower):
|
||||||
|
# remove all sessions in the current week
|
||||||
|
today = timezone.now()
|
||||||
|
startdate = today - timezone.timedelta(days=today.weekday())
|
||||||
|
enddate = startdate + timezone.timedelta(days=6)
|
||||||
|
|
||||||
|
sps = PlannedSession.objects.filter(
|
||||||
|
rower__in=[rower],
|
||||||
|
startdate__gte=startdate,
|
||||||
|
enddate__lte=enddate,
|
||||||
|
is_template=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
for ps in sps:
|
||||||
|
ps.delete()
|
||||||
|
|
||||||
|
return 1
|
||||||
|
|
||||||
|
def create_next_week_from_json(plansteps, rower, planbyrscore=False, plan=None,
|
||||||
|
startdate=timezone.now()-timezone.timedelta(days=timezone.now().weekday())+timezone.timedelta(days=7)):
|
||||||
|
trainingdays = plansteps['cycles']
|
||||||
|
# start date is the first day of the following week
|
||||||
|
|
||||||
ndays = 0
|
ndays = 0
|
||||||
for day in trainingdays:
|
for day in trainingdays:
|
||||||
try:
|
try:
|
||||||
|
|||||||
BIN
Binary file not shown.
@@ -1111,5 +1111,7 @@ urlpatterns = [
|
|||||||
name="braintree_webhook_view"),
|
name="braintree_webhook_view"),
|
||||||
re_path(r'^nextweekplan/$', views.nextweekplan_view,
|
re_path(r'^nextweekplan/$', views.nextweekplan_view,
|
||||||
name='nextweekplan_view'),
|
name='nextweekplan_view'),
|
||||||
|
re_path(r'^currentweekplan/$', views.currentweekplan_view,
|
||||||
|
name='currentweekplan_view'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
+6
-3
@@ -749,12 +749,16 @@ def steps_write_fit(steps):
|
|||||||
headers = {'Authorization': authorizationstring}
|
headers = {'Authorization': authorizationstring}
|
||||||
|
|
||||||
# convert to json, value of keys called wkt_step_name to string
|
# convert to json, value of keys called wkt_step_name to string
|
||||||
|
newsteps = []
|
||||||
for step in steps['steps']:
|
for step in steps['steps']:
|
||||||
step['wkt_step_name'] = str(step['wkt_step_name'])
|
step['wkt_step_name'] = str(step['wkt_step_name'])
|
||||||
# convert numerical values in the dict to integers
|
# convert numerical values in the dict to integers
|
||||||
for key in step.keys():
|
for key in step.keys():
|
||||||
if isinstance(step[key], (int, float)):
|
if isinstance(step[key], (int, float)):
|
||||||
step[key] = int(step[key])
|
step[key] = int(step[key])
|
||||||
|
newsteps.append(step)
|
||||||
|
|
||||||
|
steps['steps'] = newsteps
|
||||||
|
|
||||||
response = requests.post(url=url, headers=headers, json=steps)
|
response = requests.post(url=url, headers=headers, json=steps)
|
||||||
|
|
||||||
@@ -1377,7 +1381,7 @@ def step_to_string(step, short=False):
|
|||||||
if isinstance(intensity, int):
|
if isinstance(intensity, int):
|
||||||
intensity = intensitymap[intensity]
|
intensity = intensitymap[intensity]
|
||||||
|
|
||||||
|
if stype != 'RepeatStep':
|
||||||
if short and intensity.lower() in ['warmup', 'cooldown', 'rest']:
|
if short and intensity.lower() in ['warmup', 'cooldown', 'rest']:
|
||||||
s = '{intensity} {duration} {unit} {target} {repeat}'.format(
|
s = '{intensity} {duration} {unit} {target} {repeat}'.format(
|
||||||
intensity=intensity,
|
intensity=intensity,
|
||||||
@@ -1395,8 +1399,7 @@ def step_to_string(step, short=False):
|
|||||||
unit=unit,
|
unit=unit,
|
||||||
notes=notes,
|
notes=notes,
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
if stype == 'RepeatStep':
|
|
||||||
s = 'Repeat {duration}'.format(duration=duration)
|
s = 'Repeat {duration}'.format(duration=duration)
|
||||||
|
|
||||||
return s, stype, nr, repeatID, repeatValue
|
return s, stype, nr, repeatID, repeatValue
|
||||||
|
|||||||
@@ -1000,7 +1000,10 @@ def strokedatajson_v2(request, id):
|
|||||||
except:
|
except:
|
||||||
return HttpResponse("No JSON object could be decoded", status=400)
|
return HttpResponse("No JSON object could be decoded", status=400)
|
||||||
|
|
||||||
|
try:
|
||||||
df = df.sort("time")
|
df = df.sort("time")
|
||||||
|
except ColumnNotFoundError: # pragma: no cover
|
||||||
|
return HttpResponse("No time column", status=400)
|
||||||
|
|
||||||
status, comment, data = api_get_dataframe(row.startdatetime, df)
|
status, comment, data = api_get_dataframe(row.startdatetime, df)
|
||||||
if status != 200: # pragma: no cover
|
if status != 200: # pragma: no cover
|
||||||
|
|||||||
+92
-12
@@ -4211,30 +4211,110 @@ def planmacrocyclebymonth(request, id=0, userid=0): # pragma: no cover
|
|||||||
|
|
||||||
return HttpResponseRedirect(url)
|
return HttpResponseRedirect(url)
|
||||||
|
|
||||||
|
@user_passes_test(can_plan, login_url="/rowers/paidplans",
|
||||||
|
message="This functionality requires a Pro plan",
|
||||||
|
redirect_field_name=None)
|
||||||
|
def currentweekplan_view(request):
|
||||||
|
r = getrower(request.user)
|
||||||
|
|
||||||
|
form = CurrentWeekJsonForm()
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
form = CurrentWeekJsonForm(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
secret = form.cleaned_data['secret']
|
||||||
|
plan = form.cleaned_data['plan']
|
||||||
|
delete_existing = form.cleaned_data['delete_existing']
|
||||||
|
|
||||||
|
post_data = {
|
||||||
|
'secret': secret,
|
||||||
|
'plan': plan,
|
||||||
|
}
|
||||||
|
|
||||||
|
url = "http://localhost:8898/current-week-plan/"
|
||||||
|
response = requests.post(url, data=post_data)
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
if response.status_code in [200,201]:
|
||||||
|
if delete_existing:
|
||||||
|
remove_this_week_sessions(r)
|
||||||
|
|
||||||
|
create_next_week_from_json(
|
||||||
|
data, r,
|
||||||
|
startdate=timezone.now()-timezone.timedelta(days=timezone.now().weekday()))
|
||||||
|
|
||||||
|
messages.info(request,"Your planned sessions were created")
|
||||||
|
|
||||||
|
url = reverse("plannedsessions_view")
|
||||||
|
return HttpResponseRedirect(url)
|
||||||
|
|
||||||
|
active = 'nav-plan'
|
||||||
|
|
||||||
|
breadcrumbs = [
|
||||||
|
{
|
||||||
|
'url': reverse('plannedsessions_view'),
|
||||||
|
'name': 'Sessions'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'url': reverse('currentweekplan_view'),
|
||||||
|
'name': "Next week"
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
return render(request,
|
||||||
|
'loadnextweek.html',
|
||||||
|
{'rower': r,
|
||||||
|
'form': form,
|
||||||
|
'active': active,
|
||||||
|
'breadcrumbs': breadcrumbs})
|
||||||
|
|
||||||
|
|
||||||
@user_passes_test(can_plan, login_url="/rowers/paidplans",
|
@user_passes_test(can_plan, login_url="/rowers/paidplans",
|
||||||
message="This functionality requires a Pro plan",
|
message="This functionality requires a Pro plan",
|
||||||
redirect_field_name=None)
|
redirect_field_name=None)
|
||||||
def nextweekplan_view(request):
|
def nextweekplan_view(request):
|
||||||
r = getrower(request.user)
|
r = getrower(request.user)
|
||||||
|
|
||||||
form = NextWeekJsonForm()
|
fitness, fatigue = calculate_fitness(r)
|
||||||
|
|
||||||
|
|
||||||
|
form = NextWeekJsonForm(
|
||||||
|
initial={
|
||||||
|
'fitness': round(fitness),
|
||||||
|
'fatigue': round(fatigue),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
form = NextWeekJsonForm(request.POST, request.FILES)
|
form = NextWeekJsonForm(request.POST)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
f = form.cleaned_data['file']
|
fitness = form.cleaned_data['fitness']
|
||||||
if f is not None:
|
fatigue = form.cleaned_data['fatigue']
|
||||||
filename, path_and_filename = handle_uploaded_file(f)
|
secret = form.cleaned_data['secret']
|
||||||
try:
|
plan = form.cleaned_data['plan']
|
||||||
with open(path_and_filename,'r') as ff:
|
delete_existing = form.cleaned_data['delete_existing']
|
||||||
data = json.load(ff)
|
|
||||||
except:
|
|
||||||
messages.error(request,"Hmm, invalid file")
|
|
||||||
|
|
||||||
|
post_data = {
|
||||||
|
'fitness':fitness,
|
||||||
|
'fatigue':fatigue,
|
||||||
|
'plan': plan,
|
||||||
|
'secret': secret,
|
||||||
|
}
|
||||||
|
|
||||||
|
# post form.cleaned_data to localhost:8898/next-week-plan
|
||||||
|
url = "http://localhost:8898/next-week-plan/"
|
||||||
|
response = requests.post(url, data=post_data)
|
||||||
|
|
||||||
|
|
||||||
|
if response.status_code in [200,201]:
|
||||||
|
data = response.json()
|
||||||
|
if delete_existing:
|
||||||
|
remove_next_week_sessions(r)
|
||||||
create_next_week_from_json(data, r)
|
create_next_week_from_json(data, r)
|
||||||
messages.info(request,"Your planned sessions were created")
|
messages.info(request,"Your planned sessions were created")
|
||||||
# remove path_and_filename
|
else:
|
||||||
os.remove(path_and_filename)
|
messages.error(request,"Unsuccessful")
|
||||||
|
|
||||||
|
|
||||||
active = 'nav-plan'
|
active = 'nav-plan'
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from rowers.utils import (
|
|||||||
my_dict_from_instance, wavg, NoTokenError,
|
my_dict_from_instance, wavg, NoTokenError,
|
||||||
request_is_ajax, dologging
|
request_is_ajax, dologging
|
||||||
)
|
)
|
||||||
|
from rowers.fitness import calculate_fitness
|
||||||
from rowers.celery import result as celery_result
|
from rowers.celery import result as celery_result
|
||||||
from rowers.interactiveplots import *
|
from rowers.interactiveplots import *
|
||||||
from scipy.interpolate import griddata
|
from scipy.interpolate import griddata
|
||||||
@@ -120,7 +121,7 @@ from rowers.forms import (
|
|||||||
TrainingPlanBillingForm, InstantPlanSelectForm,
|
TrainingPlanBillingForm, InstantPlanSelectForm,
|
||||||
TrainingZonesForm, InstrokeForm, InStrokeMultipleCompareForm,
|
TrainingZonesForm, InstrokeForm, InStrokeMultipleCompareForm,
|
||||||
ForceCurveMultipleCompareForm, PlanByRscoreForm,
|
ForceCurveMultipleCompareForm, PlanByRscoreForm,
|
||||||
AssignChoices, NextWeekJsonForm,
|
AssignChoices, NextWeekJsonForm, CurrentWeekJsonForm,
|
||||||
)
|
)
|
||||||
|
|
||||||
from django.urls import reverse, reverse_lazy
|
from django.urls import reverse, reverse_lazy
|
||||||
|
|||||||
@@ -394,6 +394,7 @@ def rower_exportsettings_view(request, userid=0):
|
|||||||
'strava_auto_delete': 'stravatoken',
|
'strava_auto_delete': 'stravatoken',
|
||||||
'trainingpeaks_auto_export': 'tptoken',
|
'trainingpeaks_auto_export': 'tptoken',
|
||||||
'rp3_auto_import': 'rp3token',
|
'rp3_auto_import': 'rp3token',
|
||||||
|
'rp3_use_split_power': 'rp3token',
|
||||||
'nk_auto_import': 'nktoken',
|
'nk_auto_import': 'nktoken',
|
||||||
'intervals_auto_export': 'intervals_token',
|
'intervals_auto_export': 'intervals_token',
|
||||||
'intervals_resample_to_1s': 'intervals_token',
|
'intervals_resample_to_1s': 'intervals_token',
|
||||||
|
|||||||
Reference in New Issue
Block a user