From 63202ac2f1ead436e66dadb3a4338fb4b40da272 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sat, 28 Dec 2024 15:13:04 +0100 Subject: [PATCH] intervals.icu fixes --- rowers/dataroutines.py | 9 +- rowers/integrations/intervals.py | 118 ++++++++++++++++++++++++-- rowers/tasks.py | 1 - rowers/tests/testdata/testdata.tcx.gz | Bin 4000 -> 4000 bytes rowers/views/importviews.py | 4 +- rowers/views/workoutviews.py | 24 ++---- 6 files changed, 129 insertions(+), 27 deletions(-) diff --git a/rowers/dataroutines.py b/rowers/dataroutines.py index c828d047..abfe2fde 100644 --- a/rowers/dataroutines.py +++ b/rowers/dataroutines.py @@ -1406,7 +1406,12 @@ def get_title_from_fit(filename): return title -def get_workouttype_from_fit(filename, workouttype='water'): +def get_workouttype_from_fit(filename, workouttype=None): + if workouttype is None: + workouttype = 'water' + workouttype_orig = '' + else: + workouttype_orig = workouttype try: fitfile = FitFile(filename, check_crc=False) except FitHeaderError: # pragma: no cover @@ -1435,6 +1440,8 @@ def get_workouttype_from_fit(filename, workouttype='water'): workouttype = mytypes.fitmappinginv[fittype] except KeyError: pass + if workouttype_orig in ['water', 'rower']: + workouttype = workouttype_orig return workouttype diff --git a/rowers/integrations/intervals.py b/rowers/integrations/intervals.py index 56c5da64..36da54d5 100644 --- a/rowers/integrations/intervals.py +++ b/rowers/integrations/intervals.py @@ -1,10 +1,12 @@ from .integrations import SyncIntegration, NoTokenError, create_or_update_syncrecord, get_known_ids from rowers.models import Rower, User, Workout, TombStone, PlannedSession from rowingdata import rowingdata +from rowingdata import FITParser as FP +from rowingdata.otherparsers import FitSummaryData from rowers.rower_rules import user_is_not_basic, user_is_coachee from rowers import mytypes - +import shutil from rowers.rower_rules import is_workout_user, ispromember from rowers.utils import myqueue, dologging, custom_exception_handler from rowers.tasks import handle_intervals_getworkout @@ -22,7 +24,8 @@ import rowers.dataprep as dataprep from rowers.opaque import encoder from rowsandall_app.settings import ( - INTERVALS_CLIENT_ID, INTERVALS_REDIRECT_URI, INTERVALS_CLIENT_SECRET, SITE_URL + INTERVALS_CLIENT_ID, INTERVALS_REDIRECT_URI, INTERVALS_CLIENT_SECRET, SITE_URL, + UPLOAD_SERVICE_SECRET, UPLOAD_SERVICE_URL ) import django_rq @@ -272,12 +275,103 @@ class IntervalsIntegration(SyncIntegration): return workouts - + + def update_workout(self, id, *args, **kwargs) -> int: + _ = self.open() + r = self.rower + + headers = { + 'Authorization': 'Bearer ' + r.intervals_token, + } + url = self.oauth_data['base_url'] + 'activity/' + str(id) + response = requests.get(url, headers=headers) + if response.status_code != 200: + dologging('intervals.icu.log', response.text) + return 0 + + data = response.json() + ws = Workout.objects.filter(uploadedtointervals=id) + + for w in ws: + try: + w.name = data['name'] + except KeyError: + pass + try: + w.notes = data['description'] + except KeyError: + pass + try: + w.workouttype = mytypes.intervalsmappinginv[data['type']] + except KeyError: + pass + w.save() + + # we stop here now + return 1 + + url = self.oauth_data['base_url'] + 'activity/' + str(id) + '/fit-file' + response = requests.get(url, headers=headers) + if response.status_code != 200: + dologging('intervals.icu.log', response.text) + return 0 + + try: + fit_data = response.content + fit_filename = 'media/intervals_' + str(id) + '.fit' + with open(fit_filename, 'wb') as f: + f.write(fit_data) + except: + return 0 + + try: + row = FP(fit_filename) + rowdata = rowingdata(df=row.df) + rowsummary = FitSummaryData(fit_filename) + except Exception as e: + dologging('intervals.icu.log', e) + return 0 + + for w in ws: + # copy fit_file to random file name using shutil + temp_filename = 'media/' + str(uuid4()) + '.fit' + try: + shutil.copy(fit_filename, temp_filename) + + + uploadoptions = { + 'secret': UPLOAD_SERVICE_SECRET, + 'user': self.rower.user.id, + 'boattype': '1x', + 'workouttype': w.workouttype, + 'file': temp_filename, + 'intervalsid': id, + 'id': w.id, + } + url = UPLOAD_SERVICE_URL + response = requests.post(url, data=uploadoptions) + except FileNotFoundError: + return 0 + except Exception as e: + dologging('intervals.icu.log', e) + + # remove fit_file + try: + os.remove(fit_filename) + except: + pass + + return 1 def get_workout(self, id, *args, **kwargs) -> int: _ = self.open() r = self.rower + # check if workout with this id already exists + known_interval_ids = get_known_ids(r, 'intervalsid') + if id in known_interval_ids: + return self.update_workout(id) + record = create_or_update_syncrecord(r, None, intervalsid=id) _ = myqueue(queuehigh, @@ -583,8 +677,8 @@ class IntervalsIntegration(SyncIntegration): id = record['id'] try: ws = Workout.objects.filter(uploadedtointervals=id) - if w.user == self.rower: - for w in ws: + for w in ws: + if w.user == self.rower: w.delete() except Workout.DoesNotExist: pass @@ -592,3 +686,17 @@ class IntervalsIntegration(SyncIntegration): pass return 1 + + def update_activities(self, event, *args, **kwargs): + try: + record = event["activity"] + except KeyError: + records = [] + + try: + id = record['id'] + result = self.update_workout(id) + except KeyError: + pass + + return 1 diff --git a/rowers/tasks.py b/rowers/tasks.py index 0382f924..866d25bc 100644 --- a/rowers/tasks.py +++ b/rowers/tasks.py @@ -3582,7 +3582,6 @@ def handle_intervals_getworkout(rower, intervalstoken, workoutid, debug=False, * except KeyError: workouttype = 'water' - url = "https://intervals.icu/api/v1/activity/{workoutid}/fit-file".format(workoutid=workoutid) response = requests.get(url, headers=headers) diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz index d443a63314608b0795960180da2935502edba47b..825479663045a110a1a50dc800b37b8f4079804a 100644 GIT binary patch literal 4000 zcmV;R4`1*fiwFof2XJQs|8!+@bYx+4VJ>uIcmVC4NpBoC7J%>m6@m`QVG#CP__!#t z0>{o^4A`C^F*3OgDpE)88L1_x%iF&nvfGhmTZQCFQv|C(9}IPMZCzhI^6fnQ?%ny> z-kassW_7WCxQ_<*_r5!NaQO17Tdh~?)5ptozr1?V_1$mXX1SMkUT+@mzv}zTgT>n~ST{1p(mV*XPfE-k#!{)q2x+>*HlQYO^}no%h+rao2Bu?VWC5Pk+=K zGVeAge}3TKyZ&)^c5$^@ZWeFw&-eem>yLN)FRw3!f9+C(fbM$e?l_ww1sk9+@t?+7s*F0Ma)+~H9YweEEJk7c_1pSphe{pI=g)$HG2reAWs z`RV%mW!GOWukG5~cGLgOBa`Qo+EYkx%BFv9*_xBBoSF6+0bZg80)y2ut z^KSjRJ4+Nl*?M#D?)bl--2HaT<;7~<-`t$PNbkU3co(<3eR||)uY?#o{F-bJxF>;| z6v%%;VMpS21fM)$xaIk7ceozCHeX!!<&VGq<(c5tG5qs0rgQ$Q>-!`&@o=&G>+@?a z(nUZ2dAHwUei3fNtEJ9hlC+CeP)9a64ea?rSHzuMMcfla1Q!(CDd|lGxre)Rr@G|b zN=4kAAx70GpN0r7$UV-6yR#K>k4fIGcn$YhQr;P6!`)ZJ-O_fPcM9${7%Ipe@4+3b z;vS$P?vl7Cq0(sc-nfZyuS?zoSH#_$;K6D1dFM)+aZK~!?(5+$V0=KUk#{dfCc?e4 zalRruPS`}SQNuk*N&9_H@*XPU?u-Khjb0a}s6Ia*c}J*7v6E<+b#7XOvgG+53jEfjmZ zHS;{#vgGZI$X67x;a~v3i-x>F$>_YjkKdk~RX0RF$**;&O2ZDdBl$Rkchw{KC}Ve-y+!JvuKGGS4Beoo{ovgfmf##S}YGoy(UA&+gI zznwhi+*<}DjVC9)C?&J5t!sY!QOJt{hM zIOpCvkEj{#1z|EW-|YEe^3hO$7*ynA)9t4pu@TDMQdCh|VQ9Mj^krVG2}!qC$-FI_ zbsbwR?I`5E5!OX)M9PwHKD-gg+XUO7x2mjbagF?4$=iy^lL=l|T_qkT6Q6G~KTKYX zB~(>cNhn)a*Lq-%Lp~TnRCP{}ShmK-H{E^=@(3m(vBrDr09)?u^kp8|NFdTgX&p|7 zJU36yAbFRwu2r279br-3eoo|T8ttP2#Gr}KJEo#l)V{^%#~`0`ZvzHR-&<#?XzuNt z$X8^~lQra!Rr9>iWO8r0)z1%-_XaJJYP5G$)SjOac}EqI&z%#9HT~_K=c4ocoXA(C z+b3;}BdS&{bAelwb_V3d0Enu~xr^MQw9_B$*&7jECqxg8{9Vacq}y9C>_z3t@n}nG zc=yV~YZ~4#dCuKWRo+{lEWy|^Fegu5jJFOou{xi0gMzU-!5P`}6~*UmFcuj#IXOPP zEp{U0YxCiSddLgrT3Xeb7G8?h&(BHbYtrz9jTb_V56?SS(r-VjdA=fhKFKdfQq|aa z?^;FW9m!V|rIl!a2~?wfaFfyPoA-8ryc_@;uk_*hSacQgS;@RqMBW+;qLTRlMc1O9 zmCU<}$a{l8pt0%#hRMi$BR@<&8kYup&1fGe6t(B)G}?!H$RinRF>11|1IH%wcO)Mx zA}D!!86pn>)pWB%dvJSnoMQuB2bNwjjt+9eCA1;EICiQnjPZgo}xkZ*o z75d;NBlVS`XIBsUWT)9cU{&a&FRJFvX~4&d&~uvW2~=}ELYNHt+6MfZq}~|@B-LDx zShn0|PUyKJ^f7DrQ783fS1oMl$3dSvp*mJ&W?`DKnub3E^pXdBRmc`msA#l)PHJ97 z#vKPk9)n85N7uZ0cQx)pJ?PWe4KDY`)1dc7`{3RyBNLi&H;CRE0Aiig2Rj+`*o?bD z^gf~2HPOh~Tt(4yLq7`oXrfiRbO>BgCEqW^3 zDs4_u4;7&gMub!iu#u8Q=o|WJyL5c+jA@;S%DAJXAjm}8=U`iweh|Gi>~-yP^1>LC z>73N;`eF3mP>`S-@I4eQtjDJ2je#DM04odbkOEGoBQCbI%t7>Q(CNBnA;qE#m(FUy zr;5-!1D13dxePp+HE!BmKa4(@D9Y*^MqAXS)8_h7&=Z8L43T{-+Bl7xT|Wx?Xegr9Y51XR_=cM|Zw&O7a{nxyH;-d%YTon* zd@BY-S$Jg;CKIo(Eg8F_psh_B8&Zra^h9m0pFVm46O{`Wqh%@@xobZBanQ4g4oQdJ z@?@4bYmVGu^xKw+UZv()vCXA3eMj!LWg>drT<>Hu30f^Ja{ztVwoC-wk~e6*G~;gS zc74b#6RilU9Jw~M>YKZw4;7&gxr3IjtA>qb!#CUp{4vlY8fI}SsTU}_Ts_Z7&4Y^2 z+ihzFm6|7o+bGSczTQ@Z-W#&Ypsg?z_2$_&;E#cxz#uWQ3cZENtZmr5dBf z*C&gh6Rs-t5q0Pt6*cZ!SB-Jdhb;A8H{g4ojMTRQe-J&R3BtN~y#!lS!*4;` zanM`i5K+}tL!xcoyy-h~*&EMfRY6+^ZZfGFP0brbpX@a2P?x3S(6s@7>bZFdJsY9y zq|u6Bo!HcG?jow1M;Ej8#lv;lwmj$9iPdLmWfm9ud&m9{NQXAr%RVXY6}IcRhJ z4A6@q(yDnLT1M{F3$H?MnTTnd4t>DMG*oG7-T?ZTTP6Zx)YIn*BrT1cj?63G7+?E z+IsINGuKlK+76=6Mb-?e@Qn|?+4VC(AB`mIPAB!zPsXmNrsfTyXEI`~E_@RpG`oHV z=p`3kfet+uMW2@nY6JcV=z{@9T|fMgllLcro|-pr2z_438ZhY4bDRwNwtoFEdNx5w z7r9F>N=XerccN(MM?mk4XB$;6o#3#fOJ`Q-r6Tk(uW1W9srOiP!1t{LV+`~{##s!i zES(UFR@&YReXF?}MlZSgCa5xULo8Zpd#?e%_0kzeAM%>Ey2xFGqKlo)O2e-uOGjY4 zk~N@LIdUV)WL!GX`sNLy->zh>TjwrP*`O`7xqcM%K3Cr;(X*e->XvP;A4H#5vSuWe zH!oUWwEBix-@H-K^L7z)m2np(w$&{s?#;{ST`s&*qIa>V)VD^d(i62@6vg@-P@I@d_3;((d+)z#np}1 zAI}2`p{&Kg~ zI&Jpyalh?`>qq<9`43;Fn?Cw!dA95}%fumn`tay(+}Gi=ZhiXn$PdPUzoknbJ@_9-$8k5r GfB^tN_dX;5 literal 4000 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 z 1: # pragma: no cover - modality = 'all' - else: - modality = modalities[0] - else: - modalities = [m[0] for m in mytypes.workouttypes] - modality = 'all' + modalities = [m[0] for m in mytypes.workouttypes] + modality = 'all' if request.method == 'POST': dateform = DateRangeForm(request.POST) @@ -1154,8 +1144,6 @@ def workouts_join_select(request, enddate = dateform.cleaned_data['enddate'] startdatestring = startdate.strftime('%Y-%m-%d') enddatestring = enddate.strftime('%Y-%m-%d') - request.session['startdate'] = startdatestring - request.session['enddate'] = enddatestring if modalityform.is_valid(): modality = modalityform.cleaned_data['modality'] waterboattype = modalityform.cleaned_data['waterboattype'] @@ -1164,11 +1152,7 @@ def workouts_join_select(request, else: modalities = [modality] - if modality != 'water': # pragma: no cover - waterboattype = [b[0] for b in mytypes.boattypes] - request.session['modalities'] = modalities - request.session['waterboattype'] = waterboattype else: dateform = DateRangeForm(initial={ 'startdate': startdate, @@ -1211,6 +1195,7 @@ def workouts_join_select(request, startdatetime__lte=enddate, workouttype__in=modalities).order_by("-date", "-starttime").exclude(boattype__in=negtypes) + query = request.GET.get('q') if query: # pragma: no cover query_list = query.split() @@ -4954,6 +4939,7 @@ def workout_upload_api(request): message = {'status': 'false', 'message': 'invalid credentials'} return JSONResponse(status=403, data=message) + form = DocumentsForm(post_data) optionsform = TeamUploadOptionsForm(post_data) rowerform = TeamInviteForm(post_data)