From 66365b7b8413d9f38aae0fb5726ed5c5db5322c6 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Tue, 3 Dec 2024 20:39:59 +0100 Subject: [PATCH 01/49] first batch of tests --- rowers/integrations/strava.py | 2 +- rowers/rower_rules.py | 4 +- rowers/tests/test_api.py | 233 ++++++++++++++++++++++++++++++++-- rowers/uploads.py | 2 + 4 files changed, 228 insertions(+), 13 deletions(-) diff --git a/rowers/integrations/strava.py b/rowers/integrations/strava.py index b78281a0..f6e878c1 100644 --- a/rowers/integrations/strava.py +++ b/rowers/integrations/strava.py @@ -214,7 +214,7 @@ class StravaIntegration(SyncIntegration): def get_workout(self, id, *args, **kwargs) -> int: try: _ = self.open() - except NoTokenError("Strava error"): + except NoTokenError: return 0 record = create_or_update_syncrecord(self.rower, None, stravaid=id) diff --git a/rowers/rower_rules.py b/rowers/rower_rules.py index c8a14c14..05a16994 100644 --- a/rowers/rower_rules.py +++ b/rowers/rower_rules.py @@ -479,7 +479,9 @@ def is_workout_team(user, workout): @rules.predicate def can_view_workout(user, workout): - if workout.privacy != 'private': + if workout.workoutsource == 'strava': + return user == workout.user.user + if workout.privacy not in ('hidden', 'private'): return True if user.is_anonymous: # pragma: no cover return False diff --git a/rowers/tests/test_api.py b/rowers/tests/test_api.py index 1187265e..d120c8b2 100644 --- a/rowers/tests/test_api.py +++ b/rowers/tests/test_api.py @@ -24,8 +24,229 @@ from rest_framework.test import APIRequestFactory, force_authenticate import json +# import BeautifulSoup +from bs4 import BeautifulSoup + from rowers.ownapistuff import * from rowers.views.apiviews import * +from rowers.teams import add_member, add_coach + +class TeamFactory(factory.DjangoModelFactory): + class Meta: + model = Team + + name = factory.LazyAttribute(lambda _: faker.word()) + notes = faker.text() + private = 'open' + viewing = 'allmembers' + +class StravaPrivacy(TestCase): + def setUp(self): + self.u = UserFactory() + self.u2 = UserFactory() + self.u3 = UserFactory() + + self.r = Rower.objects.create(user=self.u, + birthdate=faker.profile()['birthdate'], + gdproptin=True, ftpset=True,surveydone=True, + gdproptindate=timezone.now(), + rowerplan='coach',subscription_id=1) + + self.r.stravatoken = '12' + self.r.stravarefreshtoken = '123' + self.r.stravatokenexpirydate = arrow.get(datetime.datetime.now()-datetime.timedelta(days=1)).datetime + self.r.strava_owner_id = 4 + + self.r.save() + + self.c = Client() + self.user_workouts = WorkoutFactory.create_batch(5, user=self.r) + for w in self.user_workouts: + w.workoutsource = 'strava' + w.privacy = 'hidden' + w.save() + + self.factory = RequestFactory() + self.password = faker.word() + self.u.set_password(self.password) + self.u.save() + self.factory = APIRequestFactory() + + self.r2 = Rower.objects.create(user=self.u2, + birthdate=faker.profile()['birthdate'], + gdproptin=True, ftpset=True,surveydone=True, + gdproptindate=timezone.now(), + rowerplan='coach',clubsize=3) + + self.r3 = Rower.objects.create(user=self.u3, + birthdate=faker.profile()['birthdate'], + gdproptin=True, ftpset=True,surveydone=True, + gdproptindate=timezone.now(), + rowerplan='basic') + + self.c = Client() + + self.password2 = faker.word() + self.u2.set_password(self.password2) + self.u2.save() + + self.password3 = faker.word() + self.u3.set_password(self.password3) + self.u3.save() + + self.team = TeamFactory(manager=self.u2) + + # all are team members + add_member(self.team.id, self.r) + add_member(self.team.id, self.r2) + add_member(self.team.id, self.r3) + + # r2 coaches r + add_coach(self.r2, self.r) + + self.factory = APIRequestFactory() + + # Test if workout with workoutsource strava and privacy hidden can be seen by coach + def test_privacy_coach(self): + login = self.c.login(username=self.u2.username, password=self.password2) + self.assertTrue(login) + + w = self.user_workouts[0] + url = reverse('workout_view',kwargs={'id':encoder.encode_hex(w.id)}) + response = self.c.get(url) + self.assertEqual(response.status_code,403) + + # Test if workout with workoutsource strava and privacy hidden can be seen by team member + def test_privacy_member(self): + login = self.c.login(username=self.u3.username, password=self.password3) + self.assertTrue(login) + + w = self.user_workouts[0] + url = reverse('workout_view',kwargs={'id':encoder.encode_hex(w.id)}) + response = self.c.get(url) + self.assertEqual(response.status_code,403) + + # same test as above but with user r and the response code should be 200 + def test_privacy_owner(self): + login = self.c.login(username=self.u.username, password=self.password) + self.assertTrue(login) + + w = self.user_workouts[0] + url = reverse('workout_view',kwargs={'id':encoder.encode_hex(w.id)}) + response = self.c.get(url) + self.assertEqual(response.status_code,200) + + + + # test if list_workouts returns all workouts for user r + def test_list_workouts(self): + login = self.c.login(username=self.u.username, password=self.password) + self.assertTrue(login) + + url = reverse('workouts_view') + response = self.c.get(url) + self.assertEqual(response.status_code,200) + + # the response.content is html, so we need to parse it + soup = BeautifulSoup(response.content, 'html.parser') + # the workouts look like ... and there should be 5 unique ids + # the id is a hex string + workouts = set([a['href'].split('/')[3] for a in soup.find_all('a') if a['href'].startswith('/rowers/workout/')]) + + # throw out "c2import", "nkimport", "stravaimport", "concept2import", "sporttracksimport" from the set + workouts = set([w for w in workouts if w not in [ + 'upload', 'addmanual', 'c2import', 'polarimport', 'rp3import', 'nkimport', 'stravaimport', 'concept2import', 'sporttracksimport']]) + + self.assertEqual(len(workouts),5) + + # same test as above but list_workouts with team id = self.team.id + def test_list_workouts_team(self): + login = self.c.login(username=self.u.username, password=self.password) + self.assertTrue(login) + + url = reverse('workouts_view',kwargs={'teamid':self.team.id}) + response = self.c.get(url) + self.assertEqual(response.status_code,200) + + # the response.content is html, so we need to parse it + soup = BeautifulSoup(response.content, 'html.parser') + # the workouts look like ... and there should be 5 unique ids + # the id is a hex string + workouts = set([a['href'].split('/')[3] for a in soup.find_all('a') if a['href'].startswith('/rowers/workout/')]) + + # throw out "c2import", "nkimport", "stravaimport", "concept2import", "sporttracksimport" from the set + workouts = set([w for w in workouts if w not in [ + 'upload', 'addmanual', 'c2import', 'polarimport', 'rp3import', 'nkimport', 'stravaimport', 'concept2import', 'sporttracksimport']]) + + self.assertEqual(len(workouts),0) + + # same test as the previous one but with self.r2 and the number of workouts found should 0 + def test_list_workouts_team_coach(self): + login = self.c.login(username=self.u2.username, password=self.password2) + self.assertTrue(login) + + url = reverse('workouts_view',kwargs={'teamid':self.team.id}) + response = self.c.get(url) + self.assertEqual(response.status_code,200) + + # the response.content is html, so we need to parse it + soup = BeautifulSoup(response.content, 'html.parser') + # the workouts look like ... and there should be 5 unique ids + # the id is a hex string + workouts = set([a['href'].split('/')[3] for a in soup.find_all('a') if a['href'].startswith('/rowers/workout/')]) + + # throw out "c2import", "nkimport", "stravaimport", "concept2import", "sporttracksimport" from the set + workouts = set([w for w in workouts if w not in [ + 'upload', 'addmanual', 'c2import', 'polarimport', 'rp3import', 'nkimport', 'stravaimport', 'concept2import', 'sporttracksimport']]) + + self.assertEqual(len(workouts),0) + + # same test as the previous one but with self.r3 and the number of workouts found should 0 + def test_list_workouts_team_member(self): + login = self.c.login(username=self.u3.username, password=self.password3) + self.assertTrue(login) + + url = reverse('workouts_view',kwargs={'teamid':self.team.id}) + response = self.c.get(url) + self.assertEqual(response.status_code,200) + + # the response.content is html, so we need to parse it + soup = BeautifulSoup(response.content, 'html.parser') + # the workouts look like ... and there should be 5 unique ids + # the id is a hex string + workouts = set([a['href'].split('/')[3] for a in soup.find_all('a') if a['href'].startswith('/rowers/workout/')]) + + # throw out "c2import", "nkimport", "stravaimport", "concept2import", "sporttracksimport" from the set + workouts = set([w for w in workouts if w not in [ + 'upload', 'addmanual', 'c2import', 'polarimport', 'rp3import', 'nkimport', 'stravaimport', 'concept2import', 'sporttracksimport']]) + + self.assertEqual(len(workouts),0) + + # now test strava import and test if the created workout has workoutsource strava and privacy hidden + @patch('rowers.utils.requests.get', side_effect=mocked_requests) + @patch('rowers.integrations.strava.requests.post', side_effect=mocked_requests) + @patch('rowers.dataprep.read_data') + def test_stravaimport(self, mock_get, mock_post, mocked_read_data): + login = self.c.login(username=self.u.username, password=self.password) + self.assertTrue(login) + + # remove all self.workouts + Workout.objects.filter(user=self.r).delete() + + # get the strava data like in test_strava_import in test_imports.py + response = self.c.get('/rowers/workout/stravaimport/12', follow=True) + expected_url = reverse('workout_import_view', kwargs={'source':'strava'}) + self.assertRedirects(response, expected_url, status_code=301, target_status_code=200) + + # check if the workout was created + ws = Workout.objects.filter(user=self.r) + self.assertEqual(len(ws),1) + w = ws[0] + self.assertEqual(w.workoutsource,'strava') + self.assertEqual(w.privacy,'hidden') + + + class OwnApi(TestCase): def setUp(self): @@ -35,17 +256,7 @@ class OwnApi(TestCase): birthdate=faker.profile()['birthdate'], gdproptin=True, ftpset=True,surveydone=True, gdproptindate=timezone.now(), - rowerplan='coach',subscription_id=1) - - - self.c = Client() - self.user_workouts = WorkoutFactory.create_batch(5, user=self.r) - self.factory = RequestFactory() - self.password = faker.word() - self.u.set_password(self.password) - self.u.save() - - self.factory = APIRequestFactory() + rowerplan='pro',subscription_id=1) def test_strokedataform(self): diff --git a/rowers/uploads.py b/rowers/uploads.py index 781271f7..83846898 100644 --- a/rowers/uploads.py +++ b/rowers/uploads.py @@ -141,6 +141,8 @@ def do_sync(w, options, quick=False): w.uploadedtostrava = options['stravaid'] # upload_to_strava = False do_strava_export = False + w.workoutsource = 'strava' + w.privacy = 'hidden' w.save() record = create_or_update_syncrecord(w.user, w, stravaid=options['stravaid']) except KeyError: From 8d888fcd4deff3c5121d2268bda2cc8bebb6c8d9 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Tue, 3 Dec 2024 23:21:12 +0100 Subject: [PATCH 02/49] passing basic tests developed --- rowers/tests/testdata/thyro.csv.gz | Bin 0 -> 95992 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 rowers/tests/testdata/thyro.csv.gz diff --git a/rowers/tests/testdata/thyro.csv.gz b/rowers/tests/testdata/thyro.csv.gz new file mode 100644 index 0000000000000000000000000000000000000000..df06716c52699d2ae73b9b8539d9f9aa337c99fc GIT binary patch literal 95992 zcmV)9K*hfwiwFqrc~55o|8!`1a&InUb9MmCy<3wbNp>dozQ3X$Ic6x(?-#vEyIL*H zBpPignI6}gGquAuPIr&>G|654dVSwH9udbKfh-^sGdWd=tU@Ld@Z(?jxnF+x>33iM z$L0U|>8J01{QA595iY$3OhzcmMduuU~(6`Nu#0;fG(x8_0LY|G)c>-~aBby!W4e z#hd);Z-4yZCw#=uT!6mspZ3rHTm8m&e}lLE&Fy#f{r<~;{g40p%XfeCPk+S6fAX78iXhJ$O9Hv0r@-HTRxc$)VvTJ<98NrGQ`Z`>(b> zzVGw+PYmNfU);6SltOhi1?N&JRbO!Rsm4%ZDxtKNQoH!Z4-VH5Qcw8Aa&0NM5U~dR zqifhc)UN&f!i&E|zA9ba^e0?MW4h`V7`oQWJCKj_g^34wNHO8zVfqz zi}M=dCB(`fRQ%vScy8uBG)l&k5s-fg<4%n;A_3n>I3F>(jDO`HEZ_Iz zLlgFaH(5LI=-kaO{K6}dRFWOrwRPW%54E)(bGyV|uOazZd-N{yV6<}e&bM5EPY)%; z>8M1X2Q>I4UB|ZMZTbEhl=d6|M`-DMzvOp4$;Yyy!NkwxeHJG1!oPfnd@VC3TqSPj ztINIR;Ij9w6O`DFjo=&M4aeRzx#?UVWRh_ue&IJ##Xa1!k0Hg>I}V_)J@MXd@x{Rhp|BWK!c5<1p^`5A>tyTv zj7sJ)=wJ#iRovrXyn%PvwH};{93z4De1K983iK6vQbNBbfYGqK&b8`tgGG5Y z7!VbgAwc|kM&wl)%mo5B_1th?D$j#ENKd2*m{~ZoeCz;0(D3pd+Va zPeUT)z7i5F{ZbCGFhSCAs|BiBU=|r$=sS201kcqkm>ldukI2ThM3(T|Rh6z-!Fzfx zzyX^JnPcF91i1iyh>`>$20`wMuUU1rfk;V1mXPPkD!e@j@&uoxMUevU=fPd55~5qE4R1ody%)F#?|vz~T&8JpkkTw(a((<9vC%YJ;a z5j7sNCByK+@qsq1te!d;ETL9{!)=kSB|LPOQ{?5p2N z>DTV8o*dr5OSG{lM%9K_J;m$Uqto+?@#+`;m6fOE=tPQ@05KFR_70XG48U56S2$>o zJYUpct?De?Jr2A=+wg*I$xV0N1-w;+K*1uRS>xE$4#fZxjzTD0!2qjk0dfjzbjfu( zPQZ77Re`1~W?%YFhxd2S6J9U(lDFs#s#^bSJUl;(k8?;O_C^iLz#sPPpxZ*b%0a}? zH-IVpoG%+NaZ{hcBXL8+hztcsf{Ges&b7k0#EF5n79onrv`crusgxQU?uSV=cnp-V zt=_>bg1TPsL*(V~MDJbzQmL;7i3-nU$b9TxpFv9Gb1*wou28Q$zDKi@i#IHVy0)h! zF?4aa+njY{bYVL<;A)g3I2|}B(1gMJ@%xRoedx)<*a6k0fR^ZBltR}AP9nM#9-bzJ z2OMZ{G3Rwb`|@{MVBi$9-aytP(Dg_CSwpA4S{8ZSM)Ht7jEl7cVUCwWEryjI zpcz3scIoyIQJvkzkft7H^#d&?!pL_3W$PNr6`P zAWZcfrt!{CbGz5WlrH*k>i=3qk}v@9cSwtn3Y4u1mRDw&@NQXw?#DS2qQCUbhqKFM zEHgszN7$+~dm7{sjF5CmogB3{ut6W`)X|p@Y_OIAO333T+1I)++_$?{DPeHprU~u@ zhNn-9Q1?<8X!`LnJjk=qfl+yA29c&dz4b8Fuz{q&?gJML;`H2gesoXUiK&~jvn#)W z(TL+vVs5~qlG1DHF}tE)VO~N%j48U5U88$Vpxfj~@m-3b1`)r=9a`Jtyod$bH;}~n zSvIx?=tFW)$r8+&o+x)R?cI8ao^Yx7Se(ty153ADw2TdhX{c|}g-Jh12LU@Z3^-Yj z!d;laG^*T25~A91O8T=No(1aFZUav)&4=v7{%#vkM%TtIogIV*?sRaZz^#d8_EZD! z{YZuF%a^p?+S}hW-rQi-L!<^ zF{O93w&I`;Htv0)aB~?-qR+MU)GigTgw=)TE#yEpXhcy7`)TT8m0>B_G8uV7iTRLz zfZz7wrisGzc7TffY}&Z;JonRWcqv}wr%qoytC1)Fw1JXn9rdLXq zvafq50-rNmMR(xBJBt9M=iZ=I}w0r7qJl!7KS*eBR?JKSH;BmZK#0LAw9XlKhIA=q>gH923 zCHv}kQebZRhnQXtr@rpqEo4n!`1Fyw@6>xQW}r+SLTDb!+>Y6?Ga+z^`?~JO%PVihJCnD90Y*>m8ioS{T1;^rngFK zAHT14UwHJ72`UIWr{;sTcSW}?&m*Fgz7!G=C=b~dw{X2I{!{E$j;>Ji!);#$k!YRH z^d?-&W<{kvh2%?g*4R}@>J^8iLhJL)NuYvN$uQ}G2?<998U|f+$~zSix*Mhl5{$X3 ztJs&kuRo@{K?!O=Y97!Fdd}R0hvJ3z4c5Cl-m5LAfSs0y(^%>3i-6{X_i5>xXhpf& zoH|w~?>FJWmKB4H28rVC_E!BN3_jO#1K+4Fmdu?H%?SDw^t&W>lZWM+E{Dnhm2jxx zK+em@?b# ziVmfng427@mF&yjX`mbA5y``#q)Nd)!IQ=tm#3N8%kjbf$U{GN37#B*H|Zcwge~yW4ecCdL5ig!h8rfauQ<4fVP!Z-k0`zCsNs5gIqN zI0j@7h+-PrLDQTGtmRnxK}fnEYi&&e$h4}hHALRQpY{5VTsWopWx!yC@^HIsn1IOx z7{~l8NdFXy2Ha&oEhZADoX4pb`J33`#Hq+Gri#NtlnNkJaY%nv(873mO0K*u% zp;tpnvA`z}EU+Y-dqu=1_O8#;eNn^T5dApz36pXVbuKXea`p}glZPgg<&~R>cmXDb zk?B-n(lbjPtM|)U5vzW@i!Lbam>3BY6`mY3ZgnWhO>(-JOOs8ERYEbPH%PMvi>CzF z3_lCoVS#{3)dJ)V2?s&xLBWGx4^EujlW`ghz30;8K5u)qgDx_%#>ckBbAYHiW;DYN z2`)Jpk}WxqyW%Ao*nL;k@G?P0oIc{eYm-8NO>fbGPebfSfC5R*QZ1j*XHyVrJj%ltJ~`{7}}G^uD=19++rzBkTnCDYfp2%^h(HJ zRmwx(+AVIN8pa9po{5O5gIzItJ&PmCtVpRdk|bRnZ*~irmZ&hVIH%f}(gQUP`Y`lA z^M{DVt2n-Ks{}}-q^9lwEV(`a!x^+ktQi^(it>Yo5QS7!7CmVX{h(KAdk(Lc9x)gX z^3b<#OH8R|!Ap`H3lkXZL9LmKBbf!&<=Xpx_<; zI()^k2C|_+IfK?BcgPoGtwLiDHe6mqDK-Y!y&>fw44?ZDZ20vcUi;K-USxC!*`I#Q z;zb{tj9VAwqPD|xSNgS6d6 z_XokxsUL3Nh?l{ZF8a_Cxv~hf=6Twng;bYX?5Tz9ox}~gk@8Hx1M+?Q<)MIxQYdCr zo%D~KnFh&?Zi&VbxFhz5sX#f7uVjyf!S+(P3fNt+Kfmnd5>V&d5NOc%x05G65#C@s z83$1?3m-daV32Zar8+@Io1oWegY!Eyl4E0D1|YUx9tvAo()e4ZISkbv8`W}QSzY(4 z%^BN9UCkGN=xuTED8_a7w3S1?-KOfNvh7Bsp5k$vdZtQ%P5J{imj{2$8Fa`Zjx`?? z&_?*SF)Y8XN&9IshGRVFQ&OJei!a3ocegw={eATxHAztcy<{aF%3A_EWGaeHnHPMq zv*VO{Ep+KSIBgbYIJ1!j02Le_aHmTW*}zaClV`wnYmR_uBv=SC4c}8_N=`vnaIE%N zd#QJfE%^<}cnABn>BeL)_}R2Y*%0!$Bk9$iy~td)%Lu|S?mXLGU3nkfafMYxU^Aim zvy)RUdtR(pu_Re}c~%UI9-N#(w~MSgV*M#NAnTBhKdOMa4o1aSz=8l!N2OS-6$X(Y zRdY(au+p=%6LFAKAL4xDEGg%+m3%SVvs9iwOYxO}Q8BxWx7Z{s8dW2&ABtiImQuI$@6DmKSeya3%S{PP^NiN1Y4)KsdINW$Ht4%z^Fwe`XZaA*!`@dJ8_ z=}3p%#x+AcOswLjpzYAoeQFMG6l!+#NATXo_8))qDeF6+fhDa`j^;|+?sE07c0)hMVQpuyiq=f zfD)M69;+3YA}nD_3i2R;KvxhLY8DO$rMuCw1>TBVd^$FXL0Z8EtT$Q{%A2(xl(hUH zr^ByHmUn8?Y3j!E4ee8<5^IU&AuEo3a##Q-sWV}+y~zMt{1sl~20TsUmxFOI9WWg} z!G=xGIo1S?8YtoO@hw{geK`Gew52MjihJY>biO*t#n z_5=|?L61RnkugB^3C3AsB}p&N7O|ksPyD7o#NADCikNT5-;r(rTdGp~tYE~Gmm>72 zk>8fOa^`4Of6&tJHxS@&C^&8EAP|fT>3MKMc_ld5R(WV>Ff4K^xXJTZGMmA$RwKE% zoGI--W7UcX~8Kq~i4q~;z5T1l;Lq__n z_Xpj5{Pl99m8bZzgXCX{738@*WW{ZcUjeHm78Euwmc}A9kj!W-)?XIZCk}~XmdG4D z!W1T*B5Bi<#Pd^2j?p#TD{V9XD_a$S1484cr|7^4hJdQ!jeVbV0mpie)stmD#^snY zA_r*-1*arGo3<>ywefF4_0nTiFZ$48)w$IZppwWiUForT-)Q6bBFW`FFFdapEnVZm z11dMAx(uisd)Qz{B93Y1iAvX;%-r#e$`Q zkK!jkdeEF=*v<}|FO}{C6Pkj=6pz1RJ<`Udqc$X8sJ0uO!nvw`bf#eXS*M6oxr9FR zyYxI~e-h`&aS~Ouge$YXvuD-+(NIw3n$9b;YKf*SwW9YOe*GTXQskJShRGipm$?Z#d5?n&MehNSupfF728m`W|cckrUJ{3RRDX~ z9(G+&1-(Pr>AIL>?FS{y_351pr zGtJHl#h}*Ju+mO!fF@afnB$2h97lB<9pV87E`%4vVD?+Gn0G+Mu?CxQzlk5R*@{3F zp_y1o70tR2Dtx$~7D%H?%&9yz#>vhYRnK$4{7R=18v)2eD9U+pDy25!@~}*)R+s2Q zi>vmhoQcONPRP_9nKEmCw~{K%(WxX-E7imfB6t;SsA#=POncGXz|PRVuIE_(K@1I{ zhn2q}t*qWIB|Q5XdLFC#O03dFA3`2BarRDRVSTqD-W8~5)%3OiRi}(}Xnww@g4IaN3$Mfsi<5`ez1Kx_W!-&ntL`#ne}v%5Ro0&ujl(8aBxk}{{>Jf+Z)LWu1SZTy zWEcw{WL^7oU_rlkkYBw9wwQ=`)sbou*w{rutHtLDv~1`&s5(C!12i1Fs@Cvy(vtk_ z%&n2!=c@Mgq7OIH@@HmQg0y|A%n*y!Rp@nCk8Do031{vij)={G(~deN%$9O7mh#Fk z*)@+14(cc(PLe5vQwWQKdWrYl+uS9nb$uuxjR4Amj`bhZ$XXsrUSiev3CDESc+Nt3 zIa3_d)9$JA($fcaB@Zp%ZHp&Si%H0%!P?khpXq~5PG4VGCb`_Mpa}8V&2q1AX7_EX zXLfg0$K%AY#9pE6?2avKIF@`6;L{u2v5#ENMQ1DrN$>D(c*WQ) zbw;*_2z*x93?eI|XbU9GI-#sKKbv4d1IAb!%%l#B&O4@&ZR&huc`sxe%OHF%5MALg zy;%B1OjzfrOE^}1te^R|4`na0gF(~7I>{@2Z@XvyuR^~s^tJM#y$brm2CK3`kdoL< zDnf}HX`6n@49wlcAXVd7T^w!CK7$ULV{%ve-jT%bp#fMteAkKx`& zHuzd>6)-@rsp`58G7h4Aev}(Bh^bbX^-5)>`3%7Rss4@v^5r44rZptVlC3@J<*pSw z>!>@rBH5LG2C)p4@bGxbzTKG5Ud`vqMoHb%qnp5kspwBGk5jF&9uT@z6uZz$Fd2m2 z5OR>^Ssn51_!objg=9-NghhYsJMnisyKkPrSBAn$xadQRO zeJqEX`D*zlhq}0h)YLyMx>DjrA8u4G&CZY}-gH1!Ay3)mpZ|!Z;_B=VF2VSNYV&!U!OOg3G^3sMwpI8rPP?~)U>Ym6R7+2D~pt~OJ*8Y&K&Xkgw=uC9Cd(t3c4 zyj@w&+)?`AKP}Y4M)2|w`fjoUq{|lx#*Gd;sdCm*6+PcAxw<790Vf8ewa&`K8*fK! zoLXmzDc#WiaZ=bKpqR=#-@007u*F^2z?zZ*^cia^+#6tJt+cg8Bqh8JR`@idtYDkv zp*h8I;f1Y>2g|XW4WtRq7n{t7#s48-C6j8XV9AMEomlBynWqD6t-1qib*zzOrC61^ z)+6oEx8R2;1IKn&lm$b_K^3#ZA982W88S+@th!&~LiBT>rT$9L*a!PEhGa2JKl?wl z^LIxMDLPHj zw~qVLR&?5@sqokbeP|=!HZwmlQ$>X*x53`ko?Xz|(Ke$~Z2`(i1gIxzI72IBqe$$y z^MIRa-;I8G(~XlOZ&-nsU_~d-X-6qehF5HwI+a-fI~H`T|Dc9F^&UG)4O=4T+)dhG zKZqT4rSjBadSwi=vY9p?LSSCpzl?5JkCcOA0sX=;`Aq7iSM8$Oauh?HNtN&SbEoK+ zw*o6GXbJWMU5e6bWlBW^mPOS<@p|l}tD2FmYgnJLvD4(1s&A+nk~}4`g7bcZ)g3$V zE+o?{{T(%4^dZ!ir9OqcN&P!DI{c$wB@R&*2*WLedH3VbT=Aq@ve#P;5fZ3Es>KEj zo2$vjSwL=V3L(e$=z@;rAH;a-dk;B<%c!Ny-g*(G3fq?tp!LeyR&0Vkw12jna|gV% zGx1VPQdu5{gI6>xQRb*i6-P%mEA)BsBevZM>=;{NDB00%lBgG8pxFAt%pFRmvhhG}diHpd~> zxS?wew}vsxv2Wy-$!c3ta@fHnS5B2kw0oIOqPYYo*6KYn>T{Z!}HYds)XfEHK}tZU0L5RAYy9K6KJcS*&X5Fnk(oS$!V0n_6b$QrtUO z$Gzj)7XSxF3yV25R`=1>lpRzXbMVM)Tb~;8mrD!>a-4%I$OXycB8yD6b(cO1!QBsH zcHCCm_=*jv_SfS%Z2qE;x1eFqMh+kwUtD9 zhmilqeST>glST{PIs_cN0?7h}OyqYh*TkWjjU#B=5MTq1TsIvc%ZDFJJr>R?x(5MRety33({Zt#( z>jSIqx04n0(~q?tE0;XmZ~Gr4_p|F9PaE|R?w>v%l6uj{9Y}aDdbVnm(NYmIJ2gp1 zGo^-sT)){=12I{})8IEuG^yI1j^<%$Z1Xm-m^Cd0b<7oo679t%rY?Osyg()Faw*C|EqBoAj~$;PbJVEf zTV+mw&wbg?Plu{s^l=9&j}xD_g6UoikyqT@Kf|E6K+VFQ3g;_B+Qa<5WB%nR?zB-&Auf?7ydJciX2U z*Et%Dk2}bTK0wZudZ--f*RhkJ$=kdw9y1e;QkM4wa!@H6+Ys&7H;~KPdyqOsZ->iV zR&@^D-AOK2nZSF3R-K9bG<^I;A9wND!t#-|*0}Rq zQS0Hfw@H5IxeL>T&3B{6X{(n3Lb(ad*DfhQ4DEG1@43fbWiV~+Rs#_dkLD(6^njjn zHxFM0UdSP|P&GqI%~hCYO9M3-2+g^7p|E#Hj#91K58V2|Emm(Eov6q zp5g_Ylg-%nYa{b{n0$Fj-&yPE zd~(*GI-D2K^}-Cq<00jboB|CYN_!y8O*5#mRGS*jnxCl4I!HMPOoI;801N& zboRHsYu;*m!inz#I%forF04+i@$^|J57t8FG*YJLs#U!`CJm+4cbAdB^dQ!YK7`n? zv>sxLWE7_c`>A%v)XNwp!6CJ|n+7Lc`MH5tS*q;X<_0;*_Vo2?dTC^B?wK>bRcnZx zN^D$;o|!u%-J(lT99DBI{vd^kX4VgKulS#R@U%T%wV_2iqpzvnnrrVs?X~Q)UG$-Y z%-0G9onw;N&d~*IZ)c6Y<4n&*V`;=zbIYYZ3xPSyG=na2UW zi#)W2$dZzZjXl~~a{}c+%}L#9DrWHqDPfsnau+pr^es2pO=)7D`JnoF)ZD%yFF_4j zpge?B?A@H4)+M8yMjB@*F~>5NF6|3aUgTXiX@#T5Ynz*g9=Zvba9}nky;iQ~wzYtK zbKneQULGlFGG+@aI2L>?9~}0+7Q&{x@urz|F9$*I>E`;%;w<>t~>Eyo-1gf{gJ#YJjnnc_P(_kdx_LPrF zCbF=DjDx7q9-QKo;yxM3n%GjMlHT3gFvXX`#>xV{gu*(H2(NT$d%0H%Z}6I72r8z)Jmu_|&58w;0bLSBE}Equgvbo+hk(O=w3~c0Q9t8b zIZ5A?Q|VNfw~FKub&qSce$Lgm(qb`DYt z23T^qGaw%A&|?_%ZY$dYh(@iChK7S8X4^iZXT;H1D3KSO=Xz3t9PmAT&R$v^F0JUs zrP&yMPUHYH685+R%}GF+qs=61AIdsf|XtkdLwU5FYskl~FH1QVevlF9PkH26 zRf$t;XAKKfILLgS6~4ButLGUXm!@AV#X!vXQlK1@V>27VR85qQi>;-y?`jKeN5jc8 zWn*jis6Y$1K@Cqpe3a-SGy0++sikF@BlkMBJ zBe~Iu3q^sIC2TKr1D}#v&U^U$y&c2{-{vS$Lb#FbuHCf(1ERnLgE*^2&e`FGVrP_HifQYo=!f=mEd3xPQaC=Wd8o%@ z);v~ekUy{IHT1K~b_t1PsoM%-^9fcxvfLQPk&~7mv94GZqj}_bxS`HMWbqnmud%h_ z8%`Bxh(X0Uj+d7L|J(jSrsKSWh#Q^Nd>Z;kv2|PeEc*LxpQv-hdebq;yOqG3ILTko zE5(w4aS2s5FFJ<~HNin(Z^vF8?VaB9jejE#S$GW-M)^cL%y+ay4u5jTLuYrzT$#lL zgNp;9G{a}>47znJgp7-(of;WowM<8!MSs5u9vR`#55t62dy^49iQG#Wp=TURFQKuO z6*&os;X`YOnvf&Yr@b#X@nOZ5;qJJ?m89VWWuZIrMwLjW#d`zj+tMwwm$Wk*Jm-@?=jC2Z0UB`D<#QH>yvQ3)D7$z zz0yml^2@+$+|g_y0?;7hpjH>C`OOK}PiYZvA|~f{q1qTq;L3AvS59^<(qf~tPP|?c zZV7DX@i-ZvZ00xg(VWJ)v`>%sx~hYSgCyp<)kngZ=@LS_8K11>hbg_L0{NwlqA0Pp zOX!Vmk#pGVBY_7dTPeP98mRX7=N_-;F5EQf&Gml#jHVVO9b0@Q_OI@cs5XD#yu0CX z>NI@-It-gg$#VKMZ(5(l=pM`5;i&5pZkifWs?4-L2ij|GYiYlvmynoNuYT638SdJ| zNw6>}ovRx0Sk+1qncJ_AKG^Xj+oRl(axj8h+#gLw%>%cBb6GRTjZQo=M?dgt8hyoY z9Lqn5iSdcp*_c>9xw8D*o{=ZJVZWAZQXSMw2+n@7v2z zN5_UUo4~7AzcRhGM>ZEKB~!-DxjHZkdb@TgX4P?GZ>0P9OEiPRdL846v|TUJ+n0Y3 z6Jvb>y*kDVSoGfZkUZJ4;kAYVwL-mw;Ig_Uix;;fcD7Y03+%N4L;2S19UF0-bE}MybT5AE5*~#3ZSPcOOIWf5h{gS)mzf4BBFCQ|( zUmHXV{9V0-#2S|KhJ8#T+l$m+dk9eza7xy@-ZR=}s&;?G}FJc(0aWqG1m+ z2a@U$0U0r697Ehh_(*%gYE0WKKIg|0js+jfk6=j;Aw+o=bOy`hIhO3(NuE8YN4l3H zOR+FjHLwRV4X%_qfvC?$8F=tud2CBNY_V=#!}=`p zxITmzr*Fs&lxCy4uZCHAI_)F83@`4BUL*LQ=5%1bP62Zr`$&JRXT1`Wn{Sw&?a!fz znbd(g4fN%t8K|0}(LI$MCqa$W0lWlHa{+fva)_+!qqC2Vj>~R6+bnKx7jI~Z`N7Z` zr^99)OFW6F;AUE%!|TOK1c8}LdW}qgv}hHz80S@Bv?Mp*U`|&j9m}%?EvvxWM}y$ObW^UU?~G7P1fOD_H7usZv7j#SdUbumDX zg&<9{?{UEAtt6`^!JtiCGM*dJHC;h8mrnU2mbW?oR@09j~n4%)cWMEJCxxp)gx+Rxl!OcxD;b_tpF$Z^7uUd-LC(N{OV zg!9U!-586YO=TX_S(7fv5Mbzu>y}J;#p)crArXS@lE5QjUx%`Nxkx!-1OEcQ z?hzYr0;DmNF>t$;d@wRymLbDXrOiL?tChl8Q<(f%8o#UW1?#0FI%summwxg(;6C)t zfynlrnNGsyrA1*5D^twY4rpf$wabo`MC)=FMI+WGqufM^xHe`sG$s_V0@xP4#~90` zmB130R-if9l^a50z2DLozZAp`IN0%yPmv#<)K9D2>`>b9OEUxe3yF)15(F`6SOia|_3%pWIf%O|xAf%R5_2RhU;*9JlNR zHg|?2p+^i{KE0)#8>Xog7b;EVjC#-F-Vn9($}$Is?w8E!7uF}08YgK6yFvXTO{Af$ z#;o2D6YKp%5mtbTAHSr@TO}>|%iLu^NqSEU7u|;UV+6{^HFDJABC@(T@FSFoj8jlE zq_$4iHZ75~88`?#<~?h2eBM=#IAwfSI;>iR=!+hpa|$Ze6`Ud&<#0=f z`sl=`6_T_R$kgAMK<`Odu`p&K^O&)-*EkIB7^joQf+1#3LNq|Xc7>B2c$hiOH@4oQ zY=LKo3FtWhvq8IWGmdTew3>(dsp~Q8H{`Gv+~g%W-AQ?aC`)8#nHj7d%IIdwKZ;)b z5PEERqu1yxwzPC%YV~fUQRDEe9Hf&YIiQtn^K(6lky=MH-qWZ}uXc+&@0~PnF)Z}Q zPbcwBZdqa;I*IqC=xPiNh9vNhN9;9uhXLWtI-{S7z4Z=11Do#2`(ewM?NSU8QF2=!F^yEUiIdMvCn@$`Hjy4vVrefWS4{NEk0=Y@9>-EG@R9?x(T4$bWF235- zRkoU#GWucf7rz~{od82mZ1pkzD5osk%w(xEn(38CPIu@10Jh6^DZvma^N|y0hd>9* zmJwz4sQ28j*jHwmE^UXuONT^1Gt2$RH5TSVM7Tz!r(;>tZ?0zc1&q*0XpLbjb7&ly z)u3wlUTgtW1XuM6A5%_rJ!bueoY)@fpepYW)e$!pTT_(qhtmPPO{!FypFV-%V<%7) za=FGpwq-Ck>l=;}m?5TF0>cEn&3ZC9J^3JdwasD9o zm>g1)X3}Pu1&-e>U~tv;rieX^Ls+^C$L7kq3^j(h*dJ6^STl6n>${{`0v37YedhWB zXkGgVv`PFXa*f20utF=9+4#yq9r$tT%!NCHv|QRmO7wk(bWh;|vtm98{gu7bPKOCt zugRNkljpX>p~f)4GcffVko(#VDe8-zo3Cy<0RD%|^f}gX%q!gIgM#X`8 z+{WRAYSa&jChIbE7_wr&=XsG|+B`2ad*wZw(%XHGk4~#kL6<%TUAS!5ps~+AStC1< zNhMkct#KGTtZXl~go*oWksOPfHb0DfdQ4@8Ztrf<=+J=9S(pmjd3*~E$bS6wlBC_3 zaSQp#p6zqbpWVG#Qxcw#yp`F8;W;-uTU^iQ2r$AhutC?#U=FEWPR` z+b?m_qXGsYd4zgoU4{%pRqPK{n#wzDj_?qK_TylpEp-j@1898hF`35wgj0RRl25F$%ZT^?g#Mjio<{At(9C;0i z^JPqX&&1GuJX&nY>inDnGM*;q9x@Aal+pqQJtn_r@-yupqPX35mr{>T9?0Pr4Z=!B$G~<5pOR7R4cdH= zP+=l)kaRHR!B4H9EP=xg;9ScSaNd$(s7iDXJaq89WFJv)nF6jU++&&VLN0zx%`BJg zQX1KMct$P{#XuA5}W95N$p$8Wqy~$s~>`nsa<*v8v82owz(LzjZ!3(lc zP*SjKH>4!j?!y%%y_P!34A(;52NC+9P zl58wDfyk6~#OZJ~=A5`haTL1rYAE5RcP@TAyUaLj#wSt}GTF{aW)cco;MjdTRQvi3 zImz{h^nV^O@iSA4Ty}Fuul)HATl!GaLzYG=a_Rd(g_w6Sz&VZuU26dK}1B(=#z z98i{(Px^AgC&foyuQ*KrGeh%dKV%tau{4y#LMaJ37=^|h4TD3$%u%^ysW4Y`HD>jO zn8Z@_N2DB!SVg`SKP!|~e)ugZ_r8PpAt^^yY+NI;Ysq_NEoTuIO5|{o)}dpw(WE#Q z7H3$hhnu<`TtbUcoMy$?zUDS$7{pFI~245WIRY$JjjV9V-wm%S#J8|IpR+qu80DOy-}7KPw$ErxZSE~+ z+Ssn%$UR8WTw&zVM-8c4Y*`&npi@JS+_&m_%=!&E$@ja8CVnYuYPO%kuq%Zh=8NHD ze39F$mlD?6ic=WI091usv4b!PQWq)~M|1XtE23`N+1W|5lDY$Ig#%>SSYuLB`qr$5Q^TsR$E@FwlW^7^az8U)TCycQs6P2t^XRmmbCRC93=^1I&k6$(l_bezYy-!64{QKktaZmMiE8VAisRrbV`NGU~|O zuz%`pjCSa~(s{H16BklB(s70N3&#q1H1S~lL53#FoQfbJ-D{m4ptM`C5LTiUM}etMBKn6CRc-vgt5TU0XlLir0~6ffH~Mw+b=1+6-T zvB&1;oNQF0q`uBgaHXwY?uVUMwGZL0rg*pUM%fH$mKNztXn7Z1?Fh-Y8Cd@vESgS8bEMK;3 zv@vaq#44snFpD-cur~dHjCFYfIdO58U?ooEJq}V!F9)e!wrezhSOr*>I4Zu*jQ=*=<(>L?(TeH*{ z;-pLWx9x6y2Tgh^Q}z%9$VB|(Rjq%9L)I?aHJU88#Z{?uWXK|WV@Eu_Zwv20zv5@- z0ZVGVsVr}MD-_KIVg6C`;B@Q>!OS>Fe%r61p18lv%zH3u^^-tb?7A2~Czy<L$$uoJ6*9+WDcYi-Z{jyzS_}p3r$%1P+{A?*xntlyBgl*9}vei1G76lNX zy_udlV)+VwmJ%D@GjGxtSh?0dgmp@-6o~%p!Cp9N{tUKi2F%Jm*wf4g<&ed5Rnf8fI3164 z64v<=_ROx#Qe}ZIL7Pa%5EJC$r10z0RPJ{};V-G0I475vb`Pv?ow5FModcH6c8#fl zt752Kb$Nr*TCOR^q*E#V3Ke^yRAc^Yd!h{}@h$|Vu&{EbI<_*z{Vo>s4N^=}u{<;6 zi;-AR8Yhi|okw+1h6F=VD)$X$Ke*w9QJ*M$+^_LxfQYw|UUwkstoUP7hz_MaB{jO8!VJg>xcd0B6=6v6V)npFB+#mH+VDwEQ{QNuF+wXKyQO)FU@<++>3D+`Xk-@_(TuCqAzh=35+EMX3`y@{}z}ACpj6u^#t)1CM)0In?Xbbe2JkYfRZ( zV<^2}hq(bp1v|5{*u~Qdt;T|pbc`w)Rkmo<`*7Q`P|g&S87b%hc?qF0CA3U!XMo!& zUb_}pd>^HUsTOvN#FxQ0kj~{RU|xQBr!n5+o+$5MuNTq=)@#hLT3k^q zB5JpGGQBe`E+aT9T~TquZ|UtSaw{m3^poIlu?&?}7>u@=$!;tlu53$BJv7k&@B0K_ z0JH->b`X-~nB^O4IKJi)`1l4Aii-23%c{h_0KS*|>6u_Zt}!olF~YPIg+^G5)HjNB z%$3=l#+{}Ttp;O-mA>QWH~1uYK4vXL1{F`m4mw9?funi(^cxcD>lmiSAumT+HIFTjLwY$Hduh#IB^u$fU2mhoVUq>>lvqH}K8gkf$x$Uc8efMi zV}+h=4|3!nr=poOuZ3gx1OHXR5ii^ICLD}aFta%o=W$1u2l4PyD6GElHb>x@-{pOW zvRrBSBx!Cj&;Gn^(PFxSf}qLEAUCwc+J8>?U<$CADxy@; z%i>Mj-N%7pUDunyz-00pFf34#jHi96X64!4^@3f~ITR3m^VZdOv=DwhFnn*l26<%; z>7}qwloHl_^qOJ+-wS(z9chWu(1xzOy?0+6e9dnF35cTt6fi&wgXn36B8Vqpcf6Gh zeL%|kdtrbG!iTdjGD~z0K3Dzd(*VGDh`1UmFM(d*6VNH?Fh{r%4FdPtkx=9uH;Mp zOMo>u<^HPytMh&J8e^IKqJT$kJ@uvT1M5R&CLdD|uWnKwvhe#n&rRCSQrm9V!2@rs z(JYX@a^+Xs|AmVl#gygD-LN`b1hAGj8?c0)>L`qDQwJ$Zw?d*gSQn*8J&d9Z(Z z&~RsuYf!o>eJpvnWDZ*A7D~sOEK?!P=#Z#J6Or7M$MO_r{wkWJ-@oQ>fX3N{rjjag zil?s0kYFgv{*g3hd4m(|`b_TUp%pIMHE3JVgSg`-?qClMS=c9mGOoK*K4avH}u z>K4ORD-}0$pW_UnXLQG6|0H##TyBXiK=r5`k5n$bUnASSHJKa|T(_gIc(mLUJ1MRM%x#=hLV7aSL1orX2%k6pn|JuQ1~>DI-ht2C^p3zzN+CdnW8FMQsn9vP zdX(BU{#IKHEn(TV2P+c2Mai+pMz0*n1dHp+?#$bRgCU_kM)1KDU4%4EX*zE`jsl!i zvP4c(v1AykvhTC{amPaD_|2q=@Y6V#78Y`E25PW=Bbrkv(vW4h1%2~L`>;_?NKY2?HbD7GW81k z!PFw0j+)p}7dm-YudV5Mr1H82GZS#k9jNn&>Ap{JWD{ogu)+DMoH7E|$-!D~&wj}2 z0L4!4g`@Sb#-N+*qAX{dS;;mU2Ni~>%prK+WSH^>LBsfIN??%`LQ8adYL;Vq>0Cgn z?c-7s*O_xENzM^TXca;(bNISzGCg!XXfo%vnPvKTwC0*8Z{l5LK78a%@0i#YgsshE zJs#&{ICpn@#Ut(_PNqKF+XMrnad z*PdilX3P)p6Z#ZRCZ$H5OB~oZom1LdEUn;Yu<&f|Ij0I=VPi+E zqLzr4CR>YBaZ+{*P5iW+k8luR=*cnMFCz70N77tx)5=2Plc(yXqrUmVagDm4r8iK5 zT6c{4W>aLGs;pm=43S>*&1n+tioPDMbabXK>#T36?ktdRF;)&|WUi#c5!xT|OwkVq zSm^-kn6l_~VTK$-9Ua2oRzTzp0&D;0?OWCUy6nMohc3Nxlr&V^%XW>i@Jre(rv@j% zk<(*)iiOFuazo}7#$sXA>BQ~?#-Or|p_y2vhjV04gI2C47hNiS>Xp?%lC~L>jJrLA zo2)`3k{rw}>oLnW)a3YBaSo$I+7F;kwsmU@wa*>D>|P2Oc*MAdhj2+uhXx|bNV@h? zA@*BynHJVEfxeGh@z6%{p=h1;=E|&3?J<`QHs~VVA9YQ{JTu((1ejVvEPD*O>Uzxj z4LNjXJ)(wC;g@OMtKHTYYO{Oli1n8u2J2&7BRaRFJ|`{W24WmcLyH(2n8y716|r`M zSShny+Z$s`eN!!!?eN&*pa6#({ZV`v#?dv+B!lfX6v!GRNS~5jzab|-RK+cC5UP%n zcWa~99loxwoPM0%3uY-V1v6f@YrOk1j5BE}#_9390kaz=MGfxU=Khd<2mhvi!$`9R72HAZh@G_&4oXL-H?m|U^dj?2bAGakZCRw~{*_#;VspC0q?#`Lyj?SzK z3qIp8s>mxX1=)ijJt&}E6{R?KnFE;T z8OD7j{3-C}&UqgP4|mwOMq*c3!83DJVVXrJMa9@p5VBf$dgOx9Hs4^L?CKZY^@6-!@Sw-YK#0dYIGIh{HhUKoH}Rw-8z3lcKYBB(1o!OpHDY6#n%Kpmzs$0N?Ex{A zX$AC#y_yu51|b+XoimrCdA4{|4q^;#?6LO{d^m%8!lxS6zIargixHd_%)TEy1|;ol1h&Q4?*Kh1P(0K~F5@x;38$!eZ{@mwy-%8iJ_n_f*FYZ=p%$)On+%;42dkW1 zlSz)a=U7&CjW<#*-$0hjJ^d0)3uyEpSa%S0Nz{@G&g`IeK<3Z{H$Bx_ zgz~?+EpsxSl^884hO*2iV2?PQ=~>ZvTT{QnUKhfjWR@>A4m=BZ^co4Kc{y+05BoAe z+<02MNgJKSykb{w#aoyi&Tsj|+Fn)379DI`A5+XzcN3_xVQ7(s5epL@$ad|9lw93i zr6lH4-&dbJnY}HvJ_%QNDO~=tU88MBUL4MSjFGwGpmiJ%Pq(v#IQj}#y_ITymTG%k zhfQ4BE&v8M4iXbh+cjCUyvSf{=qe>uDvU`+q3&_GmZ@JVaH>UeVD_+40Qv1}5p>cqY!jyC0%dO*6ab+Wb8; zo9^4!nc3x~4om1?+ck`eGP}RB8?9ZiyM+!giu~@VW9L=r<`==`(y3pQHj><^sV zj+NGPoj+eAg9e>R`nY{2X@->!_`F#C*)HDDl54+<9%~Ty0l)a;(Od+ z<5Hq(T>*+l#b*aW$KDk<6rHMNoN|ErxE+mSD=c2hIm#_{nZ+j@#=Tg&K* z4eL~-*o)T)GA0Hc|-W5vU!3q)&Ap|bF1%Jln+J*IJCoa$RE>!Vw`5dbsx8cldvzzkYFgv{lVB=9pQoO z(}dYd@RJAZwX%=7k$R0;bbbYGi_Ov707=hE3dzMwZN;yC3+KW?} z3AI=O1XYA>m#XzCcLr1kVxiAI)=j$JZV{*fQf% zdZi>_!m({J)}!%Lk=EvGR6ID_WOUMAw&1`Bm8*x6U6WnIM|9Ug-HG<%IIRPQQFNVE z0MMC^s5pKQM~u~%RBRvmV(BnsWtcnOfSiWeWiQnGSd;mT9?7t}zy z?{VKVKz9mSz2Tdw%_*^Umvs>LP%b#nsAQM}MY6w)Rw-laP87?d4@*yMuxRT%90yMf z&~|bRb!Dx7djmij5wR+m{X+nzm&OD_xNMh_Se7hHW|Ue0ql0->n}Zmy;%!~sD!f6D zBy_FQ$^InoE1lO3a3-;FfA!NWq9ci+=Q=_;>mZ@oU;kLUPu`_dYdL-CJF59K(`LNt0&eGS!T*Oy@FYz+lKGe-2m)UmrQ-i zl<0@l&pOH;^VF@3j^G8m&*YQJ91EP;8j{UOfvpiWBPEs+w%o5{{e~PI_J<9I zuHNc4A(mFwnLp8Bz-=`yCDHdeB9wJV2PfJ#W`sb?=*zURKJG(fEacI_af3^=e81_P zg@w8?q|5>Xd_tFM?QHG!B&jHgWkf9f$%*|J4ud_45L3J|4E|-{*g=IMs)YE~z_CzO zsMZOqywLh2RxgdPLcDC3k`>*0y6Tknl?%8Fqiry}YCf3}$9vIuq4VK|K9tS=Mot4g zW7SrRNZuFpTN#MtI20+&sP$27%r;zQLu;qQkX1r?Oxp}2l_9&CFP0A>_gc?0N79T- zNlZ(5an4Tk%X+2n337zJduSFe8DDx!nlzNMcOYLgzI0$Z1Q^40Y-f`qORsX;DDlt& zQ-&JL8dkv6c9c9jg$qh4X#JI4vZ0`a{+LRMjp%y<^np=bvb)MN5LG{QU;i*jk&RaL zQWC+iW?N1Nnvw?TXVGAp7EvPOjqoZ~c$LND+~(*G){7%ra*z0&8sk}LmDAK=DELOR z5yoRO)BU0gc?_wAiYb~9)4PuK8*)kv52tq}%~_vf1ir25UFGxq&5;#;dMSyV$1)?P zrNlB^FR(1bXH9=wLj-_OLO5@@xM{H+_$0fxs(#{H*F+Qm8r=gwO%)EFg9l&&`iY}+gv79U+hOUyr+ZjY! zm_9|~!Z&Jj%IC#%zMJhZ;*xjDR#iTq_rhr9YJ)XF;%qRL zvC+&waGi0sv_nK~QXy)?T<&)DhM4kJ(OcgdU$hw`x%1}9E$?@6ytKrdiqW`~NWXL_ zl$I^K5r$8}enP2hImOdJcL+E7d~EZ4xN6E4p*%S3ER#VRL<%RKFU-&te22I&1I($P zOZp{*7XoKzbm?g$Ix5g(Xm2r4A@5Q;F;*0U6(HkmF<5GIVnyXi{ zX#CXB!KGLqh|_l(<_?l$k|vo}PU3FqJj48?IN+@K^f-XEmjNWlR1bI4L4cvB-~c@8 z_diQHzwbBRKKH#s?xmhAoUZLs;-P-ce&gCk#t^f*M84G;*8)^=iv)3tOy*l!6qIV* zBGB!a1O!$ZJBU1~&UyPxwyhm}fJqVNqy~+eHQC~w=z(Fj=_euRY+7pn2OWm20&C#W_BC37oTe;>&{y-JKZV>&W2JN* zj7v#uaohTVT=ydLU@V2&U_5Y8de=(gu=p%Pvt1XeDJ4EhT*%i}AbP;|O7~>CXSKw) zRaeK^ojOI`R_)(qiR6AYb)y+FxnVs0LP)ly)$;jYUu%Qw`N`dBUk-$1MN>#~~$ zys!}dr1$crj%VIs<5F^0qg^ve1-x3=>ZH?BR?BtOFr&W#&s6S|sTtx))2P#sDF+|H zGAK|wo6WBzc3GX5UYez-TIgpxU7wcb-gLB#?iiEPGR7C`lCO=KO8a5Gl*mw5%#v}m zoMe=&sj)HKYLsq|oYvh#R_|DKw?UpF6LDdTwaRAscF0#`F9rp!lswjBh%hvj79a12 zDDFEydCOv2Gh&}pCSRJ1Cxt3{DT%SoF*&A@CW-N|aHn&Nv7zOr?Wo?J#tg4#?S0Ez zaRM~~aA!dMkoEwLqmoQ*m4pK|A;B{=V8c4zuHBGQ*je!r1=5c=L7oO02CWD3??vmy z1{-~LdMSx%DTyl*sNtu=(rvMJuruc+a3A{$SGqZ0CiOh6O%~Xw7jg|sy<^An)Y*E% z{CV304ZXXn2O3;k*ce?1$NCL9rS->RsCWZOQd+#xlIJ^>@IQyxYc)uE1@ux9%i=64 zS|FC$cLyeJ2FK#mwv(5bmZP7StI0CBdpfv*vvVqQzM0}atbOoV`H^@$C6(u z2e!jQgJ>2^+3JB7plA2eFNaDfiQ)p2_jxSH5Mbyjy+2gU%{M4|AIy;Th1};Ld+}kC zigUS?#J&s;V1lPMbK;1+1Z|+l;}SmICAI=43hHOZ}6K=t}*-3v3-SiN-S zv5SjhAGQGy9Elg$O{q5TZ&dm$H|Q$CG|x^W|5Mbh(0$pZ0t!I2(=(k^{P%P zmy0$iF^e~}RG;7K?W7*6G^rqI>0I?RL@%|zGLHMWl&sVi>nrxDyAy75c9vvbV3B&? z8((k|Cr7c=rDNz+T{Dl@19dXP=r-5jNQPtw8m;$rDaFkje2`(Ns=hte)oDLnbB;ku z3%Soi_gYs+mbk23N@Q^h6HMAsCP=%U=3~xmMz{L0MSYa~oIPo)-7V6NcZPG^78agW zX&)?+x`(0`H7p{i?GzY-p!z|tbY5Htv7K?tF#qr2D96mj1sSA_^R1fC4y7Vw~qj9e+RXhTB{?d?eb znt3~=g4{I9c@mnJinKyMwO&eAUs_m{9LCHdwrr_|fsVkTp>t+`8@Hh0i~UeLPBQz? zoK!IU?4Q#5|}Y=rQEg=pS?9r`?w|#m#hX#r>haCSX_09O z3K;vZjUdXgd_zrT?)HOGm>O=|rDeZHVfyDqIg1nl$)zNQJ||w45=NBMaqMgdS1Pe- z>3>)bS(XFjEnucY+C1s4bO?^zTPvKq*C5|nm1@Ig_*B4?V9-I01%jFBsMc7kRV@Hm zb5JAZ%6UVGp{u3`xg}(RoU$AzJ3EIp1?ws3Ufc1Y(ATY(l9-kp)v^x|>o^q9*Xh5F zH%%%;70LcK+p*nC=k z%;q!UqmNXtx8aL2{I$CbHpT{N55l*{X}DK$1J7MKfiF~7nW@Nz3YR6!#uBy@@Q#4! z%fQIi>Gr3m_6I|CRS`}TvB0DVS!brGj$)p%1olIQ~RZpgsa<0v~(Q9E^j?C zSk0ENrWe}l;2I?}z5Th)*K4QF7FJQ}r8Ks%C?id}F16WJsCT~!?Z?6RA@e7Vzbj_j zP8=Z_PMg6Yf@v0j3^S?N03A0&&oCOnG&vY9RGd1m9X*+36>LZ?diIAE$XsVIj0-XZ z7~?JOHPTee0~%hM^d3k|U0xA4d*aOV$OM^&4_(xtGkN zzobT5n#^n^u`Q$dK7d)vFT;%U=JawJ%3KC07EK~+Y{OL9hKczeiSfc~^DAhQh&5RN z&Cj_#`3*HP-;`cJnXAxNQVEU9D-3{eoVh?BG%;)4MU#yn3p0cmx@swR!Q(Hf3K^%g zx#AX5-vQUln)_5X_|i+s@=I&-(%libVS_RnyN(mrT9*OcYwAHRwr;zf`sX#3@uwsdyJ*CTJ2n|hoqfNax3?b+nrkNo4L>RfKj8Pv6Z@)^&Vsx zs%m{~kji1T-JknR2jF7DePX^{l}XidDZNnCm-AP|VFGI!+uS+LtNE%;Rn}G)l6ybr zg%>wFO!7Xss9h;PN=l7AOKyeK`p6#h2*^k@$?lg?R|n0P)+cNmd|()VV$kPBa2Yh9roeObz+pN;+IfF3-WI<~}5Y z`Pgyi<11@&3AZN?g)nGTbx7$?J1#tX*OjkhiHUgv31xHCyBM6~wQ)ux#BB{FxrL5g zXq~RR?u=D7&2AjVcva`G*c#u74R&KwY0X`Yko4Kre%Il<#DT%OXiJn~x%^N}k+$zp z{%Xazr^pZH5{sUHG99@jum4N*X4kJawqWW4Hv}Kq8a1AlzD~p2fyM?8*jY)$C}Ecsdx!r#}X6asxdKerODC4kc4y#K}97|$#w@8-KtZ7 z<_X9UUN~^0l(M1Tx;$uO?N=V_1IG?J4N&&r-n=!#*uFtrdAEdJ=&3FEXlh2$uwFmQ#R9VBm3ULyaAq&0J&~diZ>sQ(~TQ8Dc&gCWX zpf@p}4oEu8lrO1t8gCw=dDD5#;k^fc7r8t-R8Uxml_dt_#-2PF`ZS!ge6JCSg)_PA z#5l59mBZ}7Px4+VlUm|e(CPL7^|LC?=(le{4i|JKjq~}HIR&4YTUa7@C}fVf^hJ8) zm-WkM_Z$M^zQn{l@sl*1qLc*i?sBv|} zgU_azF*&!|5@lGi`E_z`yBzxjZq73l_>-CNvqc*iy&3|n#o zxxN*gIqh(n>&tlUfz@!D&Cs|@+_o;=tRZa zrxK4j1Jfr`lp)Hl!ldCQ)pF~S&L?I0ld9sgQ^OViuP-stPFlh_9723}n-PgL-l0#8 z<|D*;=LH3n)l-g}lM@Vn!el?y<27xP0+rPz6S}+L_o}mjs2^V0Qru1LJ+}M( zCL5T!KtDU-Sc!a+B}Tewk|$qG?rq0m7m&L%;@IqBn9G3~7q><8iU@miV9Fa;cjFOVAW>kGv7AS*?H@E51+F>~{1eLjr^6Qb}N3*y}-cng&q@T2Ba|~S@%;W9i zv(GAGS5}$bm2~Z^PR)W5j=ol!qg~^X!mev(DVY>F*oyyiXWcST7FT?S|0r5s>hYB| z#rwNDVM3YahJI?9bkKeYW`4A6Qi+e0B?eM+Z**7g>?JmnH>hAQWYdP3{9HE4S=GIg zc^N;(tv=~CXTx72cSGWgULn2Y|J*tH5z2X^Arwtr=IgRR8H7J_kA5Z{vZQuEFf6iY zzusie9*y{9^(96k?H|3yNvOTH+uR(;MWtz7^(b$Om0{nJ+c3pGNQgR4vIbzM=B8< zV3nWyvZQIT-P0Fpp?j=spn7TF66gcN?wce~&iV>l^GGGnT)ex<7U@e^{m{yW!pZ8J zgNv=))EYi7V*N64wEWg*kIs2i3f{^Rd&|VIkW)VDk>q{}r2njo9#eB!F`5;_UzBRQ zj=-iKxiG42xFoNe4FA&iObwyX5b8&5hcc_Mq;ofRj4P>RiKE%S_1v?eJ|)9tiHX%s zz0+)73aHPBg`#KT#C6ebf{-p~l$nxY&v5R?y%61AAq!WUhU8mVxqPG`cLyC@7AS*o zsNX9@Hb&GHwIEY2FHj4=bl|ga$F*lQPGhp<-96#uf?tnxbnh!5e3t?VaA`CnsJvQ%B~XA zTk4E#S7>FKDkQ&k*OSF9Z}6mjiCOqqk3mW?gBBdiPB^a;(U4M6&a>0Pk`V;|3ZS== z)a2D^h~sfEW=?%4Byv1E&zeajx1u;LSrhc;%i0`eU5WKJW=KeC@=#nZGw|fsH531+ z3t?q6iX1Cc>ZW-x@yX`$pYoS zo4-Q!K@*9#)6x&T3-V=2?)IhE*{_G1WHs-gdLONLc|*bhSz<9R_%Y&?plhpKlqTMc zl9c;8i*Ft^yRU0+Q4Z_E3cH@^<-W+^y}8e`SUOgeuZLH*l=S|L6v4&ueyjFRAccor zgk-2MF)K~8jk%hGDFew<;*=Dp2=cDqRcHElGZfnDcSY> z%A7)4j=P!wX1&#_`x_a}v$q;@%D%+JJhyDB?;ay~8HXXeUrkwN7J4p4YtmLVsI#7k zyAWg<)PcV^q(H7WgR3+RNn8qjS|Hje_(uKk$`%~0cgvAI1{n$^f>-&+Y$oPWXW{Jc zOAIV-0w7c&Q^tFV9{-TvbJ*2MO4Sjgd08jv5o4+)u+7mN!gIOD`uHpP z*iq*rD+>_b-n+H<4b0}&3V)<_eY7;ajnjlxM7k(Wihad78`9a^$l)yEbG&d1^f)gptG$F6l8BF81wn5I?Hzc&Ao zAsqpZr$hlxUDX$jT|mm?)MkT`oasqtUZNmWH7w(8~F^Sd*Tk!F6CgLh_W{3m3Z`)#i$mHZp9M$(m z90|jeBW1h|>x0|!(e{BSO>bXfa5Q5btVnYhj*R?|xT0_-imK`A$*4W)&Z_$Mtlg-Z zmhYtN&yBtk@g_cH)Y~Z|{3O}fK|_IKB8UIZ=U3*G`i8sSoh*8~VbF?$Kl$u-?J_-D z_+t67FENti+M~}jlRZ3#e2dIZ*f4@-h%@@6CapQ(rXi3qF5P8&2QTy7>$!@|$+=E9 z8l23KQi|kybY)4YH`(jYB(ILHYbUfH%3k?D(IxQgFZU$|=Q$L0uHS;88V}oZ#VJ;{ zKFeQv*lxn*Eh*m!sXP=G*K6;}$`eVp#btO!_HbDq$jAr&{Y+|tikE;5NRM8Mar=48RRzD zXqrw^4o=g@Cf~KJ@CV4ZH{E=vA<8a%qv7TY?`}TPY|6t{^bek*&$bV!0@aroT=TpY z>(BujWD2(O!nNwK)V-V*cU#EiZdx1(2`)NBPuQH9qK^urp7_Vaos=n=$Zaljly%jN zC+jNLVIJh&sBe(hO$r>JYCo0yCyfme0#yU59=u z)5KYu%uxE#(T@Sq?ygKj;oQi{kV37GJhq15hbXN)((?}*b1h#t!*GavS*^VdS1BLCU_0kI&ti@=oDk1{bDXjV*ix#p0Q#_a!F!sq?^0>eWc{;zGCN5z)TTOO1sE z8Y;`-S1@Vs!L?VUwja)czWpMl4f@Im)`~Zn6X9!-3fk4xGqGKAKfHJWyX^Vw_|-3%8w;HnaG*r0yarsbPm>nB6#l zS++M|c`XROzX4}`>C24;X@n-rB)M?Ud6Y5Aw(7#IK&HuE+GUWWRU&^%5kGVpk~gt0 zF*wEPByAFJOc8xc!SV4$eAqfJG0tura4k`}am?$jxRENGF}`^Fc#A*^ z2Z9Nj8X#5ES;IU9%zK&hGCzpzlX@L8)w{!k~4A95yA*}la1JSqB= zXnVe&@LC(EGcU)qDX;OuSJz@tiWP@5havNMz}ae9WUr(#;|!;gs^(+#Gu7mw zX~RO+=FuiFE7yIA!Da4V#L%;THtnM2TdSo_dQHs> z-u+63)xYQKh~JexrVM+KzGo*%e^KO_wS_m>Zpq?U3-BTV@>kZ6+{B6_CB{`fDQ$-` zt9pMg>8D^(y9tJ~)s9Ag5?tN#^uENvTBppSqs9C;ODlM&%sH5xN3U@@uemMz zSvyjhqAWvHd%Gwju}mFu2^*et<+Fh)?ye=p*8RPs$eN6H?Dyob&#ELGR7 z=c!=Q+{Zy)c^>ur${gI8_v$c7Udx1aYE{QS%r!iVonhM4mzWqPYPgb3pC)4O)rTDL zdSrlf-EnR$rMV5*DegN=D;?u!f z4-?)bEjyWB-u9~N>n0riNW6awN5A3Qyzp!}b~R=1Y@g5!+5lw_9{V;`oB-(uzRgyF zd`%Vi(P1%`GWrsObDbj6Vta8raLCq^)vb$9yKz*7m;H>i*iIU`Z4`5is>E;4f=o>~ zyOC#V%?lEv3N8j_#Y5Wjd}3ulP4`#_8oOM(`0Q};IWrpg+jj1weR-zZeTj+HOl7kY z1_m8=xJf&Je}=l5WA}}dV>e5Cr45>mIC1tRQ0dcGXYZvAC)6TSy`eeHl4DRFN7Av`@Mxdi ziCfaFgrSzdNqB0ZG0pwQ**TWoO)_!NtDP6Lo#6)D6{I_;jnTN*bWuV26%lavwM_Fv-QsV5_@syj~lQ0UeL_R7({f{ z`68KlwDZQRikq=#<~um{zUQ~g6IQ|&cUGozQ%`U1iycyIwE@Z=e7k#3Dq1i?jLVy}-=o>!%a%nlAhNo)n=2PTJ||D4er?)7C%;$K&!Ctjpo!~JX% z6+;%?=D-k()nus6lcy=lvYKBH(>7_Ex;EL}{9*m2((AMF32ukJ#H^WoteTN<;B7s+ z^qYKk+td$I_uUGdr*N6h1=5br>N-4e%ULN%Q%`%YY}$c|tbbW^y=jMeIc#g0M9;Oj1KahnmPaVSvwdPxvVtacj{sY%Lca@%ZQ z8$uUoqVU)hM>4m<;#5^?^s!5$4 zwM~&EXA*)731Q-t&pE$fm3)209Dxjk2XjE?Mdk9&DM#rxSNiX=3%y}IB_y1uCFr_l z;WZ}CW9zmj3YP`Spo-J*R(XtlJh}Xagxr-sB?|u0IhBA1>Pt*4cG_>&4C!5j6LIY! zTWYc|n@WlcD^FP&Jy%sfVb1UB$hhenS{Gg+7&qgm7CKJtIwsxCaX31C9+ZNDwnv#) z%gysM9I0L!J$R#}@)^Q8oQPk?5`#}k?$e*X55GGcJk%>QQ~-Ai`@UTC){U9EOpr}! zMB0i`vNJT-#8z6zcsa84VL+sPwm**_CB^%fc%VkBcA(=iT26DwN`+X2oE!Sr=#hS-V9 zs2)~_8_#M>lwmkDZ?hQ0x)4nJgLUMec7-0z7I<~_B_`UbpFl*i6flcA(&o)p`&E94 zXLr|?4W!KNg%gMo(##+~1f9{v?zyD$=)UPLGn7>g6LE9XiG;$m>D-?-U3&JI#MRK3 znCQ}2)kId_9EZq))G5sH?4t8j`*p5>xqcFvSrDBQ6p%$kz2l^uN&42DaCQ~!!Kz25 zORRm?$V49|fhT2#vWlnseTAnDCUA74|J#7Wqsbp*w7$e(KPP`KH8(moZ14~H-N!}9 zS8ny0f?}3Zs7F`!MY^zvDHM)$R`!B}Atqu7IjPSjt_;pi1JlYBWf@!hd-~H^+teU50ZW;x2d=)WRgUFEKbxog(B5uV!%PNfkd}TKv?F zE#skjr}Bxn?y*<#-f)|dAQD0B>Au2-UUQ$#L57_5<11^du{Q^DuK|^za^0Gj(f{M) z^V#qP@4hdwF;64~wa1 z865ouXPsrEFT2<%MwdgV{l?{y&PQwTjXsQf3~*GHa8oaU0aj6xi(Ktwr$(S zX4~Fu+qUg`XWuU{*MDxDbACAI9`?~3#*F6=&oqT}d+g;?SSy*%=iHLzlUMxnSWBhJ z&(vy`g3j?BOTj4Y1q}a^P~>h`bhUX+)=V6CbDvVb@kGaWMkF8{lzZU3=X!lXV*F|+ zTgrImwEA-gZPQME6GLC;`ITwydncQUR{xu}*8X6GR%bw255G=KJfgEtJM=A86Te&D zHSLi!eZA6a{gPOMB5*as#qR8SadWG-h^&!7Ya+> zPgH5mMUU^&p!nNjbJn&ZIo`?^6+-uF+#_xGi)Hi16c)P8pNS;B&;C4^;*kUlUu-gm z2!^wewn>4{MjD2md2MDi4;i2~0k!`D{oA$aAXHuR1x1UTnFdskcl#{{UpN!5%O)iw z_q8V%k3qCFJ#OM$)GRpT75CVrr?*<|#BGL?pE^Pi!oe$GXP1C9nu_No?%2?Mqeap1$&U_ zH7v_@lyvpPr%?BylrmC>OAv7(O)u8NBSur=aLuQ1fa8dyrK{zcH7?7Ww+e@9o)1 z*kM$gk?XEnsZeRXOSg}w1A~ha)Np?xR-nBawe?T>zFnb5fynnG>RyqCEdmbNx7X-X zWBdpOD84RByQ3CA?dA`SAcj<2z#5A0k(vnf8w6^tuhtRFYo#e3{lx`^)WS189Bi3$ z!P7umi$7gPy^(9-dWQBpkzN#g8qs+y_NYbOM*1InCDR8qWBu5y{$OI&Y}3+DBX2pO zu^ndi-8lU9XN~b@@jiU>p1K1xa#9p|&=>#KM z7$t=l-kZONC7WBniQd~yq4)?tD3 zD#3x9dGVs-n+wj1V>0RpCD-+ay4u>S&Ck+4@YB}$tNTgDmp|9=JwscT<^QalxM%Yz ze86g`(Zc|C3-z(@Wn!LwTue<9ruy*>i69w^i6vJcXtA$OhflY=xy2^LjfYD}#-DDC z^59g?;;Ipj}vL-!TkHZ1K|ScomV{Q)A#51mmV zZaXTu(LC3!ik>*f>2b8+^5o$T>3Vp>50VG&?onsdE65|ez4#m5UMF&b>vGBG#H z%_e*;NK!pSFofR1Fki*M&oFYp)5Vvsb7HNbBS%PmZDLgD`fD0A8cPxW-JXkdl1D~y zBpE{;?+4N~p|qS9FTCw&oIS73vYF|Jz4x};(ddvqcCbqUj{TTFv4GF0ADM#Y=!HiL zVc{sf-xnpI2#AknvF8q+J@>h|T~5kFxOFscG>tkqq;nJvgyTr0TZs5UEk1c@NLLro z)S>o?%jG_>drzTCiWidi8aIL;gZnql@Tm%al;|uC8V%^uUBNv+vZk71?Yw^83jg5% ztR^%JfL9thP+iR#1NL62ZfsdD?xerK)u`B?d&jEA|Nad=TYc~mQFC|*xR>zQm>(=0 zau^Kqg1jszySI)PM2!tege?cOlm$IWV)`3zLp2Bft-3@qsd?pjanl`qQ`up4Ue|}- zfsg#X3!Qypx`h=c>p7@ebq$Fmkxar{H5vJ)6j#kYUZ5yF8;m&BP7w=c>s9l#c)>Si zyLNJdMEaK_DO>?zp*>6LXvRgQsc+QoFqG}u#}+BgCc&#rsZ?~C(TyQ20>$9(%t1<& zbGbs%RX3(RTnm5Qhy>iB90~HrNfvPI-=~?3&g2fo9!y6y=ca6eLC* zay^7$@!8=BZ6nqHQ-sGn=4uVo5eO~0OrL&hI|nXD)EPPiMV9%Jg1+)w9=2SFGfrA@ zEP>NZiny?Hh5~6rH7{dwNA{y6c}H|4fj4#%QdyK1||0>#Pg;ZX=s48?V9 z3pYPa!vrd4Qje9}4FE?IxCj2m@bt#ia8Hw~a6CxYiGqt;9h=+P{L-B$=n0}pj~sl`n=bS!222XWA(2uzXz)($mmo1M}{dEF{53aJG1zlfe3cJ?uT)iOv!5!3rB zQ2$h4+h@9qY8 z_}Sd!0_KC0a z-3!>ZX%__CAZpD7e@l@Rm`_&cyI+JyEob4yfPe_zQ3ukWf75N!)@~u{5&NJy3j{4b zqvMyP#Zg0Kg8ZJzzXn6PdsD|5-Y?l1Xli%%L3gj8juG+HP9sr_pM5s&6P@Rye=a32 zsqZSAhq`7w3m1;^hMhHn+OBJ-`MhnZV9KX6EbLo_V|#s+T$yXkkb4 zHf@*&v%ptMH<9>O+&(;Xy4N1uSR{vJZ*@j!jhluU2Q-l9Un-Y&D{VE}^}m3=k6_j$ z@ZZ|JOH!jZr9w6i@zI}dHKr}$>3t-ZsqIdDvCfs4XWhx?b=m>>)DFho-dyZ2Zr~$5 z>QfD@%e7zDi5ufKr-HBcwcJ2v%2%171ms}#ahl!Y8(U6@Bxsq@ash*JF2*(!!Sxx% ztK`926t^PRL7?-D5G0njFbi|#$e_|F^Wm;O3d!wKp( zHUq6bUma*BFFcC0k%gzmptJc(u4r-=Yup}A|GL8~(BEj79%dBFQ%{r?JKR5KH8Zp= z+d@P+`kBe&&q67qb+aOv(TmCIXQRqi!-dHH+k`A*$Vp$^Znx-lmJO%G2gVnQH-fTb zJ8c(gnZx_?Jp!v$$80cTh4V3tCBVB`f`BsEoR5%`H1%EL-_J)D-9&y9l6mjIF z>5a|We1YHkB}2O4MbKY!nFVd8a?m6P;}^Ym+DI>YeLrg{CC5rJB{@h(a?LVYp?8P5 zz_dG7l=AXKk)Fn;_3dxS5_=YWrsU3ED_BPOVVzJF99@$oE%xHd+%#qRL z4!qLNK|qaSt&CIPAx9fRj6JkHjMc1Ej$v7Evgj$;CqHP_@b)lwFqi2)g(S#j+`fp927zZu%pvs~< zjPFf+R3*J>m6t;KhOw)7sRjlfu++%uWZD^S#YTyLM^ej`5V3&h_p;tI#Kn%e(vn5> zll?hecGu^sSFg}w@=|;KxVZ;AAseT5xMuj2>nw*elOn{~S*~II-si}YAXA5TR&Ph!c-C+?PFg4e)RJG;l={& ztNa9e1u(~t$fQTy*+qAr6jPkYlEY1D)fc=jm^x{jY7keC7bC~)I@%L?U`yqdTaOLM zp`+z+Y5m`&r6d1n+6xts5m9#UD|%IGe+(4uD`c;LjdA0q`EQi#sj{+MgqBU#ecjM!i@!8}w&eE(D_S6$ru%*iC}JSw_?%ezc2Q<3~kUon<7y4Dvsh zf1)hhQ*an@UP_WQQ=B1*W1c3>Pawv4*&rDy}w-dM8ii}yttF~oDCmQyjY1uej~ z?8Ky(PU*`->gb4NQmF?Lo$(?Q(f>XTr8r_~oRYTA0r$>-We`y{c+yzkl$x5f>bP>G zuc>)qUL(KyX(R(Af&Gfs_Dd^#DtSWNR7pr@UVGE#lC0T5gjK}pC2Br>K{A5*2 z`x^E8bBK0qW0A=`ck3}!DmEhVBsX!u#Sm%a9W*TRJQp~hcena8Gv%h9 z_hF9Q${v>nhU5(DF=Z&cawUkhLcB90DgI0_;9Xc*>+Y7$eyGB&4dVQEu6ii>nYiIG}@Lb974%{GlDWBI~Hn#U~2WIua;jdn~YPo*@s7PSDmghaHgaV6C zqc@goQ2&Kh_bf>lTZc1s2})+KM>Ge%prs#vakS{x@xY}n?yU^f$$Tit-OI27bgAVY zZ^Mf_NQq2@ERIhB@&>tzskVy%9Ihe^js#N4Ot_qzwDt7^EoXmog4JfTbCbkZogzi^ zz%-dm9_+%Zj!Djtaf0s6vSx15iDNCc&Pe!6N0wi2qktELcSEE6Fz$wMp`Sd0yli5J zy{f7Q*4Z$-CpesU4}ZF`30+G^FKT{pD|Bu0de50-MK2|*bIbqH{2nv|--CkY0wq6Q z^iBjHjwQB|IINSinhGa#5(Gbax8JhHTUi;%=Ghc!e{@5U^|15Rqunl*xx{f$^+Dy9OQ=j+4`0&F zqPqwIH^JlCtbm=Ke9q{gQzV~hY8~%iQg2EU?p|~DA+X>)GpeNy3sI2SciN<7V0m@D zR=vIBv1Hm)Nv6ex$==>=mFTamfbc0cAt@btu+dxw$*J;*CeQ|3AZu)^L>8eA{{cUF zo>z|CP3nCjZIK~9f#03|Q?Yuo*lA)!%%74mCLstm?LcCL&X0_s*(RdKqMjZDPmij&)j>5PE7~I(+tyV_G+2R7CiPpuds;Kpm3{Whyx~dRYqr9$J5w zxIT$$y>i7m;JHuc;{HO@s+G`oO|0dV)>73)-6Iu>VnlLf^eYHe%O$?-y7heE{m@JG z;zfB3tqWW{vo_;+D$9G;e7OIEi|Hfq<8|3KRu9$xk-~}n z%SgBOJBugusJw6~^hvCWZ$ju=B`PO?xdrN7 z`eSP=sMMld6C(NHdjA^l5nnEK@Nr4+s)vj}HME&i3k%@82}*T+>+}D}%s0t^Ro%i- zLsVqhNYtmJX;t{k6|O_nX0x?jL~Csvs08NoY+_ed%Wb$ju=`;meHgl3#6<<1C)`@u%ELCDUBc=N9kTm z0^tb#D%wSQrW&mBSTj_UO5&tXe~1=;(vtdF!eN11vs|~z-mbCnB4V!~L-T}PNb5zb z2_~}3-hYJvA~k!F-_g4kl(p+W7pr(D-KkQ3#{=+-;Zy~?SOz&1D%01zxapVfiCJhK zvXe4z^NI?4VQ0Y=G?lOzt%ZZI7z>|<1>5QgjyY_^V>e6HK4;tbMG<)uc^ciPX-9^W z!|Bzdr@+K$DJ<@5a^t$Lv#We>kNXJ3rLLLYq|sMH;j_`9URg2$m-Nh-X;Ji4Z_T%y z+JwJA-q%Fc(4+AH>@89c2|}di->A0grQDoA0`M@8kUn749(bOoF<{|cCHhKASb8mm zN`e!(G}E zVdDERAe~eGzV5ul1g^ELD!sI0=9m_8>Kw^ciVGiXvr~+)D+wA+cUW5c(643*$Q-fQ zQHt^_R-i6azdFxP`zK|y_Luri4M;+b{Yn6+vBIu)oGBb=AO)?Neiva;mi!I;hW#>q zB~PpS)#JZt3XYrq@U^MgB9_!jxU{xlU~!RVf+WkrlY8P0_ZJI}L{Xh08SUwQxAbUB zX`pL&-Z9@jd?~M$LpL+B45=MuF=`hbNNMA;Z~%mD3}o8;7ioRh^>@-Y{~5d>%!A@$ z@?pXhgLlhKu)`|GZ$a|$YJG7R%ZQfl%e#20(*cMgx5CfQ>Sn8fjY(+r^-c(2A}k3M z`-3(5^Zcq?xv)EEK-57pQsQ_&cgytao|7ah6(n?q|yz2aLRvA*n#%bVE z%~VBz(-CZ~?Nj7AXDm@q%4R!&cZI5nMjd`7pUmnK++m6qKanoyGtQFpF4(wr)sAp_ z|2Y$Kq#8l(H7iuj)Eo@@v?NDlD)fwQF5!)6M_$p_5Nu=VS9CZ{>b;TV)q)OATz_a4 zbkv)`zK-ZcQFx?KO=21UbP#2K%#*#dOtSx(=LqX?3W2WaI)P` z#M`6hdtG{9UCk;2dQt(_kRIYxsRT;5P_w)wYYwM5i z0F3!f&B>er(#|^`!bp%mg5L;+DOntYNOW#9 z-NLX+SPDfmluD75aycZHjo0yW8YH-16xeEmyc{NrpB`=>hU&BgFMHIV_$(7r;<5|FR+DV}@7ClGZk zbbG@0E3rFjE>qbx2tK9@x>j?qnUjI?_Y?>2f<3F+i@LX)5(l2_({+9fIpb>#wdp{a zHJhsDim!9uT);!qw1ngej)1pD7p$Ku;J^kw{2}WWvSMsN#T8yH-ySU4H%V+u^K~{^ zoq#eBeQ|%0a@n$L^dm!qP+hnFRTbLmBqy2I0UUah zAg!r_ZYa8Sti}-(yl5AaO)s&4cKKL%`xSCP5i`(_D zXC}~;wwcS=-O6_=*54s%r%Qj;-v*LIS|JL55HoFegwsQzq+88~si0$*BgeFZ06Q9h zu30j{AM%eL&Na7Zn*Iy@_Y7jL{(+d${?qbsJMKu@V$4;i!6Yec?UprV&z1_raY}4EUCZCdkb{Z$I!i9b&W^Q z!6?VBP@ge>+4!ZRN}U(d&^5{37?n=n?$R~6_nq($IZA$&5rusGb<*vs=|QFdebpDo z-v`C>NV$&u!dT{MJgqk(PsJ zlbd-u#1VJ@m>_*Bk?`1(LedBX2&Mh#(w~bnP~O^!cw*C{NxV&nei$4AT1`8{y<4NR ziqe3hcjwF4D>edOld)OGF?4%BjpDjg?n4VOOE=VL`A{D!2hnZFi#i{3*R0w0{_%Wo zR~;f-7!20{g9U3xzZqjYE|IN4H2jWiE6!59u%8wq&HiZ-p?xZKFg($pppSV=F{vIe zCs$4k449gUhI?&*Mepc?9Uh1yZFcC4SU#Wr*Ef=Dwk8T7C}ufA^IWpvk8!Y{OXnR0 zR&c0e3T%0EbwEUQCUZtCvTK%EMl+mp@DPnmxaKIeOHJyZ9z4Wu)POIw8n&`MXnXak z%-pj!7J)H+z85J9%~?9QkdM#q7l0dWj$X=*%Q)*tTw?fLu%bH<&#~j6et}en`Y4_6 z(cq0L?Uq-?$tl@em}B0VUZRz#?<}>V7PAwZ8a`TX`yQI`uc3)yUF@^JncB2&;T0RR z{i%HRsuo;TP1--%{AQy?yxLqg_ORm9)o^H=Cbq`bM`QPS%wpsrJ(O^pG1O#lDc)W> zh%c4Q-MgGiy6?@L=?Zu~=`bXAJ~%3|+G%1H{+`wcFAI~;0LBS@V&vlSK-O-}F-X!= zLW2(Jw*$5ch2`y!nb?l=)}u}jPT=gWnlwBRL~lA=vpmxj&XU&Bbk^K=Wp6TN2F8O4 zQ*FV7ym%*GDL(h6B`)uj?_N%u^Z)K6#bgxkVAB22sZ+oVvP+_!P(&pIw)~=HYt0hs z;d;>Kd!zgxvqWF^#)Hfc#$C_z=flpze-&2*V@h) zwg6XvOD-DLeJ`En6{qziV{$!p&kD0VHItM6`(wCX`P9Sw7-BgWV&&<`Z>3%K^OFgI z(i^oZ!ZRb_dShIz95hM&WR#n!1@p8J9EwoqM<7hU8*B3$X@))x zFTK?uea0RPP%l20j&PX$l@&wGwy6Id2O@ylqQN}%HGfNDhkN_yx-4sz2si9?(S}QI zUn0F#aTpz%wZgOyw3ZaVAxTZhL&hxlwr{mOHQueUHwTR}rf3!%x>vdFmXt1sOmzJKZ)phsyR3XY)HkEhm8(BMsVWAoP9fEwI+D5Lf=> z_YJ{47-etOr@KHuF)Q6&na}%lh z)eLl1cFyawbIEPr5X_c-b!*c=_)3i%)aYjEA%J zAbBHQAQ_8qjdaYevtmF)tBZycIYcMz1(h>5+_93b1_rhMY;wt^h-;SGjD-?N=&3wbXcm)2SC zS&IempY?fXhOERyPiTqcmOKZBK6H-#Qth_iUJyOR)#7MK<-2G$YVhp!<`H}Q%o1U$ zd<7qfe#f{yQl$I#hpll7LZaXd`J;`?yM-+!BOlj*YRc<_`n` zZ2WzDHbya2is18`qQBPqs3qWwXb=~(>iveAZO7!{$|fmM&N*U+6%R7JGq7YsiTxv~ z1{)?~M8VwBbnAt{b3iY3GdEYKn6PDUr7Yy znfDa4(PuPG{*260@t6A1%2bmg1)E1_ut?WUB$BiZsFcXZr@6oE_{{>XI1K zBW0RgdURt|q#{OzDCz+%`8~_7dyLWDdj)>pbZ|TQn3gG@}1dp$( zYaDm+PDB{*_wn>Ou~!qb_+%3M^))ZJ;Kz#ImwOz9bsz|HQ4n}NXHe@p!x8&VDi7Mf z4&fqXz5VaZK?$ljyLC$$x^lE4{D;ZaAVsoRa=}Hd`;Qu@;ZW+0 zDUpy5mD-Ol3RGWr!AZ3)6KO1IfSE<8K1#Z>_Q={&XCJ{8y*Ty2u7q2b*=TL?&#naD znUiTN52N{!shb{TCx4Wi8wi0w^)fQ&yW4i2ejt;$;=}hPxR|mf%};Jbh%BR8O;yN0f7FWJ)42vCrja{X!>Wr%~{L= z3EEfd{2=&?jw98T;1p-~%g#%p@h>d8#bEjtu?(pbohU=t%Bh{q4wfQWwL+5nO$8~Y z($HNo29^oKP_=~jQ5(2Lk2z0;YmTsrL{-ieeYpRNlLp-XigVHBUeA?aXNGB@YF~eA zWx}|LO#NN9n^n5IwhxG1bkqlaNOX8;&#n<+@*vtkP$;i3NAdSzGTkU|+WY=jZMKYh zGU-P$0icjteqEz5E)?em+2}Quy*uG$SL>?(cl4i!bf;j9mo5{0+Ky}WSA@%u6j#}j zee<4xFu?aDUnP66qB-?ZLn@^)vHQC4+L&M-OE7aT)Q*$ zf&GQ?&_SGr=TrL2Z5`1an6JeLr1Db_t%UK?DG`U@*fA|DrAu87h^`)FOm^jJ8;STrUbHi!vgYw{kRxbBcHJdM0~HNbTX1) zlrE1@YMBYA7168VJkW!8!s|(Cu7}@~OA-H!cg-Q`EywXSM2L*3QpN?$Aud*LnyU@@ zsm*q%3R~+I{}WG5AAi+XNN#cPj=j&3KaKY-B&#}u#6HdM+-j9a#kqBoH&9d!QdpgpVEu}C+U61SI55!c=pw&*R`e+*Uv%Q% z@|F<}BDcr1PvGQn9g`+_VbbUKGPUu-AOFbxh$Ma>HAQ7iw%y2S8QblfpM_S%&pq?I z+~L>D=9dT_Jdq0H^$m~*xeNuqm~oJ1q|f+OCM9(d2A{FSK@dn&M2U{hFnvwjf||$0 z3D>*smu%4sw<&)3IvXk-f!}0@s4Hq%Qj?i#FRM7TY^=IPM)W z+#H<^VFd3e2)3%!xr1mNWgW1XB!uze*5~7ExE4Yp(arR}JF=grAhCTh!>R6ia+%GAw)=Lld{>&uwRJgW`WU>v1(tLryzWe(9le$1!GxDpr zf@V$x(EY+`cxJJIHD;vJ@Hre|OJhr|`*<^1{kTX{b#s44LRGi}XXG`JKJfMjF9Rd) z&&#fPO#9F&f^_NuA6?}P60XsjPRF+u1DS-`lw8iDeH~H6e^wEj6@Fi~N#9RI83_UO zZHCK@_l`#sI5l;nI54JYS)1RVL5aeJ(~a-PpEg|K`EjhRR%kao1nXI*1-!DvH-SLv z6D*%SZi@Cqz?C=4wZi!dRx-j_DIXPErot&?qhIX|IRv?H3|y*q{;|`8rqrqNOLiV< zd$5$^Aq+i%0y23OG)!JHR{pw=Ox?-Y>oyl-}nLQx9C8d+fFUp%G2G)2#2`uQC z?K~L&hN?Ob#Ihia1TM7BxE(5I((Q@ex9n2+S~KqBGjUbrY}qGa->ol$sL^}aNu0s&8;XSGo) zVCd?y=9G{8_kUZbhU)#y(6I1Urs+hiiU5D=b^jnBQVTWD;zl# zV_V+8j$%i&MfEWP&4KQ)WZDqPt{T_4kM*T8($7Z;Lk?hqEmP^Gm%bl=*AaBu2p~(u z9ieq$Toc!6H0VY;>Hs={##TM7CdEGHy$Pi)Wa0zJVN-inpdK}|_XC^Q(hv=)t(Y1% zNPQ@sZcIG>cb!gHPHL2r_Q1oBCVF=Xs_AJ)) z4e(7jLCsF|aqEn=Aqg?Ui!2mf%d~UhojKos+Y}^Ic!-_+Dh|d5P&M>EZWG zujFeW;|d2~MyJ#o4u<;XR40}nL+4k`6D}`5PmO$|^H;25t)*w*`;zjjhmow!)0KbR zMpvUc5hQZm6k+O6?KHciL%9Xyy}(2oqGJwa-3pO#gBl{3~7Lv>J`bI zEhnT}5Hn%b()oF7j_P=Rq|P!P#S46UqKGsKwmCmVJQCM1D57>8t;R0=?Qu#b4cJeZ zvDf)XbY83C^#h!}HX=+l{wk2<l^xz9B z36X?rD_F6X9BM@Tu)zs-@Z%6!25E$Lu0AoMog;|GaNxcJc3)YuL=YapdMwvc4f%{I zF^%^60G_7aA->nl_|q_&(aX82|BEibre1AE8sm+HSRt>`%m})0JuU=`%_Gh>a6f^@ zPnmoPWA0iP`wv}>&N4x&G(8V-4#p#RJOg}Gdl8S^gjWm00I4BO-twdN;Y|4LOSXHI zX8~ zso^J&pR3k4S%nv4n_9;Ho^!4r;5cQFWzB;Y?G>vY?O8455^PV8q_zXSdjRSTo-& zVi#kF=Pm)%LQ|Mz@3$o63R{~|rCTrX!7G7OyA=Q>Li?}6aLBn2S{8CK@&zqdir0~y(ycH8s&Gnwp`qY5!C*F4E6mJkZ4QSz`SqdfN^s$?pate;8 z>sncEQ{edXeXDV2&BbEC9&2r+m&PE$GHL5G`}xm`^OAe5C)WT$_XjjzYFE^J#l6xM znkI5t4nk-jfagrw;-KEFUQz8l~E)`K~C=4#P&zVV6g|vd~+L4M(VyS zL(o8M==a-u!9K*&sIy==wxAc|;)%4)m@VXj2u-FF8pe+Z5Bl@d)U z4>{?WTTVwG$R5dzxbv4^=weSU0}JGW_>kx7mcbjJa=M9mDQGU-IB^d=eF>4Pg$I}R zN@Qm+7x41JBM1PDuM{p_%J+1M7aT4sSt9%`EPaZ8SBHGa%*ip#c?vAXmJYN+q3EB213&_Y+AF5+~ZN=XQDSC9PWxeHl0;SROFV zZLu!9S!|(@-2s5waRfozLN;Ryld5VM0&eVJC>n#9%6%}yU`VrLOLg=n%%4RKV ziQ?e-X^gLq{Z5p!HKlwsCfpk|g#Vo;7?noEBg%5*PSY|9 zYH4Eeh7NI&pN};(tJj;{DG=RbiCV4~PT&s(82WbzMEe#jYyQaMNz zLyy&jxMln8+hQ;r`JvN-O3-*|Q*SlklIQu#(!Nlt@ltQWM3Z;)@h0sl&H= zd3nCSeIs4xqP4y`u{q?vanDW^_gn~=UO|I@*McsAz@gGK>ESi_BGA8Z)xbJaWab6p zCfO}mS2sRpj_2;QT!P-G_bg&v4MEea`s2{>I&{DS2uMXeC_}t;|7L+az8J8^VW1)0 z(I0z`OxVJ5Aq1*gPxAldzjCQ3$4aK)D3nAeU{0K=m)hKOX5urSOW<6WofB25ZtDH04 z=lu7<@p)H^C@257Wj4M@KrF3ZH?h?6v{LO?hi=)uErqMkY3Db!Csmirfn+^{rJm14 z5>ZiIwA|C7;hESBQSb`X)TH5aXChD)wj)ptOsKaCZ7nnO4|H)Nq2TI2QeY*+YE9ti zAg2$Un3qT4>Iwx3WP1Ip)RVe)@$1XRVppz%MysYO z7e09;T`-I$n!L*eT4KcbC}Pzl&)IX&v_G@geaDwYx{UBWSB9*0cK^aAYZ8A%wI#pp zudPUm)4FpUoDSU;z%hSAYZJ)(6Y+~9mTPx`K|I2h86K9EDPeX6JLKA1)}58-5uEc; zs|;^6O_~T~#D7KJhW#W`dJiix%zGnc#Rx8d1!hI{+U9|W>6JGlyeHjBa-R`UB37E6 z-2SQfB{uy>Q%#$TCBXM1ba4o`_kF`M8UlE22xb7_*V&vWAF3^X@^$W zdxe{o0*e1jBzTf&mLY;8iVMv>3v?WeBzj^jQHml{k+LV4RB$B*o2j9_?+Xu5E13)y z^CwbAb!yJk1V$%`V*3!m1+{-_CA+r?5pMV!m5OFL0|PUnqX0qtjYji8`sbn#g(;<1 zCVRBUbD_=rbD2si7Z|Q14}0HjAP)bha?oeYZRm`dxf)LcZt<0XwKgM~uXM#QAj(+e zuwUQO8_agLe&zqIWGjL;iWY7>OlC$Y5?htKqAQ!wP7}A8znrMipTH2u!{sqf>d_Wo z)gxE4|D=K+Xzkc(RN`HCRvz5uxj4y;SxFFcB>AuV#_E$rxqI_u!X8k(ldpdMQ1O6OP);xRP;Q$Gw!P}{Nd&)8nG*Xl6uN}eTUyUOGL66#(!-j zC;9Ze_ZiNhcoqI&XEbPTQVo6oVW(mNUbkOM85yT@(*HQjH=~P0dU@G7KDPGtF<`^o z`zJpIp~o*Ug6BvIWo3J~b`;s3^U?beXFhBNs2F<~?3YG6$(Pa!lCT3jv|`Z=cl?K~ zC_v6B93EPxjcvv4R4^_Bgz>r-$DI-mSCgG{n8Vh4)JVcfgvuIZj(Cvk7Lt z>f%+%6~ds|iJW*mVl2GthNNL(=tVIc1wMZg_qEP|gRZLP+A`%fO{|E-*jvig&=(=Y|<9Qb$JD{tag_?54x&?|f*T#xLOzMnGOdEX`1k z0IBXy>Usy`&m+wJ68I^$L*b=6#Yf&F-80zgjDHS$%92Z*c$5Lp!;z_R^l6gmrs}``*E%_UndEm5 z6ceNK!%u|?c^Fonj#5X29Xdo7cK7W%ZB&Gg*`M`N2Fap_dA%*Z@$a|La#4|ErOpW34CpT zj(;Vh=mId_gAHvdWW9N>?LW?mbBnuMW`^o#DWmHow}VzpH|s9S;`53g#Mh=653@k_ zBtJMdW6+JoWo47z{HbbAD=ARAxP1;~dSVAFSbp&;#0bBM z1#JEVD%J~)2#|7LvO`nfF${u7Q^Lvb(p4_%y4@|S`dT+^H1_Ri_;D4k;P14!494ox z=`W>iniAy{&*5o0{hG|HJKS?2jKE?AcxRZj(ZYS0Oweqv+;YbcYp7))3h?+J;3$~= zKC3b_Pq0lwGi++z4FJNOPEQd&))_tcoxfm@(w2+d%_AdoS`K@JeScm^5;D2iLmc<{ zRu)(W%&aH9ad}R(_ES)TvGm!Tix0BHGMYOnmP^5=hq8-ia2BX`^U8?h+>Z*;vk50A zV~TImyzmd@j=Yejue4VPL^tHz7tL-8l}+bO%|` zBm!wT2cN5Wcj$r_3F^4Y*kjfz6H+H^KNA0B-JJ03uhuP?AQ!F_uuGD$*JM-sCwOvb z*GXJ5{k55zM^;8q5NNwxmR~cL%J9YX)Bx6GmNE1@Z+8Wp(P5FBF}&g`(994jv?Evz zYIt8CS^i9+i=g&4sM~qPdK>(EvP2P$VfG`A$AwnXNqOWRJm)QaX1s2W5NC|f0oCIp zHG16hn+nDm_)G&iQB}nZ=j>jY0Zsy3Qf(St{L;{PxmPzY_U?&~mcDK?)JVynOHwM^ z;g9U*KaJo=GK^}^bbB(dvv`#j-Lk6&YI%PN_72L-_kyVhubHS2(Udwx_e2*w0$m~H zwjP8%d!bpAqIHF|#H#}bT(P>^J~363Fs9{3Q_Mm%Hw z9&sW_D~7I4xg3ACZd}mI9T4I(+%@)qr^rS3G(U!AQ5lzi*TNYyd9rrI|1OHnJ|dw3 zG$@}HRWo3+r?jKO5XlG%hz~z7vOq+qP}rjKt(C+!-E9}miQuGK_OPW=tYqh5upOmQ zqiKAKOS3|qd>aKvbRA%`+ClxgaZZ##9dE_>>Kq{2EKbym4IF!K`3}gGek)6}i#(^S zacpcD`@=PN$qh<|?)Tp}!m>Hn+AT(nHUN4;5{H6kZ+;w->FzLWq|fEf?n&X>BFXYT zBFniyl=qFVgT13`>XRad8#4A;bUQw2E|J2$t5XZz{ z=EQW!p_>D#wm_R-B!&+PivQ)O6v!9<6G>ud_aA;r2x3AT0$>D7RNe=CC&gmG#iAJF zx+E)m>Iynnk^<1eOxMb{2)$);Af$EiETK2CCaC3zqt>(NTePo--v8i-FV6Ys7$@o1S~lqIEi7Dv9&Yn)=6lRo zR-eyO!Cb51*ki%R3M`=+K=-(@X25BZX12uPKRYF%&M>jRWmxt=Ae=1vZc2Zog`Lh} z#R{>?Jh~+&UN(gQr^T_a)aNS!T))KeD`{o~g{G~k~O}+J8L2UuNe2$8_g}rJjUI{eOtf8J!98Ce2G`>t;Hb#LFW8(Dda)#9w`IRETY8BI=16avv^bvAA{ zIGV6bu1ncJ7?8d$33*V|rxkiz%YpcVhCX7i!2#t5M(UC~$2t z5*bCqlX-8o&1-i2EZ#GNJwrX6AmLIKBd~PaF(C2s+&<8Z5!*>Ng~z8%G&^{kjouFY z4hxRyWO}+h8v2~O%8YOzQEa8?bB_^&3~m zHHo~-@J?ODUo9oenlPE)Tz@s*ct~n(O=Ljh(-X1$Aj_z74&?3!;}Z{TU_rNfF_^~9UyvGjs| z&;2WYHQ7!IitoGZg{T(fv3hQHp7=7@=BrHrh6`(G#dOFc0ywW)aW@*71}y%huMDRx z?-Y>0ne|80bB_T)>{W8nF%FgbGz>;sNq2-#zHqZ-BM7Y2MmbgwSlqOHvvaIG#_RFH zLgK(@GE$VD$;2BGH@sr|RPa}g=6*25M;0p0XA#5Oo51s8%YIBf09NZu2GUu8rPoXa zKmHhKxsa%^m1;L6DTdXqtph#f-Ee>fl^z``NqnBTQffSHYS3HG^ZO(4cCq%gL-P}5 zZ>2!PRj8m%!b_*UEKv!r9YxbM?CuRKrn;d7{j?SDfik5aj&c6b{Yc2W727QWVz09N zyklSAavp~|#9hi2^ATNL!)#KFs%QrDzAW$K;y=Hxg=L6iM6&a%$foxM{%UcvPC58YrD(W$`{`1w`bnn>lVX@C5s_Z z%cDlJikB+sFkgT|2x*wPM9I--oJH^RAXVXsAu-u{(4 zJ3XqTNm5;6;i4#Vf$-uXmi>mep?E`DXY)wt7g{kcd+mShaLBGE#xNK+GNaK6VzLf| ze@J};-<5mpCiw8$O}0o$Iw=+EEvh@#sv5TVA`z8@XW=jlPEt}+^N*{4iuD_-`&WS- z3D-?X+6Iy|**nAjc1??mjTJm8YDudywC%I(SeU4lU^l;cwb@wG{oMFCEydspL&=v; zYq+#6*7{{G1ZJ710bpYfC}A*t%b6r;s{AkGd?r9N9XLdS+A@mmot0m;ht`IYb%Fj6 zl0~zcTibV?S;A9u2iE>9D!eieZ@I;m>>wAvx-wZ5ex3PacMiWMr3Y+m49z#d^!T_h_scaQ&=`C}fsD-ay5 z_xZ<~#n4NF8_?7Nfweh~ts;y97?TX2W{|EU; z{P1FsJ|_2(!c)O#yA~+TncVx*gee6x#Alk?V28LGqmDs1k8fuS)NL5~hcF>;KcVi` zey(h}tMsz&k*-*!YJ|#M>VR=11}!oa3l0w57m6zv7gSU$+G;~jB*CSCaKRT&_=sHd z^Gp)RsF){YJ6TMWQw{RO_Gc`=>QY?nkNkcH&ct>$>Jb=72O} z2$3^4N4oFlBbY$&P$Sl~{`=HG8*s>wv>$QNYBm;0_!hN|HQ^h&Qx-NId)(;OBavNr zaD&?8N(s;H^ZJYa!?`k|ZtFkC1nJNDGaZ-)g3M<|g{M?|sqaB&O;UCYJmB2OG3p7W zoYkyX<4}(;dNFTxoTFi>vK_Q?tu$STYD<`tXIszBtqnpCno~SzfF?zut_E0CxmMb8 zPmk1d&ySbC?~)dhI;P~FeJl@R%7rmxgcEDLKjou3Sk~q97-%<nmy^DZ$ngB#g3lfv^o)NXhMIp+lJ27Ry8eFy;754+O&@ zt0+8XSGhJA;BanV9y#1$>0)C()q8YKAf@FQ#uq&h-ie5~&h`U{E!%iIYD5B`Ynm(B z_ic^;5B`4+$msFG$91xdAtfZim`@{MltE%)n!SmRDR*s?Mhecf3KXm zaz-y-R1LRDGUr<95rCaR-R{XG?S1i)*6_8)qte!hT?qRvm<(wdi+P<9;31P6{{H*x&29UYIDoD`k+Tjb93a zHbii3Og&9xwQ7Jtn0N75*JX`-9tO2xSZ^XjIE^;LZaT-uL16#i0;vGV6le3zXBdzYAdx`)m*JMf=V?=2I8>J@`|#K)SlQJt zad;tip{CFA!#$;&2^_ZUtlObg5o6V8C0u=Q%Qdu8NNUj!2aZLnm)|2B`es{{*e^Hp zg!TlZ_xv;seMozvwm@%y*ty}9eQ~TcTfLm!8#SeVk1;!}+xb++CZ;pIb7%;f1g<-D zoSo8F&jtl^`KEC;k}RBqyU_(K8PELv*s4JZAQ9XK;Q?sJ#hh*BVeNGWr#t|7QI1?ympv}@gRibH9 zooAyour}7gZ)=idM5(JnHKwyxy}I*8qw$4=de)+lpE^KJ2=ZYMKV)Yx{?CA_E!j;B zQVQ}S#+Vl4H5!?f5%SL#yjP?A-fshMhup0@k|JYl0NHoBw(6~pwUr>d1)V_S9h&6f zUNgjp*^$^T6!XZiq4;>kJ0kticxw)h{eV?>W%~HkYI_c@z*Uu&_h8JdS5CW0brI|Y z5RQxfz*o?mO9Pfc8qHvW+m_kHqZK^QgJPKIF$d`GDy0b+t3rpt2bKuhI>yz=SA!i* z?yp3r6lfe+cggg}I2+*sS13Ob9~?m3kL!t|{4`xG&U9A5 zo1oDCJJMv1bFo8zLxl$X5Z@Lg95{h?g7oy+KqC^w4x0Ifpo$0%aTY3T1k7_Ocbgk{y zxkQ3}nch{FEQ0g;$rt)yt@z10b9%|33}JF>T)tA)`Uw^xq!fjODJ&m1 z{dbR51#j_i@su1&AX$|0wNki(vA|!)PxUF3lDtAWspG_7la}fqMo;=uhb`veG7U;F zyLlT-YZx{79tIcCbjRXykZcEKTs#3JuwV1ea>d~$eSa8NUrMs ztC*}J2so{0AB(;SKZ`5361VFkriRt?_0kC@<<@USO!@SXbJ>dk_`2iM`5zW*ce6Ep zq?lHaUoiiQ?j)9C^mMJ$P?z-|68#h5Us$vS_3^$J-MGTa6Tb^{wt9-{>_DR#F7{RP zj>qBO6q38g{#!|Yv9$quW~{_XZ@e25Pq-~L@Ese1HYq@yWY1cAJ<*YK_#C?oyr|iG z;Z4()fy(N)TWv;Uhe}RL68nQI=0Y}mQA%HfCxE7-7?sE)!NMGj5<%z=OF)bRL()Tq zqd{JPT2p~X?U+y8R+9~iMRAacqF%No>2Fz{zL(KN}($d9g7|KZ$ZTu%gC|24Ys zGq?d{|FDV!GPht3=6#}L7(F8?lRM*yOy6~}wPTRQ8i=LOaW~{oabVlsX(zGT_Y>0Q z1{3T72484jr6+wW2`&_c=ah*mZGN`BP51b*$%~AEWg4tW!Si#G#n4g6YA1d5m(7Ay z>oXvgC2kCeA(sUI!j0473IDgXu}6IyVb>l%ROVB3(N1v7B+7kJP3JB*wm`R~b~lUg z>;P3LwF-f0hY4;o;CmOeY;d|otK-LU24|WZ)9)jsolFpid>CwM)*%f{G(nmqtA-A zlI8Xf$VpLUY#LF<2CEo5|JJ}<^z+|9FqA(HjN|si&RnRjhrD%Zqj6C-Af4g-jy6wm zWI|qACs!%^9vJVBfV*XOXt$aZBOJ3U`)=p?ShbFB`?%;xWoF5uZPVrNuQYWbt)*)3 zzuVGg&%emhyMgmqT&FMy$WO}WWD`L@Q2Yx67_yQ@m>PE8OVl$8IS5Dx%i$+a@A=aI zy`=kSeD1wB+!(z`-|>^OaHrwfP(KvtOMTFar(`<+{4rVEEUigg1CKqN|7phK*8S?) zG_X7~2)hyx@yb9{)0j0>7GxvG=`~5W8J+b`mFb{E>3`BAmbVEGc`bcz*Ej3Ek`-Z7 z$iaako`*V!u2w5(fazWLOjniYi@SBO%os}Da%|m{ym*nF4Z!)uTY$el0R-^1f%inh z*6CJ&6f1FS6ME84I5E_%K-a(P`U^P*R5wt2THy9BN8g*Q4h6`U$;ME}l6cXBE4Gez z$m>h)#V>scD96WT1JPeFxcfi9Sg8UtIMH23fNo2I30ME+CG)>b`vmp;<_!BaYH=gPR%2fgpI~V4HV9tfZ zo6I5&q_G!Hs5!3UA?no63QP)-tn}<~ru3=^h_WJDCo2D#LslfDYRgTIZc?O$;*nxs zE839n(_2;Wr#DFAOGVx2wPp*|(G4nV+U}>$e42fxO1+Q9moTuhzTVg~luSP+BaTq# z8YsX&>!oi@kLx#|ty*(kXtZ8UaC9UgO^!NP${0;yOyNZ-R{#yo-rY~CeGkqhNn^*@ z{P7Fz_kDtDgLdLPH}l3*)`rqwNAE^<$9JpDY1_!<>>1P!@qP5}h%XDG4tU7UT*c|G zpeEL$Xg!1ZvvKXCvQNyXM^Fkx0JDE*_Vu;r??~ZUBy7;LDLS12$%83opYtu3Pt$2i zNQzGkYZlK+kcW++Rte;#^p%y7C%ANu3zoOPCoUCX#%oCT3oEu44j5tb2-L>5eB_4G z9CD|mZ>HxM=#MlicEw4qiM8Be7B|xFSHWJS-emeds#tQ& zLRedk@dLQ~TUE$Lr{Tkd7!T6$qur}Jx|x`PZP)MrHq-Dr6+c`fzl~A*|L1z zb^*|?n5GZ*N62r0a!US~4@EwgK_7DG3nyIp#XEg?w{7ABXt*t{rBrnJ_( zW@!4z^{oG50ro@bpwKLVlr|O*8CkcU9iULYzQ75{SA3KwlNb`k@(uG}&rV+Jui~qR z*o|F=e>bW+cwt!bnI{-B6kh2hvP|0nT{#=Eo8(SBa!AP|jG0^S!IVN3V-LJ>@cKCp zB+69NnhgVGDr-RWCF&N^KvB^`4z_smQoC!%ihBDcwraxU#73CsllzE&i6e7~^eG9+ zt~RxBp(o=h6tXPy2Y%l{@stY2X@9tw^-O{)>ZD9i8L|M0mF=`j(rd=5Gs3uGAM^%k z`gn;~)W_i?5o{Tbqg{_C1lEIHxYr%h?fCDvF}V3_hrdGwL&E12qV_{l@MP)cml%^l zN~jCw9dJF@PCcu}k9R&v;Pw|Xf{};J^>JZZ&QuhsoBjPlrij;$r3(xxOVwmsMUlu@_V^w#7A||`NE4->3TX5i7O_bF4uXk<^>7Ho>gB@6+ zS}i!7L?zi`S}RY})f&?!lF4YJ-&(3+ClhU3bgZm*QUu-ii3sOE5y!T_&E^w`p)?mw z17$2zz3ljMB7szJAnJAyaG*1muw-%B1#g}Ab~DlRQr0uJ9#8K2Y#qs>=UFoPr~ZW# z#%O%xhN7i-KLHr!mAtf8GXFgC87ZLa-NBras{|TLJ={}a;RlKf^dyBFZgyuh=A0oo z_-=Xe1D?II5Uq1!$jRopY&A_xS!fxly`AG8Vxy+6( zRQ2%$I%78L-~Ll=F=!*+Z+)3xhX>EK==zRxRf>B$M}cY)<|1CfUZ28w6e|UjM9(kC z2}Ix;hw+l6FH(w5p8(ZR0e4d6bAS)#GBAQ^km)s{!f`bO5gQIL_@{lyD&C;ZqjVHq zL`tmkiXhe0l+2tP&ArUeuk@mbuO>WX*qa1|461_jIvV#l1_eq&s}zxGC4qn}l8SuZ z(sd+Vc;xU@GMJHA$jvT2b>-f%Aqf}~)I0tXaVSGLP0cj#uCr0C`6tE9`5aTWV;iVE zPUWu;VL^$d0$JLHN4#-k+EfHO5EztQrAoSwEE=nPz)tOMjpY!DF3A`#Jp)cc!DU4V z$?&lwC+<_Up4HTZQult>4dRv6u{l_eLx{9iVAMSf42g(HI`n~ z_)(9Sv}{v}*)p7PFUP|!o=ooA%r=b2k!1?+gNarv(jP*&7&6Z5TYG$vL?+>mlzV8Y zh6*UAf~%f zksQJV-Hc#_yf$$npo`b1)D#8Jay5`&vcB@b-idJ8!USn;o!1yX{WD+AhKmgRl7(z{ zY|9}>Y(RV_BuXVgVQ_E#GM7qG)Ztf>*fU77g0#fmK{UQ`ga#v`AAlSWM*Ecu4 znLT3fccKU#6E#fJ9mc{udzkezzC$W&+ws6hY8@v%Ym~RyZI^cZ4h55i;eq{GXEBtf zU0B~2DG;+pf`s$QY1>QgYSyS&ibE#xj+>0tCq*WC4jEI~P=?lV7~R0xN6aHqa1@zL z_8*+Um099H{(s(?O3QsSqrAw1#N0<`;79C9*L(QNVo6KsSK0C5#d0Jir^QeVzyOb? z#hgc#oX4ZVw7(DC&@L1#T>__u0b41=RncB<%iDk1mf`0Y>T(G(pLPw%X!ZZ?*Dbgn zwoie7Sxuz@ey#92KCsn(a34tb=c9h)LoSA`D{~Pyo$#&dJj2xUjk+CM|NFz4Wg22sq`|(fDNlT2z)}@+(C-0H4OYa(WqEE)Q8iX$9p=(O; zaoHqAnVc+v*2u?039*sPz8{KFuI-=%jk~;7%5&^z7pMMj6!EdX7;@KCmiAgCSDJmI z6eymbjF6;F<&3PDAti2?4rJFr=+1FynoyDeQw&yvVohY+ltOvOjc+f4F6@R(q$%=6 zmHw4QMyT-iEO87Y253jEqCr1&UB!n8tn|jB!DD6-N0j^&R)~7Y#iSZV7n^V6#2?7z zre(?GR=LM9Pa4H!?Rr3lG0-Iv>YB9iOE+c<;6E zjxS7^%|}+}a=Pz%P<;(X5#AY?W$MpOK-%~%PI{wspn(KAe>m%!$B70^dv0v@?JOTI zT)@2!Y>y;qE#aUUlD_cE5Uk#d(c??oJr}AaBRfP9PF{w;&v@=7orH{-w2#%V(3wA8 zuGL2Z_}D(P;3#?20O9Q0jd^bNz1dhjPs^M`RP@RRU%!13I557g=(!IuQa7b3m)6^4 zSbh5u$6#m6h#UeEst>E8c!`K3Cwqyk#P=30Y}55WL4o)_PbEV!kEW7J8D`}teAY6Y z7*_8CUA+!Np=C)l(tu;Hw)d~Bil;U+Yc!ogs;#=+K$tB^IcTKeouPwCMUpoz0xt_T z3_cM3sE!@o@ViVtGX9aSxwJ1@e`>Zp^LmsW5o`jA-$0~ASY@&;g+(MKC=+F67(8;0 zrKn=oQ-xDf`|<}jq)1>)jvps2v|u10k%3bWLZwe+rij*>N((Hu#je?)gcv_#@#K$RU ziNEDynf?9c#?MEVr%ld!YeQqBm`mtr!firXqw4h`fAUrCdbSr9nEK^5Pd5- z-bGoJyO)S^bFQfF5cUF6DR^yn#bl32^E!lu`Z2U=K65|MkolEBQ!+3D=Gj^hX`@vfkPQ+lpV`xJ=A zl+y78q|tXqYC`sjI*=z{^&zUwtM3Arbd1^gEZ+=}vz5Pfw_D$SIbj`3_V6XSHW`uk zI%|mbxPF4tPLh;+^#yYro@tv6;s4rnhS?oepL+_4g5ZLF=|~Hb)#cLbjZ^LB7psCk z`#%|x!V=v1aS-!+>JtuQ-Spgkh$nJ)oM6=ttxUNS^16>QMsvi#sjAZC*Zn=#L=0qa zRjUs%f9Jc*lWK(c`#6GuLF?Eg$e0>J%X%}0syblZUTZwk(mkx;8FriNvd@>>;_9CB zuO@=+vCX?2A)(tAGKIgW!Jkl$OxSgDId$!O^5d*8xAP>ptr(5u$v;z&!Oz=1_p5AH z*Ky%itIofvKW|sK67fM8Cw0QgFC(@0l0RlvwScd|iusW0HvRcmIx@J7mHa@tI;W;s zWgwB)srId`f#lC$E!9u@hh`})g=Fd4{fLM=`M}wA{tc&EV?$*F9w%QYv(Zj5-ta)Z zLI06xeo%T(NCS|+=r^skbH>>1Fy1sfCY0>F7&lWM(P%m|h;jp&jx~zr%Rrrw#Un|^ z+3FIYyKO&W$o#TG!x$g^YcTyB9dxd!%zCGmyJ13U=i|j~{PdV99R@(n|@o%J@JBMa6K) z*0A6ApK_Q`r8R4cdQtUyv#g0&ELFvnkJe*HZJQK5ZKhDNzqoh|EB>%Xc6|EM7@-L) znKO6i5B_Hhg=o`5I?K|UU(?BMKZzL3>0HEHJjxJlWb9>>=@1WYL?+Nn$$Z_ww zE=`P&z$@|4>;?wpP{!&pW#!YV>zN2@k)7iN9 z*ek6O*1zK#S^D(R+F7-3cv^N5AK}(gwhijVD!3?t@n)<>k!Wa@Bdvk)2Uf1hv()Tw zYNg`uBZcZ+4*x6?ITk@GG$()Gw2S&_HZT$Adn~nl%C9R=o1pXGK@H*#{u-vjuBxh5 zzu7t3Ks+vE6G_wBuD>>mWY4q{rPw}ULc+&ESUO%aQjaP;o<9VIT_=MwU&@-(wWogT zDPQ_FFYqG+d^T;AC>KY9+>JsoLxA3OE7O?X6qH??*~RRyCLiE8U z2?eX}ASq4E1k!FByxFHA_0A&o`_aQgsc#O>tuf@7ok^i ziwn7dW39b}0Y}M{cR(#0cQPB7 zXyBCQlQkE%9QKF_ZDsaS>q09*8EgiHuo=-(%c27?kJ`gEJ7T)jLkiX$H^Npo2HVb+ zizZ(|X~*{17&Oyn;eK41|BY4`lM;^vml$->kZrX57pN_ZGT9OQNva5cAGjZpuytuB z(opGobuTdo=x!b3XS3L=Ol5QPoTLb8h~p$*Noh-u9b?!u~^joie()iPchRj{^+Y$Q? zfuhO5G|Oc=oJ5tR^!YS5Hn<%8A9e!s&AgWYHj=wF*4r!<^_tA7>tMs`sLs&wgx5Bo zyn?yDqY`2R8PO~|VihBgMwz_7M5kvTm%&oWm#6<7bYsNeQQpKO`lqd(#D9)5yoAcu zB6Fg|WEyaDPYx*>daJv3gR*4m2aB1vWzho`Del8Gkn#=?YPON+=AfKRC`Xu-OqG8# zW*euDbCbEWtoNyC2(loLF9cY-6P>Zo3-TN2s;Nk za?+g}*Uw67A6NC=TH0i}(p3>Z%D01Fel2m(1(ie<-^YEVMABxbi6h|D30$X*|Hz)S zV{U%w?J%CD@_?X^BWLsEYZRN{V*|_4#w1TG{)M7S{-XWJu-n|zW#+u206z-%_wGRX zBbsH3X;*an7wFksd?|7Z%)ywXOrii^$}mhilDW3_ea)ECLtr(i*7rx0($g{GA4=9 zx%WN%<;_Hd5m0CFJysy=%(uYolGdyPVygh}t=T(t%P0yj=i zB!BS{tkN!$Fya~W)I2!k`M8Sw+(Ln~zMDK-?_M*?rC+GU8cxT9m1)8VSbz3AL3f_( zn&xOPVuDvXAK_oVl1stNxWO_It@_lOGo3v>YI?ZH5Ln6*cKaDOy4yR%DutA)n$fWl zh@#=|l?yHm0gOVeUo;I3XRFOP&25w_uI6UEk-(Ktp9NEVabYmRmTm5shD6lAHOI@g z48~y)!xUZZ@+=fpF?mW*FiWrU2M(0M2nz*drD&vaqX^dog)bH(iq^6j0a`v#cU)NZ zb>k5vx@3BH^Wl}^8~@TAz9SbmrZM@KW^9R@*YcLq>g$Vk51W-IC9_+6yVJbuEb~6S z$#uOcsrx_x!Ep5Et#yzMjrttA@SzmYOQ0i(!dUwPNX5z9$`tQ-S!>yUSGXe!HoK9N zweZ4{KG%K6=MaM{fcA~HVf~Tm2WTW^2;3js>zRqcn8%zkGEw>wc<5}hQ7%ke;AXXA zf}X9TNpF;NjB^lNJ3PbK+hYKIcjU79z_RAo;w*#wdqxymwHwK=@@o(JJ*Um#m%UT@gubsw_} zGgr&2zxK%Gk*x)SnUql;UZ2*BU+S;XG?JF4MTRu5@0xNlJbdTf6Q{W8^!3o;HATk` z=Vd+yym!qfSqL?G7H@RAB$P(NUg!Jg5T961t`|6;Gm3(Q&V40g814SK=jLt1JYueS>BW+Js0_C=o0=1=iBIyoX?c6{d0Hwr{qxU*g^c^UZM)T9fA)Qw*7RvQ0Z^AX0d-WQ; zt@Q3^)PE0G&u2T^h@V@77UFj00F(y89e!#d1I=7IQ_puVW;NYb8e=sf@$i}F1G|@W zcdF!!C1Ak5QBjHg&INT7|1|lr`r3pIWMXSwB+wK$-!jy^YmStD`L=tMHv-Ao{@W<> z>ouSikwQ4Tskv`sV%xzgh#dg3e|e6G@wr6R2kcd~NsGY1YL@RpXL}WDa#Uqa5@w8N zXWioT#&9?0>#U7YayA#{+$Ok-r{8x@Hywd<)gy7+MqZNlTpM-|(_jwM&#B7l!L10j zpx?Z)P?{K=@~l_zi6~0;VI~Jt*U63Q=Hg4B+WLf(PUb8Op&T8IiA+#^tU%0W!}4T$ zh)DvMYj9za1$DUIYvo1eYy~{q1P0_d@|t)jY}_+#v~A` z?)KN)QS|5{$#}EIZuIzvCJ<0Q~y&q zB%J=Y<4GJ*S`Wf|xjgX~M7@OR@-+&JFA7Smrq}go!p)fC@`%&9iv}yO z$*mrb4085TIwEmn(t+Ea>m3})=E?@{Pgg|)*3g(WPs^zkp7-tDtjU47u*Qth2KUD_ z7g2dVdB(}^C&qY67hx{DOx%a{bh?oR6yz71(d1HO30aWd|DL!=CkA~WIz0r*jA_)` zE6>i?YordzhWWoKcgCrmE}mH04~u_kUM1Q@ZQ~jKr@a@6Al2cp3#g|{jNHtxV=(5) zz5h)6&nG1aTF{;L{df)9*rN&nNKwQs6hmu-eahI+qn{FQ7X)xo zZ{eH zkZqSbA11v8UA!sNB#}c=%l%vlk)KKunb+v&r}M;?26B)q5RfZGefZB{r`$&BSXbv& zaag6UKz=7ftTNCRZNw2C7ekc4ik^A~)Hmp^56A{Op8=cG4|>2A*7#VTQkE^UWn7uT z&Vo{HX$Y?Sw~U2>P=u)Ig6n(9$YJ#~v-h91IZQJ0{d2{Yk^(?uHlnP)!->JNpIQw& zE>8RDKj-u|K+>SudQp`|l1p22P0i2m%D!exFE+ySkf*aLPo?BbOEF3$%fS zkBushA86atK8SNalA}!&%Qo^-WrJ7d6JusUHBI zhyOYyPRRN8oleECKZxw}Guv}sd;)_S(UdQ@c~K3SniGBg8Zte5pEoMU*HDl;6wjIs z2jsw9IV?xMMkcz)pVIWiktcaLof$q=Aq|ICZmN-O7|6_{Ymd=o?{Z`|UfF9=Fv@Xq zK@;B_fM&h7kRf&9IL0%^zoQ6*{mt1DJ8%Av`C1lm8&^+FVk1D_dU_yc1v8w<$?Jhc zU>zjW+Ci}Cc)y|=4F_sQC#spFAd^WXX=Jq%0^0VE^!0~6zc*1w4|Y%06c5OIVOa=D zC6Zd7_&lokzIo4mbR;~VpupGLLf!A=3FN9T+&=Bj+S2H&3rJJIeQfZEKuc$;#(4Ve zIiT<37jG|=BL_onj8ig3e7;y}`r{m$|RY^F<0RpSJ}D!T4toPZkkYPj6`YDL%^NHiyZ%TeR8YmxGScKHSE!Pu*O@~CP ze#q)(#EE#`yR)3|I);IvmL*Hg+Gtcb)at^wz*9wS+nM<5vguSEx&3>O>W_4S*=;qJ zq)mLWOHXZsQ&$;RE=0v2C`)Hnnq&!S^WDnC{nC1xS{fm-FQ4)xtKij53n^X~WqUp` zkuIgu1MzsFI>yJAeA(>65H(el31OZW3i!6pZ7653o;LNvo?unHgVa0)iDj}wK6BJp zeF9s|P)jYOCE=@$N3+;8 zBC5qkxzuTc(m~Kng_g7MJPQk#0n@pkZI(%l9Q+# zA&=dzuMfi@$V=6zIG(+LU)@%E;mxW6T2DpU5sM16L&7)Mw3ab%s=^X!%`RGW$}E*$ z!&28}R4VxiQ3G1~N;NPD@RpZFHzWv0-Cs9<2=K=p?FX5PcgebsN{Pz{+QQD2(#S+C zh=k2y(ggbdrei6fRN&@oO4(w_H{jLjH)uw790~JPek^2 zTjM9qMR>fBi7l%fM{@*$Gz$ohPq|QeCci&0GTpHLHfzczAK-=n?!AS}wrz`Fzna)h z{0`KMlj#jDFMc}r0KoO(8;kGO%GBMEgq)B73xOI-09M#6@wU{s!7!2k z=o+k}w5LC|x^vBg>|BsN5SrXPc-7#`%LjhmOpZp^r5>nI+8{JOJoT^Lf8b-=`Vwp0 z^A|g!v+V?l_Pa3tLY@_D9x}7pWDk3jYj!-rovutmDg40S{58AN8{Q(8sWLV-Bf56b zy9luSDjv6O0Fgvl(N1h}-*7J)mwiK#A;eQI=t9@X{31kT^EMN@aoD7aQbrP<^#s>w z0|cQtQ^UmBfF@qEPC% z%1K^PEC%K*mo%}}yfKtvoXS^WU{zZfSEzDMm3xG77stf&o#K*~HOL_|duW>bK0Tb0KbZE(_0dPtafkBO{d)%*OzhuT_g?v&BN*lRe|twqJg;EI zQt}cIdmbS%P%@r834!9Ymn)EtuDSRZ-Az!#V(?`G#kHxENG(R#2}Aa}gt#Fh1Va9sqKC=>kq%$-a!^Vo7H{YAfRk6u*d8;@QJ+VN>B z(7ABc4Hdp)dC#(`!^xWZkbsekD0Qz&Ong?UFNZ;b%<+TR%yLTuGh9=^oBxK=by6oL1lcBoxKvI)@<|`G#z6Djx+c*I} z9E-_U=0RjG}6GqrqRtTNQ&5@6po@2O#SlC#eebAp5E&B zSf=@M?QukOs2cg``@*fXu2(TER6A6kalf?bzB-nvKXA%8@qkIWnSe1Xu zGL|avfLX>Oclt=X7qsQ8HSUS+1JBi3$WL)EW(RA zu8>O?B%Etq_>wJhP+27}WUyAHUC6s*d-|Uyp=&)t-euMjqFt8a;kbBQN`dmvGnk19 zAONC$oWv(vfT}|rnUP}WD@xjee`Bk;wb~cTo%fp9ZrQuF%UhfaAg8JzE~BM-d}C)a z3jcFeyctLV;pv7n5E46a*{VRCGfWmA1i(oY>`3+^bmJH9G-V_XkcVEWDh)4R8S}7l zktPh5-E{InQ-c$>)FOJiI|)CK&rKp_{zKCNhC*xrRkjt|0aMoPAeLrHO4cq?^Nz#y}ADb*@1VpM99 z8elSs5hB){ddy-OAdRb?vM|P+NDZ!W8iWh>e>6G#TvF*OmHX+LIJg$CCF^5ToCJa2 zMU!%oAAvt*QRTp{R$rS3^t$%HMnmkN8Agg!@pbv7;;zpBb}|LDum79g9PSWP$Fet%2*hFtS)(5(XtG^Y#RUK zUymJ_Ks09*LWg<0XfeZ$Mcj`&eC}!W!< zWL^vlEi}Ghn()GCR=&~pkUG|8F(s|4uUb$aQ@(DYy+Y)(lhS?8rcOo^DeVAyQ1veF z)e+0&yi0mcXVMJw7SU&$pt?g4Ni8yt#xZ`(AOd#Gf~lDXMv^)0w(=2F2iU73_?|G4fqoT_*nr6vB*w zWL-y9cg4Wn<2DVSnx!8A2c6-5#oQj+OPsmgOw=^Vishs#4}vT6Xa6DpWIlm`@!sS_ zT_i};)~~iD@Gii>lJ&%Nm{#n8;kf#j_KpyWwpEfew=4v7ij~<{cyFStS^+*uA+Q4C zL-_#asyNVu>v1=?UYZn%t>Euxj{Fs*HRf4K4tubJkgvqA+356SudI}dTjj{*PK8fC z+6HJxPValzY>_V}oLcr+^d|riqHGp~;B|nzmR+=M{qe68U*HK}jT|%sJpK9edq||j zDLNCX_RJ~K^j!w(1iU$ASF6D3)dn73drcL2Ym4UD&A}e!`bjE1m&r&>FZQZGosPfPcf3^#zx3{aDoyW z4wp^2c4x-P_ieM26*4VPIZ9Y?r6yGI-Ol_gM225Vyh+9xUi(O^Z)DWt)n z+{_~#pA1f0mvfS5B;C4oi!It_%AMlvRpA>)SnnG1jXe)fJ|>0KLA9f+BPAM1Hnkl$C@{53CYZ0R*PNvA9en zeC?cEm=QS9cSmpD^*=IYOJhjDow)+!tZAG-0eE7Jfk+f3&f_GGI-gYdk?Ro4#+iC@ z@jvKR4YrcJh4BJ&{)>&X67yOSjFIYp#9d;G#9!!&dgxE&hWSdHaAIe&NoqNb^P-qXLa_iQ zPd8wH`6HfO@1CtHTOW)vjeRUctIce8Ujkm6L}oGF@*e{9etqREERplkvIn*+2Q>~dYUwYIbP5nJ+W!Q>gX9) zR4GSBv5!8|_vYIvd#HzDaRzjRt@^*8D5nr|6l?t7bYZj@&qN#*;Lle;XSJtE^>tWU zu@?2(vuYV{1c-*AXe#-~enI4BC5S>pf}CnSH$;gz60~I%YaagaqRbM8^20!L|=WwIcP|j6G-k$c&rpI&A$o|z0crsVJUCiO65gz z8K>V0D93lMRqxq6*64~<8Gnx(7L4LY(v|on?gDhb;A&^R9Xk!CjWQ%Wo;H)SIS)zE zKY}sh;hhFeD}&%`q`QG6QbQEFQjJW`!jovZ?kVKcK*+p?4&4qy zwf~tts(j6FwE!Rrm&$LEm$=P0CUI-UAb0l-xf2`$TEk-i`aQZ)qR4DAR1zr}FBp;> z>WRpGlaggy&@odapSjsZW8Ni~9pGmHp+J0e9O-IR0z<18;BtNKgFiN9Q%P7V7ju0= z9g_>s-cH;fDnf2#97&5=&Nh^yJ)_5Ihc-TxVw&*OP#)i1I98Nq1Ho(^a$aL~RtFZy zngLlXx#1r4H4M&@>Dkk0(WQN5yYJ#nhy@ZXyw~=KWBuAdmVUCr?)QdDXRD(k* zmdm>gm@}IdVWdzfg6N}=z#<+Qpb^5hlWEnJZ(%)=T+KHnjTb%giP7h8{C!yOZo*;{h|3r4nMb#D z&tHu9@t8A;h6@&MaRpp!1^SZmW~y<+8`qDWUUtjFMVq8>gFuf>kl+^ca#}jPCBt$BnzdcIARCNQM6xV zrXI-%$+R&!Tbnd;8aC+0LqkmxSHm{p@U(y4N1ZS_PI6APb?r~@z!aw8zDf4y zk{a8hqm~8~lm6FIuG<;M!WC3ahpktOIq|rEWc#U;MJIq4hD9dhBxZcot42@6UZ~Jh z$Go;TGX&jEKz_yI$s89n=ae>MEor>$Jj3z0nevkBS(M0$eG<2eHYE2U1QcXcMbu?J zcARfGQ7#>LHpmXFS!l}^ez1jvt}oy?#1aKVFDe}|L!{R? z9l~i-BMtj7pSlBnj26K)<~>~ms*J2juspbWj)!ECvSgp?D-ry&AL$aej+C59>cBJ$ zJ3C_E2z3JXZJ<1`^3Y)#G(Rus`M8Z08GJe%C-Sdg{>8M> zAM4e5T>!B-HmnuuYOnV2T_(N8B!Xf*W_V>^+IpGN^4|B3X2ek0XXz+19EKDUAv+X< z<0c}KB?S3c-Hrb&!MF6c>?TPh9GxLk+t!WuQh0)zJb*ZGZ1$67!IMq@41hSrMVBi! zeG3@S(o(Ad*GH~ePPEr&R%Wl!gOcQHI=ai$)oAEWatpv}jH$yQ-kAi3-Xf(zFKY~M zbpz3^s(U~e9Kt~RetjJF2Q#lROK=a?Y!Sr91c_BHB~yz7C@1TfmWshTxSUvBv-}x) zm(_cq%*=doPIHK!3S1qXR#qF9mE}F724{Q)+EEh{#b@6dLT18wqp4)N*uorLXrcEA zJ1w`G*q3;kLalVt&%3s}M!ss|#AK3$slxBRQ5{NP5l=!rS)RL@DJJ#sj7xAh+gsloQjX}|#abmPyn z#e>4*IO`vK;eQ-5;Xp_j8Zv0|%UN2ss3|8Bat`GQ0VW7t<4Z~Mw`8YG|U?sL)+qp3s+EMC+YknL6aMpYu#+*d<8 z{;w(=Hk`yHwkb=xiv3Vus7&!rkRaWU!xsaa+K$s9Ds0!ItWE^xIsTioR@SfK%D~tx zNL2%Ui*5rk4gh3|cxi|`SgNitic-9?9p{qbrII2RBKZ*}h6LoKJzLDqcX0rOY_!Z! zF>R=;&ldXkL`A=Tb@6sA+nlBKu21Lq!_j&cLGa{m{b(qGI6y8V0$=uYz1dw#lbl)7 zaQCff6w`7+JK_VVL}yAY4h9FyfA39lI${PoBtg3xa=iAkvfnhc>XLfVWCD8azOvxC z6-^Mhkf8iuG_U{|g@2Rp#XH`W+QxT$>@Y#9bPj9gkuxq*o$O^F&_DO-ZR{=BoCnu3 z7C%Ch9vhk3j9Zja(Pr)Qy!j|#SOi8xiMRI{B_kB^9Q>K)LOn=uQ+L9rPf6T)wRgV9 z6T4rp)iMgj#=&25tj{$0q9SUF09+P5n(>3JEgp8Xux=^%{&yI|%*SD~bis8xnDwW) zU7O_ADnbL?bE=foHazW}i@Oih)1+ij-yp?}G718dBKM5m0qKfe43=`z#8H^Gw;@J^ zN_A7kE|_KCNzx{YMKq~}Nkfnc^LC;f;xdgc`fhJT^)+g0L22B}hJfn3jS0-zjg*II zEw`#psI`bx5m=Gd;|{;~;HU`FYOOE>O?TdSh%8Ekv^DeRYuJJf$#QrvxZx9TTeOXs0wj`8Hx zi|HvghRbVMlz}DzhKa9~NS?IM&Rny=jwcg_^I|{RE_GRT<(K$!1U76wZdAe|f;fJ} zGqg&B#}H0@=T_d3`RYR;!ItiuVp<~Y=p#-K*;WzsfBu=S!)`8L0Xe_QVK#Ld)pG(6 zqui~!^-lm(WRyj(ZuBnnzJL}P@~02o4VJ_E)s$E&BJ+a<`#WaH3dR1hcnjV(-S0~p zsdH}*RPo7B%zjgRcHIQ$%Z-67EP>i@@j`Z}cL~Y!(3oDQ+2O}Y|8s7&u-|n8i)k96 zhVoblZp%0b|54LupaSm}b();1`@QMg$WWmDoD|4+Rxa$Z0OpL5F;5+X;KQf=ALoh? z`&=wsb>z^rCKQ>H0F+zdVr+|@98T`=Hji)7Tqk0hH)_gCT+%p_|APat`+HgecY{++ zvLGVr1yYyH`9#OGvp2^H_L#c2NNaEp%9`vBg;8M`8GRz5@%bs@5Zt4xsb7HNPP{&- zmCO31^^FexOU)L{+aNVp{yuwj3m11pZj0r3Ar_knAE4r%U|~cc)k0T>&ig@wMF}>a zog#0fIXzCt;jN^x$@u0#ZL2 zoen#U+-QpUolzwJjV3?HRn}^uP-kS$rPa(E_Z8IQ7v6qfRCXaY@c0d({lkTX->_@g zl&Ex1izPEcnNCj+i^$zU3Jvj0H}Te?j>?sCTg|B>t2(J~axhA7Yn zXdZM^os55*)5K*04dtP1Mhe&waS-d?N|t?fcwr_hYB=W6oFl7w$mBu5VGQ`z#ut|z zd?S6$sB^Cxo1eK5YV*katE<7Hea-ZaJ!Lra2-ZByIx~!%xdyK@ZMfeiA6yFUZPoU3 z^&ur~YRJwS6!f_zV*p8X97Ul{^kx?$f3t#dn%e)p4-PCh5|K~=ltxkTQvxa@u3E+P zyDIWu9__^LCvcY49TZHs8!_+3lS5A+MI@%0cbuZ2?zR>FwGiZRjNIzxOVfxBX0nPc z;t00-)=LM}7Il@^_-v7aA1#yDCtTG#$`zA4fjT!IU1HQJcrGF_T@eh?r=$dML9kZ1 zlq5h~H5g9&o2=D;%)oRR{g#qzY;3lGR67OQQgSxpk%6QW#3bdFEFsu6nt3F<;yQZ4 z54c87tZx7z^c}JUe*De>vHJlX05JkAX8RaA{EQb^SJVc^ z<)3Xj&rL@1>%YJEl*zcH3dUWC^hJ^-A`3`C3HMbqQe)ub(Ea8s&^}AM0ratvBG4fT zF}WS+>|uI=|9t*&f2f)eq0EgIH!JOmJW`TrW?En{k}XU~dBB!|n7pPxIfx9HXuQM% zy{iQs>H>c>-ofFgvLp7fSJ6ns>&OVnTs;& z;kHIB;<1L%ZB@{FQLQ=XVI9m)+u4;-eLmj8K5(&w?=N50bQo%XV7&#Us!vJ+o^bi( z#l7^)#oTni@MUMYy{wi3REl9H(>MW=Tm7Qq1UHzYHglCP7HX&@oy(<_F9;tZKhX73 z{x|o@xSMnXL`o}THP0Em#m~(b6)ffPi}ddg?M(;EXxZFR^q{A0H>c5jIU*?(m{x0B zJ34)4LKsODfR4+OmLzJ#E$2?*ZQa7rj1!U7uR#74P0J-{J$Yq#=Wch{OCC?pyX{By ziE_sI=QjJpqynBe4<9nwK9`hM%W=rb&E=fVR;KSH$fG&y*!?@e@wJ(LCT3poyKC^1 z#PSo_1h2$Ep3N&PRn^=$s4ymWI|S~gh_x;C zYVNDby;$mIPbuQIvRzNiN($gX!T@KAhYwbB>aN>|+6|ok>Ub)!0s3u{h#NJO6-hwyu~|*I!D@_a25se zGjgAfGD*Iu&4oI3LM(mg+1+D(ZG0z!5_l_)gqZ4^a88TMnyg!kpl&b>`JSnG&jxk< z>|R%1Vy9SgXmeZO z?fN+Rn-RUcNlfJoprQDimBFZ9hhID}#*>l27YOpq+_x$O_4;M(IUzC6?sX#v_l4{a zh|^Pa_{ki@_~3g0*=DFLYr>cK%s&FOV-WMmitu*QCHrbCITrVvn1bK&vnbc?yZRljeW9M_?XscENFGmT3fZ!9xYD+uy( zyn=1OFLz5<-~ZFaUFxlq-vygJ{`-ZhOM=too;JGr|sjf zlG8DeLva+2rys6nSZyV%-_TeA6--J3kH3d4nds;XdiU#JIvN*IC7*+D%0IJQZrXFg{CAyUyZsuE{F)eqvxcy z$t0AE)^Je2_>?9-Gx0>>Kl@hp5&I!3)A^L$4%46hYAqz+6nlQnC~JgnQBGnty&7eL z#zA~)$39S@`h80vX0?%8LZMluAv^KFeVlFecceasSUvy9_*D~NLl=y`@5)Y~XynVi zHj3oyP_)Zu%P0HhYps6xaBFB$UwhRf)ti8v%qfzx?bkQNY2PfO)>s2xXsNsS8@>Kja4T+Yl zviB3({Y~gA`1eKflp|+stT=ATmt=Xw(3P@ZRv_ZCA51Dt*iI#T=Os;sCWfr8EGx#_jL|-|-?I#0AbL7xjs55yJ(z8ulvqo|meK%Q zJA9(alg>Zr-BUB4E#l~*O$DLFcU{>!-IG?`RjbeD8v*c;HbLlS&M3xqOdm$CQWjL8 z9wrrO10V#z7vITtaaH+Pgu08Tb8^lJxv@_?*yGzN#Lt}MN;|_rgLxVj_|A4 zeIJZnx%VAC*NmnZzgZJZwaTc8V+X_{XlwPf0aL2f5IbwVq%t6QS?04}I|P69dO}p* z{y)QEyBjmHXHej@*mCitdY8ZYhkw2MPJ8MA;IQS;>*8BRl=sNtX$Ww-wo1F!S>SZf zRSl57fJEOMaQV=l7&9t}Z*WSoYC0&S7`jzVV5Rs&e^sT4O@>QZx1}RZNC9Ey;9#oY z(VMm#nigVKOYAWQ0HE=&R3@3Wg+k)Gpx-rlSbja9U!GG*hLg3rDvdmvQoYJ*7r07t za;&&CW6^#LZ<>h3v((``R^H9-ynRY$#qqnQZFnk^MT9&Xh-&(g#LV z&wAof-!3jt8=U&vtxtORQ6*Axd_3m8&o`b6^{d8+Up7VyG@`(2_ELhO-bGfmK+3_NaX9OEJ(pft$E@JZ{g(x`zl-8(CD%+q z?WS1&K9She+tE`DUUPqkMC7l_#`Umpu97Mb`W$w2Co!1Mq(FS+1cSRaa@Zj&Tl;Mr!@LvO%5Ev!85{6F>XX5wzi1q z$y>d{!=b0cIsU;%=)lh|7!!Ve`3gq7jQPqYb@ za3e&w)^qPjOS}0UAMIQy9Y~zlVCxco!`O8f0F4*px@<+q{Rm7mZ-_suJ)gZ>Yb*NC z<1!7aqA(Nr2_1>Lz&TzdsV^JitN}}PX{pFc8Dpabj+5_OWRAq!qKkRmaI`wi5H@I8 z2t-vWS^rbqxhjU@pWp{wHs1`$p}ACUTbTf@qp$B~08fr{<<_>f1NBO>$!s|N59)QV zq-xuuuc8TF(3hNy26-FSLQk2%cs!b*YN)ch?$+9EfTt}zlxgq6B=t1fU^711E?60$ zSQGbsQB};P*124w&g`yub2Xe8W#tFJ+!>w_8f&uZuL#F1Onk-ySkv|9^ykE;>3vjF z=!3-P6Nd;~vlR8+SU}-=Nvw|`Y)%oslym<=5_+g0Y`My~ zX2&@d1A;x}JURJmu!fnOE2l329iJfN!Ij^=%jXGDn9tqh)wDmQg$^_1AV?xaoW!Kh zdYU#%3V{>mV@WY&=ZCQ+3618|RyM`uJxjC!E{Xj%Fu!5lqvx^-K&u`NU}df7tt684 zi@NX8>tiw==%O2;1%H$szw>82TjG!x0J4!owieV-w4aD)=ddo>REM@2=7?dX{)sHc zd)O$x`y_7MRn_KGx+|t0Rt(?7?0jMOwX_U7J{6qhv??c3#uu1oZ_w8FpTtO#nkp@MK4R~d< zBpB>?i-D|NxBf06m>#8B+4@y79}`5!oQ!{U9k9 zPps(Ngvuwm0wl@eGB+F>U9L2qiljX=&POevZvN&f&S#YvpT5a24zb4jF!U5$bR@nJsv0hlpqrl4B?WZ>1rG2j z1L9SR!lat@sw#GVT93I(!Gy?W5eIkFtl8fg(-=oGuhh*cg5_);`+1&xV>h534?~zE z*pBl~@EU+Tp#jnJPEs7)elt52jGv^q(T~sD<$d%Gz(*(E!oewDWMR`)iBQgKg7dQ3 zs#1_D!b!M(EWvc4F`}mE^nVJLw9Z-)dKIUu;*<`@JB_liziwx zgj$w)Bv=S6!bS{D{Q`)$n2(IA7EfvHf77byu!03KPL3RSbPJ(x7ygZJ9ivq|#GL88 zS%DhWrh;s(&Vp{^+1LC}is`){%e2SlC0aJxc58g(4zBsfs?JA@7E%(O;I^JP0sKfs z(Fc0f73(Zm)v6$nOS#s9yJ^z9UW`{cc4|1+e9stgDXajRh$pL?0qmrau_q<>Uw%rF zzmFXRHQt1F!IJ75n9|Rg{A(dTtvzi@tT@k&9QgFq1emc+5|mPw>@dxx3r1enb+s_8 zz#nEpTMsa1-IQDzVC) zT1Kee#BK#I7r+P57q1j&Oa?|d@ME9H>D%l_phQj3Wxx9E@pLnC#ujDsctxQoO(tKq zo+f;7?~H0IZq*E`%9$sa3b-&PwS906lCe6>J9wBFU)`^+zSw38$FMoS0Oh-hrYEdk z^~L0p(Vo^YL_|<x5~MMcGUtF$!}}{`%nf-t6u$GG+XhcFB$J_r83Tga0qwt?Vp@ z%;IWFcRpyPKCoZK!tT)d#>7)knNKEfaqfXjkxB|*Bd5V;OCcQrxJdQSV@FR#$Q+79 zB3pcSR4N1;rqW=#l4mwr43w4@VW5{IgH9b_bnt}xryB&lpSFq@xzxW28>!1(%%~~A zC!_${a0~AC^_Y!lQ^H0vwQo6Fg8XVC!1KnoBsd zAs0-_JqC^qFgPp46%)SP8lZp`*9C`=qJ)~=GwxnjPDAx>ZYGP{8KS@v8Y-d><%6xE z@JKmr&+Z-X`STY93ubM}exXj&c##y4c)uSv{Qj{F^o)^Yf+2@uemuf;=$(Be=Bx@( z;fW%(oY3Kk@hVY3`Ko~#LZI>2Men>~=y#acXhck^9Mg~{GwC)}4)2XH!J^J2*!zKl zyZBhfaku=(=o|ZU5O%NOkO@{64d8F2e|7Iu1$K=}r+^SA;u=nM@@YG9 z!?rqfk*Us-4uWsk`J#( zlx&j15CNORlcnPaxkq)Ko6OZ^1R3gOVtsYj8lB?2cg?_nxnoVfX{ur(Je}ICZ<@$` z(4c(fe-85mzRV$-1r>;Vn*2K<)4Mq?iNAvya?26je^43WZL7#U@t8f?Yc77dIPh9QCnM19W1Ip19UUq8BoM@XraJzyN7@c~Kovu8W zjsYXlW4rci8E~CddfFRd-+i;Jq@B02&-@_JP>?7CSoIWWMd;EvP90EJiATodbE9o4 z`f79a);(bf&yO}NOKH9AD8^G^O0|nD6TNj?aOJnEnTH>_kP)}oA`c{4}T;Zb!H{nQ5lOSR~d*%L2 zii`N@dqe^KVq*ix%_5q~-gFuxUaS?armj5!De z25@y4zDZi@O^naA07Kb4H(RGT>xwpJi83o#GTh~9&r|G)XSYV3;FJhA_MjjQgz!B3 zIK7$Y5*8NGi5>FtzUNOunGIucm`D*ufq$s&h|)UuLquEWWt=;BFpsotxGs&hDQ?P9 z66ehvD<(vj6^KUYPd`oxlO9MZ5Hq}1A{IAEC}JGB zG=2xi@F?uy&}e@p167TMK>MS0{OO#%dshQ375tCE6~Qr1+AK!|Aql5oxP6{eKusVo z>N4XL(pX?k^_vru(-+EPYUZ8uk^3+#=&{1Onsk{_ZmE%EML~dQ4P121R3}{`+d)I3 zCPj{03m4n^(E_rCVlH<>V{EEunsrF-=MUy=`ybb+Q*96mcYxY7ZzG)VoV`4@BX^Oc z*AR2{AMs<%9jJ-S5&*n^P60vB=>}JM1P!qvI^>0g7G|ezNx^vPRE+FSU!_-&+j-RR zIg2<0t!ec2B!4kE@K>S$4D|91VJr1SCfkZXE1k0kxc_*koaN*j5LW2LMkp&IiVqG+ z-SU7?xL40n&OGIJY+OO1A#=6r|HZB`U}|Xw$=S|>N2#_i8{79Vn`e<9MNfHv@N#N( zAJ41~6wzlzpt#rA$Q?XU?mOyc6RH?$1OgKki#_RxWh`#kivZY z=^c7bJHgkjZDYsPaP~*Z{TymHAxZ)X@iLctCryci0^ZkCIuD5n{8r)w`xW&<=LvdA z?P>}o=Qs|K=x?!5l@_G8w`8WcV7ylPWW#qy^&cmI;}Q1OrxnheL%mlqQ=^DZ#pMIr z#{+|4$QLaM>?o|yCZGgkk##M`a0-c&&7)0z0AVCmI=i@R-bsr>YOI1RiI*uXus^l! zsC$#6&xaD3f<={d+zhnWl^1{lCXT&~n?PxOo&!aG;PHYSA+V2^>tM z&!*6(YjBFMd#=m_6(ZHH8E)2V2rL5Cc;`Fw3DRNLl;BwX2#&hq`ii1p!-=XDes==M zI&jI~H_Kx|^s6`|nx8c+DZZuCit_MDgT!nrC2olcYs3r;OjPxtY8a59%Z9=m^OS01 zxm(U%d*+UgK#k>pLmHK0)&ll^&XZN8t}&Vb@cS?B&GD~+Kg*_-8Dy7#Mb2Csx(A%y z`G|>Me@C6aZq%J~?ar)rT%Shkv_kP`-WqItaq5zGx=Ho&mB2F3A@{(O%5jka{UVAQhNj z&rE(VR1pd*S&hU3u?l){%NR$dyAMQx@!8p|Aw06Girl8IB)g3`Zi%|7*U7!Y|;cEp7bzO?!?xmuD=n>?Ysfg%%}s!e2eGmv-#Uw0#iZ&I-h&?=5)I0YGBJ zqxy~*@sWOYlM|Ln!c!^ylGq=8G!b5ax}3@OkW?d0ULz?@RH8ZNvY6&teVeuNXTJwp zyx^5f__D_!nkv}&gIE3HRX|?T(G-;j^4`Pjb#*cX{|y;0U(1|9yQf7KDXFK^?j=8U zX1nDPaOPKki?+-!0Qn8gYUu;1^XkqTvqZ)`2MX%ivfb0jEC(?rx@TZ|Hee5BZqK2( z+&rE4p&HeQI+6{$nb+X(vArd*VDL9GIbyM3-|9O12J`!(J8CF^Ck#3`vkS?-*OReg z$ZO0>5!Vj7H*fh^`X(#+k;dX7#wb zXOO8w`JD9cWs7P34qX~@T=Hl-t#EZjd_tCzz?y}-eeo#k)Y#8(;a&Wc(nW{{G!qOn z(9U=pYli6e%w14ZZv$?ecSz3nx7nDSKAbdDn%HcYpU=iST(*QxOdxGbBDG_9V^;v( zWkC^x_v0L~>7?H+tZzJC|nY9AsUu_@)~N@ zEwxFM%+$~*F-wWx4IhfkyJ^68P$ML~fBgfcALr&ol2#W+hbgz64fbohNEHI&sSc`% zF-?y-@Fk`y8ZJ$beW0lpR@<&c-VLB!hwplnf z(AYyI{Z?YPxru<5No_H;@IQQNb6sP7W=%9~`3hcc-&CF~9vzFvg^zy?x!OhP0Y@>v z-Lu@4&yjP15YwtV=g;YpnGJb@Dkm{RdYI|bC@r2ou~43J8nR=AZV~O08F&V=B&U?S z9gJR5xxV^kg4ZfKmxX+fb{Wc`vAw0M<;eh&ll^^t!P;6bLiJB5JQ zz_YC1@Fv)L|2;dCZ00OF?G5k;Vr+y|`gd*$Jfps+m5#2`Y#XDQB8Mn0yWeirr0{AT zJqF*!=-NFQD!i)N zeNMiAP_itoVsj`FF|_{2DSt{+d3+P2Up<4r{tk_et^zj1bD0u1&bCiyvLmeBEO z>X`rUaypW>(3pD0`w137eC*HyM@2je=CG*CIG5Z?9_0%#daz{Bd9Mi8)te?V`YXp= z5I7|oVGN~&!ygCeprEaHXLH|a2U=AZp*^24B$+)e1A4VcXS%E0sl<>eyI+4~B9M82 zGTc$?CaE4^bK2WnVUL#kkU!q_2@$L9?darFwiM~g^SEo~5*d%JJshFIQrG9u00k9& z(=Xeh^n6_V5@vhyKrx}W1C9p;QiYRgD1!L-O~qC!8eB8e2xg@rhc)zxuJFcKkB^3C zGHaJ#mq_jcK6MuPQ~-CY$Z*6Y9*RfFKPas)^PDDSjZ2g+~?YPFBZWFl~sZ z4-Vq}F26)LtQjl}7|{M-*eGAF<@u8aCagsi8dF%E&d>JDjAAdEL*&t^!L5Sq4GYee z>MnoL7>uA~VA6`tO;bso|D)a;HDAW{z7E$Jn$7aH-Ve&|*Xj>qB;x`5SN2w|16j!N z(Qp(id6L^TBKsF)7Lv%5t!wMQ4lFKo(gO_d7eS2-xi$ALhxzn;-_f_FxfUVZjnPg{ zxbmE>zwZ+wsHy&ADiMHaWHJ^DDcq0~%!dRAUZDkh=9m1>QZ z<$uiznvO3W5)-UiD=oD7n<`n{J0r{y2j76tT|ZMp>x!n5fXsX}X#QeMe^6feKHl(L z*fdq;=Ei)X$B-)Y;_;c;_*e3cGD2W*#pqLI3RYj*0qi%>Ew<#p8bGFIzL;BO)LJZ) zDU*hvD>wYMZAPk?BUjZpW~hiTu?al+I!`IY+z=gt1?tg0qfBxg-}nVHP?gRVXl*(2)mmZY3a|6(vyRS{oxL%Ny5AbBS6_n=J$oO3;y zFiq*F_79ye>hHt-=95JT@lZg@({}d|*I!?-a9pf5keCG7HQ?5t1L;?-m3N73!6a&0 zb@Bm9_8-N^b#-|$BoT6U6#?i!-_4j_l6fpL0ZEtIvwj+=igbUHw2NY$DmNkuBBa9? zw6eA@%13^~(mbu5g4ZE(5`Rg%X~23xY05LcKupUe2tCXGW3pMu0;PlhTtu?kl_^9$W-ZWoe;U7f zk$>tAzj&{t)gG`6>*y;o21CpYP%=TxAGhVr%|5GZtqvR=TmT^Y_?_HW%D@cb7D9dfTLbYpsB9LVgKEF zMGotWMLT2MF(;nR>19uQsohFks=sby9}p`l(jOv6Xih&Ja=k^JlL<3wB42#F4|dL@ zv(4JeM4piJ?LEHu(e8cCIRZpt7q?}urimni&n5qZ?uwHVr*-kyDh;#1lHqxoJz zJVBC13gvCqqR4l$g8=mO?wZ{ASw^f-6GsBl${@nPGz?yb z?@rF-^QftXZduA^?Wrm=FEx!?nbq4Su{dU-@HwYcF751LzY}C+h6rB8 zq`dU_AIstUa@@LJe_oY7A6W(RI{N<1?Kw=z1SsDRuk6Pma1gr-$WH#K(T9T0a#otV zYy1q*YstjszWl1J!W|0EQGArO$oXxwG^iG__b$RHAxtjkN@1w>t4)diIw@{Gk8KLo z5=$TmT}VPilg1U+-1;i9)wce}09Zyh&{Ns&%o#W3m+$tuOvM#5<1vrxiWp^2ZYmpv zmD8mfW^0Xt3;(|Z6YEEP$zNm`ZtDEg zTksz2yih^E447Rpe8}#L)b{`~!Gv6(fQp@#U?8}YJOzH?R$NMZ=cLK2l$AZ?JGTa# zOXjfJ=bY_I|It$c4($`%Jtx4t45+`G&~6YmZ7_r2gNnI-)D~Q01L8V_eiUOyarPIL zZU$IIcA_@nyM^BsP~B6izt_3g)7kCKeigJLc!^y5T`DkiB5J7?)S(~&ua*GPC&;4v zYk1`U3z?2RVoPRlWK_>#_y^|8%K2uUrwF)xunt&u!K*6#HmSKx&>VMk!;ZEl}f)!lK~(89Exf6!pOiDg`Ythig;Fs?)9R9c%3jH9ngjteJ}iUKRtjeh>dEs#prj zNJ5Qxq9JmW0Gs{*+`DL+og>uAA^Y-Q?uk@zDUmZmzhAWcz7(<@T7zT7b1|}28+{oJ z-0BtE6eu!wrbb+tIZ<7$NFFTYDJk>8I>i)K`W()udSsAA5Mc8-Uh%Uo>9B!jcBkK+KIG%-wba93&>y&yg3Bx*U|( zIK*>QM_V1uPXkg+EV@Ir+rk%tr>js)6LTw2x7psg^7LEvL>-3nqTtbw$OINhoqI{; zLE@*cu@sB0^Jz0BIyUyTuWjP&{LdbV5eS#UW6YEruK6c^Njyx#xvK ztmn`lAeT5O?JX@VnPPK7dFdcYbBo8nl!qpV^GEb`TgLBm;*(9n!vS zhssw$h1i4rNDCb2)+4>KUMJxHavBYDU6R>(znY_sSxg2}S?-tMjnO#U{n!2ysMz}* z&QLJls0XF^mHz4O()j^H)uDio$}7xEBC`MZ0)z6H^$6S6*Mc{)S=pyJ6YOONhiDzC zQW?{NU3ay;(LK}D9crWk+ZI58l?boTHZu1wH95I*1uq(N?r}1WfZ`8cf%_bc$8-7( zS(1(_yEe*AW22MO_QY(6#5j2w2T*WAEPc zE4Q1$Q)7E>>P{BO0>6TRSC`xI+*Q(pLLm5bc@Rvh3Le||&PpD?rJ3OiJrPFq9A2&Q z0axD&IB(GkIQvH`%P~UQ_Ya~ousq>OVu$`GWmz7_ENE-+`5>(`-d)5|a0)pbCg|9= zDTEs0QaVtb7VYm_-#I03 zlcxst$W!+mz8O7J<`^fU9B<`PP;wU{ad<uT)Fng_U+4YesaXC&Ar1ERef<27^~2C z)=k4y|Ew1%LGYSJzCnFt#KJtr;Y`S}T$h*Ir=qn&%JmvUHRE2unuXW#?wL`B{^R8- zOfWvn)#INvh7QZ_-!n9^dvMA0QBaSo z;ocb@KD1t|U~?2vqeyq;Zc-nuyBq9U+J?Wx^aVXeCqNNOea8Md4#gwsE#z&YI_g^3 zMoT3X4lCk|n5qPod7-^>H*HD^&;8rcMX2^GPjVaxs8>eZ%WVJjxdJtZO?xDRQh#h( zdmy*0c1wl8adSB9o4+j9Q`c*|?8cD26ypX6so)<@6a|9)hgw%fC(q~>E9mab5@ebk zX`tI@rI#uJx@Y)1amBV zlc{5n&p-dDb-FZZdjAI0w@Zh%ANJKYhaGDTEGLYQv%-P65AL5P57)`-Ji$~T0$_w+ z9v!YQ($_H{^~ZmcCTBlr(T%n87?{7W*T; zDjHpm8HhcJ82yeh& zr=RUQBzbq}Wk?2Oae-g})a1{AubvG9qXSkc`+x}mTW zig;L>>t%)F8?YNOXFhMZ%-9#TkxP4e^Yff^U%6^ZUU(;k8KccW)5*l@J#HfN@VxCp zyl-%<1F?e9@4a>Y-#>vF@{Dm05;E!}Q^I8R3nSfkA;p~@_Y>X@dIAlh=aw)rfP4uG zr^3=(`U}eM-Z_*EYzMvzN^k$+B6PVDszQ55H5oB!r1#bpXo%(q2487-if&&Wxjnbn zpRSzQ?l_WylB5>P(t+=7|B+(7^D1ms|{ zX$+Vxg`EKxi&-zV%l07_-;DlBBM8Vs{0jJm3?DZ#SYE5{c>6oouo(JYEKSsrt##jO zM3lLdRFe5+3CHYszdz>sb-PW<5f_-k7$wO>ecjr9z81bQ&Qi_>n34sf<196r0c(_Bj0iy?oF-JR#?x2KWw_??hNE5EZ(AEPOsMOUNXJ0Hs|! z#rB1&PK77@sx9OxMpyMNcOQ4_1G_+;Ug37YOO$)#r1fJSFlK2h)*lnW5W2ZRRvEQH z3YyqE(eFC__vgk(WiVroU}1L&v{}Wk}1>O_5nWav?}%X*UY7 zJJk+DfP%&KK*$3ft&+C{`w&jc?kDap{xV`kpdSOW1SbVT84Hts(BU%U9a>#z^G4;c z(9{>0q62U~#v+?K+u*~J>ZWGtaBKe;S}~>0@h*tCKU3tR6OGBgjwAA(pR7}n0wPae zvGxCB?n<=ewsGJO_0Hpc{Qr+F3ZO)QVt3TGva?B~WMX?prD~Em3l6i71KD39TN+m) z_1s*k+#KhhZ3WM7C94^|x^oi-Xg)m0t5(lkoMdzrWT|vPCaIbGF4&kU& zO#P2YOhg6h7tg;GUdJUE!!jGbdn-E>p^y+gnmJR*8)s zLzNhP6#QP<8EP4g7$qCGRRs0*>Vu%^=s=A;R|jdpi9^96>h1{-k!8b=oSi8iiEZK) zWZx~cXPLtXR4#$IreV4nw?tk}%??d!WY9NRZcCpjYbBh`-{QcL52poFC6T6`N&VqTg zbUdZkaS7hM42$nFZ3!yfj!YK}ed*D&G}^`TtV$+Pg2SryJFg3GX%pVhr-LR`D`e-S z%4#RG@jD>_D6$KYMTQXnnUAl~5PSD#Ey1?r(+EAwYTYIIMe`o3g83bPk2ZTl%V%7I zc$IDIp$@e~4A&+8Vhf2UZ^ybWKa3$b*dP4GjYw3NUSV2%uA(2RI!Fr9DqF`OBVCXn z&rP3y`sumqnFe5XXzulMXpd;_RgFWzA|mhU4AFN8QR*p7A4l>N_n%PrtfhiuLN38b z|JxpYQE#BAiANg%xoWgHSaUqa)nC^_r84m?)I~jGwwm^z(&&5AzDcGf%uG7Seze1$ zum({C5Roo`xm%4=X-T$aIJY2Ug`!5Iz+tuDnkPc9ikEuI#`$Ae;ut@%@rl3isAf&V z$5bwX*z#g&MiFSEH&|MPxD%aIMUK8^`}Dc6$sgw@`@M(Pz8yz_VslzEEnzv!l;lgF zK6%@&k*U;ba6|ZH`t49m9U5P>lWM!OX(S35ad%6qEubSpHVYGWNpa#9gum#xCq~Ja zK*WGe%bhkm!wb+N4-`Z)S%du)MBUUDA_*?<2j zlRHd95S>fMF%c}f2|(e&D!;qvq;U~T?~UP^vR;At>)rFw6_^<#UxJs+ieULOZ96|j zwe9eb@=#YAquxW?BHT6vZ&znha?pcTw=qkTOhKUMj4{*a3S;hSWR?(oTj4Eb|t{Q6Y&!u+1WM-xanypBsCreKw9dKUfl_Jfs(+DeSQ z&a8IcSMD+nys9F$hf8rDisc;HL_4)hs77iSZWb}vu8jCuAr~+~q7Bh=JCKl8=~#cB zPE>ga_0S3fjHY|;1G({0Y%XiUw2ARe{P}<9K4dbo$0Zo^>OSbcN3uOS@suLDvE)GT zQINvQR#6ThJHp~5y=mbmb00C4f2+KJ5po!oIhjoXvjVdo1M0s@2}{sLIIEho{5IW}_US(I ztF>qbD}HZO7v_{7IDIlDrYBhsNh45zh_icBDQ2e6aF*XkT~hvC_W={g#wCF1$!*rQ z@E(ct4asiu9*QGZYZ>3#M2ks4S~)}mmQQyJDpRWx5#VIv&W9sahD@B zr1a=d`X6J)kIY3xspx!q1tz+5bF=iXz&l$?#z@Kj(*##RdY`^$jg;~_EC3{Hfk6O74i6CP6au7ZhckRnZnE*Eb6ZsKYV<Ag80%Ow!|GU>V( z$+y!w!K5QP@t}mh%%PQyGjhZwY2spfXPWpQNXfKBwq=UY8BMf0FQGDe-}Gy>Rt%LV zf-Q0N7`j`0IvDF_*0p>|jNMDK7SF_2~I#~NSMtU++`T9q|L6ZSP-^4 zMqVGWvI#*UBGT&ZSXtkYwg%?ETW!xK7m9x!mq1L*ScBfeFi}#+aA`+&L$5rVD6W28 zOzNnUP-bX$olVlNCMLPU7asxDKQ$(#8w#fOsLX7H!21|4pL$)U5*FiqjW1Cz^KqD# z7?{}_nDx^zt!q@|Gkfh(_5f5aL5c0evX|LTp>IZhvGZ6aGXDAEA3(|QY~#>#s+KGdS78ECxG+cKp1}fF;h&<^JW1uf z-;jSX<44E&aS42$3~Fd?KT_<1k=XPq$wl@udwPwG#iD}MhSts5cXK1X>N`(mZrapG ziEiw)X0$caH4+7kc&+!WnSSi3R@6E71^KU%hr?bj!B|(PuiNpwm$cEE@rye2lHHIb4Wry>-3nE>53&6OW!c&IuJ# z!c(Dzsy9%Sbk2uYSctHD8)%p=pK%+CU%-CpC_EY*gYwz91S5T{?Jn8dQGpv=Z|Fd# z|7dl=AZx6D^7*PAT(_x7+PQov#t4|18>q{=19&m7srj1?HAqf-45?8-S)Z@~a3Z(KeO1Y78H|v^ob# zSFYsu`PDb$T{b0~n`W=mvqN`@j3$qP3{rU@1wi2;p6+ds>74Fj`@AF{xaxTRQm(b~ z{=t`EOi>?c#FI$)x zf**#6M=V?V-x*tjeJ!*1xvmpBJb^!U~-&o)>&(xnJm9&kkt4(ErMhDO9| zIyD%DEuvih&PGaHvhUJOyhPeE{!f-+dL5Slr1RXGDdF_G;YuOvnWKChLteZ%o~U!0 zDQOu$-pokd{UGE^0Q%y_ogqx}*M}E}*)mCO-NJLhgbK@MiA)KF)IW5`I%coy56cTshxY2+1HmGa1<6ORs(51aI zySkp!9!)h^4WF6&&f1tqz4~to%ow-Yj&R>F*8!voDOUS?rx&(}zOgF4wg+swY zXzjL5kVxy9C+A8!Iqu<=O&I=9WY9;`M2kz%R zw;+u9E{i-)U8Wg-X;_9wJ0&rBc3gt7*5#;!tO|2RTO&E&m3J$KU?<|Du5I?X%Exrg z#?6`3bmY|BJ+zHzlSA$ywe-r>>a%E;9Pd0ht5wE{>*rT+B5b-_ z)(JTVvT@rJu35O>`Ek#t;~I_(xddTr%OcBQ1IKPD1)3_Y$JWawe=0?7DuS7oh)J~p zuuWHT$5`W9Mss;$$+d{eA=f=lP&55q#DkeGA~G#%MiYiY6@AQa^Es?wB!0amt1nQ$ zWb4sD3GFh+#kjh2x;z1F^D#w(5aL&nIGNb^{Vy{{7>O0$LOZSqDVH(Cs#l)tNNbpNbiLge)487Y;Hk%o2-Aq zT5S1sT!PWBIZ~N&h#YB;7$A@%XI^$WoZM9FN7fzx#q5S`HZ;6`a@Fw}sIdfEch~Fy zE$CP}c0)1-`S_Y!VM0iTxl3q1R>8=kaVS`EA77feoGewaCI;Eobk&z@>)E3VW|drm zE3COvI(HLCV$d)X5u~l`XI}c`a@U#Ctd>?g)D^Y4u~z-Fk(em>dLT6=sh=&!KOSId{?VnOqEGgJ&w)f0-_vrsPLoJ)10T% zj#eJ@D(&965nxbOBn0M|JL16yZKa@8c_LB3BDT4^rOGTmzDO{%wMhGROzU50bL?z= z@g*3`n#gI+*5f5#9gdzGS<+hT3TTdu8Y3S#Z;M+47tIL zV*;U*Gr}Vigu+C;_1mS&bz`lp!I3Yml}Gac-M@}YAhy*LplwV9!75Y>$zE^tx#r|e zTs&>el?!JKQhVCXjJt~BX=gg65KZ=#r)}PJv{e+cA0noXDDF1ZJ#i>l5gPt?Dp2Uu zD_4KJEueqX-=^<+TmrEBb-7g&LXN&cuGg`CaJ?LMQoT0YJLs&9LoJV)OLx~Rk zYb&8xwEXN?bIxW?iz*I+3NdYp{9RdpmJAoQ`bq)n}Q06g?9O~AhtTR_@?a+1&ac@iZYt*aSsFxP$z6hT5i!*Q? z3Ny1dEW$3V`Ixmza3}_d21e(zyIrbq6+l_$#q{uZHs+%>mA$@>YaoUWo24>wa)7>D z?c|FWGE$PHb98Spb6MRrJFsk-T{VU;T{@fQD*6_vQEw@G{N|$Nf=fh+5K}3NHQqeF zhC>9#_gx)-W0s_$sV(W{V>jb>i->##4}& zZ&<8r5qkKwjg$AHd6OT`jcSIs5Jo}lMPwnZwB>7=P~Xt@&a0yr+?7L~c+HVFivmD$ zKwR^tN~n-+lZ3bwMY28_1R7Bc&fl{$({SxlGvUj=%3}Ruwg<-ZYu*M<-)tQoJQI<} zpp!3?@!Tr;#;I<%%Yqf>jh(Vn*n2nA0CWCSC?#@1Tg8CSihdJm0zYdTN=tqiDrgfp zcFT!CBO>O~ZK1X#;sSQf_0K!!ccx2vmdEs(w}G1MS-W!vsndg=F;-t|(T8#w%fzwh zT2(-76jYW?>b&iTiUhJ%z509G%zx%xY2z$GXhfZ!(NpgRiADzfhX4nT?l4UcjrL*5<(aW=JvcWxW19VwZ z3($v<$)NhQ3fCbqQVe&|Yw&0AZ1Etj4Xhh0bW;U!X7wt4b6JR(IFi=j*+im|p(pT` zHCjd!t1W|I<6()H`QJ20ImkP117}TIxs(`8(c~H`y=~%?Y(w8l+VXfUX4&S5n8*GT z$qi_Mm@5gU$u3E>F-p($9-Wf0swvB<)##j_Oi>Ih4HNtL-3)t47Za6jh`*vUK1*kR z&D+3I)z?tWkL?Dcf*t7Wk)z`vw1KTD&3&71u2 zuvSpBLCPh$$#qoa_RwRHNcQR>wxr^sV~Wo*+NUb<{I#SLDPo-gOl;AmO6Cm8Ycj2} zsB1du7eqr5(+@;vsu~Cy5|pcN1xD-Hq_9v|7s%g4NR_dxzF}lw8 zxtxPp_uA#1IPE88Vfsa#a`)RZmsuHDQBWrQh1krjEy$@VKpJ2SvD{WSikC_kHXKgq ze=vIFQ5jP>ysvp1IBB=G-aQIv`2m*P^YD* z(vw{l_ruaIYSB%15DiKFY_szpr&>s8fIbf{W1SW{4IMdvDzuH#!K~WP6M8O8UeFs5 z8Wc2?ZUsy^+r$j^Vz~6~^HiDko2O%b&D($tX%x$49pkifrnjCuytXJaR#vCKHSP4f z#mVSr5#cD0>Es6%GH~bzVL;*-p0>*ro>fd0@wZcgo5^g45}JN^4JINUdaF(zidF8C zF=X4}IpOw){R=OSvD^mivb10N>JEWM^5nWJs1oyiH~+46UB(-B7HU-%>P1F#YocDJ z15HP|Z9J9$toQj*r^_+Sc%QO=dP8 zUT}oOTb4fBv{uS=r!)9jDOjB`@agbhTOc+HR-0VLG<49FBs>=SRmEfS29AaoVTRtB zk>}4Ah-_(GB}Tui5KYhKe>qWb+y>6>*Oge=*PuJSCi?V}=LQ=AAIG+wjNNNzag_qh zmchhY!eqQaS$dVC_#h$42@I~EQqMAPn6CAy1Bbh6-9=V&O*P#B{PxbAS$12*YiGFi74T0W3(a=!9 z^S+D?+o-#Ak{!IguZa4}M9mMg$j8UH4X7b4*(r`ca%W(pmqs?CFLPSt!eY`DQFW>W z@|&5?W0ZUbStp6S4O-dczK47yVXuQr=K3QSZLv`4NG&ICA(@Pnd`-NS+XHFRb#Mt= z4j(!EN|-Y}Th+#?xZ^fplcQ`8#_mQ=iVS>=jp6wrZ7bE^vlizpYD3+Ks@5NzHl2tG z7}^9Op6iaf3&NczW5W{(lTHT2RD(bxifClHlZ!$OE^T6PzI`s!D8IR*a>K&74b*AP zO!dgA3{=Mi1f5fZZ3iB`GI07R=7XHA=CLf|AzdXiRvs}Yy~ffK=EkD#o-6tq`KJ&U zsc;}IjU{KZFbk^p<-s%(jVuNuYOaT}=fqFs$j zzrECwUtPyuFMSp%zSGRTwh+<=u*e-iJdXT*h#I4nB1i2xHl%%pM%2H`18D#n5Ybp} zg>ifz&4W4GkZN!Xd6k8jI=6TyYwHoQP!uj!&$zG zU5V{5Q7;{Y^e?lYqHHxl(x-dG+PK{A^`E&8$?TULKg1I1kqn;CukjFZ{cdibTL^Qq zHfivGrQung&1o>w<2G=zM`(CU7IIn_3916;e1r5mmDPeS;F%(sq;{9qN@V1JJ4eyePdyY&XA0m|MPE#zDwUWPaU zLLde8_s2i`ZE~(QB1|WwP@-tv zkVix$wvEZYFBLWL5cFBq#rsjT*KOx#=~mugdVl@L2ZYJ8$iLd2vrx6Us#r1`m9v_6_4d%9yts=ZbRSOEyDh& zvP}F5B4-T+Nf>KUIQZeyg~vcV8exAP4WsjRQ(TjwEFRQIQoXM(&iRx#^|n8X+5?dBreW> z=r_X7XbJ6AjUL1qZLLE!K9MQny^6(fVb=trff1egj_E?*f?pCnl+6OObe9JHrL*vC zVd;oE$!*{ai!N&jhnte<1T2X@^Ee4{4!Xs58TJ0B&6!TyY2dgMiiy*s$8A83 z)r$Q<+4;0(Fzd&=>Kpbt5ovPqLLqs5XJDJR&9VfxyONV!QcO7=188eu#TpDhDbX|( zLMq+(oDR=)QlL0usj?vvJB*n{;^is|nLN$e74NEM~nxmq(Ic@`ITH4e?V0B4M zL3W8g=NOue`dvDst{Z51^|O*xHqdv0&{f?or}WbxZax0#tp89-3q&t#`@jZulf_ex zP^_AahVk?N@WW{^8XS>$M{JV6#}K*8i&@$h_+M_L&&EQzZ0NWRplitmN-!H+QpJI7 zvLH*Nl?WW^vIaToqMcg__D&w95aUfna1Iye9m6xocI}9xflfnZY9hOp$OWVwoS#TT z(2x)wxDh<#?=eb(qYa*+`N?4Z&U??#CI(qQ;J6K-v`-o_3l!wh8b!f{hw60221Yv- z!B{f%T&8!=&nwDe|1|VGfbDG0#Rzavp5kopc@9-zbPhyjs6ovVa|Uz0YUy3ow|9y<54x3@(%!e{j4CCKw=7#bFFf5YD7 zZy}9KIoZ<=%7rX8^h6FfKbvvja=hagrEiUEnWoe3tIRV^sbWwqw~wo**Jy~rcRoA**b!n`HcS{owhHaf%g`P^ z`M7ZMxQ!nU58d*{k+%}DYbY!a@1_!OlH2&2+2#XoC`$>Zqf*LaA;e#-}>?}13SI|6!>xW>O~UZ+}>>AOSm*(UqMQHyaKKRo-z zoMCkV9EvU%XhG5uR4V)OZJ7->%J+}WdGMI6J=4^(EO0DJqRp}46TpcaH5#sRG&^Hb z3t5to%k(()~%y29mjKpB*#EF$hh%Cnmelfe#A-r8p zw?r+Yb_rp$GZ-^rXjnvFx#L!FET;S{hhVNY8k*L?%$Hm~TMxsEg2!$A@ER5uv2q<# zAzzNH40O@bDy%pgd%V!d1u;PjKc#AEoTc_x=g?b4?Z)1Hf~;CbS$*SyJF^$9)KO~B z;;|=oelQJ010y#3oyFMW;Us1oo=JW|p1w*-q-R5-i8XY`ZT#@F^5-N_$|dR#vDeT! z595*=<~{As0ETJ+>W2*s)QQVwwhKdtCF8*&!$mu}y)24(ln;S`|v)g%->Cw-nBkHA?gnH{?_HXbesw zvkvZJKOAdc_!`J8g&Qd@N56x!*q9sMo>FKm55AuSPRy2b^BZ}ycxMQHUG3_|V zWk>Cwy^CMnU-N6;2I{pyMmo>IBSs2xrz5%F>@r<#o~C;xvJ#QC-r#Hd& z(zST(uD;6i=;zmXVr>4NwVKB?!L=$ay9M!=z4Ff%_ws-SOk;h`+ko1Z8tv%1s`4dIKagn;8Hw7<>f_698r@|UYqBW{l{viZ zrr9r(J@=*dQ;QpY7K6!f7Kct48Wu6wZiPnsxtZFM?HpEbKmDf9D3K_+4V-Jw5-Rfw z6BjV~lpMEKTVXvTw=P1i)?X^@gDsWus@3e$v$VJtEEQWaHqPRctMKmi*rlBPxqlXU zAPqnRie$dt>Ww%oQg40z94+2o?Gwxrj*^@F@ame4fzlD7Rvo016VZq9w$R(zhC;bo z|1?DFrcrP%y8=D)RNxcBOQ`)#b!dQgo`aE@Yt~^=NF*LM4DId4wP*&A21qpKTkDTD zmhRRRZ*JDj$mz3XQ-867v`C~%ZsQe@VQ&ve-$&fklx?S_gn6(VwQ195+4jP27IqWS zORKI|1Y#QZFD=dTF+voGPG!ua=)v}(k#pIOwMoC@DBGS8G$dlR-7--L8N?12Ru_4E zi7})FAty=zAdR zflLqDw6+rax#*Srm&ahdiL1KTY4hOyGyLbNP+zm0`+CI7hu>tds3>s?%r$BlR4$`5@`FxH+d zRARxNkG>!NMNSe@i3Wm(gzU(B+cb`2F)hi9T-OkI{Y7iEaI}Bi2Do+hXN*D!y-P>C zO>Q11Q@w1N?kM0t&6&2TU{icu+s*uSCLwL2Jnjsce4l+WOU=6@QCLP+C>aDA5z+O& z_z?S~^p}Ojm1yhM*nY844dKW0Ho%>G^+^}k8`2RMAm0MlfxN(www*~_wQog5lG~KT zxHnRw7j+7Bg2kL-VDGDl4S0-`&qtkFm6oz(o?qjM$=@qC1aTa$Ol5K2|E?|RVWITG zaf)#pKuce|zQ_|M;P2IjWrn#9T&KJ-JWbRkst9Pi$}%-P8>U3iw@j!pg?8?mY1+6A zr)fg=!tmT{nv&ON6m25W$cVyxFEN2ITHAOK#eFt5<1dCo3m+xpHeffmti;6H6|_ju zTuHV<%%$-tB6qb>%q%8}UfVF5YMHpEm_+h!HzwrFDv_mo?-BrraAENUy7jjEzOAS@09?%az+5|=5 z5b=0R5e?k~V-BdbI$0N0BmUBOc{b)*C7L9+fir}~csXUVP<|buX+{5Lu`pjRo2QZK z4Qg`vRc(1U`*XEAk#_9Fo^QD!B1Y%lT!8sd*P@r4>uir0f=v^NMn-4Bdu=pio@CtT zqOMi)XOX7w=I}n->B(&b$8A78hijiCYNa*WQcJw#|J^^3&MsOXv!Ka$b-Qma!+^fEEdgT7Uz4}87Jtjni>FQ1d?Q0#&XtLBWp zAzQ0Gu&Ndcn#kpF-NZZpmdtjO%pmPc-u6qB`lk%aYk2l(tP*9C+rXKY&fSPg7L^pd z=9ct%aTMu!Z%avCu>&R3@w*d_Tn9O?O2;20(GrirZm8gHJ!7yEos*becg9~JeL2oV z`eRw|t*rSoF=$M*_T5d*AJ1bWV+|Yb;vaGM{8Z&CAt$*FoZRwhxJ%rogh*|IXa&8= zic_8zNV{05Wl_It#jd+^E;b!Mbn4M}BHN~*hmiJN6c(zie0gCg2}$C_0y>Oih z%;Bh|pg*3nI;FVfa{|%8h`)NTTadm7TQ7f{L$|UWZ`-YGe0J*(F;P2cHkmxbU>Vq zWaRm~p)ZnKs5*MaJN|rV4Fal)xB7GAt1T}O%WwH#x>vWl1^!pQ#lsxJ0zYp5$RQmv zbj)~2w6>cR@>9fv!G18fGev)r`74w8+RT6u|59%KzfJ1K0<|vUpVR1{r;($;}F@O?U>l-51^&0 Date: Tue, 3 Dec 2024 23:21:26 +0100 Subject: [PATCH 03/49] passing basic new tests --- rowers/dataprep.py | 5 ++- rowers/tests/test_api.py | 83 ++++++++++++++++++++++++++++++------ rowers/views/workoutviews.py | 2 +- 3 files changed, 76 insertions(+), 14 deletions(-) diff --git a/rowers/dataprep.py b/rowers/dataprep.py index 8d6ed877..bfd4921a 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -1304,8 +1304,11 @@ def save_workout_database(f2, r, dosmooth=True, workouttype='rower', if makeprivate: # pragma: no cover privacy = 'hidden' - else: + elif workoutsource != 'strava': privacy = 'visible' + else: + privacy = 'hidden' + # checking for inf values diff --git a/rowers/tests/test_api.py b/rowers/tests/test_api.py index d120c8b2..aaccbaa2 100644 --- a/rowers/tests/test_api.py +++ b/rowers/tests/test_api.py @@ -22,6 +22,9 @@ from rowers.opaque import encoder from rest_framework.test import APIRequestFactory, force_authenticate +UPLOAD_SERVICE_URL = '/rowers/workout/api/upload/' +UPLOAD_SERVICE_SECRET = "FoYezZWLSyfAVimumpHEeYsJjsNCerxV" + import json # import BeautifulSoup @@ -60,11 +63,6 @@ class StravaPrivacy(TestCase): self.r.save() self.c = Client() - self.user_workouts = WorkoutFactory.create_batch(5, user=self.r) - for w in self.user_workouts: - w.workoutsource = 'strava' - w.privacy = 'hidden' - w.save() self.factory = RequestFactory() self.password = faker.word() @@ -101,6 +99,17 @@ class StravaPrivacy(TestCase): add_member(self.team.id, self.r2) add_member(self.team.id, self.r3) + self.user_workouts = WorkoutFactory.create_batch(5, user=self.r) + for w in self.user_workouts: + if w.id <= 3: + w.workoutsource = 'strava' + w.privacy = 'hidden' + else: + w.workoutsource = 'concept2' + w.privacy = 'visible' + w.team.add(self.team) + w.save() + # r2 coaches r add_coach(self.r2, self.r) @@ -178,7 +187,7 @@ class StravaPrivacy(TestCase): workouts = set([w for w in workouts if w not in [ 'upload', 'addmanual', 'c2import', 'polarimport', 'rp3import', 'nkimport', 'stravaimport', 'concept2import', 'sporttracksimport']]) - self.assertEqual(len(workouts),0) + self.assertEqual(len(workouts),2) # same test as the previous one but with self.r2 and the number of workouts found should 0 def test_list_workouts_team_coach(self): @@ -199,7 +208,7 @@ class StravaPrivacy(TestCase): workouts = set([w for w in workouts if w not in [ 'upload', 'addmanual', 'c2import', 'polarimport', 'rp3import', 'nkimport', 'stravaimport', 'concept2import', 'sporttracksimport']]) - self.assertEqual(len(workouts),0) + self.assertEqual(len(workouts),2) # same test as the previous one but with self.r3 and the number of workouts found should 0 def test_list_workouts_team_member(self): @@ -220,7 +229,7 @@ class StravaPrivacy(TestCase): workouts = set([w for w in workouts if w not in [ 'upload', 'addmanual', 'c2import', 'polarimport', 'rp3import', 'nkimport', 'stravaimport', 'concept2import', 'sporttracksimport']]) - self.assertEqual(len(workouts),0) + self.assertEqual(len(workouts),2) # now test strava import and test if the created workout has workoutsource strava and privacy hidden @patch('rowers.utils.requests.get', side_effect=mocked_requests) @@ -233,10 +242,10 @@ class StravaPrivacy(TestCase): # remove all self.workouts Workout.objects.filter(user=self.r).delete() - # get the strava data like in test_strava_import in test_imports.py - response = self.c.get('/rowers/workout/stravaimport/12', follow=True) - expected_url = reverse('workout_import_view', kwargs={'source':'strava'}) - self.assertRedirects(response, expected_url, status_code=301, target_status_code=200) + # create a workout using dataprep.new_workout_from_file with workoutsource = strava + result = get_random_file(filename='rowers/tests/testdata/thyro.csv') + workout_id, message, filename = dataprep.new_workout_from_file(self.r, result['filename'], + workoutsource='strava', makeprivate=True) # check if the workout was created ws = Workout.objects.filter(user=self.r) @@ -245,6 +254,56 @@ class StravaPrivacy(TestCase): self.assertEqual(w.workoutsource,'strava') self.assertEqual(w.privacy,'hidden') + # same as test above but makeprivate = False + @patch('rowers.utils.requests.get', side_effect=mocked_requests) + @patch('rowers.integrations.strava.requests.post', side_effect=mocked_requests) + @patch('rowers.dataprep.read_data') + def test_stravaimport_public(self, mock_get, mock_post, mocked_read_data): + login = self.c.login(username=self.u.username, password=self.password) + self.assertTrue(login) + + # remove all self.workouts + Workout.objects.filter(user=self.r).delete() + + # create a workout using dataprep.new_workout_from_file with workoutsource = strava + result = get_random_file(filename='rowers/tests/testdata/thyro.csv') + workout_id, message, filename = dataprep.new_workout_from_file(self.r, result['filename'], + workoutsource='strava', makeprivate=False) + + # check if the workout was created + ws = Workout.objects.filter(user=self.r) + self.assertEqual(len(ws),1) + w = ws[0] + self.assertEqual(w.workoutsource,'strava') + self.assertEqual(w.privacy,'hidden') + + + # test ownapi with stravaid = '122' + def test_ownapi(self): + # remove all self.workouts + Workout.objects.filter(user=self.r).delete() + + result = get_random_file(filename='rowers/tests/testdata/thyro.csv') + uploadoptions = { + 'workouttype': 'water', + 'boattype': '1x', + 'notes': 'A test file upload', + 'stravaid': '122', + 'secret': UPLOAD_SERVICE_SECRET, + 'user': self.u.id, + 'file': result['filename'], + } + url = reverse('workout_upload_api') + response = self.c.post(url, uploadoptions) + self.assertEqual(response.status_code,200) + + # check if the workout was created + ws = Workout.objects.filter(user=self.r) + self.assertEqual(len(ws),1) + w = ws[0] + self.assertEqual(w.workoutsource,'strava') + self.assertEqual(w.privacy,'hidden') + diff --git a/rowers/views/workoutviews.py b/rowers/views/workoutviews.py index eb0aed30..063e4751 100644 --- a/rowers/views/workoutviews.py +++ b/rowers/views/workoutviews.py @@ -4934,7 +4934,7 @@ def workout_upload_api(request): # only allow local host hostt = request.get_host().split(':') - if hostt[0] not in ['localhost', '127.0.0.1', 'dev.rowsandall.com', 'rowsandall.com']: + if hostt[0] not in ['localhost', '127.0.0.1', 'dev.rowsandall.com', 'rowsandall.com','testserver']: message = {'status': 'false', 'message': 'permission denied for host '+hostt[0]} return JSONResponse(status=403, data=message) From fd7e7e35e5c3577ebd81a247b2d1b84c24f999d4 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 4 Dec 2024 00:34:27 +0100 Subject: [PATCH 04/49] added a simple analysis test but should be altered --- rowers/tests/test_api.py | 93 ++++++++++++++++++++++++++++++++++- rowers/views/analysisviews.py | 4 ++ 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/rowers/tests/test_api.py b/rowers/tests/test_api.py index aaccbaa2..8563eb11 100644 --- a/rowers/tests/test_api.py +++ b/rowers/tests/test_api.py @@ -33,6 +33,7 @@ from bs4 import BeautifulSoup from rowers.ownapistuff import * from rowers.views.apiviews import * from rowers.teams import add_member, add_coach +from rowers.views.analysisviews import histodata class TeamFactory(factory.DjangoModelFactory): class Meta: @@ -303,7 +304,97 @@ class StravaPrivacy(TestCase): w = ws[0] self.assertEqual(w.workoutsource,'strava') self.assertEqual(w.privacy,'hidden') - + + + # test some analysis, should only use the workouts with workoutsource != strava + @patch('rowers.dataprep.read_data', side_effect=mocked_read_data) + def test_workouts_analysis(self, mocked_read_data): + login = self.c.login(username=self.u.username, password=self.password) + self.assertTrue(login) + + url = '/rowers/history/' + response = self.c.get(url) + self.assertEqual(response.status_code,200) + + url = '/rowers/history/data/' + response = self.c.get(url) + self.assertEqual(response.status_code,200) + # response.json() has a key "script" with a javascript script + # check if this is correct + self.assertTrue('script' in response.json()) + + # now check histogram + startdate = (self.user_workouts[0].startdatetime-datetime.timedelta(days=3)).date() + enddate = (self.user_workouts[0].startdatetime+datetime.timedelta(days=3)).date() + + # make sure the dates are not naive + try: + startdate = pytz.utc.localize(startdate) + except (ValueError, AttributeError): + pass + try: + enddate = pytz.utc.localize(enddate) + except (ValueError, AttributeError): + pass + + form_data = { + 'function':'histo', + 'xparam':'hr', + 'plotfield':'spm', + 'yparam':'pace', + 'groupby':'spm', + 'palette':'monochrome_blue', + 'xaxis':'time', + 'yaxis1':'power', + 'yaxis2':'hr', + 'startdate':startdate, + 'enddate':enddate, + 'plottype':'scatter', + 'spmmin':15, + 'spmmax':55, + 'workmin':0, + 'workmax':1500, + 'includereststrokes':False, + 'modality':'all', + 'waterboattype':['1x','2x','4x'], + 'userid':self.u.id, + 'workouts':[w.id for w in Workout.objects.filter(user=self.r)], + } + + form = AnalysisChoiceForm(form_data) + optionsform = AnalysisOptionsForm(form_data) + dateform = DateRangeForm(form_data) + + result = form.is_valid() + if not result: + print(form.errors) + + self.assertTrue(form.is_valid()) + self.assertTrue(optionsform.is_valid()) + self.assertTrue(dateform.is_valid()) + + response = self.c.post('/rowers/user-analysis-select/',form_data) + + self.assertEqual(response.status_code,200) + + # get data from histodata function + ws = Workout.objects.filter(user=self.r) + + script, div = histodata(ws,form_data) + # script has a line starting with 'data = [ ... ]' + # we need to get that line + data = [line for line in script.split('\n') if line.startswith('data = [')][0] + # the line should be a list of float values + self.assertTrue(data.startswith('data = [')) + self.assertTrue(data.endswith(']')) + # count the number of commas between the brackets + self.assertEqual(data.count(','),1377) + + + + + + # try and fail to submit a workout with workoutsource = strava to a challenge diff --git a/rowers/views/analysisviews.py b/rowers/views/analysisviews.py index 1938df26..b3368972 100644 --- a/rowers/views/analysisviews.py +++ b/rowers/views/analysisviews.py @@ -2276,6 +2276,8 @@ def history_view_data(request, userid=0): ddf = ddf.with_columns(pl.col("time").diff().clip(lower_bound=0).alias("deltat")) except KeyError: # pragma: no cover pass + except ColumnNotFoundError: + pass ddf = dataprep.clean_df_stats_pl(ddf, workstrokesonly=False, ignoreadvanced=True) @@ -2288,6 +2290,8 @@ def history_view_data(request, userid=0): ddict['hrmax'] = int(ddf['hr'].max()) except (KeyError, ValueError, AttributeError): # pragma: no cover ddict['hrmax'] = 0 + except ColumnNotFoundError: + ddict['hrmax'] = 0 ddict['powermean'] = int(wavg(ddf, 'power', 'deltat')) try: From 1daed901040b752592370bfde2fb236fbbed4f64 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 4 Dec 2024 21:02:58 +0100 Subject: [PATCH 05/49] excluding strava from analysis --- rowers/dataprep.py | 3 ++ rowers/interactiveplots.py | 2 +- rowers/models.py | 24 +++++++++++- rowers/rower_rules.py | 10 +++++ rowers/tests/test_api.py | 72 +++++++++++++++++++++++++++++++++-- rowers/views/analysisviews.py | 34 ++++++++++++----- rowers/views/statements.py | 1 + rowers/views/workoutviews.py | 17 +++++---- 8 files changed, 138 insertions(+), 25 deletions(-) diff --git a/rowers/dataprep.py b/rowers/dataprep.py index bfd4921a..2e1ea17e 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -217,6 +217,9 @@ def workout_goldmedalstandard(workout, reset=False): def check_marker(workout): r = workout.user + if workout.workoutsource == 'strava': + return None + gmstandard, gmseconds = workout_goldmedalstandard(workout) if gmseconds < 60: return None diff --git a/rowers/interactiveplots.py b/rowers/interactiveplots.py index 4e4d4c74..2f0d2a5d 100644 --- a/rowers/interactiveplots.py +++ b/rowers/interactiveplots.py @@ -550,7 +550,7 @@ def goldmedalscorechart(user, startdate=None, enddate=None): workouts = Workout.objects.filter(user=user.rower, date__gte=startdate, date__lte=enddate, workouttype__in=mytypes.rowtypes, - duplicate=False).order_by('date') + duplicate=False).order_by('date').exclude(workoutsource='strava') markerworkouts = workouts.filter(rankingpiece=True) outids = [w.id for w in markerworkouts] diff --git a/rowers/models.py b/rowers/models.py index 5ddc51dc..7c196450 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -1423,9 +1423,26 @@ parchoicesy1 = list(sorted(favchartlabelsy1.items(), key=lambda x: x[1])) parchoicesy2 = list(sorted(favchartlabelsy2.items(), key=lambda x: x[1])) parchoicesx = list(sorted(favchartlabelsx.items(), key=lambda x: x[1])) +# special filter for workouts to exclude strava workouts by default +class WorkoutQuerySet(models.QuerySet): + def filter(self, *args, exclude_strava=True, **kwargs): + queryset = super().filter(*args, **kwargs) + if exclude_strava: + queryset = queryset.exclude(workoutsource='strava') + + return queryset + + def get(self, *args, **kwargs): + queryset = self + + return super().get(*args, **kwargs) + + +class WorkoutManager(models.Manager): + def get_queryset(self): + return WorkoutQuerySet(self.model, using=self._db) + # Saving a chart as a favorite chart - - class FavoriteChart(models.Model): workouttypechoices = [ ('ote', 'Erg/SkiErg'), @@ -3704,6 +3721,9 @@ class Workout(models.Model): default=False, verbose_name='Duplicate Workout') impeller = models.BooleanField(default=False, verbose_name='Impeller') + # attach the WorkoutManager + #objects = WorkoutManager() + def url(self): str = '/rowers/workout/{id}/'.format( id=encoder.encode_hex(self.id) diff --git a/rowers/rower_rules.py b/rowers/rower_rules.py index 05a16994..ed5cd2dd 100644 --- a/rowers/rower_rules.py +++ b/rowers/rower_rules.py @@ -451,6 +451,11 @@ def is_workout_user(user, workout): except AttributeError: # pragma: no cover return False + if workout.privacy == 'hidden': + return user == workout.user.user + if workout.workoutsource == 'strava': + return user == workout.user.user + if workout.user == r: return True @@ -469,6 +474,11 @@ def is_workout_team(user, workout): except AttributeError: # pragma: no cover return False + if workout.privacy == 'hidden': + return user == workout.user.user + if workout.workoutsource == 'strava': + return user == workout.user.user + if workout.user == r: return True diff --git a/rowers/tests/test_api.py b/rowers/tests/test_api.py index 8563eb11..8bd91a55 100644 --- a/rowers/tests/test_api.py +++ b/rowers/tests/test_api.py @@ -102,13 +102,17 @@ class StravaPrivacy(TestCase): self.user_workouts = WorkoutFactory.create_batch(5, user=self.r) for w in self.user_workouts: - if w.id <= 3: + if w.id <= 2: w.workoutsource = 'strava' w.privacy = 'hidden' + elif w.id == 3: # user can change privacy but cannot change workoutsource + w.workoutsource = 'strava' + w.privacy = 'visible' else: w.workoutsource = 'concept2' w.privacy = 'visible' w.team.add(self.team) + w.csvfilename = get_random_file(filename='rowers/tests/testdata/thyro.csv')['filename'] w.save() # r2 coaches r @@ -116,6 +120,13 @@ class StravaPrivacy(TestCase): self.factory = APIRequestFactory() + def tearDown(self): + for workout in self.user_workouts: + try: + os.remove(workout.csvfilename) + except (OSError, FileNotFoundError, IOError): + pass + # Test if workout with workoutsource strava and privacy hidden can be seen by coach def test_privacy_coach(self): login = self.c.login(username=self.u2.username, password=self.password2) @@ -126,6 +137,16 @@ class StravaPrivacy(TestCase): response = self.c.get(url) self.assertEqual(response.status_code,403) + # Same test as above but for 'workout_edit_view' + def test_privacy_coach_edit(self): + login = self.c.login(username=self.u2.username, password=self.password2) + self.assertTrue(login) + + w = self.user_workouts[0] + url = reverse('workout_edit_view',kwargs={'id':encoder.encode_hex(w.id)}) + response = self.c.get(url) + self.assertEqual(response.status_code,403) + # Test if workout with workoutsource strava and privacy hidden can be seen by team member def test_privacy_member(self): login = self.c.login(username=self.u3.username, password=self.password3) @@ -136,6 +157,16 @@ class StravaPrivacy(TestCase): response = self.c.get(url) self.assertEqual(response.status_code,403) + # Same test as above but for 'workout_edit_view' + def test_privacy_member_edit(self): + login = self.c.login(username=self.u3.username, password=self.password3) + self.assertTrue(login) + + w = self.user_workouts[0] + url = reverse('workout_edit_view',kwargs={'id':encoder.encode_hex(w.id)}) + response = self.c.get(url) + self.assertEqual(response.status_code,403) + # same test as above but with user r and the response code should be 200 def test_privacy_owner(self): login = self.c.login(username=self.u.username, password=self.password) @@ -146,6 +177,16 @@ class StravaPrivacy(TestCase): response = self.c.get(url) self.assertEqual(response.status_code,200) + # same test as above but for 'workout_edit_view' + def test_privacy_owner_edit(self): + login = self.c.login(username=self.u.username, password=self.password) + self.assertTrue(login) + + w = self.user_workouts[0] + url = reverse('workout_edit_view',kwargs={'id':encoder.encode_hex(w.id)}) + response = self.c.get(url) + self.assertEqual(response.status_code,200) + # test if list_workouts returns all workouts for user r @@ -168,6 +209,7 @@ class StravaPrivacy(TestCase): 'upload', 'addmanual', 'c2import', 'polarimport', 'rp3import', 'nkimport', 'stravaimport', 'concept2import', 'sporttracksimport']]) self.assertEqual(len(workouts),5) + # same test as above but list_workouts with team id = self.team.id def test_list_workouts_team(self): @@ -211,6 +253,27 @@ class StravaPrivacy(TestCase): self.assertEqual(len(workouts),2) + # same test as above but with without the teamid kwarg but with a rowerid=self.r.id + def test_list_workouts_team_coach2(self): + login = self.c.login(username=self.u2.username, password=self.password2) + self.assertTrue(login) + + url = reverse('workouts_view',kwargs={'rowerid':self.r.id}) + response = self.c.get(url) + self.assertEqual(response.status_code,200) + + # the response.content is html, so we need to parse it + soup = BeautifulSoup(response.content, 'html.parser') + # the workouts look like ... and there should be 5 unique ids + # the id is a hex string + workouts = set([a['href'].split('/')[3] for a in soup.find_all('a') if a['href'].startswith('/rowers/workout/')]) + + # throw out "c2import", "nkimport", "stravaimport", "concept2import", "sporttracksimport" from the set + workouts = set([w for w in workouts if w not in [ + 'upload', 'addmanual', 'c2import', 'polarimport', 'rp3import', 'nkimport', 'stravaimport', 'concept2import', 'sporttracksimport']]) + + self.assertEqual(len(workouts),2) + # same test as the previous one but with self.r3 and the number of workouts found should 0 def test_list_workouts_team_member(self): login = self.c.login(username=self.u3.username, password=self.password3) @@ -307,8 +370,9 @@ class StravaPrivacy(TestCase): # test some analysis, should only use the workouts with workoutsource != strava - @patch('rowers.dataprep.read_data', side_effect=mocked_read_data) - def test_workouts_analysis(self, mocked_read_data): + #@patch('rowers.dataprep.read_data', side_effect=mocked_read_data) + #def test_workouts_analysis(self, mocked_read_data): + def test_workouts_analysis(self): login = self.c.login(username=self.u.username, password=self.password) self.assertTrue(login) @@ -388,7 +452,7 @@ class StravaPrivacy(TestCase): self.assertTrue(data.startswith('data = [')) self.assertTrue(data.endswith(']')) # count the number of commas between the brackets - self.assertEqual(data.count(','),1377) + self.assertEqual(data.count(','),2062) diff --git a/rowers/views/analysisviews.py b/rowers/views/analysisviews.py index b3368972..c11ef6c0 100644 --- a/rowers/views/analysisviews.py +++ b/rowers/views/analysisviews.py @@ -199,14 +199,14 @@ def analysis_new(request, startdatetime__lte=enddate, workouttype__in=modalities, rankingpiece__in=rankingtypes, - ) + ).exclude(workoutsource='strava') elif theteam is not None and theteam.viewing == 'coachonly': # pragma: no cover workouts = Workout.objects.filter(team=theteam, user=r, startdatetime__gte=startdate, startdatetime__lte=enddate, workouttype__in=modalities, rankingpiece__in=rankingtypes, - ) + ).exclude(workoutsource='strava') elif thesession is not None: workouts = get_workouts_session(r, thesession) else: @@ -363,6 +363,7 @@ def trendflexdata(workouts, options, userid=0): savedata = options.get('savedata',False) + workouts = workouts.exclude(workoutsource='strava') fieldlist, fielddict = dataprep.getstatsfields() fieldlist = [xparam, yparam, groupby, @@ -566,6 +567,8 @@ def flexalldata(workouts, options): trendline = options['trendline'] promember = True + workouts = workouts.exclude(workoutsource='strava') + workstrokesonly = not includereststrokes userid = options['userid'] @@ -612,6 +615,9 @@ def histodata(workouts, options): workmax = options['workmax'] userid = options['userid'] + workouts = workouts.exclude(workoutsource='strava') + + if userid == 0: # pragma: no cover extratitle = '' else: @@ -645,7 +651,8 @@ def cpdata(workouts, options): u = User.objects.get(id=userid) r = u.rower - + + delta, cpvalue, avgpower, workoutnames, urls = dataprep.fetchcp_new( r, workouts) @@ -798,6 +805,8 @@ def cpdata(workouts, options): def statsdata(workouts, options): + workouts = workouts.exclude(workoutsource='strava') + includereststrokes = options['includereststrokes'] ids = options['ids'] @@ -872,12 +881,13 @@ def statsdata(workouts, options): def comparisondata(workouts, options): + workouts = workouts.exclude(workoutsource='strava') includereststrokes = options['includereststrokes'] xparam = options['xaxis'] yparam1 = options['yaxis1'] plottype = options['plottype'] promember = True - + workstrokesonly = not includereststrokes ids = [w.id for w in workouts] @@ -915,6 +925,7 @@ def comparisondata(workouts, options): def boxplotdata(workouts, options): + workouts = workouts.exclude(workoutsource='strava') includereststrokes = options['includereststrokes'] spmmin = options['spmmin'] @@ -926,7 +937,7 @@ def boxplotdata(workouts, options): plotfield = options['plotfield'] workstrokesonly = not includereststrokes - + datemapping = { w.id: w.date for w in workouts } @@ -1020,11 +1031,14 @@ def analysis_view_data(request, userid=0): for id in ids: try: - workouts.append(Workout.objects.get(id=id)) + w = Workout.objects.get(id=id) + if w.workoutsource != 'strava': + workouts.append(w) except Workout.DoesNotExist: # pragma: no cover pass + if function == 'boxplot': script, div = boxplotdata(workouts, options) elif function == 'trendflex': # pragma: no cover @@ -1069,7 +1083,7 @@ def create_marker_workouts_view(request, userid=0, workouts = Workout.objects.filter(user=theuser.rower, date__gte=startdate, date__lte=enddate, workouttype__in=mytypes.rowtypes, - duplicate=False).order_by('date') + duplicate=False).order_by('date').exclude(workoutsource='strava') for workout in workouts: _ = dataprep.check_marker(workout) @@ -1113,7 +1127,7 @@ def goldmedalscores_view(request, userid=0, theuser, startdate=startdate, enddate=enddate, ) - bestworkouts = Workout.objects.filter(id__in=ids).order_by('-date') + bestworkouts = Workout.objects.filter(id__in=ids).order_by('-date').exclude(workoutsource='strava') breadcrumbs = [ { @@ -1311,7 +1325,7 @@ def performancemanager_view(request, userid=0, mode='rower', user = therower, date__gte=startdate-datetime.timedelta(days=90), date__lte=enddate, duplicate=False, - rankingpiece=True, workouttype__in=mytypes.rowtypes).order_by('date') + rankingpiece=True, workouttype__in=mytypes.rowtypes).order_by('date').exclude(workoutsource='strava') ids = [w.id for w in markerworkouts] form = PerformanceManagerForm(initial={ @@ -1323,7 +1337,7 @@ def performancemanager_view(request, userid=0, mode='rower', ids = pd.Series(ids, dtype='int').dropna().values - bestworkouts = Workout.objects.filter(id__in=ids).order_by('-date') + bestworkouts = Workout.objects.filter(id__in=ids).order_by('-date').exclude(workoutsource='strava') breadcrumbs = [ { diff --git a/rowers/views/statements.py b/rowers/views/statements.py index d134013d..eeb04039 100644 --- a/rowers/views/statements.py +++ b/rowers/views/statements.py @@ -28,6 +28,7 @@ from rest_framework.response import Response from rq.job import Job from rules.contrib.views import permission_required, objectgetter from django.core.cache import cache +from django.db import models from django.utils.crypto import get_random_string from rq.registry import StartedJobRegistry from rq.exceptions import NoSuchJobError diff --git a/rowers/views/workoutviews.py b/rowers/views/workoutviews.py index 063e4751..67a655af 100644 --- a/rowers/views/workoutviews.py +++ b/rowers/views/workoutviews.py @@ -2204,25 +2204,25 @@ def workouts_view(request, message='', successmessage='', team=theteam, startdatetime__gte=startdate, startdatetime__lte=enddate, - privacy='visible').order_by("-date", "-starttime") + privacy='visible').order_by("-date", "-starttime").exclude(workoutsource='strava') g_workouts = Workout.objects.filter( team=theteam, startdatetime__gte=activity_startdate, startdatetime__lte=activity_enddate, duplicate=False, - privacy='visible').order_by("-date", "-starttime") + privacy='visible').order_by("-date", "-starttime").exclude(workoutsource='strava') elif theteam.viewing == 'coachonly': # pragma: no cover workouts = Workout.objects.filter( team=theteam, user=r, startdatetime__gte=startdate, startdatetime__lte=enddate, - privacy='visible').order_by("-startdatetime") + privacy='visible').order_by("-startdatetime").exclude(workoutsource='strava') g_workouts = Workout.objects.filter( team=theteam, user=r, startdatetime__gte=activity_startdate, startdatetime__lte=activity_enddate, duplicate=False, - privacy='visible').order_by("-startdatetime") + privacy='visible').order_by("-startdatetime").exclude(workoutsource='strava') elif request.user != r.user: theteam = None @@ -2230,13 +2230,13 @@ def workouts_view(request, message='', successmessage='', user=r, startdatetime__gte=startdate, startdatetime__lte=enddate, - privacy='visible').order_by("-date", "-starttime") + privacy='visible').order_by("-date", "-starttime").exclude(workoutsource='strava') g_workouts = Workout.objects.filter( user=r, startdatetime__gte=activity_startdate, startdatetime__lte=activity_enddate, duplicate=False, - privacy='visible').order_by("-startdatetime") + privacy='visible').order_by("-startdatetime").exclude(workoutsource='strava') else: theteam = None workouts = Workout.objects.filter( @@ -2252,7 +2252,7 @@ def workouts_view(request, message='', successmessage='', if g_workouts.count() == 0: g_workouts = Workout.objects.filter( user=r, - startdatetime__gte=timezone.now()-timedelta(days=15)).order_by("-startdatetime") + startdatetime__gte=timezone.now()-timedelta(days=15)).order_by("-startdatetime").exclude(workoutsource='strava') g_enddate = timezone.now() g_startdate = (timezone.now()-timedelta(days=15)) @@ -2266,7 +2266,8 @@ def workouts_view(request, message='', successmessage='', reduce(operator.and_, (Q(name__icontains=q) for q in query_list)) | reduce(operator.and_, - (Q(notes__icontains=q) for q in query_list)) + (Q(notes__icontains=q) for q in query_list)), + exclude_strava=False, ) searchform = SearchForm(initial={'q': query}) else: From 13effe6cce58a9d87bea0d31c6181922edc5e708 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 4 Dec 2024 21:19:38 +0100 Subject: [PATCH 06/49] getting all tests to pass, improved developers page --- rowers/templates/developers.html | 19 ++++++++-------- rowers/tests/test_api.py | 8 +++++++ rowers/tests/testdata/testdata.tcx.gz | Bin 3998 -> 4000 bytes rowers/views/analysisviews.py | 31 +++++++++++++++++++++----- 4 files changed, 42 insertions(+), 16 deletions(-) diff --git a/rowers/templates/developers.html b/rowers/templates/developers.html index ff0f01d0..3d966f82 100644 --- a/rowers/templates/developers.html +++ b/rowers/templates/developers.html @@ -8,7 +8,7 @@
  • -

    On this page, a work in progress, I will collect useful information +

    On this page, I will collect useful information for developers of rowing data apps and hardware.

    I presume you have an app (smartphone app, dedicated hardware, web site) @@ -61,11 +61,11 @@

Using the REST API

-

We are building a REST API which will allow you to post and +

We have a REST API which will allow you to post and receive stroke data from the site directly.

-

The REST API is a work in progress. We are open to improvement +

We are open to improvement suggestions (provided they don't break existing apps). Please send email to info@rowsandall.com with questions and/or suggestions. We @@ -84,7 +84,6 @@

  • Disadvantages

      -
    • The API is not stable and not fully tested yet.
    • You need to register your app with us. We can revoke your permissions if you misuse them.
    • The user user must grant permissions to your app.
    • @@ -114,7 +113,7 @@

      We have disabled the self service app link for security reasons. - We will replace it with a secure self service app link soon. If you + If you need to register an app, please send email to info@rowsandall.com

      Authentication

      @@ -728,11 +727,11 @@
    • peakdriveforce: Peak handle force (lbs)
    • lapidx: Lap identifier
    • hr: Heart rate (beats per minute)
    • -
    • wash: Wash as defined per Empower oarlock (degrees)
    • -
    • catch: Catch angle per Empower oarlock (degrees)
    • -
    • finish: Finish angle per Empower oarlock (degrees)
    • -
    • peakforceangle: Peak Force Angle per Empower oarlock (degrees)
    • -
    • slip: Slip as defined per Empower oarlock (degrees)
    • +
    • wash: Wash as defined for your smart power measuring oarlock (degrees)
    • +
    • catch: Catch angle for your smart power measuring oarlock (degrees)
    • +
    • finish: Finish angle for your smart power measuring oarlock (degrees)
    • +
    • peakforceangle: Peak Force Angle for your smart power measuring oarlock (degrees)
    • +
    • slip: Slip as defined for your smart power measuring oarlock (degrees)

    diff --git a/rowers/tests/test_api.py b/rowers/tests/test_api.py index 8bd91a55..649bca2f 100644 --- a/rowers/tests/test_api.py +++ b/rowers/tests/test_api.py @@ -471,6 +471,14 @@ class OwnApi(TestCase): gdproptin=True, ftpset=True,surveydone=True, gdproptindate=timezone.now(), rowerplan='pro',subscription_id=1) + self.c = Client() + self.user_workouts = WorkoutFactory.create_batch(5, user=self.r) + self.factory = RequestFactory() + self.password = faker.word() + self.u.set_password(self.password) + self.u.save() + + self.factory = APIRequestFactory() def test_strokedataform(self): diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz index a12f9e93dea8c9787d530429a5d32f8dea26f3f4..c460bd6ffea839a3141ab5d40fe6491413adb205 100644 GIT binary patch literal 4000 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 GfB^tb+&muu literal 3998 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 Date: Thu, 5 Dec 2024 21:05:42 +0100 Subject: [PATCH 07/49] more or less done --- rowers/plannedsessions.py | 41 ++++++++++++ rowers/rower_rules.py | 3 + rowers/tests/test_api.py | 13 ++-- rowers/tests/test_races.py | 88 ++++++++++++++++++++++++++ rowers/tests/testdata/testdata.tcx.gz | Bin 4000 -> 4000 bytes rowers/views/analysisviews.py | 6 +- rowers/views/racesviews.py | 12 +++- rowers/views/statements.py | 3 +- 8 files changed, 154 insertions(+), 12 deletions(-) diff --git a/rowers/plannedsessions.py b/rowers/plannedsessions.py index d9e6add5..1918d750 100644 --- a/rowers/plannedsessions.py +++ b/rowers/plannedsessions.py @@ -1597,13 +1597,28 @@ def add_workout_fastestrace(ws, race, r, recordid=0, doregister=False): enddatetime ) + # from ws, remove any w where w.workoutsource = 'strava'. For each removal add an error "strava workout not permitted" to the errors list and if there are no workouts left, return 0, comments, errors, 0 + ws2 = [] + for w in ws: + if w.workoutsource != 'strava': + ws2.append(w) + else: + errors.append('Strava workouts are not permitted') + + ws = ws2 + + if len(ws) == 0: + return result, comments, errors, 0 + ids = [w.id for w in ws] ids = list(set(ids)) + if len(ids) > 1 and race.sessiontype in ['test', 'coursetest', 'race', 'indoorrace', 'fastest_time', 'fastest_distance']: # pragma: no cover errors.append('For tests, you can only attach one workout') return result, comments, errors, 0 + if r.birthdate: age = calculate_age(r.birthdate) else: # pragma: no cover @@ -1759,6 +1774,19 @@ def add_workout_indoorrace(ws, race, r, recordid=0, doregister=False): enddatetime ) + # from ws, remove any w where w.workoutsource = 'strava'. For each removal add an error "strava workout not permitted" to the errors list and if there are no workouts left, return 0, comments, errors, 0 + ws2 = [] + for w in ws: + if w.workoutsource != 'strava': + ws2.append(w) + else: + errors.append('Strava workouts are not permitted') + + ws = ws2 + + if len(ws) == 0: + return result, comments, errors, 0 + # check if all sessions have same date dates = [w.date for w in ws] if (not all(d == dates[0] for d in dates)) and race.sessiontype not in ['challenge', 'cycletarget']: # pragma: no cover @@ -1906,6 +1934,19 @@ def add_workout_race(ws, race, r, splitsecond=0, recordid=0, doregister=False): enddatetime ) + # from ws, remove any w where w.workoutsource = 'strava'. For each removal add an error "strava workout not permitted" to the errors list and if there are no workouts left, return 0, comments, errors, 0 + ws2 = [] + for w in ws: + if w.workoutsource != 'strava': + ws2.append(w) + else: + errors.append('Strava workouts are not permitted') + + ws = ws2 + + if len(ws) == 0: + return result, comments, errors, 0 + # check if all sessions have same date dates = [w.date for w in ws] if (not all(d == dates[0] for d in dates)) and race.sessiontype not in ['challenge', 'cycletarget']: # pragma: no cover diff --git a/rowers/rower_rules.py b/rowers/rower_rules.py index ed5cd2dd..68529023 100644 --- a/rowers/rower_rules.py +++ b/rowers/rower_rules.py @@ -463,6 +463,9 @@ def is_workout_user(user, workout): # check if user is in same team as owner of workout +@rules.predicate +def workout_is_strava(workout): + return workout.workoutsource == 'strava' @rules.predicate def is_workout_team(user, workout): diff --git a/rowers/tests/test_api.py b/rowers/tests/test_api.py index 649bca2f..83b2ba82 100644 --- a/rowers/tests/test_api.py +++ b/rowers/tests/test_api.py @@ -441,6 +441,12 @@ class StravaPrivacy(TestCase): self.assertEqual(response.status_code,200) + # count number of workouts by counting the number of occurences of '
  • SportTracks
  • Polar
  • RP3
  • +
  • Intervals.icu
  • diff --git a/rowers/templates/rower_exportsettings.html b/rowers/templates/rower_exportsettings.html index 4990ce37..866a771a 100644 --- a/rowers/templates/rower_exportsettings.html +++ b/rowers/templates/rower_exportsettings.html @@ -123,6 +123,8 @@ alt="connect with RP3" width="130">

    connect with Rojabo

    +

    connect with intervals.icu

    {% endblock %} diff --git a/rowers/views/importviews.py b/rowers/views/importviews.py index f5270e17..486155b1 100644 --- a/rowers/views/importviews.py +++ b/rowers/views/importviews.py @@ -24,6 +24,7 @@ importauthorizeviews = { 'nk': 'rower_integration_authorize', 'rp3': 'rower_integration_authorize', 'garmin': 'rower_garmin_authorize', + 'intervals': 'rower_integration_authorize', } @@ -173,6 +174,37 @@ def rower_process_twittercallback(request): # pragma: no cover # Process Polar Callback +@login_required() +def rower_process_intervalscallback(request): + integration = importsources['intervals'](request.user) + r = getrower(request.user) + try: + code = request.GET['code'] + res = integration.get_token(code) + except MultiValueDictKeyError: + message = "The resource owner or authorization server denied the request" + messages.error(request, message) + + url = reverse('rower_exportsettings_view') + return HttpResponseRedirect(url) + + access_token = res[0] + athlete = res[1] + if access_token == 0: + message = res[1] + message += 'Connection to intervals.icu failed.' + messages.error(request, message) + url = reverse('rower_exportsettings_view') + return HttpResponseRedirect(url) + + r.intervals_token = access_token + r.intervals_owner_id = athlete['id'] + r.save() + + successmessage = "Tokens stored. Good to go. Please check your import/export settings" + messages.info(request, successmessage) + url = reverse('rower_exportsettings_view') + return HttpResponseRedirect(url) @login_required() def rower_process_polarcallback(request): diff --git a/rowsandall_app/urls.py b/rowsandall_app/urls.py index caf20824..67c26a3d 100644 --- a/rowsandall_app/urls.py +++ b/rowsandall_app/urls.py @@ -94,6 +94,7 @@ urlpatterns += [ re_path(r'^rp3\_callback', rowersviews.rower_process_rp3callback), re_path(r'^twitter\_callback', rowersviews.rower_process_twittercallback), re_path(r'^idoklad\_callback', rowersviews.process_idokladcallback), + re_path(r'^intervals\_icu\_callback', rowersviews.rower_process_intervalscallback), re_path(r'^i18n/', include('django.conf.urls.i18n')), re_path(r'^tz_detect/', include('tz_detect.urls')), re_path(r'^logo/', logoview), diff --git a/static/img/intervals_icu.png b/static/img/intervals_icu.png new file mode 100644 index 0000000000000000000000000000000000000000..a333c8b83e7bcfe8daba0691f4ec10014688b358 GIT binary patch literal 3776 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 Date: Mon, 9 Dec 2024 20:20:32 +0100 Subject: [PATCH 15/49] more changes --- rowers/dataroutines.py | 1 + rowers/models.py | 8 +++++-- rowers/mytypes.py | 38 ++++++++++++++++++++++++++++++++++ rowers/tasks.py | 8 +++---- rowers/uploads.py | 47 +++++++++++++++++++++++++++++++++++------- 5 files changed, 88 insertions(+), 14 deletions(-) diff --git a/rowers/dataroutines.py b/rowers/dataroutines.py index d5f1e168..61282da7 100644 --- a/rowers/dataroutines.py +++ b/rowers/dataroutines.py @@ -1367,6 +1367,7 @@ def get_workouttype_from_fit(filename, workouttype='water'): return 'water' try: workouttype = mytypes.fitmappinginv[fittype] + return workouttype except KeyError: # pragma: no cover return workouttype diff --git a/rowers/models.py b/rowers/models.py index 2075d2cf..610cd019 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -372,7 +372,7 @@ def update_records(url=c2url, verbose=True): # Create a DataFrame df = pd.DataFrame(rows, columns=headers) - except: # pragma: no cover + except: # pragma: no cover df = pd.DataFrame() if not df.empty: @@ -1172,6 +1172,8 @@ class Rower(models.Model): default='', max_length=200, blank=True, null=True) c2_auto_export = models.BooleanField(default=False) c2_auto_import = models.BooleanField(default=False) + intervals_auto_export = models.BooleanField(default=False) + intervals_auto_import = models.BooleanField(default=False) sporttrackstoken = models.CharField( default='', max_length=200, blank=True, null=True) sporttrackstokenexpirydate = models.DateTimeField(blank=True, null=True) @@ -3827,6 +3829,7 @@ class TombStone(models.Model): uploadedtosporttracks = models.BigIntegerField(default=0) uploadedtotp = models.BigIntegerField(default=0) uploadedtonk = models.BigIntegerField(default=0) + uploadedtointervals = models.BigIntegerField(default=0) @receiver(models.signals.pre_delete, sender=Workout) def create_tombstone_on_delete(sender, instance, **kwargs): @@ -3835,7 +3838,8 @@ def create_tombstone_on_delete(sender, instance, **kwargs): uploadedtoc2=instance.uploadedtoc2, uploadedtostrava=instance.uploadedtostrava, uploadedtotp=instance.uploadedtotp, - uploadedtonk=instance.uploadedtonk + uploadedtonk=instance.uploadedtonk, + uploadedtointervals=instance.uploadedtointervals, ) t.save() diff --git a/rowers/mytypes.py b/rowers/mytypes.py index afc90c8b..b0586308 100644 --- a/rowers/mytypes.py +++ b/rowers/mytypes.py @@ -180,6 +180,41 @@ fitcollection = ( fitmapping = {key: value for key, value in Reverse(fitcollection)} + + +intervalscollection = ( + ('water', 'Rowing'), + ('rower', 'VirtualRow'), + ('skierg', 'NordicSki'), + ('bike', 'Ride'), + ('bikeerg', 'VirtualRide'), + ('dynamic', 'Rowing'), + ('slides', 'Rowing'), + ('paddle', 'StandUpPaddling'), + ('snow', 'NordicSki'), + ('coastal', 'Rowing'), + ('c-boat', 'Rowing'), + ('churchboat', 'Rowing'), + ('Ride', 'Ride'), + ('Run', 'Run'), + ('NordicSki', 'NordicSki'), + ('Swim', 'Swim'), + ('Hike', 'Hike'), + ('Walk', 'Walk'), + ('Canoeing', 'Canoeing'), + ('Crossfit', 'Crossfit'), + ('StandUpPaddling', 'StandUpPaddling'), + ('IceSkate', 'IceSkate'), + ('WeightTraining', 'WeightTraining'), + ('InlineSkate', 'InlineSkate'), + ('Kayaking', 'Kayaking'), + ('Workout', 'Workout'), + ('Yoga', 'Yoga'), + ('other', 'Other'), +) + +intervalsmapping = {key: value for key, value in Reverse(intervalscollection)} + stcollection = ( ('water', 'Rowing'), ('rower', 'Rowing'), @@ -332,6 +367,9 @@ garminmappinginv = {value: key for key, value in Reverse( fitmappinginv = {value: key for key, value in Reverse( fitcollection) if value is not None} +intervalsmappinginv = {value: key for key, value in Reverse( + intervalscollection) if value is not None} + otwtypes = ( 'water', 'coastal', diff --git a/rowers/tasks.py b/rowers/tasks.py index 6fc8793a..36999b19 100644 --- a/rowers/tasks.py +++ b/rowers/tasks.py @@ -24,7 +24,7 @@ from rowers.courseutils import ( InvalidTrajectoryError ) from rowers.emails import send_template_email -from rowers.mytypes import fitmappinginv +from rowers.mytypes import intervalsmappinginv from rowers.nkimportutils import ( get_nk_summary, get_nk_allstats, get_nk_intervalstats, getdict, strokeDataToDf, add_workout_from_data @@ -3508,11 +3508,11 @@ def handle_intervals_getworkout(rower, intervalstoken, workoutid, debug=False, * title = 'Intervals workout' try: - workouttype = fitmappinginv[data['type']] - print(data['type']) + workouttype = intervalsmappinginv[data['type']] except KeyError: workouttype = 'water' + url = "https://intervals.icu/api/v1/activity/{workoutid}/fit-file".format(workoutid=workoutid) response = requests.get(url, headers=headers) @@ -3535,7 +3535,6 @@ def handle_intervals_getworkout(rower, intervalstoken, workoutid, debug=False, * duration = totaltime_sec_to_string(rowdata.duration) distance = rowdata.df[" Horizontal (meters)"].iloc[-1] except Exception as e: - print(e) return 0 uploadoptions = { @@ -3544,6 +3543,7 @@ def handle_intervals_getworkout(rower, intervalstoken, workoutid, debug=False, * 'boattype': '1x', 'workouttype': workouttype, 'file': fit_filename, + 'intervalsid': intervalsid, 'title': title, 'rpe': 0, 'notes': '', diff --git a/rowers/uploads.py b/rowers/uploads.py index 781271f7..b13881a9 100644 --- a/rowers/uploads.py +++ b/rowers/uploads.py @@ -146,6 +146,22 @@ def do_sync(w, options, quick=False): except KeyError: pass + do_icu_export = w.user.intervals_auto_export + try: + do_icu_export = options['upload_to_Intervals'] or do_icu_export + except KeyError: + pass + + try: + if options['intervalsid'] != 0 and options['intervalsid'] != '': # pragma: no cover + w.uploadedtointervals = options['intervalsid'] + # upload_to_icu = False + do_icu_export = False + w.save() + record = create_or_update_syncrecord(w.user, w, intervalsid=options['intervalsid']) + except KeyError: + pass + try: if options['nkid'] != 0 and options['nkid'] != '': # pragma: no cover w.uploadedtonk = options['nkid'] @@ -232,14 +248,29 @@ def do_sync(w, options, quick=False): except NoTokenError: # pragma: no cover id = 0 message = "Please connect to Strava first" - except: - e = sys.exc_info()[0] - t = time.localtime() - timestamp = time.strftime('%b-%d-%Y_%H%M', t) - with open('stravalog.log', 'a') as f: - f.write('\n') - f.write(timestamp) - f.write(str(e)) + except Exception as e: + dologging('stravalog.log', e) + + if do_icu_export: + intervals_integration = IntervalsIntegration(w.user.user) + try: + id = intervals_integration.workout_export(w) + dologging( + 'intervals.icu.log', + 'exporting workout {id} as {type}'.format( + id=w.id, + type=w.workouttype, + ) + ) + except NoTokenError: + id = 0 + message = "Please connect to Intervals.icu first" + except Exception as e: + dologging( + 'intervals.icu.log', + e + ) + do_st_export = w.user.sporttracks_auto_export From 2b9e2c1dfd3cf2c571e8740970953d1fb64d2636 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Mon, 9 Dec 2024 20:46:10 +0100 Subject: [PATCH 16/49] fixes for RunGap --- rowers/tests/testdata/testdata.tcx.gz | Bin 4001 -> 4002 bytes rowers/views/apiviews.py | 8 +++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz index e4b282767cbb0a0b00a3d19988f22f0eb76fbacd..1c63fdbf311e81cba18bddbb4e55981700a69cb1 100644 GIT binary patch literal 4002 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 Ib;N)H0QPu4hX4Qo literal 4001 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 Date: Mon, 9 Dec 2024 21:53:34 +0100 Subject: [PATCH 17/49] import now behaving --- rowers/dataprep.py | 7 ++++ rowers/dataroutines.py | 57 ++++++++++++++++++++++++++++---- rowers/integrations/intervals.py | 17 ++++++++-- rowers/models.py | 6 ++-- rowers/mytypes.py | 1 + rowers/tasks.py | 2 +- rowers/views/importviews.py | 5 ++- 7 files changed, 80 insertions(+), 15 deletions(-) diff --git a/rowers/dataprep.py b/rowers/dataprep.py index 8d6ed877..827a9717 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -1572,6 +1572,13 @@ def new_workout_from_file(r, f2, # Get workout type from fit & tcx if (fileformat == 'fit'): # pragma: no cover workouttype = get_workouttype_from_fit(f2, workouttype=workouttype) + new_title = get_title_from_fit(f2) + if new_title: + title = new_title + new_notes = get_notes_from_fit(f2) + if new_notes: + notes = new_notes + # if (fileformat == 'tcx'): # workouttype_from_tcx = get_workouttype_from_tcx(f2,workouttype=workouttype) # if workouttype != 'rower' and workouttype_from_tcx not in mytypes.otwtypes: diff --git a/rowers/dataroutines.py b/rowers/dataroutines.py index 61282da7..d34a1a3e 100644 --- a/rowers/dataroutines.py +++ b/rowers/dataroutines.py @@ -1350,6 +1350,39 @@ def handle_nonpainsled(f2, fileformat, summary='', startdatetime='', empowerfirm # Create new workout from file and store it in the database # This routine should be used everywhere in views.py +def get_notes_from_fit(filename): + try: + fitfile = FitFile(filename, check_crc=False) + except FitHeaderError: # pragma: no cover + return '' + + records = fitfile.messages + notes = '' + for record in records: + if record.name == 'session': + try: + notes = ' '.join(record.get_values()['description'].split()) + except KeyError: + pass + + return notes + +def get_title_from_fit(filename): + try: + fitfile = FitFile(filename, check_crc=False) + except FitHeaderError: # pragma: no cover + return '' + + records = fitfile.messages + title = '' + for record in records: + if record.name == 'workout': + try: + title = ' '.join(record.get_values()['wkt_name'].split()) + except KeyError: + pass + + return title def get_workouttype_from_fit(filename, workouttype='water'): try: @@ -1359,17 +1392,27 @@ def get_workouttype_from_fit(filename, workouttype='water'): records = fitfile.messages fittype = 'rowing' + subsporttype = '' for record in records: - if record.name in ['sport', 'lap']: + if record.name in ['sport', 'lap','session']: try: fittype = record.get_values()['sport'].lower() + try: + subsporttype = record.get_values()['sub_sport'].lower() + except KeyError: + subsporttype = '' except (KeyError, AttributeError): # pragma: no cover - return 'water' - try: - workouttype = mytypes.fitmappinginv[fittype] - return workouttype - except KeyError: # pragma: no cover - return workouttype + pass + if subsporttype: + try: + workouttype = mytypes.fitmappinginv[subsporttype] + except KeyError: + pass + else: + try: + workouttype = mytypes.fitmappinginv[fittype] + except KeyError: + pass return workouttype diff --git a/rowers/integrations/intervals.py b/rowers/integrations/intervals.py index 185ef8d9..e1cea0fd 100644 --- a/rowers/integrations/intervals.py +++ b/rowers/integrations/intervals.py @@ -107,8 +107,20 @@ class IntervalsIntegration(SyncIntegration): def get_workout_list(self, *args, **kwargs) -> int: url = self.oauth_data['base_url'] + 'athlete/0/activities?' - startdate = timezone.now() - timedelta(days=365) + startdate = timezone.now() - timedelta(days=30) enddate = timezone.now() + timedelta(days=1) + startdatestring = kwargs.get("startdate","") + enddatestring = kwargs.get("enddate","") + + try: + startdate = arrow.get(startdatestring).datetime + except: + pass + try: + enddate = arrow.get(enddatestring).datetime + except: + pass + url += 'oldest=' + startdate.strftime('%Y-%m-%d') + '&newest=' + enddate.strftime('%Y-%m-%d') headers = { 'accept': '*/*', @@ -122,7 +134,6 @@ class IntervalsIntegration(SyncIntegration): data = response.json() known_interval_ids = get_known_ids(self.rower, 'intervalsid') - workouts = [] for item in data: @@ -145,7 +156,7 @@ class IntervalsIntegration(SyncIntegration): ress = dict(zip(keys, values)) workouts.append(ress) - + return workouts diff --git a/rowers/models.py b/rowers/models.py index 610cd019..259f9f55 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -3697,7 +3697,7 @@ class Workout(models.Model): uploadedtogarmin = models.BigIntegerField(default=0) uploadedtorp3 = models.BigIntegerField(default=0) uploadedtonk = models.BigIntegerField(default=0) - uploadedtointervals = models.BigIntegerField(default=0) + uploadedtointervals = models.CharField(default=None,null=True, max_length=100) forceunit = models.CharField(default='lbs', choices=( ('lbs', 'lbs'), @@ -3829,7 +3829,7 @@ class TombStone(models.Model): uploadedtosporttracks = models.BigIntegerField(default=0) uploadedtotp = models.BigIntegerField(default=0) uploadedtonk = models.BigIntegerField(default=0) - uploadedtointervals = models.BigIntegerField(default=0) + uploadedtointervals = models.CharField(default=None,null=True, max_length=100) @receiver(models.signals.pre_delete, sender=Workout) def create_tombstone_on_delete(sender, instance, **kwargs): @@ -3855,7 +3855,7 @@ class SyncRecord(models.Model): c2id = models.BigIntegerField(unique=True,null=True,default=None) tpid = models.BigIntegerField(unique=True,null=True,default=None) rp3id = models.BigIntegerField(unique=True,null=True,default=None) - intervalsid = models.BigIntegerField(unique=True, null=True, default=None) + intervalsid = models.CharField(unique=True, null=True, default=None, max_length=100) def save(self, *args, **kwargs): if self.workout: diff --git a/rowers/mytypes.py b/rowers/mytypes.py index b0586308..3f19525c 100644 --- a/rowers/mytypes.py +++ b/rowers/mytypes.py @@ -148,6 +148,7 @@ garminmapping = {key: value for key, value in Reverse(garmincollection)} fitcollection = ( ('water', 'rowing'), ('rower', 'rowing'), + ('rower', 'indoor_rowing'), ('skierg', 'cross_country_skiing'), ('bike', 'cycling'), ('bikeerg', 'cycling'), diff --git a/rowers/tasks.py b/rowers/tasks.py index 36999b19..a43a225d 100644 --- a/rowers/tasks.py +++ b/rowers/tasks.py @@ -3543,7 +3543,7 @@ def handle_intervals_getworkout(rower, intervalstoken, workoutid, debug=False, * 'boattype': '1x', 'workouttype': workouttype, 'file': fit_filename, - 'intervalsid': intervalsid, + 'intervalsid': workoutid, 'title': title, 'rpe': 0, 'notes': '', diff --git a/rowers/views/importviews.py b/rowers/views/importviews.py index 486155b1..a02527b0 100644 --- a/rowers/views/importviews.py +++ b/rowers/views/importviews.py @@ -471,7 +471,10 @@ def workout_import_view(request, source='c2'): try: tdict = dict(request.POST.lists()) ids = tdict['workoutid'] - nkids = [int(id) for id in ids] + try: + nkids = [int(id) for id in ids] + except ValueError: + nkids = ids for nkid in nkids: try: _ = integration.get_workout(nkid, startdate=startdate, enddate=enddate) From d93f5de8d16f183d2af5d92b681946edf242da9b Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Mon, 9 Dec 2024 22:44:06 +0100 Subject: [PATCH 18/49] export to intervals.icu --- rowers/integrations/intervals.py | 92 +++++++++++++++++++++++++++++- rowers/templates/menu_workout.html | 14 +++++ 2 files changed, 104 insertions(+), 2 deletions(-) diff --git a/rowers/integrations/intervals.py b/rowers/integrations/intervals.py index e1cea0fd..2669907f 100644 --- a/rowers/integrations/intervals.py +++ b/rowers/integrations/intervals.py @@ -100,10 +100,98 @@ class IntervalsIntegration(SyncIntegration): return token def createworkoutdata(self, w, *args, **kwargs) -> str: - return NotImplemented + dozip = kwargs.get('dozip', True) + filename = w.csvfilename + try: + row = rowingdata(csvfile=filename) + except IOError: # pragma: no cover + data = dataprep.read_df_sql(w.id) + try: + datalength = len(data) + except AttributeError: + datalength = 0 + + if datalength == 0: + data.rename(columns=columndict, inplace=True) + _ = data.to_csv(w.csvfilename+'.gz', index_label='index', compression='gzip') + try: + row = rowingdata(csvfile=filename) + except IOError: # pragma: no cover + return '' # pragma: no cover + else: + return '' + + tcxfilename = w.csvfilename[:-4] + '.tcx' + try: + newnotes = w.notes + '\n from'+w.workoutsource+' via rowsandall.com' + except TypeError: + newnotes = 'from'+w.workoutsource+' via rowsandall.com' + + row.exporttotcx(tcxfilename, notes=newnotes) + if dozip: + gzfilename = tcxfilename + '.gz' + try: + with open(tcxfilename, 'rb') as inF: + s = inF.read() + with gzip.GzipFile(gzfilename, 'wb') as outF: + outF.write(s) + try: + os.remove(tcxfilename) + except WindowsError: # pragma: no cover + pass + except FileNotFoundError: + return '' + + return gzfilename + + return tcxfilename + + def workout_export(self, workout, *args, **kwargs) -> str: - return NotImplemented + token = self.open() + + filename = self.createworkoutdata(workout) + if not filename: + return 0 + + params = { + 'name': workout.name, + 'description': workout.notes, + } + + + authorizationstring = str('Bearer ' + token) + # headers with authorization string and content type multipart/form-data + headers = { + 'Authorization': authorizationstring, + } + + url = "https://intervals.icu/api/v1/athlete/{athleteid}/activities".format(athleteid=0) + + with open(filename, 'rb') as f: + files = {'file': f} + response = requests.post(url, params=params, headers=headers, files=files) + + if response.status_code not in [200, 201]: + dologging('intervals.icu.log', response.reason) + return 0 + + id = response.json()['id'] + # set workout type to workouttype + url = "https://intervals.icu/api/v1/activity/{activityid}".format(activityid=id) + + thetype = mytypes.intervalsmapping[workout.workouttype] + response = requests.put(url, headers=headers, json={'type': thetype}) + if response.status_code not in [200, 201]: + return 0 + + workout.uploadedtointervals = id + workout.save() + + os.remove(filename) + + return id def get_workout_list(self, *args, **kwargs) -> int: url = self.oauth_data['base_url'] + 'athlete/0/activities?' diff --git a/rowers/templates/menu_workout.html b/rowers/templates/menu_workout.html index f61797f6..ea138baf 100644 --- a/rowers/templates/menu_workout.html +++ b/rowers/templates/menu_workout.html @@ -231,6 +231,20 @@ {% endif %} +
  • + {% if workout.uploadedtointervals and workout.uploadedtointervals != '0' %} + + Intervals.icu + + {% elif user.rower.intervals_token == None or user.rower.intervals_token == '' %} + + Connect to Intervals.icu + + {% else %} + + Intervals.icu + + {% endif %}
  • CSV From fc52491e8a85b75f06e78896b392ebc8a331f18b Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Tue, 10 Dec 2024 08:00:41 +0100 Subject: [PATCH 19/49] fix --- rowers/integrations/c2.py | 2 +- rowers/integrations/nk.py | 2 +- rowers/integrations/rp3.py | 2 +- rowers/integrations/strava.py | 2 +- rowers/integrations/trainingpeaks.py | 2 +- rowers/tests/testdata/testdata.tcx.gz | Bin 4002 -> 4001 bytes 6 files changed, 5 insertions(+), 5 deletions(-) diff --git a/rowers/integrations/c2.py b/rowers/integrations/c2.py index 2a4a57ab..c2d21332 100644 --- a/rowers/integrations/c2.py +++ b/rowers/integrations/c2.py @@ -63,7 +63,7 @@ class C2Integration(SyncIntegration): 'client_id': C2_CLIENT_ID, 'client_secret': C2_CLIENT_SECRET, 'redirect_uri': C2_REDIRECT_URI, - 'autorization_uri': "https://log.concept2.com/oauth/authorize", + 'authorization_uri': "https://log.concept2.com/oauth/authorize", 'content_type': 'application/x-www-form-urlencoded', 'tokenname': 'c2token', 'refreshtokenname': 'c2refreshtoken', diff --git a/rowers/integrations/nk.py b/rowers/integrations/nk.py index 960b2f51..a26a7cc9 100644 --- a/rowers/integrations/nk.py +++ b/rowers/integrations/nk.py @@ -35,7 +35,7 @@ class NKIntegration(SyncIntegration): 'client_id': NK_CLIENT_ID, 'client_secret': NK_CLIENT_SECRET, 'redirect_uri': NK_REDIRECT_URI, - 'autorization_uri': NK_OAUTH_LOCATION+"/oauth/authorize", + 'authorization_uri': NK_OAUTH_LOCATION+"/oauth/authorize", 'content_type': 'application/json', 'tokenname': 'nktoken', 'refreshtokenname': 'nkrefreshtoken', diff --git a/rowers/integrations/rp3.py b/rowers/integrations/rp3.py index 245c7615..bdbae35e 100644 --- a/rowers/integrations/rp3.py +++ b/rowers/integrations/rp3.py @@ -30,7 +30,7 @@ class RP3Integration(SyncIntegration): 'client_id': RP3_CLIENT_ID, 'client_secret': RP3_CLIENT_SECRET, 'redirect_uri': RP3_REDIRECT_URI, - 'autorization_uri': "https://rp3rowing-app.com/oauth/authorize?", + 'authorization_uri': "https://rp3rowing-app.com/oauth/authorize?", 'content_type': 'application/x-www-form-urlencoded', # 'content_type': 'application/json', 'tokenname': 'rp3token', diff --git a/rowers/integrations/strava.py b/rowers/integrations/strava.py index b78281a0..8df2cb31 100644 --- a/rowers/integrations/strava.py +++ b/rowers/integrations/strava.py @@ -89,7 +89,7 @@ class StravaIntegration(SyncIntegration): 'client_id': STRAVA_CLIENT_ID, 'client_secret': STRAVA_CLIENT_SECRET, 'redirect_uri': STRAVA_REDIRECT_URI, - 'autorization_uri': "https://www.strava.com/oauth/authorize", + 'authorization_uri': "https://www.strava.com/oauth/authorize", 'content_type': 'application/json', 'tokenname': 'stravatoken', 'refreshtokenname': 'stravarefreshtoken', diff --git a/rowers/integrations/trainingpeaks.py b/rowers/integrations/trainingpeaks.py index 14e3307a..404801de 100644 --- a/rowers/integrations/trainingpeaks.py +++ b/rowers/integrations/trainingpeaks.py @@ -41,7 +41,7 @@ class TPIntegration(SyncIntegration): 'client_id': TP_CLIENT_ID, 'client_secret': TP_CLIENT_SECRET, 'redirect_uri': TP_REDIRECT_URI, - 'autorization_uri': "https://oauth.trainingpeaks.com/oauth/authorize?", + 'authorization_uri': "https://oauth.trainingpeaks.com/oauth/authorize?", 'content_type': 'application/x-www-form-urlencoded', 'tokenname': 'tptoken', 'refreshtokenname': 'tprefreshtoken', diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz index 1c63fdbf311e81cba18bddbb4e55981700a69cb1..44ca7da185282a4134935f7eb23bb2dcbe772b58 100644 GIT binary patch literal 4001 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 Ib;N)H0QPu4hX4Qo From ed3bf035399dd5d786aed13609cb80431949a3e8 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Tue, 10 Dec 2024 08:02:55 +0100 Subject: [PATCH 20/49] changing return status: --- rowers/views/apiviews.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rowers/views/apiviews.py b/rowers/views/apiviews.py index 2a478053..51c4cfb5 100644 --- a/rowers/views/apiviews.py +++ b/rowers/views/apiviews.py @@ -575,7 +575,7 @@ def strokedata_fit(request): return JsonResponse({ "status": "error", "message": f"An error occurred while saving the FIT file: {str(e)}" - }, status=500) + }, status=400) try: # Parse the FIT file @@ -585,7 +585,7 @@ def strokedata_fit(request): return JsonResponse({ "status": "error", "message": f"An error occurred while parsing the FIT file: {str(e)}" - }, status=500) + }, status=422) rowdata = rowingdata(df=row.df) From 2f5cd1b84474d36ccc68727bcb6dd3ccd2191377 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Tue, 10 Dec 2024 08:06:07 +0100 Subject: [PATCH 21/49] returning better code --- rowers/views/apiviews.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rowers/views/apiviews.py b/rowers/views/apiviews.py index 51c4cfb5..de8cc5be 100644 --- a/rowers/views/apiviews.py +++ b/rowers/views/apiviews.py @@ -601,6 +601,7 @@ def strokedata_fit(request): dologging('apilog.log','FIT error to get time') dologging('apilog.log',e) _ = myqueue(queuehigh, handle_sendemail_unrecognized, fit_filename, "fit parser") + return HttpResponse(status=422) w = Workout.objects.create(user=request.user.rower, duration=duration, From 5db98f034f7a78e950c16b00a034f445938f0b2f Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Tue, 10 Dec 2024 17:44:44 +0100 Subject: [PATCH 22/49] tested --- rowers/tests/testdata/testdata.tcx.gz | Bin 4001 -> 4000 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 44ca7da185282a4134935f7eb23bb2dcbe772b58..4ab2f8bd4647adef840c754fefc2d20fa0e34e9e 100644 GIT binary patch literal 4000 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 GfB^t2@GX7- literal 4001 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 Date: Tue, 10 Dec 2024 20:00:52 +0100 Subject: [PATCH 23/49] intervals auto im & export --- rowers/forms.py | 3 ++ rowers/integrations/intervals.py | 28 +++++++++++++-- rowers/management/commands/getsyncids.py | 6 ++-- rowers/management/commands/processemail.py | 11 ++++++ rowers/models.py | 4 ++- rowers/templates/rower_exportsettings.html | 3 ++ rowers/uploads.py | 42 +++++++++++++--------- rowers/views/workoutviews.py | 11 ++++++ 8 files changed, 87 insertions(+), 21 deletions(-) diff --git a/rowers/forms.py b/rowers/forms.py index fa3f1b4b..0ebc5fb3 100644 --- a/rowers/forms.py +++ b/rowers/forms.py @@ -554,6 +554,9 @@ class UploadOptionsForm(forms.Form): upload_to_TrainingPeaks = forms.BooleanField(initial=False, required=False, label='Export to TrainingPeaks') + upload_to_Intervals = forms.BooleanField(initial=False, + required=False, + label='Export to Intervals') # do_physics = forms.BooleanField(initial=False,required=False,label='Power Estimate (OTW)') makeprivate = forms.BooleanField(initial=False, required=False, label='Make Workout Private') diff --git a/rowers/integrations/intervals.py b/rowers/integrations/intervals.py index 2669907f..4437019e 100644 --- a/rowers/integrations/intervals.py +++ b/rowers/integrations/intervals.py @@ -183,6 +183,7 @@ class IntervalsIntegration(SyncIntegration): thetype = mytypes.intervalsmapping[workout.workouttype] response = requests.put(url, headers=headers, json={'type': thetype}) + if response.status_code not in [200, 201]: return 0 @@ -260,8 +261,31 @@ class IntervalsIntegration(SyncIntegration): self.rower.intervals_token, id) - def get_workouts(workout, *args, **kwargs) -> list: - return NotImplemented + return 1 + + def get_workouts(self, *args, **kwargs): + startdate = timezone.now() - timedelta(days=7) + enddate = timezone.now() + timedelta(days=1) + startdatestring = kwargs.get(startdate,"") + enddatestring = kwargs.get(enddate,"") + + try: + startdate = arrow.get(startdatestring).datetime + except: + pass + try: + enddate = arrow.get(enddatestring).datetime + except: + pass + + count = 0 + workouts = self.get_workout_list(startdate=startdate, enddate=enddate) + for workout in workouts: + if workout['new'] == 'NEW': + self.get_workout(workout['id']) + count +=1 + + return count def make_authorization_url(self, *args, **kwargs): return super(IntervalsIntegration, self).make_authorization_url(*args, **kwargs) diff --git a/rowers/management/commands/getsyncids.py b/rowers/management/commands/getsyncids.py index a2f9bf75..806a7218 100644 --- a/rowers/management/commands/getsyncids.py +++ b/rowers/management/commands/getsyncids.py @@ -24,7 +24,8 @@ class Command(BaseCommand): record.sporttracksid = w.uploadedtosporttracks if w.uploadedtoc2: record.c2id = w.uploadedtoc2 - + if w.uploadedtointervals: + record.intervalsid = w.uploadedtointervals try: record.save() except IntegrityError: @@ -52,7 +53,8 @@ class Command(BaseCommand): record.sporttracksid = w.uploadedtosporttracks if w.uploadedtoc2: record.c2id = w.uploadedtoc2 - + if w.uploadedtointervals: + record.intervalsid = w.uploadedtointervals try: record.save() except IntegrityError: diff --git a/rowers/management/commands/processemail.py b/rowers/management/commands/processemail.py index 6d12ff71..be02ab17 100644 --- a/rowers/management/commands/processemail.py +++ b/rowers/management/commands/processemail.py @@ -117,5 +117,16 @@ class Command(BaseCommand): lines = traceback.format_exception(exc_type, exc_value, exc_traceback) dologging('processemail.log', ''.join('!! ' + line for line in lines)) + rowers = Rower.objects.filter(intervals_auto_import=True) + for r in rowers: + try: + if user_is_not_basic(r.user) or user_is_coachee(r.user): + intervals_integration = IntervalsIntegration(r.user) + _ = intervals_integration.get_workouts() + except: + exc_type, exc_value, exc_traceback = sys.exc_info() + lines = traceback.format_exception(exc_type, exc_value, exc_traceback) + dologging('processemail.log', ''.join('!! ' + line for line in lines)) + self.stdout.write(self.style.SUCCESS( 'Successfully processed email attachments')) diff --git a/rowers/models.py b/rowers/models.py index 259f9f55..f4715081 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -4557,7 +4557,9 @@ class RowerExportForm(ModelForm): 'strava_auto_import', 'strava_auto_delete', 'trainingpeaks_auto_export', - 'rp3_auto_import' + 'rp3_auto_import', + 'intervals_auto_import', + 'intervals_auto_export', ] # Simple form to set rower's Functional Threshold Power diff --git a/rowers/templates/rower_exportsettings.html b/rowers/templates/rower_exportsettings.html index 866a771a..3ad0019d 100644 --- a/rowers/templates/rower_exportsettings.html +++ b/rowers/templates/rower_exportsettings.html @@ -37,6 +37,9 @@ {% if rower.rojabo_token is not None and rower.rojabo_token != '' %} Rojabo {% endif %} + {% if rower.intervals_token is not None and rower.intervals_token != '' %} + Intervals.icu + {% endif %}

    {% if form.errors %} diff --git a/rowers/uploads.py b/rowers/uploads.py index b13881a9..bb587c21 100644 --- a/rowers/uploads.py +++ b/rowers/uploads.py @@ -130,11 +130,14 @@ def make_plot(r, w, f1, f2, plottype, title, imagename='', plotnr=0): def do_sync(w, options, quick=False): - do_strava_export = w.user.strava_auto_export - try: - do_strava_export = options['upload_to_Strava'] or do_strava_export - except KeyError: - pass + do_strava_export = False + if w.user.strava_auto_export is True: + do_strava_export = True + else: + try: + do_strava_export = options['upload_to_Strava'] or do_strava_export + except KeyError: + pass try: if options['stravaid'] != 0 and options['stravaid'] != '': # pragma: no cover @@ -146,11 +149,15 @@ def do_sync(w, options, quick=False): except KeyError: pass - do_icu_export = w.user.intervals_auto_export - try: - do_icu_export = options['upload_to_Intervals'] or do_icu_export - except KeyError: - pass + do_icu_export = False + if w.user.intervals_auto_export is True: + do_icu_export = True + else: + try: + do_icu_export = options['upload_to_Intervals'] + except KeyError: + pass + try: if options['intervalsid'] != 0 and options['intervalsid'] != '': # pragma: no cover @@ -193,11 +200,14 @@ def do_sync(w, options, quick=False): except KeyError: pass - do_c2_export = w.user.c2_auto_export - try: - do_c2_export = options['upload_to_C2'] or do_c2_export - except KeyError: - pass + do_c2_export = False + if w.user.c2_auto_export is True: + do_c2_export = True + else: + try: + do_c2_export = options['upload_to_C2'] or do_c2_export + except KeyError: + pass try: if options['c2id'] != 0 and options['c2id'] != '': # pragma: no cover @@ -266,7 +276,7 @@ def do_sync(w, options, quick=False): id = 0 message = "Please connect to Intervals.icu first" except Exception as e: - dologging( + dologging( 'intervals.icu.log', e ) diff --git a/rowers/views/workoutviews.py b/rowers/views/workoutviews.py index a250e4aa..14112726 100644 --- a/rowers/views/workoutviews.py +++ b/rowers/views/workoutviews.py @@ -5247,6 +5247,7 @@ def workout_upload_view(request, upload_to_strava = uploadoptions.get('upload_to_Strava', False) upload_to_st = uploadoptions.get('upload_to_SportTracks', False) upload_to_tp = uploadoptions.get('upload_to_TrainingPeaks', False) + upload_to_intervals = uploadoptions.get('upload_to_Intervals', False) response = {} if request.method == 'POST': @@ -5296,6 +5297,7 @@ def workout_upload_view(request, upload_to_strava = optionsform.cleaned_data['upload_to_Strava'] upload_to_st = optionsform.cleaned_data['upload_to_SportTracks'] upload_to_tp = optionsform.cleaned_data['upload_to_TrainingPeaks'] + upload_to_intervals = optionsform.cleaned_data['upload_to_Intervals'] makeprivate = optionsform.cleaned_data['makeprivate'] landingpage = optionsform.cleaned_data['landingpage'] raceid = optionsform.cleaned_data['raceid'] @@ -5313,6 +5315,7 @@ def workout_upload_view(request, 'upload_to_Strava': upload_to_strava, 'upload_to_SportTracks': upload_to_st, 'upload_to_TrainingPeaks': upload_to_tp, + 'upload_to_Intervals': upload_to_intervals, 'landingpage': landingpage, 'boattype': boattype, 'rpe': rpe, @@ -5447,6 +5450,14 @@ def workout_upload_view(request, message = "Please connect to TrainingPeaks first" messages.error(request, message) + if (upload_to_intervals): + intervals_integration = IntervalsIntegration(request.user) + try: + id = intervals_integration.workout_export(w) + except NoTokenError: + message = "Please connect to Intervals.icu first" + messages.error(request, message) + if int(registrationid) < 0: # pragma: no cover race = VirtualRace.objects.get(id=-int(registrationid)) if race.sessiontype == 'race': From 8327e51841804638b4000790ca6dd86ee5199f66 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 11 Dec 2024 08:07:13 +0100 Subject: [PATCH 24/49] testing --- rowers/templates/rower_exportsettings.html | 4 ++-- rowers/tests/testdata/testdata.tcx.gz | Bin 4000 -> 3999 bytes 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rowers/templates/rower_exportsettings.html b/rowers/templates/rower_exportsettings.html index 3ad0019d..b6aae035 100644 --- a/rowers/templates/rower_exportsettings.html +++ b/rowers/templates/rower_exportsettings.html @@ -32,10 +32,10 @@ Strava, {% endif %} {% if rower.rp3token is not None and rower.rp3token != '' %} - RP3 + RP3, {% endif %} {% if rower.rojabo_token is not None and rower.rojabo_token != '' %} - Rojabo + Rojabo, {% endif %} {% if rower.intervals_token is not None and rower.intervals_token != '' %} Intervals.icu diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz index 4ab2f8bd4647adef840c754fefc2d20fa0e34e9e..6ae0e30902e4a847a9bea105dfbe010424bba57f 100644 GIT binary patch literal 3999 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 F0RYMsI8guq literal 4000 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 GfB^t2@GX7- From 3e9c23b03f0c26299591793687bc2af09669784c Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 11 Dec 2024 19:23:44 +0100 Subject: [PATCH 25/49] temp logging --- rowers/integrations/intervals.py | 3 ++- rowers/integrations/trainingpeaks.py | 2 +- rowers/mytypes.py | 33 ++++++++++++++++++++++++++++ rowers/views/workoutviews.py | 2 ++ 4 files changed, 38 insertions(+), 2 deletions(-) diff --git a/rowers/integrations/intervals.py b/rowers/integrations/intervals.py index 4437019e..b55a50a4 100644 --- a/rowers/integrations/intervals.py +++ b/rowers/integrations/intervals.py @@ -128,7 +128,7 @@ class IntervalsIntegration(SyncIntegration): except TypeError: newnotes = 'from'+w.workoutsource+' via rowsandall.com' - row.exporttotcx(tcxfilename, notes=newnotes) + row.exporttotcx(tcxfilename, notes=newnotes, sport=mytypes.intervalsmapping[w.workouttype]) if dozip: gzfilename = tcxfilename + '.gz' try: @@ -181,6 +181,7 @@ class IntervalsIntegration(SyncIntegration): # set workout type to workouttype url = "https://intervals.icu/api/v1/activity/{activityid}".format(activityid=id) + thetype = mytypes.intervalsmapping[workout.workouttype] response = requests.put(url, headers=headers, json={'type': thetype}) diff --git a/rowers/integrations/trainingpeaks.py b/rowers/integrations/trainingpeaks.py index 404801de..efe98532 100644 --- a/rowers/integrations/trainingpeaks.py +++ b/rowers/integrations/trainingpeaks.py @@ -66,7 +66,7 @@ class TPIntegration(SyncIntegration): except TypeError: newnotes = 'from '+w.workoutsource+' via rowsandall.com' - row.exporttotcx(tcxfilename, notes=newnotes) + row.exporttotcx(tcxfilename, notes=newnotes, sport=tpmapping(w.workouttype)) return tcxfilename diff --git a/rowers/mytypes.py b/rowers/mytypes.py index 3f19525c..194793fe 100644 --- a/rowers/mytypes.py +++ b/rowers/mytypes.py @@ -181,7 +181,40 @@ fitcollection = ( fitmapping = {key: value for key, value in Reverse(fitcollection)} +tcxcollection = ( + ('water', 'Rowing'), + ('rower', 'Rowing'), + ('skierg', 'CrossCountrySkiing'), + ('bike', 'Biking'), + ('bikeerg', 'Biking'), + ('dynamic', 'Rowing'), + ('slides', 'Rowing'), + ('paddle', 'Other'), + ('snow', 'CrossCountrySkiing'), + ('coastal', 'Rowing'), + ('c-boat', 'Rowing'), + ('churchboat', 'Rowing'), + ('Ride', 'Biking'), + ('Run', 'Running'), + ('NordicSki', 'CrossCountrySkiing'), + ('Swim', 'Swimming'), + ('Hike', 'Hiking'), + ('Walk', 'Walking'), + ('Canoeing', 'Other'), + ('Crossfit', 'Other'), + ('StandUpPaddling', 'Other'), + ('IceSkate', 'Other'), + ('WeightTraining', 'Other'), + ('InlineSkate', 'Other'), + ('Kayaking', 'Other'), + ('Workout', 'Other'), + ('Yoga', 'Other'), + ('other', 'Other'), +) +tcxmapping = {key: value for key, value in Reverse(tcxcollection)} + +tcxmappinginv = {value: key for key, value in Reverse(tcxcollection) if value is not None} intervalscollection = ( ('water', 'Rowing'), diff --git a/rowers/views/workoutviews.py b/rowers/views/workoutviews.py index 14112726..a27e3887 100644 --- a/rowers/views/workoutviews.py +++ b/rowers/views/workoutviews.py @@ -4971,6 +4971,8 @@ def workout_upload_api(request): message = {'status': 'false', 'message': 'could not find file'} return JSONResponse(status=400, data=message) + dologging('own_api.log','-temp-') + dologging('own_api.log',post_data) # sync related IDs sporttracksid = post_data.get('sporttracksid','') From 8c9347943c15037ead8a0a9cc772de25c6baa2d9 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 11 Dec 2024 19:37:23 +0100 Subject: [PATCH 26/49] more logggin --- rowers/uploads.py | 1 + rowers/views/workoutviews.py | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/rowers/uploads.py b/rowers/uploads.py index bb587c21..4337e1c1 100644 --- a/rowers/uploads.py +++ b/rowers/uploads.py @@ -158,6 +158,7 @@ def do_sync(w, options, quick=False): except KeyError: pass + 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 diff --git a/rowers/views/workoutviews.py b/rowers/views/workoutviews.py index a27e3887..14112726 100644 --- a/rowers/views/workoutviews.py +++ b/rowers/views/workoutviews.py @@ -4971,8 +4971,6 @@ def workout_upload_api(request): message = {'status': 'false', 'message': 'could not find file'} return JSONResponse(status=400, data=message) - dologging('own_api.log','-temp-') - dologging('own_api.log',post_data) # sync related IDs sporttracksid = post_data.get('sporttracksid','') From 3f53612e2ab31565584eb78560c04883f5b34a55 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 11 Dec 2024 19:39:15 +0100 Subject: [PATCH 27/49] fix --- rowers/uploads.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rowers/uploads.py b/rowers/uploads.py index 4337e1c1..935dd8ef 100644 --- a/rowers/uploads.py +++ b/rowers/uploads.py @@ -158,7 +158,7 @@ def do_sync(w, options, quick=False): except KeyError: pass - dologging("uploads.log", "do_icu_export: {do_icu_export}".format(do_icu_export=do_icu_export) + 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 From 1e17be5cc951f4950f1c2485aa45c90a7571649b Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 11 Dec 2024 21:25:14 +0100 Subject: [PATCH 28/49] fi --- rowers/integrations/intervals.py | 3 +++ rowers/uploads.py | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/rowers/integrations/intervals.py b/rowers/integrations/intervals.py index b55a50a4..4e0ac680 100644 --- a/rowers/integrations/intervals.py +++ b/rowers/integrations/intervals.py @@ -150,6 +150,7 @@ class IntervalsIntegration(SyncIntegration): def workout_export(self, workout, *args, **kwargs) -> str: token = self.open() + dologging('intervals.icu.log', "Exporting workout {id}".format(id=workout.id)) filename = self.createworkoutdata(workout) if not filename: @@ -192,6 +193,8 @@ class IntervalsIntegration(SyncIntegration): workout.save() os.remove(filename) + + dologging('intervals.icu.log', "Exported workout {id}".format(id=workout.id)) return id diff --git a/rowers/uploads.py b/rowers/uploads.py index 935dd8ef..71f07553 100644 --- a/rowers/uploads.py +++ b/rowers/uploads.py @@ -158,7 +158,7 @@ def do_sync(w, options, quick=False): except KeyError: pass - dologging("uploads.log", "do_icu_export: {do_icu_export}".format(do_icu_export=do_icu_export)) + #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 @@ -275,6 +275,7 @@ def do_sync(w, options, quick=False): ) except NoTokenError: id = 0 + dologging('intervals.icu.log','NoTokenError') message = "Please connect to Intervals.icu first" except Exception as e: dologging( From 41abcb59d0defcf978614a586fa04e6619bea4eb Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Thu, 12 Dec 2024 16:50:00 +0100 Subject: [PATCH 29/49] fix --- rowers/integrations/trainingpeaks.py | 3 ++- rowers/tests/testdata/testdata.tcx.gz | Bin 3999 -> 3999 bytes 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/rowers/integrations/trainingpeaks.py b/rowers/integrations/trainingpeaks.py index efe98532..8dbefd35 100644 --- a/rowers/integrations/trainingpeaks.py +++ b/rowers/integrations/trainingpeaks.py @@ -15,6 +15,7 @@ from rowingdata import rowingdata from rowers.rower_rules import is_workout_user import time from django_rq import job +from rowers.mytypes import tpmapping from rowers.tasks import check_tp_workout_id, handle_workout_tp_upload @@ -66,7 +67,7 @@ class TPIntegration(SyncIntegration): except TypeError: newnotes = 'from '+w.workoutsource+' via rowsandall.com' - row.exporttotcx(tcxfilename, notes=newnotes, sport=tpmapping(w.workouttype)) + row.exporttotcx(tcxfilename, notes=newnotes, sport=tpmapping[w.workouttype]) return tcxfilename diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz index 6ae0e30902e4a847a9bea105dfbe010424bba57f..f15cf38957d0b2a3b4cba860a3eabe8396f81f14 100644 GIT binary patch delta 258 zcmV+d0sa1;ADKHV9>7XbVJk5)In*gyH*V>*7YL%rUl=kQ%Z*BN8m0L>Dc?*IS* delta 258 zcmV+d0sa1;ADH|EdRpVs^D zkG{J~951fk?)zhx-kz(A*V}IMygNJFJ+1U{y*Yi}4P82Kw|~15mG{RTee-g7adCCy zHTmZ9@@#$54e6&}zZlo&{^VroyKi5v&z@iZ++WiBm=3-1aNcft()}l0GCK=#ZhroB zxAi7%_VRwe-G=K&`_cK2o~4^U{<%NvyKSF1 Date: Thu, 12 Dec 2024 17:38:56 +0100 Subject: [PATCH 30/49] bringing back message delete --- rowers/urls.py | 1 + rowers/views/userviews.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/rowers/urls.py b/rowers/urls.py index 5a4484da..06047059 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -662,6 +662,7 @@ urlpatterns = [ re_path(r'^me/messages/$', views.user_messages, name='user_messages'), re_path(r'^me/messages/delete/$', views.user_messages_delete_all, name='user_messages_delete_all'), re_path(r'^me/messages/(?P\d+)/markread/$', views.user_message_markread, name='user_message_markread'), + re_path(r'^me/messages/(?P\d+)/delete/$', views.user_message_delete, name='user_message_delete'), re_path(r'^me/messages/user/(?P\d+)/$', views.user_messages, name='user_messages'), re_path(r'^me/delete/$', views.remove_user, name='remove_user'), re_path(r'^survey/$', views.survey, name='survey'), diff --git a/rowers/views/userviews.py b/rowers/views/userviews.py index 629be7a2..ddd4fe73 100644 --- a/rowers/views/userviews.py +++ b/rowers/views/userviews.py @@ -279,7 +279,6 @@ def user_message_delete(request,id=0): # pragma: no cover messages.error(request,'Could not find this message') url = reverse('user_messages') return HttpResponseRedirect(url) - if msg.receiver == request.user.rower: msg.delete() From ab33759bd298a9efed2d12fd42ed654c25f959ed Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Fri, 13 Dec 2024 12:49:26 +0100 Subject: [PATCH 31/49] better organized export page --- rowers/models.py | 70 ++++++++ rowers/templates/rower_exportsettings.html | 199 ++++++++++++++------- rowers/views/statements.py | 8 +- rowers/views/userviews.py | 28 ++- 4 files changed, 240 insertions(+), 65 deletions(-) diff --git a/rowers/models.py b/rowers/models.py index f4715081..3ac826ff 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -4561,6 +4561,76 @@ class RowerExportForm(ModelForm): 'intervals_auto_import', 'intervals_auto_export', ] + +class RowerExportFormStrava(ModelForm): + class Meta: + model = Rower + fields = [ + 'stravaexportas', + 'strava_auto_export', + 'strava_auto_import', + 'strava_auto_delete', + ] + +class RowerExportFormIntervals(ModelForm): + class Meta: + model = Rower + fields = [ + 'intervals_auto_import', + 'intervals_auto_export', + ] + +class RowerExportFormGarmin(ModelForm): + class Meta: + model = Rower + fields = [ + 'garminactivity', + ] + +class RowerExportFormPolar(ModelForm): + class Meta: + model = Rower + fields = [ + 'polar_auto_import', + ] + +class RowerExportFormConcept2(ModelForm): + class Meta: + model = Rower + fields = [ + 'c2_auto_export', + 'c2_auto_import', + ] + +class RowerExportFormSportTracks(ModelForm): + class Meta: + model = Rower + fields = [ + 'sporttracks_auto_export', + ] + +class RowerExportFormTrainingPeaks(ModelForm): + class Meta: + model = Rower + fields = [ + 'trainingpeaks_auto_export', + 'rp3_auto_import', + ] + +class RowerExportFormRP3(ModelForm): + class Meta: + model = Rower + fields = [ + 'rp3_auto_import', + ] + +class RowerExportFormNK(ModelForm): + class Meta: + model = Rower + fields = [ + 'nk_auto_import' + ] + # Simple form to set rower's Functional Threshold Power class SimpleRowerPowerForm(ModelForm): diff --git a/rowers/templates/rower_exportsettings.html b/rowers/templates/rower_exportsettings.html index b6aae035..9625a472 100644 --- a/rowers/templates/rower_exportsettings.html +++ b/rowers/templates/rower_exportsettings.html @@ -7,8 +7,10 @@ {% block main %}

    Import and Export Settings for {{ rower.user.first_name }} {{ rower.user.last_name }}

    +
    + {% csrf_token %}
      -
    • +
    • You are currently connected to: {% if rower.c2token is not None and rower.c2token != '' %} Concept2 Logbook, @@ -41,44 +43,140 @@ Intervals.icu {% endif %}

      - -{% if form.errors %} -

      - Please correct the error{{ form.errors|pluralize }} below. -

      -{% endif %} -

      - - - {{ form.as_table }} -
      - {% csrf_token %} - -

    • -

      -{% if rower.garmintoken and rower.garmintoken != '' %} -

      - You are connected to Garmin. Switching off Garmin Connect sync is on the - Account settings - page. Look for the "Rowsandall" app. -

      -{% endif %} -

      - Garmin Connnect has no manual sync, so connecting your account to your Garmin account will - automatically auto-sync workouts from Garmin to Rowsandall (but not in the other direction). If you - want to export our structured workout sessions to your Garmin device, you have to set the "Garmin Activity" - to a activity type that is supported by your watch. Not all watches support "Custom" activities, so - you may have to set your activity to Run or Ride while rowing. -

      -

      - Strava Auto Import also imports activity changes on Strava to Rowsandall, except when you delete - a workout on Strava. If you want Deletions to propagate to Rowsandall, tick the Strava Auto Delete - check box. -

      +

      + Click on the icons to establish the connection or to renew the authorization. +

      -
    • +
    • +

      API Key

      +

      {{ apikey }}

      +

      + Regenerate +

      +

      This API key can be used to access the Rowsandall API. It is used by some third party applications to access your data. Keep it secret.

      +
    • + + {% if form.errors %} +
    • +

      + Please correct the error{{ form.errors|pluralize }} below. +

      +
    • + {% endif %} +
    • +

      Polar

      + + {{ forms.polar.as_table }} + +
      +

      connect with Polar

      +
    • +
    • +

      Concept2

      + + {{ forms.c2.as_table }} + +
      +

      connect with Concept2

      +
    • +
    • +

      NK

      + + {{ forms.nk.as_table }} + +
      +

      connect with NK Logbook

      +
    • +
    • +

      SportTracks

      + + {{ forms.sporttracks.as_table }} + +
      +

      connect with SportTracks

      +
    • +
    • +

      TrainingPeaks

      + + {{ forms.trainingpeaks.as_table }} + +
      +
    • +
    • +

      Rojabo

      +

      connect with Rojabo

      +
    • +
    • +

      Intervals.icu

      + + {{ forms.intervals.as_table }} + +
      +

      connect with intervals.icu

      +
    • +
    • +

      Garmin Connect

      + + {{ forms.garmin.as_table }} + +
      +

      connect with Garmin

      + +

      + Garmin Connnect has no manual sync, so connecting your account to your Garmin account will + automatically auto-sync workouts from Garmin to Rowsandall (but not in the other direction). If you + want to export our structured workout sessions to your Garmin device, you have to set the "Garmin Activity" + to a activity type that is supported by your watch. Not all watches support "Custom" activities, so + you may have to set your activity to Run or Ride while rowing. +

      + {% if rower.garmintoken and rower.garmintoken != '' %} +

      + You are connected to Garmin. Switching off Garmin Connect sync is on the + Account settings + page. Look for the "Rowsandall" app. +

      + {% endif %} +
    • +
    • +

      Strava

      +

      + {{ forms.strava.as_p }} +

      connect with strava

      +

      + Strava Auto Import also imports activity changes on Strava to Rowsandall, except when you delete + a workout on Strava. If you want Deletions to propagate to Rowsandall, tick the Strava Auto Delete + check box. +

      + +
    • +
    • +

      TrainingPeaks

      + + {{ forms.trainingpeaks.as_table }} + +
      +

      connect with Polar

      +
    • +
    • +

      RP3

      + + {{ forms.rp3.as_table }} + +
      +

      connect with RP3

      +
    • {% if grants %} +
    • Applications

      +

      + These applications have access to your Rowsandall data. +

      @@ -99,35 +197,12 @@ {% endfor %}
      +
    • {% endif %} -

      API Key

      -

      {{ apikey }}

      -

      - Regenerate -

      - This API key can be used to access the Rowsandall API. It is used by some third party applications to access your data. Keep it secret. -
    -

    Click on one of the icons below to connect to the service of your - choice or to renew the authorization.

    -

    connect with strava

    -

    connect with Concept2

    -

    connect with NK Logbook

    -

    connect with SportTracks

    -

    connect with Polar

    -

    connect with Polar

    - -

    connect with Garmin

    -

    connect with RP3

    -

    connect with Rojabo

    -

    connect with intervals.icu

    + + {% endblock %} diff --git a/rowers/views/statements.py b/rowers/views/statements.py index 01771ce6..09ac2fa1 100644 --- a/rowers/views/statements.py +++ b/rowers/views/statements.py @@ -179,7 +179,13 @@ from rowers.models import ( RowerPowerForm, RowerHRZonesForm, SimpleRowerPowerFo IndoorVirtualRaceForm, PlannedSessionCommentForm, Alert, Condition, StaticChartRowerForm, FollowerForm, VirtualRaceAthleteForm, InstantPlanForm, DataRowerForm, - StepEditorForm, iDokladToken ) + StepEditorForm, iDokladToken, + RowerExportFormStrava, RowerExportFormPolar, + RowerExportFormSportTracks, RowerExportFormTrainingPeaks, + RowerExportFormConcept2, RowerExportFormGarmin, + RowerExportFormIntervals, RowerExportFormRP3, + RowerExportFormNK, + ) from rowers.models import ( FavoriteForm, BaseFavoriteFormSet, SiteAnnouncement, BasePlannedSessionFormSet, get_course_timezone, BaseConditionFormSet, diff --git a/rowers/views/userviews.py b/rowers/views/userviews.py index ddd4fe73..d2d22e25 100644 --- a/rowers/views/userviews.py +++ b/rowers/views/userviews.py @@ -457,19 +457,42 @@ def rower_exportsettings_view(request, userid=0): 'polar_auto_import': 'polartoken', 'c2_auto_export': 'c2token', 'c2_auto_import': 'c2token', - 'runkeeper_auto_export': 'runkeepertoken', 'sporttracks_auto_export': 'sporttrackstoken', 'strava_auto_export': 'stravatoken', 'strava_auto_import': 'stravatoken', 'strava_auto_delete': 'stravatoken', 'trainingpeaks_auto_export': 'tptoken', 'rp3_auto_import': 'rp3token', - 'nk_auto_import': 'nktoken' + 'nk_auto_import': 'nktoken', + 'intervals_auto_export': 'intervals_token', } r = getrequestrowercoachee(request, userid=userid) + + forms = { + 'polar': RowerExportFormPolar(instance=r), + 'c2': RowerExportFormConcept2(instance=r), + 'sporttracks': RowerExportFormSportTracks(instance=r), + 'strava': RowerExportFormStrava(instance=r), + 'trainingpeaks': RowerExportFormTrainingPeaks(instance=r), + 'rp3': RowerExportFormRP3(instance=r), + 'intervals': RowerExportFormIntervals(instance=r), + 'nk': RowerExportFormNK(instance=r), + 'garmin': RowerExportFormGarmin(instance=r), + } if request.method == 'POST': form = RowerExportForm(request.POST) + forms = { + 'polar': RowerExportFormPolar(request.POST, instance=r), + 'c2': RowerExportFormConcept2(request.POST, instance=r), + 'sporttracks': RowerExportFormSportTracks(request.POST, instance=r), + 'strava': RowerExportFormStrava(request.POST, instance=r), + 'trainingpeaks': RowerExportFormTrainingPeaks(request.POST, instance=r), + 'rp3': RowerExportFormRP3(request.POST, instance=r), + 'intervals': RowerExportFormIntervals(request.POST, instance=r), + 'nk': RowerExportFormNK(request.POST, instance=r), + 'garmin': RowerExportFormGarmin(request.POST, instance=r), + } if form.is_valid(): cd = form.cleaned_data if r.rowerplan == 'basic': # pragma: no cover @@ -528,6 +551,7 @@ def rower_exportsettings_view(request, userid=0): return render(request, 'rower_exportsettings.html', {'form': form, + 'forms': forms, 'rower': r, 'breadcrumbs': breadcrumbs, 'grants': grants, From a7d48d711f7875b5ea3a6ce13503fda3306e067c Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Fri, 13 Dec 2024 13:15:48 +0100 Subject: [PATCH 32/49] adding resampling on demand, untested --- rowers/integrations/intervals.py | 13 ++++++++++- rowers/models.py | 2 ++ rowers/templates/rower_exportsettings.html | 26 ++++++++++----------- rowers/tests/testdata/testdata.tcx.gz | Bin 3999 -> 4000 bytes 4 files changed, 27 insertions(+), 14 deletions(-) diff --git a/rowers/integrations/intervals.py b/rowers/integrations/intervals.py index 4e0ac680..1cdcbfb0 100644 --- a/rowers/integrations/intervals.py +++ b/rowers/integrations/intervals.py @@ -101,7 +101,16 @@ class IntervalsIntegration(SyncIntegration): def createworkoutdata(self, w, *args, **kwargs) -> str: dozip = kwargs.get('dozip', True) - filename = w.csvfilename + # resample if wanted by user, not tested + if w.user.intervals_resample_to_1s: + datadf, id, msgs = dataprep.resample( + w.id, w.user, w, overwrite=False + ) + w_resampled = Workout.objects.get(id=id) + filename = w_resampled.csvfilename + else: + w_resampled = None + filename = w.csvfilename try: row = rowingdata(csvfile=filename) except IOError: # pragma: no cover @@ -128,6 +137,8 @@ class IntervalsIntegration(SyncIntegration): except TypeError: newnotes = 'from'+w.workoutsource+' via rowsandall.com' + if w.user.intervals_resample_to_1s and w_resampled: + w_resampled.delete() row.exporttotcx(tcxfilename, notes=newnotes, sport=mytypes.intervalsmapping[w.workouttype]) if dozip: gzfilename = tcxfilename + '.gz' diff --git a/rowers/models.py b/rowers/models.py index 3ac826ff..cd2500f7 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -1174,6 +1174,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_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) sporttrackstokenexpirydate = models.DateTimeField(blank=True, null=True) @@ -4578,6 +4579,7 @@ class RowerExportFormIntervals(ModelForm): fields = [ 'intervals_auto_import', 'intervals_auto_export', + 'intervals_resample_to_1s', ] class RowerExportFormGarmin(ModelForm): diff --git a/rowers/templates/rower_exportsettings.html b/rowers/templates/rower_exportsettings.html index 9625a472..0d7d3858 100644 --- a/rowers/templates/rower_exportsettings.html +++ b/rowers/templates/rower_exportsettings.html @@ -64,13 +64,13 @@
  • {% endif %}
  • -

    Polar

    +

    Intervals.icu

    - {{ forms.polar.as_table }} + {{ forms.intervals.as_table }}
    -

    connect with Polar

    +

    connect with intervals.icu

  • Concept2

    @@ -103,20 +103,20 @@
  • +
  • +

    Polar

    + + {{ forms.polar.as_table }} + +
    +

    connect with Polar

    +
  • Rojabo

    connect with Rojabo

  • -
  • -

    Intervals.icu

    - - {{ forms.intervals.as_table }} - -
    -

    connect with intervals.icu

    -
  • Garmin Connect

    diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz index f15cf38957d0b2a3b4cba860a3eabe8396f81f14..3a4eca337f44e3c892acabb79b59b31fb7ee2684 100644 GIT binary patch literal 4000 zcmV;R4`1*fiwFqYA6#bw|8!+@bYx+4VJ>uIcmVC4TW=Ic7J%RR6&4T4!-|@7sZ$rn zI3keMA^{r(l+D|!ViX@kz0nQq>5qCt z=H2GyUmy7Qu7B8_oL#Jzo5dUa>;3=i`lH?c%gamQw_R!w&`l5B{l>e8clP!UK0JW* zGxEnz8{Y3dKR>(Z@8A7r|7v-0clTxb`ti#{0(AF4?h(8|-nY0<&O}EX;co!@00)b- z)5ravbmx1|`|hHDu{vGezx!9mUS232`r@qbPV!;Tmq%yomzzUjf3Wy!>!$-At#)tF z(`EXxn{=P~UcT7)Y44Bt4iUq_;_}m{9Udf6>yDTISf;!Gwdds{rTw%i)$`%A zh`Z(nuDQgOuOF|suWJAPaQjv)6WPlxS0CidWqQDeg!wV<{=P!-Vs(6+Zf)7WI(vEe zv|GRKP7=k>wqD)4JNoZucfZ|oezscoS2yP`(mU`C@8Wj1&yW1#l@McxUz6_}XX;Ijt|*F4|t4ws|X=BvxT_~rL+o(ZlU!@oRZI_JN-zE5Hk4;H(>zP#ol zUG&SJcl|Bq7vVa*TIviYNxN7Db!3Cnz@8s;Mcm0%#63Yoa6!SHlHOF1d$>7ws!QIj zRK(pGVpNUtX^7y0+~aJxJ6jR=nB?7x*Km&|<(*+R+cdn!v$21@Az8>xZ#s{<-dG}&uBHSw* z=PR<~giQn+HQa-gwBP3>@1Y{@&Nv{@=yg$w>htrFcZ7-*JBfx_=cYv{OP+5*p6Vh` z)lBiU56P4hohgSXC4Dz+=Ognh6_K|lCZ%2_^CTt|Brh`}A1flC%sD|0s_={}YR9|z zkhiWL@(9LRB$do3c7iUDCDkAR;I-L^-oi91hTWF*G zDCC2QBvuoiLmx`|?`}oDjrN1&35_H0s?k1_B|o1C9ZE%39a$rQsM7FaENRcriM$OJ zkxzO$6RMK0AeM~MS(?#kj}?)RCLnmLO1o?1XF#6GxQJ0jJ}2iEBtIj+eNCf1i(v;| zGtZMPOWw|id_@r(4h8_cXvhncjLzHJ`0cq_bwlKn{91>qG%S)>(uX%I@)h~;EE`6& znkX&#q8av${7B@T0jne*U@|ga+dN-Ul-360)2pbm=OY;3G`y+L^MWRcyvm+ufT9tb z8IATe$-G#To-L4uJmO?@`xd1gChv?F44Nn{6Bf1S=S03Ddp=ueY*q6-GnzON^4RA2 z>&bJ@y=6escyiKy5_eZg}fMG2r8KuDjKnAk3yaeTNKqiAGoN7=bOxrLf#pSC@Ptcwy5{E$^1Cvx4E~f7HGC? zMdhu?WAot+k-(}8}D>cS{q}@oSZq4ucyX_ zbMCG4h?>z}5GEt@&7L179}NYFK}9|`-G2HJ8=>4SMHQtLhNjz3U*^S{kaT;M%-gbA z*Rj>ojzZoWVO_*Vq%8U7!yAFTO|T7mtIE0-*T~ zbMxd3l6N`lTGa{B5f;_$=S04y(LNeL44U}7V=7uj?OS|)4DvbmHek^7y>*s~=HAYU zd`0#=Swjw4HO~u8Cij+G{rn(#Z_pyCMtes^?fDszcT^Gi+&O_*)8F2CE;`T8iF`%6 zebUxAqH5JL7q~@fXFy&IfT+5hyT~m{JN?m~y%EuMLiEtc-;{hsy1fO%UR0hOkG7W~~oRK|WQGDJ8W06slljGCd zVkbhrHXmN7hrD2}rB$tI;iYK({G4RICJj&6cp=pI@Vs*+{r0n(=PR=3ll*cdRgI1J zu2odtkbFf^T8RdjKsDM2HyPc&d2a{E%RZp-N*|t&MOPu8mCQ>;&x z$-JwGyf+908mlf~n2gLf^26k#acQvEjP`*-QG0$)qkX7{Jd&{%qbBP*aBMPvL-MgA z@?sz;M|)dzrNUWxaw;0_(Hk$K>Xs5*(bY-k(Dz})V7m0*N=kU8Y`&s;UaixQh!7AR1tcgTV#1u zp$~2{QePQ*cJ-i7cA5YZUgQqA>< zWy@{mgq|xxAG3xZby8n;)xw5;9Q3&ps$*4V7N!}iY4|fhFL}UMg=`Upibm__q~=v* z+;K4EF{m_rbj_Q0Q{yhwgFcPj;BtRF4SHX+5ALlpGNBoFgXpaRAl6BJu#-WL&A1yx z?-P1m6OEkBRTMop^rN7UCR(LShrksz?x=b5#z3Ex^@zH%8>H;`Z&E*b^kSUXqNlR0 z(&i-fP!alIL`c;D8!1_YzM-GCOULKVnAVA?j5|sSf=r}+4z^|K2hm%@Ue`V+FN`sn z&PmO#A4cyD1qr$V-$T*DdTeUm80awxu(IF|Dd1!};$ln797N9sovv#ZQY^Y~>8u8P zstCO^U`dye%fORaKkS^nWkwiOJ^9pH{J@XG<>#Y*SVoqb2kS1oM9=btFLD%+fId=nl}o1HUb!RQt!fK z27GJ@T7&4Fv8Y?i5ZT9~jnk;v^`oGVh9X*>h9Am?Z@78$#z1c=_s`OK^Ek$)=1qUV zw_-q)g;y3~GV%J_lCdia+S;VCA;qXdPt@l6>7y4gQMqt2TBf3ryXM0m2R)nUkaXxR zPiA?u=ExmJzipZ5Rcf9U+gv)+cjRtcCZgBP^-d;}pw-ec2hfLY%S6yEd4twVGw!Bt z*N5CP(TbqTk!wS%zPTy-P!al&J80>;YS>sde8X+P9|Jw2VHT&7dV#Xb)$@$hJg5l0 z-L_Uxsd-YkjnbUz>up8oy&YW1vUM4ONs<&t*$6papG5 zK`*(X3hAWYO3}z2G&OG&^ue%T&`G_xqNy59%^L+hCF72itrfg;&5=8Gmrl$r6G6MC zt@nO1b3L`7?I8MGWX+%o-}vC0T|Wc#(MYoHbW$JvWbArsYTgifCL`AB!Z!gzv+HMo zUUK0T=+I+P^m(bEHsFtdJ{Vxs^}`Q2d4D44sd@8;(C3w`0fP=b$H|~?>(>vXXA^{U zk-PMwl+^HZCyIuC1oX~$wo&EM2@XrTbY_KKDncLgnzo>mdXGg1eBVki#y~G*oW-EZ z(g~qxrR}ZIx0<_Q^pdM@f+{07#G;k9w;J$UFP&lZA+Kqxi`+#hy4cyQH2hk!bOg35 zSp#~NBR8T<#-#(TZ{8sK?Ml|Vb?zdS4cbDR>qkNFbM=i9J^RV5ZrSGgLG*bgYerIe z^P=@dt8b|F%^L+hZx=CF8Fx`)TitTv-n@+7<-#i^dKZgIeQT6D4*Hl2uXJ{O4A7dV zP28K8&~I0=Rz~h(#MT*m2I#FxGrej76ar{Ha&L;>kA?oj){h$w7EikK%MZ`a`sL>E zyOaKhpHG&j%XPo^{dUv$dtJW=@BE*!`!jIpx+Q#f-2ZT}*d1_ZcczcOF@L`NwAy`t z@bDsWJiGX?@3StwJr`$hH{JSWcXG0QTIu6zef+ZPyL8@e_jV;JpN>0t@Vb9>c5&r3 zd2oJyvO4Pe^wTe2jLUO>b+Yu`Pp?-eFE4-YZ|QwZhhBL&AGSR0{*x}5ods7vf4$pk zoi=;giwFoF23uzW|8!+@bYx+4VJ>uIcmVC4NpBoC7J%>m6@m}RVG#CP__!#F zf^35^U}u8Z$mBMt$Q8L~q?VvAFZuUFcH6RStB^ctieMG!gQ2djt?R2tzMV&3zd1iU zc-3ER*B6_w9-@JV2VWmQIC^%~tvBn<>398R=&!!-hVHj++aIKzH`}isz8Hqf!`15b z>(}PAyE_D(l&pg-yr znK#?#e}3TKyWzX;?BZ(OZ&$DI&-eem8&3B7_t%%gzjmonKzBWK{~I43KR7rzdiwy< z&&VI&Z}{Wj>E*@M@YTbA4lnwvhx;$n*N>hb6QKJ7g{>Cjyb&(`PtR}cT}*y{_WL!Vs?-B~{DX@7FDdA>as_D8Evw!S~$`}O_} zdeWyKyG{3*@8zS7@ArO(?-(&0t*$@4-{G4iYTar7k3QY~&)v{}dwITlHHSYg(=WN+ z{AlxS-woTJ`l~1F&C8+x_i*D!q|^M;oxSXj(WZ;+|9pRi)h(B}@%5AL&HDW1`Tyw# z#NBcOw_M`J*N-;4S9SQ~c=uNHiR|^3n-B7NpC0frVSbGJzi&{yTA!Y#TkD4x7tfEM zbeosmS)%yC)|-2GC;$E6?sr=*FV>sk=H~oGdI$c(ySUr!!y`X>CB)eA*JO9VJqg^T zK>ia7dlI)J_}~GPg?r|~Povny_O!98UYq-af^3Jdr?!F@KmUiR3Q*gJzP(ki^5AIkM z_W%`fm&82@l}4NQ#?6F#UGg5dBJSP<4^E@cJ6F<-V_FV(Uk`Ty;{#fayn8V+6YiCb z^A*`~!X|=^8ty?#+V4w}_fQdcXB-e{^tvcT_4(zW@okYW|bJHS}CC~RDPj!)} zYNmKPgk;Kz&XhxxlD-?Z%aM7OipX0NlTxpec@h%}l9vUMj}?(m=A0l0Rd~h~wd37# z$Xizrc?9Dul1k>2@n6z&=a)0j3sgj&jb#+pkT2}Jiwh!86_Ixaoz97a&X=6$Ews^o z67s=B5~~T%p${efclRRSM*C6ngvJqg)o35el3z}Q4y7Wij;s+tRB3oImbB-WMBavq z$R|CW3028g5KBhsEG=lX$BM{D6A-*rrQJ313n0&AT*Rm%pObS7l3$SDzNXQh#jpdf zndixtC2to*zM_Z?2Lk|JG~@+JM(6E){Px_ex-s%eeyu}Q8Wu?`>BCzT`HFmamJK6X zO_Y{=(G2@Wej@VDfK`$YFdLb#ZJw_vN^67h=~YzO^AU`18s6OJc|nszUS-cSK+%ZJ zf=2tAWL~UE&lX5S9&t9heT&kLlXu1o22GTf35(kEOCn#9J)bQ!wyJra8BLrCd2I9i z?c_P<-ZCI*JUQt_DVcR`UGv*dLS76o1eMGS6^+<5^3#y_CM1bR4SB~+=I=miR| zBtTF#$?#lM=6%!cCn3*#a2X~jd!{zt&Oo{PR^3Z*HdG| zIrr9iM9pX~2(yv-X3vk4kA?!opdufeZa@EsjZp5EqKeWAL(}c&FY{teNV>gB=55)m z>)2{(Cn4{Rur6XFQkH!4;Y~o^CfEkORb^d^Yvk`r-d04OOz^tuD)Bg*_Go5QM=%kIHQrkX*m7^@FZ0Mo0+A+4>u^5g zxp{I%$-A6&t?Gp62#f0WOCn#>XdewA22Fh4F%_+%_ANd?1^JwN8!%}4-a1P~b8nYK zz9M^`tRaW2n&*XPlY7gpetwj^H)xSmqrIb|_WXj#JF19$?wmlZ>2L2m7oF#qM7|>3 zK51(lQMGEB3*4f#3m`8BKvZ4MUE~&}o&RXh-iYWrA$n-!?@GQR-QI#>FDg%tM_W?E zyH_4w)9}X0bMAhs^4|Jn3C5OzIeYSAymhFF)%m0w6pYmgF36s*C_Zn4vB;>&$?@rJ zu`?lGn-4G4LtZe~(yG?9@KUsXen~Q4lZGd3ybx-9c;2~^e)~nu^A*|iNq#w!s>a59 z*D5OSNWP*dtwaM%pc?Iin~iSYytgCdg15oSSD3w zMnrDp=N_?1msax+RgX$dhA(p-7;~w~yWz9qnCP zi+T$3``laIJRd?)t8Pi;EAr$>ve1}7MLve2IXSM)^OKNwCRk!klokQXhH00Cz9LtS z#GG&sL4}^6p}%AQ(`v?hmpk9Os2lGQ7>lOd-v>QZhF&T{FS%g~=mvaCvq6uo-}NZ^ zU<3(Cg+8fnA~PjTKb+a1p#TslOw7stCQ$EwVhS z&<8ggsjmz@yL!+kJIw|Ht3n@rQ8jN#13p%Sp3_`UpqlFu!fepjHsIGJ_0BLLspfjb zvgI~QLeCYUk6FWyI;k(aYGFe^4f@;()v+ow3)76%H2ejimptIBLbixPMWgjgQu8V@ z?l>6o7*rZQy5`Nht8o|VL7&EMaJfI82E8xZ2lrkXnb3^8QS{aT5bLBq*x8`RX55XU z_X)kOiAK)mDvF*P`bp476RpyvL*R-UchtOjQ=m`EdPH5>4N`XeH>saJdNIyx(No!0 zX-krNs0e*9BBW}7jg%}x-_Xz7rQ>sFOzT8c#vLUEL1xlE2ivmrqv)++uWO%^7si-P z=cH!WkE8d7f&|@w@1ba6JvKFO3iOx+SXpp~6mT{jaj~Ukj-qFSPS-UHDHdJ0bWsC7 zRfOIdu%ye#W#HMYant7darD7NQC8nD+M+I)cSQxtjug&af2J)z`C>ZKpy_&6@;08v%?usdr&E z13tC{tx@#OSk$d$i0ot0#%a{-`bp47LlLb`!w+S{H{85=Q=qq$`)BFAc^qR?^X5O` zTQMNY!Yhj~n|OU~$=DSIZEe!nkYZG!Cu(#3{Lu@Ts9d-hEmP6RUGw2jgPu)vNILYE zXS2LnbL5Vr-?dEiDmBlFZ7!YpJ92j|6VdDDdMC3<&}wO!Bk04fWg_U7yg}=w8FzEH z>qBmtXhl%v$hDzW-`o{_s0e+?9kg^^HEb*!zTr0DPk|oMFpE=3y+GOJ>UlwG9#n+h z?piCT)I2HNMrld)^|m7P-jG!WZH1wzH_x^Ke+u*j28oeX=q=1Us=mXBCp-NNpM$pIHG7%7?&aRKt(lQr-o=pIyH_t=a z<&2^A(wPLk%jl!ZuJ_1AYwn;Kcaxxx#-lP1-&5HV3}`{yDbOS3hAK*_=dvXj(1Ny; zpqJcGg>+JHrD)_1nwmEW`e0Zv=%ij;(NvA5=1qd0l5t1M)(YOa=E$A9ODE=*iJ)E6 z)_Xsjxt?0kb`*UsvSv_)Z+!60u3rH9Xe3#8I;oF-Hg-KVHE#?(lM!om;hO-V+4T!R zFS+mvbm*}t`n*(78}KJU9}F<+`r(J1ygw84)Vz6P=<`a}fI)|z<808k_3Ovcvk5}F z$X$9-N^1DI6GcNm0eWXV+o*Es1cxPEI*URt6`_xLO4Z?U()M2HTg}}#ddbx{L6wmkV$n+5dky%lm(Do)kk_=;MeZULUF>X88h$NVIs&_u ztO32sksDEF6Yx%x(lp8afAw`_C$DEhpTH6y9K zdC~f!)i>1o=1qd0cZ-;-jJqhYt!_DUZ(c_4a^aN{y^BSqzBNjn27SzhS30{s258OG zX70^P=yxkwD$?p{tH<5t^@krXhJJhe z+1c>r4`=;(zZnib-);K*pc@Y0jsKmZ>&4q1;Iq@=%cIr)fCu|Cz59*%^Yy3o{`;fv zt`f(KtGE09*rm7U>f-gb+dS{i&UQ~LeOzx&pLau-&fD$Zu0-YiaYx_099~>p-FQvD zxx74EpL9d|>DMpD^|?PeS^DnVm+Q0V*FX1{^ggCTZ#Z@1z4(SCIPqi5-+kALpZ`fl4N4*ApDM|b1CjvjZL(?{PQ|L|jGv-@+u{ipO6 z|M>F!w?5q&z!w1f|BqHTzSuwc-D5g_utUAxr04KmLfKz^pPt5V>C(p!{s%s$6Y0c& F0RYzQKaKzZ From dfad4865c908abb4b8b87c6a25d8d698b307c21f Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sat, 14 Dec 2024 13:58:23 +0100 Subject: [PATCH 33/49] ready for merge back --- rowers/dataprep.py | 4 +- rowers/forms.py | 20 +++++--- rowers/integrations/intervals.py | 1 + rowers/models.py | 2 +- rowers/templates/rower_exportsettings.html | 53 +++++++++------------ rowers/tests/testdata/testdata.tcx.gz | Bin 4000 -> 3999 bytes rowers/uploads.py | 36 +++++++------- rowers/views/userviews.py | 1 + rowers/views/workoutviews.py | 11 ----- static/img/intervals_logo_with_name.png | Bin 0 -> 18456 bytes 10 files changed, 61 insertions(+), 67 deletions(-) create mode 100644 static/img/intervals_logo_with_name.png diff --git a/rowers/dataprep.py b/rowers/dataprep.py index 827a9717..28bf84be 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -369,7 +369,7 @@ def workout_summary_to_df( return df -def resample(id, r, parent, overwrite='copy'): +def resample(id, r, parent, overwrite=False): data, row = getrowdata_db(id=id) messages = [] @@ -393,7 +393,7 @@ def resample(id, r, parent, overwrite='copy'): data['pace'] = data['pace'] / 1000. data['time'] = data['time'] / 1000. - if overwrite == 'overwrite': + if overwrite == True: # remove CP data try: cpfile = 'media/cpdata_{id}.parquet.gz'.format(id=parent.id) diff --git a/rowers/forms.py b/rowers/forms.py index 0ebc5fb3..506f900f 100644 --- a/rowers/forms.py +++ b/rowers/forms.py @@ -67,13 +67,12 @@ class FlexibleDecimalField(forms.DecimalField): class ResampleForm(forms.Form): - resamplechoices = ( - ('overwrite', 'Overwrite Workout'), - ('copy', 'Create a Duplicate Workout') - ) - + # add resamplechoice field, the result is a True or False boolean, labels are "overwrite" and "create copy" resamplechoice = forms.ChoiceField( - initial='copy', choices=resamplechoices, label='Copy behavior') + required=True, + choices=((True, 'overwrite'), (False, 'create copy')), + label='Resample choice', + widget=forms.RadioSelect) class TrainingZonesForm(forms.Form): @@ -582,6 +581,11 @@ class UploadOptionsForm(forms.Form): races = VirtualRace.objects.filter( registration_closure__gt=timezone.now()) + # set upload_to_X based on r.X_auto_export + for field in ['C2', 'Strava', 'SportTracks', 'TrainingPeaks', 'Intervals']: + if getattr(r, field.lower()+'_auto_export') and r.rowerplan in ['pro', 'plan','coach']: + self.fields['upload_to_'+field].initial = True + registrations = IndoorVirtualRaceResult.objects.filter( race__in=races, userid=r.id) @@ -665,6 +669,10 @@ class TeamUploadOptionsForm(forms.Form): upload_to_TrainingPeaks = forms.BooleanField(initial=False, required=False, label='Export to TrainingPeaks') + + upload_to_Intervals = forms.BooleanField(initial=False, + required=False, + label='Export to TrainingPeaks') # do_physics = forms.BooleanField(initial=False,required=False,label='Power Estimate (OTW)') makeprivate = forms.BooleanField(initial=False, required=False, label='Make Workout Private') diff --git a/rowers/integrations/intervals.py b/rowers/integrations/intervals.py index 1cdcbfb0..7db883ae 100644 --- a/rowers/integrations/intervals.py +++ b/rowers/integrations/intervals.py @@ -17,6 +17,7 @@ import os from uuid import uuid4 from django.utils import timezone from datetime import timedelta +import rowers.dataprep as dataprep from rowsandall_app.settings import ( INTERVALS_CLIENT_ID, INTERVALS_REDIRECT_URI, INTERVALS_CLIENT_SECRET, SITE_URL diff --git a/rowers/models.py b/rowers/models.py index cd2500f7..047d8d0a 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -4561,6 +4561,7 @@ class RowerExportForm(ModelForm): 'rp3_auto_import', 'intervals_auto_import', 'intervals_auto_export', + 'intervals_resample_to_1s' ] class RowerExportFormStrava(ModelForm): @@ -4616,7 +4617,6 @@ class RowerExportFormTrainingPeaks(ModelForm): model = Rower fields = [ 'trainingpeaks_auto_export', - 'rp3_auto_import', ] class RowerExportFormRP3(ModelForm): diff --git a/rowers/templates/rower_exportsettings.html b/rowers/templates/rower_exportsettings.html index 0d7d3858..13418d15 100644 --- a/rowers/templates/rower_exportsettings.html +++ b/rowers/templates/rower_exportsettings.html @@ -64,13 +64,12 @@ {% endif %}
  • -

    Intervals.icu

    +

    NK

  • - {{ forms.intervals.as_table }} + {{ forms.nk.as_table }}
    -

    connect with intervals.icu

    +

    connect with NK Logbook

  • Concept2

    @@ -81,12 +80,27 @@

    connect with Concept2

  • -

    NK

    +

    RP3

    - {{ forms.nk.as_table }} + {{ forms.rp3.as_table }}
    -

    connect with NK Logbook

    +

    connect with RP3

    +
  • +
  • +

    Rojabo

    +

    connect with Rojabo

    +
  • +
  • +

    Intervals.icu

    + + {{ forms.intervals.as_table }} + +
    +

    connect with intervals.icu

  • SportTracks

    @@ -102,6 +116,8 @@ {{ forms.trainingpeaks.as_table }} +

    connect with Polar

  • Polar

    @@ -112,11 +128,6 @@

    connect with Polar

  • -
  • -

    Rojabo

    -

    connect with Rojabo

    -
  • Garmin Connect

    @@ -153,24 +164,6 @@

    -
  • -

    TrainingPeaks

    -
  • - {{ forms.trainingpeaks.as_table }} - -
    -

    connect with Polar

    -
  • -
  • -

    RP3

    - - {{ forms.rp3.as_table }} - -
    -

    connect with RP3

    -
  • {% if grants %}
  • Applications

    diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz index 3a4eca337f44e3c892acabb79b59b31fb7ee2684..dd7306f84921784d4337eb7cf60575abe39ca49f 100644 GIT binary patch delta 257 zcmV+c0sj7=AD<+lIJJZMCm_J{BTJ63+ zczBUGo?U#{_gR^Kd%F^qPsbfRc-_A`ySVb2 zJUBl;SsitK`stT1#^t%cI$8Ser`M~KmzO{HxAZ=yL$5rX4_lsg|4En3&Vs9OpTFL1 zwN9J8eA;ij;quXbasGoB>81~VU7jqv%`$PwpFTXgEBAHqq+1_9esuWrv&?4u=WhEi z=`DWt`t*-wx-)=#0K5MW7FWL5J^AfpI=r(*y;`T|@GzlVUOY-q2op#5N H#DD<+W`c@3 delta 258 zcmV+d0sa1;AD|xxABzYG+aFx92dNH!vHLS{=(;6*cijJQu-F}NXLqKLzcGKl{IuGA zfAH`kaXh>Du$`N`ZufR2DxZ!!c<{P^b#`&( zHFmSi+Z(A&*5Q0xx9Fkp2i>P(ua5c2dhPx I_QZez0LCki$p8QV diff --git a/rowers/uploads.py b/rowers/uploads.py index 71f07553..fae902d5 100644 --- a/rowers/uploads.py +++ b/rowers/uploads.py @@ -245,23 +245,6 @@ def do_sync(w, options, quick=False): dologging('c2_log.log','Error C2') pass - if do_strava_export: # pragma: no cover - strava_integration = StravaIntegration(w.user.user) - try: - id = strava_integration.workout_export(w) - dologging( - 'strava_export_log.log', - 'exporting workout {id} as {type}'.format( - id=w.id, - type=w.workouttype, - ) - ) - except NoTokenError: # pragma: no cover - id = 0 - message = "Please connect to Strava first" - except Exception as e: - dologging('stravalog.log', e) - if do_icu_export: intervals_integration = IntervalsIntegration(w.user.user) try: @@ -334,4 +317,23 @@ def do_sync(w, options, quick=False): dologging('tp_export.log','No Token Error') return 0 + # we do Strava last. + if do_strava_export: # pragma: no cover + strava_integration = StravaIntegration(w.user.user) + try: + id = strava_integration.workout_export(w) + dologging( + 'strava_export_log.log', + 'exporting workout {id} as {type}'.format( + id=w.id, + type=w.workouttype, + ) + ) + except NoTokenError: # pragma: no cover + id = 0 + message = "Please connect to Strava first" + except Exception as e: + dologging('stravalog.log', e) + + return 1 diff --git a/rowers/views/userviews.py b/rowers/views/userviews.py index d2d22e25..cf004032 100644 --- a/rowers/views/userviews.py +++ b/rowers/views/userviews.py @@ -465,6 +465,7 @@ def rower_exportsettings_view(request, userid=0): 'rp3_auto_import': 'rp3token', 'nk_auto_import': 'nktoken', 'intervals_auto_export': 'intervals_token', + 'intervals_resample_to_1s': 'intervals_token', } r = getrequestrowercoachee(request, userid=userid) diff --git a/rowers/views/workoutviews.py b/rowers/views/workoutviews.py index 14112726..0412c7ff 100644 --- a/rowers/views/workoutviews.py +++ b/rowers/views/workoutviews.py @@ -5609,17 +5609,6 @@ def workout_upload_view(request, return response else: if not is_ajax: - if r.c2_auto_export and ispromember(r.user): # pragma: no cover - uploadoptions['upload_to_C2'] = True - - if r.strava_auto_export and ispromember(r.user): # pragma: no cover - uploadoptions['upload_to_Strava'] = True - - if r.sporttracks_auto_export and ispromember(r.user): # pragma: no cover - uploadoptions['upload_to_SportTracks'] = True - - if r.trainingpeaks_auto_export and ispromember(r.user): # pragma: no cover - uploadoptions['upload_to_TrainingPeaks'] = True form = DocumentsForm(initial=docformoptions) optionsform = UploadOptionsForm(initial=uploadoptions, diff --git a/static/img/intervals_logo_with_name.png b/static/img/intervals_logo_with_name.png new file mode 100644 index 0000000000000000000000000000000000000000..b448dd4e6a12f8d5d9b8888035ce6ea20d3f3f26 GIT binary patch literal 18456 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 literal 0 HcmV?d00001 From 0cf8704bd6bddb87b7644d810fe09198cb1cb6e5 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sat, 14 Dec 2024 14:14:40 +0100 Subject: [PATCH 34/49] tested --- rowers/tests/testdata/testdata.tcx.gz | Bin 3999 -> 4001 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 dd7306f84921784d4337eb7cf60575abe39ca49f..f05208b7635b1a0be253904dc3acd3f4a6c0d0bb 100644 GIT binary patch literal 4001 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 H#DD<+r2IT+ literal 3999 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 F0RaAiDCqzI From 915e562a76e520aaa8a2933f94f36611a37af92d Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sat, 14 Dec 2024 15:53:35 +0100 Subject: [PATCH 35/49] opening analysis for rowers themselves --- rowers/tests/test_api.py | 21 +++++--- rowers/tests/testdata/testdata.tcx.gz | Bin 4001 -> 4001 bytes rowers/uploads.py | 8 +++ rowers/views/analysisviews.py | 73 +++++++++++++------------- 4 files changed, 59 insertions(+), 43 deletions(-) diff --git a/rowers/tests/test_api.py b/rowers/tests/test_api.py index c8608415..14f0429a 100644 --- a/rowers/tests/test_api.py +++ b/rowers/tests/test_api.py @@ -208,7 +208,8 @@ class StravaPrivacy(TestCase): # throw out "c2import", "nkimport", "stravaimport", "concept2import", "sporttracksimport" from the set workouts = set([w for w in workouts if w not in [ - 'upload', 'addmanual', 'c2import', 'polarimport', 'rp3import', 'nkimport', 'stravaimport', 'concept2import', 'sporttracksimport']]) + 'upload', 'addmanual', 'c2import', 'polarimport', 'rp3import', 'nkimport', 'stravaimport', 'concept2import', 'sporttracksimport', + 'intervalsimport']]) self.assertEqual(len(workouts),5) @@ -230,7 +231,8 @@ class StravaPrivacy(TestCase): # throw out "c2import", "nkimport", "stravaimport", "concept2import", "sporttracksimport" from the set workouts = set([w for w in workouts if w not in [ - 'upload', 'addmanual', 'c2import', 'polarimport', 'rp3import', 'nkimport', 'stravaimport', 'concept2import', 'sporttracksimport']]) + 'upload', 'addmanual', 'c2import', 'polarimport', 'rp3import', 'nkimport', 'stravaimport', 'concept2import', 'sporttracksimport', + 'intervalsimport']]) self.assertEqual(len(workouts),2) @@ -251,7 +253,8 @@ class StravaPrivacy(TestCase): # throw out "c2import", "nkimport", "stravaimport", "concept2import", "sporttracksimport" from the set workouts = set([w for w in workouts if w not in [ - 'upload', 'addmanual', 'c2import', 'polarimport', 'rp3import', 'nkimport', 'stravaimport', 'concept2import', 'sporttracksimport']]) + 'upload', 'addmanual', 'c2import', 'polarimport', 'rp3import', 'nkimport', 'stravaimport', 'concept2import', 'sporttracksimport', + 'intervalsimport']]) self.assertEqual(len(workouts),2) @@ -272,7 +275,8 @@ class StravaPrivacy(TestCase): # throw out "c2import", "nkimport", "stravaimport", "concept2import", "sporttracksimport" from the set workouts = set([w for w in workouts if w not in [ - 'upload', 'addmanual', 'c2import', 'polarimport', 'rp3import', 'nkimport', 'stravaimport', 'concept2import', 'sporttracksimport']]) + 'upload', 'addmanual', 'c2import', 'polarimport', 'rp3import', 'nkimport', 'stravaimport', 'concept2import', 'sporttracksimport', + 'intervalsimport']]) self.assertEqual(len(workouts),2) @@ -293,7 +297,8 @@ class StravaPrivacy(TestCase): # throw out "c2import", "nkimport", "stravaimport", "concept2import", "sporttracksimport" from the set workouts = set([w for w in workouts if w not in [ - 'upload', 'addmanual', 'c2import', 'polarimport', 'rp3import', 'nkimport', 'stravaimport', 'concept2import', 'sporttracksimport']]) + 'upload', 'addmanual', 'c2import', 'polarimport', 'rp3import', 'nkimport', 'stravaimport', 'concept2import', 'sporttracksimport', + 'intervalsimport']]) self.assertEqual(len(workouts),2) @@ -447,7 +452,8 @@ class StravaPrivacy(TestCase): # print all lines of response.content that contain '
  • Strava

    -

    - {{ forms.strava.as_p }} +

    Warning: API restrictions!

    +

    + {{ forms.strava.as_p }}

    connect with strava

    Strava Auto Import also imports activity changes on Strava to Rowsandall, except when you delete From bcb1d439caaba3cf66feddd802f67109143775a3 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sun, 15 Dec 2024 14:15:05 +0100 Subject: [PATCH 40/49] tested --- rowers/tests/testdata/testdata.tcx.gz | Bin 4000 -> 4000 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 8a3a272d623fb3a4896a09bda71dd11dba428ede..45ab3cec298b0eddcb0d98c86b97664587ef734c 100644 GIT binary patch literal 4000 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 GfB^uCm@x7H literal 4000 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 GfB^s Date: Sun, 15 Dec 2024 15:57:42 +0100 Subject: [PATCH 41/49] fixing resample issue --- rowers/dataprep.py | 12 ++++++++++++ rowers/tests/testdata/testdata.tcx.gz | Bin 4000 -> 3999 bytes rowers/views/workoutviews.py | 1 + 3 files changed, 13 insertions(+) diff --git a/rowers/dataprep.py b/rowers/dataprep.py index 95fe1fc1..a824f571 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -374,6 +374,18 @@ def workout_summary_to_df( def resample(id, r, parent, overwrite=False): data, row = getrowdata_db(id=id) + rowdata = rrdata(csvfile=parent.csvfilename).df + # drop all columns except ' latitude' and ' longitude' and 'TimeStamp (sec)' from rowdata + allowedcolumns = [' latitude', ' longitude', 'TimeStamp (sec)'] + rowdata = rowdata.filter(allowedcolumns) + rowdata.rename(columns={'TimeStamp (sec)': 'time'}, inplace=True) + rowdata['time'] = (rowdata['time']-rowdata.loc[0,'time'])*1000. + rowdata.set_index('time', inplace=True) + data.set_index('time', inplace=True) + rowdata_interpolated = rowdata.reindex(data.index.union(rowdata.index)).interpolate('index') + data = data.merge(rowdata_interpolated, left_index=True, right_index=True, how='left') + data = data.reset_index() + messages = [] # resample diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz index 45ab3cec298b0eddcb0d98c86b97664587ef734c..eca15abb16d9035b8d6b2a4d682b2567cff2466b 100644 GIT binary patch literal 3999 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 F0RWJwKh6LE literal 4000 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 GfB^uCm@x7H diff --git a/rowers/views/workoutviews.py b/rowers/views/workoutviews.py index 40bd1f56..b5e57156 100644 --- a/rowers/views/workoutviews.py +++ b/rowers/views/workoutviews.py @@ -4700,6 +4700,7 @@ def workout_map_view(request, id=0): u = w.user.user r = getrower(u) rowdata = rdata(csvfile=f1) + hascoordinates = 1 if rowdata != 0: try: From 2079961b761af86b8685b4db838865c51aee884b Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sun, 15 Dec 2024 18:08:29 +0100 Subject: [PATCH 42/49] excluding strava from API calls --- rowers/urls.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rowers/urls.py b/rowers/urls.py index 06047059..8d673423 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -80,7 +80,8 @@ 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).order_by("-date", "-starttime") + return Workout.objects.filter(user=r).exclude(workoutsource='strava').order_by("-date", "-starttime") except TypeError: return [] From 4c3dfccad673251ced3c8a021096f0d773a3717e Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sun, 15 Dec 2024 19:04:09 +0100 Subject: [PATCH 43/49] fe --- rowers/tests/testdata/testdata.tcx.gz | Bin 3999 -> 4001 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 eca15abb16d9035b8d6b2a4d682b2567cff2466b..5c4f915d39460c7e965ff03b969ce83453b005cb 100644 GIT binary patch literal 4001 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 H#DD<+QU*Ul literal 3999 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 F0RWJwKh6LE From aee01a1a4aee9001ee384ebf4e2bbd60034dcfa9 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sun, 15 Dec 2024 19:27:11 +0100 Subject: [PATCH 44/49] fix --- rowers/integrations/trainingpeaks.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rowers/integrations/trainingpeaks.py b/rowers/integrations/trainingpeaks.py index 8dbefd35..93a73089 100644 --- a/rowers/integrations/trainingpeaks.py +++ b/rowers/integrations/trainingpeaks.py @@ -67,7 +67,10 @@ class TPIntegration(SyncIntegration): except TypeError: newnotes = 'from '+w.workoutsource+' via rowsandall.com' - row.exporttotcx(tcxfilename, notes=newnotes, sport=tpmapping[w.workouttype]) + try: + row.exporttotcx(tcxfilename, notes=newnotes, sport=tpmapping[w.workouttype]) + except KeyError: + row.exporttotcx(tcxfilename, notes=newnotes, sport='other') return tcxfilename From f5cc332a885064dca2e19713d208e4ae1ca59770 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Mon, 16 Dec 2024 08:07:14 +0100 Subject: [PATCH 45/49] fixing check factor schema errors --- rowers/dataroutines.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/rowers/dataroutines.py b/rowers/dataroutines.py index d34a1a3e..a47521d5 100644 --- a/rowers/dataroutines.py +++ b/rowers/dataroutines.py @@ -1648,10 +1648,14 @@ def read_data(columns, ids=[], doclean=True, workstrokesonly=True, debug=False, datadf = pl.concat(data) existing_columns = [col for col in columns if col in datadf.columns] datadf = datadf.select(existing_columns) - except (ShapeError, SchemaError, ColumnNotFoundError): - data = [ - df.select(columns) - for df in data] + except (ShapeError, SchemaError): + try: + data = [ + df.select(columns) + for df in data] + except ColumnNotFoundError: + existing_columns = [col for col in columns if col in df.columns] + df = df.select(existing_columns) # float columns floatcolumns = [] @@ -1686,14 +1690,19 @@ def read_data(columns, ids=[], doclean=True, workstrokesonly=True, debug=False, ] except ComputeError: pass + except ColumnNotFoundError: + pass try: datadf = pl.concat(data) except SchemaError: - data = [ - df.with_columns(cs.integer().cast(pl.Float64)) for df in data - ] - datadf = pl.concat(data) + try: + data = [ + df.with_columns(cs.integer().cast(pl.Float64)) for df in data + ] + datadf = pl.concat(data) + except ShapeError: + return pl.DataFrame() From 4ae3f34eec5d55ecca1f12695ae197a2594a9dde Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Mon, 16 Dec 2024 15:57:01 +0100 Subject: [PATCH 46/49] support for external_id on intervals.icu --- rowers/integrations/intervals.py | 2 ++ rowers/tests/testdata/testdata.tcx.gz | Bin 4001 -> 3999 bytes 2 files changed, 2 insertions(+) diff --git a/rowers/integrations/intervals.py b/rowers/integrations/intervals.py index e0692a57..66fdb0d5 100644 --- a/rowers/integrations/intervals.py +++ b/rowers/integrations/intervals.py @@ -18,6 +18,7 @@ from uuid import uuid4 from django.utils import timezone from datetime import timedelta 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 @@ -171,6 +172,7 @@ class IntervalsIntegration(SyncIntegration): params = { 'name': workout.name, 'description': workout.notes, + 'external_id': encoder.encode_hex(workout.id), } diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz index 5c4f915d39460c7e965ff03b969ce83453b005cb..3a360014e9c5a18ce48318a53c9853f7c09325d9 100644 GIT binary patch literal 3999 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 F0RZ{uCaC}b literal 4001 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 H#DD<+QU*Ul From 64d47a1cc090916860da4aa5eeff9e493f6b80fc Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Mon, 16 Dec 2024 18:27:14 +0100 Subject: [PATCH 47/49] fix c2 import --- rowers/tasks.py | 6 ++++-- rowers/tests/testdata/testdata.tcx.gz | Bin 3999 -> 4000 bytes 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/rowers/tasks.py b/rowers/tasks.py index a43a225d..ebadcfee 100644 --- a/rowers/tasks.py +++ b/rowers/tasks.py @@ -3695,7 +3695,8 @@ def handle_c2_async_workout(alldata, userid, c2token, c2id, delaysec, code=uuid4().hex[:16], c2id=c2id) startdatetime, starttime, workoutdate, duration, starttimeunix, timezone = utils.get_startdatetime_from_c2data( - data) + data + ) s = 'Time zone {timezone}, startdatetime {startdatetime}, duration {duration}'.format( timezone=timezone, startdatetime=startdatetime, @@ -3755,6 +3756,7 @@ def handle_c2_async_workout(alldata, userid, c2token, c2id, delaysec, strokelength = np.zeros(nr_rows) dist2 = 0.1*strokedata.loc[:, 'd'] + cumdist, intervals = make_cumvalues(dist2) try: spm = strokedata.loc[:, 'spm'] @@ -3796,7 +3798,7 @@ def handle_c2_async_workout(alldata, userid, c2token, c2id, delaysec, ' lapIdx': lapidx, ' WorkoutState': 4, ' ElapsedTime (sec)': seconds, - 'cum_dist': dist2 + 'cum_dist': cumdist }) df.sort_values(by='TimeStamp (sec)', ascending=True) diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz index 3a360014e9c5a18ce48318a53c9853f7c09325d9..1d35a89516fb6593ac599f54857c2b02232fadfe 100644 GIT binary patch literal 4000 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 F0RZ{uCaC}b From 6abc5680c2911ddf2bb18644e675485041249f26 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Mon, 16 Dec 2024 18:42:17 +0100 Subject: [PATCH 48/49] fi --- rowers/tests/testdata/testdata.tcx.gz | Bin 4000 -> 3998 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 1d35a89516fb6593ac599f54857c2b02232fadfe..7c4266eaeae9532ad1251eccd487b50f91d9ee0e 100644 GIT binary patch literal 3998 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 literal 4000 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 Date: Thu, 19 Dec 2024 15:51:17 +0100 Subject: [PATCH 49/49] adding privacy default on import or upload --- rowers/models.py | 25 +++++++++++++++-- rowers/templates/panel_comments.html | 8 ++++-- rowers/templates/rower_exportsettings.html | 8 ++++++ rowers/templates/rower_form.html | 1 + rowers/templates/workout_form.html | 31 +++++++++++---------- rowers/tests/testdata/testdata.tcx.gz | Bin 3998 -> 3999 bytes rowers/views/statements.py | 2 +- rowers/views/userviews.py | 11 +++++++- rowers/views/workoutviews.py | 10 ++++--- 9 files changed, 72 insertions(+), 24 deletions(-) diff --git a/rowers/models.py b/rowers/models.py index 89069ca1..c67ad31c 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -21,7 +21,7 @@ from django.forms import ModelForm from django.dispatch import receiver from django.forms.widgets import SplitDateTimeWidget, SelectDateWidget from django.forms.formsets import BaseFormSet - +from django.db.models.signals import post_save from django.contrib.admin.widgets import AdminDateWidget, AdminTimeWidget, AdminSplitDateTime import os @@ -1255,6 +1255,8 @@ class Rower(models.Model): getemailnotifications = models.BooleanField(default=False, verbose_name='Receive email notifications') + imports_are_private = models.BooleanField(default=False, verbose_name='Make imports private by default') + # Friends/Team friends = models.ManyToManyField("self", blank=True) mycoachgroup = models.ForeignKey( @@ -3780,6 +3782,15 @@ class Workout(models.Model): super(Workout, self).save(*args, **kwargs) + @classmethod + def post_create(cls, sender, instance, created, *args, **kwargs): + if created: + user = instance.user + if user.imports_are_private: + instance.privacy = 'hidden' + instance.save() + + def __str__(self): try: @@ -3838,6 +3849,8 @@ class Workout(models.Model): return stri +post_save.connect(Workout.post_create, sender=Workout) + class WorkoutRPEForm(ModelForm): class Meta: model = Workout @@ -4581,7 +4594,15 @@ class RowerExportForm(ModelForm): 'rp3_auto_import', 'intervals_auto_import', 'intervals_auto_export', - 'intervals_resample_to_1s' + 'intervals_resample_to_1s', + 'imports_are_private' + ] + +class RowerPrivateImportForm(ModelForm): + class Meta: + model = Rower + fields = [ + 'imports_are_private' ] class RowerExportFormStrava(ModelForm): diff --git a/rowers/templates/panel_comments.html b/rowers/templates/panel_comments.html index 09d0d0f0..2d4fd917 100644 --- a/rowers/templates/panel_comments.html +++ b/rowers/templates/panel_comments.html @@ -11,12 +11,16 @@ Distance:{{ workout.distance }}m Duration:{{ workout.duration |durationprint:"%H:%M:%S.%f" }} - + + {% if workout.privacy != 'hidden' %} + Public link to this workout https://rowsandall.com/rowers/workout/{{ workout.id|encode }} - + + {% endif %} + Comments Comment ({{ aantalcomments }}) diff --git a/rowers/templates/rower_exportsettings.html b/rowers/templates/rower_exportsettings.html index 35c5b974..56c82066 100644 --- a/rowers/templates/rower_exportsettings.html +++ b/rowers/templates/rower_exportsettings.html @@ -46,6 +46,14 @@

    Click on the icons to establish the connection or to renew the authorization.

    +

    + By default, imported workouts are set to have a public URL. However, new workouts can be set to + private by default with the following setting: + + {{ forms.imports_are_private.as_table }} + +
    +

  • API Key

    diff --git a/rowers/templates/rower_form.html b/rowers/templates/rower_form.html index 4912dd0e..1dd250ba 100644 --- a/rowers/templates/rower_form.html +++ b/rowers/templates/rower_form.html @@ -35,6 +35,7 @@
    {{ userform.as_table }} + {{ privateform.as_table }} {{ accountform.as_table }} diff --git a/rowers/templates/workout_form.html b/rowers/templates/workout_form.html index 98034e21..16c461fa 100644 --- a/rowers/templates/workout_form.html +++ b/rowers/templates/workout_form.html @@ -150,25 +150,28 @@ - + + {% if workout.privacy != 'hidden' %} + - {% for course in courses %} - - - - - {% endfor %} -
     Duration:{{ workout.duration |durationprint:"%H:%M:%S.%f" }}
    Source:{{ workout.workoutsource }}
    Public link to this workout: https://rowsandall.com/rowers/workout/{{ workout.id|encode }}/
    - Timed Course: - - {{ course }} -
    -
  • -
  • + {% endif %} + {% for course in courses %} + + + Timed Course: + + + {{ course }} + + +{% endfor %} + +
  • +
  • {% if form.errors %}

    Please correct the error{{ form.errors|pluralize }} below. diff --git a/rowers/tests/testdata/testdata.tcx.gz b/rowers/tests/testdata/testdata.tcx.gz index 7c4266eaeae9532ad1251eccd487b50f91d9ee0e..637692e1db30ba03033cef5c06601f1a450f6224 100644 GIT binary patch delta 257 zcmV+c0sj7;ADkX|?&{?(IrcJ{@=P;C27%?BdF6 z^5FdZWOdZ_>8D@57?SXD=pI)y{US9s(-_rY-4!!blK5Ti~{U=>AI}5IGe*Sv5 z)jDnV@@c>AhRa9$#rY3jq?(f7$>COP|0qp)iSX}vH_vE*a>F~}L^=h4-!^4DfdGRPcjX%<*5AXaB@K;b& H#DD<+lb@ez delta 256 zcmV+b0ssD=AD$lvABzYGpJQOL2dNH!j%>id0pYvj{)dCb?tnYHGkyGx`Sazc)$aR) zhZl+C*~N!_pLOZ&xj1{f>DDj1lauY!N*`D2Xw<}Tkblky%*Zr%riz~0m zgY)x~)lt`{pMLpbT%P-@lcn!|dc8V%dHHjHOYdVk^vc8eu;pp@pLEIWEV%k_`Rm$KU+r~S4YE+6d|=RbInZu;=o<;k+!EE9+P>BFPDa$g5ey7lqnM~6Q@%WSrP?zaDu z-r{GkPybk^I|H}}u>1dDapjBMlixn3!#i8lt95z~4-?Ae#iR5z{z#WTyz@V!09Jj( GfB^s&L715U diff --git a/rowers/views/statements.py b/rowers/views/statements.py index 07f2ceed..e0b574a1 100644 --- a/rowers/views/statements.py +++ b/rowers/views/statements.py @@ -186,7 +186,7 @@ from rowers.models import ( RowerPowerForm, RowerHRZonesForm, SimpleRowerPowerFo RowerExportFormSportTracks, RowerExportFormTrainingPeaks, RowerExportFormConcept2, RowerExportFormGarmin, RowerExportFormIntervals, RowerExportFormRP3, - RowerExportFormNK, + RowerExportFormNK, RowerPrivateImportForm, ) from rowers.models import ( FavoriteForm, BaseFavoriteFormSet, SiteAnnouncement, BasePlannedSessionFormSet, diff --git a/rowers/views/userviews.py b/rowers/views/userviews.py index cf004032..61f2a9c9 100644 --- a/rowers/views/userviews.py +++ b/rowers/views/userviews.py @@ -479,6 +479,7 @@ def rower_exportsettings_view(request, userid=0): 'intervals': RowerExportFormIntervals(instance=r), 'nk': RowerExportFormNK(instance=r), 'garmin': RowerExportFormGarmin(instance=r), + 'imports_are_private': RowerPrivateImportForm(instance=r) } if request.method == 'POST': @@ -493,6 +494,7 @@ def rower_exportsettings_view(request, userid=0): 'intervals': RowerExportFormIntervals(request.POST, instance=r), 'nk': RowerExportFormNK(request.POST, instance=r), 'garmin': RowerExportFormGarmin(request.POST, instance=r), + 'imports_are_private': RowerPrivateImportForm(request.POST, instance=r), } if form.is_valid(): cd = form.cleaned_data @@ -593,11 +595,13 @@ def rower_edit_view(request, rowerid=0, userid=0, message=""): if request.method == 'POST': accountform = AccountRowerForm(request.POST, instance=r) userform = UserForm(request.POST, instance=r.user) + privateform = RowerPrivateImportForm(request.POST, instance=r) - if accountform.is_valid() and userform.is_valid(): + if accountform.is_valid() and userform.is_valid() and privateform.is_valid(): # process cd = accountform.cleaned_data ucd = userform.cleaned_data + pcd = privateform.cleaned_data first_name = ucd['first_name'] last_name = ucd['last_name'] @@ -633,6 +637,7 @@ def rower_edit_view(request, rowerid=0, userid=0, message=""): resetbounce = True emailalternatives = cd['emailalternatives'] + imports_are_private = pcd['imports_are_private'] u.save() r.defaulttimezone = defaulttimezone @@ -644,6 +649,7 @@ def rower_edit_view(request, rowerid=0, userid=0, message=""): r.defaultlandingpage = defaultlandingpage r.showfavoritechartnotes = showfavoritechartnotes r.share_course_results = share_course_results + r.imports_are_private = imports_are_private r.sex = sex r.birthdate = birthdate r.autojoin = autojoin @@ -658,11 +664,13 @@ def rower_edit_view(request, rowerid=0, userid=0, message=""): accountform = AccountRowerForm(instance=r) userform = UserForm(instance=u) + privateform = RowerPrivateImportForm(instance=r) successmessage = 'Account Information changed' messages.info(request, successmessage) else: accountform = AccountRowerForm(instance=r) userform = UserForm(instance=r.user) + privateform = RowerPrivateImportForm(instance=r) grants = AccessToken.objects.filter(user=request.user) try: @@ -678,6 +686,7 @@ def rower_edit_view(request, rowerid=0, userid=0, message=""): 'grants': grants, 'userform': userform, 'accountform': accountform, + 'privateform': privateform, 'rower': r, 'apikey': apikey.key, }) diff --git a/rowers/views/workoutviews.py b/rowers/views/workoutviews.py index b5e57156..98257c99 100644 --- a/rowers/views/workoutviews.py +++ b/rowers/views/workoutviews.py @@ -687,7 +687,7 @@ def addmanual_view(request, raceid=0): empowerside = form.cleaned_data.get('empowerside','port') if private: # pragma: no cover - privacy = 'private' + privacy = 'hidden' else: privacy = 'visible' @@ -4270,7 +4270,7 @@ def workout_flexchart_stacked_view(request, *args, **kwargs): def workout_unsubscribe_view(request, id=0): w = get_workout(id) - if w.privacy == 'private' and w.user.user != request.user: # pragma: no cover + if w.privacy == 'hidden' and w.user.user != request.user: # pragma: no cover return HttpResponseForbidden("Permission error") comments = WorkoutComment.objects.filter(workout=w, @@ -4300,7 +4300,7 @@ def workout_unsubscribe_view(request, id=0): def workout_comment_view(request, id=0): w = get_workout(id) - if w.privacy == 'private' and w.user.user != request.user: # pragma: no cover + if w.privacy == 'hidden' and w.user.user != request.user: # pragma: no cover return HttpResponseForbidden("Permission error") comments = WorkoutComment.objects.filter(workout=w).order_by("created") @@ -4490,7 +4490,7 @@ def workout_edit_view(request, id=0, message="", successmessage=""): if private: - privacy = 'private' + privacy = 'hidden' else: # pragma: no cover privacy = 'visible' @@ -5197,6 +5197,8 @@ def workout_upload_view(request, is_ajax = False r = getrower(request.user) + if r.imports_are_private: + uploadoptions['makeprivate'] = True if r.rowerplan == 'freecoach': # pragma: no cover url = reverse('team_workout_upload_view') return HttpResponseRedirect(url)