Private
Public Access
1
0

Merge branch 'release/v23.5.0'

This commit is contained in:
2025-05-12 16:25:47 +02:00
10 changed files with 296 additions and 39 deletions
+112
View File
@@ -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
View File
@@ -316,8 +316,17 @@ class MetricsForm(forms.Form):
class CNsummaryForm(forms.Form):
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):
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
+50 -4
View File
@@ -1067,6 +1067,14 @@ def get_workouts_session(r, ps):
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):
# reads the steps and if the intensity is an integer, converts it to a string
steps = workout['steps']
@@ -1074,15 +1082,53 @@ def correct_intensity(workout):
if 'intensity' in step:
if isinstance(step['intensity'], int):
step['intensity'] = intensitymap[step['intensity']]
step['durationType'] = strcapitalize(step['durationType'])
step['targetType'] = strcapitalize(step['targetType'])
step['intensity'] = strcapitalize(step['intensity'])
return workout
def create_next_week_from_json(plansteps, rower, planbyrscore=False, plan=None,):
trainingdays = plansteps['cycles']
# start date is the first day of the following week
def remove_next_week_sessions(rower):
# remove all sessions in the next week
today = timezone.now()
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
for day in trainingdays:
try:
Binary file not shown.
+2
View File
@@ -1111,5 +1111,7 @@ urlpatterns = [
name="braintree_webhook_view"),
re_path(r'^nextweekplan/$', views.nextweekplan_view,
name='nextweekplan_view'),
re_path(r'^currentweekplan/$', views.currentweekplan_view,
name='currentweekplan_view'),
]
+6 -3
View File
@@ -749,12 +749,16 @@ def steps_write_fit(steps):
headers = {'Authorization': authorizationstring}
# convert to json, value of keys called wkt_step_name to string
newsteps = []
for step in steps['steps']:
step['wkt_step_name'] = str(step['wkt_step_name'])
# convert numerical values in the dict to integers
for key in step.keys():
if isinstance(step[key], (int, float)):
step[key] = int(step[key])
newsteps.append(step)
steps['steps'] = newsteps
response = requests.post(url=url, headers=headers, json=steps)
@@ -1377,7 +1381,7 @@ def step_to_string(step, short=False):
if isinstance(intensity, int):
intensity = intensitymap[intensity]
if stype != 'RepeatStep':
if short and intensity.lower() in ['warmup', 'cooldown', 'rest']:
s = '{intensity} {duration} {unit} {target} {repeat}'.format(
intensity=intensity,
@@ -1395,8 +1399,7 @@ def step_to_string(step, short=False):
unit=unit,
notes=notes,
)
if stype == 'RepeatStep':
else:
s = 'Repeat {duration}'.format(duration=duration)
return s, stype, nr, repeatID, repeatValue
+3
View File
@@ -1000,7 +1000,10 @@ def strokedatajson_v2(request, id):
except:
return HttpResponse("No JSON object could be decoded", status=400)
try:
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)
if status != 200: # pragma: no cover
+92 -12
View File
@@ -4211,30 +4211,110 @@ def planmacrocyclebymonth(request, id=0, userid=0): # pragma: no cover
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",
message="This functionality requires a Pro plan",
redirect_field_name=None)
def nextweekplan_view(request):
r = getrower(request.user)
form = NextWeekJsonForm()
fitness, fatigue = calculate_fitness(r)
form = NextWeekJsonForm(
initial={
'fitness': round(fitness),
'fatigue': round(fatigue),
}
)
if request.method == "POST":
form = NextWeekJsonForm(request.POST, request.FILES)
form = NextWeekJsonForm(request.POST)
if form.is_valid():
f = form.cleaned_data['file']
if f is not None:
filename, path_and_filename = handle_uploaded_file(f)
try:
with open(path_and_filename,'r') as ff:
data = json.load(ff)
except:
messages.error(request,"Hmm, invalid file")
fitness = form.cleaned_data['fitness']
fatigue = form.cleaned_data['fatigue']
secret = form.cleaned_data['secret']
plan = form.cleaned_data['plan']
delete_existing = form.cleaned_data['delete_existing']
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)
messages.info(request,"Your planned sessions were created")
# remove path_and_filename
os.remove(path_and_filename)
else:
messages.error(request,"Unsuccessful")
active = 'nav-plan'
+2 -1
View File
@@ -13,6 +13,7 @@ from rowers.utils import (
my_dict_from_instance, wavg, NoTokenError,
request_is_ajax, dologging
)
from rowers.fitness import calculate_fitness
from rowers.celery import result as celery_result
from rowers.interactiveplots import *
from scipy.interpolate import griddata
@@ -120,7 +121,7 @@ from rowers.forms import (
TrainingPlanBillingForm, InstantPlanSelectForm,
TrainingZonesForm, InstrokeForm, InStrokeMultipleCompareForm,
ForceCurveMultipleCompareForm, PlanByRscoreForm,
AssignChoices, NextWeekJsonForm,
AssignChoices, NextWeekJsonForm, CurrentWeekJsonForm,
)
from django.urls import reverse, reverse_lazy
+1
View File
@@ -394,6 +394,7 @@ def rower_exportsettings_view(request, userid=0):
'strava_auto_delete': 'stravatoken',
'trainingpeaks_auto_export': 'tptoken',
'rp3_auto_import': 'rp3token',
'rp3_use_split_power': 'rp3token',
'nk_auto_import': 'nktoken',
'intervals_auto_export': 'intervals_token',
'intervals_resample_to_1s': 'intervals_token',