From ec4822be39aae5f2211f30c6f537e58eaf444f71 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Mon, 14 Apr 2025 20:55:45 +0200 Subject: [PATCH 1/7] fixed repeatUntilStepsCmplt --- rowers/plannedsessions.py | 11 +++++++++++ rowers/tests/testdata/testdata.tcx.gz | Bin 3989 -> 3989 bytes rowers/utils.py | 4 ++++ 3 files changed, 15 insertions(+) diff --git a/rowers/plannedsessions.py b/rowers/plannedsessions.py index 28da7b70..48bfe281 100644 --- a/rowers/plannedsessions.py +++ b/rowers/plannedsessions.py @@ -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,6 +1082,9 @@ 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 diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz index 5c76f9ad8dd9f0e3cfa489d809aeb727cc8c5bc7..459a6f0001b64802232861f9383dfddc01b0a59e 100644 GIT binary patch delta 15 WcmbO#KUJPhzMF%iC3quSKR*B?4g^&I delta 15 WcmbO#KUJPhzMF&NtNuo|etrNY00g1{ diff --git a/rowers/utils.py b/rowers/utils.py index 5b8a0500..743df665 100644 --- a/rowers/utils.py +++ b/rowers/utils.py @@ -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) From 237e52f9ab4612de76e7147434d774a60fa4fe77 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 7 May 2025 20:50:44 +0200 Subject: [PATCH 2/7] some connection with the lisp server --- rowers/fitness.py | 112 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 rowers/fitness.py diff --git a/rowers/fitness.py b/rowers/fitness.py new file mode 100644 index 00000000..bcced876 --- /dev/null +++ b/rowers/fitness.py @@ -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 + From e24b2f0d4adf6008d56beb5f646eb68276bfa125 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 7 May 2025 20:50:59 +0200 Subject: [PATCH 3/7] some connection with the lisp server, commit 2 --- rowers/forms.py | 3 +- rowers/plannedsessions.py | 6 ++-- rowers/urls.py | 2 ++ rowers/utils.py | 39 +++++++++++------------ rowers/views/planviews.py | 65 ++++++++++++++++++++++++++++++-------- rowers/views/statements.py | 1 + 6 files changed, 78 insertions(+), 38 deletions(-) diff --git a/rowers/forms.py b/rowers/forms.py index f54c97b0..53ecf07c 100644 --- a/rowers/forms.py +++ b/rowers/forms.py @@ -317,7 +317,8 @@ class CNsummaryForm(forms.Form): file = forms.FileField(required=True, validators=[must_be_csv]) class NextWeekJsonForm(forms.Form): - file = forms.FileField(required=True) + fitness = forms.IntegerField(required=False, label='Fitness', initial=0) + fatigue = forms.IntegerField(required=False, label='Fatigue', initial=0) # The little window to type '4x2000m/500m' to update the workout summary diff --git a/rowers/plannedsessions.py b/rowers/plannedsessions.py index 48bfe281..2bef406d 100644 --- a/rowers/plannedsessions.py +++ b/rowers/plannedsessions.py @@ -1089,11 +1089,11 @@ def correct_intensity(workout): return workout -def create_next_week_from_json(plansteps, rower, planbyrscore=False, plan=None,): +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 - today = timezone.now() - startdate = today - timezone.timedelta(days=today.weekday())+timezone.timedelta(days=7) + ndays = 0 for day in trainingdays: try: diff --git a/rowers/urls.py b/rowers/urls.py index 05d38608..65db0431 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -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'), ] diff --git a/rowers/utils.py b/rowers/utils.py index 743df665..f6aa06bd 100644 --- a/rowers/utils.py +++ b/rowers/utils.py @@ -1381,26 +1381,25 @@ def step_to_string(step, short=False): if isinstance(intensity, int): intensity = intensitymap[intensity] - - if short and intensity.lower() in ['warmup', 'cooldown', 'rest']: - s = '{intensity} {duration} {unit} {target} {repeat}'.format( - intensity=intensity, - duration=duration, - target=target, - repeat=repeat, - unit=unit - ) - elif intensity.lower() in ['warmup', 'cooldown', 'rest']: - s = '{intensity} {duration} {unit} {target} {repeat} {notes}'.format( - intensity=intensity, - duration=duration, - target=target, - repeat=repeat, - unit=unit, - notes=notes, - ) - - if stype == 'RepeatStep': + if stype != 'RepeatStep': + if short and intensity.lower() in ['warmup', 'cooldown', 'rest']: + s = '{intensity} {duration} {unit} {target} {repeat}'.format( + intensity=intensity, + duration=duration, + target=target, + repeat=repeat, + unit=unit + ) + elif intensity.lower() in ['warmup', 'cooldown', 'rest']: + s = '{intensity} {duration} {unit} {target} {repeat} {notes}'.format( + intensity=intensity, + duration=duration, + target=target, + repeat=repeat, + unit=unit, + notes=notes, + ) + else: s = 'Repeat {duration}'.format(duration=duration) return s, stype, nr, repeatID, repeatValue diff --git a/rowers/views/planviews.py b/rowers/views/planviews.py index 09048997..90c7c353 100644 --- a/rowers/views/planviews.py +++ b/rowers/views/planviews.py @@ -4211,30 +4211,67 @@ 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) + + post_data = { + 'secret': 'noot', # to be replaced + 'plan': 'zwolsche', # to be replaced + } + + url = "http://localhost:8898/current-week-plan/" + response = requests.post(url, data=post_data) + + data = response.json() + + 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) + @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'] + + post_data = { + 'fitness':fitness, + 'fatigue':fatigue, + 'plan':'zwolsche', # to be replaced + 'secret':'noot', # to be replaced + } + + # post form.cleaned_data to localhost:8898/next-week-plan + url = "http://localhost:8898/next-week-plan/" + response = requests.post(url, data=post_data) + + data = response.json() + + create_next_week_from_json(data, r) + messages.info(request,"Your planned sessions were created") - create_next_week_from_json(data, r) - messages.info(request,"Your planned sessions were created") - # remove path_and_filename - os.remove(path_and_filename) active = 'nav-plan' diff --git a/rowers/views/statements.py b/rowers/views/statements.py index 0b9e0aa9..53e9fb13 100644 --- a/rowers/views/statements.py +++ b/rowers/views/statements.py @@ -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 From 7858803ba2d6528c622242b95d21abab01abac52 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 7 May 2025 21:47:57 +0200 Subject: [PATCH 4/7] fix --- rowers/tests/testdata/testdata.tcx.gz | Bin 3989 -> 3989 bytes rowers/views/apiviews.py | 5 ++++- rowers/views/userviews.py | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz index 459a6f0001b64802232861f9383dfddc01b0a59e..bbb37268694cda916d5fd96b47940e571f0bcb8a 100644 GIT binary patch delta 17 YcmbO#KUJPXzMF$%(-!HBjU2uF059tWX#fBK delta 17 YcmbO#KUJPXzMF%iCHQapMvh*705KZ{y8r+H diff --git a/rowers/views/apiviews.py b/rowers/views/apiviews.py index 681e3148..fc0ee311 100644 --- a/rowers/views/apiviews.py +++ b/rowers/views/apiviews.py @@ -1000,7 +1000,10 @@ def strokedatajson_v2(request, id): except: return HttpResponse("No JSON object could be decoded", status=400) - df = df.sort("time") + 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 diff --git a/rowers/views/userviews.py b/rowers/views/userviews.py index 90cb4480..9f72a107 100644 --- a/rowers/views/userviews.py +++ b/rowers/views/userviews.py @@ -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', From 41eeba92992e65ac7d680b0cee18f0a7f7d78af8 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Thu, 8 May 2025 16:14:06 +0200 Subject: [PATCH 5/7] adding currentweekplan, nextweekplan as part of project DeepWater --- rowers/forms.py | 8 +++++ rowers/plannedsessions.py | 35 ++++++++++++++++++ rowers/views/planviews.py | 73 ++++++++++++++++++++++++++++++-------- rowers/views/statements.py | 2 +- 4 files changed, 102 insertions(+), 16 deletions(-) diff --git a/rowers/forms.py b/rowers/forms.py index 53ecf07c..45a545bb 100644 --- a/rowers/forms.py +++ b/rowers/forms.py @@ -316,9 +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): + 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 diff --git a/rowers/plannedsessions.py b/rowers/plannedsessions.py index 2bef406d..5b0513f9 100644 --- a/rowers/plannedsessions.py +++ b/rowers/plannedsessions.py @@ -1088,6 +1088,41 @@ def correct_intensity(workout): return workout +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)): diff --git a/rowers/views/planviews.py b/rowers/views/planviews.py index 90c7c353..87867b58 100644 --- a/rowers/views/planviews.py +++ b/rowers/views/planviews.py @@ -4217,22 +4217,57 @@ def planmacrocyclebymonth(request, id=0, userid=0): # pragma: no cover def currentweekplan_view(request): r = getrower(request.user) - post_data = { - 'secret': 'noot', # to be replaced - 'plan': 'zwolsche', # to be replaced - } + form = CurrentWeekJsonForm() - url = "http://localhost:8898/current-week-plan/" - response = requests.post(url, data=post_data) + 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'] - data = response.json() + post_data = { + 'secret': secret, + 'plan': plan, + } - 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 = "http://localhost:8898/current-week-plan/" + response = requests.post(url, data=post_data) - url = reverse("plannedsessions_view") + 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 HttpResponseRedirect(url) + 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", @@ -4255,12 +4290,15 @@ def nextweekplan_view(request): if form.is_valid(): 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':'zwolsche', # to be replaced - 'secret':'noot', # to be replaced + 'plan': plan, + 'secret': secret, } # post form.cleaned_data to localhost:8898/next-week-plan @@ -4269,8 +4307,13 @@ def nextweekplan_view(request): data = response.json() - create_next_week_from_json(data, r) - messages.info(request,"Your planned sessions were created") + if response.status_code in [200,201]: + if delete_existing: + remove_next_week_sessions(r) + create_next_week_from_json(data, r) + messages.info(request,"Your planned sessions were created") + else: + messages.error(request,"Unsuccessful") active = 'nav-plan' diff --git a/rowers/views/statements.py b/rowers/views/statements.py index 53e9fb13..7667dae2 100644 --- a/rowers/views/statements.py +++ b/rowers/views/statements.py @@ -121,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 From 00d998cf8eb1dd1ed1ae2bb9270956bf75aedbb6 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Fri, 9 May 2025 06:36:55 +0200 Subject: [PATCH 6/7] Project Deep Water tested --- rowers/tests/testdata/testdata.tcx.gz | Bin 3989 -> 3989 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz index bbb37268694cda916d5fd96b47940e571f0bcb8a..8bad07de0f90196b846f4b231b79954f4e64736c 100644 GIT binary patch delta 16 XcmbO#KUJPxzMF%?k4JVRdmldlBZvd` delta 16 XcmbO#KUJPxzMF$%(-!HC?0x(IDk=p4 From 94ce95bb5b5338cc035ec3cb4b6a6c328c232211 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Mon, 12 May 2025 16:24:19 +0200 Subject: [PATCH 7/7] tested Deep Water --- rowers/tests/testdata/testdata.tcx.gz | Bin 3989 -> 3989 bytes rowers/views/planviews.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz index 8bad07de0f90196b846f4b231b79954f4e64736c..0c0f856ddd328cfb01839c7b5b2ad3b6be5daf60 100644 GIT binary patch delta 16 XcmbO#KUJPxzMF%i@2%oS_C9_9Dxw7& delta 16 XcmbO#KUJPxzMF%?k4JVRdmldlBZvd` diff --git a/rowers/views/planviews.py b/rowers/views/planviews.py index 87867b58..9e4bf885 100644 --- a/rowers/views/planviews.py +++ b/rowers/views/planviews.py @@ -4305,9 +4305,9 @@ def nextweekplan_view(request): url = "http://localhost:8898/next-week-plan/" response = requests.post(url, data=post_data) - data = response.json() 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)