From 72e1083a2d18906b470c5c3c3621171fb7136f49 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Mon, 4 Feb 2019 17:06:51 +0100 Subject: [PATCH 01/16] starting plan view tests --- rowers/models.py | 44 ++---- rowers/tests/test_plans.py | 211 ++++++++++++++++++++++++++ rowers/tests/testdata/testdata.csv.gz | Bin 11426 -> 11426 bytes rowers/utils.py | 14 ++ rowers/views.py | 14 +- 5 files changed, 242 insertions(+), 41 deletions(-) diff --git a/rowers/models.py b/rowers/models.py index 76a535bd..e5947fb1 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -246,7 +246,7 @@ class PowerTimeFitnessMetric(models.Model): ('water','On the water') ) - date = models.DateField(default=timezone.now) + date = models.DateField(default=datetime.date.today) last_workout = models.IntegerField(default=0) user = models.ForeignKey(User) PowerFourMin = models.FloatField(default=0) @@ -334,7 +334,7 @@ class TeamForm(ModelForm): class TeamInvite(models.Model): team = models.ForeignKey(Team) user = models.ForeignKey(User,null=True) - issuedate = models.DateField(default=timezone.now) + issuedate = models.DateField(default=datetime.date.today) code = models.CharField(max_length=150,unique=True) email = models.CharField(max_length=150,null=True,blank=True) @@ -352,7 +352,7 @@ class TeamInviteForm(ModelForm): class TeamRequest(models.Model): team = models.ForeignKey(Team) user = models.ForeignKey(User,null=True) - issuedate = models.DateField(default=timezone.now) + issuedate = models.DateField(default=datetime.date.today) code = models.CharField(max_length=150,unique=True) from utils import ( @@ -655,8 +655,8 @@ class Rower(models.Model): paidplan = models.ForeignKey(PaidPlan,null=True,default=None) - planexpires = models.DateField(default=timezone.now) - teamplanexpires = models.DateField(default=timezone.now) + planexpires = models.DateField(default=datetime.date.today) + teamplanexpires = models.DateField(default=datetime.date.today) clubsize = models.IntegerField(default=0) protrialexpires = models.DateField(blank=True,null=True) plantrialexpires = models.DateField(blank=True,null=True) @@ -1022,10 +1022,10 @@ class GeoPoint(models.Model): def half_year_from_now(): - return timezone.now()+timezone.timedelta(days=182) + return (timezone.now()+timezone.timedelta(days=182)).date() def a_week_from_now(): - return timezone.now()+timezone.timedelta(days=7) + return (timezone.now()+timezone.timedelta(days=7)).date() # models related to training planning - draft # Do we need a separate class TestTarget? @@ -1089,20 +1089,6 @@ class TrainingTargetForm(ModelForm): -# SportTracks has a TrainingGoal like this -#class TrainingGoal(models.Model): -# rower = models.ForeignKey(Rower) -# name = models.CharField(max_length=150,blank=True) -# startdate = models.DateField(default=timezone.now) -# enddate = models.DateField( -# default=timezone.now()+datetime.timedelta(days=28)) -# goalmetric = models.CharField(max_length=150,default='rower', -# choices = modechoices) -# value = models.IntegerValue(default=1) - -# I think we can use PlannedSession for that (in challenge mode) -# although such a TrainingGoal could have automatically calculated -# values without needing the user to assign class TrainingPlan(models.Model): @@ -1118,7 +1104,7 @@ class TrainingPlan(models.Model): name = models.CharField(max_length=150,blank=True) status = models.BooleanField(default=True,verbose_name='Active') target = models.ForeignKey(TrainingTarget,blank=True,null=True) - startdate = models.DateField(default=timezone.now) + startdate = models.DateField(default=datetime.date.today) enddate = models.DateField( default=half_year_from_now) @@ -1482,7 +1468,7 @@ def macrocyclecheckdates(plan): class TrainingMacroCycle(models.Model): plan = models.ForeignKey(TrainingPlan) name = models.CharField(max_length=150,blank=True) - startdate = models.DateField(default=timezone.now) + startdate = models.DateField(default=datetime.date.today) enddate = models.DateField( default=half_year_from_now) notes = models.TextField(max_length=300,blank=True) @@ -1568,7 +1554,7 @@ class TrainingMacroCycleForm(ModelForm): class TrainingMesoCycle(models.Model): plan = models.ForeignKey(TrainingMacroCycle) name = models.CharField(max_length=150,blank=True) - startdate = models.DateField(default=timezone.now) + startdate = models.DateField(default=datetime.date.today) enddate = models.DateField( default=half_year_from_now) notes = models.TextField(max_length=300,blank=True) @@ -1643,7 +1629,7 @@ class TrainingMesoCycle(models.Model): class TrainingMicroCycle(models.Model): plan = models.ForeignKey(TrainingMesoCycle) name = models.CharField(max_length=150,blank=True) - startdate = models.DateField(default=timezone.now) + startdate = models.DateField(default=datetime.date.today) enddate = models.DateField( default=half_year_from_now) notes = models.TextField(max_length=300,blank=True) @@ -1790,7 +1776,7 @@ class PlannedSession(models.Model): comment = models.TextField(max_length=500,blank=True, ) - startdate = models.DateField(default=timezone.now, + startdate = models.DateField(default=datetime.date.today, verbose_name='On or After') enddate = models.DateField(default=a_week_from_now, @@ -3302,10 +3288,10 @@ class RowerForm(ModelForm): # An announcement that goes to the right of the workouts list # optionally sends a tweet to our twitter account class SiteAnnouncement(models.Model): - created = models.DateField(default=timezone.now) + created = models.DateField(default=datetime.date.today) announcement = models.TextField(max_length=280) - expires = models.DateField(default=timezone.now) - modified = models.DateField(default=timezone.now) + expires = models.DateField(default=datetime.date.today) + modified = models.DateField(default=datetime.date.today) dotweet = models.BooleanField(default=False) def save(self, *args, **kwargs): diff --git a/rowers/tests/test_plans.py b/rowers/tests/test_plans.py index d75f848c..b4541c31 100644 --- a/rowers/tests/test_plans.py +++ b/rowers/tests/test_plans.py @@ -1,6 +1,7 @@ #from __future__ import print_function from statements import * nu = datetime.datetime.now() +from rowers.utils import allmonths,allsundays import rowers.plannedsessions as plannedsessions @@ -988,3 +989,213 @@ class MandatoryTestCompleteTest(TestCase): response = self.c.get(url) self.assertEqual(response.status_code,200) +class PlannedSessionsView(TestCase): + def setUp(self): + # user + self.u = UserFactory() + + self.r = Rower.objects.create(user=self.u, + birthdate=faker.profile()['birthdate'], + gdproptin=True, + gdproptindate=timezone.now(), + rowerplan='coach') + self.r.save() + self.c = Client() + + # workouts + # workout 1 - 2019-01-13, rScore 69 + result = get_random_file(filename='rowers/tests/testdata/2019-01-13_session.csv',name='sprintervals') + + + self.factory = RequestFactory() + self.password = faker.word() + self.u.set_password(self.password) + self.u.save() + + self.w1 = Workout.objects.create( + name='sprintervals', + notes=faker.text(), + startdatetime = result['startdatetime'], + starttime = result['starttime'], + workouttype='rower', + date=result['date'], + duration=result['duration'], + distance=result['totaldist'], + csvfilename=result['filename'], + trimp = 77, + rscore = 69, + hrtss = 43, + normp = 236, + user=self.u.rower, + ) + + + # plan + self.target = TrainingTarget.objects.create( + name = faker.word(), + manager = self.u.rower, + notes = faker.text() + ) + self.target.rowers.add(self.u.rower) + self.target.save() + + self.plan = TrainingPlan.objects.create( + manager = self.u.rower, + name = faker.word(), + status=True, + target = self.target, + startdate=timezone.now().date(), + enddate = self.target.date, + ) + + self.plan.rowers.add(self.u.rower) + self.plan.save() + + # cycles + self.macro = TrainingMacroCycle.objects.create( + plan=self.plan, + name=faker.word(), + type='userdefined', + notes = faker.text(), + startdate = self.plan.startdate, + enddate = self.plan.enddate, + ) + + mesos = TrainingMesoCycle.objects.filter(plan=self.macro) + for m in mesos: + m.delete() + + monthstarts = [d for d in allmonths(self.macro.startdate,self.macro.enddate)] + monthstarts.append(self.macro.enddate) + + for i in range(len(monthstarts)-1): + firstday = monthstarts[i] + lastday = monthstarts[i+1]-datetime.timedelta(days=1) + if lastday < self.macro.enddate and i == len(monthstarts)-2: + lastday = self.macro.enddate + + meso = TrainingMesoCycle(startdate=firstday, + enddate=lastday, + plan=self.macro, + name = '%s' % firstday.strftime("%B"), + type = 'userdefined') + meso.save() + + mesos = TrainingMesoCycle.objects.filter(plan=self.macro) + + for cycle in mesos: + micros = TrainingMicroCycle.objects.filter(plan=cycle) + for m in micros: + m.delete() + + sundays = [s for s in allsundays(cycle.startdate,cycle.enddate)] + + if sundays and sundays[-1] < cycle.enddate: + sundays = sundays+[cycle.enddate] + elif not sundays: + sundays = [cycle.enddate] + + for i in range(len(sundays)): + if i==0: + monday = cycle.startdate + else: + monday = sundays[i]-datetime.timedelta(days=6) + if monday < cycle.startdate: + monday = cycle.startdate + + nextsunday = sundays[i] + + micro = TrainingMicroCycle(startdate=monday, + enddate=nextsunday, + plan=cycle, + name = 'Week %s' % monday.isocalendar()[1], + type='userdefined') + micro.save() + + + # sessions + startdatetime = self.w1.startdatetime + + startdate = (startdatetime-datetime.timedelta(days=1)).date() + enddate = (startdatetime+datetime.timedelta(days=1)).date() + preferreddate = startdatetime.date() + + self.ps_rscore = SessionFactory( + startdate=startdate,enddate=enddate, + sessiontype='test', + sessionmode = 'rScore', + criterium = 'none', + sessionvalue = 69, + sessionunit='None', + preferreddate=preferreddate, + manager=self.u, + ) + + + self.ps_rscore.save() + added = plannedsessions.add_rower_session(self.u.rower,self.ps_rscore) + + self.ps_dist = SessionFactory( + startdate=startdate,enddate=enddate, + sessiontype='test', + sessionmode = 'distance', + criterium = 'none', + sessionvalue = result['totaldist'], + sessionunit='m', + preferreddate=preferreddate, + manager=self.u, + ) + + + self.ps_dist.save() + added = plannedsessions.add_rower_session(self.u.rower,self.ps_dist) + + self.ps_time = SessionFactory( + startdate=startdate,enddate=enddate, + sessiontype='test', + sessionmode = 'time', + criterium = 'none', + sessionvalue = 38, + sessionunit='min', + preferreddate=preferreddate, + manager=self.u, + ) + + + self.ps_time.save() + added = plannedsessions.add_rower_session(self.u.rower,self.ps_time) + + self.ps_trimp = SessionFactory( + startdate=startdate,enddate=enddate, + sessiontype='test', + sessionmode = 'TRIMP', + criterium = 'none', + sessionvalue = 77, + sessionunit='none', + preferreddate=preferreddate, + manager=self.u, + ) + + + self.ps_trimp.save() + added = plannedsessions.add_rower_session(self.u.rower,self.ps_trimp) + + + def tearDown(self): + try: + os.remove(self.w1.csvfilename) + except (IOError, WindowsError): + pass + + def test_clone_view(self): + login = self.c.login(username=self.u.username, password=self.password) + self.assertTrue(login) + + url = '/rowers/sessions/{id}/clone/'.format(id=self.ps_trimp.id) + + response = self.c.get(url,follow=True) + self.assertEqual(response.status_code,200) + self.assertRedirects(response, + expected_url='/rowers/session/5/edit/user/1/?when=2019-02-04/2019-02-10', + status_code=302,target_status_code=200) + diff --git a/rowers/tests/testdata/testdata.csv.gz b/rowers/tests/testdata/testdata.csv.gz index 1c59396d5137da885ae4cb827f1c0ad6a69cf8c5..5f502e8ca719087571448f705f2ac526e5eb84b4 100644 GIT binary patch delta 15 WcmZ1!xhRrNzMF&NeEdeXIXVC=lLce| delta 15 WcmZ1!xhRrNzMF%iL1iP`9322Dk_6fS diff --git a/rowers/utils.py b/rowers/utils.py index 9e42917f..c17850bb 100644 --- a/rowers/utils.py +++ b/rowers/utils.py @@ -468,3 +468,17 @@ def get_strava_stream(r,metric,stravaid,series_type='time',fetchresolution='high s = requests.get(url,headers=headers) return s + +def allmonths(startdate,enddate): + d = startdate + while d Date: Tue, 5 Feb 2019 08:54:26 +0100 Subject: [PATCH 02/16] removed button green class from workout comment button --- rowers/templates/workout_comments.html | 2 +- rowers/tests/test_plans.py | 63 ++++++++++++++++++++++++- rowers/tests/testdata/testdata.csv.gz | Bin 11426 -> 11437 bytes 3 files changed, 62 insertions(+), 3 deletions(-) diff --git a/rowers/templates/workout_comments.html b/rowers/templates/workout_comments.html index 1eae9f00..0260a7ea 100644 --- a/rowers/templates/workout_comments.html +++ b/rowers/templates/workout_comments.html @@ -61,7 +61,7 @@ {{ form.as_table }} {% csrf_token %} - + {% for graph in graphs %} diff --git a/rowers/tests/test_plans.py b/rowers/tests/test_plans.py index b4541c31..3c7558fc 100644 --- a/rowers/tests/test_plans.py +++ b/rowers/tests/test_plans.py @@ -1192,10 +1192,69 @@ class PlannedSessionsView(TestCase): self.assertTrue(login) url = '/rowers/sessions/{id}/clone/'.format(id=self.ps_trimp.id) + today = datetime.date.today() + b = datetime.date.today()-timezone.timedelta(today.weekday()) + e = b+timezone.timedelta(days=6) + + expected_url = '/rowers/sessions/5/edit/user/1/?when={b}/{e}'.format( + b=b, + e=e + ) response = self.c.get(url,follow=True) self.assertEqual(response.status_code,200) self.assertRedirects(response, - expected_url='/rowers/session/5/edit/user/1/?when=2019-02-04/2019-02-10', + expected_url=expected_url, status_code=302,target_status_code=200) - + + def test_multiclone_view(self): + login = self.c.login(username=self.u.username, password=self.password) + self.assertTrue(login) + + def test_multicreate_view(self): + login = self.c.login(username=self.u.username, password=self.password) + self.assertTrue(login) + + # post something + + def test_teamcreate_view(self): + login = self.c.login(username=self.u.username, password=self.password) + self.assertTrue(login) + + def test_teamedit_view(self): + login = self.c.login(username=self.u.username, password=self.password) + self.assertTrue(login) + + def test_coach_view(self): + login = self.c.login(username=self.u.username, password=self.password) + self.assertTrue(login) + + def test_plannedsessions_view(self): + login = self.c.login(username=self.u.username, password=self.password) + self.assertTrue(login) + + def test_plannedsessions_print_view(self): + login = self.c.login(username=self.u.username, password=self.password) + self.assertTrue(login) + + def plannedsession_manage_view(self): + login = self.c.login(username=self.u.username, password=self.password) + self.assertTrue(login) + + def plannedsession_edit_view(self): + login = self.c.login(username=self.u.username, password=self.password) + self.assertTrue(login) + + + def plannedsession_detach_view(self): + login = self.c.login(username=self.u.username, password=self.password) + self.assertTrue(login) + + def plannedsession_view(self): + login = self.c.login(username=self.u.username, password=self.password) + self.assertTrue(login) + + def plannedsession_delete_view(self): + login = self.c.login(username=self.u.username, password=self.password) + self.assertTrue(login) + diff --git a/rowers/tests/testdata/testdata.csv.gz b/rowers/tests/testdata/testdata.csv.gz index 5f502e8ca719087571448f705f2ac526e5eb84b4..db3fe1e85e2a2f7ccfda1fa89bf15f33594dc9ce 100644 GIT binary patch literal 11437 zcmV;eEK<`SiwFp4Kv`S@|8!+@bYx+4VJ>5Hb^v{S+m38Ua@~7>1s`dkP?Oil%tw31 zf(;mhfn>ms#u>699}mex93GA&f4$d=h}@C8yCsZ7Hmj>D){cz(CHwO)KfeC!<9~ks z`SqW^efs&=@4o--^@soT`0mpW-#-8S^S6Kd??3+Qj9zUfc@6)*kv z`XfKKKk?tbeEt0YzWnm-(@)=h|MTm&*RQ|f#ee$t^~>L0drCk5<-<04_3Uj4gDg%F|R6lC8s0@R=_Ln> z^(;+6iW?8&e+hx$2{2V!)>|Xd7qn(XM!U9*_C} z%|<*RECHuJ*F!*yuj*TKYEyDu__F>iLuQ|kn$xq*K7f&ElvaAFfrjr?GAFokP*!dmaNQfTr`EwccU47 zJVq&?4%wPDwJZi^DQo%$Gl1H`;I+Y%Y7-Bc1&kklYBfJ54-!%eW{ZQVhGj12Oe2<| zSn$TSY6Y0umvUyc1?f>Vb@sj}#ob7y7YQ_Nqaj0|kkdY%__a`IH*0{V8X9~JIP%1^ z0S@paida%S6q&J{H+bu817cMMWWM03ddU^iQhOuYmI{s-vGg(mk33gaJcCL)pfy6( z=!+Wccp%}FU#I}2asp2&c2-jY51({&m2TpJxV1HreB&}|FoP)&^{2QUgt8Z^Hz!>|#Pk^`n3m|#lG z`ryS1$jWtocS}$32G)Y0s+wrh>4FazfLau!1_qV@l?Vr#S{xLP3>YUhA9k=o^IF9N zLcqb|jAMK#*Nq}@T{e2)9qO8B{J_~~0@bV{ieq1wIN{^9D=vHtKjW*wHsjXN6KoEw zDz~x&lhTH?x)P+YLQ){SI7I-1?!Y1?vMXkV3ShA1 ztKsP2rz+$Q?j|p)BQ&g}X~4RZ1hBB`m2O;dLEqq;KnxMj)N^dx=`}Sfu(LczFmo!d zeBc^tN=MUAmP_sjISFSXByfdwr`m}D!@0x0kR6lfiIXgw zVFcD`ae0#RP-~(#MFmVQ8$aza+GSJ0F9DW1@x2uuqr#g*?(q&pnZ;tK;S_-@%sf<3 z9M__c1dkXB5?98zZidnDcf~c;SrZpL)Nzo;FlAM#mp;P%BCu_cYzRIMctEJYrH-S) ziL|Z?Ih`be%&_*2=Fg%`L=1^Og&}5DhCz)2Sexn)o()xhe zABUyjNoIk6BCh2Wfh#ONl+fc%yog9DWewk(emR8P4-!(b=+K0dx`KXAsQ}(-i z$e}uUi5R0{L4rWjS_T8;iO+R_#7Qo7+a)_qAPbw1!yeU;)dwSCh!rAdvtlD}WYvzc zs5rwDv>VHMcq0}265a^5xIySw)hH{p5^p46^+vSeN)yP!>O&D-#u|+f;Wq;YH??u8 z0d!nUDa`7ZlVjWJ^KJ7Alm~n?g1}js#=y3ZnhInZK?XCLmGvyElLW3X`%uK>z&cNY z$JY}dxLWR~z`7cXT=giK04~P(8h(M)1*^IFyCe)*1SAm2UcRww!95@cD++591gx<7 zogPh52^9)-N?&gDWt^~z!-5fFVq?(`ah1R&{>hO12^EHbAuO-p$pFuKc_aGWNdi|G zeWVc8femH`^o$m>&>NLEz?!?GGEZP+*i0_@3=ziZjiw+|hUtgocba-+GmnlCzI4c> zO1?Nr01K-RHB9Ed@gj^io~%rb@@VxhW5I*haDq(06RO$g^IgW`GGbF5_$B#0L*GCR z;$0i!=TZyYj3ne&@a;thDb3%d^wHJoJ#jN7v_?@t=`R3=>^`#o)BOsxai7P76A z_R$uh#hj2jB zSyIR>=D|P9yR)>|0!Ko_a##y^q}f*Lbd?Njs!W6#A_(I;0&N1T$w>lOSbiEFi(T>J zD$La?++$H6PQw?-h9R4UmQs?e8>A zh|8KDWp$cB7KR_1n4D##ra5F~erwR=4YcK6ylTjw>qk6ul@2dn5%ZTZO#Df5P%jG$ zKxrz|n$rZdF#c0WU!jp2b1nQ-ool(B&?;jTMt?v*-f_)?{%I*fC;-A&=^eHekA*M` z85`6a2^~UnpjM`JCkbR>{->5F6dAuu7|PdCooVd~Fovq8BSTjFUe1aGGe>(J=MdPM zS;0F1LyVmI0;9;Yo?yWgnK(%x3;U0ER5uxN5Rf_s@FvVyBS~vjKO)V(ni_{s`D%r4e589r(zE)B zEIYEnO2<3*WJS$kRfcke|7jw#tDROH$yrN`eV_zjIRI-7nv7GLfK~&VMv7MP*W{4z zt>U?F&HFf~9cOco-qa5aIa9gj@mamc*>FM~?y^zRNS~pP+~aIQQ<{L*0@}LD_06h6 z13!tos9q1Q5nA7pc25Dq+)j;VTaGNVwFX64!W4i>89;z*lr-d)ssr=}N)pH}M=;^# z-FNauJS?QWbwid}3Ug!(8^w@KoNAMs<>;p);E9A$qh5A!D^ay!QR-Sq;*rsU#zY3% z99nm163`k6SeHW4;0O^NKkYx{MAjJ>GlBpz^#euT>wrEK-1k*1TMHo~Vr+i)s7e($ zQX~j$;I!@(0c-@Yv5T>M5$c0jr;T0A=sOV7fL1m#$l|AM*!=+>#@yBomAzytmhHs427m4JBhKfMriZH z99a8W5pORlE@Iox`ZfC|v#`L}pYZk~iflSbAX|ZqDsF8R^(-X1+yS1jSYe-*JwAGR zU-)c?f0_zQL-STTL}9R)e)CUIg-VTeTuL&%^c1){O9R?J(6R;lXAsM6(9W~WjtsV< zTm9!22(>uN8U=qOjaHH+!_Kor>I5UK6@NUTkp!~v0Y%$~?8a*J`XsL9F1?=8Up0}y z(bPvTQDXFH`@*GV8UR;AwgQGBE5Z?Xt+L>ujUr%$2dElr+96+1Linqxu{Lw{YKdkK>(6T4P+Ucy zQ&|4o>D(9xt~MPPPx`3%?|g0@!I>rJR)}YMJ}X1bh|Pqq1DUyICj{0ko$F!y=?x56 zZh_tWpy0^BNkHEor(Tvt~?>L=!%7AZ8@X5~F^O z1xSJyXI+E9#7P2Jn0+=*x&i}j(cBL4h(}iImYC!XjJ1gOHQDNs4`XaOg0j32%MQ~@ zm?BtIW+o&BUDbH8Z<;_BX1{6ug2>n~63RB)KFwIx-mt7W%OnNMJjA}Ftf=9?0w3hL z@Y{>MKuQ^7Z3K0QXK87x0-H`Uy-3*oUM!hodpnEHxF8SoTrT%r{;`b7fWg#B4k^1A zru6M+M`Bi-CPaO$v(bAtB)}#k31DINd$(LJYs+;g7$&FF!{h^4)8eo?(hmKW%Peu~R!t!?(NDW!$bF&X1*0riydOr@cYo~s9Tp%+M_Isr|Mc@kCkN0L5 zGB(DF9+aq%u(y8TVv&zg&&0QhM4wR3TAmdd4@(9qMv#~@2Z_!i<6+708>0wdVf@8w zd0G3evorb=w?|rjIgnLHMuRbK1>=0xWoOo-j42+RA`BL!pG_Rl?*(aqNxKlQBO5l2 zCSm;H4w|2(jj@3bG8w#T2mXx6h)jE!$b4nqa=3CLv%1u_7!czOr7VL@v*@iBPVe=_ z+5`{4NCH_{e+E&_ka1E_19@51yH}%}$ciK5X{!0JH!&L_W{6}iWE~a@(L|d4E@r~| zd)Cn&aLQ-`T9|*y(!Z+E8{u&nSsaA7qY! z08%T>Fr5uQ)0rcLK?0REV3bWWy^L@GLv6l7V*^$g42|5j3T#QES7g6EJJ&`I9l#S` zLk~EXuC4O?5z{CkbtbQ~ZP?W(Ga`4E zQF#|jA?zq3)r}6o1||;7bo1%}l-VY8a#}*L5-~SkXOR))Q=@M3L2dTpWrPEm!+JSc z`>%;T$f4tegmpP4#Wh(SqM2Pf9l3-58^@-~R~w-KrO6OhKdG7^1{X!E1IsoSf+pqI zrU_)>0HBT8kyRb3T%6&%V&#)%(j3}kFm{7hE_ZJBti^l*jvnDFWQ~o`uxG50Al{}k zAJr)WSa^U{teKOw2b&i46v>yCycR9pA!U^n8j^w_stf3H@~j_H7CsZcFX1S0IQ^8e zo6`iNj3%Ik4`2(&EG^rxxiAkJ-jzuaXDhVK8VyC7^??X9mb>%p?p$si00i>FmS$sX ztx>uX=b3LU0I&68^^YpJAhz%~5S=2~@AD{9a} z(rOoEN=d^rCK>pfA|r~#TxBMmmMZg#Xcd?^FvO-aMRc}- z4G%V^9_M9gCui{xov}aLY1U!*!Ly9qvEhR%Y12ujmze^YF#Sfvzz-eWjX&uWc0$2`U)9U|PLMeqq zv#bX-#dWz&#l>nHap6Ixxqw|Si!#e>7XQXst_cUwIGC^fB#t6MH@nJ&%KuDG5x6b~ zu$Zox17JO!SODMpL`5gR4~8uV2E#Yi1GJCa`MAj#9=t-%V8U#$x);v2++=KKrwCv* zfHiIfC@`DAK_0{8Q%uI{X=k5C)tOYXJhE|6PDh(|&maHr*{cuh!7BB>Y7ltt14DAuoc0N>Z7qnGY!CAwo zvFXQF#z2mQrHFq^LS zv?nNsnZ>bmu&J;*e>rqcO(n@%$OfN`B7lYc2T!}R3?QIWzy}t4=2Y{^vSz?0b!o+n zPqM z*~qGfja@?^4sztMvK|7>EoJ$h(Pa)9j8s@@6i)3=&hFk)I>0fKKo<7D%FY2AXY&ek zfK{z)o?>2>W#)i3A4?s;tY=$pH^17g2s&8+;^<7#t{gNpr8hNDz%i0Q79N1QXuuer zb@E|(s7-DixU`DON=tbxwf}NP+SdMc>7OwJH~0VM8O~-f51y&$&PH*Hz!eU_G*N-U zI&=;VJ(dO9?0oIfde*;L+_f4In0j=7cin7w&}d?CA!B7df0@Liw1po#kQK5Tvr-xMBTx0#k3jc#3zt3HdEp>tiU zt8=Od8ZvwnYy7Zr8<~;>vhV=?N}My;oL+29^F%gSj*LRc?HRBM_e)FPc56?Z5$S&)oN#o}%ll zj`)=&DvPFN>;aN zB|8WuN^s#9ILBu?(RDysCCZ09yRI4r>lmBrNmA^}g6mv{#2;$tC1za0v zL$OnBxMHGfF?tfiH|SFM2F47g6tHruYBxGf@xFLJPnJV%geT3Gtf>@o!dr2&HS8RK z!Eq)|iQY~YN!8>auM-uf~3#)a;17CzIlB>evo1mC!K`2RBsRzyaY@WNfJwhx!j=7u8sY_|@_ADU4% zK7Qd*J=&Tv+~`>p9o*BVW6jJSbY!Y5_SreGCt1hS>}h?vJIBWGg09qT@Tx5vlg(0n z6k_DltW5T9sEIrY;P!NprPgwRO9g?+O1hN8 z$fj`jJ@XP51TnTtKQ>jun*p!clw{Fe5Q~gU8Lk%BY_+nEokJx@3vf#;B&PUnD61U{ z^%DlSu_|{P%1WHJX>%C!=B7$?E6@$=GTYJxMU8JXlb}ZbKvxYNhrhDz7vEMhR`Xb3 z=@j90urMZY{8MOCxwyw(440-5QgQ;`p43S4f`oKo<0ytf%Q~6LFr~ERQ|Z^LxXyjD zW^Xw&Pa)2AE5*K2!e$58M-zZ7$&V=9--VCTS>L6!JqBTqMLmHjMpWD-pM$WsJv{|vM2RlE1!ov6MD{2wX@NSs z#glaoHh1EY%5YP_+9<7$2*Kl$Hflwuy?D$ij+R@BlO4go#HW~1Q*_-`p#<6 z33*ONlWD1XoHExN$&($y7m3W#Z|S%!p4=8T5WypXMPRJn5xojur;4rD4fgkxi87 z!e4NV#A0NQq#;uuT7m7pR35%Io3EYQ*OvL#uIxBUMbFmKb+qQ@Y(w;?(b;q;wy41qw=3mJZrvtND?{iVT8#7Rd&QkQM7(UW;E=dhSXH`tZB-Gy zgeB2?7{0m55?{CshGZsvoCQZ9*madd+$}}DxM)>nEFXW)wwgTKSsIvb_xd$4?gP-U zab2{>N{McX@I_B088W0lkG#*8p~ zoXmjqYrJvXm;nK3T=dhsV$!Bcc;Ph|ty-kV`DqZ~mzw>j_trJgCqIi#w9j408C&)^uCm!$AvJh`g48-6aToZKsHDD$wg-Jf){ zPe#;1EjNz#iOkXYf4bEKZj_~A)vbS`kc=$xh1Y)eV?7Z1J1DI*{C`JT1joUR6Y zQtdXS4ex%s;w|MX+H2rf=JuSNOJUHJCmY5(Pi418t>wBAjzctORD7a61?|q7$yM_Z zr_0N@swa?D&XS&bjonSLZpV!LQFNV-1JXK4FgmwM5Kr+UF7s-NC)*%?uHiLU33ulC zI;^JI#qW>0t_r-P8+N(Uu~mBD>psVPRR^TO9K(Cjs~=n69P)sgSdONUN^#i>zkwho zx$JRTKvdeB6*Q|h4{ly}ww-6#8uRR@U2g8yY_G0{*F`E;mlpLFCz{r>wYoI*#G5^> zQ6;=^8#+d>=(sq&wEx>$^$W6EzI(NK)y-^^d7RrHYaCZ(9SZ|sE3fF%Bq(mkq76fE zVBqE^N^s#bhBsGqJGm~siv&X4+$h(4_i59efL*urVKsSoW&7pQI+6^Q0rI^qn>nLa zwA8JD>_VBB+`?sytHZ(%Ls+c}(B0$adh)L7{eEDErIR08m6_spGIwauS$Hm;8|D3c ztiHGVM4uz_OZ8f~4BonDE`xKSYw9dS^F_WE+t$jw?(nKzBy_|p^|Hy+Zw?86o00aw zFfS$nFcPCT9r8!qH)d{j$=F4d=)z^pwoCDi*TrS6#kVOQjmzoLkjT$rSC80jLbpJE z%bi>;@tvjV&5^SeIgj>s5tIU&UL0uQFgV_7O3R=t=}mFmcHAYs(_iq8#+~2#YV$aA z$aPzyFCXQ%_a*wlD_RX+-BbxL90r_nwOEh)vK;Z(>s+Q+`|D*>bgJC&iOvW1n@y46 z-tx)d3m;;WBG6sNQcC8EDZ)f(Mj;hp3I}Wv7 z=PO4WXDCo^9pzj49SdA%OW7B9JpI15ol8z$G=mcbI^7h^+_0)|cxQUC^2NIkyu~(o z`XtBEo+gh|YlV{@tcRvK#X3~B@Z^>FYvsA^-7=4i`Kzist- z=qqFh6E599cAtHf;wOw(#-DJz0_?Bh`QQfQ_UWmW8pG&52FNFsI)eAE?>eSq( zyp5yQ6-U_y^ce5p+4SHsIe&|zxI)&GXrDDk=Y3_MnbuxQ&_Ks9QPB-}3`=Rc-)i#S znXTr|%=Vzyc>|>LZm3Ne#|@B&Gehz%T<2t`s?aDt5tI~PJb=az*x5;uEVBTINK;Mw(VpsaUg@LgH_ zVA+ySM|QFNq^u!y7`&tTXbpx~R}#ovLhdL!+ylesq=Ek$^q%yZEt5`rC(%; zZv?)1;bXYcxmE?~xSGqiq9boc-$8Oe5{&K8$}x!-7dt}{R_oQaabglr662O>7dtB? zBTIbYGiW8<-*T;G8vi71d~XU>52#L_?F*y9EB;QjbjvP=YlQo3uANJY8QQO zs)QFlgKaz)KVCm#9Z%lMVENg2Q?p$fOVQ8T%JllM)iUqh+u2fwJDu9fIWCty+qa5u z*lvE7@EUd4#zfVNg1e4gXO8okdR#Y%E55{;CKA+j`CG5$)~A}cK}#VZUap+R{hG~V z&{828S>g+~(UR>bSG_P8$~ARFym)2mKfqUR*8t6^{j^<|`4HdbbZ?UvL}XM6FZ@Q+ zD^e96@7d9f)(mst;|cTZ7i67&^MJ~>sHL9l+hyDQrpKBRX!ZNQmoLoKXFUJtD3TbKA)P z)=ifH!*}o^LqA`55791$-uB(BLQqeDvH`M0U|ZGWeLt=HHtJC!KoT>-o^f*@Y`m3Z zK@|}hRl*DJ(R~V8FQ#jV!x$Khx|8rJTYdKdmt9}6-Rk*)eu&F%`mm3CHNk2A7Oxyo ze0`H8zVIHsS>aYMfontm#6afSqL_N*lYMF9V}Z>!tj8JlUDL?P*FU#@AUEUHmmGDB zD_mG7!iyFf5R#E4zVIFhv8(eB?_)&CH+I==@bOkb>czi#D=oLPw7hOth9dP`2v!QK z1Lykdd1a_#GP(p9?nCtU2L;HxrmTL(9Y%8>qu%YabyCbguUq3_UI0bs^~y0kHS6F- zGiBL;5!??)^;o^FI19i1du+M{7!E}01(26SEd%IQLZ_%>cT8(~Z3kGkbY0%J4zgc4 zbHrDL)5$|8csPYJ5^o#7UA9d~Mwa)chXWBWqVZ1ON8@4+Pk0W$apC%#0T;e20JP2P zwoVD$9;DoE61@S`l~Jh0o2x_>lhGx>@F2W_z{&+ojX5gECt1UbVddPvRuvJ;Vs%IA zyoBJmg0lO7SClgl!0`4sMleT9oMRTO3u%ZzOh%Ug!-wEJx^oad1kc2rai;ms1>XUZ z&Gk06e_X1VwR4(61WzBA$+pt<-X*ZQvbmrD%ce_!;Y3E;B@OQ0tgKyeE;$;&vGWhT z32f>dpq1!tA^Uc5zu~hGx*mhANFvi}#agGwoKj*Jbz1L~kc=$xg%4q~$=b0 z0=4xFn19D4X}gluO{XFK1lS!QF9OWAsxA4fT;&+9K5C}0E-)^<6=vRst8X<^Vluh} z7=8q5JIu;Ju6pYB2N7J1qNJ;(xT?dRo`%@IcJJ?Nx)uYSQ<`T@H~Lt&IG>P=Eb)aK zVR6uOn73&|$*cbLgyMmM>>jG#fVgzrlR8XAPqzW1s#a>g6fyIuzc%mb(*?f4UEfU?STm*_ z7oVTks~yc41gw)=@Aelj@}^6GmkWXV>?FPN&zB5M(pT=@36&kD`gM5@P<5!j-FV?F ziuIsP{9-IE&8m0b+At`W=ZaZC&aT_v$RQ@9OMo>1){BGqNU>YWR@qgemXA1E#n0-0 zJl_^`!uK*q_wC86@+`cHv6ae!KzjD6e@csiD%{=BYw-xl$P!-*e6p(uO#OHr9~o%F zA3AMvM1eY|y~F6)-;Wr~e|C!ZU2Ruc^iRf4p9BMyf56}$6F$eLy%C{qGLS9TW$r)c~K?2%YW#d9@XGw{cBE) z5LREFCiQur(8%_O0W^#lZ${I?`dJni{%y>@x=6kM^$9lSptF{ylVe7f_(tICyUM}o zViW3Re?qY8%~`jGR?Ze-(OJ&771WqsDz`o~Y^A^|rJ{F`gnK(1(d3wsCA=B%<}Px; zH*_0U;xQcy5@+MX@aYc1T<@oaM_hVej@_JdL*lyhzH@t3dbP#}Ikx<7KICiVyYPyS zWjRQ*Kiopevb_n#CvWjxI}*0_JOaJHPUV7OmG_1A8^HTiuIgd3w%h*)(dvFi H0(JlZFY7($ literal 11426 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≧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 w5V Date: Tue, 5 Feb 2019 17:32:49 +0100 Subject: [PATCH 03/16] testing session create, multi create and clone --- .../templates/plannedsession_multicreate.html | 2 +- .../plannedsessions_multiclone_select.html | 2 +- rowers/tests/.#test_urls.py | 1 + rowers/tests/test_plans.py | 98 +++++++++++++++++- rowers/tests/testdata/testdata.csv.gz | Bin 11437 -> 11426 bytes rowers/views.py | 1 + 6 files changed, 99 insertions(+), 5 deletions(-) create mode 100644 rowers/tests/.#test_urls.py diff --git a/rowers/templates/plannedsession_multicreate.html b/rowers/templates/plannedsession_multicreate.html index 3c76bda1..2981a7b0 100644 --- a/rowers/templates/plannedsession_multicreate.html +++ b/rowers/templates/plannedsession_multicreate.html @@ -73,7 +73,7 @@ Clone multiple sessions - + diff --git a/rowers/templates/plannedsessions_multiclone_select.html b/rowers/templates/plannedsessions_multiclone_select.html index 9f86cfa0..cfbddce6 100644 --- a/rowers/templates/plannedsessions_multiclone_select.html +++ b/rowers/templates/plannedsessions_multiclone_select.html @@ -114,7 +114,7 @@ {{ dateshiftform.as_table }}

- +

You can use the date and search forms above to search through all sessions.

diff --git a/rowers/tests/.#test_urls.py b/rowers/tests/.#test_urls.py new file mode 100644 index 00000000..97eb3b79 --- /dev/null +++ b/rowers/tests/.#test_urls.py @@ -0,0 +1 @@ +E408191@CZ27LT9RCGN72.14016:1549376202 \ No newline at end of file diff --git a/rowers/tests/test_plans.py b/rowers/tests/test_plans.py index 3c7558fc..d8a804b1 100644 --- a/rowers/tests/test_plans.py +++ b/rowers/tests/test_plans.py @@ -125,7 +125,8 @@ class TrainingPlanTest(TestCase): tested = True # add test for creating new sessions - def sessions_create(self): + def test_sessions_create(self): + login = self.c.login(username=self.u.username, password=self.password) self.assertTrue(login) @@ -151,8 +152,10 @@ class TrainingPlanTest(TestCase): 'name': faker.word(), } + print 'posting to sessions/create' + form = PlannedSessionForm(post_data) - self.assertEqual(form.is_valid()) + self.assertTrue(form.is_valid()) response = self.c.post(url,post_data) self.assertEqual(response.status_code,200) @@ -1001,6 +1004,28 @@ class PlannedSessionsView(TestCase): rowerplan='coach') self.r.save() self.c = Client() + + self.u2 = UserFactory(username='testbasicuser') + self.r2 = Rower.objects.create(user=self.u2, + birthdate=faker.profile()['birthdate'], + gdproptin=True, + gdproptindate=timezone.now(), + rowerplan='basic') + + self.password2 = faker.word() + self.u2.set_password(self.password2) + self.u2.save() + + self.team = Team.objects.create( + name = faker.word(), + notes = faker.text(), + manager = self.u, + ) + + self.r.team.add(self.team) + self.r2.team.add(self.team) + self.r.save() + self.r2.save() # workouts # workout 1 - 2019-01-13, rScore 69 @@ -1120,6 +1145,9 @@ class PlannedSessionsView(TestCase): enddate = (startdatetime+datetime.timedelta(days=1)).date() preferreddate = startdatetime.date() + self.startdate = startdate + self.enddate = enddate + self.ps_rscore = SessionFactory( startdate=startdate,enddate=enddate, sessiontype='test', @@ -1210,17 +1238,81 @@ class PlannedSessionsView(TestCase): def test_multiclone_view(self): login = self.c.login(username=self.u.username, password=self.password) self.assertTrue(login) + + url = '/rowers/sessions/multiclone/' + response = self.c.get(url) + self.assertEqual(response.status_code,200) + + formdata = { + 'startdate':self.startdate, + 'enddate':self.enddate, + } + + form = DateRangeForm(formdata) + self.assertTrue(form.is_valid()) + + response = self.c.post(url,formdata) + self.assertEqual(response.status_code,200) + + url = '/rowers/sessions/multiclone/?startdate={startdate}&enddate={enddate}'.format( + startdate = self.startdate, + enddate = self.enddate + ) + + formdata = { + 'plannedsessions':[self.ps_time.id,self.ps_trimp.id], + 'shiftstartdate':datetime.date.today()+timezone.timedelta(days=6) + } + + form = PlannedSessionMultipleCloneForm(formdata) + + self.assertTrue(form.is_valid()) + + form = SessionDateShiftForm(formdata) + + + self.assertTrue(form.is_valid()) + + response = self.c.post(url,formdata,follow=True) + self.assertEqual(response.status_code,200) + def test_multicreate_view(self): login = self.c.login(username=self.u.username, password=self.password) self.assertTrue(login) - # post something + # get something + url = '/rowers/sessions/multicreate/user/1/extra/1/' + response = self.c.get(url) + self.assertEqual(response.status_code,200) + + data = {} + + data['csrf_token'] = response.context['csrf_token'] + + management_form = response.context['ps_formset'].management_form + for i in 'TOTAL_FORMS', 'INITIAL_FORMS', 'MIN_NUM_FORMS', 'MAX_NUM_FORMS': + data['%s-%s' % (management_form.prefix,i)] = management_form[i].value() + + for i in range(response.context['ps_formset'].total_form_count()): + current_form = response.context['ps_formset'].forms[i] + + for field_name in current_form.fields: + value = current_form[field_name].value() + data['%s-%s' % (current_form.prefix, field_name)] = value if value is not None else '' + + + # post data + response = self.c.post(url,data,follow=True) + self.assertEqual(response.status_code,200) def test_teamcreate_view(self): login = self.c.login(username=self.u.username, password=self.password) self.assertTrue(login) + url = '/sessions/teamcreate/' + response = self.c.get(url) + def test_teamedit_view(self): login = self.c.login(username=self.u.username, password=self.password) self.assertTrue(login) diff --git a/rowers/tests/testdata/testdata.csv.gz b/rowers/tests/testdata/testdata.csv.gz index db3fe1e85e2a2f7ccfda1fa89bf15f33594dc9ce..85862f10e189c14cd1cfc5c6228bbf1ae312a935 100644 GIT binary patch literal 11426 zcmV;TEM3zdiwFp9x>;NT|8!+@bYx+4VJ>5Hb^v{S+pcWKb=`Y?1s@fl(XQ*Ps>j5N zkpO`q*a7kw(9(g!NF)XFNVfC!8Dq>^t7`3iY{(`zdw2I7Yu3D9YX9)lAHMwS>wo_6 zdVTl(@4x==*B`$A+kgMVzrMcv)AxVYzyJL64?lg?PyZD! z{rcq({Mhx0|Mv4QKm5O+fBO3UpT7Iek6*ri`Q=x<_>W(I`T37uT8uyb?)%^STmOdt zgx8aumAY%55Io>UQ}*Kzy0Nhzkc~2Uw-=M zum9)ctNu7YZs7m$<@Z1T^~*2+cKqUh{rt-xfA{4V|NVEr`M*E^(?9VSzx(q2A3wkPTfF)2z~rBQxj*_}e*XUJexh%G<==nf3;%wh`uQ(k zafDyLT;RX^`@j77PhWm{{r)dM{_j744 zkT!<-&Q%c73vld}$~)m(=ey!fbK%G)jB(~y#@iac{ax_hn(+JH`0<(n-#Y%0qTV}3 zwO&VVc;6Uri?!wP%5%O4-hm%0p9$1LzR}G5PR$we8t++NIS~OqlR(+If)6&KX{o=; z3luBSfUIpWzK8>>@ma(u*>uKcKj1OdZ zh@3$iGBrx9$s7tOg?2&dK)j&fwazoD0}q%CEbr^QN_q_*A!HM5wgsg)k}(%oDv@*{ z!5ix=1yD+x;>xTOQX?#7v^FWbO{9WtEcL=dR=y#ft-tYWqo`hN0hZ!e@Fn2J8_xwW zKocoiQD`VCeJ;M>t)q5`Q|^%TjAv>&6-Y^Gl`NYQ7!$G3HyV4-Bs86B*`X;$b!d}1 zyU;+cDYX!L$l?H+oc)xBZ8T(PZ<1-Eftb}ftW|tEL5;zKvIVB{qw@xnvyGTq1&0%4dzMjgPzB6L zb4==W7p*XFFwmdrRt6uMZv)*1GXop;#s@ZQ_+gM}G$Q29^pm2C4?@Wb;y3?rQ=drm%Q$|D?HaUSq zGJ0>GoNSFOaHj=MFqO6-2Pa<7YpEu z-~8qp-{1?Z10fZeV^V2`4`)D{6rc*`hwnR3b6YNRKMlC(>_A+m)oE@X*u^?1~nD}QfBLri$?Ud;v1?MPl_RJb4_g<4yw8N zPgQbJpga8%D?n{DTOfmX(?BjzkSpg@S)7{W>us5Ot8LN%Wc(K*fM^4v4^|;osc;~2 ztkp?|v<){^Zb`e*_)^i`bn`d$g2h*}!=eKA%`1D+(L>7wEAi-a+^}G`XT_nxBNfOS z97~#1LFiSE!(lZi8Pc}kg|1qtpkwesAccr#=mhKK+^Wn7tSV0sj2uFh4pd!+vsZmG z-_qX6MX+MhuiF zd!@1S2oh02r1xO>Si~%-&W2TojQ_jKk?9b}XEk)&APzgO(sFGbCcYoGfoGTuo`|TX zQw(`qcPQS9CGjF6rI-|ySA8`^dmSO9sU+TpIz3h9p+)^yn z=IMwVm=jwynfDx>WXRjR!?%wWZsWuo4-|@W!K&Yc#n?uoRZod;%M*6#;VoK>a}8lX z4QcKSeFr|*01;<6<(zYJnjvrdjzjLnag`TiT!<4QVz*j7UAT%LVwMoY>emOm+V(pV z_PqTLEN_LV&!Q-5Ya-rAxbi!+*rXZqw(wBCC0mt7htR9TfkLDNLib#J70iVw${hUBY$gp?|dQ@L>!{8&Yry zS4Kbv{cRIT--PVb$mc~flx^g}zv{T~B1|u6AzIW!7l~Rq`K4G3;XYFw`ScBI9PBDv z5nF_9fL{cbgI!2MA^u2!C0)}=hO(_Zkyl=61xu(VuEu^lr5S}@Cwd^l^mRJr^tgHA{ojy@zB2F z#uWeZvWo&+cZuaY^d#A-i z7-=zk!cS#!EY%ZOVHCpH4%quAj;&yRc<8^y9-*nV21|;ELLh}43@VKj4M8_Bi($=4 zhO$llP?HUraE<@C#lWDT?-&spvBI*~3A+MKOKvC&%JCN*+hrz>um=Lp*_q zvLKLYY4$=91M@1ATL&hZq2Bf$-}W{)tb8$bSfIM9BM^csxn_t;hIx>Kw|pb2pPOMN z?eMuLh!d;D4}J|M6Rk{I)*78;sCmO;8OC8{Pm2qFx)Pb$PF!i>>a19NaztK^>|QU( zM_3;Ra<$TEvypJs$mj;54|W5t>c(UmX@*)hFvWphpI5TScNSZjY)RKa4nN4c{C1Vi z?{+3I57V%MAq01-X&vqxPi!(k%uXRD<)rx`mL^HKtMTvHWIto_p!UY)|clOVE7X@s(ZI$uDIo6wB2|)$i4vwrKOC7D!ZW)ed zvSP`AwJw^W-bNpm`l_Q=FH%x@$MpWhE=vfQLUEE}WsJbaH#1swpLQfYsjP@w8*9$& zag5C3V0*)-n+ex&lA+Fxi%RVc5cv?2oXgRjpjZCEuhqa%` zXX{k@4^$O72$+Rj2Z!5KE5Rd+Vrbji(+~(#Qkix3S1WNj?cPpQN!bCtXDizUT^VgY zJ)^O3c_nDQAE@eBsCdRlh5pLW z<`H~(qV1(~rrM)0kleAC(M=%BR?b0KBpKSa^R)X8tMHUq3ux_vRr}M-7ezRbD_Qw` zWkU5%XH93Hj}W@T_eI39BdNDMOKjve5H`XUw~CVtZJT)pZ;~mMrQx>l9*e6GGtvpG zE?A5M>`rH|x>DUG%QNnZOgA7%!Y(FkCRE$T*W~TY#71u$Z+#}MsH@B)93-={ z>(#5t#z*^H9IVom-IMB-6%iyHvJUJ`N7kqA9-dE2Dz#1X!)BO)ZbBBOb>>Mg=ovpzvs<|ISgCY~*hCRm_s z%GMBZtjK9T(v5V%Vol+5Ew!xJhS9SfL06iHQ@d*-j1TN5^9_=MekvMli)Lus#8=fJ zh>MLKqeQbkQ=MJ)1H0;Cm&MJl6+GY4m1X4b&<0s6yz^wYj?%>_8bKRkRoc}eSl$t; z!M2TW+0!$&os((W1bN-c&QiNdIGzPLESNS+Te_a5CB6OZAB=)?gn-X^Het{90hlC` z!M2HS&9k$lc4!wArkY~~zz3|Pw3B8V?+~nSRO05!2JQ&e8s`dN+fAu-XMSM{opT>u zV-DbwNCw+hzOk_BxDub4b$j#PQc zWVs?L6L@mT<9om32fW;b6!6K*bUrq8zx&!bKC&A7$Qj z*HcH*A?|4!oF5DnWS;F7P;VLGz00@}?ZQQp!M34?;jilyeS{1=u))AoeepBmA};k9 zk;mG2WM%op<&~w&MnHTM6ft+Uwsz+;b8fCpRvE~AkqoviJ%gm`xHzGw0NrL;J*J*d zTzTQ*Im*uF0qp7tI?z=1Tiu+z5~MzP79Lou?HtP`y16hq&} zeq?XEB=x1|V9d(dlIenPN1}1V!pFO;yF0k9(Y@=E)EkZL5q6F75!CI1gle$DL!&x1lXwO74{i;~ zRqb2sGZK1Z-ID4EVsJ*ZGq7Z1A}kWO7R}JNv4;kxg{x?cadLX@j)G5iSzW*egRKX! zd^>HoUv>BVap(w5A!BS*Zu`Yb2O?>@>|UH==-b-Q>}{8%{%D-kO{7yQSxs8DL%Is9 zEF=U0l(}zq@~a)vwS6PJT*M*bVA?5N59bIriDu~A-m}@_c9kvCl$qP~)jx?lf)=pE zItvAvnf?wY=5pF;Ic=B|HTaJ|tuu7?loE(0X?7=9}41F7XXkc7fqiN7_a8ApX zKDKh2iL!w4d!C_v-hNwS!L@B*4};I+i<#xF3f~gpH3iI^X6U!YM{J8g&4FKl6X@rY;VrC>blE#FA%RT~Q{5A20M^Pf)MF}4jbxX5gAz1}vj>Q*-jJ2?b zZO{R@r=%&;KAmD1X~P;`uU8=?XO6?Nrc;ILc8@~ES{YH{F@~G_tKPMXC8K(5yH9Zr z82ie>clCWZ5CnZ}k_Jirh)yw#+t^R0Aa3kg0Vfhb_kK>+eD%SsxnRN24O#p8EvMZd zvW3SkkS~}lA7E~ob0!bj8p$b!Q8ui~nR~(VNf_iNOeW1`tDIIH^Na;h9-Ce2MiU8Ajc>>@urr^cR{{<{?S!%I$DNO#2z+ zz}G^E9%l83RvRCw)f?E%GvkzD#Mqc)2%8pSdR+pRjz0d+aRDVCiila89yb5hfOS24x`PL3x5sFpO>K zp?`G=7py01P4zX?m7~pmqDm|>l1_R}nfl(2oMRlOz8;}!?cDf)&z=uc&%&ri_y}w| z%`msAXGeUoYibG%iNf?c3~k1Rt0*XJ3UWADk+w};LC(0OD?M|x#KwP*nN>pLT>Rjq z>?2779FYuj+xlWxY+pGSml@m7;@#}H=Tli6-oVCd%Gi%`w$82=Reh8%2a7lyW6A2A zK`UeYP`v~kkqmQNd+MLVV))g_gSmoea_T^(>L=?Z`88$h?Tn$lHM?Yf>o{QO(V4yKUVVjxE&w+! z*1{8BDc)kAhJ=`g4Wa9{v*&0EzbOOr`jJ%)ik)j>P z=-J)T%ZAvu${JTatI&-TyEXX^6!3^8T@{Zy_BQ<(sw`MY@zM;3p^#fLgc!im`{RXZi~-QX=)LY!>aAA z21dSyLOGRsqpE$OQX@O=u@?uHV?3H@$Jh&Ue{N~AZGBEK4Gw0&xoJ`9q8jwY3!AO^ z?5vDP)&i`*{BOIK-Gr^Np3ROG-^21XGtAvOo^3JBo6dJV8bVNDkiA&~=NV_KS=|jz z!ASZqqCww#L91TO^0V&39&QixHvx2&aua-z90T zKvbJD7y=}!L0^1fQ{t$_NMNhVb)u_0vDJ%h&DrMI_#T=6`H;1?lrwvS{AOE3)X|_` zOxTQB8b=CE{a7wUUD!x9=!+LNj?`+&S0lHIFH*F+j^*CO6}HZrJH@T5E|^rg{CJ{X zbUF-eDctT%Y6RRxr^n2Y9|DO6eeVUW_^=6%U!%a1q%~OCw63?=#<){w0yo)^AvmZc;$F@wBbd_3 za9HCw)5N9>IYF|AGw_c0V%+fY$zAC&?!jp0ov|#-t%+0Hk4ldKiE7X{FKmePmcaQ^ zI2#Fggr%qtT`;x>Btd*g9qK&wG#zuJb+?XkM79IFk?S!x3KPc><*FeM;StfGFTS8P zp8?L?TVf4L1!h%4U?K!}}rCkmLZ*U|V^vxHz zaq*4YOArETYFZ;7jrF-FI=j$mzjF@^7}2#=F>ry{>HzlO^d^^B8YQr>B@{WJ&wiZ1 z%|$h&y;lnl{C38%zy`juE65bKe4BIq#&$S{HDfVtd-)^4eQ@sZ@@IrAL?Ro~Ua_6C zUdPJkTBaeb8EOrnANr6lbo{!b0(9v^Uq;R&O^!4+T8C4;T3;e?(}Lo(Q9q4x0%#JPH6D@TiW7hK?>3i3`C5gt>niK;P@ z3NAt`wNRuv(NlBD8_J!D~oDK_wp?#4sLL!3myb+wgs--d6ihrRuI9_GuZ7_ zpX%U_JF|gsv+GwxB#c!dx7hkC}FFI?Gra7JMPsetAn$t zz4AxaXD7P&sKD4LNTQH^moxG`3NXSWnjzhLxG$m_{eL68~%fh4HocmIS zdx}|XV{cEysPm76!SgHe%_FLiWh+*njYx|ec+t$PR97aAyH ze485q8TbQRacmsFO1_u-SbLatdHksfzWYdFKH%Pm(S~vXj(rm@At7YsoVh=zNQQLp zK7L`o zF-$}v8`8a=%Q(UN(^=aC)qBp_i^>d?Zl9W5-aJy`6XeH$?jdh#IvuSiW2Fe$fyQ7X%)6t(Tq(_`VyV0aPnHcRK| z>0{qbfJ8K;ygdWw3OxK>QC3u0IV}rk^Bf{>?E2{@10KSS%?G)zbgN%$k9SIwa~`oz zw#GPgJJRSM_H2YmR71+!H)vs0*w}}Zxr0B>OBq@|Pc^ggM{VFYwcGk$fl@rKKNyF7 zn-z8=xA7cPKW5*AM?^!)+cT84@tHM+&KYDfES43EjQK(0?265^?Obr(VsTqIdn`jB zHb+8=pjXzHmq+!S@^cwNkVH15yj=rt99U+gt7p_zrK!vQ?kswt>$k-9Pd4uIsaITo zOI(surS0Z$+klnBp~9fm*gfIOwupw5w{PH3hsVDh2}9Q1RXc5K4SW04yBO6$0KC*l<8x&MT5?Uf+~W*ONqg3P(2<*^u&f4-C0n>Ns6J%NS|xeIQdHNsArl#mYr+&WcC_1hD~dx9w;TbMh*_1li5Nfsv(u_A=v11q2q|N zDX≧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 w5V5Hb^v{S+m38Ua@~7>1s`dkP?Oil%tw31 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 H0(JlZFY7($ diff --git a/rowers/views.py b/rowers/views.py index 1f16a670..ace74151 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -14167,6 +14167,7 @@ def plannedsession_create_view(request, enddatestring=enddatestring) + if request.method == 'POST': sessioncreateform = PlannedSessionForm(request.POST) if sessioncreateform.is_valid(): From 966b9e8ba1435589df8829d8d94372dbf6aee715 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Tue, 5 Feb 2019 17:33:22 +0100 Subject: [PATCH 04/16] remove temp file --- rowers/tests/.#test_urls.py | 1 - rowers/tests/test_urls.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 rowers/tests/.#test_urls.py diff --git a/rowers/tests/.#test_urls.py b/rowers/tests/.#test_urls.py deleted file mode 100644 index 97eb3b79..00000000 --- a/rowers/tests/.#test_urls.py +++ /dev/null @@ -1 +0,0 @@ -E408191@CZ27LT9RCGN72.14016:1549376202 \ No newline at end of file diff --git a/rowers/tests/test_urls.py b/rowers/tests/test_urls.py index b59f6cd2..f3fc11f9 100644 --- a/rowers/tests/test_urls.py +++ b/rowers/tests/test_urls.py @@ -1,4 +1,4 @@ -#from __future__ import print_function +from __future__ import print_function from statements import * nu = datetime.datetime.now() From ca6e7746b726fae9385cd7cff855795e38eb0466 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Tue, 5 Feb 2019 18:17:58 +0100 Subject: [PATCH 05/16] testing team-create --- rowers/tests/test_plans.py | 31 +++++++++++++++++++++++++- rowers/tests/test_urls.py | 15 +++++++------ rowers/tests/testdata/testdata.csv.gz | Bin 11426 -> 11426 bytes 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/rowers/tests/test_plans.py b/rowers/tests/test_plans.py index d8a804b1..e6c93f0a 100644 --- a/rowers/tests/test_plans.py +++ b/rowers/tests/test_plans.py @@ -1310,9 +1310,38 @@ class PlannedSessionsView(TestCase): login = self.c.login(username=self.u.username, password=self.password) self.assertTrue(login) - url = '/sessions/teamcreate/' + url = '/rowers/sessions/teamcreate/' response = self.c.get(url) + self.assertEqual(response.status_code,200) + + form_data = { + 'team':[1], + 'startdate': self.w1.startdatetime.date(), + 'enddate': (self.w1.startdatetime+datetime.timedelta(days=5)).date(), + 'preferreddate': self.w1.startdatetime.date(), + 'name': faker.word(), + 'sessiontype': 'session', + 'sessionmode': 'distance', + 'criterium': 'none', + 'sessionvalue': 13000, + 'sessionunit': 'm', + 'course': None, + 'comment':faker.text() + } + + plannedsessionform = PlannedSessionForm(form_data) + + self.assertTrue(plannedsessionform.is_valid()) + + teamform = PlannedSessionTeamForm(self.u,form_data) + + self.assertTrue(teamform.is_valid()) + + response = self.c.post(url,form_data,follow=True) + self.assertEqual(response.status_code,200) + + def test_teamedit_view(self): login = self.c.login(username=self.u.username, password=self.password) self.assertTrue(login) diff --git a/rowers/tests/test_urls.py b/rowers/tests/test_urls.py index f3fc11f9..f0f2596d 100644 --- a/rowers/tests/test_urls.py +++ b/rowers/tests/test_urls.py @@ -262,10 +262,11 @@ class URLTests(TestCase): self.assertTrue(login) response = self.c.get(url,follow=True) if response.status_code != expected: - print url - print response.status_code + print(url ) + print(response.status_code) + self.assertEqual(response.status_code, - expected) + expected) html = BeautifulSoup(response.content,'html.parser') urls = [a['href'] for a in html.find_all('a')] @@ -274,10 +275,10 @@ class URLTests(TestCase): if u not in tested and 'rowers' in u and 'http' not in u and 'authorize' not in u and 'import' not in u and 'logout' not in u: response = self.c.get(u) if response.status_code not in [200,302]: - print len(tested) - print url - print u - print response.status_code + print(len(tested)) + print(url) + print(u) + print(response.status_code) tested.append(u) self.assertIn(response.status_code, [200,302]) diff --git a/rowers/tests/testdata/testdata.csv.gz b/rowers/tests/testdata/testdata.csv.gz index 85862f10e189c14cd1cfc5c6228bbf1ae312a935..d8f693ae70764728c8a25b875a46d692433da7aa 100644 GIT binary patch delta 15 WcmZ1!xhRrNzMF&N@{x^fb94YMqy^0Y delta 15 WcmZ1!xhRrNzMF$1ch^R?IXVC=4h3BR From 00643f2705d0918dc49250c32109702fd62de8ee Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Thu, 7 Feb 2019 10:08:26 +0100 Subject: [PATCH 06/16] replacing a few ix with loc.iloc --- rowers/dataprep.py | 78 +++++++++++++------------- rowers/datautils.py | 2 +- rowers/tests/statements.py | 2 +- rowers/tests/testdata/testdata.csv.gz | Bin 11426 -> 11426 bytes rowers/views.py | 32 +++++------ 5 files changed, 57 insertions(+), 57 deletions(-) diff --git a/rowers/dataprep.py b/rowers/dataprep.py index 16da3650..fb5de97e 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -118,11 +118,11 @@ def get_latlon(id): rowdata = rdata(w.csvfilename) try: try: - latitude = rowdata.df.ix[:, ' latitude'] - longitude = rowdata.df.ix[:, ' longitude'] + latitude = rowdata.df.loc[:, ' latitude'] + longitude = rowdata.df.loc[:, ' longitude'] except KeyError: - latitude = 0 * rowdata.df.ix[:, 'TimeStamp (sec)'] - longitude = 0 * rowdata.df.ix[:, 'TimeStamp (sec)'] + latitude = 0 * rowdata.df.loc[:, 'TimeStamp (sec)'] + longitude = 0 * rowdata.df.loc[:, 'TimeStamp (sec)'] return [latitude, longitude] except AttributeError: return [pd.Series([]), pd.Series([])] @@ -964,7 +964,7 @@ def save_workout_database(f2, r, dosmooth=True, workouttype='rower', totaltime = row.df['TimeStamp (sec)'].max( ) - row.df['TimeStamp (sec)'].min() try: - totaltime = totaltime + row.df.ix[0, ' ElapsedTime (sec)'] + totaltime = totaltime + row.df.loc[:, ' ElapsedTime (sec)'].iloc[0] except KeyError: pass @@ -2077,37 +2077,37 @@ def dataprep(rowdatadf, id=0, bands=True, barchart=True, otwpower=True, return 0 rowdatadf.set_index([range(len(rowdatadf))], inplace=True) - t = rowdatadf.ix[:, 'TimeStamp (sec)'] - t = pd.Series(t - rowdatadf.ix[0, 'TimeStamp (sec)']) + t = rowdatadf.loc[:, 'TimeStamp (sec)'] + t = pd.Series(t - rowdatadf.loc[:, 'TimeStamp (sec)'].iloc[0]) - row_index = rowdatadf.ix[:, ' Stroke500mPace (sec/500m)'] > 3000 + row_index = rowdatadf.loc[:, ' Stroke500mPace (sec/500m)'] > 3000 rowdatadf.loc[row_index, ' Stroke500mPace (sec/500m)'] = 3000. - p = rowdatadf.ix[:, ' Stroke500mPace (sec/500m)'] + p = rowdatadf.loc[:, ' Stroke500mPace (sec/500m)'] try: - velo = rowdatadf.ix[:,' AverageBoatSpeed (m/s)'] + velo = rowdatadf.loc[:,' AverageBoatSpeed (m/s)'] except KeyError: velo = 500./p - hr = rowdatadf.ix[:, ' HRCur (bpm)'] - spm = rowdatadf.ix[:, ' Cadence (stokes/min)'] - cumdist = rowdatadf.ix[:, 'cum_dist'] - power = rowdatadf.ix[:, ' Power (watts)'] - averageforce = rowdatadf.ix[:, ' AverageDriveForce (lbs)'] - drivelength = rowdatadf.ix[:, ' DriveLength (meters)'] + hr = rowdatadf.loc[:, ' HRCur (bpm)'] + spm = rowdatadf.loc[:, ' Cadence (stokes/min)'] + cumdist = rowdatadf.loc[:, 'cum_dist'] + power = rowdatadf.loc[:, ' Power (watts)'] + averageforce = rowdatadf.loc[:, ' AverageDriveForce (lbs)'] + drivelength = rowdatadf.loc[:, ' DriveLength (meters)'] try: - workoutstate = rowdatadf.ix[:, ' WorkoutState'] + workoutstate = rowdatadf.loc[:, ' WorkoutState'] except KeyError: workoutstate = 0 * hr - peakforce = rowdatadf.ix[:, ' PeakDriveForce (lbs)'] + peakforce = rowdatadf.loc[:, ' PeakDriveForce (lbs)'] forceratio = averageforce / peakforce forceratio = forceratio.fillna(value=0) try: - drivetime = rowdatadf.ix[:, ' DriveTime (ms)'] - recoverytime = rowdatadf.ix[:, ' StrokeRecoveryTime (ms)'] + drivetime = rowdatadf.loc[:, ' DriveTime (ms)'] + recoverytime = rowdatadf.loc[:, ' StrokeRecoveryTime (ms)'] rhythm = 100. * drivetime / (recoverytime + drivetime) rhythm = rhythm.fillna(value=0) except: @@ -2152,7 +2152,7 @@ def dataprep(rowdatadf, id=0, bands=True, barchart=True, otwpower=True, else: drivenergy = drivelength * averageforce - distance = rowdatadf.ix[:, 'cum_dist'] + distance = rowdatadf.loc[:, 'cum_dist'] velo = 500. / p distanceperstroke = 60. * velo / spm @@ -2184,26 +2184,26 @@ def dataprep(rowdatadf, id=0, bands=True, barchart=True, otwpower=True, if bands: # HR bands - data['hr_ut2'] = rowdatadf.ix[:, 'hr_ut2'] - data['hr_ut1'] = rowdatadf.ix[:, 'hr_ut1'] - data['hr_at'] = rowdatadf.ix[:, 'hr_at'] - data['hr_tr'] = rowdatadf.ix[:, 'hr_tr'] - data['hr_an'] = rowdatadf.ix[:, 'hr_an'] - data['hr_max'] = rowdatadf.ix[:, 'hr_max'] + data['hr_ut2'] = rowdatadf.loc[:, 'hr_ut2'] + data['hr_ut1'] = rowdatadf.loc[:, 'hr_ut1'] + data['hr_at'] = rowdatadf.loc[:, 'hr_at'] + data['hr_tr'] = rowdatadf.loc[:, 'hr_tr'] + data['hr_an'] = rowdatadf.loc[:, 'hr_an'] + data['hr_max'] = rowdatadf.loc[:, 'hr_max'] data['hr_bottom'] = 0.0 * data['hr'] try: - tel = rowdatadf.ix[:, ' ElapsedTime (sec)'] + tel = rowdatadf.loc[:, ' ElapsedTime (sec)'] except KeyError: rowdatadf[' ElapsedTime (sec)'] = rowdatadf['TimeStamp (sec)'] if barchart: # time increments for bar chart - time_increments = rowdatadf.ix[:, ' ElapsedTime (sec)'].diff() + time_increments = rowdatadf.loc[:, ' ElapsedTime (sec)'].diff() try: - time_increments.ix[0] = time_increments.ix[1] + time_increments.iloc[0] = time_increments.iloc[1] except KeyError: - time_increments.ix[0] = 1. + time_increments.iloc[0] = 1. time_increments = 0.5 * time_increments + 0.5 * np.abs(time_increments) x_right = (t2 + time_increments.apply(lambda x: timedeltaconv(x))) @@ -2212,28 +2212,28 @@ def dataprep(rowdatadf, id=0, bands=True, barchart=True, otwpower=True, if empower: try: - wash = rowdatadf.ix[:, 'wash'] + wash = rowdatadf.loc[:, 'wash'] except KeyError: wash = 0 * power try: - catch = rowdatadf.ix[:, 'catch'] + catch = rowdatadf.loc[:, 'catch'] except KeyError: catch = 0 * power try: - finish = rowdatadf.ix[:, 'finish'] + finish = rowdatadf.loc[:, 'finish'] except KeyError: finish = 0 * power try: - peakforceangle = rowdatadf.ix[:, 'peakforceangle'] + peakforceangle = rowdatadf.loc[:, 'peakforceangle'] except KeyError: peakforceangle = 0 * power if data['driveenergy'].mean() == 0: try: - driveenergy = rowdatadf.ix[:, 'driveenergy'] + driveenergy = rowdatadf.loc[:, 'driveenergy'] except KeyError: driveenergy = power * 60 / spm else: @@ -2246,7 +2246,7 @@ def dataprep(rowdatadf, id=0, bands=True, barchart=True, otwpower=True, drivelength = driveenergy / (averageforce * 4.44822) try: - slip = rowdatadf.ix[:, 'slip'] + slip = rowdatadf.loc[:, 'slip'] except KeyError: slip = 0 * power @@ -2319,11 +2319,11 @@ def dataprep(rowdatadf, id=0, bands=True, barchart=True, otwpower=True, if otwpower: try: - nowindpace = rowdatadf.ix[:, 'nowindpace'] + nowindpace = rowdatadf.loc[:, 'nowindpace'] except KeyError: nowindpace = p try: - equivergpower = rowdatadf.ix[:, 'equivergpower'] + equivergpower = rowdatadf.loc[:, 'equivergpower'] except KeyError: equivergpower = 0 * p + 50. diff --git a/rowers/datautils.py b/rowers/datautils.py index a18bc3dc..ecf961dc 100644 --- a/rowers/datautils.py +++ b/rowers/datautils.py @@ -103,7 +103,7 @@ def getsinglecp(df): dfnew = pd.DataFrame({ - 'time':1000*(df['TimeStamp (sec)']-df.ix[0,'TimeStamp (sec)']), + 'time':1000*(df['TimeStamp (sec)']-df.loc[:,'TimeStamp (sec)'].iloc[0]), 'power':df[' Power (watts)'] }) diff --git a/rowers/tests/statements.py b/rowers/tests/statements.py index a84f22a5..310b127e 100644 --- a/rowers/tests/statements.py +++ b/rowers/tests/statements.py @@ -4,7 +4,7 @@ pytestmark = pytest.mark.django_db from bs4 import BeautifulSoup import re -from nose_parameterized import parameterized +from parameterized import parameterized from django.test import TestCase, Client,override_settings, RequestFactory, TransactionTestCase from django.core.management import call_command diff --git a/rowers/tests/testdata/testdata.csv.gz b/rowers/tests/testdata/testdata.csv.gz index d8f693ae70764728c8a25b875a46d692433da7aa..ac467768a0522d7eada493284e7c6bc036fba39b 100644 GIT binary patch delta 16 XcmZ1!xhRrdzMF%??Q8T#_SrfBG7$yM delta 16 XcmZ1!xhRrdzMF&N@{!1m?6Y+MG`t2U diff --git a/rowers/views.py b/rowers/views.py index ace74151..45038b24 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -1757,35 +1757,35 @@ def add_workout_from_strokedata(user,importid,data,strokedata, unixtime = cum_time+starttimeunix # unixtime[0] = starttimeunix - seconds = 0.1*strokedata.ix[:,'t'] + seconds = 0.1*strokedata.loc[:,'t'] nr_rows = len(unixtime) try: - latcoord = strokedata.ix[:,'lat'] - loncoord = strokedata.ix[:,'lon'] + latcoord = strokedata.loc[:,'lat'] + loncoord = strokedata.loc[:,'lon'] except: latcoord = np.zeros(nr_rows) loncoord = np.zeros(nr_rows) try: - strokelength = strokedata.ix[:,'strokelength'] + strokelength = strokedata.loc[:,'strokelength'] except: strokelength = np.zeros(nr_rows) - dist2 = 0.1*strokedata.ix[:,'d'] + dist2 = 0.1*strokedata.loc[:,'d'] try: - spm = strokedata.ix[:,'spm'] + spm = strokedata.loc[:,'spm'] except KeyError: spm = 0*dist2 try: - hr = strokedata.ix[:,'hr'] + hr = strokedata.loc[:,'hr'] except KeyError: hr = 0*spm - pace = strokedata.ix[:,'p']/10. + pace = strokedata.loc[:,'p']/10. pace = np.clip(pace,0,1e4) pace = pace.replace(0,300) @@ -7967,7 +7967,7 @@ def workout_downloadwind_view(request,id=0, return HttpResponse("Error: CSV Data File Not Found") try: - bearing = rowdata.df.ix[:,'bearing'].values + bearing = rowdata.df.loc[:,'bearing'].values except KeyError: rowdata.add_bearing() rowdata.write_csv(f1,gzip=True) @@ -7976,7 +7976,7 @@ def workout_downloadwind_view(request,id=0, try: avglat = rowdata.df[' latitude'].mean() avglon = rowdata.df[' longitude'].mean() - avgtime = int(rowdata.df['TimeStamp (sec)'].mean()-rowdata.df.ix[0,'TimeStamp (sec)']) + avgtime = int(rowdata.df['TimeStamp (sec)'].mean()-rowdata.df.loc[:,'TimeStamp (sec)'].iloc[0]) startdatetime = dateutil.parser.parse("{}, {}".format(row.date, row.starttime)) @@ -8033,7 +8033,7 @@ def workout_downloadmetar_view(request,id=0, return HttpResponse("Error: CSV Data File Not Found") try: - bearing = rowdata.df.ix[:,'bearing'].values + bearing = rowdata.df.loc[:,'bearing'].values except KeyError: rowdata.add_bearing() rowdata.write_csv(f1,gzip=True) @@ -8043,7 +8043,7 @@ def workout_downloadmetar_view(request,id=0, avglat = rowdata.df[' latitude'].mean() avglon = rowdata.df[' longitude'].mean() airportcode = get_airport_code(avglat,avglon)[0] - avgtime = int(rowdata.df['TimeStamp (sec)'].mean()-rowdata.df.ix[0,'TimeStamp (sec)']) + avgtime = int(rowdata.df['TimeStamp (sec)'].mean()-rowdata.df.loc[:,'TimeStamp (sec)'].iloc[0]) startdatetime = dateutil.parser.parse("{}, {}".format(row.date, row.starttime)) @@ -8121,7 +8121,7 @@ def workout_wind_view(request,id=0,message="",successmessage=""): hascoordinates = 1 try: - latitude = rowdata.df.ix[:,' latitude'] + latitude = rowdata.df.loc[:,' latitude'] except KeyError: hascoordinates = 0 @@ -8129,7 +8129,7 @@ def workout_wind_view(request,id=0,message="",successmessage=""): hascoordinates = 0 try: - bearing = rowdata.df.ix[:,'bearing'].values + bearing = rowdata.df.loc[:,'bearing'].values except KeyError: rowdata.add_bearing() rowdata.write_csv(f1,gzip=True) @@ -8811,7 +8811,7 @@ def cumstats(request,theuser=0, thedict = {} for field2,verbosename in fielddict.iteritems(): try: - thedict[field2] = cor.ix[field1,field2] + thedict[field2] = cor.loc[field1,field2] except KeyError: thedict[field2] = 0 @@ -9030,7 +9030,7 @@ def workout_stats_view(request,id=0,message="",successmessage=""): thedict = {} for field2,verbosename in fielddict.iteritems(): try: - thedict[field2] = cor.ix[field1,field2] + thedict[field2] = cor.loc[field1,field2] except KeyError: thedict[field2] = 0 From 6d6bd3d9bcbfe3f4cc22ee79af5762a9c52056b8 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Thu, 7 Feb 2019 10:28:07 +0100 Subject: [PATCH 07/16] replaced copyfile with copy in tests --- rowers/datautils.py | 9 +++++---- rowers/tests/statements.py | 5 +++-- rowers/tests/testdata/testdata.csv.gz | Bin 11426 -> 11457 bytes 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/rowers/datautils.py b/rowers/datautils.py index ecf961dc..b19a7597 100644 --- a/rowers/datautils.py +++ b/rowers/datautils.py @@ -304,14 +304,15 @@ def getmaxwattinterval(tt,ww,i): if len(w_roll): # now goes with # data points - should be fixed seconds indexmax = w_roll.idxmax(axis=1) + indexmaxpos = indexmax.get_loc(indexmax) try: - t_0 = tt.ix[indexmax] - t_1 = tt.ix[indexmax-i] - deltas = tt.ix[indexmax-i:indexmax].diff().dropna() + t_0 = tt.iloc[indexmaxpos] + t_1 = tt.iloc[indexmaxpos-i] + deltas = tt.iloc[indexmaxpos-i:indexmaxpos].diff().dropna() testres = 1.0e-3*deltas.max() < 30. if testres: deltat = 1.0e-3*(t_0-t_1) - wmax = w_roll.ix[indexmax] + wmax = w_roll.iloc[indexmaxpos] #if wmax > 800 or wmax*5.0e-4*deltat > 800.0: # wmax = 0 else: diff --git a/rowers/tests/statements.py b/rowers/tests/statements.py index 310b127e..51f1f25a 100644 --- a/rowers/tests/statements.py +++ b/rowers/tests/statements.py @@ -30,7 +30,7 @@ from rowers.tasks import handle_makeplot from rowers.utils import serialize_list,deserialize_list from rowers.utils import NoTokenError from rowers.plannedsessions import get_dates_timeperiod -from shutil import copyfile +from shutil import copyfile, copy from nose.tools import assert_true from mock import Mock, patch from minimocktest import MockTestCase @@ -107,7 +107,8 @@ def get_random_file(filename='rowers/tests/testdata/testdata.csv',name=''): else: newfilename = 'rowers/tests/testdata/temp/'+fromstring+uuid4().hex[:16]+'.'+extension - copyfile(filename,newfilename) + # copyfile(filename,newfilename) + copy(filename,newfilename) thedict = { 'row':row, diff --git a/rowers/tests/testdata/testdata.csv.gz b/rowers/tests/testdata/testdata.csv.gz index ac467768a0522d7eada493284e7c6bc036fba39b..1130e1143fc03cb49f205f164102773277447aa6 100644 GIT binary patch literal 11457 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 bE%OG|dV_eM5SB%T-Ol@e0koFQfOh}@;Aj{F literal 11426 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≧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 w5V Date: Thu, 7 Feb 2019 15:30:30 +0100 Subject: [PATCH 08/16] completing tests for planned sessions --- rowers/templates/plannedsessionteamedit.html | 2 +- rowers/tests/test_plans.py | 145 +++++++++++++++++-- rowers/tests/testdata/testdata.csv.gz | Bin 11457 -> 11426 bytes rowers/views.py | 9 +- 4 files changed, 138 insertions(+), 18 deletions(-) diff --git a/rowers/templates/plannedsessionteamedit.html b/rowers/templates/plannedsessionteamedit.html index a688d92b..5bec388a 100644 --- a/rowers/templates/plannedsessionteamedit.html +++ b/rowers/templates/plannedsessionteamedit.html @@ -67,7 +67,7 @@ Clone

- +

diff --git a/rowers/tests/test_plans.py b/rowers/tests/test_plans.py index e6c93f0a..cbd061f4 100644 --- a/rowers/tests/test_plans.py +++ b/rowers/tests/test_plans.py @@ -1207,7 +1207,7 @@ class PlannedSessionsView(TestCase): self.ps_trimp.save() added = plannedsessions.add_rower_session(self.u.rower,self.ps_trimp) - + added = plannedsessions.add_team_session(self.team,self.ps_trimp) def tearDown(self): try: @@ -1224,10 +1224,7 @@ class PlannedSessionsView(TestCase): b = datetime.date.today()-timezone.timedelta(today.weekday()) e = b+timezone.timedelta(days=6) - expected_url = '/rowers/sessions/5/edit/user/1/?when={b}/{e}'.format( - b=b, - e=e - ) + expected_url = '/rowers/sessions/teamedit/5/' response = self.c.get(url,follow=True) self.assertEqual(response.status_code,200) @@ -1316,7 +1313,7 @@ class PlannedSessionsView(TestCase): self.assertEqual(response.status_code,200) form_data = { - 'team':[1], + 'team':[self.team.id], 'startdate': self.w1.startdatetime.date(), 'enddate': (self.w1.startdatetime+datetime.timedelta(days=5)).date(), 'preferreddate': self.w1.startdatetime.date(), @@ -1326,7 +1323,7 @@ class PlannedSessionsView(TestCase): 'criterium': 'none', 'sessionvalue': 13000, 'sessionunit': 'm', - 'course': None, + 'course': '', 'comment':faker.text() } @@ -1346,36 +1343,152 @@ class PlannedSessionsView(TestCase): login = self.c.login(username=self.u.username, password=self.password) self.assertTrue(login) + + url = '/rowers/sessions/teamedit/{id}/'.format(id=self.ps_trimp.id) + + response = self.c.get(url) + self.assertEqual(response.status_code,200) + + form_data = { + 'team':[1], + 'startdate': self.w1.startdatetime.date(), + 'enddate': (self.w1.startdatetime+datetime.timedelta(days=5)).date(), + 'preferreddate': self.w1.startdatetime.date(), + 'name': faker.word(), + 'sessiontype': 'session', + 'sessionmode': 'distance', + 'criterium': 'none', + 'sessionvalue': 13000, + 'sessionunit': 'm', + 'course': None, + 'comment':faker.text(), + 'members': [self.r.id,self.r2.id] + } + + response = self.c.post(url,follow=True) + self.assertEqual(response.status_code,200) + + def test_coach_view(self): login = self.c.login(username=self.u.username, password=self.password) self.assertTrue(login) + url = '/rowers/sessions/coach/?when={d1}/{d2}'.format( + d1=self.ps_trimp.startdate.strftime("%Y-%m%d"), + d2=self.ps_trimp.enddate.strftime("%Y-%m%d") + ) + + response = self.c.get(url) + self.assertEqual(response.status_code,200) + + def test_plannedsessions_view(self): login = self.c.login(username=self.u.username, password=self.password) self.assertTrue(login) + url = '/rowers/sessions/?when={d1}/{d2}'.format( + d1=self.ps_trimp.startdate.strftime("%Y-%m%d"), + d2=self.ps_trimp.enddate.strftime("%Y-%m%d") + ) + + response = self.c.get(url) + self.assertEqual(response.status_code,200) + def test_plannedsessions_print_view(self): login = self.c.login(username=self.u.username, password=self.password) self.assertTrue(login) - def plannedsession_manage_view(self): + url = '/rowers/sessions/print/?when={d1}/{d2}'.format( + d1=self.ps_trimp.startdate.strftime("%Y-%m%d"), + d2=self.ps_trimp.enddate.strftime("%Y-%m%d") + ) + + response = self.c.get(url) + self.assertEqual(response.status_code,200) + + def test_plannedsession_manage_view(self): login = self.c.login(username=self.u.username, password=self.password) self.assertTrue(login) - def plannedsession_edit_view(self): + url = '/rowers/sessions/manage/session/{id}/?when={d1}/{d2}'.format( + d1=self.ps_trimp.startdate.strftime("%Y-%m%d"), + d2=self.ps_trimp.enddate.strftime("%Y-%m%d"), + id=self.ps_trimp.id, + ) + + response = self.c.get(url) + self.assertEqual(response.status_code,200) + + def test_plannedsession_edit_view(self): login = self.c.login(username=self.u.username, password=self.password) self.assertTrue(login) + url = '/rowers/sessions/{id}/edit/'.format( + id=self.ps_time.id, + ) - def plannedsession_detach_view(self): - login = self.c.login(username=self.u.username, password=self.password) - self.assertTrue(login) + response = self.c.get(url) + self.assertEqual(response.status_code,200) - def plannedsession_view(self): - login = self.c.login(username=self.u.username, password=self.password) - self.assertTrue(login) + form_data = { + 'startdate': self.w1.startdatetime.date(), + 'enddate': (self.w1.startdatetime+datetime.timedelta(days=5)).date(), + 'preferreddate': self.w1.startdatetime.date(), + 'name': faker.word(), + 'sessiontype': 'session', + 'sessionmode': 'distance', + 'criterium': 'none', + 'sessionvalue': 13000, + 'sessionunit': 'm', + 'course': None, + 'comment':faker.text(), + } - def plannedsession_delete_view(self): + response = self.c.post(url,follow=True) + self.assertEqual(response.status_code,200) + + + def test_plannedsession_detach_view(self): + + self.ps_time.startdate = self.w1.date-datetime.timedelta(days=3) + self.ps_time.enddate = self.w1.date-datetime.timedelta(days=3) + self.ps_time.save() + + self.w1.plannedsession = self.ps_time + self.w1.save() + login = self.c.login(username=self.u.username, password=self.password) self.assertTrue(login) + url = '/rowers/sessions/{psid}/detach/{id}/'.format( + psid=self.ps_time.id, + id = self.w1.id, + ) + + response = self.c.get(url,follow=True) + self.assertEqual(response.status_code,200) + + def test_plannedsession_view(self): + login = self.c.login(username=self.u.username, password=self.password) + self.assertTrue(login) + + url = '/rowers/sessions/{psid}/'.format( + psid = self.ps_time.id + ) + + response = self.c.get(url) + self.assertEqual(response.status_code,200) + + def test_plannedsession_delete_view(self): + login = self.c.login(username=self.u.username, password=self.password) + self.assertTrue(login) + + url = '/rowers/sessions/{psid}/delete/'.format( + psid = self.ps_time.id + ) + + response = self.c.get(url) + self.assertEqual(response.status_code,200) + + response = self.c.post(url,follow=True) + self.assertEqual(response.status_code,200) diff --git a/rowers/tests/testdata/testdata.csv.gz b/rowers/tests/testdata/testdata.csv.gz index 1130e1143fc03cb49f205f164102773277447aa6..38d7cb1822b88e7c71f7c90c9eda8f0e97a63af6 100644 GIT binary patch literal 11426 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≧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 bE%OG|dV_eM5SB%T-Ol@e0koFQfOh}@;Aj{F diff --git a/rowers/views.py b/rowers/views.py index 45038b24..1df96516 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -14440,7 +14440,14 @@ def plannedsession_teamcreate_view(request, sessioncreateform = PlannedSessionForm(request.POST) sessionteamselectform = PlannedSessionTeamForm( request.user,request.POST - ) + ) + + print sessioncreateform.is_valid(),'sessioncreateform' + print sessioncreateform.errors,'errors' + print sessionteamselectform.is_valid(),'teamselectform' + print sessionteamselectform.errors,'errors' + raise ValueError + if sessioncreateform.is_valid() and sessionteamselectform.is_valid(): cd = sessioncreateform.cleaned_data startdate = cd['startdate'] From a3b309fb16e4aa9a5d8449c5a919f27bcc55477a Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Thu, 7 Feb 2019 16:02:04 +0100 Subject: [PATCH 09/16] moving to views --- rowers/tests/test_plans.py | 4 +- rowers/tests/testdata/testdata.csv.gz | Bin 11426 -> 11426 bytes rowers/views/.#__init__.py | 1 + rowers/views/analysisviews.py | 3372 +++++++++++++++ rowers/views/apiviews.py | 182 + rowers/views/errorviews.py | 30 + rowers/views/exportviews.py | 220 + rowers/views/importviews.py | 1604 ++++++++ rowers/views/otherviews.py | 142 + rowers/views/paymentviews.py | 559 +++ rowers/views/planviews.py | 2575 ++++++++++++ rowers/views/racesviews.py | 2305 +++++++++++ rowers/views/statements.py | 1148 ++++++ rowers/views/teamviews.py | 536 +++ rowers/views/userviews.py | 485 +++ rowers/views/workoutviews.py | 5407 +++++++++++++++++++++++++ rowers/{views.py => views_old.py} | 6 - 17 files changed, 18568 insertions(+), 8 deletions(-) create mode 100644 rowers/views/.#__init__.py create mode 100644 rowers/views/analysisviews.py create mode 100644 rowers/views/apiviews.py create mode 100644 rowers/views/errorviews.py create mode 100644 rowers/views/exportviews.py create mode 100644 rowers/views/importviews.py create mode 100644 rowers/views/otherviews.py create mode 100644 rowers/views/paymentviews.py create mode 100644 rowers/views/planviews.py create mode 100644 rowers/views/racesviews.py create mode 100644 rowers/views/statements.py create mode 100644 rowers/views/teamviews.py create mode 100644 rowers/views/userviews.py create mode 100644 rowers/views/workoutviews.py rename rowers/{views.py => views_old.py} (99%) diff --git a/rowers/tests/test_plans.py b/rowers/tests/test_plans.py index cbd061f4..52006a29 100644 --- a/rowers/tests/test_plans.py +++ b/rowers/tests/test_plans.py @@ -1360,7 +1360,7 @@ class PlannedSessionsView(TestCase): 'criterium': 'none', 'sessionvalue': 13000, 'sessionunit': 'm', - 'course': None, + 'course': '', 'comment':faker.text(), 'members': [self.r.id,self.r2.id] } @@ -1440,7 +1440,7 @@ class PlannedSessionsView(TestCase): 'criterium': 'none', 'sessionvalue': 13000, 'sessionunit': 'm', - 'course': None, + 'course': '', 'comment':faker.text(), } diff --git a/rowers/tests/testdata/testdata.csv.gz b/rowers/tests/testdata/testdata.csv.gz index 38d7cb1822b88e7c71f7c90c9eda8f0e97a63af6..2c1957e019950166bf00f7f15b8ee8c99670d51f 100644 GIT binary patch delta 15 WcmZ1!xhRrNzMF$%xzk3rIXVC<5(OUs delta 15 WcmZ1!xhRrNzMF%i* 1: + modality = 'all' + else: + modality = modalities[0] + else: + modalities = [m[0] for m in mytypes.workouttypes] + modality = 'all' + + + try: + rankingonly = options['rankingonly'] + except KeyError: + rankingonly = False + + try: + includereststrokes = options['includereststrokes'] + except KeyError: + includereststrokes = False + + + workstrokesonly = not includereststrokes + + waterboattype = mytypes.waterboattype + + + if startdatestring != "": + startdate = iso8601.parse_date(startdatestring) + + if enddatestring != "": + enddate = iso8601.parse_date(enddatestring) + + if enddate < startdate: + s = enddate + enddate = startdate + startdate = s + + + # get all indoor rows of in date range + + # process form + if request.method == 'POST': + form = DateRangeForm(request.POST) + modalityform = TrendFlexModalForm(request.POST) + flexaxesform = FlexAxesForm(request,request.POST) + if form.is_valid(): + startdate = form.cleaned_data['startdate'] + enddate = form.cleaned_data['enddate'] + if startdate > enddate: + s = enddate + enddate = startdate + startdate = s + startdatestring = startdate.strftime('%Y-%m-%d') + enddatestring = enddate.strftime('%Y-%m-%d') + if modalityform.is_valid(): + modality = modalityform.cleaned_data['modality'] + waterboattype = modalityform.cleaned_data['waterboattype'] + rankingonly = modalityform.cleaned_data['rankingonly'] + if modality == 'all': + modalities = [m[0] for m in mytypes.workouttypes] + else: + modalities = [modality] + + if modality != 'water': + waterboattype = [b[0] for b in mytypes.boattypes] + + + request.session['modalities'] = modalities + request.session['waterboattype'] = waterboattype + request.session['rankingonly'] = rankingonly + form = DateRangeForm(initial={ + 'startdate': startdate, + 'enddate': enddate, + }) + if flexaxesform.is_valid(): + xparam = flexaxesform.cleaned_data['xaxis'] + yparam1 = flexaxesform.cleaned_data['yaxis1'] + yparam2 = flexaxesform.cleaned_data['yaxis2'] + else: + form = DateRangeForm(initial={ + 'startdate': startdate, + 'enddate': enddate, + }) + includereststrokes = False + + workstrokesonly = not includereststrokes + modalityform = TrendFlexModalForm( + initial={ + 'modality':modality, + 'waterboattype':waterboattype, + 'rankingonly':rankingonly, + } + ) + initial = { + 'xaxis':xparam, + 'yaxis1':yparam1, + 'yaxis2':yparam2 + } + flexaxesform = FlexAxesForm(request,initial=initial) + + negtypes = [] + for b in mytypes.boattypes: + if b[0] not in waterboattype: + negtypes.append(b[0]) + + + + script = '' + div = get_call() + js_resources = '' + css_resources = '' + + + + + options = { + 'xparam': xparam, + 'yparam1': yparam1, + 'yparam2': yparam2, + 'modality': modality, + 'theuser': theuser.id, + 'waterboattype':waterboattype, + 'startdatestring':startdatestring, + 'enddatestring':enddatestring, + 'rankingonly':rankingonly, + 'includereststrokes':includereststrokes, + } + + request.session['options'] = options + + promember=0 + mayedit=0 + if not request.user.is_anonymous(): + result = request.user.is_authenticated() and ispromember(request.user) + if result: + promember = 1 + + + request.session['options'] = options + + + return render(request, 'cum_flex.html', + {'interactiveplot':script, + 'the_div':div, + 'js_res': js_resources, + 'css_res':css_resources, + 'id':theuser, + 'rower':r, + 'active':'nav-analysis', + 'theuser':theuser, + 'startdate':startdate, + 'enddate':enddate, + 'form':form, + 'optionsform':modalityform, + 'xparam':xparam, + 'yparam1':yparam1, + 'yparam2':yparam2, + 'promember':promember, + 'teams':get_my_teams(request.user), + 'flexaxesform':flexaxesform, + }) + + +def planrequired_view(request): + messages.info(request,"This functionality requires Coach or Self-Coach membership") + + return HttpResponseRedirect(reverse(paidplans_view)) + +@user_passes_test(hasplannedsessions,login_url="/rowers/paidplans", + message="This functionality requires a Coach or Self-Coach plan", + redirect_field_name=None) +def fitnessmetric_view(request,id=0,mode='rower', + startdate=timezone.now()-timezone.timedelta(days=365), + enddate=timezone.now()): + + + therower = getrequestrower(request,userid=id) + theuser = therower.user + + + if request.method == 'POST': + form = FitnessMetricForm(request.POST) + if form.is_valid(): + startdate = form.cleaned_data['startdate'] + enddate = form.cleaned_data['enddate'] + mode = form.cleaned_data['mode'] + else: + form = FitnessMetricForm() + + fitnessmetrics = PowerTimeFitnessMetric.objects.filter( + user=theuser, + date__gte=startdate, + date__lte=enddate) + + script,thediv = fitnessmetric_chart( + fitnessmetrics,theuser, + workoutmode=mode + ) + + return render(request,'fitnessmetric.html', + { + 'rower':therower, + 'active':'nav-analysis', + 'chartscript':script, + 'the_div':thediv, + 'mode':mode, + 'form':form, + }) + + +# Show ranking distances including predicted paces +@login_required() +def rankings_view(request,theuser=0, + startdate=timezone.now()-datetime.timedelta(days=365), + enddate=timezone.now(), + deltadays=-1, + startdatestring="", + enddatestring=""): + + if deltadays>0: + startdate = enddate-datetime.timedelta(days=int(deltadays)) + + if startdatestring != "": + startdate = iso8601.parse_date(startdatestring) + + if enddatestring != "": + enddate = iso8601.parse_date(enddatestring) + + if enddate < startdate: + s = enddate + enddate = startdate + startdate = s + + if theuser == 0: + theuser = request.user.id + + promember=0 + if not request.user.is_anonymous(): + r = getrower(request.user) + if r.birthdate: + age = calculate_age(r.birthdate) + worldclasspower = int(metrics.getagegrouprecord( + age, + sex=r.sex, + weightcategory=r.weightcategory, + )) + else: + worldclasspower = None + + result = request.user.is_authenticated() and ispromember(request.user) + if result: + promember=1 + + # get all indoor rows in date range + + # process form + if request.method == 'POST' and "daterange" in request.POST: + dateform = DateRangeForm(request.POST) + deltaform = DeltaDaysForm(request.POST) + if dateform.is_valid(): + startdate = dateform.cleaned_data['startdate'] + enddate = dateform.cleaned_data['enddate'] + if startdate > enddate: + s = enddate + enddate = startdate + startdate = s + elif request.method == 'POST' and "datedelta" in request.POST: + deltaform = DeltaDaysForm(request.POST) + if deltaform.is_valid(): + deltadays = deltaform.cleaned_data['deltadays'] + if deltadays: + enddate = timezone.now() + startdate = enddate-datetime.timedelta(days=deltadays) + if startdate > enddate: + s = enddate + enddate = startdate + startdate = s + dateform = DateRangeForm(initial={ + 'startdate': startdate, + 'enddate': enddate, + }) + else: + dateform = DateRangeForm() + deltaform = DeltaDaysForm() + + else: + dateform = DateRangeForm(initial={ + 'startdate': startdate, + 'enddate': enddate, + }) + deltaform = DeltaDaysForm() + + # get all 2k (if any) - this rower, in date range + try: + r = getrower(theuser) + except Rower.DoesNotExist: + allergworkouts = [] + r=0 + + + try: + uu = User.objects.get(id=theuser) + except User.DoesNotExist: + uu = '' + + + # test to fix bug + startdate = datetime.datetime.combine(startdate,datetime.time()) + enddate = datetime.datetime.combine(enddate,datetime.time(23,59,59)) + #enddate = enddate+datetime.timedelta(days=1) + + thedistances = [] + theworkouts = [] + thesecs = [] + + rankingdistances.sort() + rankingdurations.sort() + + for rankingdistance in rankingdistances: + + workouts = Workout.objects.filter( + user=r,distance=rankingdistance, + workouttype__in=['rower','dynamic','slides'], + rankingpiece=True, + startdatetime__gte=startdate, + startdatetime__lte=enddate + ).order_by('duration') + if workouts: + thedistances.append(rankingdistance) + theworkouts.append(workouts[0]) + + timesecs = 3600*workouts[0].duration.hour + timesecs += 60*workouts[0].duration.minute + timesecs += workouts[0].duration.second + timesecs += 1.e-6*workouts[0].duration.microsecond + + thesecs.append(timesecs) + + for rankingduration in rankingdurations: + + workouts = Workout.objects.filter( + user=r,duration=rankingduration, + workouttype='rower', + rankingpiece=True, + startdatetime__gte=startdate, + startdatetime__lte=enddate + ).order_by('-distance') + if workouts: + thedistances.append(workouts[0].distance) + theworkouts.append(workouts[0]) + + timesecs = 3600*workouts[0].duration.hour + timesecs += 60*workouts[0].duration.minute + timesecs += workouts[0].duration.second + timesecs += 1.e-5*workouts[0].duration.microsecond + + thesecs.append(timesecs) + + thedistances = np.array(thedistances) + thesecs = np.array(thesecs) + + thevelos = thedistances/thesecs + theavpower = 2.8*(thevelos**3) + + + # create interactive plot + if len(thedistances) !=0 : + res = interactive_cpchart( + r,thedistances,thesecs,theavpower, + theworkouts,promember=promember + ) + script = res[0] + div = res[1] + paulslope = res[2] + paulintercept = res[3] + p1 = res[4] + message = res[5] + else: + script = '' + div = '

No ranking pieces found.

' + paulslope = 1 + paulintercept = 1 + p1 = [1,1,1,1] + message = "" + + + if request.method == 'POST' and "piece" in request.POST: + form = PredictedPieceForm(request.POST) + if form.is_valid(): + value = form.cleaned_data['value'] + hourvalue,value = divmod(value,60) + if hourvalue >= 24: + hourvalue = 23 + pieceunit = form.cleaned_data['pieceunit'] + if pieceunit == 'd': + rankingdistances.append(value) + else: + rankingdurations.append(datetime.time(minute=int(value),hour=int(hourvalue))) + else: + form = PredictedPieceForm() + + rankingdistances.sort() + rankingdurations.sort() + + + predictions = [] + cpredictions = [] + + + for rankingdistance in rankingdistances: + # Paul's model + p = paulslope*np.log10(rankingdistance)+paulintercept + velo = 500./p + t = rankingdistance/velo + pwr = 2.8*(velo**3) + a = {'distance':rankingdistance, + 'duration':timedeltaconv(t), + 'pace':timedeltaconv(p), + 'power':int(pwr)} + predictions.append(a) + + # CP model - + pwr2 = p1[0]/(1+t/p1[2]) + pwr2 += p1[1]/(1+t/p1[3]) + + if pwr2 <= 0: + pwr2 = 50. + + velo2 = (pwr2/2.8)**(1./3.) + + if np.isnan(velo2) or velo2 <= 0: + velo2 = 1.0 + + t2 = rankingdistance/velo2 + + pwr3 = p1[0]/(1+t2/p1[2]) + pwr3 += p1[1]/(1+t2/p1[3]) + + if pwr3 <= 0: + pwr3 = 50. + + velo3 = (pwr3/2.8)**(1./3.) + if np.isnan(velo3) or velo3 <= 0: + velo3 = 1.0 + + t3 = rankingdistance/velo3 + p3 = 500./velo3 + + a = {'distance':rankingdistance, + 'duration':timedeltaconv(t3), + 'pace':timedeltaconv(p3), + 'power':int(pwr3)} + cpredictions.append(a) + + + + + for rankingduration in rankingdurations: + t = 3600.*rankingduration.hour + t += 60.*rankingduration.minute + t += rankingduration.second + t += rankingduration.microsecond/1.e6 + + # Paul's model + ratio = paulintercept/paulslope + + u = ((2**(2+ratio))*(5.**(3+ratio))*t*np.log(10))/paulslope + + d = 500*t*np.log(10.) + d = d/(paulslope*lambertw(u)) + d = d.real + + velo = d/t + p = 500./velo + pwr = 2.8*(velo**3) + a = {'distance':int(d), + 'duration':timedeltaconv(t), + 'pace':timedeltaconv(p), + 'power':int(pwr)} + predictions.append(a) + + # CP model + pwr = p1[0]/(1+t/p1[2]) + pwr += p1[1]/(1+t/p1[3]) + + if pwr <= 0: + pwr = 50. + + velo = (pwr/2.8)**(1./3.) + + if np.isnan(velo) or velo <=0: + velo = 1.0 + + d = t*velo + p = 500./velo + a = {'distance':int(d), + 'duration':timedeltaconv(t), + 'pace':timedeltaconv(p), + 'power':int(pwr)} + cpredictions.append(a) + + + messages.error(request,message) + return render(request, 'rankings.html', + {'rankingworkouts':theworkouts, + 'interactiveplot':script, + 'the_div':div, + 'predictions':predictions, + 'cpredictions':cpredictions, + 'nrdata':len(thedistances), + 'form':form, + 'rower':r, + 'active':'nav-analysis', + 'dateform':dateform, + 'deltaform':deltaform, + 'worldclasspower':worldclasspower, + 'id': theuser, + 'theuser':uu, + 'startdate':startdate, + 'enddate':enddate, + 'teams':get_my_teams(request.user), + }) + +@login_required() +def ajax_agegrouprecords(request, + age=25, + sex='female', + weightcategory='hwt', + userid=0): + + wcdurations = [] + wcpower = [] + durations = [1,4,30,60] + distances = [100,500,1000,2000,5000,6000,10000,21097,42195] + + df = pd.DataFrame( + list( + C2WorldClassAgePerformance.objects.filter( + sex=sex, + weightcategory=weightcategory + ).values() + ) + ) + + jsondf = df.to_json() + + job = myqueue(queue, + handle_getagegrouprecords, + jsondf,distances,durations,age,sex,weightcategory, + ) + + + + return JSONResponse( + { + 'job':job.id + } + ) + + +# Show ranking distances including predicted paces +@login_required() +def rankings_view2(request,theuser=0, + startdate=timezone.now()-datetime.timedelta(days=365), + enddate=timezone.now(), + deltadays=-1, + startdatestring="", + enddatestring=""): + + if deltadays>0: + startdate = enddate-datetime.timedelta(days=int(deltadays)) + + if startdatestring != "": + startdate = iso8601.parse_date(startdatestring) + + if enddatestring != "": + enddate = iso8601.parse_date(enddatestring) + + if enddate < startdate: + s = enddate + enddate = startdate + startdate = s + + if theuser == 0: + theuser = request.user.id + else: + lastupdated = "01-01-1900" + + + promember=0 + if not request.user.is_anonymous(): + r = getrower(request.user) + wcdurations = [] + wcpower = [] + + lastupdated = "01-01-1900" + userid = 0 + if 'options' in request.session: + options = request.session['options'] + try: + wcdurations = options['wcdurations'] + wcpower = options['wcpower'] + lastupdated = options['lastupdated'] + except KeyError: + pass + try: + userid = options['userid'] + except KeyError: + userid = 0 + else: + options = {} + + + + lastupdatedtime = arrow.get(lastupdated).timestamp + current_time = arrow.utcnow().timestamp + + deltatime_seconds = current_time - lastupdatedtime + recalc = False + if str(userid) != str(theuser) or deltatime_seconds > 3600: + recalc = True + options['lastupdated'] = arrow.utcnow().isoformat() + else: + recalc = False + + options['userid'] = theuser + + if r.birthdate: + age = calculate_age(r.birthdate) + else: + worldclasspower = None + age = 0 + + agerecords = CalcAgePerformance.objects.filter( + age = age, + sex = r.sex, + weightcategory = r.weightcategory) + + if len(agerecords) == 0: + recalc = True + wcpower = [] + wcduration = [] + else: + wcdurations = [] + wcpower = [] + for record in agerecords: + wcdurations.append(record.duration) + wcpower.append(record.power) + + options['wcpower'] = wcpower + options['wcdurations'] = wcdurations + if theuser: + options['userid'] = theuser + + request.session['options'] = options + + + result = request.user.is_authenticated() and ispromember(request.user) + if result: + promember=1 + + # get all indoor rows in date range + + # process form + if request.method == 'POST' and "daterange" in request.POST: + dateform = DateRangeForm(request.POST) + deltaform = DeltaDaysForm(request.POST) + if dateform.is_valid(): + startdate = dateform.cleaned_data['startdate'] + enddate = dateform.cleaned_data['enddate'] + if startdate > enddate: + s = enddate + enddate = startdate + startdate = s + elif request.method == 'POST' and "datedelta" in request.POST: + deltaform = DeltaDaysForm(request.POST) + if deltaform.is_valid(): + deltadays = deltaform.cleaned_data['deltadays'] + if deltadays: + enddate = timezone.now() + startdate = enddate-datetime.timedelta(days=deltadays) + if startdate > enddate: + s = enddate + enddate = startdate + startdate = s + dateform = DateRangeForm(initial={ + 'startdate': startdate, + 'enddate': enddate, + }) + else: + dateform = DateRangeForm() + deltaform = DeltaDaysForm() + + else: + dateform = DateRangeForm(initial={ + 'startdate': startdate, + 'enddate': enddate, + }) + deltaform = DeltaDaysForm() + + # get all 2k (if any) - this rower, in date range + try: + r = getrower(theuser) + except Rower.DoesNotExist: + allergworkouts = [] + r=0 + + + try: + uu = User.objects.get(id=theuser) + except User.DoesNotExist: + uu = '' + + + # test to fix bug + startdate = datetime.datetime.combine(startdate,datetime.time()) + enddate = datetime.datetime.combine(enddate,datetime.time(23,59,59)) + #enddate = enddate+datetime.timedelta(days=1) + + + thedistances = [] + theworkouts = [] + thesecs = [] + + + + rankingdistances.sort() + rankingdurations.sort() + + for rankingdistance in rankingdistances: + + workouts = Workout.objects.filter( + user=r,distance=rankingdistance, + workouttype__in=['rower','dynamic','slides'], + rankingpiece=True, + startdatetime__gte=startdate, + startdatetime__lte=enddate).order_by('duration') + if workouts: + thedistances.append(rankingdistance) + theworkouts.append(workouts[0]) + + timesecs = 3600*workouts[0].duration.hour + timesecs += 60*workouts[0].duration.minute + timesecs += workouts[0].duration.second + timesecs += 1.e-6*workouts[0].duration.microsecond + + thesecs.append(timesecs) + + for rankingduration in rankingdurations: + + workouts = Workout.objects.filter( + user=r,duration=rankingduration, + workouttype='rower', + rankingpiece=True, + startdatetime__gte=startdate, + startdatetime__lte=enddate).order_by('-distance') + if workouts: + thedistances.append(workouts[0].distance) + theworkouts.append(workouts[0]) + + timesecs = 3600*workouts[0].duration.hour + timesecs += 60*workouts[0].duration.minute + timesecs += workouts[0].duration.second + timesecs += 1.e-5*workouts[0].duration.microsecond + + thesecs.append(timesecs) + + thedistances = np.array(thedistances) + thesecs = np.array(thesecs) + + thevelos = thedistances/thesecs + theavpower = 2.8*(thevelos**3) + + + # create interactive plot + if len(thedistances) !=0 : + res = interactive_cpchart( + r,thedistances,thesecs,theavpower, + theworkouts,promember=promember, + wcdurations=wcdurations,wcpower=wcpower + ) + script = res[0] + div = res[1] + paulslope = res[2] + paulintercept = res[3] + p1 = res[4] + message = res[5] + try: + testcalc = pd.Series(res[6])*3 + except TypeError: + age = 0 + + else: + script = '' + div = '

No ranking pieces found.

' + paulslope = 1 + paulintercept = 1 + p1 = [1,1,1,1] + message = "" + + + if request.method == 'POST' and "piece" in request.POST: + form = PredictedPieceForm(request.POST) + if form.is_valid(): + value = form.cleaned_data['value'] + hourvalue,value = divmod(value,60) + if hourvalue >= 24: + hourvalue = 23 + pieceunit = form.cleaned_data['pieceunit'] + if pieceunit == 'd': + rankingdistances.append(value) + else: + rankingdurations.append(datetime.time(minute=int(value),hour=int(hourvalue))) + else: + form = PredictedPieceForm() + + rankingdistances.sort() + rankingdurations.sort() + + + predictions = [] + cpredictions = [] + + + for rankingdistance in rankingdistances: + # Paul's model + p = paulslope*np.log10(rankingdistance)+paulintercept + velo = 500./p + t = rankingdistance/velo + pwr = 2.8*(velo**3) + a = {'distance':rankingdistance, + 'duration':timedeltaconv(t), + 'pace':timedeltaconv(p), + 'power':int(pwr)} + predictions.append(a) + + # CP model - + pwr2 = p1[0]/(1+t/p1[2]) + pwr2 += p1[1]/(1+t/p1[3]) + + if pwr2 <= 0: + pwr2 = 50. + + velo2 = (pwr2/2.8)**(1./3.) + + if np.isnan(velo2) or velo2 <= 0: + velo2 = 1.0 + + t2 = rankingdistance/velo2 + + pwr3 = p1[0]/(1+t2/p1[2]) + pwr3 += p1[1]/(1+t2/p1[3]) + + if pwr3 <= 0: + pwr3 = 50. + + velo3 = (pwr3/2.8)**(1./3.) + if np.isnan(velo3) or velo3 <= 0: + velo3 = 1.0 + + t3 = rankingdistance/velo3 + p3 = 500./velo3 + + a = {'distance':rankingdistance, + 'duration':timedeltaconv(t3), + 'pace':timedeltaconv(p3), + 'power':int(pwr3)} + cpredictions.append(a) + + + + + for rankingduration in rankingdurations: + t = 3600.*rankingduration.hour + t += 60.*rankingduration.minute + t += rankingduration.second + t += rankingduration.microsecond/1.e6 + + # Paul's model + ratio = paulintercept/paulslope + + u = ((2**(2+ratio))*(5.**(3+ratio))*t*np.log(10))/paulslope + + d = 500*t*np.log(10.) + d = d/(paulslope*lambertw(u)) + d = d.real + + velo = d/t + p = 500./velo + pwr = 2.8*(velo**3) + try: + a = {'distance':int(d), + 'duration':timedeltaconv(t), + 'pace':timedeltaconv(p), + 'power':int(pwr)} + predictions.append(a) + except: + pass + + # CP model + pwr = p1[0]/(1+t/p1[2]) + pwr += p1[1]/(1+t/p1[3]) + + if pwr <= 0: + pwr = 50. + + velo = (pwr/2.8)**(1./3.) + + if np.isnan(velo) or velo <=0: + velo = 1.0 + + d = t*velo + p = 500./velo + a = {'distance':int(d), + 'duration':timedeltaconv(t), + 'pace':timedeltaconv(p), + 'power':int(pwr)} + cpredictions.append(a) + + if recalc: + wcdurations = [] + wcpower = [] + durations = [1,4,30,60] + distances = [100,500,1000,2000,5000,6000,10000,21097,42195] + + df = pd.DataFrame( + list( + C2WorldClassAgePerformance.objects.filter( + sex=r.sex, + weightcategory=r.weightcategory + ).values() + ) + ) + + jsondf = df.to_json() + + job = myqueue(queue, + handle_getagegrouprecords, + jsondf,distances,durations,age,r.sex,r.weightcategory) + try: + request.session['async_tasks'] += [(job.id,'agegrouprecords')] + except KeyError: + request.session['async_tasks'] = [(job.id,'agegrouprecords')] + + + + messages.error(request,message) + return render(request, 'rankings.html', + {'rankingworkouts':theworkouts, + 'interactiveplot':script, + 'the_div':div, + 'predictions':predictions, + 'cpredictions':cpredictions, + 'nrdata':len(thedistances), + 'form':form, + 'dateform':dateform, + 'deltaform':deltaform, + 'id': theuser, + 'theuser':uu, + 'rower':r, + 'active':'nav-analysis', + 'age':age, + 'sex':r.sex, + 'recalc':recalc, + 'weightcategory':r.weightcategory, + 'startdate':startdate, + 'enddate':enddate, + 'teams':get_my_teams(request.user), + }) + + +# Show ranking distances including predicted paces +@user_passes_test(ispromember,login_url="/rowers/paidplans", + message="This functionality requires a Pro plan or higher", + redirect_field_name=None) +def otwrankings_view(request,theuser=0, + startdate=timezone.now()-datetime.timedelta(days=365), + enddate=timezone.now(), + startdatestring="", + enddatestring=""): + + if startdatestring != "": + try: + startdate = iso8601.parse_date(startdatestring) + except ParseError: + pass + + if enddatestring != "": + try: + enddate = iso8601.parse_date(enddatestring) + except ParseError: + pass + + if enddate < startdate: + s = enddate + enddate = startdate + startdate = s + + if theuser == 0: + if 'rowerid' in request.session: + try: + r = Rower.objects.get(id=request.session['rowerid']) + theuser = r.user.id + except Rower.DoesNotExist: + theuser = request.user.id + else: + theuser = request.user.id + + promember=0 + if not request.user.is_anonymous(): + r = Rower.objects.get(user=request.user) + result = request.user.is_authenticated() and ispromember(request.user) + if result: + promember=1 + + # get all OTW rows in date range + + # process form + if request.method == 'POST': + dateform = DateRangeForm(request.POST) + if dateform.is_valid(): + startdate = dateform.cleaned_data['startdate'] + enddate = dateform.cleaned_data['enddate'] + if startdate > enddate: + s = enddate + enddate = startdate + startdate = s + form = PredictedPieceFormNoDistance(request.POST) + if form.is_valid(): + value = form.cleaned_data['value'] + else: + value = None + trankingdurations = form.cleaned_data['trankingdurations'] + trankingdurations = [ + datetime.datetime.strptime(d,"%H:%M:%S").time() for d in trankingdurations + ] + if value: + hourvalue,tvalue = divmod(value,60) + hourvalue = int(hourvalue) + minutevalue = int(tvalue) + tvalue = int(60*(tvalue-minutevalue)) + if hourvalue >= 24: + hourvalue = 23 + trankingdurations.append(datetime.time( + minute=minutevalue, + hour=hourvalue, + second=tvalue + )) + + else: + form = PredictedPieceFormNoDistance() + dateform = DateRangeForm(initial={ + 'startdate': startdate, + 'enddate': enddate, + }) + workouttypes = ['rower','slides','dynamic'] + trankingdurations = rankingdurations + + # get all 2k (if any) - this rower, in date range + try: + r = Rower.objects.get(user=theuser) + request.session['rowerid'] = r.id + except Rower.DoesNotExist: + raise Http404("Rower doesn't exist") + + + + try: + uu = User.objects.get(id=theuser) + except User.DoesNotExist: + uu = '' + + + # test to fix bug + startdate = datetime.datetime.combine(startdate,datetime.time()) + enddate = datetime.datetime.combine(enddate,datetime.time(23,59,59)) + #enddate = enddate+datetime.timedelta(days=1) + + + + thedistances = [] + theworkouts = [] + thesecs = [] + + theworkouts = Workout.objects.filter( + user=r,rankingpiece=True, + workouttype='water', + startdatetime__gte=startdate, + startdatetime__lte=enddate + ).order_by( + "-startdatetime" + ) + + + delta,cpvalue,avgpower = dataprep.fetchcp(r,theworkouts) + + runningjob = 0 + + taskstatus = get_stored_tasks_status(request) + for task in taskstatus: + if task['func_name'] == 'updatecpwater': + if 'success' in task['status'].lower() or 'finished' in task['status'].lower(): + runningjob = 1 + messages.info(request,'CP chart data have been updated') + remove_asynctask(request,task['id']) + elif 'fail' in task['status'].lower(): + runningjob = 0 + try: + remove_asynctask(request,task[id]) + messages.error(request,'Oh, your task failed') + except KeyError: + pass + elif 'started' in task['status'].lower(): + messages.info(request,'Busy updating CP chart data') + runningjob = 1 + elif 'queued' in task['status'].lower() or 'pending' in task['status'].lower(): + messages.info(request,'Getting ready to update CP chart data') + runningjob = 1 + + + + if not runningjob: + job = dataprep.runcpupdate( + r,type='water', + startdate=startdate, + enddate=enddate + ) + request.session['job_id'] = job.id + try: + request.session['async_tasks'] += [(job.id,'updatecpwater')] + except KeyError: + request.session['async_tasks'] = [(job.id,'updatecpwater')] + messages.info(request,'New calculation queued. Page will reload automatically. You can check the status of your calculations here') + + powerdf = pd.DataFrame({ + 'Delta':delta, + 'CP':cpvalue, + }) + + if powerdf.empty: + messages.info(request,'Your calculations are running in the background. Page will reload automatically. You can check the status of your calculations here') + + powerdf = powerdf[powerdf['CP']>0] + powerdf.dropna(axis=0,inplace=True) + powerdf.sort_values(['Delta','CP'],ascending=[1,0],inplace=True) + powerdf.drop_duplicates(subset='Delta',keep='first',inplace=True) + + + rowername = r.user.first_name+" "+r.user.last_name + # create interactive plot + if len(powerdf) !=0 : + res = interactive_otwcpchart(powerdf,promember=promember,rowername=rowername) + script = res[0] + div = res[1] + p1 = res[2] + ratio = res[3] + r.p0 = p1[0] + r.p1 = p1[1] + r.p2 = p1[2] + r.p3 = p1[3] + r.cpratio = ratio + r.save() + paulslope = 1 + paulintercept = 1 + message = res[4] + else: + script = '' + div = '

No ranking pieces found.

' + paulslope = 1 + paulintercept = 1 + p1 = [1,1,1,1] + message = "" + + + + + cpredictions = [] + + for rankingduration in trankingdurations: + t = 3600.*rankingduration.hour + t += 60.*rankingduration.minute + t += rankingduration.second + t += rankingduration.microsecond/1.e6 + + + # CP model + pwr = p1[0]/(1+t/p1[2]) + pwr += p1[1]/(1+t/p1[3]) + + + if pwr <= 0: + pwr = 50. + + + if not np.isnan(pwr): + try: + pwr2 = pwr*ratio + except: + pwr2 = pwr + + a = { + 'duration':timedeltaconv(t), + 'power':int(pwr), + 'upper':int(pwr2)} + cpredictions.append(a) + + + + startdatestring = startdate.strftime('%Y-%m-%d') + enddatestring = enddate.strftime('%Y-%m-%d') + request.session['startdate'] = startdatestring + request.session['enddate'] = enddatestring + + messages.error(request,message) + return render(request, 'otwrankings.html', + {'rankingworkouts':theworkouts, + 'interactiveplot':script, + 'the_div':div, + 'cpredictions':cpredictions, + 'rower':r, + 'active':'nav-analysis', + 'avgpower':avgpower, + 'form':form, + 'dateform':dateform, + 'id': theuser, + 'theuser':uu, + 'startdate':startdate, + 'enddate':enddate, + 'teams':get_my_teams(request.user), + 'workouttype':'water', + }) + +@login_required() +def otecp_toadmin_view(request,theuser=0, + startdate=timezone.now()-datetime.timedelta(days=365), + enddate=timezone.now(), + startdatestring="", + enddatestring="", + ): + + if startdatestring != "": + try: + startdate = iso8601.parse_date(startdatestring) + except ParseError: + pass + + if enddatestring != "": + try: + enddate = iso8601.parse_date(enddatestring) + except ParseError: + pass + + if theuser == 0: + theuser = request.user.id + + u = User.objects.get(id=theuser) + r = Rower.objects.get(user=u) + + startdate = datetime.datetime.combine(startdate,datetime.time()) + enddate = datetime.datetime.combine(enddate,datetime.time(23,59,59)) + + theworkouts = Workout.objects.filter( + user=r,rankingpiece=True, + workouttype__in=[ + 'rower', + 'dynamic', + 'slides' + ], + startdatetime__gte=startdate, + startdatetime__lte=enddate + ).order_by("-startdatetime") + + + delta,cpvalue,avgpower = dataprep.fetchcp( + r,theworkouts,table='cpergdata' + ) + + powerdf = pd.DataFrame({ + 'Delta':delta, + 'CP':cpvalue, + }) + + csvfilename = 'CP_data_user_{id}.csv'.format( + id = theuser + ) + + powerdf = powerdf[powerdf['CP']>0] + powerdf.dropna(axis=0,inplace=True) + powerdf.sort_values(['Delta','CP'],ascending=[1,0],inplace=True) + powerdf.drop_duplicates(subset='Delta',keep='first',inplace=True) + powerdf.to_csv(csvfilename) + + res = myqueue(queuehigh, + handle_sendemailfile, + 'Sander', + 'Roosendaal', + 'roosendaalsander@gmail.com', + csvfilename, + delete=True) + + successmessage = "The CSV file was sent to the site admin per email" + messages.info(request,successmessage) + response = HttpResponseRedirect('/rowers/list-workouts/') + + return response + +@login_required() +def otwcp_toadmin_view(request,theuser=0, + startdate=timezone.now()-datetime.timedelta(days=365), + enddate=timezone.now(), + startdatestring="", + enddatestring="", + ): + + if startdatestring != "": + try: + startdate = iso8601.parse_date(startdatestring) + except ParseError: + pass + + if enddatestring != "": + try: + enddate = iso8601.parse_date(enddatestring) + except ParseError: + pass + + if theuser == 0: + theuser = request.user.id + + u = User.objects.get(id=theuser) + r = Rower.objects.get(user=u) + + startdate = datetime.datetime.combine(startdate,datetime.time()) + enddate = datetime.datetime.combine(enddate,datetime.time(23,59,59)) + + theworkouts = Workout.objects.filter( + user=r,rankingpiece=True, + workouttype='water', + startdatetime__gte=startdate, + startdatetime__lte=enddate + ).order_by("-startdatetime") + + + delta,cpvalue,avgpower = dataprep.fetchcp( + r,theworkouts,table='cpdata' + ) + + powerdf = pd.DataFrame({ + 'Delta':delta, + 'CP':cpvalue, + }) + + csvfilename = 'CP_data_user_{id}.csv'.format( + id = theuser + ) + + powerdf = powerdf[powerdf['CP']>0] + powerdf.dropna(axis=0,inplace=True) + powerdf.sort_values(['Delta','CP'],ascending=[1,0],inplace=True) + powerdf.drop_duplicates(subset='Delta',keep='first',inplace=True) + powerdf.to_csv(csvfilename) + + res = myqueue(queuehigh, + handle_sendemailfile, + 'Sander', + 'Roosendaal', + 'roosendaalsander@gmail.com', + csvfilename, + delete=True) + + successmessage = "The CSV file was sent to the site admin per email" + messages.info(request,successmessage) + response = HttpResponseRedirect('/rowers/list-workouts/') + + return response + +# Show ranking distances including predicted paces +@user_passes_test(ispromember,login_url="/rowers/paidplans", + message="This functionality requires a Pro plan or higher", + redirect_field_name=None) +def oterankings_view(request,theuser=0, + startdate=timezone.now()-datetime.timedelta(days=365), + enddate=timezone.now(), + startdatestring="", + enddatestring=""): + + if startdatestring != "": + try: + startdate = iso8601.parse_date(startdatestring) + except ParseError: + pass + + if enddatestring != "": + try: + enddate = iso8601.parse_date(enddatestring) + except ParseError: + pass + + if enddate < startdate: + s = enddate + enddate = startdate + startdate = s + + if theuser == 0: + if 'rowerid' in request.session: + try: + r = Rower.objects.get(id=request.session['rowerid']) + theuser = r.user.id + except Rower.DoesNotExist: + theuser = request.user.id + else: + theuser = request.user.id + + + promember=0 + if not request.user.is_anonymous(): + r = Rower.objects.get(user=request.user) + result = request.user.is_authenticated() and ispromember(request.user) + if result: + promember=1 + + # get all OTW rows in date range + + # process form + if request.method == 'POST': + dateform = DateRangeForm(request.POST) + if dateform.is_valid(): + startdate = dateform.cleaned_data['startdate'] + enddate = dateform.cleaned_data['enddate'] + if startdate > enddate: + s = enddate + enddate = startdate + startdate = s + workouttypeform = OteWorkoutTypeForm(request.POST) + if workouttypeform.is_valid(): + workouttypes = workouttypeform.cleaned_data['workouttypes'] + form = PredictedPieceForm(request.POST) + if form.is_valid(): + value = form.cleaned_data['value'] + pieceunit = form.cleaned_data['pieceunit'] + else: + value = None + + try: + trankingdistances = form.cleaned_data['trankingdistances'] + except KeyError: + trankingdistances = [] + + trankingdistances = [int(d) for d in trankingdistances] + + try: + trankingdurations = form.cleaned_data['trankingdurations'] + except KeyError: + trankingdurations = [] + + trankingdurations = [ + datetime.datetime.strptime(d,"%H:%M:%S").time() for d in trankingdurations + ] + if value: + hourvalue,tvalue = divmod(value,60) + hourvalue = int(hourvalue) + minutevalue = int(tvalue) + tvalue = int(60*(tvalue-minutevalue)) + if hourvalue >= 24: + hourvalue = 23 + if pieceunit == 'd': + trankingdistances.append(value) + else: + trankingdurations.append(datetime.time( + minute=minutevalue, + hour=hourvalue, + second=tvalue + )) + else: + form = PredictedPieceForm() + dateform = DateRangeForm(initial={ + 'startdate': startdate, + 'enddate': enddate, + }) + workouttypeform = OteWorkoutTypeForm() + workouttypes = ['rower','slides','dynamic'] + trankingdistances = rankingdistances + trankingdurations = rankingdurations + + # get all 2k (if any) - this rower, in date range + try: + r = Rower.objects.get(user=theuser) + request.session['rowerid'] = r.id + except Rower.DoesNotExist: + allergworkouts = [] + raise Http404("Rower doesn't exist") + + + try: + uu = User.objects.get(id=theuser) + except User.DoesNotExist: + uu = '' + + + # test to fix bug + startdate = datetime.datetime.combine(startdate,datetime.time()) + enddate = datetime.datetime.combine(enddate,datetime.time(23,59,59)) + + + + thedistances = [] + theworkouts = [] + thesecs = [] + + theworkouts = Workout.objects.filter( + user=r,rankingpiece=True, + workouttype__in=workouttypes, + startdatetime__gte=startdate, + startdatetime__lte=enddate + ).order_by("-startdatetime") + + + delta,cpvalue,avgpower = dataprep.fetchcp( + r,theworkouts,table='cpergdata' + ) + + runningjob = 0 + + taskstatus = get_stored_tasks_status(request) + for task in taskstatus: + if task['func_name'] == 'updatecp': + if 'success' in task['status'].lower() or 'finished' in task['status'].lower(): + runningjob = 1 + messages.info(request,'CP chart data have been updated') + remove_asynctask(request,task['id']) + elif 'fail' in task['status'].lower(): + runningjob = 0 + try: + remove_asynctask(request,task[id]) + messages.error(request,'Oh, your task failed') + except KeyError: + pass + elif 'started' in task['status'].lower(): + messages.info(request,'Busy updating CP chart data') + runningjob = 1 + elif 'queued' in task['status'].lower(): + messages.info(request,'Getting ready to update CP chart data') + runningjob = 1 + + + if not runningjob: + job = dataprep.runcpupdate( + r,type='rower', + startdate=startdate, + enddate=enddate + ) + request.session['job_id'] = job.id + try: + request.session['async_tasks'] += [(job.id,'updatecp')] + except KeyError: + request.session['async_tasks'] = [(job.id,'updatecp')] + messages.info(request,'New calculation queued. Page will reload automatically. You can check the status of your calculations here') + + powerdf = pd.DataFrame({ + 'Delta':delta, + 'CP':cpvalue, + }) + + if powerdf.empty: + messages.info(request,'Your calculations are running in the background. Page will reload automatically. You can check the status of your calculations here') + + powerdf = powerdf[powerdf['CP']>0] + powerdf.dropna(axis=0,inplace=True) + powerdf.sort_values(['Delta','CP'],ascending=[1,0],inplace=True) + powerdf.drop_duplicates(subset='Delta',keep='first',inplace=True) + + rowername = r.user.first_name+" "+r.user.last_name + # create interactive plot + if len(powerdf) !=0 : + res = interactive_otwcpchart(powerdf,promember=promember,rowername=rowername) + script = res[0] + div = res[1] + p1 = res[2] + ratio = res[3] + r.ep0 = p1[0] + r.ep1 = p1[1] + r.ep2 = p1[2] + r.ep3 = p1[3] + r.ecpratio = ratio + r.save() + paulslope = 1 + paulintercept = 1 + message = res[4] + else: + ratio = 1 + script = '' + div = '

No ranking pieces found.

' + paulslope = 1 + paulintercept = 1 + p1 = [1,1,1,1] + message = "" + + + + + + cpredictions = [] + + + + for rankingduration in trankingdurations: + t = 3600.*rankingduration.hour + t += 60.*rankingduration.minute + t += rankingduration.second + t += rankingduration.microsecond/1.e6 + + + # CP model + pwr = p1[0]/(1+t/p1[2]) + pwr += p1[1]/(1+t/p1[3]) + + velo = (pwr/2.8)**(1./3.) + p = 500./velo + d = t*velo + + if pwr <= 0: + pwr = 50. + + + if not np.isnan(pwr): + try: + pwr2 = pwr*ratio + except: + pwr2 = pwr + + a = { + 'distance':int(d), + 'duration':timedeltaconv(t), + 'power':int(pwr), + 'upper':int(pwr2), + 'pace':timedeltaconv(p)} + + cpredictions.append(a) + + + # initiation - get 10 min power, then use Paul's law + + t_10 = 600. + power_10 = p1[0]/(1+t_10/p1[2]) + power_10 += p1[1]/(1+t_10/p1[3]) + + velo_10 = (power_10/2.8)**(1./3.) + pace_10 = 500./velo_10 + distance_10 = t_10*velo_10 + + paulslope = 5. + + for rankingdistance in trankingdistances: + + delta = paulslope * np.log(rankingdistance/distance_10)/np.log(2) + + + p = pace_10+delta + velo = 500./p + t = rankingdistance/velo + + pwr2 = p1[0]/(1+t/p1[2]) + pwr2 += p1[1]/(1+t/p1[3]) + try: + pwr2 *= ratio + except UnboundLocalError: + pass + + if pwr2 <= 0: + pwr2 = 50. + + velo2 = (pwr2/2.8)**(1./3.) + + if np.isnan(velo2) or velo2 <= 0: + velo2 = 1.0 + + t2 = rankingdistance/velo2 + + pwr3 = p1[0]/(1+t2/p1[2]) + pwr3 += p1[1]/(1+t2/p1[3]) + pwr3 *= ratio + + + if pwr3 <= 0: + pwr3 = 50. + + velo3 = (pwr3/2.8)**(1./3.) + if np.isnan(velo3) or velo3 <= 0: + velo3 = 1.0 + + t3 = rankingdistance/velo3 + p3 = 500./velo3 + + a = { + 'distance':rankingdistance, + 'duration':timedeltaconv(t3), + 'power':'--', + 'upper':int(pwr3), + 'pace':timedeltaconv(p3)} + + cpredictions.append(a) + + # del form.fields["pieceunit"] + + + messages.error(request,message) + return render(request, 'oterankings.html', + {'rankingworkouts':theworkouts, + 'interactiveplot':script, + 'the_div':div, + 'rower':r, + 'active':'nav-analysis', + 'cpredictions':cpredictions, + 'avgpower':avgpower, + 'form':form, + 'dateform':dateform, + 'workouttypeform':workouttypeform, + 'id': theuser, + 'theuser':uu, + 'startdate':startdate, + 'enddate':enddate, + 'teams':get_my_teams(request.user), + 'workouttype':'rower', + }) + + + +# Multi Flex Chart with Grouping +@user_passes_test(ispromember,login_url="/rowers/paidplans", + message="This functionality requires a Pro plan or higher", + redirect_field_name=None) +def user_multiflex_select(request, + startdatestring="", + enddatestring="", + message='', + successmessage='', + startdate=timezone.now()-datetime.timedelta(days=30), + enddate=timezone.now(), + userid=0): + + r = getrequestrower(request,userid=userid) + user = r.user + + if 'options' in request.session: + options = request.session['options'] + else: + options = {} + + try: + palette = request.session['palette'] + except KeyError: + palette = 'monochrome_blue' + + try: + includereststrokes = request.session['includereststrokes'] + except KeyError: + includereststrokes = False + + try: + ploterrorbars = request.session['ploterrorbars'] + except: + ploterrorbars = False + + if 'startdate' in request.session: + startdate = iso8601.parse_date(request.session['startdate']) + + + if 'enddate' in request.session: + enddate = iso8601.parse_date(request.session['enddate']) + + try: + waterboattype = request.session['waterboattype'] + except KeyError: + waterboattype = mytypes.waterboattype + else: + waterboattype = mytypes.waterboattype + + if 'rankingonly' in request.session: + rankingonly = request.session['rankingonly'] + else: + rankingonly = False + + + if 'modalities' in request.session: + modalities = request.session['modalities'] + if len(modalities) > 1: + modality = 'all' + else: + modality = modalities[0] + else: + modalities = [m[0] for m in mytypes.workouttypes] + modality = 'all' + + if request.method == 'POST': + dateform = DateRangeForm(request.POST) + if dateform.is_valid(): + startdate = dateform.cleaned_data['startdate'] + enddate = dateform.cleaned_data['enddate'] + startdatestring = startdate.strftime('%Y-%m-%d') + enddatestring = enddate.strftime('%Y-%m-%d') + request.session['startdate'] = startdatestring + request.session['enddate'] = enddatestring + modalityform = TrendFlexModalForm(request.POST) + if modalityform.is_valid(): + modality = modalityform.cleaned_data['modality'] + waterboattype = modalityform.cleaned_data['waterboattype'] + rankingonly = modalityform.cleaned_data['rankingonly'] + if modality == 'all': + modalities = [m[0] for m in mytypes.workouttypes] + else: + modalities = [modality] + + if modality != 'water': + waterboattype = [b[0] for b in mytypes.boattypes] + + + request.session['modalities'] = modalities + request.session['waterboattype'] = waterboattype + request.session['rankingonly'] = rankingonly + else: + dateform = DateRangeForm(initial={ + 'startdate':startdate, + 'enddate':enddate, + }) + + + startdate = datetime.datetime.combine(startdate,datetime.time()) + enddate = datetime.datetime.combine(enddate,datetime.time(23,59,59)) + #enddate = enddate+datetime.timedelta(days=1) + + if startdatestring: + startdate = iso8601.parse_date(startdatestring) + if enddatestring: + enddate = iso8601.parse_date(enddatestring) + + if enddate < startdate: + s = enddate + enddate = startdate + startdate = s + + + negtypes = [] + for b in mytypes.boattypes: + if b[0] not in waterboattype: + negtypes.append(b[0]) + + if rankingonly: + rankingpiece = [True] + else: + rankingpiece = [True,False] + + workouts = Workout.objects.filter( + user=r, + startdatetime__gte=startdate, + startdatetime__lte=enddate, + workouttype__in=modalities, + rankingpiece__in=rankingpiece + ).order_by( + "-date", "-starttime" + ).exclude( + boattype__in=negtypes + ) + + query = request.GET.get('q') + if query: + query_list = query.split() + workouts = workouts.filter( + reduce(operator.and_, + (Q(name__icontains=q) for q in query_list)) | + reduce(operator.and_, + (Q(notes__icontains=q) for q in query_list)) + ) + searchform = SearchForm(initial={'q':query}) + else: + searchform = SearchForm() + + form = WorkoutMultipleCompareForm() + form.fields["workouts"].queryset = workouts + + chartform = MultiFlexChoiceForm(initial={ + 'palette':palette, + 'ploterrorbars':ploterrorbars, + 'includereststrokes':includereststrokes, + }) + + modalityform = TrendFlexModalForm(initial={ + 'modality':modality, + 'waterboattype':waterboattype, + 'rankingonly':rankingonly, + }) + + messages.info(request,successmessage) + messages.error(request,message) + + startdatestring = startdate.strftime('%Y-%m-%d') + enddatestring = enddate.strftime('%Y-%m-%d') + request.session['startdate'] = startdatestring + request.session['enddate'] = enddatestring + + request.session['waterboattype'] = waterboattype + request.session['rankingonly'] = rankingonly + request.session['modalities'] = modalities + + + breadcrumbs = [ + { + 'url':'/rowers/analysis', + 'name':'Analysis' + }, + { + 'url':reverse(user_multiflex_select,kwargs={'userid':userid}), + 'name': 'Compare Select' + }, + { + 'url':reverse(multi_compare_view), + 'name': 'Comparison Chart' + } + ] + + return render(request, 'user_multiflex_select.html', + {'workouts': workouts, + 'dateform':dateform, + 'breadcrumbs':breadcrumbs, + 'startdate':startdate, + 'enddate':enddate, + 'theuser':user, + 'rower':r, + 'form':form, + 'chartform':chartform, + 'searchform':searchform, + 'modalityform':modalityform, + 'teams':get_my_teams(request.user), + }) + +@user_passes_test(ispromember,login_url="/rowers/paidplans", + message="This functionality requires a Pro plan or higher", + redirect_field_name=None) +def multiflex_data(request,userid=0, + options={ + 'includereststrokes':False, + 'ploterrorbars':False, + 'userid':0, + 'palette': 'monochrome_blue', + 'groupby': 'spm', + 'binsize': 1, + 'xparam': 'hr', + 'yparam': 'pace', + 'spmmin': 15, + 'spmmax': 55, + 'workmin': 400, + 'workmax': 1500, + 'ids': [], + 'ploterrorbars':False, + }): + + if 'options' in request.session: + options = request.session['options'] + + try: + includereststrokes = options['includereststrokes'] + except KeyError: + includereststrokes = False + + try: + ploterrorbars = options['ploterrorbars'] + except KeyError: + ploterrorbars = False + + try: + palette = request.session['palette'] + except KeyError: + palette = 'monochrome_blue' + + workstrokesonly = not includereststrokes + + if userid==0: + userid = request.user.id + + + palette = options['palette'] + groupby = options['groupby'] + binsize = options['binsize'] + xparam = options['xparam'] + yparam = options['yparam'] + spmmin = options['spmmin'] + spmmax = options['spmmax'] + workmin = options['workmin'] + workmax = options['workmax'] + ids = options['ids'] + + workouts = [] + + for id in ids: + try: + workouts.append(Workout.objects.get(id=id)) + except Workout.DoesNotExist: + pass + + labeldict = { + int(w.id): w.__unicode__() for w in workouts + } + + fieldlist,fielddict = dataprep.getstatsfields() + fieldlist = [xparam,yparam,groupby, + 'workoutid','spm','driveenergy', + 'workoutstate'] + + # prepare data frame + datadf,extracols = dataprep.read_cols_df_sql(ids,fieldlist) + + if xparam == groupby: + datadf['groupby'] = datadf[xparam] + groupy = 'groupby' + + datadf = dataprep.clean_df_stats(datadf,workstrokesonly=workstrokesonly) + + + datadf = dataprep.filter_df(datadf,'spm',spmmin, + largerthan=True) + datadf = dataprep.filter_df(datadf,'spm',spmmax, + largerthan=False) + + datadf = dataprep.filter_df(datadf,'driveenergy',workmin, + largerthan=True) + datadf = dataprep.filter_df(datadf,'driveneergy',workmax, + largerthan=False) + + + datadf.dropna(axis=0,how='any',inplace=True) + + + datemapping = { + w.id:w.date for w in workouts + } + + datadf['date'] = datadf['workoutid'] + datadf['date'].replace(datemapping,inplace=True) + + today = datetime.date.today() + datadf['days ago'] = map(lambda x : x.days, datadf.date - today) + + if groupby != 'date': + try: + bins = np.arange(datadf[groupby].min()-binsize, + datadf[groupby].max()+binsize, + binsize) + groups = datadf.groupby(pd.cut(datadf[groupby],bins,labels=False)) + except ValueError: + messages.error( + request, + "Unable to compete. Probably not enough data selected" + ) + url = reverse(user_multiflex_select) + return HttpResponseRedirect(url) + else: + bins = np.arange(datadf['days ago'].min()-binsize, + datadf['days ago'].max()+binsize, + binsize, + ) + groups = datadf.groupby(pd.cut(datadf['days ago'], bins, + labels=False)) + + + xvalues = groups.mean()[xparam] + yvalues = groups.mean()[yparam] + xerror = groups.std()[xparam] + yerror = groups.std()[yparam] + groupsize = groups.count()[xparam] + + mask = groupsize <= min([0.01*groupsize.sum(),0.2*groupsize.mean()]) + xvalues.loc[mask] = np.nan + + yvalues.loc[mask] = np.nan + xerror.loc[mask] = np.nan + yerror.loc[mask] = np.nan + groupsize.loc[mask] = np.nan + + xvalues.dropna(inplace=True) + yvalues.dropna(inplace=True) + xerror.dropna(inplace=True) + yerror.dropna(inplace=True) + groupsize.dropna(inplace=True) + + if len(groupsize) == 0: + messages.error(request,'No data in selection') + url = reverse(user_multiflex_select) + return HttpResponseRedirect(url) + else: + groupsize = 30.*np.sqrt(groupsize/float(groupsize.max())) + + df = pd.DataFrame({ + xparam:xvalues, + yparam:yvalues, + 'x':xvalues, + 'y':yvalues, + 'xerror':xerror, + 'yerror':yerror, + 'groupsize':groupsize, + }) + + + if yparam == 'pace': + df['y'] = dataprep.paceformatsecs(df['y']/1.0e3) + + aantal = len(df) + + if groupby != 'date': + try: + df['groupval'] = groups.mean()[groupby] + df['groupval'].loc[mask] = np.nan + + groupcols = df['groupval'] + except ValueError: + df['groupval'] = groups.mean()[groupby].fillna(value=0) + df['groupval'].loc[mask] = np.nan + groupcols = df['groupval'] + except KeyError: + messages.error(request,'Data selection error') + url = reverse(user_multiflex_select) + return HttpResponseRedirect(url) + else: + try: + dates = groups.min()[groupby] + dates.loc[mask] = np.nan + dates.dropna(inplace=True) + df['groupval'] = [x.strftime("%Y-%m-%d") for x in dates] + df['groupval'].loc[mask] = np.nan + groupcols = 100.*np.arange(aantal)/float(aantal) + except AttributeError: + df['groupval'] = groups.mean()['days ago'].fillna(value=0) + groupcols = 100.*np.arange(aantal)/float(aantal) + + + groupcols = (groupcols-groupcols.min())/(groupcols.max()-groupcols.min()) + + if aantal == 1: + groupcols = np.array([1.]) + + + colors = range_to_color_hex(groupcols,palette=palette) + + df['color'] = colors + + clegendx = np.arange(0,1.2,.2) + legcolors = range_to_color_hex(clegendx,palette=palette) + if groupby != 'date': + clegendy = df['groupval'].min()+clegendx*(df['groupval'].max()-df['groupval'].min()) + else: + clegendy = df.index.min()+clegendx*(df.index.max()-df.index.min()) + + + + colorlegend = zip(range(6),clegendy,legcolors) + + + if userid == 0: + extratitle = '' + else: + u = User.objects.get(id=userid) + extratitle = ' '+u.first_name+' '+u.last_name + + + + script,div = interactive_multiflex(df,xparam,yparam, + groupby, + extratitle=extratitle, + ploterrorbars=ploterrorbars, + binsize=binsize, + colorlegend=colorlegend) + + scripta= script.split('\n')[2:-1] + script = ''.join(scripta) + + + return JSONResponse({ + "script":script, + "div":div, + }) + + +@user_passes_test(ispromember,login_url="/rowers/paidplans", + message="This functionality requires a Pro plan or higher", + redirect_field_name=None) +def multiflex_view(request,userid=0, + options={ + 'includereststrokes':False, + 'ploterrorbars':False, + }): + + if 'options' in request.session: + options = request.session['options'] + + try: + includereststrokes = options['includereststrokes'] + except KeyError: + includereststrokes = False + + try: + ploterrorbars = options['ploterrorbars'] + except KeyError: + ploterrorbars = False + + try: + palette = request.session['palette'] + except KeyError: + palette = 'monochrome_blue' + + if 'startdate' in request.session: + startdate = iso8601.parse_date(request.session['startdate']) + + + if 'enddate' in request.session: + enddate = iso8601.parse_date(request.session['enddate']) + + workstrokesonly = not includereststrokes + + if userid==0: + userid = request.user.id + + if request.method == 'POST' and 'workouts' in request.POST: + form = WorkoutMultipleCompareForm(request.POST) + chartform = MultiFlexChoiceForm(request.POST) + if form.is_valid() and chartform.is_valid(): + cd = form.cleaned_data + workouts = cd['workouts'] + xparam = chartform.cleaned_data['xparam'] + yparam = chartform.cleaned_data['yparam'] + includereststrokes = chartform.cleaned_data['includereststrokes'] + ploterrorbars = chartform.cleaned_data['ploterrorbars'] + + workstrokesonly = not includereststrokes + palette = chartform.cleaned_data['palette'] + + groupby = chartform.cleaned_data['groupby'] + binsize = chartform.cleaned_data['binsize'] + if binsize <= 0: + binsize = 1 + if groupby == 'pace': + binsize *= 1000 + + spmmin = chartform.cleaned_data['spmmin'] + spmmax = chartform.cleaned_data['spmmax'] + workmin = chartform.cleaned_data['workmin'] + workmax = chartform.cleaned_data['workmax'] + + ids = [int(w.id) for w in workouts] + request.session['ids'] = ids + + else: + return HttpResponse("Form is not valid") + elif request.method == 'POST' and 'ids' in request.session: + chartform = MultiFlexChoiceForm(request.POST) + if chartform.is_valid(): + xparam = chartform.cleaned_data['xparam'] + yparam = chartform.cleaned_data['yparam'] + includereststrokes = chartform.cleaned_data['includereststrokes'] + ploterrorbars = chartform.cleaned_data['ploterrorbars'] + request.session['ploterrorbars'] = ploterrorbars + request.session['includereststrokes'] = includereststrokes + workstrokesonly = not includereststrokes + palette = chartform.cleaned_data['palette'] + + groupby = chartform.cleaned_data['groupby'] + binsize = chartform.cleaned_data['binsize'] + if binsize <= 0: + binsize = 1 + if groupby == 'pace': + binsize *= 1000. + + spmmin = chartform.cleaned_data['spmmin'] + spmmax = chartform.cleaned_data['spmmax'] + workmin = chartform.cleaned_data['workmin'] + workmax = chartform.cleaned_data['workmax'] + + ids = request.session['ids'] + request.session['ids'] = ids + workouts = dataprep.get_workouts(ids,userid) + if not workouts: + message = 'Error: Workouts in session storage do not belong to this user.' + messages.error(request,message) + url = reverse(user_multiflex_select, + kwargs={ + 'userid':userid, + } + ) + return HttpResponseRedirect(url) + + # workouts = [Workout.objects.get(id=id) for id in ids] + + + else: + return HttpResponse("invalid form") + else: + url = reverse(user_multiflex_select) + return HttpResponseRedirect(url) + + div = get_call() + + options['includereststrokes'] = includereststrokes + options['ploterrorbars'] = ploterrorbars + options['userid'] = userid + options['palette'] = palette + options['groupby'] = groupby + options['binsize'] = binsize + options['xparam'] = xparam + options['yparam'] = yparam + options['spmmin'] = spmmin + options['spmmax'] = spmmax + options['workmin'] = workmin + options['workmax'] = workmax + options['ids'] = ids + + + request.session['options'] = options + + r = getrequestrower(request,userid=userid) + + breadcrumbs = [ + { + 'url':'/rowers/analysis', + 'name':'Analysis' + }, + { + 'url':reverse(user_multiflex_select,kwargs={'userid':userid}), + 'name': 'Trend Flex Select' + }, + { + 'url':reverse(multiflex_view), + 'name': 'Trend Flex Chart' + } + ] + + + return render(request,'multiflex.html', + {'interactiveplot':'', + 'active':'nav-analysis', + 'rower':r, + 'breadcrumbs':breadcrumbs, + 'the_div':div, + 'active':'nav-analysis', + 'chartform':chartform, + 'userid':userid, + 'teams':get_my_teams(request.user), + }) + + +# Box plots +@user_passes_test(ispromember,login_url="/rowers/paidplans", + message="This functionality requires a Pro plan or higher", + redirect_field_name=None) +def user_boxplot_select(request, + startdatestring="", + enddatestring="", + message='', + successmessage='', + startdate=timezone.now()-datetime.timedelta(days=30), + enddate=timezone.now(), + options={ + 'includereststrokes':False, + 'workouttypes':['rower','dynamic','slides'], + 'waterboattype':mytypes.waterboattype, + 'rankingonly':False, + }, + userid=0): + + r = getrequestrower(request,userid=userid) + user = r.user + userid = user.id + + if 'options' in request.session: + options = request.session['options'] + + + try: + workouttypes = options['workouttypes'] + except KeyError: + workouttypes = ['rower','dynamic','slides'] + + try: + rankingonly = options['rankingonly'] + except KeyError: + rankingonly = False + + try: + includereststrokes = options['includereststrokes'] + except KeyError: + includereststrokes = False + + if 'startdate' in request.session: + startdate = iso8601.parse_date(request.session['startdate']) + + + if 'enddate' in request.session: + enddate = iso8601.parse_date(request.session['enddate']) + + workstrokesonly = not includereststrokes + + waterboattype = mytypes.waterboattype + + + if startdatestring != "": + startdate = iso8601.parse_date(startdatestring) + + if enddatestring != "": + enddate = iso8601.parse_date(enddatestring) + + if enddate < startdate: + s = enddate + enddate = startdate + startdate = s + + + if request.method == 'POST': + dateform = DateRangeForm(request.POST) + if dateform.is_valid(): + startdate = dateform.cleaned_data['startdate'] + enddate = dateform.cleaned_data['enddate'] + startdatestring = startdate.strftime('%Y-%m-%d') + enddatestring = enddate.strftime('%Y-%m-%d') + request.session['startdate'] = startdatestring + request.session['enddate'] = enddatestring + optionsform = TrendFlexModalForm(request.POST) + if optionsform.is_valid(): + modality = optionsform.cleaned_data['modality'] + waterboattype = optionsform.cleaned_data['waterboattype'] + if modality == 'all': + modalities = [m[0] for m in mytypes.workouttypes] + else: + modalities = [modality] + if modality != 'water': + waterboattype = [b[0] for b in mytypes.boattypes] + + + if 'rankingonly' in optionsform.cleaned_data: + rankingonly = optionsform.cleaned_data['rankingonly'] + else: + rankingonly = False + + request.session['modalities'] = modalities + request.session['waterboattype'] = waterboattype + else: + dateform = DateRangeForm(initial={ + 'startdate':startdate, + 'enddate':enddate, + }) + + if 'modalities' in request.session: + modalities = request.session['modalities'] + if len(modalities) > 1: + modality = 'all' + else: + modality = modalities[0] + else: + modalities = [m[0] for m in mytypes.workouttypes] + modality = 'all' + + + + + negtypes = [] + for b in mytypes.boattypes: + if b[0] not in waterboattype: + negtypes.append(b[0]) + + + startdate = datetime.datetime.combine(startdate,datetime.time()) + enddate = datetime.datetime.combine(enddate,datetime.time(23,59,59)) + #enddate = enddate+datetime.timedelta(days=1) + + if startdatestring: + startdate = iso8601.parse_date(startdatestring) + if enddatestring: + enddate = iso8601.parse_date(enddatestring) + + if enddate < startdate: + s = enddate + enddate = startdate + startdate = s + + negtypes = [] + for b in mytypes.boattypes: + if b[0] not in waterboattype: + negtypes.append(b[0]) + + + workouts = Workout.objects.filter(user=r, + startdatetime__gte=startdate, + startdatetime__lte=enddate, + workouttype__in=modalities, + ).order_by( + "-date", "-starttime" + ).exclude(boattype__in=negtypes) + # workouttypes = [w for w in workouttypes if w not in mytypes.otwtypes] + + if rankingonly: + workouts = workouts.exclude(rankingpiece=False) + + query = request.GET.get('q') + if query: + query_list = query.split() + workouts = workouts.filter( + reduce(operator.and_, + (Q(name__icontains=q) for q in query_list)) | + reduce(operator.and_, + (Q(notes__icontains=q) for q in query_list)) + ) + searchform = SearchForm(initial={'q':query}) + else: + searchform = SearchForm() + + form = WorkoutMultipleCompareForm() + form.fields["workouts"].queryset = workouts + + chartform = BoxPlotChoiceForm() + optionsform = TrendFlexModalForm(initial={ + 'modality':modality, + 'waterboattype':waterboattype, + 'rankingonly':rankingonly, + }) + + messages.info(request,successmessage) + messages.error(request,message) + + startdatestring = startdate.strftime('%Y-%m-%d') + enddatestring = enddate.strftime('%Y-%m-%d') + request.session['startdate'] = startdatestring + request.session['enddate'] = enddatestring + + + breadcrumbs = [ + { + 'url':'/rowers/analysis', + 'name':'Analysis' + }, + { + 'url':reverse(user_boxplot_select,kwargs={'userid':userid}), + 'name': 'BoxPlot Select' + }, + ] + return render(request, 'user_boxplot_select.html', + {'workouts': workouts, + 'dateform':dateform, + 'startdate':startdate, + 'enddate':enddate, + 'rower':r, + 'breadcrumbs':breadcrumbs, + 'theuser':user, + 'form':form, + 'active':'nav-analysis', + 'chartform':chartform, + 'searchform':searchform, + 'optionsform':optionsform, + 'teams':get_my_teams(request.user), + }) + +@user_passes_test(ispromember,login_url="/rowers/paidplans", + message="This functionality requires a Pro plan or higher", + redirect_field_name=None) +def boxplot_view_data(request,userid=0, + options={ + 'includereststrokes':False, + 'spmmin':15, + 'spmmax':55, + 'workmin':0, + 'workmax':1500, + 'ids':[], + 'userid':0, + 'plotfield':'spm', + }): + + if 'options' in request.session: + options = request.session['options'] + + try: + includereststrokes = options['includereststrokes'] + spmmin = options['spmmin'] + spmmax = options['spmmax'] + workmin = options['workmin'] + workmax = options['workmax'] + ids = options['ids'] + userid = options['userid'] + plotfield = options['plotfield'] + except KeyError: + includereststrokes = False + spmmin = 15 + spmmax = 55 + workmin = 0 + workmax = 55 + ids = [] + userid = 0 + plotfield = 'spm' + + + workstrokesonly = not includereststrokes + + if userid==0: + userid = request.user.id + + workouts = [] + + + if not ids: + return JSONResponse({ + "script":'', + "div":'No data found' + }) + + for id in ids: + try: + workouts.append(Workout.objects.get(id=id)) + except Workout.DoesNotExist: + pass + + labeldict = { + int(w.id): w.__unicode__() for w in workouts + } + + + datemapping = { + w.id:w.date for w in workouts + } + + + + fieldlist,fielddict = dataprep.getstatsfields() + fieldlist = [plotfield,'workoutid','spm','driveenergy', + 'workoutstate'] + + # prepare data frame + datadf,extracols = dataprep.read_cols_df_sql(ids,fieldlist) + + + + datadf = dataprep.clean_df_stats(datadf,workstrokesonly=workstrokesonly) + + datadf = dataprep.filter_df(datadf,'spm',spmmin, + largerthan=True) + datadf = dataprep.filter_df(datadf,'spm',spmmax, + largerthan=False) + datadf = dataprep.filter_df(datadf,'driveenergy',workmin, + largerthan=True) + datadf = dataprep.filter_df(datadf,'driveneergy',workmax, + largerthan=False) + + datadf.dropna(axis=0,how='any',inplace=True) + + + datadf['workoutid'].replace(datemapping,inplace=True) + datadf.rename(columns={"workoutid":"date"},inplace=True) + datadf = datadf.sort_values(['date']) + + if userid == 0: + extratitle = '' + else: + u = User.objects.get(id=userid) + extratitle = ' '+u.first_name+' '+u.last_name + + + + script,div = interactive_boxchart(datadf,plotfield, + extratitle=extratitle) + + scripta = script.split('\n')[2:-1] + script = ''.join(scripta) + + + return JSONResponse({ + "script":script, + "div":div, + }) + +@user_passes_test(ispromember,login_url="/rowers/paidplans", + message="This functionality requires a Pro plan or higher", + redirect_field_name=None) +def boxplot_view(request,userid=0, + options={ + 'includereststrokes':False, + 'rankingonly':False, + }): + + if 'options' in request.session: + options = request.session['options'] + else: + options = {} + + try: + includereststrokes = options['includereststrokes'] + except KeyError: + includereststrokes = False + options['includereststrokes'] = False + + try: + rankingonly = options['rankingonly'] + except KeyError: + rankingonly = False + options['rankingonly'] = False + + workstrokesonly = not includereststrokes + + if userid==0: + userid = request.user.id + + + if request.method == 'POST' and 'workouts' in request.POST: + form = WorkoutMultipleCompareForm(request.POST) + chartform = BoxPlotChoiceForm(request.POST) + if form.is_valid() and chartform.is_valid(): + cd = form.cleaned_data + workouts = cd['workouts'] + plotfield = chartform.cleaned_data['yparam'] + includereststrokes = chartform.cleaned_data['includereststrokes'] + request.session['includereststrokes'] = includereststrokes + workstrokesonly = not includereststrokes + + spmmin = chartform.cleaned_data['spmmin'] + spmmax = chartform.cleaned_data['spmmax'] + workmin = chartform.cleaned_data['workmin'] + workmax = chartform.cleaned_data['workmax'] + + ids = [int(w.id) for w in workouts] + request.session['ids'] = ids + + else: + url = reverse(user_boxplot_select,kwargs={'userid':userid}) + return HttpResponseRedirect(url) + elif request.method == 'POST' and 'ids' in request.session: + chartform = BoxPlotChoiceForm(request.POST) + if chartform.is_valid(): + plotfield = chartform.cleaned_data['yparam'] + includereststrokes = chartform.cleaned_data['includereststrokes'] + spmmin = chartform.cleaned_data['spmmin'] + spmmax = chartform.cleaned_data['spmmax'] + workmin = chartform.cleaned_data['workmin'] + workmax = chartform.cleaned_data['workmax'] + request.session['includereststrokes'] = includereststrokes + workstrokesonly = not includereststrokes + ids = request.session['ids'] + request.session['ids'] = ids + + + else: + url = reverse(user_boxplot_select,kwargs={'userid':userid}) + return HttpResponseRedirect(url) + else: + url = reverse(user_boxplot_select,kwargs={'userid':userid}) + return HttpResponseRedirect(url) + + div = get_call() + + + options['spmmin'] = spmmin + options['spmmax'] = spmmax + options['workmin'] = workmin + options['workmax'] = workmax + options['ids'] = ids + options['userid'] = userid + options['plotfield'] = plotfield + options['rankingonly'] = rankingonly + + + request.session['options'] = options + + r = getrequestrower(request,userid=userid) + breadcrumbs = [ + { + 'url':'/rowers/Analysis', + 'name':'Analysis' + }, + { + 'url':reverse(user_boxplot_select,kwargs={'userid':userid}), + 'name': 'BoxPlot Select' + }, + { + 'url':reverse(boxplot_view,kwargs={'userid':userid}), + 'name': 'BoxPlot Select' + }, + ] + + return render(request,'boxplot.html', + {'interactiveplot':'', + 'the_div':div, + 'rower':r, + 'breadcrumbs':breadcrumbs, + 'active':'nav-analysis', + 'chartform':chartform, + 'userid':userid, + 'teams':get_my_teams(request.user), + }) + + +# Cumulative stats page +@user_passes_test(ispromember,login_url="/rowers/paidplans",message="This functionality requires a Pro plan or higher",redirect_field_name=None) +def cumstats(request,theuser=0, + startdate=timezone.now()-datetime.timedelta(days=30), + enddate=timezone.now(), + deltadays=-1, + startdatestring="", + enddatestring="", + options={ + 'includereststrokes':False, + 'workouttypes':['rower','dynamic','slides'], + 'waterboattype':mytypes.waterboattype, + 'rankingonly':False, + }): + + r = getrequestrower(request,userid=theuser) + theuser = r.user + + if 'waterboattype' in request.session: + waterboattype = request.session['waterboattype'] + else: + waterboattype = mytypes.waterboattype + + + if 'rankingonly' in request.session: + rankingonly = request.session['rankingonly'] + else: + rankingonly = False + + if 'modalities' in request.session: + modalities = request.session['modalities'] + if len(modalities) > 1: + modality = 'all' + else: + modality = modalities[0] + else: + modalities = [m[0] for m in mytypes.workouttypes] + modality = 'all' + + + try: + rankingonly = options['rankingonly'] + except KeyError: + rankingonly = False + + try: + includereststrokes = options['includereststrokes'] + except KeyError: + includereststrokes = False + + + workstrokesonly = not includereststrokes + + waterboattype = mytypes.waterboattype + + + if startdatestring != "": + startdate = iso8601.parse_date(startdatestring) + + if enddatestring != "": + enddate = iso8601.parse_date(enddatestring) + + if enddate < startdate: + s = enddate + enddate = startdate + startdate = s + + + # get all indoor rows of in date range + + # process form + if request.method == 'POST': + form = DateRangeForm(request.POST) + modalityform = TrendFlexModalForm(request.POST) + if form.is_valid(): + startdate = form.cleaned_data['startdate'] + enddate = form.cleaned_data['enddate'] + if startdate > enddate: + s = enddate + enddate = startdate + startdate = s + startdatestring = startdate.strftime('%Y-%m-%d') + enddatestring = enddate.strftime('%Y-%m-%d') + if modalityform.is_valid(): + modality = modalityform.cleaned_data['modality'] + waterboattype = modalityform.cleaned_data['waterboattype'] + rankingonly = modalityform.cleaned_data['rankingonly'] + if modality == 'all': + modalities = [m[0] for m in mytypes.workouttypes] + else: + modalities = [modality] + + if modality != 'water': + waterboattype = [b[0] for b in mytypes.boattypes] + + + request.session['modalities'] = modalities + request.session['waterboattype'] = waterboattype + request.session['rankingonly'] = rankingonly + form = DateRangeForm(initial={ + 'startdate': startdate, + 'enddate': enddate, + }) + else: + form = DateRangeForm(initial={ + 'startdate': startdate, + 'enddate': enddate, + }) + includereststrokes = False + + workstrokesonly = not includereststrokes + modalityform = TrendFlexModalForm( + initial={ + 'modality':modality, + 'waterboattype':waterboattype, + 'rankingonly':rankingonly, + } + ) + + negtypes = [] + for b in mytypes.boattypes: + if b[0] not in waterboattype: + negtypes.append(b[0]) + + + + script = '' + div = get_call() + js_resources = '' + css_resources = '' + + options = { + 'modality': modality, + 'theuser': theuser.id, + 'waterboattype':waterboattype, + 'startdatestring':startdatestring, + 'enddatestring':enddatestring, + 'rankingonly':rankingonly, + 'includereststrokes':includereststrokes, + } + + + request.session['options'] = options + + + if modality == 'all': + modalities = [m[0] for m in mytypes.workouttypes] + else: + modalities = [modality] + + try: + startdate = iso8601.parse_date(startdatestring) + except ParseError: + startdate = timezone.now()-datetime.timedelta(days=7) + + try: + enddate = iso8601.parse_date(enddatestring) + except ParseError: + enddate = timezone.now() + + + if enddate < startdate: + s = enddate + enddate = startdate + startdate = s + + promember=0 + if theuser == 0: + theuser = request.user.id + + if not request.user.is_anonymous(): + r = getrower(request.user) + result = request.user.is_authenticated() and ispromember(request.user) + if result: + promember=1 + + r2 = getrower(theuser) + + if rankingonly: + rankingpiece = [True,] + else: + rankingpiece = [True,False] + + allworkouts = Workout.objects.filter( + user=r2, + workouttype__in=modalities, + boattype__in=waterboattype, + startdatetime__gte=startdate, + startdatetime__lte=enddate, + rankingpiece__in=rankingpiece + ).order_by("-date", "-starttime") + + ids = [int(workout.id) for workout in allworkouts] + + datemapping = { + w.id:w.date for w in allworkouts + } + + + + fieldlist,fielddict = dataprep.getstatsfields() + + # prepare data frame + datadf,extracols = dataprep.read_cols_df_sql(ids,fieldlist) + + datadf = dataprep.clean_df_stats(datadf,workstrokesonly=workstrokesonly) + + request.session['rowerid'] = r.id + + if datadf.empty: + stats = {} + cordict = {} + + response = render(request, + 'cumstats.html', + { + 'stats':stats, + 'teams':get_my_teams(request.user), + 'options':options, + 'active':'nav-analysis', + 'rower':r, + 'id':theuser, + 'theuser':theuser, + 'startdate':startdate, + 'enddate':enddate, + 'form':form, + 'optionsform':modalityform, + 'cordict':cordict, + }) + + request.session['options'] = options + + return response + + + + # Create stats + stats = {} + fielddict.pop('workoutstate') + fielddict.pop('workoutid') + + for field,verbosename in fielddict.iteritems(): + thedict = { + 'mean':datadf[field].mean(), + 'min': datadf[field].min(), + 'std': datadf[field].std(), + 'max': datadf[field].max(), + 'median': datadf[field].median(), + 'firstq':datadf[field].quantile(q=0.25), + 'thirdq':datadf[field].quantile(q=0.75), + 'verbosename':verbosename, + } + stats[field] = thedict + + # Create a dict with correlation values + cor = datadf.corr(method='spearman') + cor.fillna(value=0,inplace=True) + cordict = {} + for field1,verbosename in fielddict.iteritems(): + thedict = {} + for field2,verbosename in fielddict.iteritems(): + try: + thedict[field2] = cor.loc[field1,field2] + except KeyError: + thedict[field2] = 0 + + cordict[field1] = thedict + + # set options form correctly + initial = {} + initial['includereststrokes'] = includereststrokes + initial['waterboattype'] = waterboattype + initial['rankingonly'] = rankingonly + + + response = render(request, + 'cumstats.html', + { + 'stats':stats, + 'teams':get_my_teams(request.user), + 'active':'nav-analysis', + 'rower':r, + 'options':options, + 'id':theuser, + 'theuser':theuser, + 'startdate':startdate, + 'enddate':enddate, + 'form':form, + 'optionsform':modalityform, + 'cordict':cordict, + }) + + request.session['options'] = options + + return response + + +def agegroupcpview(request,age,normalize=0): + script,div = interactive_agegroupcpchart(age,normalized=normalize) + + response = render(request,'agegroupcp.html', + { + 'active': 'nav-analysis', + 'interactiveplot':script, + 'the_div':div, + } + ) + + return response + +def agegrouprecordview(request,sex='male',weightcategory='hwt', + distance=2000,duration=None): + if not duration: + df = pd.DataFrame( + list( + C2WorldClassAgePerformance.objects.filter( + distance=distance, + sex=sex, + weightcategory=weightcategory + ).values() + ) + ) + else: + duration = int(duration)*60 + df = pd.DataFrame( + list( + C2WorldClassAgePerformance.objects.filter( + duration=duration, + sex=sex, + weightcategory=weightcategory + ).values() + ) + ) + + + script,div = interactive_agegroup_plot(df,sex=sex,distance=distance, + duration=duration, + weightcategory=weightcategory) + + return render(request, 'agegroupchart.html', + { + 'interactiveplot':script, + 'active':'nav-analysis', + 'the_div':div, + }) diff --git a/rowers/views/apiviews.py b/rowers/views/apiviews.py new file mode 100644 index 00000000..1a0271c9 --- /dev/null +++ b/rowers/views/apiviews.py @@ -0,0 +1,182 @@ +from statements import * + + +# Stroke data form to test API upload +@login_required() +def strokedataform(request,id=0): + + try: + id=int(id) + except ValueError: + id = 0 + + try: + w = Workout.objects.get(id=id) + except Workout.DoesNotExist: + raise Http404("Workout doesn't exist") + + if request.method == 'GET': + form = StrokeDataForm() + return render(request, 'strokedata_form.html', + { + 'form':form, + 'teams':get_my_teams(request.user), + 'id':id, + 'workout':w, + }) + elif request.method == 'POST': + form = StrokeDataForm() + + return render(request, 'strokedata_form.html', + { + 'form':form, + 'teams':get_my_teams(request.user), + 'id':id, + 'workout':w, + }) + +# Process the POSTed stroke data according to the API definition +# Return the GET stroke data according to the API definition +from rest_framework_swagger.renderers import OpenAPIRenderer, SwaggerUIRenderer + +@csrf_exempt +@login_required() +@api_view(['GET','POST']) +def strokedatajson(request,id): + """ + POST: Add Stroke data to workout + GET: Get stroke data of workout + """ + row = get_workout_permitted(request.user,id) + + try: + id = int(id) + except ValueError: + return HttpResponse("Not a valid workout number",status=400) + + + if request.method == 'GET': + # currently only returns a subset. + columns = ['spm','time','hr','pace','power','distance'] + datadf = dataprep.getsmallrowdata_db(columns,ids=[id]) + with open('media/apilog.log','a') as logfile: + logfile.write(str(timezone.now())+": ") + logfile.write(request.user.username+"(GET) \n") + return JSONResponse(datadf) + + if request.method == 'POST': + checkdata,r = dataprep.getrowdata_db(id=row.id) + if not checkdata.empty: + return HttpResponse("Duplicate Error",409) + # strokedata = request.POST['strokedata'] + # checking/validating and cleaning + try: + strokedata = json.loads(request.POST['strokedata']) + except: + return HttpResponse("No JSON object could be decoded",400) + + df = pd.DataFrame(strokedata) + df.index = df.index.astype(int) + df.sort_index(inplace=True) + # time, hr, pace, spm, power, drivelength, distance, drivespeed, dragfactor, strokerecoverytime, averagedriveforce, peakdriveforce, lapidx + try: + time = df['time']/1.e3 + except KeyError: + return HttpResponse("There must be time values",status=400) + aantal = len(time) + pace = df['pace']/1.e3 + if len(pace) != aantal: + return HttpResponse("Pace array has incorrect length",status=400) + distance = df['distance'] + if len(distance) != aantal: + return HttpResponse("Distance array has incorrect length",status=400) + + spm = df['spm'] + if len(spm) != aantal: + return HttpResponse("SPM array has incorrect length",status=400) + + res = dataprep.testdata(time,distance,pace,spm) + if not res: + return HttpResponse("Data are not numerical",status=400) + + power = trydf(df,aantal,'power') + drivelength = trydf(df,aantal,'drivelength') + drivespeed = trydf(df,aantal,'drivespeed') + dragfactor = trydf(df,aantal,'dragfactor') + drivetime = trydf(df,aantal,'drivetime') + strokerecoverytime = trydf(df,aantal,'strokerecoverytime') + averagedriveforce = trydf(df,aantal,'averagedriveforce') + peakdriveforce = trydf(df,aantal,'peakdriveforce') + wash = trydf(df,aantal,'wash') + catch = trydf(df,aantal,'catch') + finish = trydf(df,aantal,'finish') + peakforceangle = trydf(df,aantal,'peakforceangle') + driveenergy = trydf(df,aantal,'driveenergy') + slip = trydf(df,aantal,'slip') + lapidx = trydf(df,aantal,'lapidx') + hr = trydf(df,aantal,'hr') + + starttime = totimestamp(row.startdatetime)+time[0] + unixtime = starttime+time + + with open('media/apilog.log','a') as logfile: + logfile.write(str(starttime)+": ") + logfile.write(request.user.username+"(POST) \r\n") + + data = pd.DataFrame({'TimeStamp (sec)':unixtime, + ' Horizontal (meters)': distance, + ' Cadence (stokes/min)':spm, + ' HRCur (bpm)':hr, + ' DragFactor':dragfactor, + ' Stroke500mPace (sec/500m)':pace, + ' Power (watts)':power, + ' DriveLength (meters)':drivelength, + ' DriveTime (ms)':drivetime, + ' StrokeRecoveryTime (ms)':strokerecoverytime, + ' AverageDriveForce (lbs)':averagedriveforce, + ' PeakDriveForce (lbs)':peakdriveforce, + ' lapIdx':lapidx, + ' ElapsedTime (sec)':time, + 'catch':catch, + 'slip':slip, + 'finish':finish, + 'wash':wash, + 'driveenergy':driveenergy, + 'peakforceangle':peakforceangle, + }) + + # Following part should be replaced with dataprep.new_workout_from_df + + r = getrower(request.user) + + timestr = row.startdatetime.strftime("%Y%m%d-%H%M%S") + csvfilename ='media/Import_'+timestr+'.csv' + + res = data.to_csv(csvfilename+'.gz',index_label='index', + compression='gzip') + row.csvfilename = csvfilename + row.save() + + powerperc = 100*np.array([r.pw_ut2, + r.pw_ut1, + r.pw_at, + r.pw_tr,r.pw_an])/r.ftp + + ftp = float(r.ftp) + if row.workouttype in mytypes.otwtypes: + ftp = ftp*(100.-r.otwslack)/100. + + rr = rrower(hrmax=r.max,hrut2=r.ut2, + hrut1=r.ut1,hrat=r.at, + hrtr=r.tr,hran=r.an,ftp=ftp, + powerperc=powerperc,powerzones=r.powerzones) + rowdata = rdata(row.csvfilename,rower=rr).df + + datadf = dataprep.dataprep(rowdata,id=row.id,bands=True,barchart=True,otwpower=True,empower=True) + # mangling + + # + return HttpResponse(row.id,status=201) + + #Method not supported + return HttpResponseNotAllowed("Method not supported") diff --git a/rowers/views/errorviews.py b/rowers/views/errorviews.py new file mode 100644 index 00000000..a1c31901 --- /dev/null +++ b/rowers/views/errorviews.py @@ -0,0 +1,30 @@ +from statements import * + +# Custom error pages with Rowsandall headers +def error500_view(request): + response = render_to_response('500.html', {}, + context_instance = RequestContext(request)) + + response.status_code = 500 + return response + +def error404_view(request): + response = render_to_response('404.html', {}, + context_instance = RequestContext(request)) + + response.status_code = 404 + return response + +def error400_view(request): + response = render_to_response('400.html', {}, + context_instance = RequestContext(request)) + + response.status_code = 400 + return response + +def error403_view(request): + response = render_to_response('403.html', {}, + context_instance = RequestContext(request)) + + response.status_code = 403 + return response diff --git a/rowers/views/exportviews.py b/rowers/views/exportviews.py new file mode 100644 index 00000000..a71d9a4f --- /dev/null +++ b/rowers/views/exportviews.py @@ -0,0 +1,220 @@ +from statements import * + + + + + + +# Export workout to TCX and send to user's email address +@login_required() +def workout_tcxemail_view(request,id=0): + r = getrower(request.user) + w = get_workout(id) + + if not checkworkoutuser(request.user,w): + raise PermissionDenied("Access denied") + + + row = rdata(w.csvfilename) + + code = str(uuid4()) + tcxfilename = code+'.tcx' + + row.exporttotcx(tcxfilename) + + with open(tcxfilename,'r') as f: + response = HttpResponse(f) + response['Content-Disposition'] = 'attachment; filename="%s"' % tcxfilename + response['Content-Type'] = 'application/octet-stream' + + os.remove(tcxfilename) + return response + + + + + +@login_required() +def plannedsessions_icsemail_view(request,userid=0): + r = getrequestrower(request,userid=userid) + startdate,enddate = get_dates_timeperiod(request) + + sps = get_sessions(r,startdate=startdate,enddate=enddate) + + cal = Calendar() + cal.add('prodid','rowsandall') + cal.add('version','1.0') + + for ps in sps: + event = Event() + comment = '{d} {u} {c}'.format( + d=ps.sessionvalue, + u = ps.sessionunit, + c = ps.criterium) + event.add('summary',ps.name) + event.add('dtstart',ps.preferreddate) + event.add('dtend',ps.preferreddate) + event['uid'] = 'plannedsession_'+str(ps.id) + event.add('description',ps.comment) + event.add('comment',comment) + cal.add_component(event) + + + response = HttpResponse(cal.to_ical()) + response['Content-Disposition'] = 'attachment; filename="training_plan_{u}_{d1}_{d2}.ics"'.format( + u = request.user.username, + d1 = startdate.strftime("%Y%m%d"), + d2 = enddate.strftime("%Y%m%d"), + ) + + response['Content-Type'] = 'application/octet-stream' + + return response + + +@login_required() +def course_kmldownload_view(request,id=0): + r = getrower(request.user) + if r.emailbounced: + message = "Please check your email address first. Email to this address bounced." + messages.error(request,message) + return HttpResponseRedirect( + reverse(course_view, + kwargs = { + 'id':str(id), + }) + ) + + course = GeoCourse.objects.get(id=id) + + kmlstring = courses.coursetokml(course) + + kmlfilename = 'course_{id}.kml'.format(id=id) + + response = HttpResponse(kmlstring) + response['Content-Disposition'] = 'attachment; filename="{filename}"'.format(filename=kmlfilename) + response['Content-Type'] = 'application/octet-stream' + + return response + + + +# Export workout to GPX and send to user's email address +@login_required() +def workout_gpxemail_view(request,id=0): + r = getrower(request.user) + w = get_workout(id) + + if not checkworkoutuser(request.user,w): + raise PermissionDenied("Access denied") + + + row = rdata(w.csvfilename) + + code = str(uuid4()) + gpxfilename = code+'.gpx' + + row.exporttogpx(gpxfilename) + + with open(gpxfilename,'r') as f: + response = HttpResponse(f) + response['Content-Disposition'] = 'attachment; filename="%s"' % gpxfilename + response['Content-Type'] = 'application/octet-stream' + + os.remove(gpxfilename) + return response + +# Get Workout summary CSV file +@login_required() +def workouts_summaries_email_view(request): + r = getrower(request.user) + if r.emailbounced: + message = "Please check your email address first. Email to this address bounced." + messages.error(request, message) + return HttpResponseRedirect( + reverse(r.defaultlandingpage, + kwargs = { + 'id':str(w.id), + }) + ) + + if request.method == 'POST': + form = DateRangeForm(request.POST) + if form.is_valid(): + startdate = form.cleaned_data['startdate'] + enddate = form.cleaned_data['enddate'] + filename = 'rowsandall_workouts_{first}_{last}.csv'.format( + first=startdate, + last=enddate + ) + df = dataprep.workout_summary_to_df(r,startdate=startdate,enddate=enddate) + df.to_csv(filename,encoding='utf-8') + res = myqueue(queuehigh,handle_sendemailsummary, + r.user.first_name, + r.user.last_name, + r.user.email, + filename, + emailbounced = r.emailbounced + ) + messages.info(request,'The summary CSV file was sent to you per email') + else: + form = DateRangeForm() + + return render(request,"export_workouts.html", + { + 'form':form + }) + + +# Get Workout CSV file and send it to user's email address +@login_required() +def workout_csvemail_view(request,id=0): + r = getrower(request.user) + + w = get_workout(id) + + if not checkworkoutuser(request.user,w): + raise PermissionDenied("Access denied") + + rowdata = rdata(w.csvfilename) + code = str(uuid4()) + filename = code+'.csv' + + rowdate = rowdata.rowdatetime + starttimeunix = arrow.get(rowdate).timestamp + df = rowdata.df + df[' ElapsedTime (sec)'] = df['TimeStamp (sec)'] + df['TimeStamp (sec)'] = df['TimeStamp (sec)'] + starttimeunix + + response = HttpResponse(df.to_csv()) + response['Content-Disposition'] = 'attachment; filename="%s"' % filename + response['Content-Type'] = 'application/octet-stream' + + return response + + +# Get Workout CSV file and send it to user's email address +@login_required() +def workout_csvtoadmin_view(request,id=0): + message = "" + r = getrower(request.user) + w = get_workout(id) + + + csvfile = w.csvfilename + res = myqueue(queuehigh, + handle_sendemailcsv, + 'Sander', + 'Roosendaal', + 'roosendaalsander@gmail.com', + csvfile) + + successmessage = "The CSV file was sent to the site admin per email" + messages.info(request,successmessage) + url = reverse(workout_view, + kwargs = { + 'id':str(w.id), + }) + response = HttpResponseRedirect(url) + + return response diff --git a/rowers/views/importviews.py b/rowers/views/importviews.py new file mode 100644 index 00000000..83ee32b9 --- /dev/null +++ b/rowers/views/importviews.py @@ -0,0 +1,1604 @@ +from statements import * + + +# Send workout to TP +@login_required() +def workout_tp_upload_view(request,id=0): + + message = "" + r = getrower(request.user) + res = -1 + try: + thetoken = tp_open(r.user) + except NoTokenError: + return HttpResponseRedirect("/rowers/me/tpauthorize/") + + # ready to upload. Hurray + w = get_workout_permitted(request.user,id) + r = w.user + + if (checkworkoutuser(request.user,w)): + tcxfile = tpstuff.createtpworkoutdata(w) + if tcxfile: + res,reason,status_code,headers = tpstuff.uploadactivity( + r.tptoken,tcxfile, + name=w.name + ) + if res == 0: + message = "Upload to TrainingPeaks failed with status code "+str(status_code)+": "+reason + try: + os.remove(tcxfile) + except WindowsError: + pass + + messages.error(request,message) + + else: # res != 0 + w.uploadedtotp = res + w.save() + os.remove(tcxfile) + messages.info(request,'Uploaded to TrainingPeaks') + + else: # no tcxfile + message = "Upload to TrainingPeaks failed" + w.uploadedtotp = -1 + w.save() + messages.error(request,message) + + else: # not allowed to upload + message = "You are not allowed to export this workout to TP" + messages.error(request,message) + + url = reverse(r.defaultlandingpage, + kwargs = { + 'id':str(w.id), + }) + + return HttpResponseRedirect(url) + + +# Send workout to Strava +# abundance of error logging here because there were/are some bugs +@login_required() +def workout_strava_upload_view(request,id=0): + message = "" + r = getrower(request.user) + res = -1 + + try: + thetoken = strava_open(request.user) + except NoTokenError: + return HttpResponseRedirect("/rowers/me/stravaauthorize/") + + if (r.stravatoken == '') or (r.stravatoken is None): + s = "Token doesn't exist. Need to authorize" + return HttpResponseRedirect("/rowers/me/stravaauthorize/") + else: + # ready to upload. Hurray + w = get_workout_permitted(request.user,id) + r = w.user + if (checkworkoutuser(request.user,w)): + try: + tcxfile,tcxmessg = stravastuff.createstravaworkoutdata(w) + if tcxfile: + with open(tcxfile,'rb') as f: + try: + newnotes = w.notes+'\n from '+w.workoutsource+' via rowsandall.com' + except TypeError: + newnotes = 'from '+w.workoutsource+' via rowsandall.com' + if w.workouttype in mytypes.rowtypes: + activity_type = r.stravaexportas + else: + activity_type = mytypes.stravamapping[w.workouttype] + + res,mes = stravastuff.handle_stravaexport( + f,w.name, + r.stravatoken, + description=newnotes, + activity_type=activity_type) + if res==0: + messages.error(request,mes) + w.uploadedtostrava = -1 + w.save() + try: + os.remove(tcxfile) + except WindowsError: + pass + url = reverse(r.defaultlandingpage, + kwargs = { + 'id':str(w.id), + }) + response = HttpResponseRedirect(url) + return response + + try: + w.uploadedtostrava = res + w.save() + try: + os.remove(tcxfile) + except WindowsError: + pass + url = reverse(workout_edit_view,kwargs={'id':w.id}) + + + messages.info(request,mes) + except: + with open("media/stravaerrors.log","a") as errorlog: + errorstring = str(sys.exc_info()[0]) + timestr = strftime("%Y%m%d-%H%M%S") + errorlog.write(timestr+errorstring+"\r\n") + errorlog.write("views.py line 826\r\n") + message = 'Error: '+errorstring + messages.error(request,message) + else: # No tcxfile + message = "Strava Data error "+tcxmessg + messages.error(request,message) + w.uploadedtostrava = -1 + w.save() + url = reverse(r.defaultlandingpage, + kwargs = { + 'id':str(w.id), + }) + response = HttpResponseRedirect(url) + + + url = reverse(r.defaultlandingpage, + kwargs = { + 'id':str(w.id), + } + ) + response = HttpResponseRedirect(url) + except ActivityUploadFailed as e: + message = "Strava Upload error: %s" % e + messages.error(request,message) + w.uploadedtostrava = -1 + w.save() + os.remove(tcxfile) + url = reverse(r.defaultlandingpage, + kwargs = { + 'id':str(w.id), + }) + response = HttpResponseRedirect(url) + + return response + +# Upload workout to Concept2 logbook +@login_required() +def workout_c2_upload_view(request,id=0): + message = "" + # ready to upload. Hurray + w = get_workout(id) + r = w.user + + try: + message,c2id = c2stuff.workout_c2_upload(request.user,w) + except NoTokenError: + return HttpResponseRedirect("/rowers/me/c2authorize/") + + if message and c2id <=0: + messages.error(request,message) + elif message: + messages.info(request,message) + + + url = reverse(r.defaultlandingpage, + kwargs = { + 'id':int(id) + }) + + + response = HttpResponseRedirect(url) + + return response + +# Upload workout to RunKeeper +@login_required() +def workout_runkeeper_upload_view(request,id=0): + message = "" + w = get_workout(id) + r = w.user + + try: + thetoken = runkeeper_open(r.user) + except NoTokenError: + return HttpResponseRedirect("/rowers/me/runkeeperauthorize/") + + # ready to upload. Hurray + + if (checkworkoutuser(request.user,w)): + data = runkeeperstuff.createrunkeeperworkoutdata(w) + if not data: + message = "Data error" + messages.error(request,message) + url = reverse(r.defaultlandingpage, + kwargs = { + 'id':str(w.id), + }) + return HttpResponseRedirect(url) + + authorizationstring = str('Bearer ' + thetoken) + headers = {'Authorization': authorizationstring, + 'user-agent': 'sanderroosendaal', + 'Content-Type': 'application/vnd.com.runkeeper.NewFitnessActivity+json', + 'Content-Length':'nnn'} + + url = "https://api.runkeeper.com/fitnessActivities" + response = requests.post(url,headers=headers,data=json.dumps(data)) + + # check for duplicate error first + if (response.status_code == 409 ): + message = "Duplicate error" + messages.error(request,message) + w.uploadedtorunkeeper = -1 + w.save() + elif (response.status_code == 201 or response.status_code==200): + runkeeperid = runkeeperstuff.getidfromresponse(response) + w.uploadedtorunkeeper = runkeeperid + w.save() + url = reverse(workout_edit_view, kwargs={'id':w.id}) + + return HttpResponseRedirect(url) + else: + s = response + message = "Something went wrong in workout_runkeeper_upload_view: %s - %s" % (s.reason,s.text) + messages.error(request,message) + + else: + message = "You are not authorized to upload this workout" + messages.error(request,message) + + url = reverse(r.defaultlandingpage, + kwargs = { + 'id':str(w.id), + }) + + return HttpResponseRedirect(url) + +# Upload workout to Underarmour +@login_required() +def workout_underarmour_upload_view(request,id=0): + message = "" + w = get_workout(id) + r = w.user + + try: + thetoken = underarmour_open(r.user) + except NoTokenError: + return HttpResponseRedirect("/rowers/me/underarmourauthorize/") + + # ready to upload. Hurray + + if (checkworkoutuser(request.user,w)): + data = underarmourstuff.createunderarmourworkoutdata(w) + if not data: + message = "Data error" + messages.error(request,message) + url = reverse(r.defaultlandingpage, + kwargs = { + 'id':str(w.id), + }) + return HttpResponseRedirect(url) + + authorizationstring = str('Bearer ' + thetoken) + headers = {'Authorization': authorizationstring, + 'Api-Key': UNDERARMOUR_CLIENT_KEY, + 'user-agent': 'sanderroosendaal', + 'Content-Type': 'application/json', + } + + url = "https://api.ua.com/v7.1/workout/" + response = requests.post(url,headers=headers,data=json.dumps(data)) + + + # check for duplicate error first + if (response.status_code == 409 ): + message = "Duplicate error" + messages.error(request,message) + w.uploadedtounderarmour = -1 + w.save() + elif (response.status_code == 201 or response.status_code==200): + underarmourid = underarmourstuff.getidfromresponse(response) + w.uploadedtounderarmour = underarmourid + w.save() + url = reverse(workout_edit_view,kwargs={'id':w.id}) + + return HttpResponseRedirect(url) + else: + s = response + message = "Something went wrong in workout_underarmour_upload_view: %s " % s.reason + messages.error(request,message) + else: + message = "You are not authorized to upload this workout" + messages.error(request,message) + + url = reverse(r.defaultlandingpage, + kwargs = { + 'id':str(w.id), + }) + + return HttpResponseRedirect(url) + +# Upload workout to SportTracks +@login_required() +def workout_sporttracks_upload_view(request,id=0): + message = "" + # ready to upload. Hurray + w = get_workout(id) + r = w.user + + try: + thetoken = sporttracks_open(r.user) + except NoTokenError: + return HttpResponseRedirect("/rowers/me/sporttracksauthorize/") + + + if (checkworkoutuser(request.user,w)): + data = sporttracksstuff.createsporttracksworkoutdata(w) + + if not data: + message = "Data error" + messages.error(request,message) + url = reverse(r.defaultlandingpage, + kwargs = { + 'id':str(w.id), + }) + return HttpResponseRedirect(url) + + authorizationstring = str('Bearer ' + thetoken) + headers = {'Authorization': authorizationstring, + 'user-agent': 'sanderroosendaal', + 'Content-Type': 'application/json'} + + url = "https://api.sporttracks.mobi/api/v2/fitnessActivities.json" + response = requests.post(url,headers=headers,data=json.dumps(data)) + + + # check for duplicate error first + if (response.status_code == 409 ): + message = "Duplicate error" + messages.error(request,message) + w.uploadedtosporttracks = -1 + w.save() + elif (response.status_code == 201 or response.status_code==200): + s= response.json() + sporttracksid = sporttracksstuff.getidfromresponse(response) + w.uploadedtosporttracks = sporttracksid + w.save() + message = "Upload to SportTracks was successful" + messages.info(request,message) + + url = reverse(workout_edit_view,kwargs={'id':w.id}) + return HttpResponseRedirect(url) + else: + s = response + message = "Something went wrong in workout_sporttracks_upload_view: %s" % s.reason + messages.error(request,message) + else: + message = "You are not authorized to upload this workout" + messages.error(request,message) + + url = reverse(r.defaultlandingpage, + kwargs = { + 'id':str(w.id), + }) + + return HttpResponseRedirect(url) + +# Concept2 authorization +@login_required() +def rower_c2_authorize(request): + # Generate a random string for the state parameter + # Save it for use later to prevent xsrf attacks + + state = str(uuid4()) + scope = "user:read,results:write" + params = {"client_id": C2_CLIENT_ID, + "response_type": "code", + "redirect_uri": C2_REDIRECT_URI} + url = "http://log.concept2.com/oauth/authorize?"+ urllib.urlencode(params) + url += "&scope="+scope + return HttpResponseRedirect(url) + +# Strava Authorization +@login_required() +def rower_strava_authorize(request): + # Generate a random string for the state parameter + # Save it for use later to prevent xsrf attacks + + state = str(uuid4()) + + params = {"client_id": STRAVA_CLIENT_ID, + "response_type": "code", + "redirect_uri": STRAVA_REDIRECT_URI, + "scope": "activity:write,activity:read_all"} + + url = "https://www.strava.com/oauth/authorize?"+ urllib.urlencode(params) + + return HttpResponseRedirect(url) + +# Polar Authorization +@login_required() +def rower_polar_authorize(request): + + state = str(uuid4()) + + params = {"client_id": POLAR_CLIENT_ID, + "response_type": "code", + "redirect_uri": POLAR_REDIRECT_URI, + "state": state, +# "scope":"accesslink.read_all" + } + url = "https://flow.polar.com/oauth2/authorization?" +urllib.urlencode(params) + + return HttpResponseRedirect(url) + + + +# Runkeeper authorization +@login_required() +def rower_runkeeper_authorize(request): + # Generate a random string for the state parameter + # Save it for use later to prevent xsrf attacks + + state = str(uuid4()) + + params = {"client_id": RUNKEEPER_CLIENT_ID, + "response_type": "code", + "state": state, + "redirect_uri": RUNKEEPER_REDIRECT_URI} + + url = "https://runkeeper.com/apps/authorize?"+ urllib.urlencode(params) + + + return HttpResponseRedirect(url) + +# SportTracks Authorization +@login_required() +def rower_sporttracks_authorize(request): + # Generate a random string for the state parameter + # Save it for use later to prevent xsrf attacks + + state = str(uuid4()) + + params = {"client_id": SPORTTRACKS_CLIENT_ID, + "response_type": "code", + "state": state, + "redirect_uri": SPORTTRACKS_REDIRECT_URI} + + url = "https://api.sporttracks.mobi/oauth2/authorize?"+ urllib.urlencode(params) + + + return HttpResponseRedirect(url) + +# Underarmour Authorization +@login_required() +def rower_underarmour_authorize(request): + # Generate a random string for the state parameter + # Save it for use later to prevent xsrf attacks + + state = str(uuid4()) + + redirect_uri = UNDERARMOUR_REDIRECT_URI + + url = 'https://www.mapmyfitness.com/v7.1/oauth2/authorize/?' \ + 'client_id={0}&response_type=code&redirect_uri={1}'.format( + UNDERARMOUR_CLIENT_KEY, redirect_uri + ) + + return HttpResponseRedirect(url) + +# Underarmour Authorization +@login_required() +def rower_tp_authorize(request): + # Generate a random string for the state parameter + # Save it for use later to prevent xsrf attacks + + state = str(uuid4()) + params = {"client_id": TP_CLIENT_KEY, + "response_type": "code", + "redirect_uri": TP_REDIRECT_URI, + "scope": "file:write", + } + url = "https://oauth.trainingpeaks.com/oauth/authorize/?" +urllib.urlencode(params) + + return HttpResponseRedirect(url) + + +# Concept2 token refresh. URL for manual refresh. Not visible to users +@login_required() +def rower_c2_token_refresh(request): + r = getrower(request.user) + res = c2stuff.do_refresh_token(r.c2refreshtoken) + + if res[0] != None: + access_token = res[0] + expires_in = res[1] + refresh_token = res[2] + expirydatetime = timezone.now()+datetime.timedelta(seconds=expires_in) + r = getrower(request.user) + r.c2token = access_token + r.tokenexpirydate = expirydatetime + r.c2refreshtoken = refresh_token + + r.save() + + successmessage = "Tokens refreshed. Good to go" + messages.info(request,successmessage) + else: + message = "Something went wrong (refreshing tokens). Please reauthorize:" + messages.error(request,message) + + url = reverse(workouts_view) + + return HttpResponseRedirect(url) + + +# Underarmour token refresh. URL for manual refresh. Not visible to users +@login_required() +def rower_underarmour_token_refresh(request): + r = getrower(request.user) + res = underarmourstuff.do_refresh_token( + r.underarmourrefreshtoken, + r.underarmourtoken + ) + access_token = res[0] + expires_in = res[1] + refresh_token = res[2] + expirydatetime = timezone.now()+datetime.timedelta(seconds=expires_in) + + r = getrower(request.user) + r.underarmourtoken = access_token + r.underarmourtokenexpirydate = expirydatetime + r.underarmourrefreshtoken = refresh_token + + r.save() + + successmessage = "Tokens refreshed. Good to go" + messages.info(request,successmessage) + + url = reverse(workouts_view) + + return HttpResponseRedirect(url) + + + +# TrainingPeaks token refresh. URL for manual refresh. Not visible to users +@login_required() +def rower_tp_token_refresh(request): + r = getrower(request.user) + res = tpstuff.do_refresh_token( + r.tprefreshtoken, + ) + access_token = res[0] + expires_in = res[1] + refresh_token = res[2] + expirydatetime = timezone.now()+datetime.timedelta(seconds=expires_in) + + r = getrower(request.user) + r.tptoken = access_token + r.tptokenexpirydate = expirydatetime + r.tprefreshtoken = refresh_token + + r.save() + + successmessage = "Tokens refreshed. Good to go" + messages.info(request,successmessage) + + url = reverse(workouts_view) + + return HttpResponseRedirect(url) + + + + +# SportTracks token refresh. URL for manual refresh. Not visible to users +@login_required() +def rower_sporttracks_token_refresh(request): + r = getrower(request.user) + res = sporttracksstuff.do_refresh_token( + r.sporttracksrefreshtoken, + ) + access_token = res[0] + expires_in = res[1] + refresh_token = res[2] + expirydatetime = timezone.now()+datetime.timedelta(seconds=expires_in) + + r = getrower(request.user) + r.sporttrackstoken = access_token + r.sporttrackstokenexpirydate = expirydatetime + r.sporttracksrefreshtoken = refresh_token + + r.save() + + successmessage = "Tokens refreshed. Good to go" + messages.info(request,successmessage) + + url = reverse(workouts_view) + + return HttpResponseRedirect(url) + + + +# Concept2 Callback +@login_required() +def rower_process_callback(request): + try: + code = request.GET['code'] + res = c2stuff.get_token(code) + except MultiValueDictKeyError: + message = "The resource owner or authorization server denied the request" + messages.error(request,message) + + url = reverse(workouts_view) + + return HttpResponseRedirect(url) + + access_token = res[0] + if access_token == 0: + message = res[1] + message += ' Contact info@rowsandall.com if this behavior persists.' + messages.error(request,message) + + url = reverse(workouts_view) + + return HttpResponseRedirect(url) + + + expires_in = res[1] + refresh_token = res[2] + expirydatetime = timezone.now()+datetime.timedelta(seconds=expires_in) + + r = getrower(request.user) + r.c2token = access_token + r.tokenexpirydate = expirydatetime + r.c2refreshtoken = refresh_token + + r.save() + + successmessage = "Tokens stored. Good to go" + messages.info(request,successmessage) + + url = reverse(workouts_view) + + return HttpResponseRedirect(url) + + + +# dummy +@login_required() +def rower_process_twittercallback(request): + return "dummy" + +# Process Polar Callback +@login_required() +def rower_process_polarcallback(request): + try: + code = request.GET['code'] + except MultiValueDictKeyError: + try: + message = request.GET['error'] + except MultiValueDictKeyError: + message = "access error" + + messages.error(request,message) + url = reverse(workouts_view) + + return HttpResponseRedirect(url) + + + + access_token, expires_in, user_id = polarstuff.get_token(code) + + expirydatetime = timezone.now()+datetime.timedelta(seconds=expires_in) + + r = getrower(request.user) + r.polartoken = access_token + r.polartokenexpirydate = expirydatetime + r.polaruserid = user_id + + r.save() + + successmessage = "Tokens stored. Good to go" + messages.info(request,successmessage) + url = reverse(workouts_view) + + return HttpResponseRedirect(url) + + + + +# Process Strava Callback +@login_required() +def rower_process_stravacallback(request): + try: + code = request.GET['code'] + scope = request.GET['scope'] + except MultiValueDictKeyError: + try: + message = request.GET['error'] + except MultiValueDictKeyError: + message = "access error" + + messages.error(request,message) + url = reverse(workouts_view) + + return HttpResponseRedirect(url) + + + res = stravastuff.get_token(code) + + if res[0]: + access_token = res[0] + expires_in = res[1] + refresh_token = res[2] + + expirydatetime = timezone.now()+datetime.timedelta(seconds=expires_in) + + r = getrower(request.user) + r.stravatoken = access_token + r.stravatokenexpirydate = expirydatetime + r.stravarefreshtoken = refresh_token + + r.save() + + successmessage = "Tokens stored. Good to go" + messages.info(request,successmessage) + url = reverse(workouts_view) + return HttpResponseRedirect(url) + else: + message = "Something went wrong with the Strava authorization" + messages.error(request,message) + url = reverse(workouts_view) + + return HttpResponseRedirect(url) + + +# Process Runkeeper callback +@login_required() +def rower_process_runkeepercallback(request): + code = request.GET['code'] + res = runkeeperstuff.get_token(code) + access_token = res[0] + + if access_token == 0: + messages.error(request,"Something went wrong importing the token") + url = reverse(workouts_view) + + return HttpResponseRedirect(url) + + + + r = getrower(request.user) + r.runkeepertoken = access_token + + r.save() + + successmessage = "Tokens stored. Good to go" + messages.info(request,successmessage) + url = reverse(workouts_view) + + return HttpResponseRedirect(url) + + + +# Process SportTracks callback +@login_required() +def rower_process_sporttrackscallback(request): + code = request.GET['code'] + res = sporttracksstuff.get_token(code) + + + access_token = res[0] + expires_in = res[1] + refresh_token = res[2] + + expirydatetime = timezone.now()+datetime.timedelta(seconds=expires_in) + + r = getrower(request.user) + r.sporttrackstoken = access_token + r.sporttrackstokenexpirydate = expirydatetime + r.sporttracksrefreshtoken = refresh_token + + r.save() + + successmessage = "Tokens stored. Good to go" + messages.info(request,successmessage) + url = reverse(workouts_view) + + return HttpResponseRedirect(url) + + + +# Process Underarmour callback +@login_required() +def rower_process_underarmourcallback(request): + code = request.GET['code'] + res = underarmourstuff.get_token(code) + + + access_token = res[0] + expires_in = res[1] + refresh_token = res[2] + expirydatetime = timezone.now()+datetime.timedelta(seconds=expires_in) + + r = getrower(request.user) + r.underarmourtoken = access_token + r.underarmourtokenexpirydate = expirydatetime + r.underarmourrefreshtoken = refresh_token + + r.save() + + successmessage = "Tokens stored. Good to go" + messages.info(request,successmessage) + url = reverse(workouts_view) + + return HttpResponseRedirect(url) + + + +# Process TrainingPeaks callback +@login_required() +def rower_process_tpcallback(request): + code = request.GET['code'] + res = tpstuff.get_token(code) + + access_token = res[0] + expires_in = res[1] + refresh_token = res[2] + expirydatetime = timezone.now()+datetime.timedelta(seconds=expires_in) + + r = getrower(request.user) + r.tptoken = access_token + r.tptokenexpirydate = expirydatetime + r.tprefreshtoken = refresh_token + + r.save() + + successmessage = "Tokens stored. Good to go" + messages.info(request,successmessage) + url = reverse(workouts_view) + + return HttpResponseRedirect(url) + + + +# Process Own API callback - for API testing purposes +@login_required() +def rower_process_testcallback(request): + code = request.GET['code'] + res = ownapistuff.get_token(code) + + + access_token = res[0] + expires_in = res[1] + refresh_token = res[2] + expirydatetime = timezone.now()+datetime.timedelta(seconds=expires_in) + + text = "Access Token:\n" + text += access_token + + text += "\n\nRefresh Token:\n" + text += refresh_token + + return HttpResponse(text) + + + +# The page where you select which Strava workout to import +@login_required() +def workout_stravaimport_view(request,message="",userid=0): + r = getrequestrower(request,userid=userid) + if r.user != request.user: + messages.info(request,"You cannot import other people's workouts from Strava") + + try: + thetoken = strava_open(request.user) + except NoTokenError: + return HttpResponseRedirect("/rowers/me/stravaauthorize/") + + + res = stravastuff.get_strava_workout_list(request.user) + + + if (res.status_code != 200): + if (res.status_code == 401): + r = getrower(request.user) + if (r.stravatoken == '') or (r.stravatoken is None): + s = "Token doesn't exist. Need to authorize" + return HttpResponseRedirect("/rowers/me/stravaauthorize/") + message = "Something went wrong in workout_stravaimport_view" + messages.error(request,message) + url = reverse(workouts_view) + return HttpResponseRedirect(url) + else: + workouts = [] + r = getrower(request.user) + stravaids = [int(item['id']) for item in res.json()] + stravadata = [{ + 'id':int(item['id']), + 'elapsed_time':item['elapsed_time'], + 'start_date':item['start_date'], + } for item in res.json()] + + wfailed = Workout.objects.filter(user=r,uploadedtostrava=-1) + + for w in wfailed: + for item in stravadata: + elapsed_time = item['elapsed_time'] + start_date = item['start_date'] + stravaid = item['id'] + if arrow.get(start_date) == arrow.get(w.startdatetime): + elapsed_td = datetime.timedelta(seconds=int(elapsed_time)) + elapsed_time = datetime.datetime.strptime( + str(elapsed_td), + "%H:%M:%S" + ) + if str(elapsed_time)[-7:] == str(w.duration)[-7:]: + w.uploadedtostrava = int(stravaid) + w.save() + + + knownstravaids = uniqify([ + w.uploadedtostrava for w in Workout.objects.filter(user=r) + ]) + newids = [stravaid for stravaid in stravaids if not stravaid in knownstravaids] + + for item in res.json(): + d = int(float(item['distance'])) + i = item['id'] + if i in knownstravaids: + nnn = '' + else: + nnn = 'NEW' + n = item['name'] + ttot = str(datetime.timedelta(seconds=int(float(item['elapsed_time'])))) + s = item['start_date'] + r = item['type'] + keys = ['id','distance','duration','starttime','type','name','new'] + values = [i,d,ttot,s,r,n,nnn] + res = dict(zip(keys,values)) + workouts.append(res) + + breadcrumbs = [ + { + 'url':'/rowers/list-workouts/', + 'name':'Workouts' + }, + { + 'url':reverse(workout_stravaimport_view), + 'name':'Strava' + }, + ] + + + r = getrower(request.user) + + return render(request,'strava_list_import.html', + {'workouts':workouts, + 'rower':r, + 'active':'nav-workouts', + 'breadcrumbs':breadcrumbs, + 'teams':get_my_teams(request.user), + }) + + return HttpResponse(res) + +# The page where you select which RunKeeper workout to import +@login_required() +def workout_runkeeperimport_view(request,message="",userid=0): + res = runkeeperstuff.get_runkeeper_workout_list(request.user) + if (res.status_code != 200): + if (res.status_code == 401): + r = getrower(request.user) + if (r.runkeepertoken == '') or (r.runkeepertoken is None): + s = "Token doesn't exist. Need to authorize" + return HttpResponseRedirect("/rowers/me/runkeeperauthorize/") + message = "Something went wrong in workout_runkeeperimport_view" + messages.error(request,message) + + if settings.DEBUG: + return HttpResponse(res) + else: + url = reverse(workouts_view) + return HttpResponseRedirect(url) + + workouts = [] + for item in res.json()['items']: + d = int(float(item['total_distance'])) + i = getidfromuri(item['uri']) + ttot = str(datetime.timedelta(seconds=int(float(item['duration'])))) + s = item['start_time'] + r = item['type'] + keys = ['id','distance','duration','starttime','type'] + values = [i,d,ttot,s,r] + + res = dict(zip(keys,values)) + workouts.append(res) + + breadcrumbs = [ + { + 'url':'/rowers/list-workouts/', + 'name':'Workouts' + }, + { + 'url':reverse(workout_runkeeperimport_view), + 'name':'Runkeeper' + } + ] + + r = getrower(request.user) + + return render(request,'runkeeper_list_import.html', + {'workouts':workouts, + 'rower':r, + 'active':'nav-workouts', + 'breadcrumbs':breadcrumbs, + 'teams':get_my_teams(request.user), + }) + + return HttpResponse(res) + +# The page where you select which RunKeeper workout to import +@login_required() +def workout_underarmourimport_view(request,message="",userid=0): + res = underarmourstuff.get_underarmour_workout_list(request.user) + if (res.status_code != 200): + return HttpResponseRedirect("/rowers/me/underarmourauthorize/") + + workouts = [] + items = res.json()['_embedded']['workouts'] + for item in items: + s = item['start_datetime'] + i,r = underarmourstuff.get_idfromuri(request.user,item['_links']) + n = item['name'] + try: + d = item['aggregates']['distance_total'] + except KeyError: + d = 0 + try: + ttot = item['aggregates']['active_time_total'] + except KeyError: + ttot = 0 + + keys = ['id','distance','duration','starttime','type'] + values = [i,d,ttot,s,r] + thedict = dict(zip(keys,values)) + + workouts.append(thedict) + + rower = getrower(request.user) + breadcrumbs = [ + { + 'url':'/rowers/list-workouts/', + 'name':'Workouts' + }, + { + 'url':reverse(workout_c2import_view), + 'name':'Concept2' + }, + ] + + return render(request,'underarmour_list_import.html', + {'workouts':workouts, + 'breadcrumbs':breadcrumbs, + 'rower':rower, + 'active':'nav-workouts', + 'teams':get_my_teams(request.user), + }) + + return HttpResponse(res) + +# the page where you select which Polar workout to Import +@login_required() +def workout_polarimport_view(request,userid=0): + exercises = polarstuff.get_polar_workouts(request.user) + workouts = [] + + try: + a = exercises.status_code + if a == 401: + messages.error(request,'Not authorized. You need to connect to Polar first') + url = reverse(workouts_view) + return HttpResponseRedirect(url) + except: + pass + + for exercise in exercises: + try: + d = exercise['distance'] + except KeyError: + d = 0 + + i = exercise['id'] + transactionid = exercise['transaction-id'] + starttime = exercise['start-time'] + rowtype = exercise['sport'] + durationstring = exercise['duration'] + duration = isodate.parse_duration(durationstring) + keys = ['id','distance','duration','starttime','type','transactionid'] + values = [i,d,duration,starttime,rowtype,transactionid] + res = dict(zip(keys,values)) + workouts.append(res) + + breadcrumbs = [ + { + 'url':'/rowers/list-workouts/', + 'name':'Workouts' + }, + { + 'url':reverse(workout_polarimport_view), + 'name':'Polar' + }, + ] + + r = getrower(request.user) + + return render(request, 'polar_list_import.html', + { + 'workouts':workouts, + 'active':'nav-workouts', + 'rower':r, + 'breadcrumbs':breadcrumbs, + 'teams':get_my_teams(request.user), + }) + + + + +# The page where you select which SportTracks workout to import +@login_required() +def workout_sporttracksimport_view(request,message="",userid=0): + + + res = sporttracksstuff.get_sporttracks_workout_list(request.user) + if (res.status_code != 200): + if (res.status_code == 401): + r = getrower(request.user) + if (r.sporttrackstoken == '') or (r.sporttrackstoken is None): + s = "Token doesn't exist. Need to authorize" + return HttpResponseRedirect("/rowers/me/sporttracksauthorize/") + else: + return HttpResponseRedirect("/rowers/me/sporttracksrefresh/") + message = "Something went wrong in workout_sporttracksimport_view" + messages.error(request,message) + if settings.DEBUG: + return HttpResponse(res) + else: + url = reverse(workouts_view) + return HttpResponseRedirect(url) + + workouts = [] + r = getrower(request.user) + stids = [int(getidfromuri(item['uri'])) for item in res.json()['items']] + knownstids = uniqify([ + w.uploadedtosporttracks for w in Workout.objects.filter(user=r) + ]) + newids = [stid for stid in stids if not stid in knownstids] + for item in res.json()['items']: + d = int(float(item['total_distance'])) + i = int(getidfromuri(item['uri'])) + if i in knownstids: + nnn = '' + else: + nnn = 'NEW' + n = item['name'] + ttot = str(datetime.timedelta(seconds=int(float(item['duration'])))) + s = item['start_time'] + r = item['type'] + keys = ['id','distance','duration','starttime','type','name','new'] + values = [i,d,ttot,s,r,n,nnn] + res = dict(zip(keys,values)) + workouts.append(res) + + r = getrower(request.user) + + breadcrumbs = [ + { + 'url':'/rowers/list-workouts/', + 'name':'Workouts' + }, + { + 'url':reverse(workout_sporttracksimport_view), + 'name':'SportTracks' + }, + ] + + return render(request,'sporttracks_list_import.html', + {'workouts':workouts, + 'breadcrumbs':breadcrumbs, + 'active':'nav-workouts', + 'rower':r, + 'teams':get_my_teams(request.user), + }) + + return HttpResponse(res) + +# List of workouts on Concept2 logbook. This view only used for debugging +@login_required() +def c2listdebug_view(request,page=1,message=""): + try: + thetoken = c2_open(request.user) + except NoTokenError: + return HttpResponseRedirect("/rowers/me/c2authorize/") + + r = getrower(request.user) + + res = c2stuff.get_c2_workout_list(request.user,page=page) + + if (res.status_code != 200): + message = "Something went wrong in workout_c2import_view (C2 token renewal)" + messages.error(request,message) + if settings.DEBUG: + return HttpResponse(res) + else: + url = reverse(workouts_view) + return HttpResponseRedirect(url) + else: + workouts = [] + + for item in res.json()['data']: + d = item['distance'] + i = item['id'] + ttot = item['time_formatted'] + s = item['date'] + r = item['type'] + s2 = item['source'] + c = item['comments'] + keys = ['id','distance','duration','starttime','rowtype','source','comment'] + values = [i,d,ttot,s,r,s2,c] + res = dict(zip(keys,values)) + workouts.append(res) + + + return render(request, + 'c2_list_import2.html', + {'workouts':workouts, + 'teams':get_my_teams(request.user), + }) + +# Import all unknown workouts available on Concept2 logbook +@login_required() +def workout_getc2workout_all(request,page=1,message=""): + try: + thetoken = c2_open(request.user) + except NoTokenError: + return HttpResponseRedirect("/rowers/me/c2authorize/") + + res = c2stuff.get_c2_workout_list(request.user,page=page) + + if (res.status_code != 200): + message = "Something went wrong in workout_c2import_view (C2 token refresh)" + messages.error(request,message) + else: + r = getrower(request.user) + c2ids = [item['id'] for item in res.json()['data']] + alldata = {} + for item in res.json()['data']: + alldata[item['id']] = item + + knownc2ids = uniqify([ + w.uploadedtoc2 for w in Workout.objects.filter(user=r) + ]) + newids = [c2id for c2id in c2ids if not c2id in knownc2ids] + + for c2id in newids: + workoutid = c2stuff.create_async_workout(alldata, + request.user,c2id) + + url = reverse(workouts_view) + return HttpResponseRedirect(url) + + +# List of workouts available on Concept2 logbook - for import +@login_required() +def workout_c2import_view(request,page=1,userid=0,message=""): + + r = getrequestrower(request,userid=userid) + + if r.user != request.user: + messages.info(request,"You cannot import other people's workouts from Concept2") + + r = getrower(request.user) + + try: + thetoken = c2_open(request.user) + except NoTokenError: + return HttpResponseRedirect("/rowers/me/c2authorize/") + + res = c2stuff.get_c2_workout_list(request.user,page=page) + + if (res.status_code != 200): + message = "Something went wrong in workout_c2import_view (C2 token refresh)" + messages.error(request,message) + url = reverse(workouts_view) + return HttpResponseRedirect(url) + + workouts = [] + c2ids = [item['id'] for item in res.json()['data']] + knownc2ids = uniqify([ + w.uploadedtoc2 for w in Workout.objects.filter(user=r) + ]) + newids = [c2id for c2id in c2ids if not c2id in knownc2ids] + for item in res.json()['data']: + d = item['distance'] + i = item['id'] + ttot = item['time_formatted'] + s = item['date'] + r = item['type'] + s2 = item['source'] + c = item['comments'] + if i in knownc2ids: + nnn = '' + else: + nnn = 'NEW' + keys = ['id','distance','duration','starttime','rowtype','source','comment','new'] + values = [i,d,ttot,s,r,s2,c,nnn] + res = dict(zip(keys,values)) + workouts.append(res) + + + breadcrumbs = [ + { + 'url':'/rowers/list-workouts/', + 'name':'Workouts' + }, + { + 'url':reverse(workout_c2import_view), + 'name':'Concept2' + }, + { + 'url':reverse(workout_c2import_view,kwargs={'page':page}), + 'name':'Page '+str(page) + } + ] + + r = getrower(request.user) + + return render(request, + 'c2_list_import2.html', + {'workouts':workouts, + 'rower':r, + 'active':'nav-workouts', + 'breadcrumbs':breadcrumbs, + 'teams':get_my_teams(request.user), + 'page':page, + }) + +importsources = { + 'c2':c2stuff, + 'strava':stravastuff, + 'polar':polarstuff, + 'ownapi':ownapistuff, + 'runkeeper':runkeeperstuff, + 'sporttracks':sporttracksstuff, + 'trainingpeaks':tpstuff, + 'underarmour':underarmourstuff + } + +@login_required() +def workout_getimportview(request,externalid,source = 'c2'): + res = importsources[source].get_workout(request.user,externalid) + if not res[0]: + messages.error(request,res[1]) + url = reverse(workouts_view) + + return HttpResponseRedirect(url) + + + strokedata = res[1] + data = res[0] + + + # Now works only for C2 + try: + if strokedata == 0: + messages.error(request,'An error occurred importing the workout from Concept2') + url = reverse(workouts_view) + return HttpResponseRedirect(url) + except ValueError: + pass + + if strokedata.empty: + distance = data['distance'] + c2id = data['id'] + workouttype = mytypes.c2mappinginv[data['type']] + verified = data['verified'] + startdatetime = iso8601.parse_date(data['date']) + weightclass = data['weight_class'] + weightcategory = 'hwt' + if weightclass == "L": + weightcategory = 'lwt' + totaltime = data['time']/10. + duration = dataprep.totaltime_sec_to_string(totaltime) + duration = datetime.datetime.strptime(duration,'%H:%M:%S.%f').time() + + try: + timezone_str = data['timezone'] + except: + timezone_str = 'UTC' + + if timezone_str is None: + timezone_str = 'UTC' + + workoutdate = startdatetime.astimezone( + pytz.timezone(timezone_str) + ).strftime('%Y-%m-%d') + starttime = startdatetime.astimezone( + pytz.timezone(timezone_str) + ).strftime('%H:%M:%S') + + r = getrower(request.user) + + id, message = dataprep.create_row_df(r, + distance, + duration, + startdatetime, + workouttype=workouttype) + + w = Workout.objects.get(id=id) + w.uploadedtoc2 = c2id + w.name = 'Imported from C2' + w.workouttype = workouttype + w.save() + + message = "This workout does not have any stroke data associated with it. We created synthetic stroke data." + messages.info(request,message) + url = reverse(r.defaultlandingpage, + kwargs = { + 'id':int(id), + }) + + return HttpResponseRedirect(url) + + # strokedata not empty - continue + id,message = importsources[source].add_workout_from_data( + request.user, + externalid,data, + strokedata, + source=source, + workoutsource=source) + + w = get_workout(id) + + if 'workout' in data: + if 'splits' in data['workout']: + splitdata = data['workout']['splits'] + elif 'intervals' in data['workout']: + splitdata = data['workout']['intervals'] + else: + splitdata = False + else: + splitdata = False + + # splitdata (only for C2) + if splitdata: + w.summary,sa,results = c2stuff.summaryfromsplitdata(splitdata,data,w.csvfilename) + w.save() + + from rowingdata.trainingparser import getlist + # set stroke data in CSV file + if sa: + values = getlist(sa) + units = getlist(sa,sel='unit') + types = getlist(sa,sel='type') + + rowdata = rdata(w.csvfilename) + if rowdata: + rowdata.updateintervaldata(values, + units,types,results) + + rowdata.write_csv(w.csvfilename,gzip=True) + dataprep.update_strokedata(w.id,rowdata.df) + + + + if source == 'strava': + w.uploadedtostrava = externalid + elif source == 'c2': + w.uploadedtoc2 = externalid + elif source == 'polar': + w.uploadedtopolar = externalid + elif source == 'runkeeper': + w.uploadedtorunkeeper = externalid + elif source == 'sporttracks': + w.uploadedtosporttracks = externalid + elif source == 'trainingpeaks': + w.uploadedtotp = externalid + elif source == 'underarmour': + w.uploadedtounderarmour = externalid + + w.save() + + if message: + messages.error(request,message) + + r = getrower(request.user) + + url = reverse(r.defaultlandingpage, + kwargs = { + 'id':int(id) + }) + + return HttpResponseRedirect(url) + + + + + +# Imports all new workouts from SportTracks +@login_required() +def workout_getsporttracksworkout_all(request): + res = sporttracksstuff.get_sporttracks_workout_list(request.user) + if (res.status_code == 200): + r = getrower(request.user) + stids = [int(getidfromuri(item['uri'])) for item in res.json()['items']] + knownstids = uniqify([ + w.uploadedtosporttracks for w in Workout.objects.filter(user=r) + ]) + newids = [stid for stid in stids if not stid in knownstids] + for sporttracksid in newids: + res = sporttracksstuff.get_sporttracks_workout( + request.user,sporttracksid) + data = res.json() + + id,message = add_workout_from_stdata( + request.user,sporttracksid,data + ) + if id==0: + messages.error(request,message) + + else: + w = Workout.objects.get(id=id) + w.uploadedtosporttracks=sporttracksid + w.save() + + url = reverse(workouts_view) + return HttpResponseRedirect(url) + + +# Imports all new workouts from SportTracks +@login_required() +def workout_getstravaworkout_all(request): + r = getrower(request.user) + res = stravastuff.get_strava_workouts(r) + if res == 1: + messages.info(request,"Your workouts are being imported and should appear on the site in the next 15 minutes") + else: + messages.error(request,"Couldn't import Strava workouts ") + + url = reverse(workouts_view) + return HttpResponseRedirect(url) + + +# Imports all new workouts from SportTracks +@login_required() +def workout_getstravaworkout_next(request): + + r = Rower.objects.get(user=request.user) + + res = stravastuff.get_strava_workout_list(r.user) + + if (res.status_code != 200): + return 0 + else: + stravaids = [int(item['id']) for item in res.json()] + + alldata = {} + for item in res.json(): + alldata[item['id']] = item + + knownstravaids = uniqify([ + w.uploadedtostrava for w in Workout.objects.filter(user=r) + ]) + newids = [stravaid for stravaid in stravaids if not stravaid in knownstravaids] + + theid = newids[0] + + workoutid = stravastuff.create_async_workout(alldata,r.user,stravaid,debug=True) + + + + url = reverse(workouts_view) + return HttpResponseRedirect(url) + + diff --git a/rowers/views/otherviews.py b/rowers/views/otherviews.py new file mode 100644 index 00000000..1b1764d6 --- /dev/null +++ b/rowers/views/otherviews.py @@ -0,0 +1,142 @@ +from statements import * + + +@login_required() +def deactivate_user(request): + pk = request.user.id + user = User.objects.get(pk=pk) + user_form = DeactivateUserForm(instance=user) + if request.user.is_authenticated() and request.user.id == user.id: + if request.method == "POST": + user_form = DeactivateUserForm(request.POST, instance=user) + if user_form.is_valid(): + if not user_form.cleaned_data['is_active']: + r = Rower.objects.get(user=user) + if r.paidplan is not None and r.paidplan.paymentprocessor == 'braintree': + try: + subscriptions = braintreestuff.find_subscriptions(r) + for subscription in subscriptions: + success, themessages,errormessages = braintreestuff.cancel_subscription(r,id) + for message in themessages: + messages.info(request,message) + except ProcessorCustomerError: + pass + + r.paidplan = None + r.teamplanexpires = timezone.now() + r.planexpires = timezone.now() + r.clubsize = 0 + r.rowerplan = 'basic' + r.save() + + deactivate_user = user_form.save(commit=False) + user.is_active = False + user.save() + deactivate_user.save() + # url = reverse(auth_views.logout_then_login) + url = '/logout/?next=/login' + return HttpResponseRedirect(url) + + return render(request, "userprofile_deactivate.html", { + "user_form": user_form, + }) + else: + raise PermissionDenied + +@login_required() +def user_gdpr_optin(request): + r = getrower(request.user) + r.gdproptin = False + r.gdproptindate = None + r.save() + nexturl = request.GET.get('next','/rowers/list-workouts/') + if r.gdproptin: + return HttpResponseRedirect(nexturl) + + return render(request,'gdpr_optin.html',{ + "next": nexturl + }) + +@login_required() +def user_gdpr_confirm(request): + r = getrower(request.user) + r.gdproptin = True + r.gdproptindate = timezone.now() + r.save() + + nexturl = request.GET.get('next','/rowers/list-workouts/') + + return HttpResponseRedirect(nexturl) + + + +@login_required() +def remove_user(request): + pk = request.user.id + user = User.objects.get(pk=pk) + user_form = DeleteUserForm(instance=user) + if request.user.is_authenticated() and request.user.id == user.id: + if request.method == "POST": + user_form = DeleteUserForm(request.POST,instance=user) + if user_form.is_valid(): + cd = user_form.cleaned_data + name = user.first_name+' '+user.last_name + email = user.email + + r = Rower.objects.get(user=user) + if r.paidplan is not None and r.paidplan.paymentprocessor == 'braintree': + try: + subscriptions = braintreestuff.find_subscriptions(r) + for subscription in subscriptions: + success, themessages,errormessages = braintreestuff.cancel_subscription(r,id) + for message in themessages: + messages.info(request,message) + except ProcessorCustomerError: + pass + + if cd['delete_user']: + user.delete() + res = myqueue(queuehigh, + handle_sendemail_userdeleted, + name, email) + + url = '/logout/?next=/login' +# url = reverse(auth_views.logout_then_login) + return HttpResponseRedirect(url) + return render(request, "userprofile_delete.html", { + "user_form": user_form, + }) + else: + raise PermissionDenied + + + +# Shows analysis page +@login_required() +def analysis_view(request,userid=0): + r = getrequestrower(request,userid=userid) + return render(request, + "analysis.html", + { + 'active':'nav-analysis', + 'rower':r, + } + ) + +# Shows laboratory page +@login_required() +def laboratory_view(request,userid=0): + r = getrequestrower(request,userid=userid) + return render(request, + "laboratory.html", + { + 'active':'nav-analysis', + 'rower':r, + } + ) + + + + + + diff --git a/rowers/views/paymentviews.py b/rowers/views/paymentviews.py new file mode 100644 index 00000000..1d422931 --- /dev/null +++ b/rowers/views/paymentviews.py @@ -0,0 +1,559 @@ + +def paidplans_view(request): + if not request.user.is_anonymous(): + r = getrequestrower(request) + if r.paymentprocessor != 'braintree' and r.paymenttype == 'recurring': + messages.error(request,'Automated payment processing is currently only available through BrainTree (by PayPal). You are currently on a recurring payment plan with PayPal. Contact the site administrator at support@rowsandall.com before you proceed') + else: + r = None + + + + return render(request, + 'paidplans.html', + {'rower':r}) + +@login_required() +def billing_view(request): + if not PAYMENT_PROCESSING_ON: + url = reverse('promembership') + return HttpResponseRedirect(url) + + r = getrequestrower(request) + + if r.paymentprocessor != 'braintree' and r.paymenttype == 'recurring': + messages.error(request,'Automated payment processing is currently only available through BrainTree (by PayPal). You are currently on a recurring payment plan with PayPal. Contact the site administrator at support@rowsandall.com before you proceed') + + if payments.is_existing_customer(r): + url = reverse(upgrade_view) + return HttpResponseRedirect(url) + + if request.method == 'POST': + billingaddressform = RowerBillingAddressForm(request.POST) + planselectform = PlanSelectForm(request.POST,paymentprocessor='braintree') + if billingaddressform.is_valid(): + cd = billingaddressform.cleaned_data + for attr, value in cd.items(): + setattr(r, attr, value) + r.save() + + if billingaddressform.is_valid(): + if planselectform.is_valid(): + plan = planselectform.cleaned_data['plan'] + try: + customer_id = braintreestuff.create_customer(r) + except ProcessorCustomerError: + messages.error(request,"Something went wrong registering you as a customer.") + url = reverse(billing_view) + return HttpResponseRedirect(url) + url = reverse(payment_confirm_view, + kwargs={ + 'planid':plan.id + }) + return HttpResponseRedirect(url) + + + else: + billingaddressform = RowerBillingAddressForm(instance=r) + planselectform = PlanSelectForm(paymentprocessor='braintree') + + return render(request, + 'billing.html', + {'rower':r, + 'billingaddressform':billingaddressform, + 'planselectform':planselectform, + }) + +@login_required() +def upgrade_view(request): + if not PAYMENT_PROCESSING_ON: + url = reverse('promembership') + return HttpResponseRedirect(url) + + r = getrequestrower(request) + + if r.paymentprocessor != 'braintree' and r.paymenttype == 'recurring': + messages.error(request,'Automated payment processing is currently only available through BrainTree (by PayPal). You are currently on a recurring payment plan with PayPal. Contact the site administrator at support@rowsandall.com before you proceed') + + if r.subscription_id is None or r.subscription_id == '': + url = reverse(billing_view) + return HttpResponseRedirect(url) + + if request.method == 'POST': + billingaddressform = RowerBillingAddressForm(request.POST) + planselectform = PlanSelectForm(request.POST,paymentprocessor='braintree') + if billingaddressform.is_valid(): + cd = billingaddressform.cleaned_data + for attr, value in cd.items(): + setattr(r, attr, value) + r.save() + + if planselectform.is_valid(): + plan = planselectform.cleaned_data['plan'] + if billingaddressform.is_valid(): + url = reverse(upgrade_confirm_view, + kwargs={ + 'planid':plan.id + }) + return HttpResponseRedirect(url) + + else: + billingaddressform = RowerBillingAddressForm(instance=r) + planselectform = PlanSelectForm(paymentprocessor='braintree', + rower=r) + + return render(request, + 'upgrade.html', + {'rower':r, + 'billingaddressform':billingaddressform, + 'planselectform':planselectform, + }) + +@login_required() +def downgrade_view(request): + if not PAYMENT_PROCESSING_ON: + url = reverse('promembership') + return HttpResponseRedirect(url) + + r = getrequestrower(request) + + if r.paymentprocessor != 'braintree' and r.paymenttype == 'recurring': + messages.error(request,'Automated payment processing is currently only available through BrainTree (by PayPal). You are currently on a recurring payment plan with PayPal. Contact the site administrator at support@rowsandall.com before you proceed') + + if r.subscription_id is None or r.subscription_id == '': + url = reverse(billing_view) + return HttpResponseRedirect(url) + + if request.method == 'POST': + billingaddressform = RowerBillingAddressForm(request.POST) + planselectform = PlanSelectForm(request.POST,paymentprocessor='braintree') + if billingaddressform.is_valid(): + cd = billingaddressform.cleaned_data + for attr, value in cd.items(): + setattr(r, attr, value) + r.save() + + if planselectform.is_valid(): + plan = planselectform.cleaned_data['plan'] + + if plan.price > r.paidplan.price: + nextview = upgrade_confirm_view + elif plan.price == r.paidplan.price: + messages.info(request,'You did not select a new plan') + url = reverse(downgrade_view) + return HttpResponseRedirect(url) + else: + nextview = downgrade_confirm_view + + if billingaddressform.is_valid(): + url = reverse(nextview, + kwargs={ + 'planid':plan.id + }) + return HttpResponseRedirect(url) + + else: + billingaddressform = RowerBillingAddressForm(instance=r) + planselectform = PlanSelectForm(paymentprocessor='braintree', + rower=r,includeall=True, initial={'plan':r.paidplan}) + + return render(request, + 'downgrade.html', + {'rower':r, + 'billingaddressform':billingaddressform, + 'planselectform':planselectform, + }) + +@login_required() +def plan_stop_view(request): + if not PAYMENT_PROCESSING_ON: + url = reverse('promembership') + return HttpResponseRedirect(url) + + r = getrequestrower(request) + + subscriptions = [] + + if r.paymentprocessor != 'braintree' and r.paymenttype == 'recurring': + messages.error(request,'Automated payment processing is currently only available through BrainTree (by PayPal). You are currently on a recurring payment plan with PayPal. Contact the site administrator at support@rowsandall.com before you proceed') + + if r.paidplan is not None and r.paidplan.paymentprocessor == 'braintree': + try: + subscriptions = braintreestuff.find_subscriptions(r) + except ProcessorCustomerError: + r.paymentprocessor = None + r.save() + + + + return render(request, + 'subscriptions_cancel.html', + {'rower':r, + 'subscriptions':subscriptions + }) + +@login_required() +def plan_tobasic_view(request,id=0): + if not PAYMENT_PROCESSING_ON: + url = reverse('promembership') + return HttpResponseRedirect(url) + + r = getrequestrower(request) + + if r.paidplan.paymentprocessor == 'braintree': + success, themessages,errormessages = braintreestuff.cancel_subscription(r,id) + for message in themessages: + messages.info(request,message) + + for message in errormessages: + messages.error(request,message) + + url = reverse(plan_stop_view) + + return HttpResponseRedirect(url) + + + +@login_required() +def upgrade_confirm_view(request,planid = 0): + if not PAYMENT_PROCESSING_ON: + url = reverse('promembership') + return HttpResponseRedirect(url) + + try: + plan = PaidPlan.objects.get(id=planid) + except PaidPlan.DoesNotExist: + messages.error(request,"Something went wrong. Please try again.") + url = reverse(billing_view) + return HttpResponseRedirect(url) + + r = getrequestrower(request) + + if r.paymentprocessor != 'braintree' and r.paymenttype == 'recurring': + messages.error(request,'Automated payment processing is currently only available through BrainTree (by PayPal). You are currently on a recurring payment plan with PayPal. Contact the site administrator at support@rowsandall.com before you proceed') + + client_token = braintreestuff.get_client_token(r) + + return render(request, + "upgradeconfirm.html", + { + 'plan':plan, + 'client_token':client_token, + 'rower':r, + }) + +@login_required() +def downgrade_confirm_view(request,planid = 0): + if not PAYMENT_PROCESSING_ON: + url = reverse('promembership') + return HttpResponseRedirect(url) + + try: + plan = PaidPlan.objects.get(id=planid) + except PaidPlan.DoesNotExist: + messages.error(request,"Something went wrong. Please try again.") + url = reverse(billing_view) + return HttpResponseRedirect(url) + + r = getrequestrower(request) + + client_token = braintreestuff.get_client_token(r) + + return render(request, + "downgradeconfirm.html", + { + 'plan':plan, + 'client_token':client_token, + 'rower':r, + }) + + +@login_required() +def payment_confirm_view(request,planid = 0): + if not PAYMENT_PROCESSING_ON: + url = reverse('promembership') + return HttpResponseRedirect(url) + + try: + plan = PaidPlan.objects.get(id=planid) + except PaidPlan.DoesNotExist: + messages.error(request,"Something went wrong. Please try again.") + url = reverse(billing_view) + return HttpResponseRedirect(url) + + r = getrequestrower(request) + + if r.paymentprocessor != 'braintree' and r.paymenttype == 'recurring': + messages.error(request,'Automated payment processing is currently only available through BrainTree (by PayPal). You are currently on a recurring payment plan with PayPal. Contact the site administrator at support@rowsandall.com before you proceed') + + client_token = braintreestuff.get_client_token(r) + + return render(request, + "paymentconfirm.html", + { + 'plan':plan, + 'client_token':client_token, + 'rower':r, + }) + + +@login_required() +def checkouts_view(request): + if not PAYMENT_PROCESSING_ON: + url = reverse('promembership') + return HttpResponseRedirect(url) + + + r = getrequestrower(request) + + if r.paymentprocessor != 'braintree' and r.paymenttype == 'recurring': + messages.error(request,'Automated payment processing is currently only available through BrainTree (by PayPal). You are currently on a recurring payment plan with PayPal. Contact the site administrator at support@rowsandall.com before you proceed') + + if request.method != 'POST': + url = reverse(paidplans_view) + return HttpResponseRedirect(url) + + form = BillingForm(request.POST) + if form.is_valid(): + data = form.cleaned_data + success,amount = braintreestuff.create_subscription(r,data) + if success: + messages.info(request,"Your payment has succeeded and your plan has been updated") + url = "{baseurl}?amount={amount:.2f}".format( + baseurl = reverse(payment_completed_view), + amount = amount) + return HttpResponseRedirect(url) + else: + messages.error(request,"There was a problem with your payment") + url = reverse(billing_view) + return HttpResponseRedirect(url) + + else: + messages.error(request,"There was an error in the payment form") + url = reverse(billing_view) + return HttpResponseRedirect(url) + + url = reverse(paidplans_view) + return HttpResponseRedirect(url) + +@login_required() +def upgrade_checkouts_view(request): + if not PAYMENT_PROCESSING_ON: + url = reverse('promembership') + return HttpResponseRedirect(url) + + + r = getrequestrower(request) + + if request.method != 'POST': + url = reverse(paidplans_view) + return HttpResponseRedirect(url) + + form = BillingForm(request.POST) + if form.is_valid(): + data = form.cleaned_data + success,amount = braintreestuff.update_subscription(r,data) + if success: + messages.info(request,"Your payment has succeeded and your plan has been updated") + url = "{baseurl}?amount={amount:.2f}".format( + baseurl = reverse(payment_completed_view), + amount = amount) + return HttpResponseRedirect(url) + else: + messages.error(request,"There was a problem with your payment") + url = reverse(upgrade_view) + return HttpResponseRedirect(url) + + else: + messages.error(request,"There was an error in the payment form") + url = reverse(upgrade_view) + return HttpResponseRedirect(url) + + url = reverse(paidplans_view) + return HttpResponseRedirect(url) + +@login_required() +def downgrade_checkouts_view(request): + if not PAYMENT_PROCESSING_ON: + url = reverse('promembership') + return HttpResponseRedirect(url) + + + r = getrequestrower(request) + + if request.method != 'POST': + url = reverse(paidplans_view) + return HttpResponseRedirect(url) + + form = BillingForm(request.POST) + if form.is_valid(): + data = form.cleaned_data + success = braintreestuff.update_subscription(r,data,method='down') + if success: + messages.info(request,"Your plan has been updated") + url = reverse(downgrade_completed_view) + return HttpResponseRedirect(url) + else: + messages.error(request,"There was a problem with your transaction") + url = reverse(upgrade_view) + return HttpResponseRedirect(url) + + else: + messages.error(request,"There was an error in the payment form") + url = reverse(upgrade_view) + return HttpResponseRedirect(url) + + url = reverse(paidplans_view) + return HttpResponseRedirect(url) + + +@login_required() +def payment_completed_view(request): + if not PAYMENT_PROCESSING_ON: + url = reverse('promembership') + return HttpResponseRedirect(url) + + amount = request.GET.get('amount',0) + + + r = getrequestrower(request) + + return render(request, + "payment_completed.html", + { + 'rower':r, + 'amount':amount, + }) + +@login_required() +def downgrade_completed_view(request): + if not PAYMENT_PROCESSING_ON: + url = reverse('promembership') + return HttpResponseRedirect(url) + + r = getrequestrower(request) + + return render(request, + "downgrade_completed.html", + { + 'rower':r + }) + +# User registration +def rower_register_view(request): + + nextpage = request.GET.get('next','/rowers/list-workouts/') + if nextpage == '': + nextpage = '/rowers/list-workouts/' + + if request.method == 'POST': + #form = RegistrationFormUniqueEmail(request.POST) + form = RegistrationFormSex(request.POST) + if form.is_valid(): + first_name = form.cleaned_data['first_name'] + last_name = form.cleaned_data['last_name'] + email = form.cleaned_data['email'] + password = form.cleaned_data['password1'] + username = form.cleaned_data['username'] + sex = form.cleaned_data['sex'] + birthdate = form.cleaned_data['birthdate'] + weightcategory = form.cleaned_data['weightcategory'] + adaptiveclass = form.cleaned_data['adaptiveclass'] + nextpage = request.POST['next'] + theuser = User.objects.create_user(username,password=password) + theuser.first_name = first_name + theuser.last_name = last_name + theuser.email = email + theuser.save() + + birthdate = birthdate.replace(tzinfo=None) + + therower = Rower(user=theuser,sex=sex,birthdate=birthdate, + weightcategory=weightcategory, + adaptiveclass=adaptiveclass) + + therower.save() + + # create default favorite charts + add_defaultfavorites(therower) + + # Create Sample workout + f = 'media/testdata.csv.gz' + timestr = strftime("%Y%m%d-%H%M%S") + f2 = f[:-7]+timestr+'.csv.gz' + copyfile(f,f2) + + response = dataprep.new_workout_from_file(therower,f2, + title='New User Sample Data', + notes='This is an example workout to get you started') + newworkoutid = response[0] + w = Workout.objects.get(id=newworkoutid) + w.startdatetime = timezone.now() + w.save() + + # Create and send email + fullemail = first_name + " " + last_name + " " + "<" + email + ">" + subject = "Thank you for registering on rowsandall.com" + from_address = 'Sander Roosendaal ' + + d = {'first_name':theuser.first_name} + + send_template_email(from_address,[fullemail], + subject,'registeremail.html',d) + + + subject2 = "New User" + message2 = "New user registered.\n" + message2 += fullemail + "\n" + message2 += "User name: "+username + + send_mail(subject2, message2, + 'Rowsandall Server ', + ['roosendaalsander@gmail.com']) + + theuser = authenticate(username=username,password=password) + login(request,theuser) + + return HttpResponseRedirect(nextpage) + # '/rowers/register/thankyou/') + + else: + return render(request, + "registration_form.html", + {'form':form, + 'next':nextpage,}) + else: + form = RegistrationFormSex() + return render(request, + "registration_form.html", + {'form':form, + 'next':nextpage,}) + +@login_required() +def transactions_view(request): + if not request.user.is_staff: + raise PermissionDenied("Not Allowed") + + if request.method == 'POST': + dateform = DateRangeForm(request.POST) + if dateform.is_valid(): + startdate = dateform.cleaned_data['startdate'] + enddate = dateform.cleaned_data['enddate'] + + df = braintreestuff.get_transactions(startdate,enddate) + filename="transactions_{s}_{e}.csv".format(s = startdate, e = enddate) + response = HttpResponse(df.to_csv()) + response['Content-Disposition'] = 'attachment; filename="%s"' % filename + response['Content-Type'] = 'application/octet-stream' + + return response + + else: + dateform = DateRangeForm() + + return render(request, + 'transactions.html', + { + 'dateform':dateform + }) + diff --git a/rowers/views/planviews.py b/rowers/views/planviews.py new file mode 100644 index 00000000..9ba2d6e8 --- /dev/null +++ b/rowers/views/planviews.py @@ -0,0 +1,2575 @@ +from statements import * + + +# Cloning sessions +@user_passes_test(hasplannedsessions,login_url="/rowers/paidplans/", + message="This functionality requires a Coach or Self-Coach plan", + redirect_field_name=None) +def plannedsession_multiclone_view( + request, + userid=0,): + + r = getrequestrower(request,userid=userid) + + + startdate,enddate = get_dates_timeperiod(request) + + + if request.method == 'POST' and 'daterange' in request.POST: + dateform = DateRangeForm(request.POST) + if dateform.is_valid(): + startdate = dateform.cleaned_data['startdate'] + enddate = dateform.cleaned_data['enddate'] + startdatestring = startdate.strftime('%Y-%m-%d') + enddatestring = enddate.strftime('%Y-%m-%d') + request.session['startdate'] = startdatestring + request.session['enddate'] = enddatestring + else: + dateform = DateRangeForm(initial={ + 'startdate':startdate, + 'enddate':enddate, + }) + + if request.method == 'POST' and 'plannedsessions' in request.POST: + form = PlannedSessionMultipleCloneForm(request.POST) + dateshiftform = SessionDateShiftForm(request.POST) + if form.is_valid() and dateshiftform.is_valid(): + cd = form.cleaned_data + sps = cd['plannedsessions'] + std = min([ps.startdate for ps in sps]) + shiftstartdate = dateshiftform.cleaned_data['shiftstartdate'] + delta = shiftstartdate-std + lastdate = shiftstartdate + for ps in sps: + rowers = ps.rower.all() + teams = ps.team.all() + ps.pk = None + ps.startdate += delta + ps.preferreddate += delta + ps.enddate += delta + if ps.enddate > lastdate: + lastdate = ps.enddate + ps.save() + for rower in rowers: + add_rower_session(rower,ps) + for team in teams: + add_team_session(team,ps) + + startdatestring = shiftstartdate.strftime('%Y-%m-%d') + enddatestring = lastdate.strftime('%Y-%m-%d') + + url = reverse(plannedsessions_view, + kwargs = { + 'userid':r.user.id, + }) + + + url+='?when='+startdatestring+'/'+enddatestring + + return HttpResponseRedirect(url) + + sps = PlannedSession.objects.filter( + manager=request.user, + rower__in=[r], + startdate__lte=enddate, + enddate__gte=startdate).order_by( + "startdate","preferreddate","enddate").exclude( + sessiontype='race') + + query = request.GET.get('q') + if query: + query_list = query.split() + sps = sps.filter( + reduce(operator.and_, + (Q(name__icontains=q) for q in query_list)) | + reduce(operator.and_, + (Q(comment__icontains=q) for q in query_list)) + ) + + form = PlannedSessionMultipleCloneForm() + form.fields["plannedsessions"].queryset = sps + + dateshiftform = SessionDateShiftForm() + + try: + trainingplan = TrainingPlan.objects.filter( + startdate__lte = startdate, + rowers = r, + enddate__gte = enddate)[0] + except IndexError: + trainingplan = None + + timeperiod = startdate.strftime('%Y-%m-%d')+'/'+enddate.strftime('%Y-%m-%d') + breadcrumbs = [ + { + 'url': reverse(plannedsessions_view), + 'name': 'Planned Sessions' + }, + { + 'url': reverse(plannedsession_multiclone_view), + 'name': 'Clone Multiple Sessions' + } + ] + + dateform = DateRangeForm(initial={ + 'startdate':startdate, + 'enddate':enddate, + }) + + return render(request, 'plannedsessions_multiclone_select.html', + {'plannedsessions':sps, + 'breadcrumbs':breadcrumbs, + 'plan':trainingplan, + 'dateform':dateform, + 'startdate':startdate, + 'enddate':enddate, + 'form':form, + 'dateshiftform':dateshiftform, + 'rower':r, + 'active':'nav-plan', + 'timeperiod':timeperiod, + } + ) + +# Individual user creates training for himself +@user_passes_test(hasplannedsessions,login_url="/rowers/paidplans/", + message="This functionality requires a Coach or Self-Coach plan", + redirect_field_name=None) +def plannedsession_create_view(request, + userid=0, + startdatestring='', + enddatestring=''): + + r = getrequestrower(request,userid=userid) + + + + + startdate,enddate = get_dates_timeperiod(request,startdatestring=startdatestring, + enddatestring=enddatestring) + + + + if request.method == 'POST': + sessioncreateform = PlannedSessionForm(request.POST) + if sessioncreateform.is_valid(): + cd = sessioncreateform.cleaned_data + startdate = cd['startdate'] + enddate = cd['enddate'] + preferreddate = cd['preferreddate'] + sessiontype = cd['sessiontype'] + sessionmode = cd['sessionmode'] + criterium = cd['criterium'] + sessionvalue = cd['sessionvalue'] + sessionunit = cd['sessionunit'] + comment = cd['comment'] + course = cd['course'] + name = cd['name'] + + if sessionunit == 'min': + sessionmode = 'time' + elif sessionunit in ['km','m']: + sessionmode = 'distance' + + ps = PlannedSession( + name=name, + startdate=startdate, + enddate=enddate, + preferreddate=preferreddate, + course=course, + sessiontype=sessiontype, + sessionmode=sessionmode, + sessionvalue=sessionvalue, + sessionunit=sessionunit, + comment=comment, + criterium=criterium, + manager=request.user) + + ps.save() + + add_rower_session(r,ps) + + + request.session['fstartdate'] = str(arrow.get(startdate)) + request.session['fenddate'] = str(arrow.get(enddate)) + request.session['fprefdate'] = str(arrow.get(preferreddate)) + + else: + if 'fstartdate' in request.session: + try: + fstartdate = arrow.get(request.session['fstartdate']).date() + except KeyError: + fstartdate = timezone.now().date() + if fstartdate < startdate: + fstartdate = startdate + try: + fenddate = arrow.get(request.session['fenddate']).date() + except KeyError: + fenddate = timezone.now().date() + if fenddate > enddate: + fenddate = enddate + try: + fprefdate = arrow.get(request.session['fprefdate']).date() + except KeyError: + fprefdate = timezone.now().date() + + if fprefdate < startdate: + fprefdate = startdate + + if fprefdate > enddate: + fprefdate = enddate + + + forminitial = { + 'startdate':fstartdate, + 'enddate':fenddate, + 'preferreddate':fprefdate + } + else: + preferreddate = startdate + if preferreddate < timezone.now().date(): + preferreddate = timezone.now().date() + + if preferreddate > enddate: + preferreddate = enddate + + forminitial = { + 'startdate':startdate, + 'enddate':enddate, + 'preferreddate':preferreddate, + } + + sessioncreateform = PlannedSessionForm(initial=forminitial) + + if request.GET.get('startdate') or request.GET.get('when'): + startdate, enddate = get_dates_timeperiod(request) + + sps = get_sessions(r,startdate=startdate,enddate=enddate).exclude( + sessiontype='race') + try: + trainingplan = TrainingPlan.objects.filter( + startdate__lte = startdate, + rowers = r, + enddate__gte = enddate)[0] + except IndexError: + trainingplan = None + + timeperiod = startdate.strftime('%Y-%m-%d')+'/'+enddate.strftime('%Y-%m-%d') + + dateform = DateRangeForm(initial={ + 'startdate':startdate, + 'enddate':enddate, + }) + + return render(request,'plannedsessioncreate.html', + { + 'teams':get_my_teams(request.user), + 'plan':trainingplan, + 'dateform':dateform, + 'form':sessioncreateform, + 'active':'nav-plan', + 'plannedsessions':sps, + 'rower':r, + 'timeperiod':timeperiod, + }) + +@user_passes_test(hasplannedsessions,login_url="/rowers/paidplans/", + message="This functionality requires a Coach or Self-Coach plan", + redirect_field_name=None) +def plannedsession_multicreate_view(request, + teamid=0,userid=0,extrasessions=0): + + extrasessions=int(extrasessions) + + r = getrequestrower(request,userid=userid) + + + startdate,enddate = get_dates_timeperiod(request) + try: + trainingplan = TrainingPlan.objects.filter( + startdate__lte = startdate, + rowers = r, + enddate__gte = enddate)[0] + except IndexError: + trainingplan = None + + m = Rower.objects.get(user=request.user) + + qset = PlannedSession.objects.filter( + rower__in=[r], + manager = request.user, + startdate__lte=enddate, + enddate__gte=startdate, + ).order_by("startdate","preferreddate","enddate").exclude( + sessiontype='race') + + + + initial = { + 'startdate':startdate, + 'enddate':enddate, + 'sessionvalue':60, + 'manager':request.user, + 'name': 'NEW SESSION' + } + + + + initials = [initial for i in range(extrasessions)] + + PlannedSessionFormSet = modelformset_factory( + PlannedSession, + form=PlannedSessionFormSmall, + can_delete=True, + extra=extrasessions, + ) + if request.method == "POST": + ps_formset = PlannedSessionFormSet(queryset = qset, + data = request.POST) + if ps_formset.is_valid(): + instances = ps_formset.save(commit=False) + for ps in instances: + ps.save() + add_rower_session(r,ps) + messages.info(request,"Saved changes for Planned Session "+str(ps)) + for obj in ps_formset.deleted_objects: + messages.info(request,"Deleted Planned Session "+str(obj)) + obj.delete() + else: + print ps_formset.errors + + url = reverse(plannedsession_multicreate_view, + kwargs = { + 'userid':r.user.id, + } + ) + + + startdatestring = startdate.strftime('%Y-%m-%d') + enddatestring = enddate.strftime('%Y-%m-%d') + url += '?when='+startdatestring+'/'+enddatestring + + return HttpResponseRedirect(url) + + ps_formset = PlannedSessionFormSet(queryset = qset, + initial=initials) + timeperiod = startdate.strftime('%Y-%m-%d')+'/'+enddate.strftime('%Y-%m-%d') + breadcrumbs = [ + { + 'url': reverse(plannedsessions_view), + 'name': 'Planned Sessions' + }, + { + 'url': reverse(plannedsession_multicreate_view), + 'name': 'Plan MicroCycle' + } + ] + + dateform = DateRangeForm(initial={ + 'startdate':startdate, + 'enddate':enddate + }) + + context = { + 'ps_formset':ps_formset, + 'breadcrumbs':breadcrumbs, + 'rower':r, + 'active':'nav-plan', + 'dateform':dateform, + 'plan':trainingplan, + 'timeperiod':timeperiod, + 'teams':get_my_teams(request.user), + 'extrasessions': extrasessions+1 + } + + + return render(request,'plannedsession_multicreate.html',context) + +# Manager creates sessions for entire team +@user_passes_test(iscoachmember,login_url="/rowers/paidplans/", + redirect_field_name=None) +def plannedsession_teamcreate_view(request, + teamid=0,userid=0): + + therower = getrequestrower(request,userid=userid) + + + + teams = Team.objects.filter(manager=request.user) + if len(teams)>0: + teamchoices = [(team.id, team.name) for team in teams] + teaminitial = [str(teams[0].id)] + else: + messages.info(request,"You have no teams established yet. We are redirecting you to the Team Management page.") + url = reverse(rower_teams_view) + return HttpResponseRedirect(url) + + startdate,enddate = get_dates_timeperiod(request) + + trainingplan = None + + sps = [] + for team in teams: + res = get_sessions_manager(request.user,startdate=startdate,enddate=enddate) + sps += res + + sps = list(set(sps)) + ids = [ps.id for ps in sps] + sps = PlannedSession.objects.filter(id__in=ids).order_by( + "preferreddate","startdate","enddate") + + if request.method == 'POST': + sessioncreateform = PlannedSessionForm(request.POST) + sessionteamselectform = PlannedSessionTeamForm( + request.user,request.POST + ) + + if sessioncreateform.is_valid() and sessionteamselectform.is_valid(): + cd = sessioncreateform.cleaned_data + startdate = cd['startdate'] + enddate = cd['enddate'] + preferreddate = cd['preferreddate'] + sessiontype = cd['sessiontype'] + sessionmode = cd['sessionmode'] + criterium = cd['criterium'] + sessionvalue = cd['sessionvalue'] + sessionunit = cd['sessionunit'] + comment = cd['comment'] + course = cd['course'] + name = cd['name'] + + if sessionunit == 'min': + sessionmode = 'time' + elif sessionunit in ['km','m']: + sessionmode = 'distance' + + ps = PlannedSession( + name=name, + startdate=startdate, + enddate=enddate, + preferreddate=preferreddate, + sessiontype=sessiontype, + sessionmode=sessionmode, + sessionvalue=sessionvalue, + sessionunit=sessionunit, + comment=comment, + criterium=criterium, + course=course, + manager=request.user) + + ps.save() + + cd = sessionteamselectform.cleaned_data + teams = cd['team'] + request.session['teams'] = [team.id for team in teams] + for team in teams: + add_team_session(team,ps) + rs = Rower.objects.filter(team__in=[team]) + for r in rs: + add_rower_session(r,ps) + + + url = reverse(plannedsession_teamcreate_view) + startdatestring = startdate.strftime('%Y-%m-%d') + enddatestring = enddate.strftime('%Y-%m-%d') + url += '?when='+startdatestring+'/'+enddatestring + + return HttpResponseRedirect(url) + else: + timeperiod = startdate.strftime('%Y-%m-%d')+'/'+enddate.strftime('%Y-%m-%d') + breadcrumbs = [ + { + 'url': reverse(plannedsessions_view), + 'name': 'Planned Sessions' + }, + { + 'url': reverse(plannedsession_teamcreate_view), + 'name': 'Add Team Session' + } + ] + + return render(request,'plannedsessionteamcreate.html', + { + 'teams':get_my_teams(request.user), + 'plan':trainingplan, + 'breadcrumbs':breadcrumbs, + 'form':sessioncreateform, + 'teamform':sessionteamselectform, + 'timeperiod':timeperiod, + 'plannedsessions':sps, + 'rower':therower, + 'active':'nav-plan' + }) + + else: + initial = { + 'startdate':startdate, + 'enddate':enddate, + 'preferreddate':startdate, + } + + if 'teams' in request.session: + teams = request.session['teams'] + theteams = Team.objects.filter(id__in=teams) + initialteam = { + 'team':theteams + } + else: + initialteam = {} + + sessioncreateform = PlannedSessionForm(initial=initial) + sessionteamselectform = PlannedSessionTeamForm( + request.user,initial=initialteam + ) + + timeperiod = startdate.strftime('%Y-%m-%d')+'/'+enddate.strftime('%Y-%m-%d') + breadcrumbs = [ + { + 'url': reverse(plannedsessions_view), + 'name': 'Planned Sessions' + }, + { + 'url': reverse(plannedsession_teamcreate_view), + 'name': 'Add Team Session' + } + ] + + dateform = DateRangeForm(initial={ + 'startdate':startdate, + 'enddate':enddate, + }) + + return render(request,'plannedsessionteamcreate.html', + { + 'teams':get_my_teams(request.user), + 'plan':trainingplan, + 'dateform':dateform, + 'breadcrumbs':breadcrumbs, + 'form':sessioncreateform, + 'teamform':sessionteamselectform, + 'timeperiod':timeperiod, + 'plannedsessions':sps, + 'rower':therower, + 'active':'nav-plan' + }) + +# Manager edits sessions for entire team +@user_passes_test(iscoachmember,login_url="/rowers/paidplans/", + redirect_field_name=None) +def plannedsession_teamedit_view(request, + sessionid=0,userid=0): + + r = getrequestrower(request,userid=userid) + + + try: + ps = PlannedSession.objects.get(id=sessionid) + except PlannedSession.DoesNotExist: + raise Http404("This session doesn't exist") + if not ps.manager == request.user: + raise PermissionDenied("You are not the manager of this session") + + teams = Team.objects.filter(manager=request.user) + teamchoices = [(team.id, team.name) for team in teams] + + teaminitial = ps.team.all() + + startdate,enddate = get_dates_timeperiod(request) + + try: + trainingplan = TrainingPlan.objects.filter( + startdate__lte = startdate, + rowers = r, + enddate__gte = enddate)[0] + except IndexError: + trainingplan = None + + sps = [] + rowers = [] + for team in teams: + res = get_sessions_manager(request.user,startdate=startdate,enddate=enddate) + sps += res + rowers += Rower.objects.filter(team__in=[team]) + + rowers = list(set(rowers)) + + + sps = list(set(sps)) + ids = [pps.id for pps in sps] + sps = PlannedSession.objects.filter(id__in=ids).order_by( + "preferreddate","startdate","enddate") + + if request.method == 'POST': + sessioncreateform = PlannedSessionForm(request.POST,instance=ps) + sessionteamselectform = PlannedSessionTeamForm( + request.user,request.POST + ) + sessionrowerform = PlannedSessionTeamMemberForm(ps,request.POST) + if sessioncreateform.is_valid(): + cd = sessioncreateform.cleaned_data + + if cd['sessionunit'] == 'min': + cd['sessionmode'] = 'time' + elif cd['sessionunit'] in ['km','m']: + cd['sessionmode'] = 'distance' + + + res,message = update_plannedsession(ps,cd) + + if res: + messages.info(request,message) + else: + messages.error(request,message) + + + # some logic when to add all selected rowers + if sessionteamselectform.is_valid(): + cd = sessionteamselectform.cleaned_data + selectedteams = cd['team'] + for team in teams: + if team in selectedteams: + add_team_session(team,ps) + rs = Rower.objects.filter(team__in=[team]) + for r in rs: + add_rower_session(r,ps) + else: + remove_team_session(team,ps) + else: + selectedteams = [] + for team in teams: + remove_team_session(team,ps) + + + if sessionrowerform.is_valid(): + cd = sessionrowerform.cleaned_data + selectedrowers = cd['members'] + for r in rowers: + if r in selectedrowers: + add_rower_session(r,ps) + else: + remove_rower_session(r,ps) + for t in selectedteams: + if t in r.team.all(): + add_rower_session(r,ps) + + + url = reverse(plannedsession_teamedit_view, + kwargs = { + 'sessionid':sessionid, + }) + + startdatestring = startdate.strftime('%Y-%m-%d') + enddatestring = enddate.strftime('%Y-%m-%d') + url += '?when='+startdatestring+'/'+enddatestring + + + return HttpResponseRedirect(url) + else: + sessioncreateform = PlannedSessionForm(instance=ps) + sessionteamselectform = PlannedSessionTeamForm( + request.user + ) + sessionteamselectform.fields['team'].initial = teaminitial + sessionrowerform = PlannedSessionTeamMemberForm( + ps + ) + + + sessionrowerform.fields['members'].initial = ps.rower.all() + + timeperiod = startdate.strftime('%Y-%m-%d')+'/'+enddate.strftime('%Y-%m-%d') + breadcrumbs = [ + { + 'url': reverse(plannedsessions_view), + 'name': 'Planned Sessions' + }, + { + 'url': reverse(plannedsession_teamcreate_view), + 'name': 'Add Team Session' + } + ] + + dateform = DateRangeForm(initial={ + 'startdate':startdate, + 'enddate':enddate, + }) + + return render(request,'plannedsessionteamedit.html', + { + 'plannedsession':ps, + 'plan':trainingplan, + 'dateform':dateform, + 'breadcrumbs':breadcrumbs, + 'rower':r, + 'active':'nav-plan', + 'teams':get_my_teams(request.user), + 'form':sessioncreateform, + 'teamform':sessionteamselectform, + 'rowersform':sessionrowerform, + 'timeperiod':timeperiod, + 'plannedsessions':sps, + }) + +#@user_passes_test(iscoachmember,login_url="/rowers/paidplans/", +# redirect_field_name=None) +@login_required() +def plannedsessions_coach_view(request, + teamid=0,userid=0): + + + therower = getrower(request.user) + + + startdate,enddate = get_dates_timeperiod(request) + + + trainingplan = None + + if teamid != 0: + try: + theteam = Team.objects.get(id=teamid) + except Team.DoesNotExist: + theteam = False + else: + theteam = False + + if request.user.rower.rowerplan == 'coach': + sps = get_sessions_manager(request.user,teamid=teamid, + enddate=enddate, + startdate=startdate) + else: + rteams = therower.team.filter(viewing='allmembers') + sps = get_sessions(therower,startdate=startdate,enddate=enddate) + + rowers = [therower] + for ps in sps: + if request.user.rower.rowerplan == 'coach': + rowers += ps.rower.all() + else: + rowers += ps.rower.filter(team__in=rteams) + + rowers = list(set(rowers)) + + statusdict = [] + + for ps in sps: + rowerstatus = {} + rowercolor = {} + for r in rowers: + ratio, status,completiondate = is_session_complete(r,ps) + rowerstatus[r.id] = status + rowercolor[r.id] = cratiocolors[status] + sessiondict = { + 'id': ps.id, + 'results':rowerstatus, + 'name': ps.name, + 'startdate': ps.startdate, + 'color': rowercolor, + 'preferreddate': ps.preferreddate, + 'enddate': ps.enddate, + } + statusdict.append(sessiondict) + + unmatchedworkouts = [] + for r in rowers: + unmatchedworkouts += Workout.objects.filter( + user=r, + plannedsession=None, + date__gte=startdate,date__lte=enddate) + + + myteams = Team.objects.filter(manager=request.user) + + timeperiod = startdate.strftime('%Y-%m-%d')+'/'+enddate.strftime('%Y-%m-%d') + breadcrumbs = [ + { + 'url': reverse(plannedsessions_view), + 'name': 'Planned Sessions' + }, + { + 'url': reverse(plannedsessions_coach_view), + 'name': 'Coach View' + } + ] + + dateform = DateRangeForm(initial={ + 'startdate':startdate, + 'enddate':enddate, + }) + + return render(request,'plannedsessionscoach.html', + { + 'myteams':myteams, + 'plannedsessions':sps, + 'breadcrumbs':breadcrumbs, + 'plan':trainingplan, + 'statusdict':statusdict, + 'dateform':dateform, + 'timeperiod':timeperiod, + 'rowers':rowers, + 'rower':therower, + 'active':'nav-plan', + 'theteam':theteam, + 'unmatchedworkouts':unmatchedworkouts, + 'rower':getrower(request.user) + } + ) + +from rowers.plannedsessions import cratiocolors + +@login_required() +def plannedsessions_view(request, + userid=0,startdatestring='',enddatestring=''): + + r = getrequestrower(request,userid=userid) + + if startdatestring: + try: + startdate = iso8601.parse_date(startdatestring) + except ParseError: + pass + + if enddatestring: + try: + enddate = iso8601.parse_date(enddatestring) + except ParseError: + pass + + + + startdate,enddate = get_dates_timeperiod( + request, + startdatestring=startdatestring, + enddatestring=enddatestring) + + try: + trainingplan = TrainingPlan.objects.filter( + startdate__lte = startdate, + rowers = r, + enddate__gte = enddate)[0] + except IndexError: + trainingplan = None + + + sps = get_sessions(r,startdate=startdate,enddate=enddate) + + completeness = {} + actualvalue = {} + completiondate = {} + sessioncolor = {} + + totals = { + 'trimp':0, + 'rscore':0, + 'distance':0, + 'time':0, + 'plannedtime':0, + 'planneddistance':0, + 'plannedtrimp':0, + 'plannedrscore':0, + 'actualtime':0, + 'actualdistance':0, + 'actualtrimp':0, + 'actualrscore':0, + } + + ws = Workout.objects.filter( + user=r, + date__gte=startdate,date__lte=enddate) + + for w in ws: + thetrimp,hrtss = dataprep.workout_trimp(w) + totals['trimp'] += thetrimp + tss = dataprep.workout_rscore(w)[0] + if not np.isnan(tss) and tss != 0: + totals['rscore'] += tss + elif tss == 0: + totals['rscore'] += hrtss + tss = hrtss + totals['distance'] += w.distance + totals['time'] += timefield_to_seconds_duration(w.duration) + if w.plannedsession: + if w.plannedsession.sessionmode == 'distance': + totals['actualdistance'] += w.distance + elif w.plannedsession.sessionmode == 'time': + totals['actualtime'] += timefield_to_seconds_duration(w.duration) + elif w.plannedsession.sessionmode == 'rScore': + totals['actualrscore'] += tss + elif w.plannedsession.sessionmode == 'TRIMP': + totals['actualtrimp'] += thetrimp + + if not sps and request.user.rower.rowerplan == 'basic': + messages.error(request, + "You must purchase Coach or Self-coach plans or be part of a team to get planned sessions") + + for ps in sps: + ratio,status,cdate = is_session_complete(r,ps) + actualvalue[ps.id] = int(ps.sessionvalue*ratio) + completeness[ps.id] = status + sessioncolor[ps.id] = cratiocolors[status] + ws = Workout.objects.filter(user=r,plannedsession=ps) + completiondate[ps.id] = cdate + if ps.sessionmode == 'distance': + totals['planneddistance'] += ps.sessionvalue + elif ps.sessionmode == 'time': + totals['plannedtime'] += ps.sessionvalue + elif ps.sessionmode == 'rScore': + totals['plannedrscore'] += ps.sessionvalue + elif ps.sessionmode == 'TRIMP': + totals['plannedtrimp'] += ps.sessionvalue + + totals['time'] = int(totals['time']/60.) + totals['actualtime'] = int(totals['actualtime']/60.) + totals['plannedtime'] = int(totals['plannedtime']) + + unmatchedworkouts = Workout.objects.filter( + user=r, + plannedsession=None, + date__gte=startdate,date__lte=enddate) + + timeperiod = startdate.strftime('%Y-%m-%d')+'/'+enddate.strftime('%Y-%m-%d') + breadcrumbs = [ + { + 'url': reverse(plannedsessions_view), + 'name': 'Planned Sessions' + }, + ] + + initial = { + 'startdate':startdate, + 'enddate':enddate, + } + + dateform = DateRangeForm(initial=initial) + + + return render(request,'plannedsessions.html', + { + 'teams':get_my_teams(request.user), + 'breadcrumbs':breadcrumbs, + 'plannedsessions':sps, + 'plan':trainingplan, + 'active': 'nav-plan', + 'dateform':dateform, + 'totals':totals, + 'rower':r, + 'timeperiod':timeperiod, + 'completeness':completeness, + 'sessioncolor':sessioncolor, + 'actualvalue':actualvalue, + 'completiondate':completiondate, + 'unmatchedworkouts':unmatchedworkouts, + }) + +@login_required() +def plannedsessions_print_view(request,userid=0): + + r = getrequestrower(request,userid=userid) + + + + startdate,enddate = get_dates_timeperiod(request) + + try: + trainingplan = TrainingPlan.objects.filter( + startdate__lte = startdate, + rowers = r, + enddate__gte = enddate)[0] + except IndexError: + trainingplan = None + + sps = get_sessions(r,startdate=startdate,enddate=enddate) + + completeness = {} + actualvalue = {} + completiondate = {} + + timeperiod = startdate.strftime('%Y-%m-%d')+'/'+enddate.strftime('%Y-%m-%d') + return render(request,'plannedsessions_print.html', + { + 'teams':get_my_teams(request.user), + 'plan':trainingplan, + 'plannedsessions':sps, + 'rower':r, + 'active':'nav-plan', + 'startdate':startdate, + 'enddate':enddate, + 'timeperiod':timeperiod, + }) + + +@login_required() +def plannedsessions_manage_view(request,userid=0, + initialsession=0): + + is_ajax = False + if request.is_ajax(): + is_ajax = True + + + + r = getrequestrower(request,userid=userid) + + startdate,enddate = get_dates_timeperiod(request) + + try: + trainingplan = TrainingPlan.objects.filter( + startdate__lte = startdate, + rowers = r, + enddate__gte = enddate)[0] + except IndexError: + trainingplan = None + + sps = get_sessions(r,startdate=startdate,enddate=enddate) + if initialsession==0: + try: + initialsession=sps[0].id + except IndexError: + initialsession=0 + + if initialsession: + try: + ps0 = PlannedSession.objects.get(id=initialsession) + except PlannedSession.DoesNotExist: + ps0 = None + else: + ps0 = None + + ws = Workout.objects.filter( + user=r,date__gte=startdate, + date__lte=enddate + ).order_by( + "date","startdatetime","id" + ) + + + initialworkouts = [w.id for w in Workout.objects.filter(user=r,plannedsession=ps0)] + + linkedworkouts = [] + for w in ws: + if w.plannedsession is not None: + linkedworkouts.append(w.id) + + plannedsessionstuple = [] + + for ps in sps: + sessiontpl = (ps.id,ps.__unicode__()) + plannedsessionstuple.append(sessiontpl) + + plannedsessionstuple = tuple(plannedsessionstuple) + + workoutdata = {} + workoutdata['initial'] = [] + + choices = [] + + for w in ws: + wtpl = (w.id, w.__unicode__()) + choices.append(wtpl) + if w.id in initialworkouts: + workoutdata['initial'].append(w.id) + + workoutdata['choices'] = tuple(choices) + + if request.method == 'POST': + ps_form = PlannedSessionSelectForm(plannedsessionstuple,request.POST) + w_form = WorkoutSessionSelectForm(workoutdata,request.POST) + + if ps_form.is_valid(): + ps = PlannedSession.objects.get(id=ps_form.cleaned_data['plannedsession']) + if w_form.is_valid(): + selectedworkouts = w_form.cleaned_data['workouts'] + else: + selectedworkouts = [] + + if len(selectedworkouts)==0: + for w in ws: + remove_workout_plannedsession(w,ps) + + if selectedworkouts: + workouts = Workout.objects.filter(user=r,id__in=selectedworkouts) + for w in ws: + if w.id not in selectedworkouts: + remove_workout_plannedsession(w,ps) + + result,comments,errors = add_workouts_plannedsession(workouts,ps,r) + for c in comments: + messages.info(request,c) + for er in errors: + messages.error(request,er) + + + ps_form = PlannedSessionSelectForm(plannedsessionstuple, + initialsession=initialsession) + w_form = WorkoutSessionSelectForm(workoutdata=workoutdata) + + + if is_ajax: + ajax_workouts = [] + for id,name in workoutdata['choices']: + ininitial = id in initialworkouts + inlinked = id in linkedworkouts + ajax_workouts.append((id,name,ininitial,inlinked)) + + ajax_response = { + 'workouts':ajax_workouts, + 'plannedsessionstuple':plannedsessionstuple, + } + + + return JSONResponse(ajax_response) + + + breadcrumbs = [ + { + 'url':reverse(plannedsessions_view, + kwargs={'userid':userid}), + 'name': 'Plan' + }, + { + 'url':reverse(plannedsessions_manage_view, + kwargs={ + 'userid':userid, + 'initialsession':initialsession, + } + ), + 'name': 'Link Sessions to Workouts' + }, + ] + + timeperiod = startdate.strftime('%Y-%m-%d')+'/'+enddate.strftime('%Y-%m-%d') + + dateform = DateRangeForm(initial={ + 'startdate':startdate, + 'enddate':enddate, + }) + + return render(request,'plannedsessionsmanage.html', + { + 'teams':get_my_teams(request.user), + 'plan':trainingplan, + 'dateform':dateform, + 'plannedsessions':sps, + 'workouts':ws, + 'active':'nav-plan', + 'breadcrumbs':breadcrumbs, + 'timeperiod':timeperiod, + 'rower':r, + 'ps_form':ps_form, + 'w_form':w_form, + }) + + +# Clone an existing planned session +# need clarity on cloning behavior time shift +@user_passes_test(hasplannedsessions,login_url="/rowers/paidplans/", + message="This functionality requires a Coach or Self-Coach plan", + redirect_field_name=None) +def plannedsession_clone_view(request,id=0,userid=0): + + r = getrequestrower(request,userid=userid) + + + startdate,enddate = get_dates_timeperiod(request) + + try: + trainingplan = TrainingPlan.objects.filter( + startdate__lte = startdate, + rowers = r, + enddate__gte = enddate)[0] + except IndexError: + trainingplan = None + + try: + ps = PlannedSession.objects.get(id=id) + except PlannedSession.DoesNotExist: + raise Http404("Planned Session does not exist") + + if ps.manager != request.user: + raise PermissionDenied("You are not allowed to clone this planned session") + + rowers = ps.rower.all() + teams = ps.team.all() + + ps.pk = None + + deltadays = ps.enddate-ps.startdate + + ps.startdate = timezone.now().date() + ps.enddate = (timezone.now()+deltadays).date() + ps.preferreddate = ps.preferreddate+deltadays + ps.name += ' (copy)' + + ps.save() + + for rower in rowers: + add_rower_session(rower,ps) + for team in teams: + add_team_session(team,ps) + + url = reverse(plannedsession_edit_view, + kwargs = { + 'id':ps.id, + 'userid':r.user.id, + } + ) + + startdatestring = startdate.strftime('%Y-%m-%d') + enddatestring = enddate.strftime('%Y-%m-%d') + url += '?when='+startdatestring+'/'+enddatestring + + + return HttpResponseRedirect(url) + + +# Edit an existing planned session +@user_passes_test(hasplannedsessions,login_url="/rowers/paidplans/", + message="This functionality requires a Coach or Self-Coach plan", + redirect_field_name=None) +def plannedsession_edit_view(request,id=0,userid=0): + + r = getrequestrower(request,userid=userid) + + + + startdate,enddate = get_dates_timeperiod(request) + + + try: + trainingplan = TrainingPlan.objects.filter( + startdate__lte = startdate, + rowers = r, + enddate__gte = enddate)[0] + except IndexError: + trainingplan = None + + try: + ps = PlannedSession.objects.get(id=id) + except PlannedSession.DoesNotExist: + raise Http404("Planned Session does not exist") + + if ps.manager != request.user: + raise PermissionDenied("You are not allowed to edit this planned session") + + if ps.sessiontype in ['race','indoorrace']: + raise PermissionDenied("You are not allowed to edit this planned session because it is a race") + + if ps.team.all() or len(ps.rower.all())>1: + url = reverse(plannedsession_teamedit_view, + kwargs={ + 'sessionid':id, + }) + return HttpResponseRedirect(url) + + if request.method == 'POST': + sessioncreateform = PlannedSessionForm(request.POST,instance=ps) + if sessioncreateform.is_valid(): + cd = sessioncreateform.cleaned_data + + if cd['sessionunit'] == 'min': + cd['sessionmode'] = 'time' + elif cd['sessionunit'] in ['km','m']: + cd['sessionmode'] = 'distance' + + + res,message = update_plannedsession(ps,cd) + + if res: + messages.info(request,message) + else: + messages.error(request,message) + + url = reverse(plannedsession_edit_view, + kwargs={ + 'id':int(ps.id), + 'userid':r.user.id, + }) + + startdatestring = startdate.strftime('%Y-%m-%d') + enddatestring = enddate.strftime('%Y-%m-%d') + url += '?when='+startdatestring+'/'+enddatestring + + return HttpResponseRedirect(url) + else: + sessioncreateform = PlannedSessionForm(instance=ps) + + sps = get_sessions(r,startdate=startdate,enddate=enddate) + + breadcrumbs = [ + { + 'url':reverse(plannedsessions_view, + kwargs={'userid':userid}), + 'name': 'Plan' + }, + { + 'url': reverse(plannedsessions_view, + kwargs={'userid':userid}), + 'name': 'Sessions' + }, + { + 'url':reverse(plannedsession_view, + kwargs={ + 'userid':userid, + 'id':id, + } + ), + 'name': ps.id + }, + { + 'url':reverse(plannedsession_edit_view, + kwargs={ + 'userid':userid, + 'id':id, + } + ), + 'name': 'Edit' + } + ] + + + dateform = DateRangeForm(initial={ + 'startdate':startdate, + 'enddate':enddate, + }) + + timeperiod = startdate.strftime('%Y-%m-%d')+'/'+enddate.strftime('%Y-%m-%d') + + return render(request,'plannedsessionedit.html', + { + 'teams':get_my_teams(request.user), + 'plan':trainingplan, + 'breadcrumbs':breadcrumbs, + 'form':sessioncreateform, + 'active':'nav-plan', + 'plannedsessions':sps, + 'thesession':ps, + 'dateform':dateform, + 'rower':r, + 'timeperiod':timeperiod, + }) + + +@login_required() +def plannedsession_detach_view(request,id=0,psid=0): + + r = getrequestrower(request) + + try: + ps = PlannedSession.objects.get(id=psid) + except PlannedSession.DoesNotExist: + raise Http404("Planned Session does not exist") + + w = get_workout(id) + + if (checkworkoutuser(request.user,w)==False): + return HttpResponseForbidden("Permission Error") + + remove_workout_plannedsession(w,ps) + + url = reverse(plannedsession_view,kwargs={'id':psid}) + + return HttpResponseRedirect(url) + +@login_required() +def plannedsession_view(request,id=0,userid=0): + + r = getrequestrower(request,userid=userid) + + + + try: + ps = PlannedSession.objects.get(id=id) + except PlannedSession.DoesNotExist: + raise Http404("Planned Session does not exist") + + if ps.sessiontype in ['race','indoorrace']: + url = reverse(virtualevent_view, + kwargs={'id':ps.id} + ) + return HttpResponseRedirect(url) + + if ps.course: + coursescript,coursediv = course_map(ps.course) + else: + coursescript = '' + coursediv = '' + + m = ps.manager + mm = Rower.objects.get(user=m) + + if ps.manager != request.user: + if r.rowerplan == 'coach' and r not in ps.rower.all(): + teams = Team.objects.filter(manager=request.user) + members = Rower.objects.filter(team__in=teams).distinct() + teamusers = [m.user for m in members] + if ps.manager not in teamusers: + raise PermissionDenied("You do not have access to this session") + elif r not in ps.rower.all(): + raise PermissionDenied("You do not have access to this session") + + resultsdict = get_session_metrics(ps) + resultsdict = pd.DataFrame(resultsdict).transpose().to_dict() + + psdict = my_dict_from_instance(ps,PlannedSession) + + ws = get_workouts_session(r,ps) + + ratio,status,completiondate = is_session_complete(r,ps) + + ratio = int(100.*ratio) + + # ranking for test + ranking = [] + + if ps.sessiontype in ['test','coursetest']: + if ps.sessionmode == 'distance': + rankws = Workout.objects.filter( + plannedsession=ps).order_by("duration") + else: + rankws = Workout.objects.filter( + plannedsession=ps).order_by("-distance") + for w in rankws: + dd = w.duration + dddelta = datetime.timedelta(hours=dd.hour, + minutes=dd.minute, + seconds=dd.second, + microseconds=dd.microsecond) + wdict = { + 'name': w.user.user.first_name+' '+w.user.user.last_name, + 'date': w.date, + 'distance': w.distance, + 'time': dddelta, + 'type': w.workouttype, + 'coursecompleted':True, + } + if ps.sessiontype == 'coursetest': + vs = CourseTestResult.objects.filter(plannedsession=ps, + workoutid=w.id) + + if vs: + for record in vs: + if record.workoutid == w.id: + coursemeters = record.distance + coursecompleted = record.coursecompleted + t = record.duration + wdict['time'] = datetime.timedelta( + hours=t.hour, + seconds=t.second, + minutes=t.minute, + microseconds=t.microsecond + ) + wdict['distance'] = int(round(coursemeters)) + wdict['coursecompleted'] = coursecompleted + else: + record = CourseTestResult( + userid=w.user.id, + workoutid=w.id, + plannedsession=ps, + duration=w.duration, + coursecompleted=False, + ) + record.save() + job = myqueue(queue,handle_check_race_course, + w.csvfilename,w.id,ps.course.id, + record.id,mode='coursetest') + + intsecs = 0 + microsecs = 0 + + # taking workout duration plus 1 minute penalty + wdict['time'] = w.duration + wdict['distance'] = ps.course.distance + wdict['coursecompleted'] = False + + + ranking.append(wdict) + if ps.sessiontype == 'coursetest': + ranking = sorted(ranking, key=lambda k: k['time']) + + # if coursetest, need to reorder the ranking + + startdate,enddate = get_dates_timeperiod(request) + try: + trainingplan = TrainingPlan.objects.filter( + startdate__lte = startdate, + rowers = r, + enddate__gte = enddate)[0] + except IndexError: + trainingplan = None + + timeperiod = startdate.strftime('%Y-%m-%d')+'/'+enddate.strftime('%Y-%m-%d') + breadcrumbs = [ + { + 'url':reverse(plannedsessions_view, + kwargs={'userid':userid}), + 'name': 'Plan' + }, + { + 'url': reverse(plannedsessions_view, + kwargs={'userid':userid}), + 'name': 'Sessions' + }, + { + 'url':reverse(plannedsession_view, + kwargs={ + 'userid':userid, + 'id':id, + } + ), + 'name': ps.id + } + ] + + return render(request,'plannedsessionview.html', + { + 'psdict': psdict, + 'attrs':[ + 'name','startdate','enddate','preferreddate', + 'sessiontype', + 'sessionmode','criterium', + 'sessionvalue','sessionunit','comment', + ], + 'workouts': ws, + 'active':'nav-plan', + 'breadcrumbs':breadcrumbs, + 'manager':mm, + 'rower':r, + 'ratio':ratio, + 'plan':trainingplan, + 'status':status, + 'results':resultsdict, + 'plannedsession':ps, + 'timeperiod':timeperiod, + 'ranking':ranking, + 'coursescript': coursescript, + 'coursediv': coursediv + } + ) + +class PlannedSessionDelete(DeleteView): + model = PlannedSession + template_name = 'plannedsessiondeleteconfirm.html' + + # extra parameters + def get_context_data(self, **kwargs): + context = super(PlannedSessionDelete,self).get_context_data(**kwargs) + + if 'userid' in kwargs: + userid = kwargs['userid'] + else: + userid = 0 + + context['active']= 'nav-plan' + context['rower'] = getrequestrower(self.request,userid=userid) + context['ps'] = self.object + + psdict = my_dict_from_instance(self.object,PlannedSession) + + context['psdict'] = psdict + + context['attrs'] = ['name','startdate','enddate','sessiontype'] + + breadcrumbs = [ + { + 'url':reverse(plannedsessions_view, + kwargs={'userid':userid}), + 'name': 'Plan' + }, + { + 'url': reverse(plannedsessions_view, + kwargs={'userid':userid}), + 'name': 'Sessions' + }, + { + 'url':reverse(plannedsession_view, + kwargs={ + 'userid':userid, + 'id':self.object.pk, + } + ), + 'name': self.object.pk + }, + { + 'url':reverse('plannedsession_delete_view', + kwargs={'pk':self.object.pk}), + 'name': 'Delete' + } + ] + + context['breadcrumbs'] = breadcrumbs + + return context + + def get_success_url(self): + ws = Workout.objects.filter(plannedsession=self.object) + for w in ws: + w.plannedsession = None + w.save() + + url = reverse(plannedsessions_view) + + return url + + def get_object(self, *args, **kwargs): + obj = super(PlannedSessionDelete, self).get_object(*args, **kwargs) + m = Rower.objects.get(user=obj.manager) + if not checkaccessuser(self.request.user,m): + raise PermissionDenied('You are not allowed to delete this planned session') + + return obj + + +@user_passes_test(hasplannedsessions,login_url="/rowers/paidplans", + message="This functionality requires a Coach or Self-Coach plan", + redirect_field_name=None) +def rower_create_trainingplan(request,userid=0): + + therower = getrequestrower(request,userid=userid) + theuser = therower.user + themanager = getrower(request.user) + + if request.method == 'POST' and 'date' in request.POST: + targetform = TrainingTargetForm(request.POST,user=request.user) + if targetform.is_valid(): + name = targetform.cleaned_data['name'] + date = targetform.cleaned_data['date'] + notes = targetform.cleaned_data['notes'] + + t = TrainingTarget( + name=name, + date=date, + manager=themanager, + notes=notes) + + + t.save() + t.rowers.add(therower) + t.save() + + elif request.method == 'POST' and 'startdate' in request.POST: + form = TrainingPlanForm(request.POST,user=request.user) + + + if form.is_valid(): + name = form.cleaned_data['name'] + try: + target = form.cleaned_data['target'] + except KeyError: + try: + targetid = request.POST['target'] + if targetid != '': + target = TrainingTarget.objects.get(id=int(targetid)) + else: + target = None + except KeyError: + target = None + startdate = form.cleaned_data['startdate'] + enddate = form.cleaned_data['enddate'] + + try: + athletes = form.cleaned_data['rowers'] + except KeyError: + athletes = [therower] + + p = TrainingPlan( + name=name, + target=target, + manager=themanager, + startdate=startdate, + enddate=enddate, + ) + + p.save() + + for athlete in athletes: + p.rowers.add(athlete) + + + targets = TrainingTarget.objects.filter( + rowers=therower, + date__gte=datetime.date.today(), + ).order_by("date") + targetform = TrainingTargetForm(user=request.user) + + plans = TrainingPlan.objects.filter(rowers=therower).order_by("-startdate") + + + form = TrainingPlanForm(targets=targets, + initial={'status':False,'rowers':[therower]}, + user=request.user) + + breadcrumbs = [ + { + 'url':reverse(plannedsessions_view, + kwargs={'userid':userid}), + 'name': 'Plan' + }, + { + 'url':reverse(rower_create_trainingplan, + kwargs={'userid':userid}), + 'name': 'Manage Plans and Targets' + } + ] + + + return render(request,'trainingplan_create.html', + { + 'form':form, + 'rower':therower, + 'breadcrumbs':breadcrumbs, + 'plans':plans, + 'active':'nav-plan', + 'targets':targets, + 'targetform':targetform, + }) + +@user_passes_test(hasplannedsessions,login_url="/rowers/paidplans", + message="This functionality requires a Coach or Self-Coach plan", + redirect_field_name=None) +def rower_delete_trainingtarget(request,id=0): + try: + target = TrainingTarget.objects.get(id=id) + except TrainingPlan.DoesNotExist: + raise Http404("Training Plan Does Not Exist") + + if checkaccessuser(request.user,target.manager): + target.delete() + messages.info(request,"We have deleted the training target") + else: + raise PermissionDenied("Access denied") + + url = reverse(rower_create_trainingplan) + + return HttpResponseRedirect(url) + + +@user_passes_test(hasplannedsessions,login_url="/rowers/paidplans", + message="This functionality requires a Coach or Self-Coach plan", + redirect_field_name=None) +def rower_delete_trainingplan(request,id=0): + try: + plan = TrainingPlan.objects.get(id=id) + except TrainingPlan.DoesNotExist: + raise Http404("Training Plan Does Not Exist") + + if checkaccessuser(request.user,plan.manager): + plan.delete() + messages.info(request,"We have deleted the training plan") + else: + raise PermissionDenied("Access denied") + + url = reverse(rower_create_trainingplan) + + return HttpResponseRedirect(url) + +class TrainingPlanDelete(DeleteView): + model = TrainingPlan + template_name = 'trainingplan_delete.html' + success_url = reverse_lazy(rower_create_trainingplan) + + def get_object(self, *args, **kwargs): + obj = super(TrainingPlanDelete, self).get_object(*args, **kwargs) + if not checkaccessuser(self.request.user,obj.manager): + raise PermissionDenied('You are not allowed to delete this training plan') + + return obj + +class MicroCycleDelete(DeleteView): + model = TrainingMicroCycle + template_name = 'trainingplan_delete.html' + + # extra parameters + def get_context_data(self, **kwargs): + context = super(MicroCycleDelete, self).get_context_data(**kwargs) + + if 'userid' in kwargs: + userid = kwargs['userid'] + else: + userid=0 + + breadcrumbs = [ + { + 'url':reverse(plannedsessions_view, + kwargs={'userid':userid}), + 'name': 'Plan' + }, + { + 'url':reverse(rower_trainingplan_view, + kwargs={'userid':userid, + 'id':self.object.plan.plan.plan.id}), + 'name': self.object.plan.plan.plan.name + }, + { + 'url':reverse('macrocycle_update_view', + kwargs={'pk':self.object.plan.plan.pk}), + 'name': self.object.plan.plan.name + }, + { + 'url':reverse('mesocycle_update_view', + kwargs={'pk':self.object.plan.pk}), + 'name': self.object.plan.name + }, + { + 'url':reverse('microcycle_update_view', + kwargs={'pk':self.object.pk}), + 'name': self.object.name + } + + ] + + context['active'] = 'nav-plan' + context['breadcrumbs'] = breadcrumbs + context['rower'] = getrequestrower(self.request,userid=userid) + + return context + + def get_success_url(self): + plan = self.object.plan.plan.plan + createmacrofillers(plan) + thismesoid = self.object.plan.pk + return reverse(rower_trainingplan_view, + kwargs = { + 'id':plan.id, + 'thismesoid':thismesoid + }) + + def get_object(self, *args, **kwargs): + obj = super(MicroCycleDelete, self).get_object(*args, **kwargs) + if not checkaccessuser(self.request.user,obj.plan.plan.plan.manager): + raise PermissionDenied('You are not allowed to delete this training plan cycle') + return obj + + +class MesoCycleDelete(DeleteView): + model = TrainingMesoCycle + template_name = 'trainingplan_delete.html' + + # extra parameters + def get_context_data(self, **kwargs): + context = super(MesoCycleDelete, self).get_context_data(**kwargs) + + if 'userid' in kwargs: + userid = kwargs['userid'] + else: + userid=0 + + breadcrumbs = [ + { + 'url':reverse(plannedsessions_view, + kwargs={'userid':userid}), + 'name': 'Plan' + }, + { + 'url':reverse(rower_trainingplan_view, + kwargs={'userid':userid, + 'id':self.object.plan.plan.id}), + 'name': self.object.plan.plan.name + }, + { + 'url':reverse('macrocycle_update_view', + kwargs={'pk':self.object.plan.pk}), + 'name': self.object.plan.name + }, + { + 'url':reverse('mesocycle_update_view', + kwargs={'pk':self.object.pk}), + 'name': self.object.name + } + + ] + + context['active'] = 'nav-plan' + context['breadcrumbs'] = breadcrumbs + context['rower'] = getrequestrower(self.request,userid=userid) + + return context + + def get_success_url(self): + plan = self.object.plan.plan + thismacroid = self.object.plan.pk + createmacrofillers(plan) + return reverse(rower_trainingplan_view, + kwargs = { + 'id':plan.id, + 'thismacroid':thismacroid, + }) + + def get_object(self, *args, **kwargs): + obj = super(MesoCycleDelete, self).get_object(*args, **kwargs) + + if not checkaccessuser(self.request.user,obj.plan.plan.manager): + raise PermissionDenied('You are not allowed to delete this training plan cycle') + + return obj + + +class MacroCycleDelete(DeleteView): + model = TrainingMacroCycle + template_name = 'trainingplan_delete.html' + + # extra parameters + def get_context_data(self, **kwargs): + context = super(MacroCycleDelete, self).get_context_data(**kwargs) + + if 'userid' in kwargs: + userid = kwargs['userid'] + else: + userid=0 + + breadcrumbs = [ + { + 'url':reverse(plannedsessions_view, + kwargs={'userid':userid}), + 'name': 'Plan' + }, + { + 'url':reverse(rower_trainingplan_view, + kwargs={'userid':userid, + 'id':self.object.plan.id}), + 'name': self.object.plan.name + }, + { + 'url':reverse('macrocycle_update_view', + kwargs={'pk':self.object.pk}), + 'name': self.object.name + } + ] + + context['active'] = 'nav-plan' + context['breadcrumbs'] = breadcrumbs + context['rower'] = getrequestrower(self.request,userid=userid) + + return context + + def get_success_url(self): + plan = self.object.plan + createmacrofillers(plan) + return reverse(rower_trainingplan_view, + kwargs = { + 'id':plan.id + }) + + def get_object(self, *args, **kwargs): + obj = super(MacroCycleDelete, self).get_object(*args, **kwargs) + if not checkaccessuser(self.request.user,obj.plan.manager): + raise PermissionDenied('You are not allowed to delete this training plan cycle') + + return obj + + +@user_passes_test(hasplannedsessions,login_url="/rowers/paidplans", + message="This functionality requires a Coach or Self-Coach plan", + redirect_field_name=None) +def rower_trainingplan_view(request, + id=0, + userid=0, + thismicroid=0, + thismacroid=0, + thismesoid=0): + + + startdate,enddate = get_dates_timeperiod(request) + + + try: + plan = TrainingPlan.objects.get(id=id) + except TrainingPlan.DoesNotExist: + raise Http404("Training Plan Does Not Exist") + + r = getrequestrower(request,userid=userid) + + if not checkaccessuser(request.user,plan.manager): + if request.user.rower not in plan.rowers.all(): + raise PermissionDenied("Access denied") + + createmacrofillers(plan) + macrocycles = TrainingMacroCycle.objects.filter( + plan=plan, + type='userdefined').order_by("startdate") + + + for m in macrocycles: + createmesofillers(m) + m.plantime = 0 + m.actualtime = 0 + m.plandistance = 0 + m.actualdistance = 0 + m.planrscore = 0 + m.actualrscore = 0 + m.plantrimp = 0 + m.actualtrimp = 0 + + + mesocycles = TrainingMesoCycle.objects.filter( + plan=m, + type='userdefined').order_by("startdate") + + for me in mesocycles: + me.plantime = 0 + me.actualtime = 0 + me.plandistance = 0 + me.actualdistance = 0 + me.planrscore = 0 + me.actualrscore = 0 + me.plantrimp = 0 + me.actualtrimp = 0 + + microcycles = TrainingMicroCycle.objects.filter( + plan=me, + type='userdefined').order_by("startdate") + + for mm in microcycles: + sps = get_sessions(r,startdate=mm.startdate,enddate=mm.enddate) + + # sps = PlannedSession.objects.filter( + # rower = r, + # startdate__lte=mm.enddate, + # enddate__gte=mm.startdate) + + + mm.plantime = 0 + mm.actualtime = 0 + mm.plandistance = 0 + mm.actualdistance = 0 + mm.planrscore = 0 + mm.actualrscore = 0 + mm.plantrimp = 0 + mm.actualtrimp = 0 + + + if mm.type == 'userdefined': + for ps in sps: + ratio, status, cdate = is_session_complete(r,ps) + if ps.sessionmode == 'time': + mm.plantime += ps.sessionvalue + mm.actualtime += int(ps.sessionvalue*ratio) + elif ps.sessionmode == 'distance' and ps.sessiontype != 'race': + mm.plandistance += ps.sessionvalue + mm.actualdistance += int(ps.sessionvalue*ratio) + elif ps.sessionmode == 'rScore': + mm.planrscore += ps.sessionvalue + mm.actualrscore += int(ps.sessionvalue*ratio) + elif ps.sessionmode == 'TRIMP': + mm.plantrimp += ps.sessionvalue + mm.actualtrimp += int(ps.sessionvalue*ratio) + + mm.save() + + me.plantime += mm.plantime + me.actualtime += mm.actualtime + me.plandistance += mm.plandistance + me.actualdistance += mm.actualdistance + me.planrscore += mm.planrscore + me.actualrscore += mm.actualrscore + me.plantrimp += mm.plantrimp + me.actualtrimp += mm.actualtrimp + + if me.type == 'userdefined': + me.save() + + m.plantime += me.plantime + m.actualtime += me.actualtime + m.plandistance += me.plandistance + m.actualdistance += me.actualdistance + m.planrscore += me.planrscore + m.actualrscore += me.actualrscore + m.plantrimp += me.plantrimp + m.actualtrimp += me.actualtrimp + + + + if m.type == 'userdefined': + m.save() + + createmacrofillers(plan) + macrocycles = TrainingMacroCycle.objects.filter(plan=plan).order_by("startdate") + + count = 0 + cycles = {} + + for m in macrocycles: + createmesofillers(m) + mesocycles = TrainingMesoCycle.objects.filter(plan=m).order_by("startdate") + mesos = {} + count2 = 0 + for me in mesocycles: + createmicrofillers(me) + microcycles = TrainingMicroCycle.objects.filter(plan=me).order_by("startdate") + + mesos[count2] = (me, microcycles) + count2 += 1 + + cycles[count] = (m,mesos) + count += 1 + + breadcrumbs = [ + { + 'url':reverse(plannedsessions_view, + kwargs={'userid':userid}), + 'name': 'Plan' + }, + { + 'url':reverse(rower_trainingplan_view, + kwargs={'userid':userid, + 'id':id}), + 'name': plan.name + } + ] + + if not thismicroid and not thismacroid and not thismesoid: + try: + thismicro = get_todays_micro(plan,thedate=startdate) + thismicroid = thismicro.pk + except AttributeError: + thismicroid = None + + + return render(request,'trainingplan.html', + { + 'plan':plan, + 'active':'nav-plan', + 'breadcrumbs':breadcrumbs, + 'rower':r, + 'cycles':cycles, + 'thismicroid':thismicroid, + 'thismacroid':thismacroid, + 'thismesoid':thismesoid, + } + ) + +class TrainingMacroCycleUpdate(UpdateView): + model = TrainingMacroCycle + template_name = 'trainingplan_edit.html' + form_class = TrainingMacroCycleForm + + # extra parameters + def get_context_data(self, **kwargs): + context = super(TrainingMacroCycleUpdate, self).get_context_data(**kwargs) + + if 'userid' in kwargs: + userid = kwargs['userid'] + else: + userid=0 + + breadcrumbs = [ + { + 'url':reverse(plannedsessions_view, + kwargs={'userid':userid}), + 'name': 'Plan' + }, + { + 'url':reverse(rower_trainingplan_view, + kwargs={'userid':userid, + 'id':self.object.plan.id}), + 'name': self.object.plan.name + }, + { + 'url':reverse('macrocycle_update_view', + kwargs={'pk':self.object.pk}), + 'name': self.object.name + } + ] + + context['active'] = 'nav-plan' + context['breadcrumbs'] = breadcrumbs + context['rower'] = getrequestrower(self.request,userid=userid) + + return context + + def get_success_url(self): + plan = self.object.plan + createmacrofillers(plan) + return reverse(rower_trainingplan_view, + kwargs = { + 'id':plan.id, + 'thismacroid':self.object.id, + } + ) + + def form_valid(self, form): + form.instance.user = self.request.user + form.instance.post_date = datetime.datetime.now() + macrocycle = form.save() + mesocyclecheckdates(macrocycle) + return super(TrainingMacroCycleUpdate, self).form_valid(form) + + def get_object(self, *args, **kwargs): + obj = super(TrainingMacroCycleUpdate, self).get_object(*args, **kwargs) + if obj.plan.manager is not None and self.request.user.rower != obj.plan.manager: + raise PermissionDenied('You are not allowed to edit this training plan cycle') + + if not checkaccessuser(self.request.user,obj.plan.manager): + raise PermissionDenied('You are not allowed to edit this training plan cycle') + else: + obj.type = 'userdefined' + obj.save() + return obj + +class TrainingMesoCycleUpdate(UpdateView): + model = TrainingMesoCycle + template_name = 'trainingplan_edit.html' + form_class = TrainingMesoCycleForm + + # extra parameters + def get_context_data(self, **kwargs): + context = super(TrainingMesoCycleUpdate, self).get_context_data(**kwargs) + + if 'userid' in kwargs: + userid = kwargs['userid'] + else: + userid=0 + + breadcrumbs = [ + { + 'url':reverse(plannedsessions_view, + kwargs={'userid':userid}), + 'name': 'Plan' + }, + { + 'url':reverse(rower_trainingplan_view, + kwargs={'userid':userid, + 'id':self.object.plan.plan.id}), + 'name': self.object.plan.plan.name + }, + { + 'url':reverse('macrocycle_update_view', + kwargs={'pk':self.object.plan.pk}), + 'name': self.object.plan.name + }, + { + 'url':reverse('mesocycle_update_view', + kwargs={'pk':self.object.pk}), + 'name': self.object.name + } + + ] + + context['active'] = 'nav-plan' + context['breadcrumbs'] = breadcrumbs + context['rower'] = getrequestrower(self.request,userid=userid) + + return context + + def get_success_url(self): + plan = self.object.plan + createmesofillers(plan) + return reverse(rower_trainingplan_view, + kwargs = { + 'id':plan.plan.id, + 'thismesoid':self.object.id, + } + ) + + def form_valid(self, form): + form.instance.user = self.request.user + form.instance.post_date = datetime.datetime.now() + mesocycle = form.save() + microcyclecheckdates(mesocycle) + return super(TrainingMesoCycleUpdate, self).form_valid(form) + + def get_object(self, *args, **kwargs): + obj = super(TrainingMesoCycleUpdate, self).get_object(*args, **kwargs) + if obj.plan.plan.manager is not None and self.request.user.rower != obj.plan.plan.manager: + raise PermissionDenied('You are not allowed to edit this training plan cycle') + + else: + obj.type = 'userdefined' + obj.save() + obj.plan.type = 'userdefined' + obj.plan.save() + return obj + +class TrainingMicroCycleUpdate(UpdateView): + model = TrainingMicroCycle + template_name = 'trainingplan_edit.html' + form_class = TrainingMicroCycleForm + + # extra parameters + def get_context_data(self, **kwargs): + context = super(TrainingMicroCycleUpdate, self).get_context_data(**kwargs) + + if 'userid' in kwargs: + userid = kwargs['userid'] + else: + userid=0 + + breadcrumbs = [ + { + 'url':reverse(plannedsessions_view, + kwargs={'userid':userid}), + 'name': 'Plan' + }, + { + 'url':reverse(rower_trainingplan_view, + kwargs={'userid':userid, + 'id':self.object.plan.plan.plan.id}), + 'name': self.object.plan.plan.plan.name + }, + { + 'url':reverse('macrocycle_update_view', + kwargs={'pk':self.object.plan.plan.pk}), + 'name': self.object.plan.plan.name + }, + { + 'url':reverse('mesocycle_update_view', + kwargs={'pk':self.object.plan.pk}), + 'name': self.object.plan.name + }, + { + 'url':reverse('microcycle_update_view', + kwargs={'pk':self.object.pk}), + 'name': self.object.name + } + + ] + + context['active'] = 'nav-plan' + context['breadcrumbs'] = breadcrumbs + context['rower'] = getrequestrower(self.request,userid=userid) + + return context + + def get_success_url(self): + plan = self.object.plan + createmicrofillers(plan) + return reverse(rower_trainingplan_view, + kwargs = { + 'id':plan.plan.plan.id, + 'thismicroid':self.object.pk + } + ) + def form_valid(self, form): + form.instance.user = self.request.user + form.instance.post_date = datetime.datetime.now() + microcycle = form.save() + + return super(TrainingMicroCycleUpdate, self).form_valid(form) + + def get_object(self, *args, **kwargs): + obj = super(TrainingMicroCycleUpdate, self).get_object(*args, **kwargs) + if obj.plan.plan.plan.manager is not None and self.request.user.rower != obj.plan.plan.plan.manager: + raise PermissionDenied('You are not allowed to edit this training plan cycle') + + + else: + obj.type = 'userdefined' + obj.save() + obj.plan.type = 'userdefined' + obj.plan.save() + return obj + +class TrainingPlanUpdate(UpdateView): + model = TrainingPlan + template_name = 'trainingplan_edit.html' + form_class = TrainingPlanForm + + # extra parameters + def get_context_data(self, **kwargs): + context = super(TrainingPlanUpdate, self).get_context_data(**kwargs) + + if 'userid' in kwargs: + userid = kwargs['userid'] + else: + userid=0 + + breadcrumbs = [ + { + 'url':reverse(plannedsessions_view, + kwargs={'userid':userid}), + 'name': 'Plan' + }, + { + 'url':reverse(rower_trainingplan_view, + kwargs={'userid':userid, + 'id':self.object.id}), + 'name': self.object.name + }, + { + 'url':reverse('trainingplan_update_view', + kwargs={'pk':self.object.pk}), + 'name': 'Edit' + } + + ] + + context['active'] = 'nav-plan' + context['breadcrumbs'] = breadcrumbs + context['rower'] = getrequestrower(self.request,userid=userid) + + return context + + def get_success_url(self): + return reverse(rower_create_trainingplan) + + def form_valid(self, form): + form.instance.user = self.request.user + form.instance.post_date = datetime.datetime.now() + plan = form.save() + plan.manager = self.request.user.rower + plan.save() + macrocyclecheckdates(plan) + return super(TrainingPlanUpdate, self).form_valid(form) + + def get_object(self, *args, **kwargs): + obj = super(TrainingPlanUpdate, self).get_object(*args, **kwargs) + if obj.manager is not None and self.request.user.rower != obj.manager: + raise PermissionDenied('You are not allowed to edit this training plan cycle') + if obj.manager.rowerplan not in ['coach','plan']: + raise PermissionDenied('You are not allowed to edit this training plan') + + return obj + +class TrainingTargetUpdate(UpdateView): + model = TrainingTarget + template_name = 'trainingplan_edit.html' + form_class = TrainingTargetForm + + # extra parameters + def get_context_data(self, **kwargs): + context = super(TrainingTargetUpdate, self).get_context_data(**kwargs) + + if 'userid' in kwargs: + userid = kwargs['userid'] + else: + userid=0 + + breadcrumbs = [ + { + 'url':reverse(plannedsessions_view, + kwargs={'userid':userid}), + 'name': 'Plan' + }, + { + 'url':reverse('trainingtarget_update_view', + kwargs={'pk':self.object.pk}), + 'name': 'Edit' + } + + ] + + context['active'] = 'nav-plan' + context['breadcrumbs'] = breadcrumbs + context['rower'] = getrequestrower(self.request,userid=userid) + + return context + + def get_success_url(self): + return reverse(rower_create_trainingplan) + + def form_valid(self, form): + form.instance.user = self.request.user + form.instance.post_date = datetime.datetime.now() + target = form.save() + return super(TrainingTargetUpdate, self).form_valid(form) + + def get_object(self, *args, **kwargs): + obj = super(TrainingTargetUpdate, self).get_object(*args, **kwargs) + if obj.manager is not None and self.request.user.rower != obj.manager: + raise PermissionDenied('You are not allowed to edit this training plan cycle') + + return obj + +from rowers.utils import allsundays + +@user_passes_test(hasplannedsessions,login_url="/rowers/paidplans", + message="This functionality requires a Coach or Self-Coach plan", + redirect_field_name=None) +def planmesocyclebyweek(request,id=0,userid=0): + try: + cycle = TrainingMesoCycle.objects.get(id=id) + except TrainingMesoCycle.DoesNotExist: + raise Http404("Training Cycle does not exist") + + if not checkaccessuser(request.user,cycle.plan.plan.manager): + raise PermissionDenied("You are not allowed to do this") + + micros = TrainingMicroCycle.objects.filter(plan=cycle) + for m in micros: + m.delete() + + cycle.type = 'userdefined' + cycle.save() + + #we're still here. We have permission + sundays = [s for s in allsundays(cycle.startdate,cycle.enddate)] + + if sundays and sundays[-1] < cycle.enddate: + sundays = sundays+[cycle.enddate] + elif not sundays: + sundays = [cycle.enddate] + + for i in range(len(sundays)): + if i==0: + monday = cycle.startdate + else: + monday = sundays[i]-timedelta(days=6) + if monday < cycle.startdate: + monday = cycle.startdate + + nextsunday = sundays[i] + + micro = TrainingMicroCycle(startdate = monday, + enddate = nextsunday, + plan = cycle, + name = 'Week %s' % monday.isocalendar()[1], + type = 'userdefined') + micro.save() + + micros = TrainingMicroCycle.objects.filter(plan=cycle) + + url = reverse(rower_trainingplan_view, + kwargs = {'userid':str(userid), + 'id':str(cycle.plan.plan.id), + 'thismicroid':str(micros[0].id)}) + + return HttpResponseRedirect(url) + +from rowers.utils import allmonths + +@user_passes_test(hasplannedsessions,login_url="/rowers/paidplans", + message="This functionality requires a Coach or Self-Coach plan", + redirect_field_name=None) +def planmacrocyclebymonth(request,id=0,userid=0): + try: + cycle = TrainingMacroCycle.objects.get(id=id) + except TrainingMacroCycle.DoesNotExist: + raise Http404("Training Cycle does not exist") + + if not checkaccessuser(request.user,cycle.plan.manager): + raise PermissionDenied("You are not allowed to do this") + + mesos = TrainingMesoCycle.objects.filter(plan=cycle) + for m in mesos: + m.delete() + + cycle.type = 'userdefined' + cycle.save() + + #we're still here. We have permission + monthstarts = [d for d in allmonths(cycle.startdate,cycle.enddate)] + monthstarts.append(cycle.enddate) + for i in range(len(monthstarts)-1): + firstday = monthstarts[i] + lastday = monthstarts[i+1]-timedelta(days=1) + if lastday < cycle.enddate and i == len(monthstarts)-2: + lastday = cycle.enddate + + + meso = TrainingMesoCycle(startdate = firstday, + enddate = lastday, + plan = cycle, + name = '%s' % firstday.strftime("%B"), + type = 'userdefined') + meso.save() + + mesos = TrainingMesoCycle.objects.filter(plan=cycle) + + url = reverse(rower_trainingplan_view, + kwargs = {'userid':str(userid), + 'id':str(cycle.plan.id), + 'thismesoid':str(mesos[0].id)}) + + return HttpResponseRedirect(url) + + + diff --git a/rowers/views/racesviews.py b/rowers/views/racesviews.py new file mode 100644 index 00000000..a82a6d17 --- /dev/null +++ b/rowers/views/racesviews.py @@ -0,0 +1,2305 @@ +from statements import * + + +# List Courses +@login_required() +def courses_view(request): + r = getrower(request.user) + + courses = GeoCourse.objects.all().order_by("country","name") + + # add search processing + query = request.GET.get('q') + if query: + query_list = query.split() + courses = GeoCourse.objects.filter( + reduce(operator.and_, + (Q(name__icontains=q) for q in query_list)) | + reduce(operator.and_, + (Q(country__icontains=q) for q in query_list)) | + reduce(operator.and_, + (Q(notes__icontains=q) for q in query_list)) + ) + searchform = SearchForm(initial={'q':query}) + else: + searchform = SearchForm() + + return render(request,'list_courses.html', + {'courses':courses, + 'active':'nav-racing', + 'searchform':searchform, + 'rower':r, + }) + + +# for ajax calls +def course_map_view(request,id=0): + try: + course = GeoCourse.objects.get(id=id) + except GeoCourse.DoesNotExist: + return Http404("Course doesn't exist") + + script,div = course_map(course) + + breadcrumbs = [ + { + 'url': reverse(virtualevents_view), + 'name': 'Racing' + }, + { + 'url': reverse(courses_view), + 'name': 'Courses' + }, + { + 'url': reverse(course_view,kwargs={'id':course.id}), + 'name': course.name + }, + { + 'url': reverse(course_map_view,kwargs={'id':course.id}), + 'name': 'Map' + } + ] + + r = getrower(request.user) + + + return render(request, + 'coursemap.html', + { + 'mapdiv':div, + 'course':course, + 'mapscript':script, + 'active':'nav-racing', + 'rower':r, + 'breadcrumbs':breadcrumbs, + }) + + +@login_required() +def course_replace_view(request,id=0): + try: + course = GeoCourse.objects.get(id=id) + except GeoCourse.DoesNotExist: + return Http404("Course doesn't exist") + + r = getrower(request.user) + + if course.manager != r: + raise PermissionDenied("Access denied") + + thecourses = GeoCourse.objects.filter(manager=r).exclude(id=id) + + if request.method == 'POST': + form = CourseSelectForm(request.POST) + if form.is_valid(): + course2 = form.cleaned_data['course'] + res = courses.replacecourse(course,course2) + + url = reverse(course_view, + kwargs = { + 'id':course2.id + }) + + return HttpResponseRedirect(url) + else: + + form = CourseSelectForm() + form.fields["course"].queryset = thecourses + + script,div = course_map(course) + + breadcrumbs = [ + { + 'url': reverse(virtualevents_view), + 'name': 'Racing' + }, + { + 'url': reverse(courses_view), + 'name': 'Courses' + }, + { + 'url': reverse(course_view,kwargs={'id':course.id}), + 'name': course.name + }, + { + 'url': reverse(course_replace_view,kwargs={'id':course.id}), + 'name': 'Replace Markers' + } + ] + + return render(request, + 'course_replace.html', + {'course':course, + 'active':'nav-racing', + 'breadcrumbs':breadcrumbs, + 'rower':r, + 'mapdiv':div, + 'mapscript':script, + 'form':form}) + +@login_required() +def course_delete_view(request,id=0): + try: + course = GeoCourse.objects.get(id=id) + except GeoCourse.DoesNotExist: + return Http404("Course doesn't exist") + + r = getrower(request.user) + + if course.manager != r: + raise PermissionDenied("Access denied") + + ps = PlannedSession.objects.filter(course=course) + nosessions = len(ps) == 0 + + if nosessions: + course.delete() + + url = reverse(courses_view) + + return HttpResponseRedirect(url) + +@login_required() +def course_edit_view(request,id=0): + try: + course = GeoCourse.objects.get(id=id) + except GeoCourse.DoesNotExist: + return Http404("Course doesn't exist") + + r = getrower(request.user) + + if course.manager != r: + raise PermissionDenied("Access denied") + + ps = PlannedSession.objects.filter(course=course) + nosessions = len(ps) == 0 + + script,div = course_map(course) + + if request.method == 'POST': + form = GeoCourseEditForm(request.POST) + if form.is_valid(): + name = form.cleaned_data['name'] + country = form.cleaned_data['country'] + notes = form.cleaned_data['notes'] + if isinstance(name,unicode): + name = name.encode('utf8') + elif isinstance(name, str): + name = name.decode('utf8') + + course.name = name + course.country = country + course.notes = notes + course.save() + + form = GeoCourseEditForm(instance=course) + + breadcrumbs = [ + { + 'url': reverse(virtualevents_view), + 'name': 'Racing' + }, + { + 'url': reverse(courses_view), + 'name': 'Courses' + }, + { + 'url': reverse(course_view,kwargs={'id':course.id}), + 'name': course.name + }, + { + 'url': reverse(course_edit_view,kwargs={'id':course.id}), + 'name': 'Edit' + } + ] + + return render(request, 'course_edit_view.html', + { + 'course':course, + 'active':'nav-racing', + 'breadcrumbs':breadcrumbs, + 'mapscript':script, + 'mapdiv':div, + 'nosessions':nosessions, + 'rower':r, + 'form':form, + } + ) + +@login_required() +def course_view(request,id=0): + try: + course = GeoCourse.objects.get(id=id) + except GeoCourse.DoesNotExist: + return Http404("Course doesn't exist") + + r = getrower(request.user) + + script,div = course_map(course) + + breadcrumbs = [ + { + 'url': reverse(virtualevents_view), + 'name': 'Racing' + }, + { + 'url': reverse(courses_view), + 'name': 'Courses' + }, + { + 'url': reverse(course_view,kwargs={'id':course.id}), + 'name': course.name + }, + ] + + return render(request, 'course_view.html', + { + 'active':'nav-racing', + 'breadcrumbs':breadcrumbs, + 'course':course, + 'mapscript':script, + 'mapdiv':div, + 'nosessions':False, + 'rower':r, + } + ) + +@login_required() +def logo_delete_view(request,id=0): + try: + logo = RaceLogo.objects.get(id=id) + except RaceLogo.DoesNotExist: + raise Http404("Logo doesn't exist") + + if logo.user == request.user: + logo.delete() + messages.info(request,"Logo Deleted") + + url = reverse(virtualevents_view) + + return HttpResponseRedirect(url) + +@login_required() +def virtualevent_setlogo_view(request,id=0,logoid=0): + try: + race = VirtualRace.objects.get(id=id) + except VirtualRace.DoesNotExist: + raise Http404("Race doesn't exist") + + try: + logo = RaceLogo.objects.get(id=logoid) + except RaceLogo.DoesNotExist: + raise Http404("Logo doesn't exist") + + if logo.user == request.user and race.manager == request.user: + otherlogos = race.logos.all() + for otherlogo in otherlogos: + otherlogo.race.remove(race) + + + logo.race.add(race) + logo.save() + else: + message = "You do not own this race or this image" + messages.error(request,message) + + url = reverse(virtualevent_view, + kwargs={'id':id}) + + return HttpResponseRedirect(url) + +# Image upload to virtual event +@login_required() +def virtualevent_uploadimage_view(request,id=0): + is_ajax = False + if request.is_ajax(): + is_ajax = True + + r = getrower(request.user) + + try: + race = VirtualRace.objects.get(id=id) + except VirtualRace.DoesNotExist: + raise Http404("Race doesn't exist") + + logos = RaceLogo.objects.filter(user=request.user).order_by("-creationdatetime") + + breadcrumbs = [ + { + 'url': reverse(virtualevents_view), + 'name': 'Racing' + }, + { + 'url': reverse(virtualevent_view,kwargs={'id':id}), + 'name': race.name + }, + { + 'url': reverse(virtualevent_uploadimage_view, + kwargs={'id':id}), + 'name': 'Add Image' + } + ] + + if request.method == 'POST': + if len(logos) >= 6: + messages.error(request,"You cannot have more than 6 logos") + url = reverse(virtualevent_imageupload_view, + kwargs={'id':id}) + + return HttpResponseRedirect(url) + + form = ImageForm(request.POST, request.FILES) + if form.is_valid(): + f = form.cleaned_data['file'] + + if f is not None: + filename, path_and_filename = handle_uploaded_image(f) + try: + width, height = Image.open(path_and_filename).size + except: + message = "Not a valid image" + messages.error(request,message) + os.remove(path_and_filename) + url = reverse(virtualevent_image_view, + kwargs={'id':id}) + + if is_ajax: + return JSONResponse({'result':0, 'url':0}) + else: + return HttpResponseRedirect(url) + + otherlogos = race.logos.all() + for logo in otherlogos: + logo.race.remove(race) + + logo = RaceLogo(user = request.user, + creationdatetime=timezone.now(), + filename = path_and_filename, + width=width, height=height) + logo.save() + logo.race.add(race) + logo.save() + + + url = reverse(virtualevent_view, + kwargs={'id':id}) + + if is_ajax: + return JSONResponse({'result':1, 'url':url}) + else: + return HttpResponseRedirect(url) + else: + messages.error(request,"Something went wrong - no file attached") + url = reverse(virtualevent_uploadimage_view, + kwargs = {'id':id}) + + if is_ajax: + return JSONResponse({'result':0,'url':1}) + else: + return HttpResponseRedirect(url) + else: + + form = ImageForm() + + + if is_ajax: + return {'result':0} + + + return render(request,'logo_form.html', + {'form':form, + 'rower':r, + 'logos':logos, + 'active':'nav-racing', + 'breadcrumbs':breadcrumbs, + 'race':race, + }) + + +# Image upload +@login_required() +def course_upload_view(request): + is_ajax = False + if request.is_ajax(): + is_ajax = True + + r = getrower(request.user) + + if request.method == 'POST': + form = CourseForm(request.POST,request.FILES) + + if form.is_valid(): + f = form.cleaned_data['file'] + name = form.cleaned_data['name'] + notes = form.cleaned_data['notes'] + if f is not None: + filename,path_and_filename = handle_uploaded_file(f) + + cs = courses.kmltocourse(path_and_filename) + + for course in cs: + cname = name+' - '+course['name'] + cnotes = notes+'\n\n'+course['description'] + polygons = course['polygons'] + + course = courses.createcourse(r,cname,polygons,notes=cnotes) + + os.remove(path_and_filename) + + url = reverse(courses_view) + if is_ajax: + return JSONResponse({'result':1,'url':url}) + else: + return HttpResponseRedirect(url) + else: + messages.error(request,'Something went wrong - no file attached') + url = reverse(course_upload_view) + + if is_ajax: + return JSONResponse({'result':0,'url':0}) + else: + return HttpResponseRedirect(url) + else: + messages.error(request,'Form is not valid') + return render(request,'course_form.html', + {'form':form, + }) + + else: + if not is_ajax: + form = CourseForm() + return render(request,'course_form.html', + {'form':form, + 'active':'nav-racing', + }) + else: + return {'result':0} + + +def virtualevents_view(request): + is_ajax = False + if request.is_ajax(): + is_ajax = True + + # default races + races1 = VirtualRace.objects.filter( + startdate__gte=datetime.date.today(), + ) + races2 = VirtualRace.objects.filter( + startdate__lte=datetime.date.today(), + evaluation_closure__gte=timezone.now()-datetime.timedelta(days=3), + ) + + + races = (races1 | races2).order_by("startdate","start_time") + + if not request.user.is_anonymous(): + r = getrower(request.user) + else: + r = None + + if request.method == 'POST': + # process form + form = VirtualRaceSelectForm(request.POST) + if form.is_valid(): + cd = form.cleaned_data + country = cd['country'] + regattatype = cd['regattatype'] + if country == 'All': + countries = VirtualRace.objects.order_by('country').values_list('country').distinct() + else: + countries = [country, + 'Indoor'] + + if regattatype == 'upcoming': + races1 = VirtualRace.objects.filter( + startdate__gte=datetime.date.today(), + country__in=countries + ) + races2 = VirtualRace.objects.filter( + startdate__lte=datetime.date.today(), + evaluation_closure__gte=timezone.now(), + country__in=countries + ) + + + races = (races1 | races2).order_by("startdate","start_time") + + + elif regattatype == 'previous': + races = VirtualRace.objects.filter( + evaluation_closure__lt=timezone.now(), + country__in=countries + ).order_by("-startdate","-start_time") + elif regattatype == 'ongoing': + races = VirtualRace.objects.filter( + startdate__lte=datetime.date.today(), + evaluation_closure__gte=timezone.now(), + country__in=countries + ).order_by("startdate","start_time") + elif regattatype == 'my': + mysessions = get_my_session_ids(r) + races = VirtualRace.objects.filter( + id__in=mysessions, + country__in=countries + ).order_by("startdate","start_time") + elif regattatype == 'all': + races = VirtualRace.objects.filter( + country__in=countries + ).order_by("startdate","start_time") + else: + + form = VirtualRaceSelectForm() + + if is_ajax: + return render(request,'racelist.html', + { 'races':races, + 'rower':r, + }) + + breadcrumbs = [ + { + 'url':reverse(virtualevents_view), + 'name': 'Racing' + }, + ] + + return render(request,'virtualevents.html', + { 'races':races, + 'form':form, + 'breadcrumbs':breadcrumbs, + 'active':'nav-racing', + 'rower':r, + } + ) + +@login_required() +def virtualevent_disqualify_view(request,raceid=0,recordid=0): + + r = getrower(request.user) + + try: + race = VirtualRace.objects.get(id=raceid) + except VirtualRace.DoesNotExist: + raise Http404("Virtual Race does not exist") + + if r.user != race.manager: + raise PermissionDenied("Access denied") + + if race.sessiontype == 'race': + recordobj = VirtualRaceResult + else: + recordobj = IndoorVirtualRaceResult + + # datum moet voor race evaluation date zijn (ook in template controleren) + + try: + record = recordobj.objects.get(id=recordid) + except recordobj.DoesNotExist: + messages.error(request,"We couldn't find the record") + + if timezone.now() > race.evaluation_closure+datetime.timedelta(hours=1): + messages.error(request,"The evaluation is already closed and the results are official") + url = reverse(virtualevent_view,kwargs={'id':raceid}) + + return HttpResponseRedirect(url) + + if request.method == 'POST': + form = DisqualificationForm(request.POST) + if form.is_valid(): + message = form.cleaned_data['message'] + reason = form.cleaned_data['reason'] + disqualifier = disqualifiers[reason] + + r = Rower.objects.get(id=record.userid) + name = record.username + + job = myqueue(queue,handle_send_disqualification_email, + r.user.email, name, + disqualifier,message,race.name) + + messages.info(request,"We have invalidated the result for: "+str(record)) + + record.coursecompleted = False + record.save() + + url = reverse(virtualevent_view,kwargs={'id':raceid}) + + return HttpResponseRedirect(url) + + else: + form = DisqualificationForm(request.POST) + + workout = Workout.objects.get(id=record.workoutid) + + g = GraphImage.objects.filter(workout=workout).order_by("-creationdatetime") + for i in g: + try: + width,height = Image.open(i.filename).size + i.width = width + i.height = height + i.save() + except: + pass + + script, div = interactive_chart(record.workoutid) + + f1 = workout.csvfilename + rowdata = rdata(f1) + hascoordinates = 1 + if rowdata != 0: + try: + latitude = rowdata.df[' latitude'] + if not latitude.std(): + hascoordinates = 0 + except KeyError, AttributeError: + hascoordinates = 0 + else: + hascoordinates = 0 + + if hascoordinates: + mapscript, mapdiv = leaflet_chart(rowdata.df[' latitude'], + rowdata.df[' longitude'], + workout.name) + else: + mapscript = "" + mapdiv = "" + + breadcrumbs = [ + { + 'url':reverse(virtualevents_view), + 'name': 'Racing' + }, + { + 'url':reverse(virtualevent_view, + kwargs={'id':race.id}), + 'name': race.name + }, + { + 'url':reverse(virtualevent_disqualify_view, + kwargs={'raceid':raceid, + 'recordid':recordid}), + 'name': 'Disqualify Entry' + }, + ] + + buttons = [] + + if not request.user.is_anonymous(): + if race_can_register(r,race): + buttons += ['registerbutton'] + + if race_can_adddiscipline(r,race): + buttons += ['adddisciplinebutton'] + + if race_can_submit(r,race): + buttons += ['submitbutton'] + + if race_can_resubmit(r,race): + buttons += ['resubmitbutton'] + + if race_can_withdraw(r,race): + buttons += ['withdrawbutton'] + + if race_can_edit(r,race): + buttons += ['editbutton'] + + return render(request,"disqualification_view.html", + {'workout':workout, + 'active':'nav-racing', + 'graphs':g, + 'buttons':buttons, + 'interactiveplot':script, + 'the_div':div, + 'mapscript':mapscript, + 'mapdiv':mapdiv, + 'form':form, + 'race':race, + 'record':record, + }) + +def virtualevent_view(request,id=0): + + results = [] + + if not request.user.is_anonymous(): + r = getrower(request.user) + else: + r = None + + + try: + race = VirtualRace.objects.get(id=id) + except VirtualRace.DoesNotExist: + raise Http404("Virtual Race does not exist") + + if race.sessiontype == 'race': + script,div = course_map(race.course) + resultobj = VirtualRaceResult + else: + script = '' + div = '' + resultobj = IndoorVirtualRaceResult + + records = resultobj.objects.filter(race=race) + + + buttons = [] + + # to-do - add DNS + dns = [] + if timezone.now() > race.evaluation_closure: + dns = resultobj.objects.filter( + race=race, + workoutid__isnull=True, + ) + + + if not request.user.is_anonymous(): + if race_can_register(r,race): + buttons += ['registerbutton'] + + if race_can_adddiscipline(r,race): + buttons += ['adddisciplinebutton'] + + if race_can_submit(r,race): + buttons += ['submitbutton'] + + if race_can_resubmit(r,race): + buttons += ['resubmitbutton'] + + if race_can_withdraw(r,race): + buttons += ['withdrawbutton'] + + if race_can_edit(r,race): + buttons += ['editbutton'] + + if request.method == 'POST': + form = RaceResultFilterForm(request.POST,records=records) + if form.is_valid(): + cd = form.cleaned_data + try: + sex = cd['sex'] + except KeyError: + sex = ['female','male','mixed'] + + try: + boattype = cd['boattype'] + except KeyError: + boattype = mytypes.waterboattype + + try: + boatclass = cd['boatclass'] + except KeyError: + if race.sessiontype == 'race': + boatclass = [t for t in mytypes.otwtypes] + else: + boatclass = [t for t in mytypes.otetypes] + + age_min = cd['age_min'] + age_max = cd['age_max'] + + try: + weightcategory = cd['weightcategory'] + except KeyError: + weightcategory = ['hwt','lwt'] + + try: + adaptiveclass = cd['adaptiveclass'] + except KeyError: + adaptiveclass = ['None','PR1','PR2','PR3','FES'] + + if race.sessiontype == 'race': + results = resultobj.objects.filter( + race=race, + workoutid__isnull=False, + boatclass__in=boatclass, + boattype__in=boattype, + sex__in=sex, + weightcategory__in=weightcategory, + adaptiveclass__in=adaptiveclass, + age__gte=age_min, + age__lte=age_max + ).order_by("duration") + else: + results = resultobj.objects.filter( + race=race, + workoutid__isnull=False, + boatclass__in=boatclass, + sex__in=sex, + weightcategory__in=weightcategory, + adaptiveclass__in=adaptiveclass, + age__gte=age_min, + age__lte=age_max + ).order_by("duration","-distance") + + + # to-do - add DNS + dns = [] + if timezone.now() > race.evaluation_closure: + dns = resultobj.objects.filter( + race=race, + workoutid__isnull=True, + boatclass__in=boatclass, + sex__in=sex, + weightcategory__in=weightcategory, + adaptiveclass__in=adaptiveclass, + age__gte=age_min, + age__lte=age_max + ) + else: + results = resultobj.objects.filter( + race=race, + workoutid__isnull=False, + coursecompleted=True, + ).order_by("duration","-distance") + + if results: + form = RaceResultFilterForm(records=records) + else: + form = None + + + + breadcrumbs = [ + { + 'url':reverse(virtualevents_view), + 'name': 'Racing' + }, + { + 'url':reverse(virtualevent_view, + kwargs={'id':race.id} + ), + 'name': race.name + } + ] + + racelogos = race.logos.all() + + if racelogos: + racelogo = racelogos[0] + else: + racelogo = None + + return render(request,'virtualevent.html', + { + 'coursescript':script, + 'coursediv':div, + 'breadcrumbs':breadcrumbs, + 'race':race, + 'rower':r, + 'results':results, + 'buttons':buttons, + 'dns':dns, + 'records':records, + 'racelogo':racelogo, + 'form':form, + 'active':'nav-racing', + }) + +def virtualevent_ranking_view(request,id=0): + + results = [] + + if not request.user.is_anonymous(): + r = getrower(request.user) + else: + r = None + + + try: + race = VirtualRace.objects.get(id=id) + except VirtualRace.DoesNotExist: + raise Http404("Virtual Race does not exist") + + if race.sessiontype == 'race': + script,div = course_map(race.course) + resultobj = VirtualRaceResult + else: + script = '' + div = '' + resultobj = IndoorVirtualRaceResult + + records = resultobj.objects.filter(race=race) + + + buttons = [] + + # to-do - add DNS + dns = [] + if timezone.now() > race.evaluation_closure: + dns = resultobj.objects.filter( + race=race, + workoutid__isnull=True, + ) + + + if not request.user.is_anonymous(): + if race_can_register(r,race): + buttons += ['registerbutton'] + + if race_can_adddiscipline(r,race): + buttons += ['adddisciplinebutton'] + + if race_can_submit(r,race): + buttons += ['submitbutton'] + + if race_can_resubmit(r,race): + buttons += ['resubmitbutton'] + + if race_can_withdraw(r,race): + buttons += ['withdrawbutton'] + + if race_can_edit(r,race): + buttons += ['editbutton'] + + if request.method == 'POST': + form = RaceResultFilterForm(request.POST,records=records) + if form.is_valid(): + cd = form.cleaned_data + try: + sex = cd['sex'] + except KeyError: + sex = ['female','male','mixed'] + + try: + boattype = cd['boattype'] + except KeyError: + boattype = mytypes.waterboattype + + try: + boatclass = cd['boatclass'] + except KeyError: + if race.sessiontype == 'race': + boatclass = [t for t in mytypes.otwtypes] + else: + boatclass = [t for t in mytypes.otetypes] + + age_min = cd['age_min'] + age_max = cd['age_max'] + + try: + weightcategory = cd['weightcategory'] + except KeyError: + weightcategory = ['hwt','lwt'] + + try: + adaptiveclass = cd['adaptiveclass'] + except KeyError: + adaptiveclass = ['None','PR1','PR2','PR3','FES'] + + if race.sessiontype == 'race': + results = resultobj.objects.filter( + race=race, + workoutid__isnull=False, + boatclass__in=boatclass, + boattype__in=boattype, + sex__in=sex, + weightcategory__in=weightcategory, + adaptiveclass__in=adaptiveclass, + age__gte=age_min, + age__lte=age_max + ).order_by("duration") + else: + results = resultobj.objects.filter( + race=race, + workoutid__isnull=False, + boatclass__in=boatclass, + sex__in=sex, + weightcategory__in=weightcategory, + adaptiveclass__in=adaptiveclass, + age__gte=age_min, + age__lte=age_max + ).order_by("duration","-distance") + + + # to-do - add DNS + dns = [] + if timezone.now() > race.evaluation_closure: + dns = resultobj.objects.filter( + race=race, + workoutid__isnull=True, + boatclass__in=boatclass, + sex__in=sex, + weightcategory__in=weightcategory, + adaptiveclass__in=adaptiveclass, + age__gte=age_min, + age__lte=age_max + ) + else: + results = resultobj.objects.filter( + race=race, + workoutid__isnull=False, + coursecompleted=True, + ).order_by("duration","-distance") + + if results: + form = RaceResultFilterForm(records=records) + else: + form = None + + + + breadcrumbs = [ + { + 'url':reverse(virtualevents_view), + 'name': 'Racing' + }, + { + 'url':reverse(virtualevent_view, + kwargs={'id':race.id} + ), + 'name': race.name + } + ] + + racelogos = race.logos.all() + + if racelogos: + racelogo = racelogos[0] + else: + racelogo = None + + return render(request,'virtualeventranking.html', + { + 'coursescript':script, + 'coursediv':div, + 'breadcrumbs':breadcrumbs, + 'race':race, + 'rower':r, + 'results':results, + 'buttons':buttons, + 'dns':dns, + 'records':records, + 'racelogo':racelogo, + 'form':form, + 'active':'nav-racing', + }) + + +@login_required() +def virtualevent_withdraw_view(request,id=0,recordid=None): + r = getrower(request.user) + + try: + race = VirtualRace.objects.get(id=id) + except VirtualRace.DoesNotExist: + raise Http404("Virtual Race does not exist") + + if race_can_withdraw(r,race): + remove_rower_race(r,race,recordid=recordid) + messages.info(request, + "You have successfully withdrawn from this race.") + else: + messages.error(request,"You cannot withdraw from this race") + + url = reverse(virtualevent_view, + kwargs = { + 'id':race.id + }) + + return HttpResponseRedirect(url) + +@login_required() +def virtualevent_addboat_view(request,id=0): + r = getrower(request.user) + try: + race = VirtualRace.objects.get(id=id) + except VirtualRace.DoesNotExist: + raise Http404("Virtual Race does not exist") + + if not race_can_adddiscipline(r,race): + messages.error(request,"You cannot register for this race") + + url = reverse(virtualevent_view, + kwargs = { + 'id':race.id + }) + + return HttpResponseRedirect(url) + + records = VirtualRaceResult.objects.filter( + userid = r.id, + race = race + ) + + boattypes = [record.boattype for record in records] + boatclasses = [record.boatclass for record in records] + allowedboats = tuple([ type for type in mytypes.boattypes if type[0] not in boattypes] ) + + + # we're still here + if request.method == 'POST': + # process form + form = VirtualRaceResultForm(request.POST) + if form.is_valid(): + cd = form.cleaned_data + teamname = cd['teamname'] + boattype = cd['boattype'] + boatclass = cd['boatclass'] + weightcategory = cd['weightcategory'] + adaptiveclass = cd['adaptiveclass'] + age = cd['age'] + mix = cd['mix'] + + sex = r.sex + if mix: + sex = 'mixed' + + if boattype == '1x' and r.birthdate: + age = calculate_age(r.birthdate) + sex = r.sex + + if sex == 'not specified': + sex = 'male' + + if boattype in boattypes and boatclass in boatclasses: + # check if different sexes + therecords = records.filter( + boattype=boattype, + boatclass=boatclass) + + thesexes = [record.sex for record in therecords] + if sex in thesexes: + + messages.error( + request, + "You have already registered in that boat class/type" + ) + url = reverse(virtualevent_view, + kwargs = { + 'id': race.id + } + ) + + return HttpResponseRedirect(url) + + record = VirtualRaceResult( + userid=r.id, + teamname=teamname, + race=race, + username = u'{f} {l}'.format( + f = r.user.first_name, + l = r.user.last_name + ), + weightcategory=weightcategory, + adaptiveclass=adaptiveclass, + duration=datetime.time(0,0), + boattype=boattype, + boatclass=boatclass, + coursecompleted=False, + sex=sex, + age=age + ) + + record.save() + + add_rower_race(r,race) + + + + messages.info( + request, + "You have successfully registered for this race. Good luck!" + ) + + url = reverse(virtualevent_view, + kwargs = { + 'id':race.id + }) + + return HttpResponseRedirect(url) + + else: + initial = { + 'age': calculate_age(r.birthdate), + 'weightcategory': r.weightcategory, + 'adaptiveclass': r.adaptiveclass, + } + + form = VirtualRaceResultForm(initial=initial) + + breadcrumbs = [ + { + 'url':reverse(virtualevents_view), + 'name': 'Racing' + }, + { + 'url':reverse(virtualevent_view, + kwargs={'id':race.id} + ), + 'name': race.name + }, + { + 'url': reverse(virtualevent_addboat_view, + kwargs = {'id':race.id} + ), + 'name': 'Add Discipline' + } + ] + + + buttons = [] + + if not request.user.is_anonymous(): + if race_can_register(r,race): + buttons += ['registerbutton'] + + if race_can_adddiscipline(r,race): + buttons += ['adddisciplinebutton'] + + if race_can_submit(r,race): + buttons += ['submitbutton'] + + if race_can_resubmit(r,race): + buttons += ['resubmitbutton'] + + if race_can_withdraw(r,race): + buttons += ['withdrawbutton'] + + if race_can_edit(r,race): + buttons += ['editbutton'] + + return render(request,'virtualeventregister.html', + { + 'form':form, + 'buttons':buttons, + 'breadcrumbs':breadcrumbs, + 'race':race, + 'userid':r.user.id, + 'active': 'nav-racing', + }) + +@login_required() +def virtualevent_register_view(request,id=0): + r = getrower(request.user) + try: + race = VirtualRace.objects.get(id=id) + except VirtualRace.DoesNotExist: + raise Http404("Virtual Race does not exist") + + if not race_can_register(r,race): + messages.error(request,"You cannot register for this race") + + url = reverse(virtualevent_view, + kwargs = { + 'id':race.id + }) + + return HttpResponseRedirect(url) + + # we're still here + if request.method == 'POST': + # process form + form = VirtualRaceResultForm(request.POST) + if form.is_valid(): + cd = form.cleaned_data + teamname = cd['teamname'] + boattype = cd['boattype'] + boatclass = cd['boatclass'] + weightcategory = cd['weightcategory'] + adaptiveclass = cd['adaptiveclass'] + age = cd['age'] + mix = cd['mix'] + + sex = r.sex + if mix: + sex = 'mixed' + + if boattype == '1x' and r.birthdate: + age = calculate_age(r.birthdate) + sex = r.sex + + if sex == 'not specified': + sex = 'male' + + record = VirtualRaceResult( + userid=r.id, + teamname=teamname, + race=race, + username = u'{f} {l}'.format( + f = r.user.first_name, + l = r.user.last_name + ), + weightcategory=weightcategory, + adaptiveclass=adaptiveclass, + duration=datetime.time(0,0), + boatclass=boatclass, + boattype=boattype, + coursecompleted=False, + sex=sex, + age=age + ) + + record.save() + + add_rower_race(r,race) + + otherrecords = IndoorVirtualRaceResult.objects.filter( + race = race).exclude(userid = r.id) + + for otherrecord in otherrecords: + otheruser = Rower.objects.get(id=otherrecord.userid) + othername = otheruser.user.first_name+' '+otheruser.user.last_name + registeredname = r.user.first_name+' '+r.user.last_name + if otherrecord.emailnotifications: + job = myqueue( + queue, + handle_sendemail_raceregistration, + otheruser.user.email, othername, + registeredname, + race.name, + race.id + ) + + + messages.info( + request, + "You have successfully registered for this race. Good luck!" + ) + + url = reverse(virtualevent_view, + kwargs = { + 'id':race.id + }) + + return HttpResponseRedirect(url) + + else: + initial = { + 'age': calculate_age(r.birthdate), + 'weightcategory': r.weightcategory, + 'adaptiveclass': r.adaptiveclass, + } + + form = VirtualRaceResultForm(initial=initial) + + breadcrumbs = [ + { + 'url':reverse(virtualevents_view), + 'name': 'Racing' + }, + { + 'url':reverse(virtualevent_view, + kwargs={'id':race.id} + ), + 'name': race.name + }, + { + 'url': reverse(virtualevent_register_view, + kwargs = {'id':race.id} + ), + 'name': 'Register' + } + ] + + buttons = [] + + if not request.user.is_anonymous(): + if race_can_register(r,race): + buttons += ['registerbutton'] + + if race_can_adddiscipline(r,race): + buttons += ['adddisciplinebutton'] + + if race_can_submit(r,race): + buttons += ['submitbutton'] + + if race_can_resubmit(r,race): + buttons += ['resubmitbutton'] + + if race_can_withdraw(r,race): + buttons += ['withdrawbutton'] + + if race_can_edit(r,race): + buttons += ['editbutton'] + + return render(request,'virtualeventregister.html', + { + 'form':form, + 'buttons':buttons, + 'breadcrumbs':breadcrumbs, + 'race':race, + 'userid':r.user.id, + + }) + +@login_required() +def virtualevent_toggle_email_view(request,id=0): + r = getrower(request.user) + race = VirtualRace.objects.get(id=id) + records = VirtualRaceResult.objects.filter(userid=r.id,race=race) + + if True in [record.emailnotifications for record in records]: + newsetting = False + else: + newsetting = True + + for record in records: + record.emailnotifications = newsetting + record.save() + + url = reverse(virtualevent_view, + kwargs={'id':record.race.id}) + + return HttpResponseRedirect(url) + +@login_required() +def indoorvirtualevent_toggle_email_view(request,id=0): + r = getrower(request.user) + race = VirtualRace.objects.get(id=id) + + records = IndoorVirtualRaceResult.objects.filter(userid=r.id, + race=race) + + if True in [record.emailnotifications for record in records]: + newsetting = False + else: + newsetting = True + + for record in records: + record.emailnotifications = newsetting + record.save() + + url = reverse(virtualevent_view, + kwargs={'id':record.race.id}) + + return HttpResponseRedirect(url) + +@login_required() +def indoorvirtualevent_register_view(request,id=0): + r = getrower(request.user) + try: + race = VirtualRace.objects.get(id=id) + except VirtualRace.DoesNotExist: + raise Http404("Virtual Race does not exist") + + if not race_can_register(r,race): + messages.error(request,"You cannot register for this race") + + url = reverse(virtualevent_view, + kwargs = { + 'id':race.id + }) + + return HttpResponseRedirect(url) + + # we're still here + if request.method == 'POST': + # process form + form = IndoorVirtualRaceResultForm(request.POST) + if form.is_valid(): + cd = form.cleaned_data + teamname = cd['teamname'] + weightcategory = cd['weightcategory'] + adaptiveclass = cd['adaptiveclass'] + age = cd['age'] + boatclass = cd['boatclass'] + + sex = r.sex + + if r.birthdate: + age = calculate_age(r.birthdate) + sex = r.sex + + if sex == 'not specified': + sex = 'male' + + record = IndoorVirtualRaceResult( + userid=r.id, + teamname=teamname, + race=race, + username = u'{f} {l}'.format( + f = r.user.first_name, + l = r.user.last_name + ), + weightcategory=weightcategory, + adaptiveclass=adaptiveclass, + duration=datetime.time(0,0), + boatclass=boatclass, + coursecompleted=False, + sex=sex, + age=age + ) + + record.save() + + add_rower_race(r,race) + + otherrecords = IndoorVirtualRaceResult.objects.filter( + race = race).exclude(userid = r.id) + + for otherrecord in otherrecords: + otheruser = Rower.objects.get(id=otherrecord.userid) + othername = otheruser.user.first_name+' '+otheruser.user.last_name + registeredname = r.user.first_name+' '+r.user.last_name + if otherrecord.emailnotifications: + job = myqueue( + queue, + handle_sendemail_raceregistration, + otheruser.user.email, othername, + registeredname, + race.name, + race.id + ) + + + messages.info( + request, + "You have successfully registered for this race. Good luck!" + ) + + url = reverse(virtualevent_view, + kwargs = { + 'id':race.id + }) + + return HttpResponseRedirect(url) + + else: + initial = { + 'age': calculate_age(r.birthdate), + 'weightcategory': r.weightcategory, + 'adaptiveclass': r.adaptiveclass, + } + + form = IndoorVirtualRaceResultForm(initial=initial) + + breadcrumbs = [ + { + 'url':reverse(virtualevents_view), + 'name': 'Racing' + }, + { + 'url':reverse(virtualevent_view, + kwargs={'id':race.id} + ), + 'name': race.name + }, + { + 'url': reverse(indoorvirtualevent_register_view, + kwargs = {'id':race.id} + ), + 'name': 'Register' + } + ] + + buttons = [] + + if not request.user.is_anonymous(): + if race_can_register(r,race): + buttons += ['registerbutton'] + + if race_can_adddiscipline(r,race): + buttons += ['adddisciplinebutton'] + + if race_can_submit(r,race): + buttons += ['submitbutton'] + + if race_can_resubmit(r,race): + buttons += ['resubmitbutton'] + + if race_can_withdraw(r,race): + buttons += ['withdrawbutton'] + + if race_can_edit(r,race): + buttons += ['editbutton'] + + return render(request,'virtualeventregister.html', + { + 'form':form, + 'buttons':buttons, + 'race':race, + 'breadcrumbs':breadcrumbs, + 'userid':r.user.id, + + }) + +@login_required() +def indoorvirtualevent_create_view(request): + r = getrower(request.user) + + if request.method == 'POST': + racecreateform = IndoorVirtualRaceForm(request.POST) + if racecreateform.is_valid(): + cd = racecreateform.cleaned_data + startdate = cd['startdate'] + start_time = cd['start_time'] + enddate = cd['enddate'] + end_time = cd['end_time'] + comment = cd['comment'] + sessionunit = cd['sessionunit'] + sessionvalue = cd['sessionvalue'] + name = cd['name'] + registration_form = cd['registration_form'] + registration_closure = cd['registration_closure'] + evaluation_closure = cd['evaluation_closure'] + contact_phone = cd['contact_phone'] + contact_email = cd['contact_email'] + + # correct times + + timezone_str = cd['timezone'] + + startdatetime = datetime.datetime.combine(startdate,start_time) + enddatetime = datetime.datetime.combine(enddate,end_time) + + + startdatetime = pytz.timezone(timezone_str).localize( + startdatetime + ) + enddatetime = pytz.timezone(timezone_str).localize( + enddatetime + ) + evaluation_closure = pytz.timezone(timezone_str).localize( + evaluation_closure.replace(tzinfo=None) + ) + + if registration_form == 'manual': + try: + registration_closure = pytz.timezone( + timezone_str + ).localize( + registration_closure.replace(tzinfo=None) + ) + except AttributeError: + registration_closure = startdatetime + elif registration_form == 'windowstart': + registration_closure = startdatetime + elif registration_form == 'windowend': + registration_closure = enddatetime + else: + registration_closure = evaluation_closure + + if sessionunit == 'min': + sessionmode = 'time' + else: + sessionmode = 'distance' + + vs = VirtualRace( + name=name, + startdate=startdate, + preferreddate = startdate, + start_time = start_time, + enddate=enddate, + end_time=end_time, + comment=comment, + sessiontype = 'indoorrace', + sessionunit = sessionunit, + sessionmode = sessionmode, + sessionvalue = sessionvalue, + course=None, + timezone=timezone_str, + evaluation_closure=evaluation_closure, + registration_closure=registration_closure, + contact_phone=contact_phone, + contact_email=contact_email, + country = 'Indoor', + manager=request.user, + ) + + vs.save() + + # create Site Announcement & Tweet + if settings.DEBUG: + dotweet = False + elif 'dev' in settings.SITE_URL: + dotweet = False + else: + dotweet = True + try: + sa = SiteAnnouncement( + announcement = "New Virtual Indoor Race on rowsandall.com: {name}".format( + name = name.encode('utf8'), + ), + dotweet = dotweet + ) + + sa.save() + except UnicodeEncodeError: + sa = SiteAnnouncement( + announcement = "New Virtual Indoor Race on rowsandall.com: {name}".format( + name = name, + ), + dotweet = dotweet + ) + + + sa.save() + + url = reverse(virtualevents_view) + return HttpResponseRedirect(url) + else: + + racecreateform = IndoorVirtualRaceForm(timezone=r.defaulttimezone) + + + breadcrumbs = [ + { + 'url':reverse(virtualevents_view), + 'name': 'Racing' + }, + { + 'url':reverse(indoorvirtualevent_create_view, + ), + 'name': 'New Indoor Virtual Regatta' + }, + ] + + return render(request,'indoorvirtualeventcreate.html', + { + 'form':racecreateform, + 'breadcrumbs':breadcrumbs, + 'rower':r, + 'active':'nav-racing', + + }) + +@login_required() +def virtualevent_create_view(request): + r = getrower(request.user) + + if request.method == 'POST': + racecreateform = VirtualRaceForm(request.POST) + if racecreateform.is_valid(): + cd = racecreateform.cleaned_data + startdate = cd['startdate'] + start_time = cd['start_time'] + enddate = cd['enddate'] + end_time = cd['end_time'] + comment = cd['comment'] + course = cd['course'] + name = cd['name'] + registration_form = cd['registration_form'] + registration_closure = cd['registration_closure'] + evaluation_closure = cd['evaluation_closure'] + contact_phone = cd['contact_phone'] + contact_email = cd['contact_email'] + + # correct times + + geocourse = GeoCourse.objects.get(id= course.id) + timezone_str = get_course_timezone(geocourse) + + startdatetime = datetime.datetime.combine(startdate,start_time) + enddatetime = datetime.datetime.combine(enddate,end_time) + + + startdatetime = pytz.timezone(timezone_str).localize( + startdatetime + ) + enddatetime = pytz.timezone(timezone_str).localize( + enddatetime + ) + evaluation_closure = pytz.timezone(timezone_str).localize( + evaluation_closure.replace(tzinfo=None) + ) + + if registration_form == 'manual': + try: + registration_closure = pytz.timezone( + timezone_str + ).localize( + registration_closure.replace(tzinfo=None) + ) + except AttributeError: + registration_closure = startdatetime + elif registration_form == 'windowstart': + registration_closure = startdatetime + elif registration_form == 'windowend': + registration_closure = enddatetime + else: + registration_closure = evaluation_closure + + + vs = VirtualRace( + name=name, + startdate=startdate, + preferreddate = startdate, + start_time = start_time, + enddate=enddate, + end_time=end_time, + course=geocourse, + comment=comment, + sessiontype = 'race', + timezone=timezone_str, + evaluation_closure=evaluation_closure, + registration_closure=registration_closure, + contact_phone=contact_phone, + contact_email=contact_email, + country = course.country, + manager=request.user, + ) + + vs.save() + + # create Site Announcement & Tweet + if settings.DEBUG: + dotweet = False + elif 'dev' in settings.SITE_URL: + dotweet = False + else: + dotweet = True + try: + sa = SiteAnnouncement( + announcement = "New Virtual Race on rowsandall.com: {name} on course {course}".format( + name = name.encode('utf8'), + course = course.name.encode('utf8') + ), + dotweet = dotweet + ) + + sa.save() + except UnicodeEncodeError: + sa = SiteAnnouncement( + announcement = "New Virtual Race on rowsandall.com: {name} on course {course}".format( + name = name, + course = str(course.name.encode('utf8','ignore')) + ), + dotweet = dotweet + ) + + + sa.save() + + url = reverse(virtualevents_view) + return HttpResponseRedirect(url) + else: + + racecreateform = VirtualRaceForm() + + + breadcrumbs = [ + { + 'url':reverse(virtualevents_view), + 'name': 'Racing' + }, + { + 'url':reverse(virtualevent_create_view, + ), + 'name': 'New Virtual Regatta' + }, + ] + return render(request,'virtualeventcreate.html', + { + 'form':racecreateform, + 'breadcrumbs':breadcrumbs, + 'rower':r, + 'active':'nav-racing', + + }) + +@login_required() +def virtualevent_edit_view(request,id=0): + r = getrower(request.user) + + try: + race = VirtualRace.objects.get(id=id) + if race.manager != request.user: + raise PermissionDenied("Access denied") + except VirtualRace.DoesNotExist: + raise Http404("Virtual Race does not exist") + + start_time = race.start_time + start_date = race.startdate + startdatetime = datetime.datetime.combine(start_date,start_time) + startdatetime = pytz.timezone(race.timezone).localize( + startdatetime + ) + + if timezone.now() > startdatetime: + messages.error(request,"You cannot edit a race after the start of the race window") + url = reverse(virtualevent_view, + kwargs={ + 'id':race.id, + }) + + if request.method == 'POST': + racecreateform = VirtualRaceForm(request.POST,instance=race) + if racecreateform.is_valid(): + cd = racecreateform.cleaned_data + + res, message = update_virtualrace(race,cd) + + if res: + messages.info(request,message) + else: + messages.error(request,message) + + url = reverse(virtualevent_view, + kwargs = { + 'id':race.id + }) + + return HttpResponseRedirect(url) + + else: + + racecreateform = VirtualRaceForm(instance=race) + + breadcrumbs = [ + { + 'url':reverse(virtualevents_view), + 'name': 'Racing' + }, + { + 'url':reverse(virtualevent_view, + kwargs={'id':race.id} + ), + 'name': race.name + }, + { + 'url': reverse(virtualevent_edit_view, + kwargs = {'id':race.id} + ), + 'name': 'Edit' + } + ] + + buttons = [] + + if not request.user.is_anonymous(): + if race_can_register(r,race): + buttons += ['registerbutton'] + + if race_can_adddiscipline(r,race): + buttons += ['adddisciplinebutton'] + + if race_can_submit(r,race): + buttons += ['submitbutton'] + + if race_can_resubmit(r,race): + buttons += ['resubmitbutton'] + + if race_can_withdraw(r,race): + buttons += ['withdrawbutton'] + + if race_can_edit(r,race): + buttons += ['editbutton'] + + return render(request,'virtualeventedit.html', + { + 'form':racecreateform, + 'breadcrumbs':breadcrumbs, + 'buttons':buttons, + 'rower':r, + 'race':race, + + }) + +@login_required() +def indoorvirtualevent_edit_view(request,id=0): + r = getrower(request.user) + + try: + race = VirtualRace.objects.get(id=id) + if race.manager != request.user: + raise PermissionDenied("Access denied") + except VirtualRace.DoesNotExist: + raise Http404("Virtual Race does not exist") + + start_time = race.start_time + start_date = race.startdate + startdatetime = datetime.datetime.combine(start_date,start_time) + startdatetime = pytz.timezone(race.timezone).localize( + startdatetime + ) + + if timezone.now() > startdatetime: + messages.error(request,"You cannot edit a race after the start of the race window") + url = reverse(virtualevent_view, + kwargs={ + 'id':race.id, + }) + + if request.method == 'POST': + racecreateform = IndoorVirtualRaceForm(request.POST,instance=race) + if racecreateform.is_valid(): + cd = racecreateform.cleaned_data + + res, message = update_indoorvirtualrace(race,cd) + + if res: + messages.info(request,message) + else: + messages.error(request,message) + + url = reverse(virtualevent_view, + kwargs = { + 'id':race.id + }) + + return HttpResponseRedirect(url) + + else: + + racecreateform = IndoorVirtualRaceForm(instance=race) + + + breadcrumbs = [ + { + 'url':reverse(virtualevents_view), + 'name': 'Racing' + }, + { + 'url':reverse(virtualevent_view, + kwargs={'id':race.id} + ), + 'name': race.name + }, + { + 'url': reverse(indoorvirtualevent_edit_view, + kwargs = {'id':race.id} + ), + 'name': 'Edit' + } + ] + + buttons = [] + + if not request.user.is_anonymous(): + if race_can_register(r,race): + buttons += ['registerbutton'] + + if race_can_adddiscipline(r,race): + buttons += ['adddisciplinebutton'] + + if race_can_submit(r,race): + buttons += ['submitbutton'] + + if race_can_resubmit(r,race): + buttons += ['resubmitbutton'] + + if race_can_withdraw(r,race): + buttons += ['withdrawbutton'] + + if race_can_edit(r,race): + buttons += ['editbutton'] + + + return render(request,'virtualeventedit.html', + { + 'form':racecreateform, + 'buttons':buttons, + 'breadcrumbs':breadcrumbs, + 'rower':r, + 'race':race, + + }) + + +@login_required() +def virtualevent_submit_result_view(request,id=0,workoutid=0): + + r = getrower(request.user) + + try: + race = VirtualRace.objects.get(id=id) + except VirtualRace.DoesNotExist: + raise Http404("Virtual Race does not exist") + + start_time = race.start_time + start_date = race.startdate + startdatetime = datetime.datetime.combine(start_date, start_time) + startdatetime = pytz.timezone(race.timezone).localize(startdatetime) + + end_time = race.end_time + end_date = race.enddate + enddatetime = datetime.datetime.combine(end_date, end_time) + enddatetime = pytz.timezone(race.timezone).localize(enddatetime) + + can_submit = race_can_submit(r,race) or race_can_resubmit(r,race) + + if race.sessiontype == 'race': + resultobj = VirtualRaceResult + else: + resultobj = IndoorVirtualRaceResult + + records = resultobj.objects.filter( + userid = r.id, + race=race + ) + + + entrychoices = [] + + for record in records: + rtpl = (record.id, record.__unicode__()) + entrychoices.append(rtpl) + + entries = {} + entries['choices'] = entrychoices + entries['initial'] = [records[0].id] + + if not can_submit: + messages.error(request,'You cannot submit a result to this race') + url = reverse(virtualevent_view, + kwargs = { + 'id':id + } + ) + return HttpResponseRedirect(url) + + ws = Workout.objects.filter( + user=r, + workouttype__in=mytypes.rowtypes, + startdatetime__gte=startdatetime, + startdatetime__lte=enddatetime, + ).order_by("date","startdatetime","id") + + if not ws: + messages.info( + request, + 'You have no workouts executed during the race window. Please upload a result or enter it manually.' + ) + + url = reverse(virtualevent_view, + kwargs = { + 'id':id + }) + + return HttpResponseRedirect(url) + + + initialworkouts = [w.id for w in Workout.objects.filter( + user=r,plannedsession=race + )] + + workoutdata = {} + workoutdata['initial'] = [] + + choices = [] + + for w in ws: + wtpl = (w.id, w.__unicode__()) + choices.append(wtpl) + if w.id in initialworkouts: + workoutdata['initial'].append(w.id) + + workoutdata['choices'] = tuple(choices) + + if request.method == 'POST': + w_form = WorkoutRaceSelectForm(workoutdata,entries,request.POST) + + if w_form.is_valid(): + selectedworkout = w_form.cleaned_data['workouts'] + splitsecond = 0 + recordid = w_form.cleaned_data['record'] + else: + selectedworkout = None + + + if selectedworkout is not None: + + + workouts = Workout.objects.filter(id=selectedworkout) + + if race.sessiontype == 'race': + result,comments,errors,jobid = add_workout_race( + workouts,race,r, + splitsecond=splitsecond,recordid=recordid) + else: + result,comments,errors,jobid = add_workout_indoorrace( + workouts,race,r,recordid=recordid) + + + for c in comments: + messages.info(request,c) + for er in errors: + messages.error(request,er) + + if jobid: + try: + request.session['async_tasks'] += [(jobid,'submit_race')] + except KeyError: + request.session['async_tasks'] = [(jobid,'submit_race')] + + messages.info(request,"We are evaluating your result. The page will reload when we're done. Your result will show up if you adhered to the course") + + if result: + otherrecords = resultobj.objects.filter( + race = race).exclude(userid = r.id) + if not jobid: + messages.info(request,"Result submitted successfully.") + + for otherrecord in otherrecords: + otheruser = Rower.objects.get(id=otherrecord.userid) + othername = otheruser.user.first_name+' '+otheruser.user.last_name + registeredname = r.user.first_name+' '+r.user.last_name + if otherrecord.emailnotifications: + job = myqueue( + queue, + handle_sendemail_racesubmission, + otheruser.user.email, othername, + registeredname, + race.name, + race.id + ) + + + # redirect to race page + url = reverse(virtualevent_view, + kwargs = { + 'id':race.id + }) + + return HttpResponseRedirect(url) + + else: + if workoutid: + workoutdata['initial'] = workoutid + w_form = WorkoutRaceSelectForm(workoutdata,entries) + + + + breadcrumbs = [ + { + 'url':reverse(virtualevents_view), + 'name': 'Racing' + }, + { + 'url':reverse(virtualevent_view, + kwargs={'id':race.id} + ), + 'name': race.name + }, + { + 'url': reverse(virtualevent_submit_result_view, + kwargs = {'id':race.id} + ), + 'name': 'Submit Result' + } + ] + + buttons = [] + + if not request.user.is_anonymous(): + if race_can_register(r,race): + buttons += ['registerbutton'] + + if race_can_adddiscipline(r,race): + buttons += ['adddisciplinebutton'] + + if race_can_submit(r,race): + buttons += ['submitbutton'] + + if race_can_resubmit(r,race): + buttons += ['resubmitbutton'] + + if race_can_withdraw(r,race): + buttons += ['withdrawbutton'] + + if race_can_edit(r,race): + buttons += ['editbutton'] + + + return render(request,'race_submit.html', + { + 'race':race, + 'buttons':buttons, + 'workouts':ws, + 'breadcrumbs':breadcrumbs, + 'active':'nav-racing', + 'rower':r, + 'w_form':w_form, + }) diff --git a/rowers/views/statements.py b/rowers/views/statements.py new file mode 100644 index 00000000..c148438a --- /dev/null +++ b/rowers/views/statements.py @@ -0,0 +1,1148 @@ + +import time +import colorsys +import timestring +import zipfile +import bleach +import arrow +import pytz +from pytz import UnknownTimeZoneError +import operator +import warnings +import urllib +import yaml +from PIL import Image +from numbers import Number +from django.views.generic.base import TemplateView +from django.contrib.auth import views as auth_views +from django.db.models import Q +from django import template +from django.db import IntegrityError, transaction +from django.views.decorators.csrf import csrf_exempt +from matplotlib.backends.backend_agg import FigureCanvas +import gc +from pyparsing import ParseException +from uuid import uuid4 +import codecs +import isodate +import re +import cgi +from icalendar import Calendar, Event +import rowers.braintreestuff as braintreestuff +import rowers.payments as payments + +from django.shortcuts import render +from django.template.loader import render_to_string + +from django.views.generic.edit import UpdateView,DeleteView + +from django.http import ( + HttpResponse, HttpResponseRedirect, + JsonResponse, + HttpResponseForbidden, HttpResponseNotAllowed, + HttpResponseNotFound,Http404 + ) +from django.contrib.auth import authenticate, login, logout +from rowers.forms import ( + LoginForm,DocumentsForm,UploadOptionsForm,ImageForm,CourseForm, + TeamUploadOptionsForm,WorkFlowLeftPanelForm,WorkFlowMiddlePanelForm, + WorkFlowLeftPanelElement,WorkFlowMiddlePanelElement, + LandingPageForm,PlannedSessionSelectForm,WorkoutSessionSelectForm, + PlannedSessionTeamForm,PlannedSessionTeamMemberForm, + VirtualRaceSelectForm,WorkoutRaceSelectForm,CourseSelectForm, + RaceResultFilterForm,PowerIntervalUpdateForm,FlexAxesForm, + FlexOptionsForm,DataFrameColumnsForm,OteWorkoutTypeForm, + MetricsForm,DisqualificationForm,disqualificationreasons, + disqualifiers,SearchForm,BillingForm,PlanSelectForm + ) +from django.core.urlresolvers import reverse, reverse_lazy + +from django.core.exceptions import PermissionDenied +from django.template import RequestContext +from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger +from django.conf import settings +from django.utils.datastructures import MultiValueDictKeyError +from django.utils import timezone,translation +from django.core.mail import send_mail, BadHeaderError +from rowers.forms import ( + SummaryStringForm,IntervalUpdateForm,StrokeDataForm, + StatsOptionsForm,PredictedPieceForm,DateRangeForm,DeltaDaysForm, + FitnessMetricForm,PredictedPieceFormNoDistance, + EmailForm, RegistrationForm, RegistrationFormTermsOfService, + RegistrationFormUniqueEmail,RegistrationFormSex, + CNsummaryForm,UpdateWindForm, + UpdateStreamForm,WorkoutMultipleCompareForm,ChartParamChoiceForm, + FusionMetricChoiceForm,BoxPlotChoiceForm,MultiFlexChoiceForm, + TrendFlexModalForm,WorkoutSplitForm,WorkoutJoinParamForm, + PlannedSessionMultipleCloneForm,SessionDateShiftForm, + ) +from rowers.models import ( + Workout, User, Rower, WorkoutForm,FavoriteChart, + PlannedSession, DeactivateUserForm,DeleteUserForm, + TrainingPlan,TrainingPlanForm,TrainingTarget,TrainingTargetForm, + TrainingMacroCycle,TrainingMesoCycle,TrainingMicroCycle, + TrainingTarget,TrainingTargetForm, + TrainingMacroCycleForm,createmacrofillers, + createmicrofillers, createmesofillers, + microcyclecheckdates,mesocyclecheckdates,macrocyclecheckdates, + TrainingMesoCycleForm, TrainingMicroCycleForm, + RaceLogo,RowerBillingAddressForm,PaidPlan, + ) +from rowers.models import ( + RowerPowerForm,RowerForm,GraphImage,AdvancedWorkoutForm, + RowerPowerZonesForm,AccountRowerForm,UserForm,StrokeData, + Team,TeamForm,TeamInviteForm,TeamInvite,TeamRequest, + WorkoutComment,WorkoutCommentForm,RowerExportForm, + CalcAgePerformance,PowerTimeFitnessMetric,PlannedSessionForm, + PlannedSessionFormSmall,GeoCourseEditForm,VirtualRace, + VirtualRaceForm,VirtualRaceResultForm,RowerImportExportForm, + IndoorVirtualRaceResultForm,IndoorVirtualRaceResult, + IndoorVirtualRaceForm, + ) +from rowers.models import ( + FavoriteForm,BaseFavoriteFormSet,SiteAnnouncement,BasePlannedSessionFormSet, + get_course_timezone + ) +from rowers.metrics import rowingmetrics,defaultfavoritecharts +from rowers import metrics +from rowers import courses +import rowers.uploads as uploads +from django.forms.formsets import formset_factory +from django.forms import modelformset_factory + +from django.contrib.auth.decorators import login_required #,user_passes_test +from rowers.decorators import user_passes_test +from time import strftime,strptime,mktime,time,daylight +import os,sys +import datetime +import iso8601 +import c2stuff +from c2stuff import c2_open +from runkeeperstuff import runkeeper_open +from sporttracksstuff import sporttracks_open +from tpstuff import tp_open +from iso8601 import ParseError +import stravastuff +from stravastuff import strava_open +import polarstuff +import sporttracksstuff +import underarmourstuff +from underarmourstuff import underarmour_open +import tpstuff +import runkeeperstuff +import ownapistuff +from ownapistuff import TEST_CLIENT_ID, TEST_CLIENT_SECRET, TEST_REDIRECT_URI +from rowsandall_app.settings import ( + C2_CLIENT_ID, C2_REDIRECT_URI, C2_CLIENT_SECRET, + STRAVA_CLIENT_ID, STRAVA_REDIRECT_URI, STRAVA_CLIENT_SECRET, + POLAR_CLIENT_ID, POLAR_REDIRECT_URI, POLAR_CLIENT_SECRET, + SPORTTRACKS_CLIENT_ID, SPORTTRACKS_REDIRECT_URI, + SPORTTRACKS_CLIENT_SECRET, + UNDERARMOUR_CLIENT_ID, UNDERARMOUR_REDIRECT_URI, + UNDERARMOUR_CLIENT_SECRET,UNDERARMOUR_CLIENT_KEY, + RUNKEEPER_CLIENT_ID,RUNKEEPER_REDIRECT_URI,RUNKEEPER_CLIENT_SECRET, + TP_CLIENT_ID,TP_REDIRECT_URI,TP_CLIENT_KEY,TP_CLIENT_SECRET, + BRAINTREE_MERCHANT_ID,BRAINTREE_PUBLIC_KEY,BRAINTREE_PRIVATE_KEY, + PAYMENT_PROCESSING_ON + ) + +from rowers.tasks_standalone import addcomment2 +from django.contrib import messages +from async_messages import messages as a_messages + +from django.contrib.admin.widgets import AdminDateWidget,AdminTimeWidget,AdminSplitDateTime + + +import requests +import json +from rest_framework.renderers import JSONRenderer +from rest_framework.parsers import JSONParser +from rowers.rows import handle_uploaded_file,handle_uploaded_image +from rowers.plannedsessions import * +from rowers.tasks import handle_makeplot,handle_otwsetpower,handle_sendemailtcx,handle_sendemailcsv +from rowers.tasks import ( + handle_sendemail_unrecognized,handle_sendemailnewcomment, + handle_sendemailsummary, + handle_send_disqualification_email, + handle_sendemailfile, + handle_sendemailkml, + handle_sendemailnewresponse, handle_updatedps, + handle_updatecp,long_test_task,long_test_task2, + handle_zip_file,handle_getagegrouprecords, + handle_updatefitnessmetric, + handle_update_empower, + handle_sendemailics, + handle_sendemail_userdeleted, + handle_sendemail_raceregistration, + handle_sendemail_racesubmission, + ) + +from scipy.signal import savgol_filter +from django.shortcuts import render_to_response +from Cookie import SimpleCookie +from shutil import copyfile,move +import mytypes +from rowingdata import rower as rrower +from rowingdata import main as rmain +from rowingdata import rowingdata as rrdata +from rowingdata import make_cumvalues +from rowingdata import summarydata +import pandas as pd +import numpy as np +import matplotlib.pyplot as plt + +from rowers.emails import send_template_email,htmlstrip + +from pytz import timezone as tz,utc +from timezonefinder import TimezoneFinder +import dateutil +import mpld3 +from mpld3 import plugins +import stravalib +from stravalib.exc import ActivityUploadFailed,TimeoutExceeded +from weather import get_wind_data,get_airport_code,get_metar_data + +from oauth2_provider.models import Application,Grant,AccessToken + +import django_rq +queue = django_rq.get_queue('default') +queuelow = django_rq.get_queue('low') +queuehigh = django_rq.get_queue('low') + +import redis +import threading +from redis import StrictRedis,Redis +from rq.exceptions import NoSuchJobError +from rq.registry import StartedJobRegistry +from rq import Queue,cancel_job + +from django.core.cache import cache +from django_mailbox.models import Message,Mailbox,MessageAttachment + + + +# Utility to get stroke data in a JSON response +class JSONResponse(HttpResponse): + def __init__(self, data, **kwargs): + content = JSONRenderer().render(data) + kwargs['content_type'] = 'application/json' + super(JSONResponse, self).__init__(content, **kwargs) + + +def getrequestrower(request,rowerid=0,userid=0,notpermanent=False): + + userid = int(userid) + rowerid = int(rowerid) + + if notpermanent == False: + if rowerid == 0 and 'rowerid' in request.session: + rowerid = request.session['rowerid'] + + if userid != 0: + rowerid = 0 + + try: + + if rowerid != 0: + r = Rower.objects.get(id=rowerid) + elif userid != 0: + u = User.objects.get(id=userid) + r = getrower(u) + else: + r = getrower(request.user) + + except Rower.DoesNotExist: + raise Http404("Rower doesn't exist") + + if not checkaccessuser(request.user,r): + raise PermissionDenied("You have no access to this user") + + if notpermanent == False: + request.session['rowerid'] = r.id + + return r + + +def getrower(user): + try: + if user.is_anonymous(): + return None + except AttributeError: + if User.objects.get(id=user).is_anonymous(): + return None + try: + r = Rower.objects.get(user=user) + except Rower.DoesNotExist: + r = Rower(user=user) + r.save() + + return r + + +def get_workout(id): + try: + w = Workout.objects.get(id=id) + except Workout.DoesNotExist: + raise Http404("Workout doesn't exist") + + return w + +def get_workout_permitted(user,id): + w = get_workout(id) + + if (checkworkoutuser(user,w)==False): + raise PermissionDenied("Access denied") + + return w + +def getvalue(data): + perc = 0 + total = 1 + done = 0 + id = 0 + session_key = 'noot' + for i in data.iteritems(): + if i[0] == 'total': + total = float(i[1]) + if i[0] == 'done': + done = float(i[1]) + if i[0] == 'id': + id = i[1] + if i[0] == 'session_key': + session_key = i[1] + + return total,done,id,session_key + +class SessionTaskListener(threading.Thread): + def __init__(self, r, channels): + threading.Thread.__init__(self) + self.redis = r + self.pubsub = self.redis.pubsub() + self.pubsub.subscribe(channels) + + def work(self, item): + + try: + data = json.loads(item['data']) + total,done,id,session_key = getvalue(data) + perc = int(100.*done/total) + cache.set(id,perc,3600) + + except TypeError: + pass + + def run(self): + for item in self.pubsub.listen(): + if item['data'] == "KILL": + self.pubsub.unsubscribe() + print self, "unsubscribed and finished" + break + else: + self.work(item) + + +queuefailed = Queue("failed",connection=Redis()) +redis_connection = StrictRedis() +r = Redis() + +# this doesn't yet work on production +#if settings.DEBUG: +# client = SessionTaskListener(r,['tasks']) +# client.start() + + +rq_registry = StartedJobRegistry(queue.name,connection=redis_connection) +rq_registryhigh = StartedJobRegistry(queuehigh.name,connection=redis_connection) +rq_registrylow = StartedJobRegistry(queuelow.name,connection=redis_connection) + +from rq.job import Job + +from rest_framework_swagger.views import get_swagger_view +from rest_framework.renderers import JSONRenderer +from rest_framework.parsers import JSONParser +from rest_framework.response import Response +from rowers.serializers import RowerSerializer,WorkoutSerializer +from rest_framework import status,permissions,generics +from rest_framework.decorators import api_view, renderer_classes + +from permissions import IsOwnerOrNot + +import plots +import mailprocessing + +from io import BytesIO +from scipy.special import lambertw + +from dataprep import timedeltaconv +from dataprep import getsmallrowdata_db + +from scipy.interpolate import griddata + +#LOCALTIMEZONE = tz('Etc/UTC') +USER_LANGUAGE = 'en-US' + +from interactiveplots import * +from rowers.celery import result as celery_result + +# Define the API documentation +schema_view = get_swagger_view(title='Rowsandall API') + +def remove_asynctask(request,id): + try: + oldtasks = request.session['async_tasks'] + except KeyError: + oldtasks = [] + + newtasks = [] + for task in oldtasks: + if id not in task[0]: + newtasks += [(task[0],task[1])] + + request.session['async_tasks'] = newtasks + +def get_job_result(jobid): + if settings.DEBUG: + result = celery_result.AsyncResult(jobid).result + else: + running_job_ids = rq_registry.get_job_ids() + if len(running_job_ids) and jobid in running_job_ids: + # job is running + return None + else: + # job is ready + try: + job = Job.fetch(jobid,connection=redis_connection) + result = job.result + except NoSuchJobError: + return None + + return result + +verbose_job_status = { + 'updatecp': 'Critical Power Calculation for Ergometer Workouts', + 'updatecpwater': 'Critical Power Calculation for OTW Workouts', + 'otwsetpower': 'Rowing Physics OTW Power Calculation', + 'agegrouprecords': 'Calculate age group records', + 'make_plot': 'Create static chart', + 'long_test_task': 'Long Test Task', + 'long_test_task2': 'Long Test Task 2', + 'update_empower': 'Correct Empower Inflated Power Bug', + 'submit_race': 'Checking Race Course Result', + } + +def get_job_status(jobid): + if settings.DEBUG: + job = celery_result.AsyncResult(jobid) + jobresult = job.result + + + if 'fail' in job.status.lower(): + jobresult = '0' + summary = { + 'status': job.status, + 'result': jobresult, + 'started_at': None + } + else: + try: + job = Job.fetch(jobid,connection=redis_connection) + summary = { + 'status':job.status, + 'result':job.result, + 'started_at':job.started_at + } + except NoSuchJobError: + summary = { + 'status': 'success', + 'result': 1, + 'started_at':None, + } + + try: + if 'fail' in summary['status'].lower(): + summary['failed'] = True + else: + summary['failed'] = False + + if 'success' in summary['status'].lower(): + summary['finished'] = True + elif 'finished' in summary['status'].lower(): + summary['finished'] = True + else: + summary['finished'] = False + except AttributeError: + summary = { + 'status': 'failed', + 'result': 0, + 'finished': True, + 'failed': True, + 'started_at':None, + } + + return summary + +def kill_async_job(request,id='aap'): + if settings.DEBUG: + job = celery_result.AsyncResult(id) + job.revoke() + else: + try: + cancel_job(id,connection=redis_connection) + except NoSuchJobError: + pass + + remove_asynctask(request,id) + cache.delete(id) + url = reverse(session_jobs_status) + + return HttpResponseRedirect(url) + +@login_required() +def raise_500(request): + if request.user.is_superuser: + raise ValueError + else: + return HttpResponse("invalid") + +@login_required() +def test_job_view(request,aantal=100): + + session_key = request.session._session_key + + job = myqueue(queuehigh,long_test_task,int(aantal), + session_key=session_key) + + + try: + request.session['async_tasks'] += [(job.id,'long_test_task')] + except KeyError: + request.session['async_tasks'] = [(job.id,'long_test_task')] + + url = reverse(session_jobs_status) + + return HttpResponseRedirect(url) + +@login_required() +def test_job_view2(request,aantal=100): + + + job = myqueue(queuehigh,long_test_task2,int(aantal), + secret=settings.PROGRESS_CACHE_SECRET) + + + try: + request.session['async_tasks'] += [(job.id,'long_test_task2')] + except KeyError: + request.session['async_tasks'] = [(job.id,'long_test_task2')] + + url = reverse(session_jobs_status) + + return HttpResponseRedirect(url) + +@csrf_exempt +def post_progress(request,id=None,value=0): + if request.method == 'POST': + try: + secret = request.POST['secret'] + except KeyError: + return HttpResponse('Access Denied',status=401) + if secret == settings.PROGRESS_CACHE_SECRET: + if not id: + try: + id = request.POST['id'] + except KeyError: + return HttpResponse('Invalid request',400) + try: + value = request.POST['value'] + except KeyError: + pass + + cache.set(id,value,3600) + # test + result = cache.get(id) + + return HttpResponse('progress cached '+str(result), + status=201) + else: # secret not given + return HttpResponse('access denied',status=401) + + else: # request method is not POST + return HttpResponse('GET method not allowed',status=405) + +def get_all_queued_jobs(userid=0): + r = StrictRedis() + + jobs = [] + + celerykeys = r.keys('celery*') + for key in celerykeys: + id= key[17:] + job = celery_result.AsyncResult(id) + jobresult = job.result + if 'fail' in job.status.lower(): + jobresult = '0' + jobs.append( + (id,{ + 'status':job.status, + 'result':jobresult, + 'function':'', + 'meta':job.info, + })) + + ids = [j.id for j in queue.jobs] + ids += [j.id for j in queuehigh.jobs] + ids += [j.id for j in queuelow.jobs] + ids += [j.id for j in queuefailed.jobs] + + + for id in ids: + job = Job.fetch(id,connection=redis_connection) + jobs.append( + (id,{ + 'status':job.get_status(), + 'result':job.result, + 'function':job.func_name, + 'meta':job.meta, + })) + + return jobs + +def get_stored_tasks_status(request): + try: + taskids = request.session['async_tasks'] + except KeyError: + taskids = [] + + taskstatus = [] + for id,func_name in reversed(taskids): + progress = 0 + cached_progress = cache.get(id) + finished = get_job_status(id)['finished'] + if finished: + cache.set(id,100) + progress = 100 + elif cached_progress>0: + progress = cached_progress + else: + progress = 0 + + this_task_status = { + 'id':id, + 'status':get_job_status(id)['status'], + 'failed':get_job_status(id)['failed'], + 'finished':get_job_status(id)['finished'], + 'func_name':func_name, + 'verbose': verbose_job_status[func_name], + 'progress': progress, + } + + taskstatus.append(this_task_status) + + + return taskstatus + +@login_required() +def get_thumbnails(request,id): + row = get_workout_permitted(request.user,id) + + + r = getrower(request.user) + result = request.user.is_authenticated() and ispromember(request.user) + if result: + promember=1 + if request.user == row.user.user: + mayedit=1 + + comments = WorkoutComment.objects.filter(workout=row) + + aantalcomments = len(comments) + + favorites,maxfav = getfavorites(r,row) + + charts = [] + + charts = thumbnails_set(r,id,favorites) + try: + if charts[0]['script'] == '': + charts = [] + except IndexError: + charts = [] + + + return JSONResponse(charts) + + +def get_blog_posts(request): + response = requests.get( + 'https://analytics.rowsandall.com/wp-json/wp/v2/posts') + if response.status_code == 200: + blogs_json = response.json() + # with open('blogs.txt','w') as o: + # o.write(json.dumps(blogs_json,indent=2,sort_keys=True)) + else: + blogs_json = [] + + blogposts = [] + + + for postdata in blogs_json[0:3]: + + try: + featuredmedia = postdata['featured_media'] + url = 'https://analytics.rowsandall.com/wp-json/wp/v2/media/%d' % featuredmedia + response = requests.get(url) + + if response.status_code == 200: + image_json = response.json() + image_url = image_json[ + 'media_details' + ][ + 'sizes' + ][ + 'thumbnail' + ][ + 'source_url' + ] + else: + image_url = '' + except KeyError: + image_url = '' + + + title = postdata['title']['rendered'].encode( + 'ascii','xmlcharrefreplace') + + excerpt = postdata['excerpt']['rendered'].encode( + 'ascii','xmlcharrefreplace') + + ptester = re.compile('\(\w.*)\<\/p\>') + excerpt_first = ptester.match(excerpt).group(1) + + thedict = { + 'title': title, + 'author': '', + 'image': image_url, + 'excerpt': excerpt_first, + 'link': postdata['link'], + } + + blogposts.append(thedict) + + return JSONResponse(blogposts) + + + +@login_required() +def get_testscript(request,id): + row = get_workout_permitted(request.user,id) + r = getrower(request.user) + + object = { + "script":""" +
+ +
+ """, + "div":""" +
+Hoi +
+""" + } + + + return JSONResponse([object,object]) + +@login_required() +def session_jobs_view(request): + taskstatus = get_stored_tasks_status(request) + + return HttpResponse(json.dumps(taskstatus)) + +@login_required() +def session_jobs_status(request): + taskstatus = get_stored_tasks_status(request) + + return render(request, + 'async_tasks.html', + {'taskstatus':taskstatus}) + +# Test if row data include candidates +def rowhascoordinates(row): + # create interactive plot + f1 = row.csvfilename + u = row.user.user + r = getrower(u) + rowdata = rdata(f1) + hascoordinates = 1 + if rowdata != 0: + try: + latitude = rowdata.df[' latitude'] + + if not latitude.std(): + hascoordinates = 0 + except KeyError,AttributeError: + hascoordinates = 0 + + else: + hascoordinates = 0 + + return hascoordinates + + +# Wrapper around the rowingdata call to catch some exceptions +# Checks for CSV file, then for gzipped CSV file, and if all fails, returns 0 +def rdata(file,rower=rrower()): + try: + res = rrdata(csvfile=file,rower=rower) + except IOError, IndexError: + try: + res = rrdata(csvfile=file+'.gz',rower=rower) + except IOError, IndexError: + res = 0 + + return res + +# Query to get teams managed and member of +def get_my_teams(user): + try: + therower = Rower.objects.get(user=user) + try: + teams1 = therower.team.all() + except AttributeError: + teams1 = [] + + teams2 = Team.objects.filter(manager=user) + teams = list(set(teams1).union(set(teams2))) + except TypeError: + teams = [] + + return teams + +# Used for the interval editor - translates seconds to a time object +def get_time(second): + if (second<=0) or (second>1e9): + hours = 0 + minutes=0 + sec=0 + microsecond = 0 + elif math.isnan(second): + hours = 0 + minutes=0 + sec=0 + microsecond = 0 + else: + days = int(second/(24.*3600.)) % (24*3600) + hours = int((second-24.*3600.*days)/3600.) % 24 + minutes = int((second-3600.*(hours+24.*days))/60.) % 60 + sec = int(second-3600.*(hours+24.*days)-60.*minutes) % 60 + microsecond = int(1.0e6*(second-3600.*(hours+24.*days)-60.*minutes-sec)) + return datetime.time(hours,minutes,sec,microsecond) + + +# get the workout ID from the SportTracks URI +def getidfromsturi(uri,length=8): + return uri[len(uri)-length:] + +import re + +def getidfromuri(uri): + m = re.search('/(\w.*)\/(\d+)',uri) + return m.group(2) + + + +from utils import ( + geo_distance,serialize_list,deserialize_list,uniqify, + str2bool,range_to_color_hex,absolute,myqueue,get_call, + calculate_age,rankingdistances,rankingdurations, + is_ranking_piece,my_dict_from_instance,wavg,NoTokenError + ) + +import datautils + +from rowers.models import checkworkoutuser,checkaccessuser + +# Check if a user is a Coach member +def iscoachmember(user): + if not user.is_anonymous(): + try: + r = Rower.objects.get(user=user) + except Rower.DoesNotExist: + r = Rower(user=user) + r.save() + + result = user.is_authenticated() and (r.rowerplan=='coach') + else: + result = False + + return result + +# Check if a user can create planned sessions +def hasplannedsessions(user): + if not user.is_anonymous(): + try: + r = Rower.objects.get(user=user) + except Rower.DoesNotExist: + r = Rower(user=user) + r.save() + + result = user.is_authenticated() and (r.rowerplan=='coach' or r.rowerplan=='plan') + if not result and r.plantrialexpires: + result = user.is_authenticated() and r.plantrialexpires >= datetime.date.today() + else: + result = False + + return result + +from rowers.utils import isprorower,ProcessorCustomerError + +# Check if a user is a Pro member +def ispromember(user): + if not user.is_anonymous(): + try: + r = Rower.objects.get(user=user) + except Rower.DoesNotExist: + r = Rower(user=user) + r.save() + + result = user.is_authenticated() and isprorower(r) + else: + result = False + return result + +# More User/Rower utils +def add_defaultfavorites(r): + for c in defaultfavoritecharts: + f = FavoriteChart(user=r, + yparam1=c['yparam1'], + yparam2=c['yparam2'], + xparam=c['xparam'], + plottype=c['plottype'], + workouttype=c['workouttype'], + reststrokes=c['reststrokes'], + notes=c['notes']) + + f.save() + return 1 + +# Shows email form and sends it if submitted +def sendmail(request): + if request.method == 'POST': + form = EmailForm(request.POST) + if form.is_valid(): + firstname = form.cleaned_data['firstname'] + lastname = form.cleaned_data['lastname'] + email = form.cleaned_data['email'] + subject = form.cleaned_data['subject'] + botcheck = form.cleaned_data['botcheck'].lower() + message = form.cleaned_data['message'] + if botcheck == 'yes': + try: + fullemail = firstname + " " + lastname + " " + "<" + email + ">" + send_mail(subject, message, fullemail, ['info@rowsandall.com']) + return HttpResponseRedirect('/rowers/email/thankyou/') + except: + return HttpResponseRedirect('/rowers/email/') + else: + messages.error(request,'You have to answer YES to the question') + return HttpResponseRedirect('/rowers/email/') + else: + return HttpResponseRedirect('/rowers/email/') + else: + return HttpResponseRedirect('/rowers/email/') + + +# Create workout data from Strava or Concept2 +# data and create the associated Workout object and save it +def add_workout_from_strokedata(user,importid,data,strokedata, + source='c2',splitdata=None, + workoutsource='concept2'): + try: + workouttype = data['type'] + except KeyError: + workouttype = 'rower' + + if workouttype not in [x[0] for x in Workout.workouttypes]: + workouttype = 'other' + try: + comments = data['comments'] + except: + comments = ' ' + +# comments = "Imported data \n %s" % comments +# comments = "Imported data \n"+comments # str(comments) + try: + thetimezone = tz(data['timezone']) + except: + thetimezone = 'UTC' + + r = getrower(user) + try: + rowdatetime = iso8601.parse_date(data['date_utc']) + except KeyError: + rowdatetime = iso8601.parse_date(data['start_date']) + except ParseError: + rowdatetime = iso8601.parse_date(data['date']) + + + try: + c2intervaltype = data['workout_type'] + + except KeyError: + c2intervaltype = '' + + try: + title = data['name'] + except KeyError: + title = "" + try: + t = data['comments'].split('\n', 1)[0] + title += t[:20] + except: + title = 'Imported' + + starttimeunix = arrow.get(rowdatetime).timestamp + + res = make_cumvalues(0.1*strokedata['t']) + cum_time = res[0] + lapidx = res[1] + + unixtime = cum_time+starttimeunix + # unixtime[0] = starttimeunix + seconds = 0.1*strokedata.loc[:,'t'] + + nr_rows = len(unixtime) + + try: + latcoord = strokedata.loc[:,'lat'] + loncoord = strokedata.loc[:,'lon'] + except: + latcoord = np.zeros(nr_rows) + loncoord = np.zeros(nr_rows) + + + try: + strokelength = strokedata.loc[:,'strokelength'] + except: + strokelength = np.zeros(nr_rows) + + dist2 = 0.1*strokedata.loc[:,'d'] + + try: + spm = strokedata.loc[:,'spm'] + except KeyError: + spm = 0*dist2 + + try: + hr = strokedata.loc[:,'hr'] + except KeyError: + hr = 0*spm + pace = strokedata.loc[:,'p']/10. + pace = np.clip(pace,0,1e4) + pace = pace.replace(0,300) + + velo = 500./pace + + power = 2.8*velo**3 + + # save csv + # Create data frame with all necessary data to write to csv + df = pd.DataFrame({'TimeStamp (sec)':unixtime, + ' Horizontal (meters)': dist2, + ' Cadence (stokes/min)':spm, + ' HRCur (bpm)':hr, + ' longitude':loncoord, + ' latitude':latcoord, + ' Stroke500mPace (sec/500m)':pace, + ' Power (watts)':power, + ' DragFactor':np.zeros(nr_rows), + ' DriveLength (meters)':np.zeros(nr_rows), + ' StrokeDistance (meters)':strokelength, + ' DriveTime (ms)':np.zeros(nr_rows), + ' StrokeRecoveryTime (ms)':np.zeros(nr_rows), + ' AverageDriveForce (lbs)':np.zeros(nr_rows), + ' PeakDriveForce (lbs)':np.zeros(nr_rows), + ' lapIdx':lapidx, + ' ElapsedTime (sec)':seconds + }) + + + df.sort_values(by='TimeStamp (sec)',ascending=True) + + timestr = strftime("%Y%m%d-%H%M%S") + + + # Create CSV file name and save data to CSV file + csvfilename ='media/{code}_{importid}.csv'.format( + importid=importid, + code = uuid4().hex[:16] + ) + + res = df.to_csv(csvfilename+'.gz',index_label='index', + compression='gzip') + + + # with Concept2 + if source=='c2': + try: + totaldist = data['distance'] + totaltime = data['time']/10. + except KeyError: + totaldist = 0 + totaltime = 0 + else: + totaldist = 0 + totaltime = 0 + + id,message = dataprep.save_workout_database( + csvfilename,r, + workouttype=workouttype, + title=title,notes=comments, +# totaldist=totaldist, +# totaltime=totaltime, + workoutsource=workoutsource, + dosummary=True + ) + + + + return id,message + + + + +def keyvalue_get_default(key,options,def_options): + + try: + return options[key] + except KeyError: + return def_options[key] + + + + +# Creates unix time stamp from a datetime object +def totimestamp(dt, epoch=datetime.datetime(1970,1,1,tzinfo=tz('UTC'))): + td = dt - epoch + # return td.total_seconds() + return (td.microseconds + (td.seconds + td.days * 86400) * 10**6) / 10**6 +# Check if a column of a dataframe has the required (aantal) +# number of elements. Also checks if the column is a numerical type +# Replaces any faulty columns with zeros +def trydf(df,aantal,column): + try: + s = df[column] + if len(s) != aantal: + return np.zeros(aantal) + if not np.issubdtype(s,np.number): + return np.zeros(aantal) + except KeyError: + s = np.zeros(aantal) + + return s + +import teams +from rowers.models import C2WorldClassAgePerformance + + diff --git a/rowers/views/teamviews.py b/rowers/views/teamviews.py new file mode 100644 index 00000000..563a0bea --- /dev/null +++ b/rowers/views/teamviews.py @@ -0,0 +1,536 @@ +from statements import * + + +@login_required() +def team_view(request,id=0,userid=0): + ismember = 0 + hasrequested = 0 + r = getrequestrower(request,userid=userid) + + myteams, memberteams, otherteams = get_teams(request) + teams.remove_expired_invites() + + + try: + t = Team.objects.get(id=id) + except Team.DoesNotExist: + raise Http404("Team doesn't exist") + + + if request.method == 'POST' and request.user == t.manager: + inviteform = TeamInviteForm(request.POST) + inviteform.fields['user'].queryset = User.objects.filter(rower__isnull=False,rower__team__in=myteams).distinct().exclude(rower__team__name=t.name) + if inviteform.is_valid(): + cd = inviteform.cleaned_data + newmember = cd['user'] + email = cd['email'] + inviteid,text = teams.create_invite(t,t.manager, + user=newmember, + email=email) + if inviteid: + teams.send_invite_email(inviteid) + successmessage = text + messages.info(request,successmessage) + else: + message = text + messages.error(request,message) + + elif request.user == t.manager: + inviteform = TeamInviteForm() + inviteform.fields['user'].queryset = User.objects.filter(rower__isnull=False,rower__team__in=myteams).distinct().exclude(rower__team__name=t.name) + else: + inviteform = '' + + members = Rower.objects.filter(team=t).order_by('user__last_name','user__first_name') + thisteammyrequests = TeamRequest.objects.filter(team=t,user=request.user) + if len(thisteammyrequests): + hasrequested = 1 + + if r in members: + ismember = 1 + + breadcrumbs = [ + { + 'url':reverse(rower_teams_view), + 'name': 'Teams' + }, + { + 'url':reverse(team_view,kwargs={'id':id}), + 'name': t.name + } + ] + + + return render(request, 'team.html', + { + 'team':t, + 'teams':get_my_teams(request.user), + 'myteams':myteams, + 'memberteams':memberteams, + 'members':members, + 'breadcrumbs':breadcrumbs, + 'active':'nav-teams', + 'inviteform':inviteform, + 'ismember':ismember, + 'hasrequested':hasrequested, + }) + +@login_required() +def team_leaveconfirm_view(request,id=0): + try: + t = Team.objects.get(id=id) + except Team.DoesNotExist: + raise Http404("Team doesn't exist") + + myteams, memberteams, otherteams = get_teams(request) + + breadcrumbs = [ + { + 'url':reverse(rower_teams_view), + 'name': 'Teams' + }, + { + 'url':reverse(team_view,kwargs={'id':id}), + 'name': t.name + }, + { + 'url':reverse(team_leaveconfirm_view,kwargs={'id':id}), + 'name': 'Leave' + } + ] + return render(request,'teamleaveconfirm.html', + { + 'team':t, + 'teams':get_my_teams(request.user), + 'myteams':myteams, + 'memberteams':memberteams, + 'otherteams':otherteams, + 'active':'nav-teams', + 'breadcrumbs':breadcrumbs, + }) + +@login_required() +def rower_calcdps_view(request): + r = getrower(request.user) + + ws = [(w.id,w.csvfilename) for w in Workout.objects.filter(user=r)] + res = myqueue(queue,handle_updatedps,r.user.email,ws,debug=False, + emailbounced=r.emailbounced) + + messages.info(request,"Your workouts are being updated in the background. You will receive email when this is done.") + + url = reverse(workouts_view) + return HttpResponseRedirect(url) + +@login_required() +def team_leave_view(request,id=0): + r = getrower(request.user) + teams.remove_member(id,r) + + url = reverse(rower_teams_view) + response = HttpResponseRedirect(url) + return response + +from rowers.forms import TeamInviteCodeForm + +def get_teams(request): + r = Rower.objects.get(user=request.user) + + myteams = Team.objects.filter( + manager=request.user).order_by('name') + memberteams = Team.objects.filter( + rower=r).exclude(manager=request.user).order_by('name') + otherteams = Team.objects.filter( + private='open').exclude( + rower=r).exclude(manager=request.user).order_by('name') + + return myteams, memberteams, otherteams + +@login_required() +def rower_teams_view(request,message='',successmessage=''): + if request.method == 'POST': + form = TeamInviteCodeForm(request.POST) + if form.is_valid(): + code = form.cleaned_data['code'] + res,text = teams.process_invite_code(request.user,code) + if res: + successmessage = text + else: + message = text + else: + form = TeamInviteCodeForm() + + r = getrower(request.user) + ts = Team.objects.filter(rower=r) + + + myteams, memberteams, otherteams = get_teams(request) + teams.remove_expired_invites() + + + invites = TeamInvite.objects.filter(user=request.user) + requests = TeamRequest.objects.filter(user=request.user) + myrequests = TeamRequest.objects.filter(team__in=myteams) + myinvites = TeamInvite.objects.filter(team__in=myteams) + clubsize = teams.count_invites(request.user)+teams.count_club_members(request.user) + max_clubsize = r.clubsize + + messages.info(request,successmessage) + messages.error(request,message) + + breadcrumbs = [ + { + 'url':reverse(rower_teams_view), + 'name': 'Teams' + } + ] + + return render(request, 'teams.html', + { + 'teams':ts, + 'active':'nav-teams', + 'breadcrumbs':breadcrumbs, + 'clubsize':clubsize, + 'max_clubsize':max_clubsize, + 'myteams':myteams, + 'memberteams':memberteams, + 'invites':invites, + 'otherteams':otherteams, + 'requests':requests, + 'myrequests':myrequests, + 'form':form, + 'myinvites':myinvites, + }) + +@user_passes_test(iscoachmember,login_url="/rowers/paidplans",redirect_field_name=None) +def invitation_revoke_view(request,id): + res,text = teams.revoke_invite(request.user,id) + if res: + messages.info(request,text) + successmessage = text + else: + message = text + messages.error(request,text) + + url = reverse(rower_teams_view) + + return HttpResponseRedirect(url) + +@user_passes_test(iscoachmember,login_url="/rowers/paidplans",redirect_field_name=None) +def manager_member_drop_view(request,teamid,userid, + message='',successmessage=''): + rower = Rower.objects.get(user__id=userid) + res, text = teams.mgr_remove_member(teamid,request.user,rower) + if res: + messages.info(request,text) + else: + messages.error(request,text) + + url = reverse(rower_teams_view) + + return HttpResponseRedirect(url) + +@user_passes_test(iscoachmember,login_url="/rowers/paidplans",redirect_field_name=None) +def manager_requests_view(request,code=None,message='',successmessage=''): + if code: + res,text = teams.process_request_code(request.user,code) + if res: + successmessage = text + message = '' + else: + message = text + successmessage = '' + + messages.info(request,successmessage) + messages.error(request,message) + url = reverse(rower_teams_view,kwargs={ + }) + return HttpResponseRedirect(url) + + +@login_required() +def team_requestmembership_view(request,teamid,userid): + try: + t = Team.objects.get(id=teamid) + except Team.DoesNotExist: + raise Http404("Team doesn't exist") + + res,text = teams.create_request(t,userid) + if res: + messages.info(request,text) + else: + messages.error(request,text) + + url = reverse(team_view,kwargs={ + 'id':int(teamid), + }) + + + return HttpResponseRedirect(url) + +@login_required() +def request_revoke_view(request,id=0): + res,text = teams.revoke_request(request.user,id) + + if res: + messages.info(request,text) + + else: + messages.error(request,text) + + url = reverse(rower_teams_view) + + return HttpResponseRedirect(url) + +@user_passes_test(iscoachmember,login_url="/rowers/paidplans",redirect_field_name=None) +def request_reject_view(request,id=0): + res,text = teams.reject_request(request.user,id) + + if res: + messages.info(request,text) + else: + messages.error(request,text) + + url = reverse(rower_teams_view) + + return HttpResponseRedirect(url) + +@user_passes_test(iscoachmember,login_url="/rowers/paidplans",redirect_field_name=None) +def invitation_reject_view(request,id=0): + res,text = teams.reject_invitation(request.user,id) + + if res: + messages.info(request,text) + else: + messages.error(request,text) + + url = reverse(rower_teams_view) + + return HttpResponseRedirect(url) + +@login_required() +def rower_invitations_view(request,code=None,message='',successmessage=''): + + if code: + teams.remove_expired_invites() + res,text = teams.process_invite_code(request.user,code) + if res: + messages.info(request,text) + teamid=res + url = reverse(team_view,kwargs={ + 'id':teamid, + }) + else: + messages.error(request,text) + + url = reverse(rower_teams_view) + + return HttpResponseRedirect(url) + + url = reverse(rower_teams_view,kwargs={ + }) + return HttpResponseRedirect(url) + +@user_passes_test(iscoachmember,login_url="/rowers/paidplans",redirect_field_name=None) +def team_edit_view(request,id=0): + try: + t = Team.objects.get(id=id) + except Team.DoesNotExist: + raise Http404("Team does not exist") + + if request.method == 'POST': + teamcreateform = TeamForm(request.POST,instance=t) + if teamcreateform.is_valid(): + cd = teamcreateform.cleaned_data + name = cd['name'] + notes = cd['notes'] + manager = request.user + private = cd['private'] + viewing = cd['viewing'] + res,message=teams.update_team(t,name,manager,private,notes, + viewing) + if res: + messages.info(request,message) + else: + messages.error(request,message) + + url = reverse(team_view, + kwargs={ + 'id':int(id), + } + ) + + response = HttpResponseRedirect(url) + return response + + else: + teamcreateform = TeamForm(instance=t) + + myteams, memberteams, otherteams = get_teams(request) + + breadcrumbs = [ + { + 'url':reverse(rower_teams_view), + 'name': 'Teams' + }, + { + 'url':reverse(team_view,kwargs={'id':id}), + 'name': t.name + }, + { + 'url':reverse(team_edit_view,kwargs={'id':id}), + 'name': 'Edit' + } + ] + + return render(request,'teamedit.html', + { + 'form':teamcreateform, + 'teams':get_my_teams(request.user), + 'myteams':myteams, + 'memberteams':memberteams, + 'otherteams':otherteams, + 'active':'nav-teams', + 'breadcrumbs':breadcrumbs, + 'team':t, + }) + +@user_passes_test(iscoachmember,login_url="/rowers/paidplans",redirect_field_name=None) +def team_create_view(request): + if request.method == 'POST': + teamcreateform = TeamForm(request.POST) + if teamcreateform.is_valid(): + cd = teamcreateform.cleaned_data + name = cd['name'] + notes = cd['notes'] + manager = request.user + private = cd['private'] + viewing = cd['viewing'] + res,message=teams.create_team(name,manager,private,notes, + viewing) + url = reverse(rower_teams_view) + response = HttpResponseRedirect(url) + return response + + else: + teamcreateform = TeamForm() + + myteams, memberteams, otherteams = get_teams(request) + + breadcrumbs = [ + { + 'url':reverse(rower_teams_view), + 'name': 'Teams' + }, + { + 'url':reverse(team_create_view), + 'name': "New Team" + }, + ] + return render(request,'teamcreate.html', + { + 'teams':get_my_teams(request.user), + 'form':teamcreateform, + 'myteams':myteams, + 'memberteams':memberteams, + 'otherteams':otherteams, + 'active':'nav-teams', + 'breadcrumbs':breadcrumbs, + }) + +@user_passes_test(iscoachmember,login_url="/rowers/paidplans",redirect_field_name=None) +def team_deleteconfirm_view(request,id): + r = getrower(request.user) + try: + t = Team.objects.get(id=id) + except Team.DoesNotExist: + raise Http404("This team doesn't exist") + if t.manager != request.user: + raise PermissionDenied("You are not allowed to delete this team") + + myteams, memberteams, otherteams = get_teams(request) + + breadcrumbs = [ + { + 'url':reverse(rower_teams_view), + 'name': 'Teams' + }, + { + 'url':reverse(team_view,kwargs={'id':id}), + 'name': t.name + }, + { + 'url':reverse(team_deleteconfirm_view,kwargs={'id':id}), + 'name': 'Leave' + } + ] + return render(request,'teamdeleteconfirm.html', + { + 'teams':get_my_teams(request.user), + 'team':t, + 'myteams':myteams, + 'memberteams':memberteams, + 'otherteams':otherteams, + 'active':'nav-teams', + }) + +@user_passes_test(iscoachmember,login_url="/rowers/paidplans",redirect_field_name=None) +def team_delete_view(request,id): + r = getrower(request.user) + try: + t = Team.objects.get(id=id) + except Team.DoesNotExist: + raise Http404("This team doesn't exist") + if t.manager != request.user: + raise PermissionDenied("You are not allowed to delete this team") + + teams.remove_team(t.id) + + url = reverse(rower_teams_view) + response = HttpResponseRedirect(url) + return response + +@user_passes_test(iscoachmember,login_url="/rowers/paidplans",redirect_field_name=None) +def team_members_stats_view(request,id): + r = getrower(request.user) + try: + t = Team.objects.get(id=id) + except Team.DoesNotExist: + raise Http404("This team doesn't exist") + if t.manager != request.user: + raise PermissionDenied("You are not allowed to see this page") + + members = Rower.objects.filter(team=t).order_by("user__last_name","user__first_name") + + theusers = [member.user for member in members] + + myteams, memberteams, otherteams = get_teams(request) + + breadcrumbs = [ + { + 'url':reverse(rower_teams_view), + 'name': 'Teams' + }, + { + 'url':reverse(team_view,kwargs={'id':id}), + 'name': t.name + }, + { + 'url':reverse(team_members_stats_view,kwargs={'id':id}), + 'name': 'Members Stats' + } + ] + + response = render(request,'teamstats.html', + { + 'teams':get_my_teams(request.user), + 'myteams':myteams, + 'memberteams':memberteams, + 'otherteams':otherteams, + 'active':'nav-teams', + 'breadcrumbs':breadcrumbs, + 'team':t, + 'theusers':theusers, + }) + + return response diff --git a/rowers/views/userviews.py b/rowers/views/userviews.py new file mode 100644 index 00000000..ef3dd6ab --- /dev/null +++ b/rowers/views/userviews.py @@ -0,0 +1,485 @@ +from statements import * + +@login_required() +def start_trial_view(request): + r = getrower(request.user) + + if r.protrialexpires is not None: + messages.error(request,'You do not qualify for a trial') + url = '/rowers/paidplans' + return HttpResponseRedirect(url) + + r.protrialexpires = datetime.date.today()+datetime.timedelta(13) + r.save() + + url = reverse(workouts_view) + + messages.info(request,'We have started your 14 day trial period') + + subject2 = "User started Pro Trial" + message2 = "User Started Pro Trial.\n" + message2 += request.user.email + "\n" + message2 += "User name: "+request.user.username + + send_mail(subject2, message2, + 'Rowsandall Server ', + ['roosendaalsander@gmail.com']) + + return HttpResponseRedirect(url) + +@login_required() +def start_plantrial_view(request): + r = getrower(request.user) + + if r.plantrialexpires is not None: + messages.error(request,'You do not qualify for a trial') + url = '/rowers/paidplans' + return HttpResponseRedirect(url) + + r.plantrialexpires = datetime.date.today()+datetime.timedelta(13) + r.protrialexpires = datetime.date.today()+datetime.timedelta(13) + r.save() + + url = reverse(workouts_view) + + messages.info(request,'We have started your 14 day trial period') + + subject2 = "User started Plan Trial" + message2 = "User Started Plan Trial.\n" + message2 += request.user.email + "\n" + message2 += "User name: "+request.user.username + + send_mail(subject2, message2, + 'Rowsandall Server ', + ['roosendaalsander@gmail.com']) + + return HttpResponseRedirect(url) + +# Page where user can manage his favorite charts +@login_required() +def rower_favoritecharts_view(request,userid=0): + message = '' + successmessage = '' + r = getrequestrower(request,userid=userid,notpermanent=True) + favorites = FavoriteChart.objects.filter(user=r).order_by('id') + aantal = len(favorites) + favorites_data = [{'yparam1':f.yparam1, + 'yparam2':f.yparam2, + 'xparam':f.xparam, + 'plottype':f.plottype, + 'workouttype':f.workouttype, + 'reststrokes':f.reststrokes, + 'notes':f.notes,} + for f in favorites] + FavoriteChartFormSet = formset_factory(FavoriteForm,formset=BaseFavoriteFormSet,extra=0) + if aantal==0: + FavoriteChartFormSet = formset_factory(FavoriteForm,formset=BaseFavoriteFormSet,extra=1) + + + if request.method == 'POST': + favorites_formset = FavoriteChartFormSet(request.POST) + if favorites_formset.is_valid(): + new_instances = [] + for favorites_form in favorites_formset: + yparam1 = favorites_form.cleaned_data.get('yparam1') + yparam2 = favorites_form.cleaned_data.get('yparam2') + xparam = favorites_form.cleaned_data.get('xparam') + plottype = favorites_form.cleaned_data.get('plottype') + workouttype = favorites_form.cleaned_data.get('workouttype') + reststrokes = favorites_form.cleaned_data.get('reststrokes') + notes = favorites_form.cleaned_data.get('notes') + new_instances.append(FavoriteChart(user=r, + yparam1=yparam1, + yparam2=yparam2, + xparam=xparam, + plottype=plottype, + notes=notes, + workouttype=workouttype, + reststrokes=reststrokes)) + try: + with transaction.atomic(): + FavoriteChart.objects.filter(user=r).delete() + FavoriteChart.objects.bulk_create(new_instances) + successmessage = "You have updated your favorites" + messages.info(request,message) + if len(new_instances)==0: + FavoriteChartFormSet=formset_factory(FavoriteForm,formset=BaseFavoriteFormSet,extra=1) + favorites_formset = FavoriteChartFormSet() + except IntegrityError: + message = "something went wrong" + messages.error(request,message) + else: + favorites_formset = FavoriteChartFormSet(initial=favorites_data) + + + context = { + 'favorites_formset':favorites_formset, + 'teams':get_my_teams(request.user), + 'rower':r, + } + + + + return render(request,'favoritecharts.html',context) + +# page where user sets his export settings +@login_required() +def rower_exportsettings_view(request,userid=0): + r = getrequestrower(request,userid=userid) + if request.method == 'POST': + form = RowerExportForm(request.POST) + if form.is_valid(): + cd = form.cleaned_data + + for attr, value in cd.items(): + setattr(r, attr, value) + + r.save() + messages.info(request,'Settings saved') + else: + form = RowerExportForm(instance=r) + + breadcrumbs = [ + { + 'url':'/rowers/me/edit/', + 'name': 'Profile' + }, + { + 'url': reverse(rower_exportsettings_view), + 'name': 'Export Settings' + } + ] + + return render(request, 'rower_exportsettings.html', + {'form':form, + 'rower':r, + 'breadcrumbs': breadcrumbs, + }) + + +# Page where user can set his details +# Add email address to form so user can change his email address +@login_required() +def rower_edit_view(request,rowerid=0,userid=0,message=""): + r = getrequestrower(request,rowerid=rowerid,userid=userid,notpermanent=True) + + rowerid = r.id + + breadcrumbs = [ + { + 'url':'/rowers/me/edit/', + 'name': 'Profile' + }, + { + 'url': reverse(rower_edit_view), + 'name': 'Account Settings' + } + ] + + + if request.method == 'POST': + accountform = AccountRowerForm(request.POST) + userform = UserForm(request.POST,instance=r.user) + + if accountform.is_valid() and userform.is_valid(): + # process + cd = accountform.cleaned_data + ucd = userform.cleaned_data + + first_name = ucd['first_name'] + last_name = ucd['last_name'] + email = ucd['email'] + sex = cd['sex'] + adaptiveclass = cd['adaptiveclass'] + defaultlandingpage = cd['defaultlandingpage'] + weightcategory = cd['weightcategory'] + birthdate = cd['birthdate'] + showfavoritechartnotes = cd['showfavoritechartnotes'] + getemailnotifications = cd['getemailnotifications'] + getimportantemails = cd['getimportantemails'] + defaulttimezone=cd['defaulttimezone'] + u = r.user + if u.email != email and len(email): + resetbounce = True + else: + resetbounce = False + if len(first_name): + u.first_name = first_name + u.last_name = last_name + if len(email): ## and check_email_freeforuse(u,email): + u.email = email + resetbounce = True + + + u.save() + r.defaulttimezone=defaulttimezone + r.weightcategory = weightcategory + r.adaptiveclass = adaptiveclass + r.getemailnotifications = getemailnotifications + r.getimportantemails = getimportantemails + r.defaultlandingpage = defaultlandingpage + r.showfavoritechartnotes = showfavoritechartnotes + r.sex = sex + r.birthdate = birthdate + + if resetbounce and r.emailbounced: + r.emailbounced = False + r.save() + + accountform = AccountRowerForm(instance=r) + userform = UserForm(instance=u) + successmessage = 'Account Information changed' + messages.info(request,successmessage) + else: + accountform = AccountRowerForm(instance=r) + userform = UserForm(instance=r.user) + + + grants = AccessToken.objects.filter(user=request.user) + return render(request, 'rower_form.html', + { + 'teams':get_my_teams(request.user), + 'breadcrumbs':breadcrumbs, + 'grants':grants, + 'userform':userform, + 'accountform':accountform, + 'rower':r, + }) + + +# Page where user can set his details +# Add email address to form so user can change his email address +@login_required() +def rower_prefs_view(request,userid=0,message=""): + r = getrequestrower(request,userid=userid,notpermanent=True) + + rowerid = r.id + + breadcrumbs = [ + { + 'url':'/rowers/me/edit/', + 'name': 'Profile' + }, + { + 'url': reverse(rower_prefs_view), + 'name': 'Zones' + } + ] + + form = RowerForm(instance=r) + powerform = RowerPowerForm(instance=r) + powerzonesform = RowerPowerZonesForm(instance=r) + + if request.method == 'POST' and "ut2" in request.POST: + form = RowerForm(request.POST) + if form.is_valid(): + # something + cd = form.cleaned_data + hrmax = cd['max'] + ut2 = cd['ut2'] + ut1 = cd['ut1'] + at = cd['at'] + tr = cd['tr'] + an = cd['an'] + rest = cd['rest'] + + r.max = max(min(hrmax,250),10) + r.ut2 = max(min(ut2,250),10) + r.ut1 = max(min(ut1,250),10) + r.at = max(min(at,250),10) + r.tr = max(min(tr,250),10) + r.an = max(min(an,250),10) + r.rest = max(min(rest,250),10) + r.save() + successmessage = "Your Heart Rate data were changed" + messages.info(request,successmessage) + elif request.method == 'POST' and "ftp" in request.POST: + powerform = RowerPowerForm(request.POST) + if powerform.is_valid(): + cd = powerform.cleaned_data + hrftp = cd['hrftp'] + if hrftp == 0: + hrftp = int((r.an+r.tr)/2.) + ftp = cd['ftp'] + otwslack = cd['otwslack'] + + powerfrac = 100*np.array([r.pw_ut2, + r.pw_ut1, + r.pw_at, + r.pw_tr,r.pw_an])/r.ftp + r.ftp = max(min(ftp,650),50) + r.otwslack = max(min(otwslack,50),0) + ut2,ut1,at,tr,an = (r.ftp*powerfrac/100.).astype(int) + r.pw_ut2 = ut2 + r.pw_ut1 = ut1 + r.pw_at = at + r.pw_tr = tr + r.pw_an = an + r.hrftp = hrftp + r.save() + message = "FTP and/or OTW slack values changed." + messages.info(request,message) + + elif request.method == 'POST' and "ut3name" in request.POST: + powerzonesform = RowerPowerZonesForm(request.POST) + if powerzonesform.is_valid(): + cd = powerzonesform.cleaned_data + pw_ut2 = cd['pw_ut2'] + pw_ut1 = cd['pw_ut1'] + pw_at = cd['pw_at'] + pw_tr = cd['pw_tr'] + pw_an = cd['pw_an'] + ut3name = cd['ut3name'] + ut2name = cd['ut2name'] + ut1name = cd['ut1name'] + atname = cd['atname'] + trname = cd['trname'] + anname = cd['anname'] + powerzones = [ut3name,ut2name,ut1name,atname,trname,anname] + + r.pw_ut2 = pw_ut2 + r.pw_ut1 = pw_ut1 + r.pw_at = pw_at + r.pw_tr = pw_tr + r.pw_an = pw_an + r.powerzones = powerzones + r.save() + successmessage = "Your Power Zone data were changed" + messages.info(request,successmessage) + + return render(request, 'rower_preferences.html', + { + 'form':form, + 'teams':get_my_teams(request.user), + 'powerform':powerform, + 'powerzonesform':powerzonesform, + 'breadcrumbs':breadcrumbs, + 'rower':r, + }) + + +# Revoke an app that you granted access through the API. +# this views is called when you press a button on the User edit page +# the button is only there when you have granted access to an app +@login_required() +def rower_revokeapp_view(request,id=0): + try: + tokens = AccessToken.objects.filter(user=request.user,application=id) + refreshtokens = AccessToken.objects.filter(user=request.user,application=id) + for token in tokens: + token.revoke() + for token in refreshtokens: + token.revoke() + + r = getrower(request.user) + form = RowerForm(instance=r) + powerform = RowerPowerForm(instance=r) + grants = AccessToken.objects.filter(user=request.user) + url = reverse(rower_edit_view) + return HttpResponseRedirect(url) + except AccessToken.DoesNotExist: + raise Http404("Access token doesn't exist") + + +@login_required() +def rower_update_empower_view( + request, + startdate=timezone.now()-datetime.timedelta(days=365), + enddate=timezone.now() +): + try: + r = getrower(request.user) + except Rower.DoesNotExist: + raise Http404("Rower doesn't exist") + + if request.method == 'POST' and 'daterange' in request.POST: + dateform = DateRangeForm(request.POST) + if dateform.is_valid(): + startdate = dateform.cleaned_data['startdate'] + enddate = dateform.cleaned_data['enddate'] + startdatestring = startdate.strftime('%Y-%m-%d') + enddatestring = enddate.strftime('%Y-%m-%d') + request.session['startdate'] = startdatestring + request.session['enddate'] = enddatestring + else: + dateform = DateRangeForm(initial={ + 'startdate':startdate, + 'enddate':enddate, + }) + + + if request.method == 'POST' and 'workouts' in request.POST: + form = WorkoutMultipleCompareForm(request.POST) + if form.is_valid(): + cd = form.cleaned_data + workouts = cd['workouts'] + workoutdicts = [] + + for w in workouts: + if w.user != r: + message = "You can only alter your own workouts" + messages.error(request,message) + if 'x' in w.boattype and w.oarlength is not None and w.oarlength > 3.30: + message = "Oarlength and boat type mismatch for workout "+str(w.id)+". Skipping workout" + messages.error(request,message) + elif 'x' not in w.boattype and w.oarlength is not None and w.oarlength <= 3.30: + message = "Oarlength and boat type mismatch for workout "+str(w.id)+". Skipping workout" + messages.error(request,message) + elif w.oarlength is None: + message = "Incorrect oarlength in workout "+str(w.id)+". Skipping workout" + messages.error(request,message) + else: + + + workoutdict = { + 'id':w.id, + 'boattype':w.boattype, + 'filename':w.csvfilename, + 'inboard':w.inboard, + 'oarlength':w.oarlength + } + + workoutdicts.append(workoutdict) + + w.workoutsource = 'speedcoach2corrected' + w.save() + + + job = myqueue(queuelow,handle_update_empower, + request.user.email,workoutdicts, + debug=False, + emailbounced=r.emailbounced) + + try: + request.session['async_tasks'] += [(job.id,'update_empower')] + except KeyError: + request.session['async_tasks'] = [(job.id,'update_empower')] + + successmessage = 'Your workouts are being updated in the background. You will receive email when this is done. You can check the status of your calculations here' + + messages.info(request,successmessage) + + url = reverse(workouts_view) + return HttpResponseRedirect(url) + else: + + workouts = Workout.objects.filter( + startdatetime__gte=startdate, + startdatetime__lte=enddate, + workoutsource='speedcoach2', + user=r, + ).order_by("-date","-starttime") + + form = WorkoutMultipleCompareForm() + form.fields["workouts"].queryset = workouts + # GET request = prepare form + + return render(request, 'empower_fix.html', + {'workouts':workouts, + 'active': 'nav-workouts', + 'dateform':dateform, + 'form':form, + 'rower':r + }) + + diff --git a/rowers/views/workoutviews.py b/rowers/views/workoutviews.py new file mode 100644 index 00000000..e27deb08 --- /dev/null +++ b/rowers/views/workoutviews.py @@ -0,0 +1,5407 @@ +from statements import * + + +# Show the EMpower Oarlock generated Stroke Profile +@user_passes_test(ispromember,login_url="/rowers/paidplans/", + message="This functionality requires a Pro plan or higher", + redirect_field_name=None) +def workout_forcecurve_view(request,id=0,workstrokesonly=False): + row = get_workout(id) + + promember=0 + mayedit=0 + if not request.user.is_anonymous(): + r = getrower(request.user) + result = request.user.is_authenticated() and ispromember(request.user) + if result: + promember=1 + if request.user == row.user.user: + mayedit=1 + + if not promember: + return HttpResponseRedirect("/rowers/about/") + + if request.method == 'POST' and 'workstrokesonly' in request.POST: + workstrokesonly = request.POST['workstrokesonly'] + if workstrokesonly == 'True': + workstrokesonly = True + else: + workstrokesonly = False + + script,div,js_resources,css_resources = interactive_forcecurve([row], + workstrokesonly=workstrokesonly) + + breadcrumbs = [ + { + 'url':'/rowers/list-workouts/', + 'name':'Workouts' + }, + { + 'url':get_workout_default_page(request,id), + 'name': row.name + }, + { + 'url':reverse(workout_forcecurve_view,kwargs={'id':id}), + 'name': 'Empower Force Curve' + } + + ] + + r = getrower(request.user) + + return render(request, + 'forcecurve_single.html', + { + 'the_script':script, + 'rower':r, + 'workout':row, + 'breadcrumbs':breadcrumbs, + 'active':'nav-workouts', + 'the_div':div, + 'js_res': js_resources, + 'css_res':css_resources, + 'id':int(id), + 'mayedit':mayedit, + 'workstrokesonly': not workstrokesonly, + 'teams':get_my_teams(request.user), + }) + +# Test asynchronous tasking and messaging +@login_required() +def workout_test_task_view(request,id=0): + row = Workout.objects.get(id=id) + res = myqueue(queuehigh,addcomment2,request.user.id,row.id) + + + url = reverse(workout_edit_view, + kwargs = { + 'id':int(id), + }) + return HttpResponseRedirect(url) + +# Show Stroke power histogram for a workout +@login_required() +def workout_histo_view(request,id=0): + w = get_workout(id) + + promember=0 + mayedit=0 + if not request.user.is_anonymous(): + r = getrower(request.user) + result = request.user.is_authenticated() and ispromember(request.user) + if result: + promember=1 + if request.user == w.user.user: + mayedit=1 + + if not promember: + return HttpResponseRedirect("/rowers/about/") + + res = interactive_histoall([w]) + script = res[0] + div = res[1] + + breadcrumbs = [ + { + 'url':'/rowers/list-workouts/', + 'name':'Workouts' + }, + { + 'url':get_workout_default_page(request,id), + 'name': w.name + }, + { + 'url':reverse(workout_histo_view,kwargs={'id':id}), + 'name': 'Histogram' + } + + ] + + + return render(request, + 'histo_single.html', + {'interactiveplot':script, + 'breadcrumbs':breadcrumbs, + 'active':'nav-workouts', + 'workout':w, + 'rower':r, + 'the_div':div, + 'id':int(id), + 'mayedit':mayedit, + 'teams':get_my_teams(request.user), + }) + + + +# Histogram for a date/time range +@user_passes_test(ispromember,login_url="/rowers/paidplans", + message="This functionality requires a Pro plan or higher", + redirect_field_name=None) +def histo(request,theuser=0, + startdate=timezone.now()-datetime.timedelta(days=365), + enddate=timezone.now(), + deltadays=-1, + enddatestring=timezone.now().strftime("%Y-%m-%d"), + startdatestring=(timezone.now()-datetime.timedelta(days=30)).strftime("%Y-%m-%d"), + options={ + 'includereststrokes':False, + 'workouttypes':[i[0] for i in mytypes.workouttypes], + 'waterboattype':mytypes.waterboattype, + 'rankingonly': False, + }): + + r = getrequestrower(request,userid=theuser) + theuser = r.user + + if 'waterboattype' in request.session: + waterboattype = request.session['waterboattype'] + else: + waterboattype = mytypes.waterboattype + + + if 'rankingonly' in request.session: + rankingonly = request.session['rankingonly'] + else: + rankingonly = False + + if 'modalities' in request.session: + modalities = request.session['modalities'] + if len(modalities) > 1: + modality = 'all' + else: + modality = modalities[0] + else: + modalities = [m[0] for m in mytypes.workouttypes] + modality = 'all' + + + try: + rankingonly = options['rankingonly'] + except KeyError: + rankingonly = False + + try: + includereststrokes = options['includereststrokes'] + except KeyError: + includereststrokes = False + + + workstrokesonly = not includereststrokes + + waterboattype = mytypes.waterboattype + + + if startdatestring != "": + startdate = iso8601.parse_date(startdatestring) + + if enddatestring != "": + enddate = iso8601.parse_date(enddatestring) + + if enddate < startdate: + s = enddate + enddate = startdate + startdate = s + + + # get all indoor rows of in date range + + # process form + if request.method == 'POST': + form = DateRangeForm(request.POST) + modalityform = TrendFlexModalForm(request.POST) + if form.is_valid(): + startdate = form.cleaned_data['startdate'] + enddate = form.cleaned_data['enddate'] + if startdate > enddate: + s = enddate + enddate = startdate + startdate = s + startdatestring = startdate.strftime('%Y-%m-%d') + enddatestring = enddate.strftime('%Y-%m-%d') + if modalityform.is_valid(): + modality = modalityform.cleaned_data['modality'] + waterboattype = modalityform.cleaned_data['waterboattype'] + rankingonly = modalityform.cleaned_data['rankingonly'] + if modality == 'all': + modalities = [m[0] for m in mytypes.workouttypes] + else: + modalities = [modality] + + if modality != 'water': + waterboattype = [b[0] for b in mytypes.boattypes] + + + request.session['modalities'] = modalities + request.session['waterboattype'] = waterboattype + request.session['rankingonly'] = rankingonly + form = DateRangeForm(initial={ + 'startdate': startdate, + 'enddate': enddate, + }) + else: + form = DateRangeForm(initial={ + 'startdate': startdate, + 'enddate': enddate, + }) + includereststrokes = False + + workstrokesonly = not includereststrokes + modalityform = TrendFlexModalForm( + initial={ + 'modality':modality, + 'waterboattype':waterboattype, + 'rankingonly':rankingonly, + } + ) + + negtypes = [] + for b in mytypes.boattypes: + if b[0] not in waterboattype: + negtypes.append(b[0]) + + + + script = '' + div = get_call() + js_resources = '' + css_resources = '' + + + + + options = { + 'modality': modality, + 'theuser': theuser.id, + 'waterboattype':waterboattype, + 'startdatestring':startdatestring, + 'enddatestring':enddatestring, + 'rankingonly':rankingonly, + 'includereststrokes':includereststrokes, + } + + request.session['options'] = options + + promember=0 + mayedit=0 + if not request.user.is_anonymous(): + result = request.user.is_authenticated() and ispromember(request.user) + if result: + promember = 1 + + + request.session['options'] = options + + return render(request, 'histo.html', + {'interactiveplot':script, + 'the_div':div, + 'id':theuser, + 'active':'nav-analysis', + 'theuser':theuser, + 'rower':r, + 'startdate':startdate, + 'enddate':enddate, + 'form':form, + 'optionsform':modalityform, + 'teams':get_my_teams(request.user), + }) + +# add a workout manually +@login_required() +def addmanual_view(request): + r = Rower.objects.get(user=request.user) + + breadcrumbs = [ + { + 'url':'/rowers/list-workouts/', + 'name':'Workouts' + }, + { + 'url':reverse(addmanual_view), + 'name': 'Add Manual Entry' + }, + ] + + if request.method == 'POST': + # Form was submitted + form = WorkoutForm(request.POST) + metricsform = MetricsForm(request.POST) + if form.is_valid() and metricsform.is_valid(): + # Get values from form + name = form.cleaned_data['name'] + if name == '': + name = 'Manual Entry' + date = form.cleaned_data['date'] + starttime = form.cleaned_data['starttime'] + workouttype = form.cleaned_data['workouttype'] + duration = form.cleaned_data['duration'] + weightcategory = form.cleaned_data['weightcategory'] + adaptiveclass = form.cleaned_data['adaptiveclass'] + distance = form.cleaned_data['distance'] + notes = form.cleaned_data['notes'] + thetimezone = form.cleaned_data['timezone'] + private = form.cleaned_data['private'] + avghr = metricsform.cleaned_data['avghr'] + avgpwr = metricsform.cleaned_data['avgpwr'] + avgspm = metricsform.cleaned_data['avgspm'] + try: + ps = form.cleaned_data['plannedsession'] + except KeyError: + ps = None + + try: + boattype = request.POST['boattype'] + except KeyError: + boattype = '1x' + try: + privacy = request.POST['privacy'] + except KeyError: + privacy = 'visible' + try: + rankingpiece = form.cleaned_data['rankingpiece'] + except KeyError: + rankingpiece = False + + try: + duplicate = form.cleaned_data['duplicate'] + except KeyError: + duplicate = False + + if private: + privacy = 'private' + else: + privacy = 'visible' + + startdatetime = (str(date) + ' ' + str(starttime)) + startdatetime = datetime.datetime.strptime(startdatetime, + "%Y-%m-%d %H:%M:%S") + startdatetime = timezone.make_aware(startdatetime) + startdatetime = startdatetime.astimezone( + pytz.timezone(thetimezone) + ) + + + + id,message = dataprep.create_row_df(r, + distance, + duration,startdatetime, + weightcategory=weightcategory, + adaptiveclass=adaptiveclass, + avghr=avghr, + rankingpiece=rankingpiece, + avgpwr=avgpwr, + duplicate=duplicate, + avgspm=avgspm, + title = name, + notes=notes, + workouttype=workouttype) + + + + if message: + messages.error(request,message) + + if id: + w = Workout.objects.get(id=id) + w.rankingpiece = rankingpiece + w.privacy = privacy + w.weightcategory = weightcategory + w.adaptiveclass = adaptiveclass + w.notes = notes + w.plannedsession = ps + w.name = name + w.workouttype = workouttype + w.boattype = boattype + w.save() + if ps: + add_workouts_plannedsession([w],ps,w.user) + + messages.info(request,'New workout created') + + url = reverse( + workout_edit_view, + kwargs={'id':id} + ) + return HttpResponseRedirect(url) + else: + return render(request,'manualadd.html', + {'form':form, + 'metricsform':metricsform, + 'breadcrumbs':breadcrumbs, + 'active':'nav-workouts', + }) + + initial = { + 'workouttype':'rower', + 'date':datetime.date.today(), + 'starttime':timezone.now(), + 'timezone':r.defaulttimezone, + 'duration':datetime.timedelta(minutes=2), + 'distance':500, + + } + form = WorkoutForm(initial=initial) + metricsform = MetricsForm() + + return render(request,'manualadd.html', + {'form':form, + 'metricsform':metricsform, + 'breadcrumbs':breadcrumbs, + 'active':'nav-workouts', + }) + +@login_required() +def fitness_metric_view(request,mode='rower',days=42): + r = getrower(request.user) + startdate = timezone.now()-datetime.timedelta(days=days) + + # test if not something already done + ms = PowerTimeFitnessMetric.objects.filter(user=request.user) + if not ms: + url = reverse(workouts_view) + return HttpResponseRedirect(url) + + max_workout_id = max([m.last_workout for m in ms]) + last_update_date = max([m.date.strftime('%Y-%m-%d') for m in ms]) + + + now_date = timezone.now().strftime('%Y-%m-%d') + + + if mode == 'rower': + workouts = Workout.objects.filter( + user=r, + workouttype__in=['rower','dynamic','slides'], + startdatetime__gte=startdate) + else: + workouts = Workout.objects.filter( + user=r, + workouttype__in=['water','coastal'], + startdatetime__gte=startdate) + + theids = [int(w.id) for w in workouts] + max_id = max(theids) + + if last_update_date >= now_date or max_workout_id >= max_id: + return HttpResponse("already done today or no new workouts") + + + job = myqueue(queue, + handle_updatefitnessmetric, + request.user.id,mode,theids, + ) + + return HttpResponse("job queued") + + +@user_passes_test(ispromember,login_url="/rowers/paidplans", + message="This functionality requires a Pro plan or higher", + redirect_field_name=None) +def workout_update_cp_view(request,id=0): + row = get_workout(id) + + if (checkworkoutuser(request.user,row)==False): + message = "You are not allowed to edit this workout" + messages.error(request,message) + url = reverse(workouts_view) + + return HttpResponseRedirect(url) + + row.rankingpiece = True + row.save() + + r = getrower(request.user) + + dataprep.runcpupdate(r) + + if row.workouttype in mytypes.otwtypes: + url = reverse(otwrankings_view) + else: + url = reverse(oterankings_view) + + return HttpResponseRedirect(url) + + +# Reload the workout and calculate the summary from the stroke data (lapIDx) +@login_required() +def workout_recalcsummary_view(request,id=0): + row = get_workout(id) + + if (checkworkoutuser(request.user,row)==False): + message = "You are not allowed to edit this workout" + messages.error(request,message) + url = reverse(workouts_view) + + return HttpResponseRedirect(url) + + filename = row.csvfilename + rowdata = rdata(filename) + if rowdata: + row.summary = rowdata.allstats() + row.save() + successmessage = "Summary Updated" + messages.info(request,successmessage) + url = reverse(workout_edit_view, + kwargs = { + 'id':int(id), + }) + else: + message = "Something went wrong. Could not update summary" + messages.error(request,message) + url = reverse(workout_edit_view, + kwargs = { + 'id':int(id), + }) + + return HttpResponseRedirect(url) + + +# Joining workout +@user_passes_test(ispromember,login_url="/rowers/paidplans", + message="This functionality requires a Pro plan or higher", + redirect_field_name=None) +def workouts_join_view(request): + promember=0 + if not request.user.is_anonymous(): + r = getrower(request.user) + result = request.user.is_authenticated() and ispromember(request.user) + if result: + promember=1 + + + if request.method == 'POST' and 'workouts' in request.POST: + form = WorkoutMultipleCompareForm(request.POST) + paramform = WorkoutJoinParamForm(request.POST) + if form.is_valid() and paramform.is_valid(): + workout_name = paramform.cleaned_data['workout_name'] + set_private = paramform.cleaned_data['set_private'] + + cd = form.cleaned_data + workouts = cd['workouts'] + ids = [int(w.id) for w in workouts] + request.session['ids'] = ids + + + id,message = dataprep.join_workouts(r,ids, + title=workout_name, + setprivate=set_private) + + if message: + messages.error(request,message) + + url = reverse(r.defaultlandingpage, + kwargs = { + 'id':int(id), + }) + + return HttpResponseRedirect(url) + + else: + return HttpResponse("form is not valid") + + else: + url = reverse(workouts_join_select) + return HttpResponseRedirect(url) + +@user_passes_test(ispromember,login_url="/rowers/paidplans", + message="This functionality requires a Pro plan or higher", + redirect_field_name=None) +def workouts_join_select(request, + startdatestring="", + enddatestring="", + message='', + successmessage='', + startdate=timezone.now()-datetime.timedelta(days=30), + enddate=timezone.now()+datetime.timedelta(days=1), + teamid=0): + + try: + r = getrower(request.user) + except Rower.DoesNotExist: + raise Http404("Rower doesn't exist") + + + if 'waterboattype' in request.session: + waterboattype = request.session['waterboattype'] + else: + waterboattype = mytypes.waterboattype + + + if 'modalities' in request.session: + modalities = request.session['modalities'] + if len(modalities) > 1: + modality = 'all' + else: + modality = modalities[0] + else: + modalities = [m[0] for m in mytypes.workouttypes] + modality = 'all' + + if request.method == 'POST' and 'daterange' in request.POST: + dateform = DateRangeForm(request.POST) + if dateform.is_valid(): + startdate = dateform.cleaned_data['startdate'] + enddate = dateform.cleaned_data['enddate'] + startdatestring = startdate.strftime('%Y-%m-%d') + enddatestring = enddate.strftime('%Y-%m-%d') + request.session['startdate'] = startdatestring + request.session['enddate'] = enddatestring + else: + dateform = DateRangeForm(initial={ + 'startdate':startdate, + 'enddate':enddate, + }) + + + if request.method == 'POST' and 'modality' in request.POST: + modalityform = TrendFlexModalForm(request.POST) + if modalityform.is_valid(): + modality = modalityform.cleaned_data['modality'] + waterboattype = modalityform.cleaned_data['waterboattype'] + if modality == 'all': + modalities = [m[0] for m in mytypes.workouttypes] + else: + modalities = [modality] + + if modality != 'water': + waterboattype = [b[0] for b in mytypes.boattypes] + + + request.session['modalities'] = modalities + request.session['waterboattype'] = waterboattype + + negtypes = [] + for b in mytypes.boattypes: + if b[0] not in waterboattype: + negtypes.append(b[0]) + + startdate = datetime.datetime.combine(startdate,datetime.time()) + enddate = datetime.datetime.combine(enddate,datetime.time(23,59,59)) + #enddate = enddate+datetime.timedelta(days=1) + + if startdatestring: + startdate = iso8601.parse_date(startdatestring) + if enddatestring: + enddate = iso8601.parse_date(enddatestring) + + if enddate < startdate: + s = enddate + enddate = startdate + startdate = s + + try: + theteam = Team.objects.get(id=teamid) + except Team.DoesNotExist: + theteam = 0 + + if r.rowerplan == 'basic' and theteam==0: + raise PermissionDenied("Access denied") + + if theteam and (theteam.viewing == 'allmembers' or theteam.manager == request.user): + workouts = Workout.objects.filter(team=theteam, + startdatetime__gte=startdate, + startdatetime__lte=enddate, + workouttype__in=modalities).order_by("-date", "-starttime").exclude(boattype__in=negtypes) + elif theteam and theteam.viewing == 'coachonly': + workouts = Workout.objects.filter(team=theteam,user=r, + startdatetime__gte=startdate, + startdatetime__lte=enddate, + workouttype__in=modalities).order_by("-date","-starttime").exclude(boattype__in=negtypes) + + + else: + theteam = None + workouts = Workout.objects.filter(user=r, + startdatetime__gte=startdate, + startdatetime__lte=enddate, + workouttype__in=modalities).order_by("-date", "-starttime").exclude(boattype__in=negtypes) + + query = request.GET.get('q') + if query: + query_list = query.split() + workouts = workouts.filter( + reduce(operator.and_, + (Q(name__icontains=q) for q in query_list)) | + reduce(operator.and_, + (Q(notes__icontains=q) for q in query_list)) + ) + searchform = SearchForm(initial={'q':query}) + else: + searchform = SearchForm() + + form = WorkoutMultipleCompareForm() + form.fields["workouts"].queryset = workouts + + if theteam: + theid = theteam.id + else: + theid = 0 + + joinparamform = WorkoutJoinParamForm() + modalityform = TrendFlexModalForm(initial={ + 'modality':modality, + 'waterboattype':waterboattype + }) + + + messages.info(request,successmessage) + messages.error(request,message) + + return render(request, 'workout_join_select.html', + {'workouts': workouts, + 'dateform':dateform, + 'searchform':searchform, + 'startdate':startdate, + 'enddate':enddate, + 'active':'nav-workouts', + 'team':theteam, + 'form':form, + 'joinparamform':joinparamform, + 'modalityform':modalityform, + 'teams':get_my_teams(request.user), + }) + +# Team comparison +@login_required() +def team_comparison_select(request, + startdatestring="", + enddatestring="", + message='', + successmessage='', + userid=0, + startdate=timezone.now()-datetime.timedelta(days=30), + enddate=timezone.now(), + id=0, + teamid=0): + + r = getrequestrower(request,userid=userid) + requestrower = getrower(request.user) + + request.session.pop('ps',None) + + if 'waterboattype' in request.session: + waterboattype = request.session['waterboattype'] + else: + waterboattype = mytypes.waterboattype + + if 'rankingonly' in request.session: + rankingonly = request.session['rankingonly'] + else: + rankingonly = False + + if 'modalities' in request.session: + modalities = request.session['modalities'] + if len(modalities) > 1: + modality = 'all' + else: + modality = modalities[0] + else: + modalities = [m[0] for m in mytypes.workouttypes] + modality = 'all' + + if request.method == 'POST': + dateform = DateRangeForm(request.POST) + if dateform.is_valid(): + startdate = dateform.cleaned_data['startdate'] + enddate = dateform.cleaned_data['enddate'] + startdatestring = startdate.strftime('%Y-%m-%d') + enddatestring = enddate.strftime('%Y-%m-%d') + request.session['startdate'] = startdatestring + request.session['enddate'] = enddatestring + + modalityform = TrendFlexModalForm(request.POST) + if modalityform.is_valid(): + modality = modalityform.cleaned_data['modality'] + waterboattype = modalityform.cleaned_data['waterboattype'] + if modality == 'all': + modalities = [m[0] for m in mytypes.workouttypes] + else: + modalities = [modality] + + if modality != 'water': + waterboattype = [b[0] for b in mytypes.boattypes] + + + if 'rankingonly' in modalityform.cleaned_data: + rankingonly = modalityform.cleaned_data['rankingonly'] + else: + rankingonly = False + + request.session['modalities'] = modalities + request.session['waterboattype'] = waterboattype + else: + dateform = DateRangeForm(initial={ + 'startdate':startdate, + 'enddate':enddate, + }) + modalityform = TrendFlexModalForm(initial={ + 'modality':modality, + 'waterboattype':waterboattype, + 'rankingonly':rankingonly, + }) + + + + + negtypes = [] + for b in mytypes.boattypes: + if b[0] not in waterboattype: + negtypes.append(b[0]) + + startdate = datetime.datetime.combine(startdate,datetime.time()) + enddate = datetime.datetime.combine(enddate,datetime.time(23,59,59)) + #enddate = enddate+datetime.timedelta(days=1) + + if startdatestring: + startdate = iso8601.parse_date(startdatestring) + if enddatestring: + enddate = iso8601.parse_date(enddatestring) + + if enddate < startdate: + s = enddate + enddate = startdate + startdate = s + + try: + theteam = Team.objects.get(id=teamid) + except Team.DoesNotExist: + theteam = 0 + + if requestrower.rowerplan == 'basic' and theteam==0: + if requestrower.protrialexpires is None or requestrower.protrialexpires timezone.now(): + activity_enddate = timezone.now() + activity_startdate = activity_enddate-datetime.timedelta(days=15) + else: + activity_enddate = enddate + except ValueError: + activity_enddate = enddate + + g_startdate = activity_startdate + g_enddate = activity_enddate + + + if teamid: + try: + theteam = Team.objects.get(id=teamid) + except Team.DoesNotExist: + raise Http404("Team doesn't exist") + + if theteam.viewing == 'allmembers' or theteam.manager == request.user: + workouts = Workout.objects.filter( + team=theteam, + startdatetime__gte=startdate, + startdatetime__lte=enddate, + privacy='visible').order_by("-date","-starttime") + g_workouts = Workout.objects.filter( + team=theteam, + startdatetime__gte=activity_startdate, + startdatetime__lte=activity_enddate, + duplicate=False, + privacy='visible').order_by("-date", "-starttime") + elif theteam.viewing == 'coachonly': + workouts = Workout.objects.filter( + team=theteam,user=r, + startdatetime__gte=startdate, + startdatetime__lte=enddate, + privacy='visible').order_by("-startdatetime") + g_workouts = Workout.objects.filter( + team=theteam,user=r, + startdatetime__gte=activity_startdate, + enddatetime__lte=activity_enddate, + duplicate=False, + privacy='visible').order_by("-startdatetime") + + + elif request.user != r.user: + + theteam = None + workouts = Workout.objects.filter( + user=r, + startdatetime__gte=startdate, + startdatetime__lte=enddate, + privacy='visible').order_by("-date", "-starttime") + g_workouts = Workout.objects.filter( + user=r, + startdatetime__gte=activity_startdate, + startdatetime__lte=activity_enddate, + duplicate=False, + privacy='visible').order_by("-startdatetime") + else: + theteam = None + workouts = Workout.objects.filter( + user=r, + startdatetime__gte=startdate, + startdatetime__lte=enddate).order_by("-date", "-starttime") + g_workouts = Workout.objects.filter( + user=r, + duplicate=False, + startdatetime__gte=activity_startdate, + startdatetime__lte=activity_enddate).order_by("-startdatetime") + + + if len(g_workouts) == 0: + g_workouts = Workout.objects.filter( + user=r, + startdatetime__gte=timezone.now()-timedelta(days=15)).order_by("-startdatetime") + g_enddate = timezone.now() + g_startdate = (timezone.now()-timedelta(days=15)) + + if rankingonly: + workouts = workouts.exclude(rankingpiece=False) + + workoutsnohr = workouts.exclude(averagehr__isnull=False) + for w in workoutsnohr: + res = dataprep.workout_trimp(w) + + query = request.GET.get('q') + if query: + query_list = query.split() + workouts = workouts.filter( + reduce(operator.and_, + (Q(name__icontains=q) for q in query_list)) | + reduce(operator.and_, + (Q(notes__icontains=q) for q in query_list)) + ) + searchform = SearchForm(initial={'q':query}) + else: + searchform = SearchForm() + + paginator = Paginator(workouts,20) # show 25 workouts per page + page = request.GET.get('page') + + try: + workouts = paginator.page(page) + except PageNotAnInteger: + workouts = paginator.page(1) + except EmptyPage: + workouts = paginator.page(paginator.num_pages) + + today = timezone.now() + announcements = SiteAnnouncement.objects.filter( + expires__gte=today + ).order_by( + "-created", + "-id" + ) + + if theteam: + stack='rower' + else: + stack='type' + + + script,div = interactive_activitychart(g_workouts, + g_startdate, + g_enddate, + stack=stack) + + + messages.info(request,successmessage) + messages.error(request,message) + + breadcrumbs = [ + { + 'url':'/rowers/list-workouts/', + 'name':'Workouts' + }, + ] + + return render(request, 'list_workouts.html', + {'workouts': workouts, + 'active': 'nav-workouts', + 'rower':r, + 'searchform':searchform, + 'breadcrumbs':breadcrumbs, + 'dateform':dateform, + 'startdate':startdate, + 'enddate':enddate, + 'announcements':announcements[0:4], + 'team':theteam, + 'rankingonly':rankingonly, + 'teams':get_my_teams(request.user), + 'interactiveplot':script, + 'the_div':div, + }) + + + + +# List of workouts to compare a selected workout to +@user_passes_test(ispromember,login_url="/rowers/paidplans", + message="This functionality requires a Pro plan or higher", + redirect_field_name=None) +def workout_fusion_list(request,id=0,message='',successmessage='', + startdatestring="",enddatestring="", + startdate=timezone.now()-datetime.timedelta(days=365), + enddate=timezone.now()): + + try: + r = getrower(request.user) + except Rower.DoesNotExist: + raise Http404("User has no rower instance") + + u = User.objects.get(id=r.user.id) + if request.method == 'POST': + dateform = DateRangeForm(request.POST) + if dateform.is_valid(): + startdate = dateform.cleaned_data['startdate'] + enddate = dateform.cleaned_data['enddate'] + else: + dateform = DateRangeForm(initial={ + 'startdate':startdate, + 'enddate':enddate, + }) + + if startdatestring: + startdate = iso8601.parse_date(startdatestring) + if enddatestring: + enddate = iso8601.parse_date(enddatestring) + + startdate = datetime.datetime.combine(startdate,datetime.time()) + enddate = datetime.datetime.combine(enddate,datetime.time(23,59,59)) + #enddate = enddate+datetime.timedelta(days=1) + + if enddate < startdate: + s = enddate + enddate = startdate + startdate = s + + workouts = Workout.objects.filter(user=r, + startdatetime__gte=startdate, + startdatetime__lte=enddate).order_by("-date", "-starttime").exclude(id=id) + + query = request.GET.get('q') + if query: + query_list = query.split() + workouts = workouts.filter( + reduce(operator.and_, + (Q(name__icontains=q) for q in query_list)) | + reduce(operator.and_, + (Q(notes__icontains=q) for q in query_list)) + ) + searchform = SearchForm(initial={'q':query}) + else: + searchform = SearchForm() + + paginator = Paginator(workouts,15) # show 25 workouts per page + page = request.GET.get('page') + + try: + workouts = paginator.page(page) + except PageNotAnInteger: + workouts = paginator.page(1) + except EmptyPage: + workouts = paginator.page(paginator.num_pages) + row = get_workout(id) + + messages.info(request,successmessage) + messages.error(request,message) + + breadcrumbs = [ + { + 'url':'/rowers/list-workouts/', + 'name':'Workouts' + }, + { + 'url':get_workout_default_page(request,row.id), + 'name': row.name + }, + { + 'url':reverse(workout_fusion_list,kwargs={'id':id}), + 'name': 'Sensor Fusion' + } + + ] + + return render(request, 'fusion_list.html', + {'id':int(id), + 'workout':row, + 'rower':r, + 'searchform':searchform, + 'active':'nav-workouts', + 'breadcrumbs':breadcrumbs, + 'workouts': workouts, + 'last_name':u.last_name, + 'first_name':u.first_name, + 'dateform':dateform, + 'startdate':startdate, + 'enddate':enddate, + 'teams':get_my_teams(request.user), + }) + +# Basic view of workout +def workout_view(request,id=0): + request.session['referer'] = absolute(request)['PATH'] + + if not request.user.is_anonymous(): + rower = getrower(request.user) + else: + rower = None + + try: + row = Workout.objects.get(id=id) + except Workout.DoesNotExist: + raise Http404("Workout doesn't exist") + + comments = WorkoutComment.objects.filter(workout=row) + + aantalcomments = len(comments) + + + if row.privacy == 'private' and not checkworkoutuser(request.user,row): + raise PermissionDenied("Access denied") + + g = GraphImage.objects.filter(workout=row).order_by("-creationdatetime") + for i in g: + try: + width,height = Image.open(i.filename).size + i.width = width + i.height = height + i.save() + except: + pass + + + + # create interactive plot + res = interactive_chart(id) + script = res[0] + div = res[1] + + # create map + f1 = row.csvfilename + rowdata = rdata(f1) + hascoordinates = 1 + if rowdata != 0: + try: + latitude = rowdata.df[' latitude'] + if not latitude.std(): + hascoordinates = 0 + except KeyError,AttributeError: + hascoordinates = 0 + + else: + hascoordinates = 0 + + + if hascoordinates: + mapscript,mapdiv = leaflet_chart(rowdata.df[' latitude'], + rowdata.df[' longitude'], + row.name) + + + else: + mapscript = "" + mapdiv = "" + + breadcrumbs = [ + { + 'url':'/rowers/list-workouts/', + 'name':'Workouts' + }, + { + 'url':reverse(workout_view,kwargs={'id':id}), + 'name': row.name, + } + + ] + + u = row.user.user + + recordsindoor = IndoorVirtualRaceResult.objects.filter(workoutid= row.id) + records = VirtualRaceResult.objects.filter(workoutid= row.id) + + return render(request, 'workout_view.html', + {'workout':row, + 'rower':rower, + 'breadcrumbs':breadcrumbs, + 'active':'nav-workouts', + 'graphs':g, + 'last_name':u.last_name, + 'records':records, + 'recordsindoor':recordsindoor, + 'first_name':u.first_name, + 'interactiveplot':script, + 'aantalcomments':aantalcomments, + 'mapscript':mapscript, + 'mapdiv':mapdiv, + 'teams':get_my_teams(request.user), + 'the_div':div}) + + +# Resets stroke data to raw data (pace) +@user_passes_test(ispromember,login_url="/rowers/paidplans", + message="This functionality requires a Pro plan or higher", + redirect_field_name=None) +def workout_undo_smoothenpace_view( + request,id=0,message="",successmessage="" +): + row = get_workout(id) + r = getrower(request.user) + + if (checkworkoutuser(request.user,row)==False): + message = "You are not allowed to edit this workout" + messages.error(request,message) + url = reverse(workouts_view) + + return HttpResponseRedirect(url) + + filename = row.csvfilename + row = rdata(filename) + if row == 0: + return HttpResponse("Error: CSV Data File Not Found") + + if 'originalvelo' in row.df: + velo = row.df['originalvelo'].values + row.df[' Stroke500mPace (sec/500m)'] = 500./velo + + row.write_csv(filename,gzip=True) + dataprep.update_strokedata(id,row.df) + + url = reverse(r.defaultlandingpage, + kwargs = { + 'id':id, + } + ) + + + return HttpResponseRedirect(url) + + +# Data smoothing of pace data +@user_passes_test(ispromember,login_url="/rowers/paidplans", + message="This functionality requires a Pro plan or higher", + redirect_field_name=None) +def workout_smoothenpace_view(request,id=0,message="",successmessage=""): + row = get_workout(id) + + previousurl = request.META.get('HTTP_REFERER') + + r = getrower(request.user) + + if (checkworkoutuser(request.user,row)==False): + message = "You are not allowed to edit this workout" + messages.error(request,message) + url = reverse(workouts_view) + + return HttpResponseRedirect(url) + + filename = row.csvfilename + row = rdata(filename) + if row == 0: + return HttpResponse("Error: CSV Data File Not Found") + + pace = row.df[' Stroke500mPace (sec/500m)'].values + velo = 500./pace + + if not 'originalvelo' in row.df: + row.df['originalvelo'] = velo + + velo2 = stravastuff.ewmovingaverage(velo,5) + + pace2 = 500./abs(velo2) + + row.df[' Stroke500mPace (sec/500m)'] = pace2 + + row.df = row.df.fillna(0) + + row.write_csv(filename,gzip=True) + dataprep.update_strokedata(id,row.df) + + messages.info(request,'A smoothening filter was applied to your pace data') + + if previousurl: + url = previousurl + else: + url = reverse(r.defaultlandingpage, + kwargs = { + 'id':id, + } + ) + + return HttpResponseRedirect(url) + +# Process CrewNerd Summary CSV and update summary +@user_passes_test(ispromember,login_url="/rowers/paidplans", + message="This functionality requires a Pro plan or higher", + redirect_field_name=None) +def workout_crewnerd_summary_view(request,id=0,message="",successmessage=""): + row = get_workout(id) + r = getrower(request.user) + breadcrumbs = [ + { + 'url':'/rowers/list-workouts/', + 'name':'Workouts' + }, + { + 'url':get_workout_default_page(request,id), + 'name': row.name + }, + { + 'url':reverse(workout_crewnerd_summary_view,kwargs={'id':id}), + 'name': 'CrewNerd Summary' + } + + ] + + if request.method == 'POST': + form = CNsummaryForm(request.POST,request.FILES) + if form.is_valid(): + f = request.FILES['file'] + res = handle_uploaded_file(f) + fname = res[1] + try: + sumd = summarydata(fname) + row.summary = sumd.allstats() + row.save() + os.remove(fname) + successmessage = "CrewNerd summary added" + messages.info(request,successmessage) + url = reverse(workout_edit_view, + kwargs = { + 'id':int(id), + }) + + return HttpResponseRedirect(url) + except: + try: + os.remove(fname) + except: + pass + message = "Something went wrong (workout_crewnerd_summary_view)" + messages.error(request,message) + url = reverse(workout_edit_view, + kwargs = { + 'id':int(id), + }) + return HttpResponseRedirect(url) + else: + return render(request, + "cn_form.html", + {'form':form, + 'active':'nav-workouts', + 'rower':r, + 'workout':row, + 'breadcrumbs':breadcrumbs, + 'teams':get_my_teams(request.user), + 'id':row.id}) + else: + form = CNsummaryForm() + + return render(request, + "cn_form.html", + {'form':form, + 'active':'nav-workouts', + 'rower':r, + 'workout':row, + 'breadcrumbs':breadcrumbs, + 'teams':get_my_teams(request.user), + 'id':row.id}) + +# Get weather for given location and date/time +@user_passes_test(ispromember,login_url="/rowers/paidplans", + message="This functionality requires a Pro plan or higher", + redirect_field_name=None) +def workout_downloadwind_view(request,id=0, + airportcode=None, + message="",successmessage=""): + row = get_workout(id) + + f1 = row.csvfilename + if (checkworkoutuser(request.user,row)==False): + message = "You are not allowed to edit this workout" + messages.error(request,message) + url = reverse(workouts_view) + + return HttpResponseRedirect(url) + + # create bearing + rowdata = rdata(f1) + if rowdata == 0: + return HttpResponse("Error: CSV Data File Not Found") + + try: + bearing = rowdata.df.loc[:,'bearing'].values + except KeyError: + rowdata.add_bearing() + rowdata.write_csv(f1,gzip=True) + + # get wind + try: + avglat = rowdata.df[' latitude'].mean() + avglon = rowdata.df[' longitude'].mean() + avgtime = int(rowdata.df['TimeStamp (sec)'].mean()-rowdata.df.loc[:,'TimeStamp (sec)'].iloc[0]) + startdatetime = dateutil.parser.parse("{}, {}".format(row.date, + row.starttime)) + + starttimeunix = int(arrow.get(row.startdatetime).timestamp) + #starttimeunix = int(mktime(startdatetime.utctimetuple())) + avgtime = starttimeunix+avgtime + winddata = get_wind_data(avglat,avglon,avgtime) + windspeed = winddata[0] + windbearing = winddata[1] + message = winddata[2] + row.notes += "\n"+message + row.save() + rowdata.add_wind(windspeed,windbearing) + rowdata.write_csv(f1,gzip=True) + + messages.info(request,message) + + kwargs = { + 'id':int(id)} + + url = reverse(workout_wind_view,kwargs=kwargs) + response = HttpResponseRedirect(url) + except KeyError: + message = "No latitude/longitude data" + messages.error(request,message) + kwargs = { + 'id':int(id) + } + url = reverse(workout_wind_view,kwargs=kwargs) + response = HttpResponseRedirect(url) + + + + return response + +# Get weather for given location and date/time +@user_passes_test(ispromember,login_url="/rowers/paidplans",message="This functionality requires a Pro plan or higher",redirect_field_name=None) +def workout_downloadmetar_view(request,id=0, + airportcode=None, + message="",successmessage=""): + row = get_workout(id) + + f1 = row.csvfilename + if (checkworkoutuser(request.user,row)==False): + message = "You are not allowed to edit this workout" + messages.error(request,message) + url = reverse(workouts_view) + + return HttpResponseRedirect(url) + + # create bearing + rowdata = rdata(f1) + if rowdata == 0: + return HttpResponse("Error: CSV Data File Not Found") + + try: + bearing = rowdata.df.loc[:,'bearing'].values + except KeyError: + rowdata.add_bearing() + rowdata.write_csv(f1,gzip=True) + + # get wind + try: + avglat = rowdata.df[' latitude'].mean() + avglon = rowdata.df[' longitude'].mean() + airportcode = get_airport_code(avglat,avglon)[0] + avgtime = int(rowdata.df['TimeStamp (sec)'].mean()-rowdata.df.loc[:,'TimeStamp (sec)'].iloc[0]) + startdatetime = dateutil.parser.parse("{}, {}".format(row.date, + row.starttime)) + + starttimeunix = arrow.get(row.startdatetime).timestamp + #starttimeunix = int(mktime(startdatetime.utctimetuple())) + avgtime = starttimeunix +avgtime + winddata = get_metar_data(airportcode,avgtime) + windspeed = winddata[0] + windbearing = winddata[1] + message = winddata[2] + row.notes += "\n"+message + row.save() + rowdata.add_wind(windspeed,windbearing) + rowdata.write_csv(f1,gzip=True) + messages.info(request,message) + + kwargs = { + 'id':int(id)} + + url = reverse(workout_wind_view,kwargs=kwargs) + response = HttpResponseRedirect(url) + except KeyError: + message = "No latitude/longitude data" + messages.error(request,message) + kwargs = { + 'id':int(id) + } + url = reverse(workout_wind_view,kwargs=kwargs) + response = HttpResponseRedirect(url) + + + + return response + + +# Show form to update wind data +@user_passes_test(ispromember,login_url="/rowers/paidplans",message="This functionality requires a Pro plan or higher",redirect_field_name=None) +def workout_wind_view(request,id=0,message="",successmessage=""): + row = get_workout(id) + r = getrower(request.user) + breadcrumbs = [ + { + 'url':'/rowers/list-workouts/', + 'name':'Workouts' + }, + { + 'url':get_workout_default_page(request,id), + 'name': row.name + }, + { + 'url':reverse(workout_wind_view,kwargs={'id':id}), + 'name': 'Wind' + } + + ] + + if (checkworkoutuser(request.user,row)==False): + message = "You are not allowed to edit this workout" + messages.error(request,message) + url = reverse(workouts_view) + + return HttpResponseRedirect(url) + + + # get data + f1 = row.csvfilename + u = row.user.user + r = getrower(u) + + # create bearing + rowdata = rdata(f1) + if row == 0: + return HttpResponse("Error: CSV Data File Not Found") + + + hascoordinates = 1 + try: + latitude = rowdata.df.loc[:,' latitude'] + except KeyError: + hascoordinates = 0 + + if hascoordinates and not latitude.std(): + hascoordinates = 0 + + try: + bearing = rowdata.df.loc[:,'bearing'].values + except KeyError: + rowdata.add_bearing() + rowdata.write_csv(f1,gzip=True) + + + if hascoordinates: + avglat = rowdata.df[' latitude'].mean() + avglon = rowdata.df[' longitude'].mean() + airportcode,newlat,newlon,airportdistance = get_airport_code(avglat,avglon) + airportcode = airportcode.upper() + airportdistance = airportdistance[0] + else: + airportcode = 'UNKNOWN' + airportdistance = 0 + + + if request.method == 'POST': + # process form + form = UpdateWindForm(request.POST) + + if form.is_valid(): + + vwind1 = form.cleaned_data['vwind1'] + vwind2 = form.cleaned_data['vwind2'] + dist1 = form.cleaned_data['dist1'] + dist2 = form.cleaned_data['dist2'] + winddirection1 = form.cleaned_data['winddirection1'] + winddirection2 = form.cleaned_data['winddirection2'] + windunit = form.cleaned_data['windunit'] + + rowdata.update_wind(vwind1,vwind2, + winddirection1, + winddirection2, + dist1,dist2, + units=windunit) + + rowdata.write_csv(f1,gzip=True) + + + else: + message = "Invalid Form" + messages.error(request,message) + kwargs = { + 'id':int(id) + } + url = reverse(workout_wind_view,kwargs=kwargs) + response = HttpResponseRedirect(url) + + else: + form = UpdateWindForm() + + # create interactive plot + res = interactive_windchart(id,promember=1) + script = res[0] + div = res[1] + + if hascoordinates: + gmscript,gmdiv = leaflet_chart( + rowdata.df[' latitude'], + rowdata.df[' longitude'], + row.name) + else: + gmscript = "" + gmdiv = "No GPS data available" + + + messages.info(request,successmessage) + messages.error(request,message) + + return render(request, + 'windedit.html', + {'workout':row, + 'rower':r, + 'breadcrumbs':breadcrumbs, + 'active':'nav-workouts', + 'teams':get_my_teams(request.user), + 'interactiveplot':script, + 'form':form, + 'airport':airportcode, + 'airportdistance':airportdistance, + 'the_div':div, + 'gmap':gmscript, + 'gmapdiv':gmdiv}) + + +# Show form to update River stream data (for river dwellers) +@user_passes_test(ispromember,login_url="/rowers/paidplans",message="This functionality requires a Pro plan or higher",redirect_field_name=None) +def workout_stream_view(request,id=0,message="",successmessage=""): + row = get_workout(id) + r = getrower(request.user) + + if (checkworkoutuser(request.user,row)==False): + message = "You are not allowed to edit this workout" + messages.error(request,message) + url = reverse(workouts_view) + + return HttpResponseRedirect(url) + + + # create interactive plot + f1 = row.csvfilename + u = row.user.user + r = getrower(u) + + rowdata = rdata(f1) + if rowdata == 0: + return HttpResponse("Error: CSV Data File Not Found") + + if request.method == 'POST': + # process form + form = UpdateStreamForm(request.POST) + + if form.is_valid(): + + dist1 = form.cleaned_data['dist1'] + dist2 = form.cleaned_data['dist2'] + stream1 = form.cleaned_data['stream1'] + stream2 = form.cleaned_data['stream2'] + streamunit = form.cleaned_data['streamunit'] + + rowdata.update_stream(stream1,stream2,dist1,dist2, + units=streamunit) + + rowdata.write_csv(f1,gzip=True) + + + else: + message = "Invalid Form" + messages.error(request,message) + kwargs = { + 'id':int(id)} + url = reverse(workout_wind_view,kwargs=kwargs) + response = HttpResponseRedirect(url) + + else: + form = UpdateStreamForm() + + # create interactive plot + res = interactive_streamchart(id,promember=1) + script = res[0] + div = res[1] + + breadcrumbs = [ + { + 'url':'/rowers/list-workouts/', + 'name':'Workouts' + }, + { + 'url':get_workout_default_page(request,id), + 'name': row.name + }, + { + 'url':reverse(workout_stream_view,kwargs={'id':id}), + 'name': 'Stream' + } + + ] + + messages.info(request,successmessage) + messages.error(request,message) + return render(request, + 'streamedit.html', + {'workout':row, + 'rower':r, + 'breadcrumbs':breadcrumbs, + 'active':'nav-workouts', + 'teams':get_my_teams(request.user), + 'interactiveplot':script, + 'form':form, + 'the_div':div}) + +# Form to set average crew weight and boat type, then run power calcs +@user_passes_test(ispromember, login_url="/rowers/paidplans",redirect_field_name=None) +def workout_otwsetpower_view(request,id=0,message="",successmessage=""): + w = get_workout(id) + r = getrower(request.user) + + mayedit = 0 + if request.user == w.user.user: + mayedit=1 + if checkworkoutuser(request.user,w): + mayedit=1 + + if (checkworkoutuser(request.user,w)==False): + message = "You are not allowed to edit this workout" + messages.error(request,message) + url = reverse(workouts_view) + + return HttpResponseRedirect(url) + + + if request.method == 'POST': + # process form + form = AdvancedWorkoutForm(request.POST) + + if form.is_valid(): + quick_calc = form.cleaned_data['quick_calc'] + boattype = form.cleaned_data['boattype'] + weightvalue = form.cleaned_data['weightvalue'] + w.boattype = boattype + w.weightvalue = weightvalue + w.save() + + + # load row data & create power/wind/bearing columns if not set + f1 = w.csvfilename + rowdata = rdata(f1) + if rowdata == 0: + return HttpResponse("Error: CSV Data File Not Found") + try: + vstream = rowdata.df['vstream'] + except KeyError: + rowdata.add_stream(0) + rowdata.write_csv(f1,gzip=True) + + try: + bearing = rowdata.df['bearing'] + except KeyError: + rowdata.add_bearing() + rowdata.write_csv(f1,gzip=True) + + try: + vwind = rowdata.df['vwind'] + except KeyError: + rowdata.add_wind(0,0) + rowdata.write_csv(f1,gzip=True) + + # do power calculation (asynchronous) + r = w.user + u = r.user + + first_name = u.first_name + last_name = u.last_name + emailaddress = u.email + + job = myqueue(queuelow, + handle_otwsetpower,f1,boattype, + weightvalue, + first_name,last_name,emailaddress,id, + ps=[r.p0,r.p1,r.p2,r.p3], + ratio=r.cpratio, + quick_calc = quick_calc, + emailbounced = r.emailbounced + ) + + try: + request.session['async_tasks'] += [(job.id,'otwsetpower')] + except KeyError: + request.session['async_tasks'] = [(job.id,'otwsetpower')] + + successmessage = 'Your calculations have been submitted. You will receive an email when they are done. You can check the status of your calculations here' + messages.info(request,successmessage) + kwargs = { + 'id':int(id)} + + try: + url = request.session['referer'] + except KeyError: + url = reverse(workout_advanced_view,kwargs=kwargs) + + response = HttpResponseRedirect(url) + return response + + else: + message = "Invalid Form" + messages.error(request,message) + kwargs = { + 'id':int(id)} + url = reverse(workout_otwsetpower_view,kwargs=kwargs) + response = HttpResponseRedirect(url) + + else: + form = AdvancedWorkoutForm(instance=w) + + breadcrumbs = [ + { + 'url':'/rowers/list-workouts/', + 'name':'Workouts' + }, + { + 'url':get_workout_default_page(request,id), + 'name': w.name + }, + { + 'url':reverse(workout_otwsetpower_view,kwargs={'id':id}), + 'name': 'OTW Power' + } + + ] + + + messages.error(request,message) + messages.info(request,successmessage) + return render(request, + 'otwsetpower.html', + {'workout':w, + 'rower':w, + 'mayedit':mayedit, + 'active':'nav-workouts', + 'breadcrumbs':breadcrumbs, + 'teams':get_my_teams(request.user), + 'form':form, + }) + +@login_required() +def instroke_view(request,id=0): + w = get_workout(id) + r = getrower(request.user) + mayedit = 0 + if request.user == w.user.user: + mayedit=1 + if checkworkoutuser(request.user,w): + mayedit=1 + + breadcrumbs = [ + { + 'url':'/rowers/list-workouts/', + 'name':'Workouts' + }, + { + 'url':get_workout_default_page(request,id), + 'name': w.name + }, + { + 'url':reverse(instroke_view,kwargs={'id':id}), + 'name': 'In-Stroke Metrics' + } + + ] + +# form = WorkoutForm(instance=row) + g = GraphImage.objects.filter(workout=w).order_by("-creationdatetime") + # check if user is owner of this workout + + if (checkworkoutuser(request.user,w)==False): + message = "You are not allowed to edit this workout" + messages.error(request,message) + url = reverse(workouts_view) + + return HttpResponseRedirect(url) + + from metrics import nometrics + rowdata = rrdata(csvfile=w.csvfilename) + try: + instrokemetrics = rowdata.get_instroke_columns() + instrokemetrics = [m for m in instrokemetrics if not m in nometrics] + except AttributeError: + instrokemetrics = [] + + + return render(request, + 'instroke.html', + {'workout':w, + 'rower':r, + 'active':'nav-workouts', + 'breadcrumbs':breadcrumbs, + 'mayedit':mayedit, + 'teams':get_my_teams(request.user), + 'instrokemetrics':instrokemetrics, + }) + + +# generate instroke chart +@login_required() +def instroke_chart(request,id=0,metric=''): + w = get_workout(id) + + if (checkworkoutuser(request.user,w)==False): + message = "You are not allowed to edit this workout" + messages.error(request,message) + url = reverse(workouts_view) + + return HttpResponseRedirect(url) + + rowdata = rrdata(csvfile=w.csvfilename) + instrokemetrics = rowdata.get_instroke_columns() + + + if metric in instrokemetrics: + f1 = w.csvfilename[6:-4] + timestr = strftime("%Y%m%d-%H%M%S") + imagename = f1+timestr+'.png' + fullpathimagename = 'static/plots/'+imagename + u = w.user.user + r = getrower(u) + title = w.name + fig1 = rowdata.get_plot_instroke(metric) + canvas = FigureCanvas(fig1) + canvas.print_figure('static/plots/'+imagename) + plt.close(fig1) + fig1.clf() + gc.collect() + + try: + width,height = Image.open(fullpathimagename).size + except: + width = 1200 + height = 600 + + imgs = GraphImage.objects.filter(workout=w) + if len(imgs) < 7: + i = GraphImage(workout=w, + creationdatetime=timezone.now(), + filename=fullpathimagename, + width=width,height=height) + + i.save() + else: + messages.error(request,'You have reached the maximum number of static images for this workout. Delete an image first') + + + r = getrower(request.user) + url = reverse(r.defaultlandingpage, + kwargs = { + 'id':id, + }) + + return HttpResponseRedirect(url) + + +# data explorer +@login_required() +def workout_data_view(request, id=0): + + r = getrower(request.user) + w = get_workout(id) + + if not checkworkoutuser(request.user,w): + raise PermissionDenied('Access Denied') + + breadcrumbs = [ + { + 'url':'/rowers/list-workouts/', + 'name':'Workouts' + }, + { + 'url':get_workout_default_page(request,id), + 'name': w.name + }, + { + 'url':reverse(workout_data_view,kwargs={'id':id}), + 'name': 'Data Explorer' + } + + ] + + + datadf,row = dataprep.getrowdata_db(id=id) + + + datadf.sort_values(['ftime'],inplace=True) + + columns = datadf.columns.values + + to_be_dropped = [ + 'id','time','hr_an','hr_at','hr_bottom','hr_max', + 'hr_tr','hr_ut1','hr_ut2','x_right', + ] + + to_be_dropped = [c for c in to_be_dropped if c in columns] + + datadf.drop(labels=to_be_dropped,inplace=True,axis=1) + + + cols = ['ftime','cumdist','fpace','spm', + 'hr','power','driveenergy','drivelength','averageforce', + 'peakforce','distance','drivespeed','workoutstate', + 'catch','finish','peakforceangle','wash','slip','rhythm', + 'effectiveangle','totalangle','distanceperstroke','velo'] + + + tcols = ['ftime','cumdist','fpace','spm','hr','power'] + + datadf = datadf[cols] + datadf.loc[:,'hr'] = datadf['hr'].astype('int') + datadf.loc[:,'power'] = datadf['power'].astype('int') + datadf.loc[:,'distance'] = datadf['distance'].astype('int') + datadf.loc[:,'spm'] = 10*datadf['spm'].astype('int')/10. + + + if request.method == 'POST': + form = DataFrameColumnsForm(request.POST) + if form.is_valid(): + tcols = form.cleaned_data['cols'] + + else: + form = DataFrameColumnsForm(initial = {'cols':tcols}) + + datadf = datadf[tcols] + + for col in cols: + try: + if datadf[col].mean() == 0 and datadf[col].std() == 0: + datadf.drop(labels=[col],axis=1,inplace=True) + except (TypeError,KeyError): + pass + + # pd.set_option('display.width', 1000) + pd.set_option('colheader_justify', 'left') + + htmltable = datadf.to_html( + bold_rows=True, + show_dimensions=True,border=1, + classes='pandastable',justify='justify' + ) + + return render(request, + 'workout_data.html', + { + 'htmltable': htmltable, + 'form':form, + 'teams':get_my_teams(request.user), + 'workout': w, + 'breadcrumbs': breadcrumbs, + + } + ) + + +# Stats page +@login_required() +def workout_stats_view(request,id=0,message="",successmessage=""): + + r = getrower(request.user) + w = get_workout(id) + + mayedit = 0 + if request.user == w.user.user: + mayedit=1 + if checkworkoutuser(request.user,w): + mayedit=1 + + breadcrumbs = [ + { + 'url':'/rowers/list-workouts/', + 'name':'Workouts' + }, + { + 'url':get_workout_default_page(request,id), + 'name': w.name + }, + { + 'url':reverse(workout_stats_view,kwargs={'id':id}), + 'name': 'Stats' + } + + ] + + workstrokesonly = True + if request.method == 'POST' and 'workstrokesonly' in request.POST: + workstrokesonly = str2bool(request.POST['workstrokesonly']) + + + # prepare data frame + datadf,row = dataprep.getrowdata_db(id=id) + if (checkworkoutuser(request.user,row)==False): + raise PermissionDenied('Access Denied') + + datadf = dataprep.clean_df_stats(datadf,workstrokesonly=workstrokesonly) + + if datadf.empty: + datadf,row = dataprep.getrowdata_db(id=id) + datadf = dataprep.clean_df_stats(datadf,workstrokesonly=False) + workstrokesonly=False + if datadf.empty: + return HttpResponse("CSV data file not found") + + #datadf['deltat'] = datadf['time'].diff() + + + workoutstateswork = [1,4,5,8,9,6,7] + workoutstatesrest = [3] + workoutstatetransition = [0,2,10,11,12,13] + + + # Create stats + stats = {} + + fieldlist,fielddict = dataprep.getstatsfields() + fielddict.pop('workoutstate') + fielddict.pop('workoutid') + + for field,verbosename in fielddict.iteritems(): + thedict = { + 'mean':datadf[field].mean(), + 'wmean': wavg(datadf, field, 'deltat'), + 'min': datadf[field].min(), + 'std': datadf[field].std(), + 'max': datadf[field].max(), + 'median': datadf[field].median(), + 'firstq':datadf[field].quantile(q=0.25), + 'thirdq':datadf[field].quantile(q=0.75), + 'verbosename':verbosename, + } + stats[field] = thedict + + # Create a dict with correlation values + cor = datadf.corr(method='spearman') + cor.fillna(value=0,inplace=True) + cordict = {} + for field1,verbosename in fielddict.iteritems(): + thedict = {} + for field2,verbosename in fielddict.iteritems(): + try: + thedict[field2] = cor.loc[field1,field2] + except KeyError: + thedict[field2] = 0 + + cordict[field1] = thedict + + # additional non-automated stats + otherstats = {} + + # Normalized power & TSS + tss,normp = dataprep.workout_rscore(w) + + + if not np.isnan(tss) and tss != 0: + otherstats['tss'] = { + 'verbose_name':'rScore', + 'value':int(tss), + 'unit':'' + } + + if not np.isnan(normp): + otherstats['np'] = { + 'verbose_name':'rPower', + 'value':int(10*normp)/10., + 'unit':'Watt' + } + + # HR Drift + tmax = datadf['time'].max() + tmin = datadf['time'].min() + thalf = tmin+0.5*(tmax-tmin) + mask1 = datadf['time'] < thalf + mask2 = datadf['time'] > thalf + + hr1 = datadf.loc[mask1,'hr'].mean() + hr2 = datadf.loc[mask2,'hr'].mean() + + pwr1 = datadf.loc[mask1,'power'].mean() + pwr2 = datadf.loc[mask2,'power'].mean() + + try: + hrdrift = ((pwr1/hr1)-(pwr2/hr2))/(pwr1/hr1) + hrdrift *= 100. + if not np.isnan(hrdrift): + hrdrift = int(100*hrdrift)/100. + otherstats['hrdrift'] = { + 'verbose_name': 'Heart Rate Drift', + 'value': hrdrift, + 'unit': '%', + } + except ZeroDivisionError,ValueError: + pass + + # TRIMP + trimp,hrtss = dataprep.workout_trimp(w) + + otherstats['trimp'] = { + 'verbose_name': 'TRIMP', + 'value': trimp, + 'unit': '' + } + + otherstats['hrScore'] = { + 'verbose_name': 'rScore (HR)', + 'value': hrtss, + 'unit':'' + } + + return render(request, + 'workoutstats.html', + { + 'stats':stats, + 'teams':get_my_teams(request.user), + 'workout':w, + 'rower':r, + 'mayedit':mayedit, + 'breadcrumbs':breadcrumbs, + 'active':'nav-workouts', + 'workstrokesonly':workstrokesonly, + 'cordict':cordict, + 'otherstats':otherstats, + }) + + + +# Change default landing page +@login_required() +def workflow_default_view(request): + r = getrower(request.user) + if r.defaultlandingpage == 'workout_edit_view': + r.defaultlandingpage = 'workout_workflow_view' + else: + r.defaultlandingpage = 'workout_edit_view' + + r.save() + + url = reverse(workout_workflow_config2_view) + + return HttpResponseRedirect(url) + +def get_workout_default_page(request,id): + if request.user.is_anonymous(): + return reverse(workout_view,kwargs={'id':str(id)}) + else: + r = Rower.objects.get(user=request.user) + if r.defaultlandingpage == 'workout_edit_view': + return reverse(workout_edit_view,kwargs={'id':str(id)}) + else: + return reverse(workout_workflow_view,kwargs={'id':str(id)}) + +# Workflow configuration +@login_required() +def workout_workflow_config2_view(request,userid=0): + request.session['referer'] = absolute(request)['PATH'] + request.session[translation.LANGUAGE_SESSION_KEY] = USER_LANGUAGE + try: + workoutid = request.session['lastworkout'] + except KeyError: + workoutid = 0 + + + r = getrequestrower(request,userid=userid,notpermanent=True) + + MiddlePanelFormSet = formset_factory(WorkFlowMiddlePanelElement,extra=1) + LeftPanelFormSet = formset_factory(WorkFlowLeftPanelElement,extra=1) + + + if request.method == 'POST': + wasmiddle = [1 for key,value in request.POST.items() if 'middlepanel' in key.lower()] + wasleft = [1 for key,valye in request.POST.items() if 'leftpanel' in key.lower()] + if wasmiddle: + middlepanel_formset = MiddlePanelFormSet(request.POST, + prefix='middlepanel') + newmiddlepanel = [] + if middlepanel_formset.is_valid(): + for form in middlepanel_formset: + value = form.cleaned_data.get('panel') + if value != 'None': + newmiddlepanel.append(value) + + + newmiddlepanel = [i for i in newmiddlepanel if i != None] + r.workflowmiddlepanel = newmiddlepanel + try: + r.save() + except IntegrityError: + messages.error(request,'Something went wrong') + if wasleft: + leftpanel_formset = LeftPanelFormSet(request.POST, + prefix='leftpanel') + newleftpanel = [] + if leftpanel_formset.is_valid(): + for form in leftpanel_formset: + value = form.cleaned_data.get('panel') + if value != 'None': + newleftpanel.append(value) + + + newleftpanel = [i for i in newleftpanel if i != None] + r.workflowleftpanel = newleftpanel + try: + r.save() + except IntegrityError: + messages.error(request,'Something went wrong') + + leftpanelform_data = [{'panel':panel} + for panel in r.workflowleftpanel] + + + middlepanelform_data = [{'panel':panel} + for panel in r.workflowmiddlepanel] + + leftpanel_formset = LeftPanelFormSet(initial=leftpanelform_data, + prefix='leftpanel') + middlepanel_formset = MiddlePanelFormSet(initial=middlepanelform_data, + prefix='middlepanel') + + + tmplt = 'workflowconfig2.html' + + return render(request,tmplt, + { + 'rower':r, + 'leftpanel_formset':leftpanel_formset, + 'middlepanel_formset':middlepanel_formset, + 'workoutid': workoutid, + }) + + +def getfavorites(r,row): + workouttype = 'ote' + if row.workouttype in mytypes.otwtypes: + workouttype = 'otw' + + matchworkouttypes = [workouttype,'all'] + + workoutsource = row.workoutsource + if 'speedcoach2' in row.workoutsource: + workoutsource = 'speedcoach2' + + try: + favorites = FavoriteChart.objects.filter(user=r, + workouttype__in=matchworkouttypes).order_by("id") + favorites2 = FavoriteChart.objects.filter(user=r, + workouttype__in=[workoutsource]).order_by("id") + + favorites = favorites | favorites2 + + + maxfav = len(favorites)-1 + except: + favorites = None + maxfav = 0 + + return favorites,maxfav + + +# Workflow View +@login_required() +def workout_workflow_view(request,id): + request.session['referer'] = absolute(request)['PATH'] + request.session['lastworkout'] = id + request.session[translation.LANGUAGE_SESSION_KEY] = USER_LANGUAGE + row = get_workout_permitted(request.user,id) + + r = getrower(request.user) + result = request.user.is_authenticated() and ispromember(request.user) + if result: + promember=1 + if request.user == row.user.user: + mayedit=1 + + comments = WorkoutComment.objects.filter(workout=row) + + aantalcomments = len(comments) + + favorites,maxfav = getfavorites(r,row) + + + charts = get_call() + + + if 'panel_map.html' in r.workflowmiddlepanel and rowhascoordinates(row): + rowdata = rdata(row.csvfilename) + mapscript,mapdiv = leaflet_chart2(rowdata.df[' latitude'], + rowdata.df[' longitude'], + row.name) + else: + mapscript = '' + mapdiv = '' + + + + statcharts = GraphImage.objects.filter(workout=row) + + + middleTemplates = [] + for t in r.workflowmiddlepanel: + try: + template.loader.get_template(t) + middleTemplates.append(t) + except template.TemplateDoesNotExist: + pass + + leftTemplates = [] + for t in r.workflowleftpanel: + try: + template.loader.get_template(t) + leftTemplates.append(t) + except template.TemplateDoesNotExist: + pass + + + breadcrumbs = [ + { + 'url':'/rowers/list-workouts/', + 'name':'Workouts' + }, + { + 'url':get_workout_default_page(request,id), + 'name': row.name + }, + { + 'url':reverse(workout_workflow_view,kwargs={'id':id}), + 'name': 'View' + } + + ] + + return render(request, + 'workflow.html', + { + 'middleTemplates':middleTemplates, + 'leftTemplates':leftTemplates, + 'active':'nav-workouts', + 'breadcrumbs':breadcrumbs, + 'charts':charts, + 'workout':row, + 'mapscript':mapscript, + 'mapdiv':mapdiv, + 'statcharts':statcharts, + 'rower':r, + 'aantalcomments':aantalcomments, + }) + +# The famous flex chart +@login_required() +def workout_flexchart3_view(request,*args,**kwargs): + + try: + id = kwargs['id'] + except KeyError: + raise Http404("Invalid workout number") + + if 'promember' in kwargs: + promember = kwargs['promember'] + else: + promember = 0 + + try: + favoritenr = int(request.GET['favoritechart']) + except: + favoritenr = -1 + + row = get_workout(id) + + promember=0 + mayedit=0 + if not request.user.is_anonymous(): + r = getrower(request.user) + result = request.user.is_authenticated() and ispromember(request.user) + if result: + promember=1 + if request.user == row.user.user: + mayedit=1 + if checkworkoutuser(request.user,row): + mayedit=1 + + workouttype = 'ote' + if row.workouttype in mytypes.otwtypes: + workouttype = 'otw' + + favorites,maxfav = getfavorites(r,row) + + # check if favoritenr is not out of range + if favorites: + try: + t = favorites[favoritenr].xparam + except IndexError: + favoritenr=0 + except AssertionError: + favoritenr=0 + + if 'xparam' in kwargs: + xparam = kwargs['xparam'] + else: + if favorites: + xparam = favorites[favoritenr].xparam + else: + xparam = 'distance' + + if 'yparam1' in kwargs: + yparam1 = kwargs['yparam1'] + else: + if favorites: + yparam1 = favorites[favoritenr].yparam1 + else: + yparam1 = 'pace' + + if 'yparam2' in kwargs: + yparam2 = kwargs['yparam2'] + if yparam2 == '': + yparam2 = 'None' + else: + if favorites: + yparam2 = favorites[favoritenr].yparam2 + if yparam2 == '': + yparam2 = 'None' + else: + yparam2 = 'hr' + + if not request.user.is_anonymous(): + r = getrower(request.user) + if favoritenr>=0 and r.showfavoritechartnotes: + try: + favoritechartnotes = favorites[favoritenr].notes + except IndexError: + favoritechartnotes = '' + else: + favoritechartnotes = '' + else: + favoritechartnotes = '' + favoritenr = 0 + + if 'plottype' in kwargs: + plottype = kwargs['plottype'] + else: + if favorites: + plottype = favorites[favoritenr].plottype + else: + plottype = 'line' + + if 'workstrokesonly' in kwargs: + workstrokesonly = kwargs['workstrokesonly'] + else: + if favorites: + workstrokesonly = not favorites[favoritenr].reststrokes + else: + workstrokesonly = False + + if request.method == 'POST' and 'savefavorite' in request.POST: + if not request.user.is_anonymous(): + workstrokesonly = request.POST['workstrokesonlysave'] + reststrokes = not workstrokesonly + r = getrower(request.user) + try: + range = metrics.yaxmaxima[xparam] + if yparam1 is not None: + range = metrics.yaxmaxima[yparam1] + if yparam2 is not None: + range = metrics.yaxmaxima[yparam2] + f = FavoriteChart(user=r,xparam=xparam, + yparam1=yparam1,yparam2=yparam2, + plottype=plottype,workouttype=workouttype, + reststrokes=reststrokes) + f.save() + + except KeyError: + messages.error(request,'We cannot save the ad hoc metrics in a favorite chart') + + if request.method == 'POST' and 'xaxis' in request.POST: + flexoptionsform = FlexOptionsForm(request.POST) + if flexoptionsform.is_valid(): + cd = flexoptionsform.cleaned_data + includereststrokes = cd['includereststrokes'] + plottype = cd['plottype'] + + workstrokesonly = not includereststrokes + + flexaxesform = FlexAxesForm(request,request.POST) + + if flexaxesform.is_valid(): + cd = flexaxesform.cleaned_data + xparam = cd['xaxis'] + yparam1 = cd['yaxis1'] + yparam2 = cd['yaxis2'] + else: + pass + + + if not promember: + for name,d in rowingmetrics: + if d['type'] != 'basic': + if xparam == name: + xparam = 'time' + messages.info(request,'To use '+d['verbose_name']+', you have to be Pro member') + if yparam1 == name: + yparam1 = 'pace' + messages.info(request,'To use '+d['verbose_name']+', you have to be Pro member') + if yparam2 == name: + yparam2 = 'spm' + messages.info(request,'To use '+d['verbose_name']+', you have to be Pro member') + + # bring back slashes +# yparam1 = yparam1.replace('_slsh_','/') +# yparam2 = yparam2.replace('_slsh_','/') +# xparam = xparam.replace('_slsh_','/') + + # create interactive plot + try: + ( + script, + div, + js_resources, + css_resources, + workstrokesonly + ) = interactive_flex_chart2( + id,xparam=xparam,yparam1=yparam1, + yparam2=yparam2, + promember=promember,plottype=plottype, + workstrokesonly=workstrokesonly + ) + except ValueError: + ( + script, + div, + js_resources, + css_resources, + workstrokesonly + ) = interactive_flex_chart2( + id,xparam=xparam,yparam1=yparam1, + yparam2=yparam2, + promember=promember,plottype=plottype, + workstrokesonly=workstrokesonly + ) + js_resources = "" + css_resources = "" + + + axchoicesbasic = {ax[0]:ax[1] for ax in axes if ax[4]=='basic'} + axchoicespro = {ax[0]:ax[1] for ax in axes if ax[4]=='pro'} + noylist = ["time","distance"] + axchoicesbasic.pop("cumdist") + + if row.workouttype in mytypes.otwtypes: + for name,d in rowingmetrics: + if d['mode'] == 'erg': + axchoicespro.pop(name) + + else: + for name,d in rowingmetrics: + if d['mode'] == 'water': + axchoicespro.pop(name) + + from rowers.metrics import nometrics + + rowdata = rdata(row.csvfilename) + try: + rowdata.set_instroke_metrics() + except AttributeError: + pass + try: + additionalmetrics = rowdata.get_additional_metrics() + additionalmetrics = [m for m in additionalmetrics if not m in nometrics] + except AttributeError: + additionalmetrics = [] + + + # extrametrics = {m.replace('/','_slsh_'):m for m in additionalmetrics} + extrametrics = additionalmetrics + + # xparam = xparam.replace('/','_slsh_') + # yparam1 = yparam1.replace('/','_slsh_') + # yparam2 = yparam2.replace('/','_slsh_') + + +# for metric in nometrics: +# try: +# extrametrics.pop(metric) +# except KeyError: +# pass + + initial = { + 'xaxis':xparam, + 'yaxis1':yparam1, + 'yaxis2':yparam2, + } + flexaxesform = FlexAxesForm(request,initial=initial, + extrametrics=extrametrics) + + initial = { + 'includereststrokes': not workstrokesonly, + 'plottype':plottype + } + + flexoptionsform = FlexOptionsForm(initial=initial) + + row = Workout.objects.get(id=id) + + breadcrumbs = [ + { + 'url':'/rowers/list-workouts/', + 'name':'Workouts' + }, + { + 'url':get_workout_default_page(request,id), + 'name': row.name + }, + { + 'url':reverse(workout_flexchart3_view,kwargs=kwargs), + 'name': 'Flex Chart' + } + + ] + + + return render(request, + 'flexchart3otw.html', + {'the_script':script, + 'the_div':div, + 'breadcrumbs':breadcrumbs, + 'rower':r, + 'active':'nav-workouts', + 'workout':row, + 'chartform':flexaxesform, + 'optionsform':flexoptionsform, + 'js_res': js_resources, + 'css_res':css_resources, + 'teams':get_my_teams(request.user), + 'id':int(id), + 'xparam':xparam, + 'yparam1':yparam1, + 'yparam2':yparam2, + 'plottype':plottype, + 'axchoicesbasic':axchoicesbasic, + 'axchoicespro':axchoicespro, + 'extrametrics':extrametrics, + 'favoritechartnotes':favoritechartnotes, + 'noylist':noylist, + 'mayedit':mayedit, + 'promember':promember, + 'workstrokesonly': not workstrokesonly, + 'favoritenr':favoritenr, + 'maxfav':maxfav, + }) + + + +# The interactive plot with wind corrected pace for OTW outings +def workout_otwpowerplot_view(request,id=0,message="",successmessage=""): + w = get_workout(id) + r = getrower(request.user) + + breadcrumbs = [ + { + 'url':'/rowers/list-workouts/', + 'name':'Workouts' + }, + { + 'url':get_workout_default_page(request,id), + 'name': w.name + }, + { + 'url':reverse(workout_otwpowerplot_view,kwargs={'id':id}), + 'name': 'Interactive OTW Power Plot' + } + + ] + + # check if user is owner of this workout + + + # create interactive plot + f1 = w.csvfilename + u = w.user.user + # r = getrower(u) + + promember=0 + mayedit=0 + if not request.user.is_anonymous(): + r = getrower(request.user) + result = request.user.is_authenticated() and ispromember(request.user) + if result: + promember=1 + if request.user == w.user.user: + mayedit=1 + + # create interactive plot + res = interactive_otw_advanced_pace_chart(id,promember=promember) + script = res[0] + div = res[1] + + messages.error(request,message) + messages.info(request,successmessage) + + return render(request, + 'otwinteractive.html', + {'workout':w, + 'rower':r, + 'active':'nav-workouts', + 'breadcrumbs':breadcrumbs, + 'teams':get_my_teams(request.user), + 'interactiveplot':script, + 'the_div':div, + 'mayedit':mayedit}) + + +# +@login_required() +def workout_unsubscribe_view(request,id=0): + w = get_workout(id) + + if w.privacy == 'private' and w.user.user != request.user: + return HttpResponseForbidden("Permission error") + + comments = WorkoutComment.objects.filter(workout=w, + user=request.user).order_by("created") + + for c in comments: + c.notification = False + c.save() + + form = WorkoutCommentForm() + + successmessage = 'You have been unsubscribed from new comment notifications for this workout' + + messages.info(request,successmessage) + + return render(request, + 'workout_comments.html', + {'workout':w, + 'teams':get_my_teams(request.user), + 'comments':comments, + 'form':form, + }) + + +# list of comments to a workout +@login_required() +def workout_comment_view(request,id=0): + w = get_workout(id) + + if w.privacy == 'private' and w.user.user != request.user: + return HttpResponseForbidden("Permission error") + + comments = WorkoutComment.objects.filter(workout=w).order_by("created") + + # ok we're permitted + if request.method == 'POST': + r = w.user + form = WorkoutCommentForm(request.POST) + if form.is_valid(): + cd = form.cleaned_data + comment = cd['comment'] + comment = bleach.clean(comment) + if isinstance(comment,unicode): + comment = comment.encode('utf8') + elif isinstance(comment, str): + comment = comment.decode('utf8') + + notification = cd['notification'] + c = WorkoutComment(workout=w,user=request.user,comment=comment, + notification=notification) + c.save() + url = reverse(workout_comment_view, + kwargs={ + 'id':id + }) + message = '{name} says: {comment}'.format( + name = request.user.first_name, + comment = comment, + url = url, + ) + if request.user != r.user: + a_messages.info(r.user,message.encode('ascii','ignore')) + + res = myqueue(queuehigh, + handle_sendemailnewcomment,r.user.first_name, + r.user.last_name, + r.user.email, + request.user.first_name, + request.user.last_name, + comment,w.name,w.id, + emailbounced = r.emailbounced + ) + + commenters = {oc.user for oc in comments if oc.notification} + for u in commenters: + a_messages.info(u,message) + if u != request.user and u != r.user: + ocr = Rower.objects.get(user=u) + res = myqueue(queuelow, + handle_sendemailnewresponse, + u.first_name, + u.last_name, + u.email, + request.user.first_name, + request.user.last_name, + comment, + w.name, + w.id, + c.id, + emailbounced = ocr.emailbounced + ) + + url = reverse(workout_comment_view,kwargs = { + 'id':id}) + return HttpResponseRedirect(url) + + form = WorkoutCommentForm() + + g = GraphImage.objects.filter(workout=w).order_by("-creationdatetime") + for i in g: + try: + width,height = Image.open(i.filename).size + i.width = width + i.height = height + i.save() + except: + pass + + rower = getrower(request.user) + + breadcrumbs = [ + { + 'url':'/rowers/list-workouts/', + 'name':'Workouts' + }, + { + 'url':get_workout_default_page(request,id), + 'name': w.name + }, + { + 'url':reverse(workout_comment_view,kwargs={'id':id}), + 'name': 'Comments' + } + + ] + + + return render(request, + 'workout_comments.html', + {'workout':w, + 'rower':rower, + 'breadcrumbs':breadcrumbs, + 'active':'nav-workouts', + 'teams':get_my_teams(request.user), + 'graphs':g, + 'comments':comments, + 'form':form, + }) + + + +# The basic edit page +@login_required() +def workout_edit_view(request,id=0,message="",successmessage=""): + request.session[translation.LANGUAGE_SESSION_KEY] = USER_LANGUAGE + request.session['referer'] = absolute(request)['PATH'] + + + row = get_workout(id) + + + if (checkworkoutuser(request.user,row)==False): + raise PermissionDenied("Access denied") + + form = WorkoutForm(instance=row) + + if request.method == 'POST': + # Form was submitted + form = WorkoutForm(request.POST,instance=row) + if form.is_valid(): + # Get values from form + name = form.cleaned_data['name'] + date = form.cleaned_data['date'] + starttime = form.cleaned_data['starttime'] + workouttype = form.cleaned_data['workouttype'] + weightcategory = form.cleaned_data['weightcategory'] + adaptiveclass = form.cleaned_data['adaptiveclass'] + duration = form.cleaned_data['duration'] + distance = form.cleaned_data['distance'] + private = form.cleaned_data['private'] + notes = form.cleaned_data['notes'] + thetimezone = form.cleaned_data['timezone'] + + try: + ps = form.cleaned_data['plannedsession'] + except KeyError: + ps = None + + try: + boattype = request.POST['boattype'] + except KeyError: + boattype = Workout.objects.get(id=id).boattype + try: + privacy = request.POST['privacy'] + except KeyError: + privacy = Workout.objects.get(id=id).privacy + try: + rankingpiece = form.cleaned_data['rankingpiece'] + except KeyError: + rankingpiece =- Workout.objects.get(id=id).rankingpiece + + try: + duplicate = form.cleaned_data['duplicate'] + except KeyError: + duplicate = Workout.objects.get(id=id).duplicate + + if private: + privacy = 'private' + else: + privacy = 'visible' + + startdatetime = datetime.datetime.combine( + date,starttime + ) + + try: + startdatetime = pytz.timezone(thetimezone).localize( + startdatetime + ) + except UnknownTimeZoneError: + pass + + try: + # aware object can be in any timezone + out = startdatetime.astimezone(pytz.utc) + except (ValueError, TypeError): + startdatetime = timezone.make_aware(startdatetime) + + try: + startdatetime = startdatetime.astimezone(pytz.timezone(thetimezone)) + except UnknownTimeZoneError: + thetimezone = 'UTC' + + + + row.name = name + row.date = date + row.starttime = starttime + row.startdatetime = startdatetime + row.workouttype = workouttype + row.weightcategory = weightcategory + row.adaptiveclass = adaptiveclass + row.notes = notes + row.duration = duration + row.distance = distance + row.boattype = boattype + row.duplicate = duplicate + row.privacy = privacy + row.rankingpiece = rankingpiece + row.timezone = thetimezone + row.plannedsession = ps + + try: + row.save() + except IntegrityError: + pass + + if ps: + add_workouts_plannedsession([row],ps,row.user) + + # change data in csv file + + r = rdata(row.csvfilename) + if r == 0: + return HttpResponse("Error: CSV Data File Not Found") + r.rowdatetime = startdatetime + r.write_csv(row.csvfilename,gzip=True) + dataprep.update_strokedata(id,r.df) + successmessage = "Changes saved" + + if rankingpiece: + dataprep.runcpupdate(row.user,type=row.workouttype) + + messages.info(request,successmessage) + url = reverse(workout_edit_view, + kwargs = { + 'id':str(row.id), + }) + response = HttpResponseRedirect(url) + + #else: # form not POSTed + form = WorkoutForm(instance=row) + + row = get_workout(id) + + g = GraphImage.objects.filter(workout=row).order_by("-creationdatetime") + for i in g: + try: + width,height = Image.open(i.filename).size + i.width = width + i.height = height + i.save() + except: + pass + + + # create interactive plot + f1 = row.csvfilename + u = row.user.user + r = getrower(u) + rowdata = rdata(f1) + hascoordinates = 1 + if rowdata != 0: + try: + latitude = rowdata.df[' latitude'] + longitude = rowdata.df[' longitude'] + if not latitude.std(): + hascoordinates = 0 + if not longitude.std(): + hascoordinates = 0 + except (KeyError,AttributeError): + hascoordinates = 0 + + else: + hascoordinates = 0 + + + mapscript = "" + mapdiv = "" + + if hascoordinates: + try: + mapscript,mapdiv = leaflet_chart( + rowdata.df[' latitude'], + rowdata.df[' longitude'], + row.name) + except KeyError: + pass + + breadcrumbs = [ + { + 'url':'/rowers/list-workouts/', + 'name':'Workouts' + }, + { + 'url':get_workout_default_page(request,id), + 'name': row.name + }, + { + 'url':reverse(workout_edit_view,kwargs={'id':id}), + 'name': 'Edit' + } + + ] + + if row.workouttype in mytypes.otetypes: + indoorraces = get_indoorraces(row) + else: + indoorraces = [] + + r = getrower(request.user) + + # render page + return render(request, 'workout_form.html', + {'form':form, + 'workout':row, + 'teams':get_my_teams(request.user), + 'graphs':g, + 'breadcrumbs':breadcrumbs, + 'rower':r, + 'indoorraces':indoorraces, + 'active':'nav-workouts', + 'mapscript':mapscript, + 'mapdiv':mapdiv, + 'rower':r, + }) + + + + +@login_required() +def workout_map_view(request,id=0): + request.session[translation.LANGUAGE_SESSION_KEY] = USER_LANGUAGE + request.session['referer'] = absolute(request)['PATH'] + + w = get_workout(id) + + breadcrumbs = [ + { + 'url':'/rowers/list-workouts/', + 'name':'Workouts' + }, + { + 'url':get_workout_default_page(request,id), + 'name': w.name + }, + { + 'url':reverse(workout_map_view,kwargs={'id':id}), + 'name': 'Map' + } + + ] + + + # create interactive plot + f1 = w.csvfilename + u = w.user.user + r = getrower(u) + rowdata = rdata(f1) + hascoordinates = 1 + if rowdata != 0: + try: + latitude = rowdata.df[' latitude'] + if not latitude.std(): + hascoordinates = 0 + except KeyError,AttributeError: + hascoordinates = 0 + + else: + hascoordinates = 0 + + + if hascoordinates: + mapscript,mapdiv = leaflet_chart2(rowdata.df[' latitude'], + rowdata.df[' longitude'], + w.name) + else: + mapscript = "" + mapdiv = "" + + mayedit=0 + if not request.user.is_anonymous(): + r = getrower(request.user) + result = request.user.is_authenticated() and ispromember(request.user) + if result: + promember=1 + if request.user == w.user.user: + mayedit=1 + + return render(request, 'map_view.html', + {'mapscript':mapscript, + 'workout':w, + 'rower':r, + 'breadcrumbs':breadcrumbs, + 'active':'nav-workouts', + 'mapdiv':mapdiv, + 'mayedit':mayedit, + }) + + + + +# Image upload +@login_required() +def workout_uploadimage_view(request,id): + is_ajax = False + if request.is_ajax(): + is_ajax = True + + r = getrower(request.user) + + w = get_workout(id) + + breadcrumbs = [ + { + 'url':'/rowers/list-workouts/', + 'name':'Workouts' + }, + { + 'url':get_workout_default_page(request,id), + 'name': w.name + }, + { + 'url':reverse(workout_uploadimage_view,kwargs={'id':id}), + 'name': 'Upload Image' + } + + ] + + if not checkworkoutuser(request.user,w): + raise PermissionDenied("You are not allowed to edit this workout") + + images = GraphImage.objects.filter(workout=w) + + + if len(images) >= 6: + message = "You have reached the maximum number of static images for this workout" + messages.error(request,message) + url = reverse(r.defaultlandingpage, + kwargs = { + 'id':int(id), + }) + return HttpResponseRedirect(url) + + + if request.method == 'POST': + form = ImageForm(request.POST,request.FILES) + + if form.is_valid(): + f = form.cleaned_data['file'] + + if f is not None: + filename,path_and_filename = handle_uploaded_image(f) + try: + width,height = Image.open(path_and_filename).size + except: + message = "Not a valid image" + messages.error(request,message) + os.remove(path_and_filename) + url = reverse(workout_uploadimage_view, + kwargs = {'id':id}) + + if is_ajax: + return JSONResponse({'result':0,'url':0}) + else: + return HttpResponseRedirect(url) + + i = GraphImage(workout=w, + creationdatetime=timezone.now(), + filename = path_and_filename, + width=width,height=height) + i.save() + + url = reverse(r.defaultlandingpage, + kwargs = {'id':id}) + if is_ajax: + return JSONResponse({'result':1,'url':url}) + else: + return HttpResponseRedirect(url) + else: + messages.error(request,'Something went wrong - no file attached') + url = reverse(workout_uploadimage_view, + kwargs = {'id':id}) + + if is_ajax: + return JSONResponse({'result':0,'url':0}) + else: + return HttpResponseRedirect(url) + else: + return HttpResponse("Form is not valid") + + else: + if not is_ajax: + form = ImageForm() + return render(request,'image_form.html', + {'form':form, + 'rower':r, + 'active':'nav-workouts', + 'breadcrumbs':breadcrumbs, + 'teams':get_my_teams(request.user), + 'workout': w, + }) + else: + return {'result':0} + + + +# Generic chart creation +@login_required() +def workout_add_chart_view(request,id,plotnr=1): + + w = get_workout(id) + r = getrower(request.user) + + plotnr = int(plotnr) + + if (checkworkoutuser(request.user,w)==False): + raise PermissionDenied("You are not allowed add plots to this workout") + else: + f1 = w.csvfilename[6:-4] + timestr = strftime("%Y%m%d-%H%M%S") + imagename = f1+timestr+'.png' + u = w.user.user + r = getrower(u) + title = w.name + res,jobid = uploads.make_plot( + r,w,f1,w.csvfilename,'timeplot',title,plotnr=plotnr, + imagename=imagename + ) + if res == 0: + messages.error(request,jobid) + else: + try: + request.session['async_tasks'] += [(jobid,'make_plot')] + except KeyError: + request.session['async_tasks'] = [(jobid,'make_plot')] + + + url = reverse(r.defaultlandingpage,kwargs={'id':str(w.id)}) + + return HttpResponseRedirect(url) + + + + + +@login_required +def workout_toggle_ranking(request,id=0): + is_ajax = False + if request.is_ajax(): + is_ajax = True + + row = get_workout_permitted(request.user,id) + + row.rankingpiece = not row.rankingpiece + row.save() + + if is_ajax: + response = JSONResponse({'result':row.rankingpiece}, + content_type='application/json') + + return response + else: + url = reverse(workouts_view) + response = HttpResponseRedirect(url) + + return response + + +# This is the main view for processing uploaded files +@login_required() +def workout_upload_view(request, + uploadoptions={ + 'makeprivate':False, + 'make_plot':False, + 'upload_to_C2':False, + 'plottype':'timeplot', + 'landingpage':'workout_edit_view', + }, + docformoptions={ + 'workouttype':'rower', + }): + + is_ajax = False + if request.is_ajax(): + is_ajax = True + + r = getrower(request.user) + + breadcrumbs = [ + { + 'url':'/rowers/list-workouts/', + 'name':'Workouts' + }, + { + 'url': reverse(workout_upload_view), + 'name': 'Upload' + } + ] + + if 'uploadoptions' in request.session: + uploadoptions = request.session['uploadoptions'] + try: + defaultlandingpage = uploadoptions['landingpage'] + except KeyError: + uploadoptions['landingpage'] = r.defaultlandingpage + defaultlandingpage = r.defaultlandingpage + else: + request.session['uploadoptions'] = uploadoptions + + if 'docformoptions' in request.session: + docformoptions = request.session['docformoptions'] + else: + request.session['docformoptions'] = docformoptions + + try: + makeprivate = uploadoptions['makeprivate'] + except KeyError: + makeprivate = False + try: + make_plot = uploadoptions['make_plot'] + except KeyError: + make_plot = False + + try: + workouttype = docformoptions['workouttype'] + except KeyError: + workouttype = 'rower' + + try: + boattype = docformoptions['boattype'] + except KeyError: + boattype = '1x' + + try: + workoutsource = uploadoptions['workoutsource'] + except KeyError: + workoutsource = None + + try: + plottype = uploadoptions['plottype'] + except KeyError: + plottype = 'timeplot' + + try: + landingpage = uploadoptions['landingpage'] + except KeyError: + landingpage = r.defaultlandingpage + uploadoptions['landingpage'] = landingpage + + try: + upload_to_c2 = uploadoptions['upload_to_C2'] + except KeyError: + upload_to_c2 = False + + try: + upload_to_strava = uploadoptions['upload_to_Strava'] + except KeyError: + upload_to_strava = False + + try: + upload_to_st = uploadoptions['upload_to_SportTracks'] + except KeyError: + upload_to_st = False + + try: + upload_to_rk = uploadoptions['upload_to_RunKeeper'] + except KeyError: + upload_to_rk = False + + try: + upload_to_ua = uploadoptions['upload_to_MapMyFitness'] + except KeyError: + upload_to_ua = False + + try: + upload_to_tp = uploadoptions['upload_to_TrainingPeaks'] + except KeyError: + upload_to_tp = False + + + + + response = {} + if request.method == 'POST': + form = DocumentsForm(request.POST,request.FILES) + optionsform = UploadOptionsForm(request.POST,request=request) + + if form.is_valid(): +# f = request.FILES['file'] + f = form.cleaned_data['file'] + + if f is not None: + res = handle_uploaded_file(f) + else: + messages.error(request, + "Something went wrong - no file attached") + url = reverse(workout_upload_view) + if is_ajax: + return JSONResponse({'result':0,'url':0}) + else: + return HttpResponseRedirect(url) + + t = form.cleaned_data['title'] + workouttype = form.cleaned_data['workouttype'] + boattype = form.cleaned_data['boattype'] + + request.session['docformoptions'] = { + 'workouttype':workouttype, + 'boattype': boattype, + } + + notes = form.cleaned_data['notes'] + offline = form.cleaned_data['offline'] + + race = None + if optionsform.is_valid(): + make_plot = optionsform.cleaned_data['make_plot'] + plottype = optionsform.cleaned_data['plottype'] + upload_to_c2 = optionsform.cleaned_data['upload_to_C2'] + upload_to_strava = optionsform.cleaned_data['upload_to_Strava'] + upload_to_st = optionsform.cleaned_data['upload_to_SportTracks'] + upload_to_rk = optionsform.cleaned_data['upload_to_RunKeeper'] + upload_to_ua = optionsform.cleaned_data['upload_to_MapMyFitness'] + upload_to_tp = optionsform.cleaned_data['upload_to_TrainingPeaks'] + makeprivate = optionsform.cleaned_data['makeprivate'] + landingpage = optionsform.cleaned_data['landingpage'] + + try: + race = optionsform.cleaned_data['submitrace'] + except KeyError: + race = None + + uploadoptions = { + 'makeprivate':makeprivate, + 'make_plot':make_plot, + 'plottype':plottype, + 'upload_to_C2':upload_to_c2, + 'upload_to_Strava':upload_to_strava, + 'upload_to_SportTracks':upload_to_st, + 'upload_to_RunKeeper':upload_to_rk, + 'upload_to_MapMyFitness':upload_to_ua, + 'upload_to_TrainingPeaks':upload_to_tp, + 'landingpage':landingpage, + 'boattype': boattype, + 'workouttype': workouttype, + } + + + request.session['uploadoptions'] = uploadoptions + + f1 = res[0] # file name + f2 = res[1] # file name incl media directory + + if not offline: + id,message,f2 = dataprep.new_workout_from_file( + r,f2, + workouttype=workouttype, + workoutsource=workoutsource, + boattype=boattype, + makeprivate=makeprivate, + title = t, + notes='' + ) + else: + workoutsbox = Mailbox.objects.filter(name='workouts')[0] + uploadoptions['fromuploadform'] = True + bodyyaml = yaml.safe_dump( + uploadoptions, + default_flow_style=False + ) + msg = Message(mailbox=workoutsbox, + from_header=r.user.email, + subject = t,body=bodyyaml) + msg.save() + f3 = 'media/mailbox_attachments/'+f2[6:] + copyfile(f2,f3) + f3 = f3[6:] + a = MessageAttachment(message=msg,document=f3) + a.save() + os.remove(f2) + + messages.info( + request, + "The file was too large to process in real time. It will be processed in a background process. You will receive an email when it is ready") + url = reverse(workout_upload_view) + if is_ajax: + return JSONResponse({'result':1,'url':url}) + else: + response = HttpResponseRedirect(url) + return response + + if not id: + messages.error(request,message) + url = reverse(workout_upload_view) + if is_ajax: + return JSONResponse({'result':0,'url':url}) + else: + response = HttpResponseRedirect(url) + return response + elif id == -1: + message = 'The zip archive will be processed in the background. The files in the archive will only be uploaded without the extra actions. You will receive email when the workouts are ready.' + messages.info(request,message) + url = reverse(workout_upload_view) + if is_ajax: + return JSONResponse({'result':1,'url':url}) + else: + response = HttpResponseRedirect(url) + return response + else: + if message: + messages.error(request,message) + + url = reverse(workout_edit_view, + kwargs = { + 'id':int(id), + }) + + if is_ajax: + response = {'result': 1,'url':url} + else: + response = HttpResponseRedirect(url) + + w = Workout.objects.get(id=id) + + r = getrower(request.user) + if (make_plot): + res,jobid = uploads.make_plot(r,w,f1,f2,plottype,t) + if res == 0: + messages.error(request,jobid) + else: + try: + request.session['async_tasks'] += [(jobid,'make_plot')] + except KeyError: + request.session['async_tasks'] = [(jobid,'make_plot')] + + # upload to C2 + if (upload_to_c2): + try: + message,id = c2stuff.workout_c2_upload(request.user,w) + except NoTokenError: + id = 0 + message = "Something went wrong with the Concept2 sync" + if id>1: + messages.info(request,message) + else: + messages.error(request,message) + + if (upload_to_strava): + try: + message,id = stravastuff.workout_strava_upload( + request.user,w + ) + except NoTokenError: + id = 0 + message = "Please connect to Strava first" + if id>1: + messages.info(request,message) + else: + messages.error(request,message) + + if (upload_to_st): + try: + message,id = sporttracksstuff.workout_sporttracks_upload( + request.user,w + ) + except NoTokenError: + message = "Please connect to SportTracks first" + id = 0 + if id>1: + messages.info(request,message) + else: + messages.error(request,message) + + if (upload_to_rk): + try: + message,id = runkeeperstuff.workout_runkeeper_upload( + request.user,w + ) + except NoTokenError: + message = "Please connect to Runkeeper first" + id = 0 + + if id>1: + messages.info(request,message) + else: + messages.error(request,message) + + + if (upload_to_ua): + try: + message,id = underarmourstuff.workout_ua_upload( + request.user,w + ) + except NoTokenError: + message = "Please connect to MapMyFitness first" + id = 0 + + if id>1: + messages.info(request,message) + else: + messages.error(request,message) + + + if (upload_to_tp): + try: + message,id = tpstuff.workout_tp_upload( + request.user,w + ) + except NoTokenError: + message = "Please connect to TrainingPeaks first" + id = 0 + + if id>1: + messages.info(request,message) + else: + messages.error(request,message) + + if race and race_can_submit(r,race): + records = IndoorVirtualRaceResult.objects.filter( + race=race, + userid=r.id + ) + + if records: + + result,comments,errors,jobid = add_workout_indoorrace( + [w],race,r,recordid=records[0].id + ) + + if result: + messages.info( + request, + "We have submitted your workout to the race") + + for c in comments: + messages.info(request,c) + for er in errors: + messages.error(request,er) + + + if landingpage != 'workout_upload_view': + url = reverse(landingpage, + kwargs = { + 'id':w.id, + }) + else: + url = reverse(landingpage) + + if is_ajax: + response = {'result':1,'url':url} + else: + response = HttpResponseRedirect(url) + else: + if not is_ajax: + response = render(request, + 'document_form.html', + {'form':form, + 'teams':get_my_teams(request.user), + 'optionsform': optionsform, + }) + + + if is_ajax: + return JSONResponse(response) + else: + return response + else: + if not is_ajax: + if r.c2_auto_export and isprorower(r): + uploadoptions['upload_to_C2'] = True + + if r.strava_auto_export and isprorower(r): + uploadoptions['upload_to_Strava'] = True + + if r.sporttracks_auto_export and isprorower(r): + uploadoptions['upload_to_SportTracks'] = True + + if r.runkeeper_auto_export and isprorower(r): + uploadoptions['upload_to_RunKeeper'] = True + + if r.trainingpeaks_auto_export and isprorower(r): + uploadoptions['upload_to_TrainingPeaks'] = True + + if r.mapmyfitness_auto_export and isprorower(r): + uploadoptions['upload_to_MapMyFitness'] = True + + form = DocumentsForm(initial=docformoptions) + optionsform = UploadOptionsForm(initial=uploadoptions, + request=request) + return render(request, 'document_form.html', + {'form':form, + 'active':'nav-workouts', + 'breadcrumbs':breadcrumbs, + 'teams':get_my_teams(request.user), + 'optionsform': optionsform, + }) + else: + return {'result':0} + + +# This is the main view for processing uploaded files +@user_passes_test(iscoachmember,login_url="/rowers/paidplans",redirect_field_name=None) +def team_workout_upload_view(request,message="", + successmessage="", + uploadoptions={ + 'make_plot':False, + 'plottype':'timeplot', + }): + + if 'uploadoptions' in request.session: + uploadoptions = request.session['uploadoptions'] + else: + request.session['uploadoptions'] = uploadoptions + + breadcrumbs = [ + { + 'url':'/rowers/list-workouts/', + 'name':'Workouts' + }, + { + 'url': reverse(team_workout_upload_view), + 'name': 'Team Upload' + } + ] + + + myteams = Team.objects.filter(manager=request.user) + + make_plot = uploadoptions['make_plot'] + plottype = uploadoptions['plottype'] + + r = getrower(request.user) + if request.method == 'POST': + form = DocumentsForm(request.POST,request.FILES) + optionsform = TeamUploadOptionsForm(request.POST) + + rowerform = TeamInviteForm(request.POST) + rowerform.fields.pop('email') + rowerform.fields['user'].queryset = User.objects.filter(rower__isnull=False,rower__team__in=myteams).distinct() + if form.is_valid(): + f = request.FILES['file'] + res = handle_uploaded_file(f) + t = form.cleaned_data['title'] + offline = form.cleaned_data['offline'] + boattype = form.cleaned_data['boattype'] + if rowerform.is_valid(): + u = rowerform.cleaned_data['user'] + if u: + r = getrower(u) + else: + message = 'Please select a rower' + messages.error(request,message) + messages.info(request,successmessage) + response = render(request, + 'team_document_form.html', + {'form':form, + 'teams':get_my_teams(request.user), + 'optionsform': optionsform, + 'rowerform': rowerform, + }) + + return response + + workouttype = form.cleaned_data['workouttype'] + + notes = form.cleaned_data['notes'] + + if optionsform.is_valid(): + make_plot = optionsform.cleaned_data['make_plot'] + plottype = optionsform.cleaned_data['plottype'] + + uploadoptions = { + 'makeprivate':False, + 'make_plot':make_plot, + 'plottype':plottype, + 'upload_to_C2':False, + } + + + request.session['uploadoptions'] = uploadoptions + + f1 = res[0] # file name + f2 = res[1] # file name incl media directory + + + if not offline: + id,message,f2 = dataprep.new_workout_from_file( + r,f2, + workouttype=workouttype, + boattype=boattype, + makeprivate=False, + title = t, + notes='' + ) + else: + job = myqueue( + queuehigh, + handle_zip_file, + r.user.email, + t, + f2, + emailbounced = r.emailbounced + ) + + messages.info( + request, + "The file was too large to process in real time. It will be processed in a background process. The user will receive an email when it is ready" + ) + + + url = reverse(team_workout_upload_view) + response = HttpResponseRedirect(url) + return response + + + if not id: + messages.error(request,message) + url = reverse(team_workout_upload_view) + response = HttpResponseRedirect(url) + return response + elif id == -1: + message = 'The zip archive will be processed in the background. The files in the archive will only be uploaded without the extra actions. You will receive email when the workouts are ready.' + messages.info(request,message) + url = reverse(team_workout_upload_view) + response = HttpResponseRedirect(url) + return response + + else: + successmessage = "The workout was added to the user's account" + messages.info(request,successmessage) + + url = reverse(team_workout_upload_view) + + response = HttpResponseRedirect(url) + w = Workout.objects.get(id=id) + + r = getrower(request.user) + if (make_plot): + id,jobid = uploads.make_plot(r,w,f1,f2,plottype,t) + + + + + else: + + response = render(request, + 'team_document_form.html', + {'form':form, + 'teams':get_my_teams(request.user), + 'active': 'nav-workouts', + 'breadcrumbs':breadcrumbs, + 'optionsform': optionsform, + 'rowerform': rowerform, + }) + + return response + else: + form = DocumentsForm() + optionsform = TeamUploadOptionsForm(initial=uploadoptions) + rowerform = TeamInviteForm() + rowerform.fields.pop('email') + rowerform.fields['user'].queryset = User.objects.filter(rower__isnull=False,rower__team__in=myteams).distinct() + return render(request, 'team_document_form.html', + {'form':form, + 'teams':get_my_teams(request.user), + 'optionsform': optionsform, + 'active': 'nav-workouts', + 'breadcrumbs':breadcrumbs, + 'rower':r, + 'rowerform':rowerform, + }) + + + + + +# A page with all the recent graphs (searchable on workout name) +@login_required() +def graphs_view(request): + request.session['referer'] = reverse(graphs_view) + try: + r = getrower(request.user) + workouts = Workout.objects.filter(user=r).order_by("-date", "-starttime") + query = request.GET.get('q') + if query: + query_list = query.split() + workouts = workouts.filter( + reduce(operator.and_, + (Q(name__icontains=q) for q in query_list)) | + reduce(operator.and_, + (Q(notes__icontains=q) for q in query_list)) + ) + searchform = SearchForm(initial={'q':query}) + else: + searchform = SearchForm() + + g = GraphImage.objects.filter(workout__in=workouts).order_by("-creationdatetime") + + + paginator = Paginator(g,8) + page = request.GET.get('page') + + try: + g = paginator.page(page) + except PageNotAnInteger: + g = paginator.page(1) + except EmptyPage: + g = paginator.page(paginator.num_pages) + + return render(request, 'list_graphs.html', + {'graphs': g, + 'searchform':searchform, + 'active':'nav-workouts', + 'teams':get_my_teams(request.user), + }) + + except Rower.DoesNotExist: + raise Http404("User has no rower instance") + +# Show the chart (png image) +def graph_show_view(request,id): + try: + g = GraphImage.objects.get(id=id) + try: + width,height = Image.open(g.filename).size + g.width = width + g.height = height + g.save() + except: + pass + + w = Workout.objects.get(id=g.workout.id) + r = Rower.objects.get(id=w.user.id) + + breadcrumbs = [ + { + 'url':'/rowers/list-workouts/', + 'name':'Workouts' + }, + { + 'url':get_workout_default_page(request,w.id), + 'name': w.name + }, + { + 'url':reverse(graph_show_view,kwargs={'id':id}), + 'name': 'Chart' + } + + ] + + + return render(request,'show_graph.html', + {'graph':g, + 'teams':get_my_teams(request.user), + 'workout':w, + 'breadcrumbs':breadcrumbs, + 'active':'nav-workouts', + 'rower':r,}) + + except GraphImage.DoesNotExist: + raise Http404("This graph doesn't exist") + except Workout.DoesNotExist: + raise Http404("This workout doesn't exist") + +# Restore original stroke data and summary +@login_required() +def workout_summary_restore_view(request,id,message="",successmessage=""): + try: + row = Workout.objects.get(id=id) + if (checkworkoutuser(request.user,row)==False): + raise PermissionDenied("You are not allowed to edit this workout") + except Workout.DoesNotExist: + raise Http404("Workout doesn't exist") + + s = "" + # still here - this is a workout we may edit + f1 = row.csvfilename + u = row.user.user + r = getrower(u) + powerperc = 100*np.array([r.pw_ut2, + r.pw_ut1, + r.pw_at, + r.pw_tr,r.pw_an])/r.ftp + + ftp = float(r.ftp) + if row.workouttype in mytypes.otwtypes: + ftp = ftp*(100.-r.otwslack)/100. + + rr = rrower(hrmax=r.max,hrut2=r.ut2, + hrut1=r.ut1,hrat=r.at, + hrtr=r.tr,hran=r.an,ftp=ftp, + powerperc=powerperc,powerzones=r.powerzones) + rowdata = rdata(f1,rower=rr) + if rowdata == 0: + raise Http404("Error: CSV Data File Not Found") + rowdata.restoreintervaldata() + rowdata.write_csv(f1,gzip=True) + dataprep.update_strokedata(id,rowdata.df) + intervalstats = rowdata.allstats() + row.summary = intervalstats + row.save() + + # create interactive plot + try: + res = interactive_chart(id,promember=1) + script = res[0] + div = res[1] + except ValueError: + pass + + + messages.info(request,'Original Interval Data Restored') + url = reverse(workout_summary_edit_view, + kwargs={ + 'id':int(id), + } + ) + return HttpResponseRedirect(url) + +# Split a workout +@user_passes_test(ispromember,login_url="/rowers/paidplans",message="This functionality requires a Pro plan or higher",redirect_field_name=None) +def workout_split_view(request,id=id): + row = get_workout_permitted(request.user,id) + + r = row.user + + breadcrumbs = [ + { + 'url':'/rowers/list-workouts/', + 'name':'Workouts' + }, + { + 'url':get_workout_default_page(request,row.id), + 'name': row.name + }, + { + 'url':reverse(graph_show_view,kwargs={'id':id}), + 'name': 'Chart' + } + + ] + if request.method == 'POST': + form = WorkoutSplitForm(request.POST) + if form.is_valid(): + splittime = form.cleaned_data['splittime'] + splitsecond = splittime.hour*3600 + splitsecond += splittime.minute*60 + splitsecond += splittime.second + splitsecond += splittime.microsecond/1.e6 + splitmode = form.cleaned_data['splitmode'] + try: + ids,mesgs = dataprep.split_workout( + r,row,splitsecond,splitmode + ) + except IndexError: + messages.error(request,"Something went wrong in Split") + + for message in mesgs: + messages.info(request,message) + + + if request.user == r: + url = reverse(workouts_view) + else: + mgrids = [team.id for team in Team.objects.filter(manager=request.user)] + rwrids = [team.id for team in r.team.all()] + teamids = list(set(mgrids) & set(rwrids)) + if len(teamids) > 0: + teamid = teamids[0] + + url = reverse(workouts_view, + kwargs={ + 'teamid':int(teamid), + } + ) + else: + url = reverse(workouts_view) + + rowname = row.name + if isinstance(rowname,unicode): + rowname = rowname.encode('utf8') + elif isinstance(rowname, str): + rowname = rowname.decode('utf8') + + qdict = {'q':rowname} + url+='?'+urllib.urlencode(qdict) + + return HttpResponseRedirect(url) + + form = WorkoutSplitForm() + + # create interactive plot + try: + res = interactive_chart(id,promember=1) + script = res[0] + div = res[1] + except ValueError: + pass + + return render(request, 'splitworkout.html', + {'form':form, + 'rower':r, + 'breadcrumbs':breadcrumbs, + 'active':'nav-workouts', + 'workout':row, + 'thediv':script, + 'thescript':div, + }) + + +# Fuse two workouts +@user_passes_test(ispromember,login_url="/rowers/paidplans",message="This functionality requires a Pro plan or higher",redirect_field_name=None) +def workout_fusion_view(request,id1=0,id2=1): + + r = getrower(request.user) + + try: + w1 = Workout.objects.get(id=id1) + w2 = Workout.objects.get(id=id2) + r = w1.user + if (checkworkoutuser(request.user,w1)==False) or \ + (checkworkoutuser(request.user,w2)==False): + raise PermissionDenied("You are not allowed to use these workouts") + except Workout.DoesNotExist: + raise Http404("One of the workouts doesn't exist") + + if request.method == 'POST': + form = FusionMetricChoiceForm(request.POST,instance=w2) + if form.is_valid(): + cd = form.cleaned_data + columns = cd['columns'] + timeoffset = cd['offset'] + posneg = cd['posneg'] + if posneg == 'neg': + timeoffset = -timeoffset + + # Create DataFrame + df,forceunit = dataprep.datafusion(id1,id2,columns,timeoffset) + + + idnew,message = dataprep.new_workout_from_df(r,df, + title='Fused data', + parent=w1, + forceunit=forceunit) + if message != None: + messages.error(request,message) + else: + successmessage = 'Data fused' + messages.info(request,message) + + url = reverse(workout_edit_view, + kwargs={ + 'id':idnew, + }) + + return HttpResponseRedirect(url) + else: + form = FusionMetricChoiceForm(instance=w2) + + breadcrumbs = [ + { + 'url':'/rowers/list-workouts/', + 'name':'Workouts' + }, + { + 'url':get_workout_default_page(request,w1.id), + 'name': str(w1.id) + }, + { + 'url':reverse(workout_fusion_list,kwargs={'id':id1}), + 'name': 'Sensor Fusion' + }, + { + 'url':reverse(workout_fusion_view,kwargs={'id1':id1,'id2':id2}), + 'name': str(w2.id) + } + + ] + + return render(request, 'fusion.html', + {'form':form, + 'teams':get_my_teams(request.user), + 'workout':w1, + 'rower':r, + 'breadcrumbs':breadcrumbs, + 'active':'nav-workouts', + 'workout1':w1, + 'workout2':w2, + }) + + +# Edit the splits/summary +@login_required() +def workout_summary_edit_view(request,id,message="",successmessage="" + ): + row = get_workout_permitted(request.user,id) + r = getrower(request.user) + breadcrumbs = [ + { + 'url':'/rowers/list-workouts/', + 'name':'Workouts' + }, + { + 'url':get_workout_default_page(request,row.id), + 'name': row.name + }, + { + 'url':reverse(workout_summary_edit_view,kwargs={'id':id}), + 'name': 'Edit Intervals' + } + + ] + + s = "" + # still here - this is a workout we may edit + f1 = row.csvfilename + u = row.user.user + r = getrower(u) + powerperc = 100*np.array([r.pw_ut2, + r.pw_ut1, + r.pw_at, + r.pw_tr,r.pw_an])/r.ftp + + ftp = float(r.ftp) + if row.workouttype in mytypes.otwtypes: + ftp = ftp*(100.-r.otwslack)/100. + + rr = rrower(hrmax=r.max,hrut2=r.ut2, + hrut1=r.ut1,hrat=r.at, + hrtr=r.tr,hran=r.an,ftp=ftp, + powerperc=powerperc,powerzones=r.powerzones) + rowdata = rdata(f1,rower=rr) + if rowdata == 0: + return HttpResponse("Error: CSV Data File Not Found") + intervalstats = rowdata.allstats() + try: + itime,idist,itype = rowdata.intervalstats_values() + except TypeError: + return HttpResponse("Error: CSV Data File Not Found") + nrintervals = len(idist) + + + savebutton = 'nosavebutton' + formvalues = {} + form = SummaryStringForm() + + tss,normp = dataprep.workout_rscore(row) + + normv,normw = dataprep.workout_normv(row,pp=8.0) + + work = int(normw) + power = int(normp) + try: + pace_secs = int(500./normv) + except (OverflowError, ZeroDivisionError): + pace_secs = 140. + + try: + avpace = datetime.timedelta(seconds=int(500./normv)) + except (OverflowError, ZeroDivisionError): + avpace = datetime.timedelta(seconds=130) + + data = { + 'power': int(normp), + 'pace': avpace, + 'selector': 'power', + 'work': int(normw) + } + + powerorpace = 'power' + + if normp == 0: + data['selector'] = 'pace' + powerorpace = 'pace' + + + + powerupdateform = PowerIntervalUpdateForm(initial=data) + + # We have submitted the mini language interpreter + if request.method == 'POST' and "intervalstring" in request.POST: + form = SummaryStringForm(request.POST) + if form.is_valid(): + cd = form.cleaned_data + s = cd["intervalstring"] + try: + rowdata.updateinterval_string(s) + except ParseException,err: + messages.error(request,'Parsing error in column '+str(err.col)) + intervalstats = rowdata.allstats() + itime,idist,itype = rowdata.intervalstats_values() + nrintervals = len(idist) + savebutton = 'savestringform' + powerupdateform = PowerIntervalUpdateForm(initial=data) + + # we are saving the results obtained from the split by power/pace interpreter + elif request.method == 'POST' and "savepowerpaceform" in request.POST: + powerorpace = request.POST['powerorpace'] + value_pace = request.POST['value_pace'] + value_power = request.POST['value_power'] + value_work = request.POST['value_work'] + if powerorpace == 'power': + power = int(value_power) + elif powerorpace == 'pace': + try: + pace_secs = float(value_pace) + except ValueError: + pace_secs = float(value_pace.replace(',','.')) + elif powerorpace == 'work': + work = int(value_work) + + if powerorpace == 'power' and power is not None: + try: + rowdata.updateinterval_metric( + ' Power (watts)',power,mode='larger', + debug=False,smoothwindow=15.) + except: + messages.error(request,'Error updating power') + elif powerorpace == 'pace': + try: + velo = 500./pace_secs + rowdata.updateinterval_metric( + ' AverageBoatSpeed (m/s)',velo,mode='larger', + debug=False,smoothwindow=15.) + except: + messages.error(request,'Error updating pace') + elif powerorpace == 'work': + try: + rowdata.updateinterval_metric( + 'driveenergy',work,mode='larger', + debug=False,smoothwindow=15.) + except: + messages.error(request,'Error updating Work per Stroke') + + intervalstats = rowdata.allstats() + itime,idist,itype = rowdata.intervalstats_values() + nrintervals = len(idist) + + row.summary = intervalstats + row.save() + + rowdata.write_csv(f1,gzip=True) + + dataprep.update_strokedata(id,rowdata.df) + + messages.info(request,"Updated interval data saved") + data = { + 'power': power, + 'pace': datetime.timedelta(seconds=int(pace_secs)), + 'work': work, + 'selector': powerorpace + } + form = SummaryStringForm() + powerupdateform = PowerIntervalUpdateForm(initial=data) + savebutton = 'savepowerpaceform' + formvalues = { + 'powerorpace': powerorpace, + 'value_power': power, + 'value_pace': pace_secs, + 'value_work':work, + } + + + # we are saving the results obtained from the mini language interpreter + elif request.method == 'POST' and "savestringform" in request.POST: + s = request.POST["savestringform"] + try: + rowdata.updateinterval_string(s) + except ParseException,err: + messages.error(request,'Parsing error in column '+str(err.col)) + + intervalstats = rowdata.allstats() + itime,idist,itype = rowdata.intervalstats_values() + nrintervals = len(idist) + row.summary = intervalstats + #intervalstats = rowdata.allstats() + if s: + try: + row.notes = u'{n} \n {s}'.format( + n = row.notes, + s = s + ) + except TypeError: + pass + + row.save() + rowdata.write_csv(f1,gzip=True) + dataprep.update_strokedata(id,rowdata.df) + messages.info(request,"Updated interval data saved") + data = {'intervalstring':s} + form = SummaryStringForm(initial=data) + powerupdateform = PowerIntervalUpdateForm(initial={ + 'power': int(normp), + 'pace': avpace, + 'selector': 'power', + 'work': int(normw) + }) + savebutton = 'savestringform' + + # we are saving the results obtained from the power update form + elif request.method == 'POST' and 'selector' in request.POST: + powerupdateform = PowerIntervalUpdateForm(request.POST) + if powerupdateform.is_valid(): + cd = powerupdateform.cleaned_data + powerorpace = cd['selector'] + power = cd['power'] + pace = cd['pace'] + work = cd['work'] + try: + pace_secs = pace.seconds+pace.microseconds/1.0e6 + except AttributeError: + pace_secs = 120. + + if powerorpace == 'power' and power is not None: + try: + rowdata.updateinterval_metric(' Power (watts)',power,mode='larger', + debug=False,smoothwindow=15) + except: + messages.error(request,'Error updating power') + elif powerorpace == 'pace': + try: + velo = 500./pace_secs + rowdata.updateinterval_metric(' AverageBoatSpeed (m/s)',velo,mode='larger', + debug=False,smoothwindow=15) + except: + messages.error(request,'Error updating pace') + elif powerorpace == 'work': + try: + rowdata.updateinterval_metric( + 'driveenergy',work,mode='larger', + debug=False,smoothwindow=15.) + except: + messages.error(request,'Error updating Work per Stroke') + + + intervalstats = rowdata.allstats() + itime,idist,itype = rowdata.intervalstats_values() + nrintervals = len(idist) + savebutton = 'savepowerpaceform' + formvalues = { + 'powerorpace': powerorpace, + 'value_power': power, + 'value_pace': pace_secs, + 'value_work': work, + } + powerupdateform = PowerIntervalUpdateForm(initial=cd) + form = SummaryStringForm() + + + form = SummaryStringForm() + + # we are saving the results obtained from the detailed form + elif request.method == 'POST' and "savedetailform" in request.POST: + savebutton = 'savedetailform' + form = SummaryStringForm() + nrintervals = int(request.POST['nrintervals']) + detailform = IntervalUpdateForm(request.POST,aantal=nrintervals) + itime = [] + idist = [] + itype = [] + ivalues = [] + iunits = [] + itypes = [] + iresults = [] + for i in xrange(nrintervals): + try: + t = datetime.datetime.strptime(request.POST['intervalt_%s' % i],"%H:%M:%S.%f") + except ValueError: + t = datetime.datetime.strptime(request.POST['intervalt_%s' % i],"%H:%M:%S") + + timesecs = 3600*t.hour+60*t.minute+t.second+t.microsecond/1.e6 + itime += [timesecs] + idist += [int(request.POST['intervald_%s' % i])] + itype += [int(request.POST['type_%s' % i])] + + if itype[i] == 3: # rest + itypes += ['rest'] + ivalues += [timesecs] + iresults += [idist[i]] + iunits += ['seconds'] + if itype[i] == 5 or itype[i] == 2: # distance based work + itypes += ['work'] + ivalues += [idist[i]] + iresults += [timesecs] + iunits += ['meters'] + if itype[i] == 4 or itype[i] == 1: # time based work + itypes += ['work'] + ivalues += [timesecs] + iresults += [idist[i]] + iunits += ['seconds'] + + + rowdata.updateintervaldata(ivalues,iunits,itypes,iresults=iresults) + intervalstats = rowdata.allstats() + row.summary = intervalstats + try: + row.notes += "\n"+s + except TypeError: + pass + + row.save() + rowdata.write_csv(f1,gzip=True) + dataprep.update_strokedata(id,rowdata.df) + messages.info(request,"Updated interval data saved") + + form = SummaryStringForm() + powerupdateform = PowerIntervalUpdateForm(initial={ + 'power': int(normp), + 'pace': avpace, + 'selector': 'power', + 'work': int(normw) + }) + + + # we are processing the details form + elif request.method == 'POST' and "nrintervals" in request.POST: + savebutton = 'savedetailform' + nrintervals = int(request.POST['nrintervals']) + detailform = IntervalUpdateForm(request.POST,aantal=nrintervals) + if detailform.is_valid(): + cd = detailform.cleaned_data + itime = [] + idist = [] + itype = [] + ivalues = [] + iunits = [] + itypes = [] + iresults = [] + for i in xrange(nrintervals): + t = cd['intervalt_%s' % i] + timesecs = t.total_seconds() + itime += [timesecs] + idist += [cd['intervald_%s' % i]] + itype += [cd['type_%s' % i]] + + if itype[i] == '3': # rest + itypes += ['rest'] + ivalues += [timesecs] + iresults += [idist[i]] + iunits += ['seconds'] + if itype[i] == '5' or itype[i] == '2': # distance based work + itypes += ['work'] + ivalues += [idist[i]] + iresults += [timesecs] + iunits += ['meters'] + if itype[i] == '4' or itype[i] == '1': # time based work + itypes += ['work'] + ivalues += [timesecs] + iresults += [idist[i]] + iunits += ['seconds'] + + rowdata.updateintervaldata(ivalues,iunits, + itypes,iresults=iresults) + intervalstats = rowdata.allstats() + + + form = SummaryStringForm() + powerupdateform = PowerIntervalUpdateForm() + + + + initial = {} + for i in xrange(nrintervals): + try: + initial['intervald_%s' % i] = idist[i] + initial['intervalt_%s' % i] = get_time(itime[i]) + initial['type_%s' % i] = itype[i] + except IndexError: + pass + + + detailform = IntervalUpdateForm(aantal=nrintervals,initial=initial) + + # create interactive plot + try: + intervaldata = { + 'itime':itime, + 'idist':idist, + 'itype':itype, + 'selector': powerorpace, + 'normp': normp, + 'normv': normv, + } + res = interactive_chart(id,promember=1,intervaldata=intervaldata) + script = res[0] + div = res[1] + except ValueError: + pass + + # render page + + return render(request, 'summary_edit.html', + {'form':form, + 'detailform':detailform, + 'powerupdateform':powerupdateform, + 'workout':row, + 'rower':r, + 'breadcrumbs':breadcrumbs, + 'active':'nav-workouts', + 'teams':get_my_teams(request.user), + 'intervalstats':intervalstats, + 'nrintervals':nrintervals, + 'interactiveplot':script, + 'the_div':div, + 'intervalstring':s, + 'savebutton':savebutton, + 'formvalues':formvalues, + }) + + +class GraphDelete(DeleteView): + login_required = True + model = GraphImage + template_name = 'graphimage_delete_confirm.html' + + # extra parameters + def get_context_data(self, **kwargs): + context = super(GraphDelete, self).get_context_data(**kwargs) + + breadcrumbs = [ + { + 'url':'/rowers/list-workouts/', + 'name':'Workouts' + }, + { + 'url':get_workout_default_page(self.request,self.object.workout.id), + 'name': str(self.object.workout.id) + }, + { + 'url':reverse(graph_show_view,kwargs={'id':self.object.pk}), + 'name': 'Chart' + }, + { 'url':reverse('graph_delete',kwargs={'pk':str(self.object.pk)}), + 'name': 'Delete' + } + + ] + + context['active'] = 'nav-workouts' + context['rower'] = getrower(self.request.user) + context['breadcrumbs'] = breadcrumbs + + return context + + + def get_success_url(self): + w = self.object.workout + return reverse(workout_edit_view,kwargs={'id':str(w.id)}) + + def get_object(self, *args, **kwargs): + obj = super(GraphDelete, self).get_object(*args, **kwargs) + if not checkaccessuser(self.request.user,obj.workout.user): + raise PermissionDenied('You are not allowed to delete this chart') + + return obj + +class WorkoutDelete(DeleteView): + login_required = True + model = Workout + template_name = 'workout_delete_confirm.html' + + # extra parameters + def get_context_data(self, **kwargs): + context = super(WorkoutDelete, self).get_context_data(**kwargs) + + breadcrumbs = [ + { + 'url':'/rowers/list-workouts/', + 'name':'Workouts' + }, + { + 'url':get_workout_default_page(self.request,self.object.id), + 'name': str(self.object.id) + }, + { 'url':reverse('workout_delete',kwargs={'pk':str(self.object.pk)}), + 'name': 'Delete' + } + + ] + + mayedit=0 + promember=0 + if not self.request.user.is_anonymous(): + r = getrower(self.request.user) + result = self.request.user.is_authenticated() and ispromember(self.request.user) + if result: + promember=1 + if self.request.user == self.object.user.user: + mayedit=1 + + context['active'] = 'nav-workouts' + context['rower'] = getrower(self.request.user) + context['breadcrumbs'] = breadcrumbs + context['workout'] = self.object + context['mayedit'] = mayedit + context['promember'] = promember + + return context + + + def get_success_url(self): + return reverse(workouts_view) + + def get_object(self, *args, **kwargs): + obj = super(WorkoutDelete, self).get_object(*args, **kwargs) + if not checkaccessuser(self.request.user,obj.user): + raise PermissionDenied('You are not allowed to delete this workout') + + return obj + diff --git a/rowers/views.py b/rowers/views_old.py similarity index 99% rename from rowers/views.py rename to rowers/views_old.py index 1df96516..2a2358f3 100644 --- a/rowers/views.py +++ b/rowers/views_old.py @@ -14442,12 +14442,6 @@ def plannedsession_teamcreate_view(request, request.user,request.POST ) - print sessioncreateform.is_valid(),'sessioncreateform' - print sessioncreateform.errors,'errors' - print sessionteamselectform.is_valid(),'teamselectform' - print sessionteamselectform.errors,'errors' - raise ValueError - if sessioncreateform.is_valid() and sessionteamselectform.is_valid(): cd = sessioncreateform.cleaned_data startdate = cd['startdate'] From bc8bda7a38157f086ebc104fa5de421b7973299d Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Thu, 7 Feb 2019 17:41:17 +0100 Subject: [PATCH 10/16] passing tests - views.py split over multiple files --- rowers/__init__.py | 2 + rowers/tests/testdata/testdata.csv.gz | Bin 11426 -> 11426 bytes rowers/urls.py | 542 +++++++++++++------------- rowers/views/.#__init__.py | 1 - rowers/views/__init__.py | 12 + rowers/views/analysisviews.py | 4 +- rowers/views/importviews.py | 66 ++-- rowers/views/paymentviews.py | 2 + rowers/views/planviews.py | 2 +- rowers/views/statements.py | 85 ++-- rowers/views/teamviews.py | 2 +- rowers/views/workoutviews.py | 36 +- 12 files changed, 389 insertions(+), 365 deletions(-) delete mode 100644 rowers/views/.#__init__.py create mode 100644 rowers/views/__init__.py diff --git a/rowers/__init__.py b/rowers/__init__.py index 66cd71ed..03d28f5e 100644 --- a/rowers/__init__.py +++ b/rowers/__init__.py @@ -1,3 +1,5 @@ from __future__ import absolute_import from .tasks import app as celery_app + + diff --git a/rowers/tests/testdata/testdata.csv.gz b/rowers/tests/testdata/testdata.csv.gz index 2c1957e019950166bf00f7f15b8ee8c99670d51f..66b5f5a1abf5302dbe7ab2373273984278d51207 100644 GIT binary patch delta 15 WcmZ1!xhRrNzMF$XA#Nkv9322CbOf{j delta 15 WcmZ1!xhRrNzMF$%xzk3rIXVC<5(OUs diff --git a/rowers/urls.py b/rowers/urls.py index 3972e842..63174c62 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -7,7 +7,7 @@ from models import Workout,Rower,StrokeData,FavoriteChart from rest_framework import routers, serializers, viewsets,permissions from rest_framework.urlpatterns import format_suffix_patterns from rest_framework.permissions import * -from . import views +from rowers import views from django.contrib.auth import views as auth_views from django.views.generic.base import TemplateView from django.conf.urls import ( @@ -108,198 +108,207 @@ urlpatterns = [ url(r'^o/authorize/$', base.AuthorizationView.as_view(), name="authorize"), url(r'^o/token/$', base.TokenView.as_view(), name="token"), url(r'^', include(router.urls)), - url(r'^api-docs/$', views.schema_view), + url(r'^api-docs/$', views.schema_view,name='schema_view'), url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')), - url(r'^api/workouts/(?P\d+)/strokedata/$',views.strokedatajson), - url(r'^500v/$',views.error500_view), + url(r'^api/workouts/(?P\d+)/strokedata/$',views.strokedatajson,name='strokedatajson'), + url(r'^500v/$',views.error500_view,name='error500_view'), url(r'^502/$', TemplateView.as_view(template_name='502.html'),name='502'), url(r'^500/$', TemplateView.as_view(template_name='500.html'),name='500'), url(r'^404/$', TemplateView.as_view(template_name='404.html'),name='404'), url(r'^400/$', TemplateView.as_view(template_name='400.html'),name='400'), url(r'^403/$', TemplateView.as_view(template_name='403.html'),name='403'), # url(r'^imports/$', views.imports_view), - url(r'^exportallworkouts/?/$',views.workouts_summaries_email_view), - url(r'^update_empower/$',views.rower_update_empower_view), - url(r'^agegroupcp/(?P\d+)/$',views.agegroupcpview), - url(r'^agegroupcp/(?P\d+)/(?P\d+)/$',views.agegroupcpview), + url(r'^exportallworkouts/?/$',views.workouts_summaries_email_view,name='workouts_summaries_email_view'), + url(r'^update_empower/$',views.rower_update_empower_view,name='rower_update_empower_view'), + url(r'^agegroupcp/(?P\d+)/$',views.agegroupcpview,name='agegroupcpview'), + url(r'^agegroupcp/(?P\d+)/(?P\d+)/$',views.agegroupcpview,name='agegroupcpview'), url(r'^ajax_agegroup/(?P\d+)/(?P\w+.*)/(?P\w+.*)/(?P\d+)/$', - views.ajax_agegrouprecords), - url(r'^updatefitness/(?P\w+.*)/(?P\d+)/$',views.fitness_metric_view), - url(r'^updatefitness/(?P\w+.*)/$',views.fitness_metric_view), - url(r'^updatefitness/$',views.fitness_metric_view), + views.ajax_agegrouprecords,name='ajax_agegrouprecords'), + url(r'^updatefitness/(?P\w+.*)/(?P\d+)/$',views.fitness_metric_view,name='fitness_metric_view'), + url(r'^updatefitness/(?P\w+.*)/$',views.fitness_metric_view,name='fitness_metric_view'), + url(r'^updatefitness/$',views.fitness_metric_view,name='fitness_metric_view'), url(r'^agegrouprecords/(?P\w+.*)/(?P\w+.*)/(?P\d+)m/$', - views.agegrouprecordview), + views.agegrouprecordview,name='agegrouprecordview'), url(r'^agegrouprecords/(?P\w+.*)/(?P\w+.*)/(?P\d+)min/$', - views.agegrouprecordview), + views.agegrouprecordview,name='agegrouprecordview'), url(r'^agegrouprecords/(?P\w+.*)/(?P\w+.*)/$', - views.agegrouprecordview), - url(r'^list-workouts/ranking/$',views.workouts_view,{'rankingonly':True}), - url(r'^list-workouts/team/(?P\d+)/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.workouts_view), - url(r'^list-workouts/team/(?P\d+)/$',views.workouts_view), - url(r'^(?P\d+)/list-workouts/$',views.workouts_view), - url(r'^(?P\d+)/list-workouts/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.workouts_view), - url(r'^list-workouts/user/(?P\d+)/$',views.workouts_view), - url(r'^list-workouts/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/user/(?P\d+)/$',views.workouts_view), - url(r'^list-workouts/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.workouts_view), - url(r'^virtualevents/$',views.virtualevents_view), - url(r'^virtualevent/create/$',views.virtualevent_create_view), - url(r'^virtualevent/createindoor/$',views.indoorvirtualevent_create_view), + views.agegrouprecordview,name='agegrouprecordview'), + url(r'^list-workouts/ranking/$',views.workouts_view,{'rankingonly':True}, + name='workouts_view'), + url(r'^list-workouts/team/(?P\d+)/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.workouts_view, + name='workouts_view'), + url(r'^list-workouts/team/(?P\d+)/$',views.workouts_view, + name='workouts_view'), + url(r'^(?P\d+)/list-workouts/$',views.workouts_view, + name='workouts_view'), + url(r'^(?P\d+)/list-workouts/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.workouts_view, + name='workouts_view'), + url(r'^list-workouts/user/(?P\d+)/$',views.workouts_view, + name='workouts_view'), + url(r'^list-workouts/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/user/(?P\d+)/$',views.workouts_view, + name='workouts_view'), + url(r'^list-workouts/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.workouts_view, + name='workouts_view'), + url(r'^virtualevents/$',views.virtualevents_view,name='virtualevents_view'), + url(r'^virtualevent/create/$',views.virtualevent_create_view,name='virtualevent_create_view'), + url(r'^virtualevent/createindoor/$',views.indoorvirtualevent_create_view,name='indoorvirtualevent_create_view'), url(r'^raceregistration/togglenotification/(?P\d+)/$', - views.virtualevent_toggle_email_view), + views.virtualevent_toggle_email_view,name='virtualevent_toggle_email_view'), url(r'^indoorraceregistration/togglenotification/(?P\d+)/$', - views.indoorvirtualevent_toggle_email_view), - url(r'^virtualevent/(?P\d+)/$',views.virtualevent_view), - url(r'^virtualevent/(?P\d+)/ranking$',views.virtualevent_ranking_view), - url(r'^virtualevent/(?P\d+)/edit/$',views.virtualevent_edit_view), - url(r'^virtualevent/(?P\d+)/editindoor/$',views.indoorvirtualevent_edit_view), - url(r'^virtualevent/(?P\d+)/register/$',views.virtualevent_register_view), - url(r'^virtualevent/(?P\d+)/registerindoor/$',views.indoorvirtualevent_register_view), - url(r'^virtualevent/(?P\d+)/adddiscipline/$',views.virtualevent_addboat_view), - url(r'^virtualevent/(?P\d+)/withdraw/(?P\d+)/$',views.virtualevent_withdraw_view), - url(r'^virtualevent/(?P\d+)/withdraw/$',views.virtualevent_withdraw_view), + views.indoorvirtualevent_toggle_email_view,name='indoorvirtualevent_toggle_email_view'), + url(r'^virtualevent/(?P\d+)/$',views.virtualevent_view,name='virtualevent_view'), + url(r'^virtualevent/(?P\d+)/ranking$',views.virtualevent_ranking_view,name='virtualevent_ranking_view'), + url(r'^virtualevent/(?P\d+)/edit/$',views.virtualevent_edit_view,name='virtualevent_edit_view'), + url(r'^virtualevent/(?P\d+)/editindoor/$',views.indoorvirtualevent_edit_view,name='indoorvirtualevent_edit_view'), + url(r'^virtualevent/(?P\d+)/register/$',views.virtualevent_register_view,name='virtualevent_register_view'), + url(r'^virtualevent/(?P\d+)/registerindoor/$',views.indoorvirtualevent_register_view,name='indoorvirtualevent_register_view'), + url(r'^virtualevent/(?P\d+)/adddiscipline/$',views.virtualevent_addboat_view,name='virtualevent_addboat_view'), + url(r'^virtualevent/(?P\d+)/withdraw/(?P\d+)/$',views.virtualevent_withdraw_view,name='virtualevent_withdraw_view'), + url(r'^virtualevent/(?P\d+)/withdraw/$',views.virtualevent_withdraw_view,name='virtualevent_withdraw_view'), url(r'^virtualevent/(?P\d+)/submit/$', - views.virtualevent_submit_result_view), + views.virtualevent_submit_result_view,name='virtualevent_submit_result_view'), url(r'^virtualevent/(?P\d+)/submit/(?P\d+)/$', - views.virtualevent_submit_result_view), + views.virtualevent_submit_result_view,name='virtualevent_submit_result_view'), url(r'^virtualevent/(?P\d+)/disqualify/(?P\d+)/', - views.virtualevent_disqualify_view), - url(r'^list-workouts/$',views.workouts_view), - url(r'^list-courses/$',views.courses_view), - url(r'^courses/upload/$',views.course_upload_view), - url(r'^workout/addmanual/$',views.addmanual_view), - url(r'^team-compare-select/workout/(?P\d+)/team/(?P\d+)/user/(?P\d+)/$',views.team_comparison_select), - url(r'^team-compare-select/team/(?P\d+)/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/user/(?P\d+)/$',views.team_comparison_select), - url(r'^team-compare-select/team/(?P\d+)/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.team_comparison_select), - url(r'^team-compare-select/workout/(?P\d+)/team/(?P\d+)/$',views.team_comparison_select), - url(r'^team-compare-select/team/(?P\d+)/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.team_comparison_select), - url(r'^team-compare-select/team/(?P\d+)/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/user/(?P\d+)/$',views.team_comparison_select), - url(r'^team-compare-select/workout/(?P\d+)/team/(?P\d+)/$',views.team_comparison_select), - url(r'^team-compare-select/workout/(?P\d+)/team/(?P\d+)/user/(?P\d+)/$',views.team_comparison_select), - url(r'^team-compare-select/workout/(?P\d+)/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.team_comparison_select), - url(r'^team-compare-select/workout/(?P\d+)/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/user/(?P\d+)/$',views.team_comparison_select), - url(r'^team-compare-select/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/user/(?P\d+)/$',views.team_comparison_select), - url(r'^team-compare-select/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.team_comparison_select), - url(r'^team-compare-select/workout/(?P\d+)/user/(?P\d+)/$',views.team_comparison_select), - url(r'^team-compare-select/team/(?P\d+)/user/(?P\d+)/$',views.team_comparison_select), - url(r'^team-compare-select/workout/(?P\d+)/$',views.team_comparison_select), - url(r'^team-compare-select/team/(?P\d+)/$',views.team_comparison_select), - url(r'^team-compare-select/user/(?P\d+)/$',views.team_comparison_select), - url(r'^team-compare-select/$',views.team_comparison_select), - url(r'^workouts-join-select/team/(?P\d+)/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.workouts_join_select), - url(r'^workouts-join/$',views.workouts_join_view), - url(r'^workouts-join-select/team/(?P\d+)/$',views.workouts_join_select), - url(r'^workouts-join-select/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.workouts_join_select), - url(r'^workouts-join-select/$',views.workouts_join_select), - url(r'^user-boxplot-select/user/(?P\d+)/$',views.user_boxplot_select), - url(r'^user-boxplot-select/$',views.user_boxplot_select), - url(r'^user-multiflex-select/user/(?P\d+)/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.user_multiflex_select), - url(r'^user-multiflex-select/user/(?P\d+)/$',views.user_multiflex_select), - url(r'^user-multiflex-select/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.user_multiflex_select), - url(r'^user-multiflex-select/$',views.user_multiflex_select), - url(r'^list-jobs/$',views.session_jobs_view), - url(r'^jobs-status/$',views.session_jobs_status), + views.virtualevent_disqualify_view,name='virtualevent_submit_disqualify_view'), + url(r'^list-workouts/$',views.workouts_view, + name='workouts_view'), + url(r'^list-courses/$',views.courses_view,name='courses_view'), + url(r'^courses/upload/$',views.course_upload_view,name='course_upload_view'), + url(r'^workout/addmanual/$',views.addmanual_view,name='addmanual_view'), + url(r'^team-compare-select/workout/(?P\d+)/team/(?P\d+)/user/(?P\d+)/$',views.team_comparison_select,name='team_comparison_select'), + url(r'^team-compare-select/team/(?P\d+)/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/user/(?P\d+)/$',views.team_comparison_select,name='team_comparison_select'), + url(r'^team-compare-select/team/(?P\d+)/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.team_comparison_select,name='team_comparison_select'), + url(r'^team-compare-select/workout/(?P\d+)/team/(?P\d+)/$',views.team_comparison_select,name='team_comparison_select'), + url(r'^team-compare-select/team/(?P\d+)/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.team_comparison_select,name='team_comparison_select'), + url(r'^team-compare-select/team/(?P\d+)/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/user/(?P\d+)/$',views.team_comparison_select,name='team_comparison_select'), + url(r'^team-compare-select/workout/(?P\d+)/team/(?P\d+)/$',views.team_comparison_select,name='team_comparison_select'), + url(r'^team-compare-select/workout/(?P\d+)/team/(?P\d+)/user/(?P\d+)/$',views.team_comparison_select,name='team_comparison_select'), + url(r'^team-compare-select/workout/(?P\d+)/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.team_comparison_select,name='team_comparison_select'), + url(r'^team-compare-select/workout/(?P\d+)/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/user/(?P\d+)/$',views.team_comparison_select,name='team_comparison_select'), + url(r'^team-compare-select/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/user/(?P\d+)/$',views.team_comparison_select,name='team_comparison_select'), + url(r'^team-compare-select/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.team_comparison_select,name='team_comparison_select'), + url(r'^team-compare-select/workout/(?P\d+)/user/(?P\d+)/$',views.team_comparison_select,name='team_comparison_select'), + url(r'^team-compare-select/team/(?P\d+)/user/(?P\d+)/$',views.team_comparison_select,name='team_comparison_select'), + url(r'^team-compare-select/workout/(?P\d+)/$',views.team_comparison_select,name='team_comparison_select'), + url(r'^team-compare-select/team/(?P\d+)/$',views.team_comparison_select,name='team_comparison_select'), + url(r'^team-compare-select/user/(?P\d+)/$',views.team_comparison_select,name='team_comparison_select'), + url(r'^team-compare-select/$',views.team_comparison_select,name='team_comparison_select'), + url(r'^workouts-join-select/team/(?P\d+)/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.workouts_join_select,name='workouts_join_select'), + url(r'^workouts-join/$',views.workouts_join_view,name='workouts_join_view'), + url(r'^workouts-join-select/team/(?P\d+)/$',views.workouts_join_select,name='workouts_join_select'), + url(r'^workouts-join-select/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.workouts_join_select,name='workouts_join_select'), + url(r'^workouts-join-select/$',views.workouts_join_select,name='workouts_join_select'), + url(r'^user-boxplot-select/user/(?P\d+)/$',views.user_boxplot_select,name='user_boxplot_select'), + url(r'^user-boxplot-select/$',views.user_boxplot_select,name='user_boxplot_select'), + url(r'^user-multiflex-select/user/(?P\d+)/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.user_multiflex_select,name='user_multiflex_select'), + url(r'^user-multiflex-select/user/(?P\d+)/$',views.user_multiflex_select,name='user_multiflex_select'), + url(r'^user-multiflex-select/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.user_multiflex_select,name='user_multiflex_select'), + url(r'^user-multiflex-select/$',views.user_multiflex_select,name='user_multiflex_select'), + url(r'^list-jobs/$',views.session_jobs_view,name='session_jobs_view'), + url(r'^jobs-status/$',views.session_jobs_status,name='session_jobs_status'), url(r'^job-kill/(?P.*)/$',views.kill_async_job), url(r'^test-job/(?P\d+)/$',views.test_job_view), url(r'^test-job2/(?P\d+)/$',views.test_job_view2), - url(r'^record-progress/(?P\d+)/(?P.*)/$',views.post_progress), + url(r'^record-progress/(?P\d+)/(?P.*)/$',views.post_progress,name='post_progress'), url(r'^record-progress/(?P.*)/$',views.post_progress), url(r'^record-progress/$',views.post_progress), - url(r'^list-graphs/$',views.graphs_view), - url(r'^fitness-progress/$',views.fitnessmetric_view), - url(r'^fitness-progress/user/(?P\d+)/$',views.fitnessmetric_view), - url(r'^fitness-progress/user/(?P\d+)/(?P\w+.*)/$',views.fitnessmetric_view), - url(r'^ote-bests/user/(?P\d+)/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.rankings_view), - url(r'^ote-bests/user/(?P\d+)/$',views.rankings_view), - url(r'^ote-bests/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.rankings_view), - url(r'^ote-bests/$',views.rankings_view), - url(r'^(?P\d+)/ote-bests/$',views.rankings_view), - url(r'^(?P\d+)/ote-bests2/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.rankings_view2), - url(r'^ote-bests2/user/(?P\d+)/$',views.rankings_view2), - url(r'^ote-bests2/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.rankings_view2), - url(r'^ote-bests2/$',views.rankings_view2), - url(r'^otw-bests/user/(?P\d+)/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.otwrankings_view), - url(r'^otw-bests/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.otwrankings_view), - url(r'^otw-bests/user/(?P\d+)/$',views.otwrankings_view), - url(r'^otw-bests/$',views.otwrankings_view), - url(r'^ote-ranking/user/(?P\d+)/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.oterankings_view), - url(r'^ote-ranking/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.oterankings_view), - url(r'^ote-ranking/$',views.oterankings_view), - url(r'^ote-ranking/user/(?P\d+)/$',views.oterankings_view), - url(r'^flexall/(?P\w+.*)/(?P\w+.*)/(?P\w+.*)/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/user/(?P\d+)/$',views.cum_flex), - url(r'^flexall/(?P\w+.*)/(?P\w+.*)/(?P\w+.*)/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.cum_flex), - url(r'^flexall/(?P\w+.*)/(?P\w+.*)/(?P\w+.*)/$',views.cum_flex), - url(r'^flexall/user/(?P\d+)/$',views.cum_flex), - url(r'^flexall/$',views.cum_flex), - url(r'^flexalldata/$',views.cum_flex_data), - url(r'^histo/user/(?P\d+)/$',views.histo), - url(r'^histodata/$',views.histo_data), - url(r'^histo/user/(?P\d+)/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.histo), - url(r'^histo/$',views.histo), - url(r'^cumstats/user/(?P\d+)/$',views.cumstats), - url(r'^cumstats/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.cumstats), - url(r'^cumstats/user/(?P\d+)/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.cumstats), - url(r'^cumstats/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.cumstats), - url(r'^cumstats/$',views.cumstats), - url(r'^graph/(?P\d+)/$',views.graph_show_view), + url(r'^list-graphs/$',views.graphs_view,name='graphs_view'), + url(r'^fitness-progress/$',views.fitnessmetric_view,name='fitnessmetric_view'), + url(r'^fitness-progress/user/(?P\d+)/$',views.fitnessmetric_view,name='fitnessmetric_view'), + url(r'^fitness-progress/user/(?P\d+)/(?P\w+.*)/$',views.fitnessmetric_view,name='fitnessmetric_view'), + url(r'^ote-bests/user/(?P\d+)/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.rankings_view,name='rankings_view'), + url(r'^ote-bests/user/(?P\d+)/$',views.rankings_view,name='rankings_view'), + url(r'^ote-bests/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.rankings_view,name='rankings_view'), + url(r'^ote-bests/$',views.rankings_view,name='rankings_view'), + url(r'^(?P\d+)/ote-bests/$',views.rankings_view,name='rankings_view'), + url(r'^(?P\d+)/ote-bests2/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.rankings_view2,name='rankings_view2'), + url(r'^ote-bests2/user/(?P\d+)/$',views.rankings_view2,name='rankings_view2'), + url(r'^ote-bests2/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.rankings_view2,name='rankings_view2'), + url(r'^ote-bests2/$',views.rankings_view2,name='rankings_view2'), + url(r'^otw-bests/user/(?P\d+)/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.otwrankings_view,name='otwrankings_view'), + url(r'^otw-bests/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.otwrankings_view,name='otwrankings_view'), + url(r'^otw-bests/user/(?P\d+)/$',views.otwrankings_view,name='otwrankings_view'), + url(r'^otw-bests/$',views.otwrankings_view,name='otwrankings_view'), + url(r'^ote-ranking/user/(?P\d+)/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.oterankings_view,name='oterankings_view'), + url(r'^ote-ranking/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.oterankings_view,name='oterankings_view'), + url(r'^ote-ranking/$',views.oterankings_view,name='oterankings_view'), + url(r'^ote-ranking/user/(?P\d+)/$',views.oterankings_view,name='oterankings_view'), + url(r'^flexall/(?P\w+.*)/(?P\w+.*)/(?P\w+.*)/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/user/(?P\d+)/$',views.cum_flex,name='cum_flex'), + url(r'^flexall/(?P\w+.*)/(?P\w+.*)/(?P\w+.*)/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.cum_flex,name='cum_flex'), + url(r'^flexall/(?P\w+.*)/(?P\w+.*)/(?P\w+.*)/$',views.cum_flex,name='cum_flex'), + url(r'^flexall/user/(?P\d+)/$',views.cum_flex,name='cum_flex'), + url(r'^flexall/$',views.cum_flex,name='cum_flex'), + url(r'^flexalldata/$',views.cum_flex_data,name='cum_flex_data'), + url(r'^histo/user/(?P\d+)/$',views.histo,name='histo'), + url(r'^histodata/$',views.histo_data,name='histo_data'), + url(r'^histo/user/(?P\d+)/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.histo,name='histo'), + url(r'^histo/$',views.histo,name='histo'), + url(r'^cumstats/user/(?P\d+)/$',views.cumstats,name='cumstats'), + url(r'^cumstats/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.cumstats,name='cumstats'), + url(r'^cumstats/user/(?P\d+)/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.cumstats,name='cumstats'), + url(r'^cumstats/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.cumstats,name='cumstats'), + url(r'^cumstats/$',views.cumstats,name='cumstats'), + url(r'^graph/(?P\d+)/$',views.graph_show_view,name='graph_show_view'), url(r'^graph/(?P\d+)/delete/$',views.GraphDelete.as_view(),name='graph_delete'), - url(r'^workout/(?P\d+)/get-thumbnails/$',views.get_thumbnails), - url(r'^workout/(?P\d+)/toggle-ranking/$',views.workout_toggle_ranking), + url(r'^workout/(?P\d+)/get-thumbnails/$',views.get_thumbnails,name='get_thumbnails'), + url(r'^workout/(?P\d+)/toggle-ranking/$',views.workout_toggle_ranking,name='workout_toggle_ranking'), url(r'^workout/(?P\d+)/get-testscript/$',views.get_testscript), - url(r'^workout/upload/team/$',views.team_workout_upload_view), + url(r'^workout/upload/team/$',views.team_workout_upload_view,name='team_workout_upload_view'), url(r'^workout/upload/$',views.workout_upload_view,name='workout_upload_view'), - url(r'^workout/(?P\d+)/histo/$',views.workout_histo_view), + url(r'^workout/(?P\d+)/histo/$',views.workout_histo_view,name='workout_histo_view'), url(r'^workout/(?P\d+)/task/$',views.workout_test_task_view), - url(r'^workout/(?P\d+)/forcecurve/$',views.workout_forcecurve_view), - url(r'^workout/(?P\d+)/unsubscribe/$',views.workout_unsubscribe_view), + url(r'^workout/(?P\d+)/forcecurve/$',views.workout_forcecurve_view,name='workout_forcecurve_view'), + url(r'^workout/(?P\d+)/unsubscribe/$',views.workout_unsubscribe_view,name='workout_unsubscribe_view'), # url(r'^workout/(?P\d+)/export/$',views.workout_export_view), - url(r'^workout/(?P\d+)/comment/$',views.workout_comment_view), - url(r'^workout/(?P\d+)/emailtcx/$',views.workout_tcxemail_view), - url(r'^workout/(?P\d+)/emailgpx/$',views.workout_gpxemail_view), - url(r'^workout/(?P\d+)/emailcsv/$',views.workout_csvemail_view), - url(r'^workout/(?P\d+)/csvtoadmin/$',views.workout_csvtoadmin_view), - url(r'^ergcpdatatoadmin/(?P\d+)/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.otecp_toadmin_view), - url(r'^otwcpdatatoadmin/(?P\d+)/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.otwcp_toadmin_view), + url(r'^workout/(?P\d+)/comment/$',views.workout_comment_view,name='workout_comment_view'), + url(r'^workout/(?P\d+)/emailtcx/$',views.workout_tcxemail_view,name='workout_tcxemail_view'), + url(r'^workout/(?P\d+)/emailgpx/$',views.workout_gpxemail_view,name='workout_gpxemail_view'), + url(r'^workout/(?P\d+)/emailcsv/$',views.workout_csvemail_view,name='workout_csvemail_view'), + url(r'^workout/(?P\d+)/csvtoadmin/$',views.workout_csvtoadmin_view,name='workout_csvtoadmin_view'), + url(r'^ergcpdatatoadmin/(?P\d+)/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.otecp_toadmin_view,name='otecp_toadmin_view'), + url(r'^otwcpdatatoadmin/(?P\d+)/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.otwcp_toadmin_view,name='otwcp_toadmin_view'), # url(r'^workout/compare/(?P\d+)/$',views.workout_comparison_list), # url(r'^workout/compare2/(?P\d+)/(?P\d+)/(?P\w+.*)/(?P\w+.*)/$',views.workout_comparison_view), # url(r'^workout/compare/(?P\d+)/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.workout_comparison_list), url(r'^workout/(?P\d+)/edit/$',views.workout_edit_view, name='workout_edit_view'), - url(r'^workout/(?P\d+)/map/$',views.workout_map_view), + url(r'^workout/(?P\d+)/map/$',views.workout_map_view,name='workout_map_view'), # url(r'^workout/(?P\d+)/setprivate/$',views.workout_setprivate_view), - url(r'^workout/(?P\d+)/updatecp/$',views.workout_update_cp_view), + url(r'^workout/(?P\d+)/updatecp/$',views.workout_update_cp_view,name='workout_update_cp_view'), # url(r'^workout/(?P\d+)/makepublic/$',views.workout_makepublic_view), # url(r'^workout/(?P\d+)/geeky/$',views.workout_geeky_view), # url(r'^workout/(?P\d+)/advanced/$',views.workout_advanced_view), - url(r'^workout/(?P\d+)/instroke/(?P\w+.*)/$',views.instroke_chart), - url(r'^workout/(?P\d+)/instroke/$',views.instroke_view), + url(r'^workout/(?P\d+)/instroke/(?P\w+.*)/$',views.instroke_chart,name='instroke_chart'), + url(r'^workout/(?P\d+)/instroke/$',views.instroke_view,name='instroke_view'), url(r'^workout/(?P\d+)/stats/$',views.workout_stats_view,name='workout_stats_view'), url(r'^workout/(?P\d+)/data/$',views.workout_data_view, name='workout_data_view'), - url(r'^workout/(?P\d+)/otwsetpower/$',views.workout_otwsetpower_view), - url(r'^workout/(?P\d+)/interactiveotwplot/$',views.workout_otwpowerplot_view), - url(r'^workout/(?P\d+)/wind/$',views.workout_wind_view), - url(r'^workout/(?P\d+)/image/$',views.workout_uploadimage_view), - url(r'^virtualevent/(?P\d+)/compare/$',views.virtualevent_compare_view), + url(r'^workout/(?P\d+)/otwsetpower/$',views.workout_otwsetpower_view,name='workout_otwsetpower_view'), + url(r'^workout/(?P\d+)/interactiveotwplot/$',views.workout_otwpowerplot_view,name='workout_otwpowerplot_view'), + url(r'^workout/(?P\d+)/wind/$',views.workout_wind_view,name='workout_wind_view'), + url(r'^workout/(?P\d+)/image/$',views.workout_uploadimage_view,name='workout_uploadimage_view'), + url(r'^virtualevent/(?P\d+)/compare/$',views.virtualevent_compare_view,name='virtualevent_compare_view'), url(r'^virtualevent/(?P\d+)/image/$', - views.virtualevent_uploadimage_view), + views.virtualevent_uploadimage_view,name='virtualevent_uploadimage_view'), url(r'^virtualevent/(?P\d+)/setimage/(?P\d+)/$', - views.virtualevent_setlogo_view), + views.virtualevent_setlogo_view,name='virtualevent_setlog_view'), url(r'^logo/(?P\d+)/delete/$', - views.logo_delete_view), - url(r'^workout/(?P\d+)/darkskywind/$',views.workout_downloadwind_view), - url(r'^workout/(?P\d+)/metar/(?P\w+)/$',views.workout_downloadmetar_view), - url(r'^workout/(?P\d+)/stream/$',views.workout_stream_view), + views.logo_delete_view,name='logo_delete_view'), + url(r'^workout/(?P\d+)/darkskywind/$',views.workout_downloadwind_view,name='workout_downloadwind_view'), + url(r'^workout/(?P\d+)/metar/(?P\w+)/$',views.workout_downloadmetar_view,name='workout_downloadmetar_view'), + url(r'^workout/(?P\d+)/stream/$',views.workout_stream_view,name='workout_stream_view'), # url(r'^workout/(?P\d+)/crewnerdsummary/$',views.workout_crewnerd_summary_view), url(r'^workout/(?P\d+)/editintervals/$',views.workout_summary_edit_view, name='workout_summary_edit_view'), - url(r'^workout/(?P\d+)/restore/$',views.workout_summary_restore_view), - url(r'^workout/(?P\d+)/split/$',views.workout_split_view), + url(r'^workout/(?P\d+)/restore/$',views.workout_summary_restore_view,name='workout_summary_restore_view'), + url(r'^workout/(?P\d+)/split/$',views.workout_split_view,name='workout_split_view'), # url(r'^workout/(?P\d+)/interactiveplot/$',views.workout_biginteractive_view), - url(r'^workout/(?P\d+)/view/$',views.workout_view), - url(r'^workout/(?P\d+)/$',views.workout_view), - url(r'^workout/fusion/(?P\d+)/(?P\d+)/$',views.workout_fusion_view), - url(r'^workout/fusion/(?P\d+)/$',views.workout_fusion_list), - url(r'^workout/fusion/(?P\d+)/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.workout_fusion_list), + url(r'^workout/(?P\d+)/view/$',views.workout_view,name='workout_view'), + url(r'^workout/(?P\d+)/$',views.workout_view,name='workout_view'), + url(r'^workout/fusion/(?P\d+)/(?P\d+)/$',views.workout_fusion_view,name='workout_fusion_view'), + url(r'^workout/fusion/(?P\d+)/$',views.workout_fusion_list,name='workout_fusion_list'), + url(r'^workout/fusion/(?P\d+)/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)/$',views.workout_fusion_list,name='workout_fusion_list'), url(r'^help/$',TemplateView.as_view( template_name='help.html'),name='help' ), @@ -308,125 +317,126 @@ urlpatterns = [ # keeping the old URLs for retrofit url(r'^workout/(?P\d+)/addtimeplot/$', views.workout_add_chart_view, - {'plotnr':'1'}), + {'plotnr':'1'},name='workout_add_chart_view'), url(r'^workout/(?P\d+)/adddistanceplot/$', views.workout_add_chart_view, - {'plotnr':'2'}), + {'plotnr':'2'},name='workout_add_chart_view'), url(r'^workout/(?P\d+)/addpiechart/$', views.workout_add_chart_view, - {'plotnr':'3'}), + {'plotnr':'3'},name='workout_add_chart_view'), url(r'^workout/(?P\d+)/adddistanceplot2/$', views.workout_add_chart_view, - {'plotnr':'7'}), + {'plotnr':'7'},name='workout_add_chart_view'), url(r'^workout/(?P\d+)/addtimeplot2/$', views.workout_add_chart_view, - {'plotnr':'8'}), + {'plotnr':'8'},name='workout_add_chart_view'), url(r'^workout/(?P\d+)/addotwpowerplot/$', views.workout_add_chart_view, - {'plotnr':'9'}), + {'plotnr':'9'},name='workout_add_chart_view'), url(r'^workout/(?P\d+)/addpowerpiechart/$', views.workout_add_chart_view, - {'plotnr':'13'}), + {'plotnr':'13'},name='workout_add_chart_view'), # addstatic is the new URL -> need to update in templates url(r'^workout/(?P\d+)/addstatic/(?P\d+)/$', - views.workout_add_chart_view), - url(r'^workout/(?P\d+)/addstatic/$',views.workout_add_chart_view), + views.workout_add_chart_view,name='workout_add_chart_view'), + url(r'^workout/(?P\d+)/addstatic/$',views.workout_add_chart_view,name='workout_add_chart_view'), url(r'^workout/(?P\d+)/delete/$',login_required( views.WorkoutDelete.as_view()), name='workout_delete'), - url(r'^workout/(?P\d+)/smoothenpace/$',views.workout_smoothenpace_view), - url(r'^workout/(?P\d+)/undosmoothenpace/$',views.workout_undo_smoothenpace_view), - url(r'^workout/c2import/$',views.workout_c2import_view), - url(r'^workout/c2list/$',views.workout_c2import_view), - url(r'^workout/c2list/(?P\d+)/$',views.workout_c2import_view), - url(r'^workout/c2list/user/(?P\d+)/$',views.workout_c2import_view), - url(r'^workout/c2list/(?P\d+)/user/(?P\d+)/$',views.workout_c2import_view), - url(r'^workout/stravaimport/$',views.workout_stravaimport_view), - url(r'^workout/stravaimport/user/(?P\d+)/$',views.workout_stravaimport_view), - url(r'^workout/c2import/all/$',views.workout_getc2workout_all), - url(r'^workout/c2import/all/(?P\d+)/$',views.workout_getc2workout_all), - url(r'^workout/(?P\w+.*)import/(?P\d+)/$',views.workout_getimportview), - url(r'^workout/stravaimport/all/$',views.workout_getstravaworkout_all), - url(r'^workout/stravaimport/next/$',views.workout_getstravaworkout_next), - url(r'^workout/sporttracksimport/$',views.workout_sporttracksimport_view), - url(r'^workout/sporttracksimport/user/(?P\d+)/$',views.workout_sporttracksimport_view), - url(r'^workout/sporttracksimport/all/$',views.workout_getsporttracksworkout_all), - url(r'^workout/polarimport/$',views.workout_polarimport_view), - url(r'^workout/polarimport/user/(?P\d+)/',views.workout_polarimport_view), - url(r'^workout/runkeeperimport/$',views.workout_runkeeperimport_view), - url(r'^workout/runkeeperimport/user/(?P\d+)/$',views.workout_runkeeperimport_view), - url(r'^workout/underarmourimport/$',views.workout_underarmourimport_view), + url(r'^workout/(?P\d+)/smoothenpace/$',views.workout_smoothenpace_view,name='workout_smoothenpace_view'), + url(r'^workout/(?P\d+)/undosmoothenpace/$',views.workout_undo_smoothenpace_view,name='workout_undo_smoothenpace_view'), + url(r'^workout/c2import/$',views.workout_c2import_view,name='workout_c2import_view'), + url(r'^workout/c2list/$',views.workout_c2import_view,name='workout_c2import_view'), + url(r'^workout/c2list/(?P\d+)/$',views.workout_c2import_view,name='workout_c2import_view'), + url(r'^workout/c2list/user/(?P\d+)/$',views.workout_c2import_view,name='workout_c2import_view'), + url(r'^workout/c2list/(?P\d+)/user/(?P\d+)/$',views.workout_c2import_view,name='workout_c2import_view'), + url(r'^workout/stravaimport/$',views.workout_stravaimport_view,name='workout_stravaimport_view'), + url(r'^workout/stravaimport/user/(?P\d+)/$',views.workout_stravaimport_view,name='workout_stravaimport_view'), + url(r'^workout/c2import/all/$',views.workout_getc2workout_all,name='workout_getc2workout_all'), + url(r'^workout/c2import/all/(?P\d+)/$',views.workout_getc2workout_all,name='workout_getc2workout_all'), + url(r'^workout/(?P\w+.*)import/(?P\d+)/$',views.workout_getimportview,name='workout_getimportview'), + url(r'^workout/stravaimport/all/$',views.workout_getstravaworkout_all,name='workout_getstravaworkout_all'), + url(r'^workout/stravaimport/next/$',views.workout_getstravaworkout_next,name='workout_getstravaworkout_next'), + url(r'^workout/sporttracksimport/$',views.workout_sporttracksimport_view,name='workout_sporttracksimport_view'), + url(r'^workout/sporttracksimport/user/(?P\d+)/$',views.workout_sporttracksimport_view,name='workout_sporttracksimport_view'), + url(r'^workout/sporttracksimport/all/$',views.workout_getsporttracksworkout_all,name='workout_getsporttracksworkout_all'), + url(r'^workout/polarimport/$',views.workout_polarimport_view,name='workout_polarimport_view'), + url(r'^workout/polarimport/user/(?P\d+)/',views.workout_polarimport_view,name='workout_polarimport_view'), + url(r'^workout/runkeeperimport/$',views.workout_runkeeperimport_view,name='workout_runkeeperimport_view'), + url(r'^workout/runkeeperimport/user/(?P\d+)/$',views.workout_runkeeperimport_view,name='workout_runkeeperimport_view'), + url(r'^workout/underarmourimport/$',views.workout_underarmourimport_view,name='workout_underarmourimport_view'), # url(r'^workout/(?P\d+)/deleteconfirm/$',views.workout_delete_confirm_view), - url(r'^workout/(?P\d+)/c2uploadw/$',views.workout_c2_upload_view), - url(r'^workout/(?P\d+)/stravauploadw/$',views.workout_strava_upload_view), - url(r'^workout/(?P\d+)/recalcsummary/$',views.workout_recalcsummary_view), - url(r'^workout/(?P\d+)/sporttracksuploadw/$',views.workout_sporttracks_upload_view), - url(r'^workout/(?P\d+)/runkeeperuploadw/$',views.workout_runkeeper_upload_view), - url(r'^workout/(?P\d+)/underarmouruploadw/$',views.workout_underarmour_upload_view), - url(r'^workout/(?P\d+)/tpuploadw/$',views.workout_tp_upload_view), - url(r'^multi-compare/workout/(?P\d+)/user/(?P\d+)/$',views.multi_compare_view), - url(r'^multi-compare/workout/(?P\d+)/$',views.multi_compare_view), - url(r'^multi-compare/$',views.multi_compare_view), - url(r'^user-boxplot/user/(?P\d+)/$',views.boxplot_view), - url(r'^user-boxplot/$',views.boxplot_view), - url(r'^user-boxplot-data/$',views.boxplot_view_data), - url(r'^user-multiflex/user/(?P\d+)/$',views.multiflex_view), - url(r'^user-multiflex/$',views.multiflex_view), - url(r'^user-multiflex-data/$',views.multiflex_data), - url(r'^me/deactivate/$',views.deactivate_user), - url(r'^me/delete/$',views.remove_user), - url(r'^me/gdpr-optin-confirm/?/$',views.user_gdpr_confirm), - url(r'^me/gdpr-optin-confirm/$',views.user_gdpr_confirm), - url(r'^me/gdpr-optin/?/$',views.user_gdpr_optin), - url(r'^me/gdpr-optin/$',views.user_gdpr_optin), - url(r'^me/teams/$',views.rower_teams_view), - url(r'^me/calcdps/$',views.rower_calcdps_view), - url(r'^me/exportsettings/$',views.rower_exportsettings_view), - url(r'^me/exportsettings/user/(?P\d+)/$',views.rower_exportsettings_view), - url(r'^team/(?P\d+)/$',views.team_view), - url(r'^team/(?P\d+)/memberstats/$',views.team_members_stats_view), - url(r'^team/(?P\d+)/edit/$',views.team_edit_view), - url(r'^team/(?P\d+)/leaveconfirm/$',views.team_leaveconfirm_view), - url(r'^team/(?P\d+)/leave/$',views.team_leave_view), - url(r'^team/(?P\d+)/deleteconfirm/$',views.team_deleteconfirm_view), - url(r'^team/(?P\d+)/requestmembership/(?P\d+)/$',views.team_requestmembership_view), - url(r'^team/(?P\d+)/delete/$',views.team_delete_view), - url(r'^team/create/$',views.team_create_view), - url(r'^me/team/(?P\d+)/drop/(?P\d+)/$',views.manager_member_drop_view), - url(r'^me/invitation/(?P\d+)/reject/$',views.invitation_reject_view), - url(r'^me/invitation/(?P\d+)/revoke/$',views.invitation_revoke_view), - url(r'^me/invitation/$',views.rower_invitations_view), - url(r'^me/raise500/$',views.raise_500), - url(r'^me/invitation/(\w+.*)/$',views.rower_invitations_view), - url(r'^me/request/(?P\d+)/revoke/$',views.request_revoke_view), - url(r'^me/request/(?P\d+)/reject/$',views.request_reject_view), - url(r'^me/request/(\w+.*)/$',views.manager_requests_view), - url(r'^me/request/$',views.manager_requests_view), - url(r'^me/edit/$',views.rower_edit_view), - url(r'^me/edit/user/(?P\d+)/$',views.rower_edit_view), - url(r'^me/preferences/$',views.rower_prefs_view), - url(r'^me/transactions/$',views.transactions_view), - url(r'^me/preferences/user/(?P\d+)/$',views.rower_prefs_view), - url(r'^me/edit/(.+.*)/$',views.rower_edit_view), - url(r'^me/c2authorize/$',views.rower_c2_authorize), - url(r'^me/polarauthorize/$',views.rower_polar_authorize), - url(r'^me/revokeapp/(?P\d+)/$',views.rower_revokeapp_view), - url(r'^me/stravaauthorize/$',views.rower_strava_authorize), - url(r'^me/sporttracksauthorize/$',views.rower_sporttracks_authorize), - url(r'^me/underarmourauthorize/$',views.rower_underarmour_authorize), - url(r'^me/tpauthorize/$',views.rower_tp_authorize), - url(r'^me/runkeeperauthorize/$',views.rower_runkeeper_authorize), - url(r'^me/sporttracksrefresh/$',views.rower_sporttracks_token_refresh), - url(r'^me/underarmourrefresh/$',views.rower_underarmour_token_refresh), - url(r'^me/tprefresh/$',views.rower_tp_token_refresh), - url(r'^me/c2refresh/$',views.rower_c2_token_refresh), - url(r'^me/favoritecharts/$',views.rower_favoritecharts_view), - url(r'^me/favoritecharts/user/(?P\d+)/$',views.rower_favoritecharts_view), + url(r'^workout/(?P\d+)/c2uploadw/$',views.workout_c2_upload_view,name='workout_c2_upload_view'), + url(r'^workout/(?P\d+)/stravauploadw/$',views.workout_strava_upload_view,name='workout_strava_upload_view'), + url(r'^workout/(?P\d+)/recalcsummary/$',views.workout_recalcsummary_view,name='workout_recalcsummary_view'), + url(r'^workout/(?P\d+)/sporttracksuploadw/$',views.workout_sporttracks_upload_view,name='workout_sporttracks_upload_view'), + url(r'^workout/(?P\d+)/runkeeperuploadw/$',views.workout_runkeeper_upload_view,name='workout_runkeeper_upload_view'), + url(r'^workout/(?P\d+)/underarmouruploadw/$',views.workout_underarmour_upload_view,name='workout_underarmour_upload_view'), + url(r'^workout/(?P\d+)/tpuploadw/$',views.workout_tp_upload_view,name='workout_tp_upload_view'), + url(r'^multi-compare/workout/(?P\d+)/user/(?P\d+)/$',views.multi_compare_view, + name='multi_compare_view'), + url(r'^multi-compare/workout/(?P\d+)/$',views.multi_compare_view,name='multi_compare_view'), + url(r'^multi-compare/$',views.multi_compare_view,name='multi_compare_view'), + url(r'^user-boxplot/user/(?P\d+)/$',views.boxplot_view,name='boxplot_view'), + url(r'^user-boxplot/$',views.boxplot_view,name='boxplot_view'), + url(r'^user-boxplot-data/$',views.boxplot_view_data,name='boxplot_view_data'), + url(r'^user-multiflex/user/(?P\d+)/$',views.multiflex_view,name='multiflex_view'), + url(r'^user-multiflex/$',views.multiflex_view,name='multiflex_view'), + url(r'^user-multiflex-data/$',views.multiflex_data,name='multiflex_data'), + url(r'^me/deactivate/$',views.deactivate_user,name='deactivate_user'), + url(r'^me/delete/$',views.remove_user,name='remove_user'), + url(r'^me/gdpr-optin-confirm/?/$',views.user_gdpr_confirm,name='user_gdpr_confirm'), + url(r'^me/gdpr-optin-confirm/$',views.user_gdpr_confirm,name='user_gdpr_confirm'), + url(r'^me/gdpr-optin/?/$',views.user_gdpr_optin,name='user_gdpr_optin'), + url(r'^me/gdpr-optin/$',views.user_gdpr_optin,name='user_gdpr_optin'), + url(r'^me/teams/$',views.rower_teams_view,name='rower_teams_view'), + url(r'^me/calcdps/$',views.rower_calcdps_view,name='rower_calcdps_view'), + url(r'^me/exportsettings/$',views.rower_exportsettings_view,name='rower_exportsettings_view'), + url(r'^me/exportsettings/user/(?P\d+)/$',views.rower_exportsettings_view,name='rower_exportsettings_view'), + url(r'^team/(?P\d+)/$',views.team_view,name='team_view'), + url(r'^team/(?P\d+)/memberstats/$',views.team_members_stats_view,name='team_members_stats_view'), + url(r'^team/(?P\d+)/edit/$',views.team_edit_view,name='team_edit_view'), + url(r'^team/(?P\d+)/leaveconfirm/$',views.team_leaveconfirm_view,name='team_leaveconfirm_view'), + url(r'^team/(?P\d+)/leave/$',views.team_leave_view,name='team_leave_view'), + url(r'^team/(?P\d+)/deleteconfirm/$',views.team_deleteconfirm_view,name='team_deleteconfirm_view'), + url(r'^team/(?P\d+)/requestmembership/(?P\d+)/$',views.team_requestmembership_view,name='team_requestmembership_view'), + url(r'^team/(?P\d+)/delete/$',views.team_delete_view,name='team_delete_view'), + url(r'^team/create/$',views.team_create_view,name='team_create_view'), + url(r'^me/team/(?P\d+)/drop/(?P\d+)/$',views.manager_member_drop_view,name='manager_member_drop_view'), + url(r'^me/invitation/(?P\d+)/reject/$',views.invitation_reject_view,name='invitation_reject_view'), + url(r'^me/invitation/(?P\d+)/revoke/$',views.invitation_revoke_view,name='invitation_revoke_view'), + url(r'^me/invitation/$',views.rower_invitations_view,name='rower_invitations_view'), + url(r'^me/raise500/$',views.raise_500,name='raise_500'), + url(r'^me/invitation/(\w+.*)/$',views.rower_invitations_view,name='rower_invitations_view'), + url(r'^me/request/(?P\d+)/revoke/$',views.request_revoke_view,name='request_revoke_view'), + url(r'^me/request/(?P\d+)/reject/$',views.request_reject_view,name='request_reject_view'), + url(r'^me/request/(\w+.*)/$',views.manager_requests_view,name='manager_requests_view'), + url(r'^me/request/$',views.manager_requests_view,name='manager_requests_view'), + url(r'^me/edit/$',views.rower_edit_view,name='rower_edit_view'), + url(r'^me/edit/user/(?P\d+)/$',views.rower_edit_view,name='rower_edit_view'), + url(r'^me/preferences/$',views.rower_prefs_view,name='rower_prefs_view'), + url(r'^me/transactions/$',views.transactions_view,name='transactions_view'), + url(r'^me/preferences/user/(?P\d+)/$',views.rower_prefs_view,name='rower_prefs_view'), + url(r'^me/edit/(.+.*)/$',views.rower_edit_view,name='rower_edit_view'), + url(r'^me/c2authorize/$',views.rower_c2_authorize,name='rower_c2_authorize'), + url(r'^me/polarauthorize/$',views.rower_polar_authorize,name='rower_polar_authorize'), + url(r'^me/revokeapp/(?P\d+)/$',views.rower_revokeapp_view,name='rower_revokeapp_view'), + url(r'^me/stravaauthorize/$',views.rower_strava_authorize,name='rower_strava_authorize'), + url(r'^me/sporttracksauthorize/$',views.rower_sporttracks_authorize,name='rower_sporttracks_authorize'), + url(r'^me/underarmourauthorize/$',views.rower_underarmour_authorize,name='rower_underarmour_authorize'), + url(r'^me/tpauthorize/$',views.rower_tp_authorize,name='rower_tp_authorize'), + url(r'^me/runkeeperauthorize/$',views.rower_runkeeper_authorize,name='rower_runkeeper_authorize'), + url(r'^me/sporttracksrefresh/$',views.rower_sporttracks_token_refresh,name='rower_sporttracks_token_refresh'), + url(r'^me/underarmourrefresh/$',views.rower_underarmour_token_refresh,name='rower_underarmoud_token_refresh'), + url(r'^me/tprefresh/$',views.rower_tp_token_refresh,name='rower_tp_token_refresh'), + url(r'^me/c2refresh/$',views.rower_c2_token_refresh,name='rower_c2_token_refresh'), + url(r'^me/favoritecharts/$',views.rower_favoritecharts_view,name='rower_favoritecharts_view'), + url(r'^me/favoritecharts/user/(?P\d+)/$',views.rower_favoritecharts_view,name='rower_favoritecharts_view'), # url(r'^me/workflowconfig/$',views.workout_workflow_config_view), - url(r'^me/workflowconfig2/$',views.workout_workflow_config2_view), - url(r'^me/workflowconfig2/user/(?P\d+)/$',views.workout_workflow_config2_view), - url(r'^me/workflowdefault/$',views.workflow_default_view), - url(r'^email/send/$', views.sendmail), + url(r'^me/workflowconfig2/$',views.workout_workflow_config2_view,name='workout_workflow_config2_view'), + url(r'^me/workflowconfig2/user/(?P\d+)/$',views.workout_workflow_config2_view,name='workout_workflow_config2_view'), + url(r'^me/workflowdefault/$',views.workflow_default_view,name='workflow_default_view'), + url(r'^email/send/$', views.sendmail,name='sendmail'), url(r'^email/thankyou/$', TemplateView.as_view(template_name='thankyou.html'), name='thankyou'), url(r'^email/$', TemplateView.as_view(template_name='email.html'), name='email'), url(r'^about', TemplateView.as_view(template_name='about_us.html'),name='about'), @@ -440,39 +450,39 @@ urlpatterns = [ url(r'^analysis/$', views.analysis_view,name='analysis'), url(r'^laboratory/$', views.laboratory_view,name='laboratory'), url(r'^promembership', TemplateView.as_view(template_name='promembership.html'),name='promembership'), - url(r'^checkout/(?P\d+)/$',views.payment_confirm_view), - url(r'^upgradecheckout/(?P\d+)/$',views.upgrade_confirm_view), - url(r'^downgradecheckout/(?P\d+)/$',views.downgrade_confirm_view), + url(r'^checkout/(?P\d+)/$',views.payment_confirm_view,name='payment_confirm_view'), + url(r'^upgradecheckout/(?P\d+)/$',views.upgrade_confirm_view,name='upgrade_confirm_view'), + url(r'^downgradecheckout/(?P\d+)/$',views.downgrade_confirm_view,name='downgrade_confirm_view'), url(r'^billing/$',views.billing_view,name='billing'), url(r'^upgrade/$',views.upgrade_view,name='upgrade'), url(r'^downgrade/$',views.downgrade_view,name='downgrade'), - url(r'^paymentcompleted/$',views.payment_completed_view), - url(r'^downgradecompleted/$',views.downgrade_completed_view), + url(r'^paymentcompleted/$',views.payment_completed_view,name='payment_completed_view'), + url(r'^downgradecompleted/$',views.downgrade_completed_view,name='downgrade_completed_view'), url(r'^paidplans/$',views.paidplans_view,name='paidplans'), - url(r'^me/cancelsubscriptions/$',views.plan_stop_view), - url(r'^me/cancelsubscription/(?P[\w\ ]+.*)/$',views.plan_tobasic_view), + url(r'^me/cancelsubscriptions/$',views.plan_stop_view,name='plan_stop_view'), + url(r'^me/cancelsubscription/(?P[\w\ ]+.*)/$',views.plan_tobasic_view,name='plan_tobasic_view'), url(r'^checkouts/$',views.checkouts_view,name='checkouts'), url(r'^upgradecheckouts/$',views.upgrade_checkouts_view,name='upgrade_checkouts'), url(r'^downgradecheckouts/$',views.downgrade_checkouts_view,name='downgrade_checkouts'), - url(r'^planrequired/',views.planrequired_view), - url(r'^starttrial/$',views.start_trial_view), - url(r'^startplantrial/$',views.start_plantrial_view), + url(r'^planrequired/',views.planrequired_view,name='planrequired_view'), + url(r'^starttrial/$',views.start_trial_view,name='start_trial_view'), + url(r'^startplantrial/$',views.start_plantrial_view,name='start_plantrial_view'), # url(r'^planmembership', TemplateView.as_view(template_name='planmembership.html'),name='planmembership'), # url(r'^paypaltest', TemplateView.as_view(template_name='paypaltest.html'),name='paypaltest'), url(r'^legal', TemplateView.as_view(template_name='legal.html'),name='legal'), - url(r'^register/$',views.rower_register_view), + url(r'^register/$',views.rower_register_view,name='rower_register_view'), url(r'^register/thankyou/$', TemplateView.as_view(template_name='registerthankyou.html'), name='registerthankyou'), url(r'^workout/(?P\d+)/workflow/$',views.workout_workflow_view, name='workout_workflow_view'), - url(r'^workout/(?P\d+)/flexchart/(?P[\w\ ]+.*)/(?P[\w\ ]+.*)/(?P[\w\ ]+.*)/(?P\w+)/$',views.workout_flexchart3_view), - url(r'^workout/(?P\d+)/flexchart/(?P\w+.*)/(?P[\w\ ]+.*)/(?P[\w\ ]+.*)/(?P\w+.*)/$',views.workout_flexchart3_view), - url(r'^workout/(?P\d+)/flexchart/(?P\w+.*)/(?P[\w\ ]+.*)/(?P[\w\ ]+.*)/$',views.workout_flexchart3_view), - url(r'^workout/(?P\d+)/flexchart/$',views.workout_flexchart3_view), + url(r'^workout/(?P\d+)/flexchart/(?P[\w\ ]+.*)/(?P[\w\ ]+.*)/(?P[\w\ ]+.*)/(?P\w+)/$',views.workout_flexchart3_view,name='workout_flexchart3_view'), + url(r'^workout/(?P\d+)/flexchart/(?P\w+.*)/(?P[\w\ ]+.*)/(?P[\w\ ]+.*)/(?P\w+.*)/$',views.workout_flexchart3_view,name='workout_flexchart3_view'), + url(r'^workout/(?P\d+)/flexchart/(?P\w+.*)/(?P[\w\ ]+.*)/(?P[\w\ ]+.*)/$',views.workout_flexchart3_view,name='workout_flexchart3_view'), + url(r'^workout/(?P\d+)/flexchart/$',views.workout_flexchart3_view,name='workout_flexchart3_view'), # url(r'^workout/compare/(?P\d+)/(?P\d+)/(?P\w+.*)/(?P[\w\ ]+.*)/(?P[\w\ ]+.*)/$',views.workout_comparison_view2), # url(r'^workout/compare/(?P\d+)/(?P\d+)/(?P\w+.*)/(?P[\w\ ]+.*)/$',views.workout_comparison_view2), - url(r'^test\_callback',views.rower_process_testcallback), - url(r'^createplan/$',views.rower_create_trainingplan), - url(r'^createplan/user/(?P\d+)/$',views.rower_create_trainingplan), + url(r'^test\_callback',views.rower_process_testcallback,name='rower_process_testcallback'), + url(r'^createplan/$',views.rower_create_trainingplan,name='rower_create_trainingplan'), + url(r'^createplan/user/(?P\d+)/$',views.rower_create_trainingplan,name='rower_create_trainingplan'), url(r'^deleteplan/(?P\d+)/$',login_required( views.TrainingPlanDelete.as_view())), url(r'^deletemicrocycle/(?P\d+)/$',login_required( diff --git a/rowers/views/.#__init__.py b/rowers/views/.#__init__.py deleted file mode 100644 index 24819440..00000000 --- a/rowers/views/.#__init__.py +++ /dev/null @@ -1 +0,0 @@ -E408191@CZ27LT9RCGN72.1380:1549472010 \ No newline at end of file diff --git a/rowers/views/__init__.py b/rowers/views/__init__.py new file mode 100644 index 00000000..848a9a00 --- /dev/null +++ b/rowers/views/__init__.py @@ -0,0 +1,12 @@ +from .analysisviews import * +from .apiviews import * +from .errorviews import * +from .exportviews import * +from .importviews import * +from .otherviews import * +from .paymentviews import * +from .planviews import * +from .racesviews import * +from .teamviews import * +from .userviews import * +from .workoutviews import * diff --git a/rowers/views/analysisviews.py b/rowers/views/analysisviews.py index b31ad0cc..3fc8cc46 100644 --- a/rowers/views/analysisviews.py +++ b/rowers/views/analysisviews.py @@ -401,7 +401,7 @@ def cum_flex(request,theuser=0, def planrequired_view(request): messages.info(request,"This functionality requires Coach or Self-Coach membership") - return HttpResponseRedirect(reverse(paidplans_view)) + return HttpResponseRedirect(reverse('paidplans')) @user_passes_test(hasplannedsessions,login_url="/rowers/paidplans", message="This functionality requires a Coach or Self-Coach plan", @@ -2149,7 +2149,7 @@ def user_multiflex_select(request, 'name': 'Compare Select' }, { - 'url':reverse(multi_compare_view), + 'url':reverse('multi_compare_view'), 'name': 'Comparison Chart' } ] diff --git a/rowers/views/importviews.py b/rowers/views/importviews.py index 83ee32b9..b0891fe4 100644 --- a/rowers/views/importviews.py +++ b/rowers/views/importviews.py @@ -118,7 +118,7 @@ def workout_strava_upload_view(request,id=0): os.remove(tcxfile) except WindowsError: pass - url = reverse(workout_edit_view,kwargs={'id':w.id}) + url = reverse('workout_edit_view',kwargs={'id':w.id}) messages.info(request,mes) @@ -235,7 +235,7 @@ def workout_runkeeper_upload_view(request,id=0): runkeeperid = runkeeperstuff.getidfromresponse(response) w.uploadedtorunkeeper = runkeeperid w.save() - url = reverse(workout_edit_view, kwargs={'id':w.id}) + url = reverse('workout_edit_view', kwargs={'id':w.id}) return HttpResponseRedirect(url) else: @@ -300,7 +300,7 @@ def workout_underarmour_upload_view(request,id=0): underarmourid = underarmourstuff.getidfromresponse(response) w.uploadedtounderarmour = underarmourid w.save() - url = reverse(workout_edit_view,kwargs={'id':w.id}) + url = reverse('workout_edit_view',kwargs={'id':w.id}) return HttpResponseRedirect(url) else: @@ -367,7 +367,7 @@ def workout_sporttracks_upload_view(request,id=0): message = "Upload to SportTracks was successful" messages.info(request,message) - url = reverse(workout_edit_view,kwargs={'id':w.id}) + url = reverse('workout_edit_view',kwargs={'id':w.id}) return HttpResponseRedirect(url) else: s = response @@ -528,7 +528,7 @@ def rower_c2_token_refresh(request): message = "Something went wrong (refreshing tokens). Please reauthorize:" messages.error(request,message) - url = reverse(workouts_view) + url = reverse('workouts_view') return HttpResponseRedirect(url) @@ -556,7 +556,7 @@ def rower_underarmour_token_refresh(request): successmessage = "Tokens refreshed. Good to go" messages.info(request,successmessage) - url = reverse(workouts_view) + url = reverse('workouts_view') return HttpResponseRedirect(url) @@ -584,7 +584,7 @@ def rower_tp_token_refresh(request): successmessage = "Tokens refreshed. Good to go" messages.info(request,successmessage) - url = reverse(workouts_view) + url = reverse('workouts_view') return HttpResponseRedirect(url) @@ -613,7 +613,7 @@ def rower_sporttracks_token_refresh(request): successmessage = "Tokens refreshed. Good to go" messages.info(request,successmessage) - url = reverse(workouts_view) + url = reverse('workouts_view') return HttpResponseRedirect(url) @@ -629,7 +629,7 @@ def rower_process_callback(request): message = "The resource owner or authorization server denied the request" messages.error(request,message) - url = reverse(workouts_view) + url = reverse('workouts_view') return HttpResponseRedirect(url) @@ -639,7 +639,7 @@ def rower_process_callback(request): message += ' Contact info@rowsandall.com if this behavior persists.' messages.error(request,message) - url = reverse(workouts_view) + url = reverse('workouts_view') return HttpResponseRedirect(url) @@ -658,7 +658,7 @@ def rower_process_callback(request): successmessage = "Tokens stored. Good to go" messages.info(request,successmessage) - url = reverse(workouts_view) + url = reverse('workouts_view') return HttpResponseRedirect(url) @@ -681,7 +681,7 @@ def rower_process_polarcallback(request): message = "access error" messages.error(request,message) - url = reverse(workouts_view) + url = reverse('workouts_view') return HttpResponseRedirect(url) @@ -700,7 +700,7 @@ def rower_process_polarcallback(request): successmessage = "Tokens stored. Good to go" messages.info(request,successmessage) - url = reverse(workouts_view) + url = reverse('workouts_view') return HttpResponseRedirect(url) @@ -720,7 +720,7 @@ def rower_process_stravacallback(request): message = "access error" messages.error(request,message) - url = reverse(workouts_view) + url = reverse('workouts_view') return HttpResponseRedirect(url) @@ -743,12 +743,12 @@ def rower_process_stravacallback(request): successmessage = "Tokens stored. Good to go" messages.info(request,successmessage) - url = reverse(workouts_view) + url = reverse('workouts_view') return HttpResponseRedirect(url) else: message = "Something went wrong with the Strava authorization" messages.error(request,message) - url = reverse(workouts_view) + url = reverse('workouts_view') return HttpResponseRedirect(url) @@ -762,7 +762,7 @@ def rower_process_runkeepercallback(request): if access_token == 0: messages.error(request,"Something went wrong importing the token") - url = reverse(workouts_view) + url = reverse('workouts_view') return HttpResponseRedirect(url) @@ -775,7 +775,7 @@ def rower_process_runkeepercallback(request): successmessage = "Tokens stored. Good to go" messages.info(request,successmessage) - url = reverse(workouts_view) + url = reverse('workouts_view') return HttpResponseRedirect(url) @@ -803,7 +803,7 @@ def rower_process_sporttrackscallback(request): successmessage = "Tokens stored. Good to go" messages.info(request,successmessage) - url = reverse(workouts_view) + url = reverse('workouts_view') return HttpResponseRedirect(url) @@ -830,7 +830,7 @@ def rower_process_underarmourcallback(request): successmessage = "Tokens stored. Good to go" messages.info(request,successmessage) - url = reverse(workouts_view) + url = reverse('workouts_view') return HttpResponseRedirect(url) @@ -856,7 +856,7 @@ def rower_process_tpcallback(request): successmessage = "Tokens stored. Good to go" messages.info(request,successmessage) - url = reverse(workouts_view) + url = reverse('workouts_view') return HttpResponseRedirect(url) @@ -908,7 +908,7 @@ def workout_stravaimport_view(request,message="",userid=0): return HttpResponseRedirect("/rowers/me/stravaauthorize/") message = "Something went wrong in workout_stravaimport_view" messages.error(request,message) - url = reverse(workouts_view) + url = reverse('workouts_view') return HttpResponseRedirect(url) else: workouts = [] @@ -999,7 +999,7 @@ def workout_runkeeperimport_view(request,message="",userid=0): if settings.DEBUG: return HttpResponse(res) else: - url = reverse(workouts_view) + url = reverse('workouts_view') return HttpResponseRedirect(url) workouts = [] @@ -1098,7 +1098,7 @@ def workout_polarimport_view(request,userid=0): a = exercises.status_code if a == 401: messages.error(request,'Not authorized. You need to connect to Polar first') - url = reverse(workouts_view) + url = reverse('workouts_view') return HttpResponseRedirect(url) except: pass @@ -1164,7 +1164,7 @@ def workout_sporttracksimport_view(request,message="",userid=0): if settings.DEBUG: return HttpResponse(res) else: - url = reverse(workouts_view) + url = reverse('workouts_view') return HttpResponseRedirect(url) workouts = [] @@ -1231,7 +1231,7 @@ def c2listdebug_view(request,page=1,message=""): if settings.DEBUG: return HttpResponse(res) else: - url = reverse(workouts_view) + url = reverse('workouts_view') return HttpResponseRedirect(url) else: workouts = [] @@ -1285,7 +1285,7 @@ def workout_getc2workout_all(request,page=1,message=""): workoutid = c2stuff.create_async_workout(alldata, request.user,c2id) - url = reverse(workouts_view) + url = reverse('workouts_view') return HttpResponseRedirect(url) @@ -1310,7 +1310,7 @@ def workout_c2import_view(request,page=1,userid=0,message=""): if (res.status_code != 200): message = "Something went wrong in workout_c2import_view (C2 token refresh)" messages.error(request,message) - url = reverse(workouts_view) + url = reverse('workouts_view') return HttpResponseRedirect(url) workouts = [] @@ -1380,7 +1380,7 @@ def workout_getimportview(request,externalid,source = 'c2'): res = importsources[source].get_workout(request.user,externalid) if not res[0]: messages.error(request,res[1]) - url = reverse(workouts_view) + url = reverse('workouts_view') return HttpResponseRedirect(url) @@ -1393,7 +1393,7 @@ def workout_getimportview(request,externalid,source = 'c2'): try: if strokedata == 0: messages.error(request,'An error occurred importing the workout from Concept2') - url = reverse(workouts_view) + url = reverse('workouts_view') return HttpResponseRedirect(url) except ValueError: pass @@ -1552,7 +1552,7 @@ def workout_getsporttracksworkout_all(request): w.uploadedtosporttracks=sporttracksid w.save() - url = reverse(workouts_view) + url = reverse('workouts_view') return HttpResponseRedirect(url) @@ -1566,7 +1566,7 @@ def workout_getstravaworkout_all(request): else: messages.error(request,"Couldn't import Strava workouts ") - url = reverse(workouts_view) + url = reverse('workouts_view') return HttpResponseRedirect(url) @@ -1598,7 +1598,7 @@ def workout_getstravaworkout_next(request): - url = reverse(workouts_view) + url = reverse('workouts_view') return HttpResponseRedirect(url) diff --git a/rowers/views/paymentviews.py b/rowers/views/paymentviews.py index 1d422931..0e7b88f9 100644 --- a/rowers/views/paymentviews.py +++ b/rowers/views/paymentviews.py @@ -1,4 +1,6 @@ +from statements import * + def paidplans_view(request): if not request.user.is_anonymous(): r = getrequestrower(request) diff --git a/rowers/views/planviews.py b/rowers/views/planviews.py index 9ba2d6e8..8f0c26ee 100644 --- a/rowers/views/planviews.py +++ b/rowers/views/planviews.py @@ -401,7 +401,7 @@ def plannedsession_teamcreate_view(request, teaminitial = [str(teams[0].id)] else: messages.info(request,"You have no teams established yet. We are redirecting you to the Team Management page.") - url = reverse(rower_teams_view) + url = reverse('rower_teams_view') return HttpResponseRedirect(url) startdate,enddate = get_dates_timeperiod(request) diff --git a/rowers/views/statements.py b/rowers/views/statements.py index c148438a..28319cb6 100644 --- a/rowers/views/statements.py +++ b/rowers/views/statements.py @@ -103,9 +103,9 @@ from rowers.models import ( FavoriteForm,BaseFavoriteFormSet,SiteAnnouncement,BasePlannedSessionFormSet, get_course_timezone ) -from rowers.metrics import rowingmetrics,defaultfavoritecharts -from rowers import metrics -from rowers import courses +from rowers.metrics import rowingmetrics,defaultfavoritecharts,nometrics +from rowers import metrics as metrics +from rowers import courses as courses import rowers.uploads as uploads from django.forms.formsets import formset_factory from django.forms import modelformset_factory @@ -116,22 +116,22 @@ from time import strftime,strptime,mktime,time,daylight import os,sys import datetime import iso8601 -import c2stuff -from c2stuff import c2_open -from runkeeperstuff import runkeeper_open -from sporttracksstuff import sporttracks_open -from tpstuff import tp_open +import rowers.c2stuff as c2stuff +from rowers.c2stuff import c2_open +from rowers.runkeeperstuff import runkeeper_open +from rowers.sporttracksstuff import sporttracks_open +from rowers.tpstuff import tp_open from iso8601 import ParseError -import stravastuff -from stravastuff import strava_open -import polarstuff -import sporttracksstuff -import underarmourstuff -from underarmourstuff import underarmour_open -import tpstuff -import runkeeperstuff -import ownapistuff -from ownapistuff import TEST_CLIENT_ID, TEST_CLIENT_SECRET, TEST_REDIRECT_URI +import rowers.stravastuff as stravastuff +from rowers.stravastuff import strava_open +import rowers.polarstuff as polarstuff +import rowers.sporttracksstuff as sporttracksstuff +import rowers.underarmourstuff as underarmourstuff +from rowers.underarmourstuff import underarmour_open +import rowers.tpstuff as tpstuff +import rowers.runkeeperstuff as runkeeperstuff +import rowers.ownapistuff as ownapistuff +from rowers.ownapistuff import TEST_CLIENT_ID, TEST_CLIENT_SECRET, TEST_REDIRECT_URI from rowsandall_app.settings import ( C2_CLIENT_ID, C2_REDIRECT_URI, C2_CLIENT_SECRET, STRAVA_CLIENT_ID, STRAVA_REDIRECT_URI, STRAVA_CLIENT_SECRET, @@ -181,7 +181,7 @@ from scipy.signal import savgol_filter from django.shortcuts import render_to_response from Cookie import SimpleCookie from shutil import copyfile,move -import mytypes +import rowers.mytypes as mytypes from rowingdata import rower as rrower from rowingdata import main as rmain from rowingdata import rowingdata as rrdata @@ -200,7 +200,7 @@ import mpld3 from mpld3 import plugins import stravalib from stravalib.exc import ActivityUploadFailed,TimeoutExceeded -from weather import get_wind_data,get_airport_code,get_metar_data +from rowers.weather import get_wind_data,get_airport_code,get_metar_data from oauth2_provider.models import Application,Grant,AccessToken @@ -228,6 +228,33 @@ class JSONResponse(HttpResponse): kwargs['content_type'] = 'application/json' super(JSONResponse, self).__init__(content, **kwargs) +def getfavorites(r,row): + workouttype = 'ote' + if row.workouttype in mytypes.otwtypes: + workouttype = 'otw' + + matchworkouttypes = [workouttype,'all'] + + workoutsource = row.workoutsource + if 'speedcoach2' in row.workoutsource: + workoutsource = 'speedcoach2' + + try: + favorites = FavoriteChart.objects.filter(user=r, + workouttype__in=matchworkouttypes).order_by("id") + favorites2 = FavoriteChart.objects.filter(user=r, + workouttype__in=[workoutsource]).order_by("id") + + favorites = favorites | favorites2 + + + maxfav = len(favorites)-1 + except: + favorites = None + maxfav = 0 + + return favorites,maxfav + def getrequestrower(request,rowerid=0,userid=0,notpermanent=False): @@ -365,23 +392,23 @@ from rowers.serializers import RowerSerializer,WorkoutSerializer from rest_framework import status,permissions,generics from rest_framework.decorators import api_view, renderer_classes -from permissions import IsOwnerOrNot +from rowers.permissions import IsOwnerOrNot -import plots -import mailprocessing +import rowers.plots as plots +import rowers.mailprocessing as mailprocessing from io import BytesIO from scipy.special import lambertw -from dataprep import timedeltaconv -from dataprep import getsmallrowdata_db +from rowers.dataprep import timedeltaconv +from rowers.dataprep import getsmallrowdata_db from scipy.interpolate import griddata #LOCALTIMEZONE = tz('Etc/UTC') USER_LANGUAGE = 'en-US' -from interactiveplots import * +from rowers.interactiveplots import * from rowers.celery import result as celery_result # Define the API documentation @@ -852,14 +879,14 @@ def getidfromuri(uri): -from utils import ( +from rowers.utils import ( geo_distance,serialize_list,deserialize_list,uniqify, str2bool,range_to_color_hex,absolute,myqueue,get_call, calculate_age,rankingdistances,rankingdurations, is_ranking_piece,my_dict_from_instance,wavg,NoTokenError ) -import datautils +import rowers.datautils as datautils from rowers.models import checkworkoutuser,checkaccessuser @@ -1142,7 +1169,7 @@ def trydf(df,aantal,column): return s -import teams +import rowers.teams as teams from rowers.models import C2WorldClassAgePerformance diff --git a/rowers/views/teamviews.py b/rowers/views/teamviews.py index 563a0bea..06e8e894 100644 --- a/rowers/views/teamviews.py +++ b/rowers/views/teamviews.py @@ -119,7 +119,7 @@ def rower_calcdps_view(request): messages.info(request,"Your workouts are being updated in the background. You will receive email when this is done.") - url = reverse(workouts_view) + url = reverse('workouts_view') return HttpResponseRedirect(url) @login_required() diff --git a/rowers/views/workoutviews.py b/rowers/views/workoutviews.py index e27deb08..e97af583 100644 --- a/rowers/views/workoutviews.py +++ b/rowers/views/workoutviews.py @@ -514,9 +514,9 @@ def workout_update_cp_view(request,id=0): dataprep.runcpupdate(r) if row.workouttype in mytypes.otwtypes: - url = reverse(otwrankings_view) + url = reverse('otwrankings_view') else: - url = reverse(oterankings_view) + url = reverse('oterankings_view') return HttpResponseRedirect(url) @@ -944,7 +944,7 @@ def team_comparison_select(request, 'name': firstworkout.name }, { - 'url':reverse(team_comparison_select,kwargs={'id':id,'teamid':teamid}), + 'url':reverse('team_comparison_select',kwargs={'id':id,'teamid':teamid}), 'name':'Compare Select' }, ] @@ -955,7 +955,7 @@ def team_comparison_select(request, 'name':'Workouts' }, { - 'url':reverse(team_comparison_select,kwargs={'teamid':teamid}), + 'url':reverse('team_comparison_select',kwargs={'teamid':teamid}), 'name': 'Compare Select' }, @@ -2428,7 +2428,6 @@ def instroke_view(request,id=0): return HttpResponseRedirect(url) - from metrics import nometrics rowdata = rrdata(csvfile=w.csvfilename) try: instrokemetrics = rowdata.get_instroke_columns() @@ -2880,33 +2879,6 @@ def workout_workflow_config2_view(request,userid=0): }) -def getfavorites(r,row): - workouttype = 'ote' - if row.workouttype in mytypes.otwtypes: - workouttype = 'otw' - - matchworkouttypes = [workouttype,'all'] - - workoutsource = row.workoutsource - if 'speedcoach2' in row.workoutsource: - workoutsource = 'speedcoach2' - - try: - favorites = FavoriteChart.objects.filter(user=r, - workouttype__in=matchworkouttypes).order_by("id") - favorites2 = FavoriteChart.objects.filter(user=r, - workouttype__in=[workoutsource]).order_by("id") - - favorites = favorites | favorites2 - - - maxfav = len(favorites)-1 - except: - favorites = None - maxfav = 0 - - return favorites,maxfav - # Workflow View @login_required() From d13f5be6f7a93ef844401acc7ba1365a5a97a9ff Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Thu, 7 Feb 2019 21:23:30 +0100 Subject: [PATCH 11/16] plannedsessions ok - need more on cycles --- 2a1bfe9f-acd8-4fbc-9b75-8caf203e4fcf.gpx | 574 +++++++++++++++++++++++ rowers/tests/test_plans.py | 59 ++- rowers/tests/testdata/testdata.csv.gz | Bin 11426 -> 11426 bytes rowers/views/planviews.py | 2 + 4 files changed, 625 insertions(+), 10 deletions(-) create mode 100644 2a1bfe9f-acd8-4fbc-9b75-8caf203e4fcf.gpx diff --git a/2a1bfe9f-acd8-4fbc-9b75-8caf203e4fcf.gpx b/2a1bfe9f-acd8-4fbc-9b75-8caf203e4fcf.gpx new file mode 100644 index 00000000..462f2508 --- /dev/null +++ b/2a1bfe9f-acd8-4fbc-9b75-8caf203e4fcf.gpx @@ -0,0 +1,574 @@ +Garmin InternationalExport by rowingdata + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/rowers/tests/test_plans.py b/rowers/tests/test_plans.py index 52006a29..6b818c3d 100644 --- a/rowers/tests/test_plans.py +++ b/rowers/tests/test_plans.py @@ -1349,11 +1349,15 @@ class PlannedSessionsView(TestCase): response = self.c.get(url) self.assertEqual(response.status_code,200) + s = self.w1.startdatetime.date().strftime("%Y-%m-%d") + e = (self.w1.startdatetime+datetime.timedelta(days=5)).date().strftime("%Y-%m-%d") + p = self.w1.startdatetime.date().strftime("%Y-%m-%d") + form_data = { - 'team':[1], - 'startdate': self.w1.startdatetime.date(), - 'enddate': (self.w1.startdatetime+datetime.timedelta(days=5)).date(), - 'preferreddate': self.w1.startdatetime.date(), + 'team':['1'], + 'startdate': s, + 'enddate': e, + 'preferreddate': p, 'name': faker.word(), 'sessiontype': 'session', 'sessionmode': 'distance', @@ -1362,9 +1366,28 @@ class PlannedSessionsView(TestCase): 'sessionunit': 'm', 'course': '', 'comment':faker.text(), - 'members': [self.r.id,self.r2.id] + 'members': ['{id1}'.format(id1=self.r.id)], + 'initial-startdate':s, + 'initial-enddate':e, + 'initial-preferreddate':p + } + + form = PlannedSessionForm(form_data,instance=self.ps_trimp) + if not form.is_valid(): + print form.errors + self.assertTrue(form.is_valid()) + + form = PlannedSessionTeamForm(self.u,form_data) + if not form.is_valid(): + print form.errors + self.assertTrue(form.is_valid()) + + form = PlannedSessionTeamMemberForm(self.ps_trimp,form_data) + if not form.is_valid(): + print form.errors + self.assertTrue(form.is_valid()) response = self.c.post(url,follow=True) self.assertEqual(response.status_code,200) @@ -1373,9 +1396,19 @@ class PlannedSessionsView(TestCase): login = self.c.login(username=self.u.username, password=self.password) self.assertTrue(login) + d1 = (self.ps_trimp.startdate-datetime.timedelta(days=1)).strftime( + "%Y-%m-%d") + d2 = (self.ps_trimp.enddate+datetime.timedelta(days=1)).strftime( + "%Y-%m-%d") + + sps = plannedsessions.get_sessions_manager(self.u,teamid=0, + enddate=d2,startdate=d1) + + self.assertTrue(len(sps)>0) + url = '/rowers/sessions/coach/?when={d1}/{d2}'.format( - d1=self.ps_trimp.startdate.strftime("%Y-%m%d"), - d2=self.ps_trimp.enddate.strftime("%Y-%m%d") + d1=d1, + d2=d2, ) response = self.c.get(url) @@ -1431,9 +1464,9 @@ class PlannedSessionsView(TestCase): self.assertEqual(response.status_code,200) form_data = { - 'startdate': self.w1.startdatetime.date(), - 'enddate': (self.w1.startdatetime+datetime.timedelta(days=5)).date(), - 'preferreddate': self.w1.startdatetime.date(), + 'startdate': self.w1.startdatetime.date().strftime("%Y-%m-%d"), + 'enddate': (self.w1.startdatetime+datetime.timedelta(days=5)).date().strftime("%Y-%m-%d"), + 'preferreddate': self.w1.startdatetime.date().strftime("%Y-%m-%d"), 'name': faker.word(), 'sessiontype': 'session', 'sessionmode': 'distance', @@ -1444,6 +1477,12 @@ class PlannedSessionsView(TestCase): 'comment':faker.text(), } + form = PlannedSessionForm(form_data,instance=self.ps_time) + if not form.is_valid(): + print form.errors + + self.assertTrue(form.is_valid()) + response = self.c.post(url,follow=True) self.assertEqual(response.status_code,200) diff --git a/rowers/tests/testdata/testdata.csv.gz b/rowers/tests/testdata/testdata.csv.gz index 66b5f5a1abf5302dbe7ab2373273984278d51207..5112eb13dbcf91d0fc9543432ae2b25ac758d3ac 100644 GIT binary patch delta 15 WcmZ1!xhRrNzMF%?Ve&?{IXVC;Jp~B> delta 15 WcmZ1!xhRrNzMF$XA#Nkv9322CbOf{j diff --git a/rowers/views/planviews.py b/rowers/views/planviews.py index 8f0c26ee..32de7ce1 100644 --- a/rowers/views/planviews.py +++ b/rowers/views/planviews.py @@ -605,6 +605,8 @@ def plannedsession_teamedit_view(request, request.user,request.POST ) sessionrowerform = PlannedSessionTeamMemberForm(ps,request.POST) + + if sessioncreateform.is_valid(): cd = sessioncreateform.cleaned_data From 77aba205c59d4a06cf66a1e312829ea1ffde4fec Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Fri, 8 Feb 2019 10:12:09 +0100 Subject: [PATCH 12/16] force curve with workstrokesonly --- rowers/tests/statements.py | 2 + rowers/tests/test_aworkouts.py | 52 ++++ rowers/tests/testdata/onwater.csv | 98 +++++++ rowers/tests/testdata/testdata.csv.gz | Bin 11426 -> 11457 bytes rowers/views/analysisviews.py | 171 ++++++++++++ rowers/views/statements.py | 9 + rowers/views/workoutviews.py | 361 +++++++------------------- 7 files changed, 424 insertions(+), 269 deletions(-) create mode 100644 rowers/tests/test_aworkouts.py create mode 100644 rowers/tests/testdata/onwater.csv diff --git a/rowers/tests/statements.py b/rowers/tests/statements.py index 51f1f25a..0b9aaf47 100644 --- a/rowers/tests/statements.py +++ b/rowers/tests/statements.py @@ -37,6 +37,8 @@ from minimocktest import MockTestCase import pandas as pd import rowers.c2stuff as c2stuff +from django.core.urlresolvers import reverse, reverse_lazy + import json import numpy as np diff --git a/rowers/tests/test_aworkouts.py b/rowers/tests/test_aworkouts.py new file mode 100644 index 00000000..cd936992 --- /dev/null +++ b/rowers/tests/test_aworkouts.py @@ -0,0 +1,52 @@ +from statements import * + +nu = datetime.datetime.now() + +class WaterWorkoutViewTest(TestCase): + def setUp(self): + self.u = UserFactory() + + self.r = Rower.objects.create(user=self.u, + birthdate=faker.profile()['birthdate'], + gdproptin=True, + gdproptindate=timezone.now(), + rowerplan='coach') + + 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() + + result = get_random_file(filename='rowers/tests/testdata/onwater.csv') + + self.wwater = WorkoutFactory(user=self.r, + csvfilename=result['filename'], + starttime=result['starttime'], + startdatetime=result['startdatetime'], + duration=result['duration'], + distance=result['totaldist'] + ) + + def tearDown(self): + pass + + @patch('rowers.dataprep.create_engine') + @patch('rowers.dataprep.getsmallrowdata_db') + def test_forcecurve(self, mocked_sqlalchemy, mocked_getsmallrowdata_db): + login = self.c.login(username=self.u.username, password=self.password) + self.assertTrue(login) + + url = reverse('workout_forcecurve_view',kwargs={'id':self.wwater.id}) + + response = self.c.get(url) + self.assertEqual(response.status_code,200) + + form_data = { + 'workstrokesonly': True + } + + response = self.c.post(url,form_data) + self.assertEqual(response.status_code,200) + diff --git a/rowers/tests/testdata/onwater.csv b/rowers/tests/testdata/onwater.csv new file mode 100644 index 00000000..1a07204c --- /dev/null +++ b/rowers/tests/testdata/onwater.csv @@ -0,0 +1,98 @@ +,index, lapIdx,TimeStamp (sec), Horizontal (meters),GPS Split,GPS Speed, Cadence (stokes/min), HRCur (bpm),Stroke Count,cum_dist, Stroke500mPace (sec/500m), ElapsedTime (sec), Power (watts), DriveLength (meters), StrokeDistance (meters), DriveTime (ms), DragFactor, StrokeRecoveryTime (ms), AverageDriveForce (lbs), AverageBoatSpeed (m/s), PeakDriveForce (lbs), AverageDriveForce (N), PeakDriveForce (N), WorkoutState, Stroke Number,originalvelo,hr_ut2,hr_ut1,hr_at,hr_tr,hr_an,hr_max,lim_ut2,lim_ut1,lim_at,lim_tr,lim_an,lim_max,pw_ut2,pw_ut1,pw_at,pw_tr,pw_an,pw_max,limpw_ut2,limpw_ut1,limpw_at,limpw_tr,limpw_an +0,0,0.0,1469705701.0,3.2,0.0,0.74,35.5,112,1,3.2,695.931477516,0.0,0,0,0,0,0,0,0,0.74,0,0.0,0.0,4,0,0.74,0.0,0.0,0.0,0.0,0.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +1,1,0.0,1469705702.6,8.6,0.0,2.07,38.5,113,2,8.6,228.8,1.59999990463,0,0,0,0,0,0,0,2.07,0,0.0,0.0,4,1,2.07,113.0,0.0,0.0,0.0,0.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +2,2,0.0,1469705704.5,15.1,0.0,3.36,38.5,115,3,15.1,153.336955279,3.5,0,0,0,0,0,0,0,3.36,0,0.0,0.0,4,3,3.35999999999,115.0,0.0,0.0,0.0,0.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +3,3,0.0,1469705705.6,21.4,0.0,4.09,39.0,115,4,21.4,124.977058407,4.59999990463,0,0,0,0,0,0,0,4.09,0,0.0,0.0,4,3,4.09,115.0,0.0,0.0,0.0,0.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +4,4,0.0,1469705707.5,28.8,0.0,4.43,38.5,118,5,28.8,112.083019815,6.5,0,0,0,0,0,0,0,4.43,0,0.0,0.0,4,5,4.42999999999,118.0,0.0,0.0,0.0,0.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +5,5,0.0,1469705708.8,36.2,0.0,4.64,37.5,125,6,36.2,106.442632632,7.79999995232,0,0,0,0,0,0,0,4.64,0,0.0,0.0,4,5,4.63999999999,125.0,0.0,0.0,0.0,0.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +6,6,0.0,1469705710.5,43.1,0.0,4.66,36.0,130,7,43.1,104.628021775,9.5,0,0,0,0,0,0,0,4.66,0,0.0,0.0,4,6,4.66,130.0,0.0,0.0,0.0,0.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +7,7,0.0,1469705712.2,52.0,0.0,4.76,34.0,135,8,52.0,106.01911804,11.2000000477,0,0,0,0,0,0,0,4.76,0,0.0,0.0,4,7,4.75999999999,135.0,0.0,0.0,0.0,0.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +8,8,0.0,1469705714.0,60.1,0.0,4.71,34.5,142,9,60.1,107.565165936,13.0,0,0,0,0,0,0,0,4.71,0,0.0,0.0,4,8,4.70999999998,142.0,0.0,0.0,0.0,0.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +9,9,0.0,1469705715.8,68.1,0.0,4.53,33.5,148,10,68.1,108.679681206,14.7999999523,0,0,0,0,0,0,0,4.53,0,0.0,0.0,4,9,4.53000000001,0.0,0.0,148.0,0.0,0.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +10,10,0.0,1469705717.6,76.3,0.0,4.44,32.5,154,11,76.3,109.466700689,16.5999999046,0,0,0,0,0,0,0,4.44,0,0.0,0.0,4,10,4.43999999998,0.0,0.0,154.0,0.0,0.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +11,11,0.0,1469705719.5,83.8,0.0,4.44,33.0,156,12,83.8,109.893487851,18.5,0,0,0,0,0,0,0,4.44,0,0.0,0.0,4,11,4.43999999998,0.0,0.0,156.0,0.0,0.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +12,12,0.0,1469705721.2,92.5,0.0,4.62,33.5,159,13,92.5,110.627456239,20.2000000477,0,0,0,0,0,0,0,4.62,0,0.0,0.0,4,12,4.62,0.0,0.0,159.0,0.0,0.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +13,13,0.0,1469705723.0,100.8,0.0,4.64,33.5,163,14,100.8,110.362782274,22.0,0,0,0,0,0,0,0,4.64,0,0.0,0.0,4,13,4.63999999999,0.0,0.0,0.0,163.0,0.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +14,14,0.0,1469705724.8,109.1,0.0,4.59,32.0,166,15,109.1,109.351182981,23.7999999523,0,0,0,0,0,0,0,4.59,0,0.0,0.0,4,14,4.58999999998,0.0,0.0,0.0,166.0,0.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +15,15,0.0,1469705726.8,118.1,0.0,4.52,32.5,168,16,118.1,108.636197885,25.7999999523,0,0,0,0,0,0,0,4.52,0,0.0,0.0,4,15,4.51999999998,0.0,0.0,0.0,0.0,168.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +16,16,0.0,1469705728.5,125.1,0.0,4.5,33.0,169,17,125.1,108.494979894,27.5,0,0,0,0,0,0,0,4.5,0,0.0,0.0,4,16,4.5,0.0,0.0,0.0,0.0,169.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +17,17,0.0,1469705730.4,134.7,0.0,4.66,33.5,171,18,134.7,109.232014911,29.4000000954,0,0,0,0,0,0,0,4.66,0,0.0,0.0,4,17,4.66,0.0,0.0,0.0,0.0,171.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +18,18,0.0,1469705732.2,143.0,0.0,4.66,32.5,172,19,143.0,109.4493854,31.2000000477,0,0,0,0,0,0,0,4.66,0,0.0,0.0,4,18,4.66,0.0,0.0,0.0,0.0,172.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +19,19,0.0,1469705734.0,151.2,0.0,4.58,33.0,172,20,151.2,109.222559423,33.0,0,0,0,0,0,0,0,4.58,0,0.0,0.0,4,19,4.57999999999,0.0,0.0,0.0,0.0,172.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +20,20,0.0,1469705735.8,159.4,0.0,4.52,33.0,173,21,159.4,109.097567302,34.7999999523,0,0,0,0,0,0,0,4.52,0,0.0,0.0,4,20,4.51999999998,0.0,0.0,0.0,0.0,173.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +21,21,0.0,1469705737.5,166.1,0.0,4.47,33.5,175,22,166.1,109.622785185,36.5,0,0,0,0,0,0,0,4.47,0,0.0,0.0,4,21,4.47000000001,0.0,0.0,0.0,0.0,175.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +22,22,0.0,1469705739.4,175.7,0.0,4.58,33.5,175,23,175.7,110.908884086,38.4000000954,0,0,0,0,0,0,0,4.58,0,0.0,0.0,4,22,4.57999999999,0.0,0.0,0.0,0.0,175.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +23,23,0.0,1469705741.2,183.8,0.0,4.57,32.5,175,24,183.8,111.571150665,40.2000000477,0,0,0,0,0,0,0,4.57,0,0.0,0.0,4,23,4.57,0.0,0.0,0.0,0.0,175.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +24,24,0.0,1469705743.0,191.8,0.0,4.47,33.0,175,25,191.8,111.781044645,42.0,0,0,0,0,0,0,0,4.47,0,0.0,0.0,4,24,4.47000000001,0.0,0.0,0.0,0.0,175.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +25,25,0.0,1469705744.8,199.8,0.0,4.39,32.0,176,26,199.8,111.973606594,43.7999999523,0,0,0,0,0,0,0,4.39,0,0.0,0.0,4,25,4.39,0.0,0.0,0.0,0.0,176.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +26,26,0.0,1469705746.7,207.8,0.0,4.39,32.5,176,27,207.8,112.082434148,45.7000000477,0,0,0,0,0,0,0,4.39,0,0.0,0.0,4,26,4.39,0.0,0.0,0.0,0.0,176.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +27,27,0.0,1469705748.6,216.7,0.0,4.49,33.0,174,28,216.7,112.462053887,47.5999999046,0,0,0,0,0,0,0,4.49,0,0.0,0.0,4,27,4.49000000001,0.0,0.0,0.0,0.0,174.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +28,28,0.0,1469705750.4,224.8,0.0,4.49,32.0,174,29,224.8,110.702820986,49.4000000954,0,0,0,0,0,0,0,4.49,0,0.0,0.0,4,28,4.49000000001,0.0,0.0,0.0,0.0,174.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +29,29,0.0,1469705752.5,232.9,0.0,4.47,33.0,174,30,232.9,111.287510895,51.5,0,0,0,0,0,0,0,4.47,0,0.0,0.0,4,30,4.47000000001,0.0,0.0,0.0,0.0,174.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +30,30,0.0,1469705754.0,240.9,0.0,4.48,32.0,175,31,240.9,113.880099386,53.0,0,0,0,0,0,0,0,4.48,0,0.0,0.0,4,30,4.48000000001,0.0,0.0,0.0,0.0,175.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +31,31,0.0,1469705755.8,248.9,0.0,4.43,32.0,175,32,248.9,117.397229535,54.7999999523,0,0,0,0,0,0,0,4.43,0,0.0,0.0,4,31,4.42999999999,0.0,0.0,0.0,0.0,175.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +32,32,0.0,1469705756.2,250.4,0.0,4.43,32.0,175,32,250.4,120.181532945,55.2000000477,0,0,0,0,0,0,0,4.43,0,0.0,0.0,4,32,4.42999999999,0.0,0.0,0.0,0.0,175.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +33,33,1.0,1469705757.6,5.7,0.0,3.62,30.5,160,1,256.1,121.464367621,56.5999999046,0,0,0,0,0,0,0,3.62,0,0.0,0.0,4,32,3.62000000001,0.0,0.0,160.0,0.0,0.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +34,34,1.0,1469705759.4,13.4,0.0,3.85,33.5,161,2,263.8,121.497391629,58.4000000954,0,0,0,0,0,0,0,3.85,0,0.0,0.0,4,33,3.85,0.0,0.0,0.0,161.0,0.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +35,35,1.0,1469705761.2,20.7,0.0,4.2,33.5,162,3,271.1,120.37982782,60.2000000477,0,0,0,0,0,0,0,4.2,0,0.0,0.0,4,34,4.19999999999,0.0,0.0,0.0,162.0,0.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +36,36,1.0,1469705763.0,29.2,0.0,4.47,33.5,162,4,279.6,118.387283716,62.0,0,0,0,0,0,0,0,4.47,0,0.0,0.0,4,35,4.47000000001,0.0,0.0,0.0,162.0,0.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +37,37,1.0,1469705764.8,37.3,0.0,4.47,33.0,164,5,287.7,115.233986774,63.7999999523,0,0,0,0,0,0,0,4.47,0,0.0,0.0,4,36,4.47000000001,0.0,0.0,0.0,164.0,0.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +38,38,1.0,1469705766.6,45.3,0.0,4.44,33.5,165,6,295.7,111.287510895,65.5999999046,0,0,0,0,0,0,0,4.44,0,0.0,0.0,4,37,4.43999999998,0.0,0.0,0.0,165.0,0.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +39,39,1.0,1469705768.5,53.5,0.0,4.42,33.5,165,7,303.9,111.718168135,67.5,0,0,0,0,0,0,0,4.42,0,0.0,0.0,4,38,4.41999999999,0.0,0.0,0.0,165.0,0.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +40,40,1.0,1469705770.2,60.7,0.0,4.38,32.0,167,8,311.1,113.471050335,69.2000000477,0,0,0,0,0,0,0,4.38,0,0.0,0.0,4,39,4.37999999998,0.0,0.0,0.0,167.0,0.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +41,41,1.0,1469705772.2,70.1,0.0,4.44,32.0,171,9,320.5,115.140584238,71.2000000477,0,0,0,0,0,0,0,4.44,0,0.0,0.0,4,40,4.43999999998,0.0,0.0,0.0,0.0,171.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +42,42,1.0,1469705774.1,77.8,0.0,4.3,32.0,172,10,328.2,116.273396972,73.0999999046,0,0,0,0,0,0,0,4.3,0,0.0,0.0,4,41,4.30000000002,0.0,0.0,0.0,0.0,172.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +43,43,1.0,1469705776.1,85.2,0.0,4.12,31.0,174,11,335.6,117.556791714,75.0999999046,0,0,0,0,0,0,0,4.12,0,0.0,0.0,4,42,4.12,0.0,0.0,0.0,0.0,174.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +44,44,1.0,1469705777.9,93.4,0.0,4.2,31.5,174,12,343.8,119.395508057,76.9000000954,0,0,0,0,0,0,0,4.2,0,0.0,0.0,4,43,4.19999999999,0.0,0.0,0.0,0.0,174.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +45,45,1.0,1469705779.9,101.5,0.0,4.21,32.0,176,13,351.9,121.165219651,78.9000000954,0,0,0,0,0,0,0,4.21,0,0.0,0.0,4,44,4.20999999999,0.0,0.0,0.0,0.0,176.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +46,46,1.0,1469705781.6,108.9,0.0,4.11,30.5,176,14,359.3,122.757318225,80.5999999046,0,0,0,0,0,0,0,4.11,0,0.0,0.0,4,45,4.10999999998,0.0,0.0,0.0,0.0,176.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +47,47,1.0,1469705783.6,116.9,0.0,4.02,32.0,175,15,367.3,123.722400388,82.5999999046,0,0,0,0,0,0,0,4.02,0,0.0,0.0,4,46,4.01999999999,0.0,0.0,0.0,0.0,175.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +48,48,1.0,1469705785.5,124.1,0.0,3.96,30.5,177,16,374.5,123.80237793,84.5,0,0,0,0,0,0,0,3.96,0,0.0,0.0,4,47,3.95999999999,0.0,0.0,0.0,0.0,177.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +49,49,1.0,1469705787.5,132.1,0.0,4.04,32.0,177,17,382.5,123.700282002,86.5,0,0,0,0,0,0,0,4.04,0,0.0,0.0,4,48,4.03999999999,0.0,0.0,0.0,0.0,177.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +50,50,1.0,1469705789.2,139.7,0.0,4.06,32.0,178,18,390.1,122.672370408,88.2000000477,0,0,0,0,0,0,0,4.06,0,0.0,0.0,4,49,4.05999999999,0.0,0.0,0.0,0.0,178.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +51,51,1.0,1469705791.1,146.7,0.0,4.09,33.0,178,19,397.1,121.011418513,90.0999999046,0,0,0,0,0,0,0,4.09,0,0.0,0.0,4,50,4.09,0.0,0.0,0.0,0.0,178.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +52,52,1.0,1469705793.1,155.4,0.0,4.27,31.0,177,20,405.8,119.62256154,92.0999999046,0,0,0,0,0,0,0,4.27,0,0.0,0.0,4,51,4.27000000001,0.0,0.0,0.0,0.0,177.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +53,53,1.0,1469705794.9,163.1,0.0,4.27,32.0,177,21,413.5,118.342215896,93.9000000954,0,0,0,0,0,0,0,4.27,0,0.0,0.0,4,52,4.27000000001,0.0,0.0,0.0,0.0,177.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +54,54,1.0,1469705797.1,171.6,0.0,4.24,31.5,178,22,422.0,117.740696015,96.0999999046,0,0,0,0,0,0,0,4.24,0,0.0,0.0,4,53,4.24,0.0,0.0,0.0,0.0,178.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +55,55,1.0,1469705798.6,179.3,0.0,4.22,32.0,179,23,429.7,117.599973684,97.5999999046,0,0,0,0,0,0,0,4.22,0,0.0,0.0,4,54,4.22000000001,0.0,0.0,0.0,0.0,179.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +56,56,1.0,1469705800.7,187.9,0.0,4.21,30.5,179,24,438.3,117.565167825,99.7000000477,0,0,0,0,0,0,0,4.21,0,0.0,0.0,4,55,4.20999999999,0.0,0.0,0.0,0.0,179.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +57,57,1.0,1469705802.5,195.8,0.0,4.28,30.5,180,25,446.2,117.967332124,101.5,0,0,0,0,0,0,0,4.28,0,0.0,0.0,4,56,4.27999999998,0.0,0.0,0.0,0.0,180.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +58,58,1.0,1469705804.5,204.3,0.0,4.22,30.5,180,26,454.7,118.489957355,103.5,0,0,0,0,0,0,0,4.22,0,0.0,0.0,4,57,4.22000000001,0.0,0.0,0.0,0.0,180.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +59,59,1.0,1469705806.4,212.2,0.0,4.2,30.5,180,27,462.6,118.763530461,105.400000095,0,0,0,0,0,0,0,4.2,0,0.0,0.0,4,58,4.19999999999,0.0,0.0,0.0,0.0,180.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +60,60,1.0,1469705808.5,221.4,0.0,4.26,29.5,180,28,471.8,117.583212735,107.5,0,0,0,0,0,0,0,4.26,0,0.0,0.0,4,59,4.25999999999,0.0,0.0,0.0,0.0,180.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +61,61,1.0,1469705810.4,229.8,0.0,4.17,30.5,180,29,480.2,119.140852815,109.400000095,0,0,0,0,0,0,0,4.17,0,0.0,0.0,4,60,4.16999999999,0.0,0.0,0.0,0.0,180.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +62,62,1.0,1469705812.3,236.6,0.0,4.11,33.5,181,30,487.0,121.480877381,111.299999952,0,0,0,0,0,0,0,4.11,0,0.0,0.0,4,61,4.10999999998,0.0,0.0,0.0,0.0,0.0,181.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +63,63,1.0,1469705814.0,244.8,0.0,4.24,33.5,181,31,495.2,122.38470448,113.0,0,0,0,0,0,0,0,4.24,0,0.0,0.0,4,62,4.24,0.0,0.0,0.0,0.0,0.0,181.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +64,64,1.0,1469705815.4,250.9,0.0,4.24,33.5,181,31,501.3,122.183234979,114.400000095,0,0,0,0,0,0,0,4.24,0,0.0,0.0,4,63,4.24,0.0,0.0,0.0,0.0,0.0,181.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +65,65,2.0,1469705816.3,0.9,0.0,3.66,28.0,158,1,502.2,121.2563172,115.299999952,0,0,0,0,0,0,0,3.66,0,0.0,0.0,4,63,3.66,0.0,0.0,158.0,0.0,0.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +66,66,2.0,1469705817.8,9.8,0.0,4.12,33.5,160,2,511.1,119.786227655,116.799999952,0,0,0,0,0,0,0,4.12,0,0.0,0.0,4,64,4.12,0.0,0.0,160.0,0.0,0.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +67,67,2.0,1469705819.6,18.0,0.0,4.44,32.5,162,3,519.3,117.678039467,118.599999905,0,0,0,0,0,0,0,4.44,0,0.0,0.0,4,65,4.43999999998,0.0,0.0,0.0,162.0,0.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +68,68,2.0,1469705821.5,26.1,0.0,4.47,33.0,161,4,527.4,115.319480659,120.5,0,0,0,0,0,0,0,4.47,0,0.0,0.0,4,66,4.47000000001,0.0,0.0,0.0,161.0,0.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +69,69,2.0,1469705823.4,35.0,0.0,4.43,32.0,164,5,536.3,112.531083761,122.400000095,0,0,0,0,0,0,0,4.43,0,0.0,0.0,4,67,4.42999999999,0.0,0.0,0.0,164.0,0.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +70,70,2.0,1469705825.1,41.9,0.0,4.43,33.0,165,6,543.2,109.693981917,124.099999905,0,0,0,0,0,0,0,4.43,0,0.0,0.0,4,68,4.42999999999,0.0,0.0,0.0,165.0,0.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +71,71,2.0,1469705827.1,51.5,0.0,4.6,33.5,166,7,552.8,110.111805834,126.099999905,0,0,0,0,0,0,0,4.6,0,0.0,0.0,4,69,4.6,0.0,0.0,0.0,166.0,0.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +72,72,2.0,1469705828.9,59.7,0.0,4.59,33.5,168,8,561.0,110.235734879,127.900000095,0,0,0,0,0,0,0,4.59,0,0.0,0.0,4,70,4.58999999998,0.0,0.0,0.0,0.0,168.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +73,73,2.0,1469705830.7,68.1,0.0,4.53,32.0,171,9,569.4,109.674351541,129.700000048,0,0,0,0,0,0,0,4.53,0,0.0,0.0,4,71,4.53000000001,0.0,0.0,0.0,0.0,171.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +74,74,2.0,1469705832.4,76.4,0.0,4.55,33.0,172,10,577.7,109.081478016,131.400000095,0,0,0,0,0,0,0,4.55,0,0.0,0.0,4,72,4.55,0.0,0.0,0.0,0.0,172.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +75,75,2.0,1469705834.3,84.0,0.0,4.54,32.5,173,11,585.3,108.95736186,133.299999952,0,0,0,0,0,0,0,4.54,0,0.0,0.0,4,73,4.54000000001,0.0,0.0,0.0,0.0,173.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +76,76,2.0,1469705836.0,92.9,0.0,4.63,33.0,174,12,594.2,109.581343074,135.0,0,0,0,0,0,0,0,4.63,0,0.0,0.0,4,74,4.63000000001,0.0,0.0,0.0,0.0,174.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +77,77,2.0,1469705837.9,101.3,0.0,4.61,33.5,176,13,602.6,109.437100452,136.900000095,0,0,0,0,0,0,0,4.61,0,0.0,0.0,4,75,4.61000000001,0.0,0.0,0.0,0.0,176.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +78,78,2.0,1469705839.7,109.6,0.0,4.58,33.5,177,14,610.9,108.898173861,138.700000048,0,0,0,0,0,0,0,4.58,0,0.0,0.0,4,76,4.57999999999,0.0,0.0,0.0,0.0,177.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +79,79,2.0,1469705841.5,118.0,0.0,4.54,33.5,178,15,619.3,108.514739286,140.5,0,0,0,0,0,0,0,4.54,0,0.0,0.0,4,77,4.54000000001,0.0,0.0,0.0,0.0,178.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +80,80,2.0,1469705843.1,125.0,0.0,4.54,34.0,178,16,626.3,108.408891045,142.099999905,0,0,0,0,0,0,0,4.54,0,0.0,0.0,4,78,4.54000000001,0.0,0.0,0.0,0.0,178.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +81,81,2.0,1469705845.1,134.8,0.0,4.69,34.5,179,17,636.1,108.970092917,144.099999905,0,0,0,0,0,0,0,4.69,0,0.0,0.0,4,79,4.69000000002,0.0,0.0,0.0,0.0,179.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +82,82,2.0,1469705846.7,142.2,0.0,4.67,34.0,180,18,643.5,108.943527129,145.700000048,0,0,0,0,0,0,0,4.67,0,0.0,0.0,4,80,4.67000000001,0.0,0.0,0.0,0.0,180.0,0.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +83,83,2.0,1469705848.5,150.6,0.0,4.59,34.5,181,19,651.9,108.491687362,147.5,0,0,0,0,0,0,0,4.59,0,0.0,0.0,4,81,4.58999999998,0.0,0.0,0.0,0.0,0.0,181.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +84,84,2.0,1469705850.3,158.8,0.0,4.52,33.5,181,20,660.1,108.195629805,149.299999952,0,0,0,0,0,0,0,4.52,0,0.0,0.0,4,82,4.51999999998,0.0,0.0,0.0,0.0,0.0,181.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +85,85,2.0,1469705852.1,166.4,0.0,4.52,33.5,181,21,667.7,108.474130564,151.099999905,0,0,0,0,0,0,0,4.52,0,0.0,0.0,4,83,4.51999999998,0.0,0.0,0.0,0.0,0.0,181.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +86,86,2.0,1469705853.9,175.2,0.0,4.68,33.5,182,22,676.5,109.550561798,152.900000095,0,0,0,0,0,0,0,4.68,0,0.0,0.0,4,84,4.67999999998,0.0,0.0,0.0,0.0,0.0,182.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +87,87,2.0,1469705855.7,183.2,0.0,4.65,32.0,182,23,684.5,110.12141592,154.700000048,0,0,0,0,0,0,0,4.65,0,0.0,0.0,4,85,4.65000000002,0.0,0.0,0.0,0.0,0.0,182.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +88,88,2.0,1469705857.5,191.2,0.0,4.51,33.0,182,24,692.5,110.388908617,156.5,0,0,0,0,0,0,0,4.51,0,0.0,0.0,4,86,4.51,0.0,0.0,0.0,0.0,0.0,182.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +89,89,2.0,1469705859.4,200.1,0.0,4.42,32.0,183,25,701.4,111.075438088,158.400000095,0,0,0,0,0,0,0,4.42,0,0.0,0.0,4,87,4.41999999999,0.0,0.0,0.0,0.0,0.0,183.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +90,90,2.0,1469705861.2,207.4,0.0,4.38,32.0,183,26,708.7,112.186192469,160.200000048,0,0,0,0,0,0,0,4.38,0,0.0,0.0,4,88,4.37999999998,0.0,0.0,0.0,0.0,0.0,183.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +91,91,2.0,1469705863.0,216.1,0.0,4.47,32.0,183,27,717.4,113.541324808,162.0,0,0,0,0,0,0,0,4.47,0,0.0,0.0,4,89,4.47000000001,0.0,0.0,0.0,0.0,0.0,183.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +92,92,2.0,1469705865.0,224.7,0.0,4.44,32.5,183,28,726.0,113.933934253,164.0,0,0,0,0,0,0,0,4.44,0,0.0,0.0,4,90,4.43999999998,0.0,0.0,0.0,0.0,0.0,183.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +93,93,2.0,1469705867.1,232.4,0.0,4.34,33.0,184,29,733.7,114.217860585,166.099999905,0,0,0,0,0,0,0,4.34,0,0.0,0.0,4,91,4.34,0.0,0.0,0.0,0.0,0.0,184.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +94,94,2.0,1469705868.6,240.6,0.0,4.35,32.0,184,30,741.9,114.508095643,167.599999905,0,0,0,0,0,0,0,4.35,0,0.0,0.0,4,92,4.34999999999,0.0,0.0,0.0,0.0,0.0,184.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +95,95,2.0,1469705870.7,249.0,0.0,4.34,30.5,184,31,750.3,114.922206507,169.700000048,0,0,0,0,0,0,0,4.34,0,0.0,0.0,4,93,4.34,0.0,0.0,0.0,0.0,0.0,184.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 +96,96,2.0,1469705871.0,250.4,0.0,4.34,30.5,184,31,751.7,115.581707376,170.0,0,0,0,0,0,0,0,4.34,0,0.0,0.0,4,93,4.34,0.0,0.0,0.0,0.0,0.0,184.0,142,146,160,167,180,192,0.0,0.0,0.0,0.0,0.0,0.0,124.3,169.5,203.4,237.3,271.2 diff --git a/rowers/tests/testdata/testdata.csv.gz b/rowers/tests/testdata/testdata.csv.gz index 5112eb13dbcf91d0fc9543432ae2b25ac758d3ac..7dc55d87c477e548aa76296844e2bf2104abde41 100644 GIT binary patch literal 11457 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 bE%OG|dV_eM5SB%T-Ol@e0koFQfOh}@SgjW_ literal 11426 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≧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 w5V 1: + modality = 'all' + else: + modality = modalities[0] + else: + modalities = [m[0] for m in mytypes.workouttypes] + modality = 'all' + + + try: + rankingonly = options['rankingonly'] + except KeyError: + rankingonly = False + + try: + includereststrokes = options['includereststrokes'] + except KeyError: + includereststrokes = False + + + workstrokesonly = not includereststrokes + + waterboattype = mytypes.waterboattype + + + if startdatestring != "": + startdate = iso8601.parse_date(startdatestring) + + if enddatestring != "": + enddate = iso8601.parse_date(enddatestring) + + if enddate < startdate: + s = enddate + enddate = startdate + startdate = s + + + # get all indoor rows of in date range + + # process form + if request.method == 'POST': + form = DateRangeForm(request.POST) + modalityform = TrendFlexModalForm(request.POST) + if form.is_valid(): + startdate = form.cleaned_data['startdate'] + enddate = form.cleaned_data['enddate'] + if startdate > enddate: + s = enddate + enddate = startdate + startdate = s + startdatestring = startdate.strftime('%Y-%m-%d') + enddatestring = enddate.strftime('%Y-%m-%d') + if modalityform.is_valid(): + modality = modalityform.cleaned_data['modality'] + waterboattype = modalityform.cleaned_data['waterboattype'] + rankingonly = modalityform.cleaned_data['rankingonly'] + if modality == 'all': + modalities = [m[0] for m in mytypes.workouttypes] + else: + modalities = [modality] + + if modality != 'water': + waterboattype = [b[0] for b in mytypes.boattypes] + + + request.session['modalities'] = modalities + request.session['waterboattype'] = waterboattype + request.session['rankingonly'] = rankingonly + form = DateRangeForm(initial={ + 'startdate': startdate, + 'enddate': enddate, + }) + else: + form = DateRangeForm(initial={ + 'startdate': startdate, + 'enddate': enddate, + }) + includereststrokes = False + + workstrokesonly = not includereststrokes + modalityform = TrendFlexModalForm( + initial={ + 'modality':modality, + 'waterboattype':waterboattype, + 'rankingonly':rankingonly, + } + ) + + negtypes = [] + for b in mytypes.boattypes: + if b[0] not in waterboattype: + negtypes.append(b[0]) + + + + script = '' + div = get_call() + js_resources = '' + css_resources = '' + + + + + options = { + 'modality': modality, + 'theuser': theuser.id, + 'waterboattype':waterboattype, + 'startdatestring':startdatestring, + 'enddatestring':enddatestring, + 'rankingonly':rankingonly, + 'includereststrokes':includereststrokes, + } + + request.session['options'] = options + + promember=0 + mayedit=0 + if not request.user.is_anonymous(): + result = request.user.is_authenticated() and ispromember(request.user) + if result: + promember = 1 + + + request.session['options'] = options + + return render(request, 'histo.html', + {'interactiveplot':script, + 'the_div':div, + 'id':theuser, + 'active':'nav-analysis', + 'theuser':theuser, + 'rower':r, + 'startdate':startdate, + 'enddate':enddate, + 'form':form, + 'optionsform':modalityform, + 'teams':get_my_teams(request.user), + }) # The Flex plot for a large selection of workouts @login_required() diff --git a/rowers/views/statements.py b/rowers/views/statements.py index 28319cb6..6b3cddeb 100644 --- a/rowers/views/statements.py +++ b/rowers/views/statements.py @@ -255,6 +255,15 @@ def getfavorites(r,row): return favorites,maxfav +def get_workout_default_page(request,id): + if request.user.is_anonymous(): + return reverse('workout_view',kwargs={'id':str(id)}) + else: + r = Rower.objects.get(user=request.user) + if r.defaultlandingpage == 'workout_edit_view': + return reverse('workout_edit_view',kwargs={'id':str(id)}) + else: + return reverse('workout_workflow_view',kwargs={'id':str(id)}) def getrequestrower(request,rowerid=0,userid=0,notpermanent=False): diff --git a/rowers/views/workoutviews.py b/rowers/views/workoutviews.py index e97af583..8fcb3866 100644 --- a/rowers/views/workoutviews.py +++ b/rowers/views/workoutviews.py @@ -41,7 +41,7 @@ def workout_forcecurve_view(request,id=0,workstrokesonly=False): 'name': row.name }, { - 'url':reverse(workout_forcecurve_view,kwargs={'id':id}), + 'url':reverse('workout_forcecurve_view',kwargs={'id':id}), 'name': 'Empower Force Curve' } @@ -73,7 +73,7 @@ def workout_test_task_view(request,id=0): res = myqueue(queuehigh,addcomment2,request.user.id,row.id) - url = reverse(workout_edit_view, + url = reverse('workout_edit_view', kwargs = { 'id':int(id), }) @@ -111,7 +111,7 @@ def workout_histo_view(request,id=0): 'name': w.name }, { - 'url':reverse(workout_histo_view,kwargs={'id':id}), + 'url':reverse('workout_histo_view',kwargs={'id':id}), 'name': 'Histogram' } @@ -133,177 +133,6 @@ def workout_histo_view(request,id=0): -# Histogram for a date/time range -@user_passes_test(ispromember,login_url="/rowers/paidplans", - message="This functionality requires a Pro plan or higher", - redirect_field_name=None) -def histo(request,theuser=0, - startdate=timezone.now()-datetime.timedelta(days=365), - enddate=timezone.now(), - deltadays=-1, - enddatestring=timezone.now().strftime("%Y-%m-%d"), - startdatestring=(timezone.now()-datetime.timedelta(days=30)).strftime("%Y-%m-%d"), - options={ - 'includereststrokes':False, - 'workouttypes':[i[0] for i in mytypes.workouttypes], - 'waterboattype':mytypes.waterboattype, - 'rankingonly': False, - }): - - r = getrequestrower(request,userid=theuser) - theuser = r.user - - if 'waterboattype' in request.session: - waterboattype = request.session['waterboattype'] - else: - waterboattype = mytypes.waterboattype - - - if 'rankingonly' in request.session: - rankingonly = request.session['rankingonly'] - else: - rankingonly = False - - if 'modalities' in request.session: - modalities = request.session['modalities'] - if len(modalities) > 1: - modality = 'all' - else: - modality = modalities[0] - else: - modalities = [m[0] for m in mytypes.workouttypes] - modality = 'all' - - - try: - rankingonly = options['rankingonly'] - except KeyError: - rankingonly = False - - try: - includereststrokes = options['includereststrokes'] - except KeyError: - includereststrokes = False - - - workstrokesonly = not includereststrokes - - waterboattype = mytypes.waterboattype - - - if startdatestring != "": - startdate = iso8601.parse_date(startdatestring) - - if enddatestring != "": - enddate = iso8601.parse_date(enddatestring) - - if enddate < startdate: - s = enddate - enddate = startdate - startdate = s - - - # get all indoor rows of in date range - - # process form - if request.method == 'POST': - form = DateRangeForm(request.POST) - modalityform = TrendFlexModalForm(request.POST) - if form.is_valid(): - startdate = form.cleaned_data['startdate'] - enddate = form.cleaned_data['enddate'] - if startdate > enddate: - s = enddate - enddate = startdate - startdate = s - startdatestring = startdate.strftime('%Y-%m-%d') - enddatestring = enddate.strftime('%Y-%m-%d') - if modalityform.is_valid(): - modality = modalityform.cleaned_data['modality'] - waterboattype = modalityform.cleaned_data['waterboattype'] - rankingonly = modalityform.cleaned_data['rankingonly'] - if modality == 'all': - modalities = [m[0] for m in mytypes.workouttypes] - else: - modalities = [modality] - - if modality != 'water': - waterboattype = [b[0] for b in mytypes.boattypes] - - - request.session['modalities'] = modalities - request.session['waterboattype'] = waterboattype - request.session['rankingonly'] = rankingonly - form = DateRangeForm(initial={ - 'startdate': startdate, - 'enddate': enddate, - }) - else: - form = DateRangeForm(initial={ - 'startdate': startdate, - 'enddate': enddate, - }) - includereststrokes = False - - workstrokesonly = not includereststrokes - modalityform = TrendFlexModalForm( - initial={ - 'modality':modality, - 'waterboattype':waterboattype, - 'rankingonly':rankingonly, - } - ) - - negtypes = [] - for b in mytypes.boattypes: - if b[0] not in waterboattype: - negtypes.append(b[0]) - - - - script = '' - div = get_call() - js_resources = '' - css_resources = '' - - - - - options = { - 'modality': modality, - 'theuser': theuser.id, - 'waterboattype':waterboattype, - 'startdatestring':startdatestring, - 'enddatestring':enddatestring, - 'rankingonly':rankingonly, - 'includereststrokes':includereststrokes, - } - - request.session['options'] = options - - promember=0 - mayedit=0 - if not request.user.is_anonymous(): - result = request.user.is_authenticated() and ispromember(request.user) - if result: - promember = 1 - - - request.session['options'] = options - - return render(request, 'histo.html', - {'interactiveplot':script, - 'the_div':div, - 'id':theuser, - 'active':'nav-analysis', - 'theuser':theuser, - 'rower':r, - 'startdate':startdate, - 'enddate':enddate, - 'form':form, - 'optionsform':modalityform, - 'teams':get_my_teams(request.user), - }) # add a workout manually @login_required() @@ -316,7 +145,7 @@ def addmanual_view(request): 'name':'Workouts' }, { - 'url':reverse(addmanual_view), + 'url':reverse('addmanual_view'), 'name': 'Add Manual Entry' }, ] @@ -418,7 +247,7 @@ def addmanual_view(request): messages.info(request,'New workout created') url = reverse( - workout_edit_view, + 'workout_edit_view', kwargs={'id':id} ) return HttpResponseRedirect(url) @@ -457,7 +286,7 @@ def fitness_metric_view(request,mode='rower',days=42): # test if not something already done ms = PowerTimeFitnessMetric.objects.filter(user=request.user) if not ms: - url = reverse(workouts_view) + url = reverse('workouts_view') return HttpResponseRedirect(url) max_workout_id = max([m.last_workout for m in ms]) @@ -502,7 +331,7 @@ def workout_update_cp_view(request,id=0): if (checkworkoutuser(request.user,row)==False): message = "You are not allowed to edit this workout" messages.error(request,message) - url = reverse(workouts_view) + url = reverse('workouts_view') return HttpResponseRedirect(url) @@ -529,7 +358,7 @@ def workout_recalcsummary_view(request,id=0): if (checkworkoutuser(request.user,row)==False): message = "You are not allowed to edit this workout" messages.error(request,message) - url = reverse(workouts_view) + url = reverse('workouts_view') return HttpResponseRedirect(url) @@ -540,14 +369,14 @@ def workout_recalcsummary_view(request,id=0): row.save() successmessage = "Summary Updated" messages.info(request,successmessage) - url = reverse(workout_edit_view, + url = reverse('workout_edit_view', kwargs = { 'id':int(id), }) else: message = "Something went wrong. Could not update summary" messages.error(request,message) - url = reverse(workout_edit_view, + url = reverse('workout_edit_view', kwargs = { 'id':int(id), }) @@ -599,7 +428,7 @@ def workouts_join_view(request): return HttpResponse("form is not valid") else: - url = reverse(workouts_join_select) + url = reverse('workouts_join_select') return HttpResponseRedirect(url) @user_passes_test(ispromember,login_url="/rowers/paidplans", @@ -1082,11 +911,11 @@ def virtualevent_compare_view(request,id=0): breadcrumbs = [ { - 'url': reverse(virtualevents_view), + 'url': reverse('virtualevents_view'), 'name': 'Racing' }, { - 'url':reverse(virtualevent_view, + 'url':reverse('virtualevent_view', kwargs={ 'id':race.id, } @@ -1094,7 +923,7 @@ def virtualevent_compare_view(request,id=0): 'name': race.name }, { - 'url':reverse(virtualevent_compare_view, + 'url':reverse('virtualevent_compare_view', kwargs={ 'id':race.id, } @@ -1159,7 +988,7 @@ def plannedsession_compare_view(request,id=0,userid=0): request.session['plottype'] = plottype request.session['ps'] = ps.id - url = reverse(multi_compare_view,kwargs={'userid':userid,'id':ids[0]}) + url = reverse('multi_compare_view',kwargs={'userid':userid,'id':ids[0]}) return HttpResponseRedirect(url) @@ -1237,7 +1066,7 @@ def multi_compare_view(request,id=0,userid=0): ) else: - url = reverse(team_comparison_select, + url = reverse('team_comparison_select', kwargs={ 'id':id, 'teamid':0}) @@ -1262,11 +1091,11 @@ def multi_compare_view(request,id=0,userid=0): 'name':'Workouts' }, { - 'url':reverse(team_comparison_select,kwargs={'teamid':teamid}), + 'url':reverse('team_comparison_select',kwargs={'teamid':teamid}), 'name': 'Compare Select' }, { - 'url':reverse(multi_compare_view), + 'url':reverse('multi_compare_view'), 'name': 'Comparison Chart' } ] @@ -1275,12 +1104,12 @@ def multi_compare_view(request,id=0,userid=0): ps = PlannedSession.objects.get(id=int(request.session['ps'])) breadcrumbs = [ { - 'url': reverse(plannedsessions_view, + 'url': reverse('plannedsessions_view', kwargs={'userid':userid}), 'name': 'Sessions' }, { - 'url':reverse(plannedsession_view, + 'url':reverse('plannedsession_view', kwargs={ 'userid':userid, 'id':ps.id, @@ -1289,7 +1118,7 @@ def multi_compare_view(request,id=0,userid=0): 'name': ps.id }, { - 'url':reverse(plannedsession_compare_view, + 'url':reverse('plannedsession_compare_view', kwargs={ 'userid':userid, 'id':ps.id, @@ -1610,7 +1439,7 @@ def workout_fusion_list(request,id=0,message='',successmessage='', 'name': row.name }, { - 'url':reverse(workout_fusion_list,kwargs={'id':id}), + 'url':reverse('workout_fusion_list',kwargs={'id':id}), 'name': 'Sensor Fusion' } @@ -1703,7 +1532,7 @@ def workout_view(request,id=0): 'name':'Workouts' }, { - 'url':reverse(workout_view,kwargs={'id':id}), + 'url':reverse('workout_view',kwargs={'id':id}), 'name': row.name, } @@ -1745,7 +1574,7 @@ def workout_undo_smoothenpace_view( if (checkworkoutuser(request.user,row)==False): message = "You are not allowed to edit this workout" messages.error(request,message) - url = reverse(workouts_view) + url = reverse('workouts_view') return HttpResponseRedirect(url) @@ -1785,7 +1614,7 @@ def workout_smoothenpace_view(request,id=0,message="",successmessage=""): if (checkworkoutuser(request.user,row)==False): message = "You are not allowed to edit this workout" messages.error(request,message) - url = reverse(workouts_view) + url = reverse('workouts_view') return HttpResponseRedirect(url) @@ -1841,7 +1670,7 @@ def workout_crewnerd_summary_view(request,id=0,message="",successmessage=""): 'name': row.name }, { - 'url':reverse(workout_crewnerd_summary_view,kwargs={'id':id}), + 'url':reverse('workout_crewnerd_summary_view',kwargs={'id':id}), 'name': 'CrewNerd Summary' } @@ -1860,7 +1689,7 @@ def workout_crewnerd_summary_view(request,id=0,message="",successmessage=""): os.remove(fname) successmessage = "CrewNerd summary added" messages.info(request,successmessage) - url = reverse(workout_edit_view, + url = reverse('workout_edit_view', kwargs = { 'id':int(id), }) @@ -1873,7 +1702,7 @@ def workout_crewnerd_summary_view(request,id=0,message="",successmessage=""): pass message = "Something went wrong (workout_crewnerd_summary_view)" messages.error(request,message) - url = reverse(workout_edit_view, + url = reverse('workout_edit_view', kwargs = { 'id':int(id), }) @@ -1914,7 +1743,7 @@ def workout_downloadwind_view(request,id=0, if (checkworkoutuser(request.user,row)==False): message = "You are not allowed to edit this workout" messages.error(request,message) - url = reverse(workouts_view) + url = reverse('workouts_view') return HttpResponseRedirect(url) @@ -1954,7 +1783,7 @@ def workout_downloadwind_view(request,id=0, kwargs = { 'id':int(id)} - url = reverse(workout_wind_view,kwargs=kwargs) + url = reverse('workout_wind_view',kwargs=kwargs) response = HttpResponseRedirect(url) except KeyError: message = "No latitude/longitude data" @@ -1962,7 +1791,7 @@ def workout_downloadwind_view(request,id=0, kwargs = { 'id':int(id) } - url = reverse(workout_wind_view,kwargs=kwargs) + url = reverse('workout_wind_view',kwargs=kwargs) response = HttpResponseRedirect(url) @@ -1980,7 +1809,7 @@ def workout_downloadmetar_view(request,id=0, if (checkworkoutuser(request.user,row)==False): message = "You are not allowed to edit this workout" messages.error(request,message) - url = reverse(workouts_view) + url = reverse('workouts_view') return HttpResponseRedirect(url) @@ -2020,7 +1849,7 @@ def workout_downloadmetar_view(request,id=0, kwargs = { 'id':int(id)} - url = reverse(workout_wind_view,kwargs=kwargs) + url = reverse('workout_wind_view',kwargs=kwargs) response = HttpResponseRedirect(url) except KeyError: message = "No latitude/longitude data" @@ -2028,7 +1857,7 @@ def workout_downloadmetar_view(request,id=0, kwargs = { 'id':int(id) } - url = reverse(workout_wind_view,kwargs=kwargs) + url = reverse('workout_wind_view',kwargs=kwargs) response = HttpResponseRedirect(url) @@ -2051,7 +1880,7 @@ def workout_wind_view(request,id=0,message="",successmessage=""): 'name': row.name }, { - 'url':reverse(workout_wind_view,kwargs={'id':id}), + 'url':reverse('workout_wind_view',kwargs={'id':id}), 'name': 'Wind' } @@ -2060,7 +1889,7 @@ def workout_wind_view(request,id=0,message="",successmessage=""): if (checkworkoutuser(request.user,row)==False): message = "You are not allowed to edit this workout" messages.error(request,message) - url = reverse(workouts_view) + url = reverse('workouts_view') return HttpResponseRedirect(url) @@ -2132,7 +1961,7 @@ def workout_wind_view(request,id=0,message="",successmessage=""): kwargs = { 'id':int(id) } - url = reverse(workout_wind_view,kwargs=kwargs) + url = reverse('workout_wind_view',kwargs=kwargs) response = HttpResponseRedirect(url) else: @@ -2181,7 +2010,7 @@ def workout_stream_view(request,id=0,message="",successmessage=""): if (checkworkoutuser(request.user,row)==False): message = "You are not allowed to edit this workout" messages.error(request,message) - url = reverse(workouts_view) + url = reverse('workouts_view') return HttpResponseRedirect(url) @@ -2218,7 +2047,7 @@ def workout_stream_view(request,id=0,message="",successmessage=""): messages.error(request,message) kwargs = { 'id':int(id)} - url = reverse(workout_wind_view,kwargs=kwargs) + url = reverse('workout_wind_view',kwargs=kwargs) response = HttpResponseRedirect(url) else: @@ -2239,7 +2068,7 @@ def workout_stream_view(request,id=0,message="",successmessage=""): 'name': row.name }, { - 'url':reverse(workout_stream_view,kwargs={'id':id}), + 'url':reverse('workout_stream_view',kwargs={'id':id}), 'name': 'Stream' } @@ -2273,7 +2102,7 @@ def workout_otwsetpower_view(request,id=0,message="",successmessage=""): if (checkworkoutuser(request.user,w)==False): message = "You are not allowed to edit this workout" messages.error(request,message) - url = reverse(workouts_view) + url = reverse('workouts_view') return HttpResponseRedirect(url) @@ -2345,7 +2174,7 @@ def workout_otwsetpower_view(request,id=0,message="",successmessage=""): try: url = request.session['referer'] except KeyError: - url = reverse(workout_advanced_view,kwargs=kwargs) + url = reverse('workout_edit_view',kwargs=kwargs) response = HttpResponseRedirect(url) return response @@ -2355,7 +2184,7 @@ def workout_otwsetpower_view(request,id=0,message="",successmessage=""): messages.error(request,message) kwargs = { 'id':int(id)} - url = reverse(workout_otwsetpower_view,kwargs=kwargs) + url = reverse('workout_otwsetpower_view',kwargs=kwargs) response = HttpResponseRedirect(url) else: @@ -2371,7 +2200,7 @@ def workout_otwsetpower_view(request,id=0,message="",successmessage=""): 'name': w.name }, { - 'url':reverse(workout_otwsetpower_view,kwargs={'id':id}), + 'url':reverse('workout_otwsetpower_view',kwargs={'id':id}), 'name': 'OTW Power' } @@ -2411,7 +2240,7 @@ def instroke_view(request,id=0): 'name': w.name }, { - 'url':reverse(instroke_view,kwargs={'id':id}), + 'url':reverse('instroke_view',kwargs={'id':id}), 'name': 'In-Stroke Metrics' } @@ -2424,7 +2253,7 @@ def instroke_view(request,id=0): if (checkworkoutuser(request.user,w)==False): message = "You are not allowed to edit this workout" messages.error(request,message) - url = reverse(workouts_view) + url = reverse('workouts_view') return HttpResponseRedirect(url) @@ -2456,7 +2285,7 @@ def instroke_chart(request,id=0,metric=''): if (checkworkoutuser(request.user,w)==False): message = "You are not allowed to edit this workout" messages.error(request,message) - url = reverse(workouts_view) + url = reverse('workouts_view') return HttpResponseRedirect(url) @@ -2526,7 +2355,7 @@ def workout_data_view(request, id=0): 'name': w.name }, { - 'url':reverse(workout_data_view,kwargs={'id':id}), + 'url':reverse('workout_data_view',kwargs={'id':id}), 'name': 'Data Explorer' } @@ -2628,7 +2457,7 @@ def workout_stats_view(request,id=0,message="",successmessage=""): 'name': w.name }, { - 'url':reverse(workout_stats_view,kwargs={'id':id}), + 'url':reverse('workout_stats_view',kwargs={'id':id}), 'name': 'Stats' } @@ -2786,19 +2615,10 @@ def workflow_default_view(request): r.save() - url = reverse(workout_workflow_config2_view) + url = reverse('workout_workflow_config2_view') return HttpResponseRedirect(url) -def get_workout_default_page(request,id): - if request.user.is_anonymous(): - return reverse(workout_view,kwargs={'id':str(id)}) - else: - r = Rower.objects.get(user=request.user) - if r.defaultlandingpage == 'workout_edit_view': - return reverse(workout_edit_view,kwargs={'id':str(id)}) - else: - return reverse(workout_workflow_view,kwargs={'id':str(id)}) # Workflow configuration @login_required() @@ -2946,7 +2766,7 @@ def workout_workflow_view(request,id): 'name': row.name }, { - 'url':reverse(workout_workflow_view,kwargs={'id':id}), + 'url':reverse('workout_workflow_view',kwargs={'id':id}), 'name': 'View' } @@ -3232,7 +3052,7 @@ def workout_flexchart3_view(request,*args,**kwargs): 'name': row.name }, { - 'url':reverse(workout_flexchart3_view,kwargs=kwargs), + 'url':reverse('workout_flexchart3_view',kwargs=kwargs), 'name': 'Flex Chart' } @@ -3286,7 +3106,7 @@ def workout_otwpowerplot_view(request,id=0,message="",successmessage=""): 'name': w.name }, { - 'url':reverse(workout_otwpowerplot_view,kwargs={'id':id}), + 'url':reverse('workout_otwpowerplot_view',kwargs={'id':id}), 'name': 'Interactive OTW Power Plot' } @@ -3387,7 +3207,7 @@ def workout_comment_view(request,id=0): c = WorkoutComment(workout=w,user=request.user,comment=comment, notification=notification) c.save() - url = reverse(workout_comment_view, + url = reverse('workout_comment_view', kwargs={ 'id':id }) @@ -3428,8 +3248,9 @@ def workout_comment_view(request,id=0): emailbounced = ocr.emailbounced ) - url = reverse(workout_comment_view,kwargs = { - 'id':id}) + url = reverse('workout_comment_view', + kwargs = { + 'id':id}) return HttpResponseRedirect(url) form = WorkoutCommentForm() @@ -3456,7 +3277,7 @@ def workout_comment_view(request,id=0): 'name': w.name }, { - 'url':reverse(workout_comment_view,kwargs={'id':id}), + 'url':reverse('workout_comment_view',kwargs={'id':id}), 'name': 'Comments' } @@ -3600,7 +3421,7 @@ def workout_edit_view(request,id=0,message="",successmessage=""): dataprep.runcpupdate(row.user,type=row.workouttype) messages.info(request,successmessage) - url = reverse(workout_edit_view, + url = reverse('workout_edit_view', kwargs = { 'id':str(row.id), }) @@ -3665,7 +3486,7 @@ def workout_edit_view(request,id=0,message="",successmessage=""): 'name': row.name }, { - 'url':reverse(workout_edit_view,kwargs={'id':id}), + 'url':reverse('workout_edit_view',kwargs={'id':id}), 'name': 'Edit' } @@ -3713,7 +3534,7 @@ def workout_map_view(request,id=0): 'name': w.name }, { - 'url':reverse(workout_map_view,kwargs={'id':id}), + 'url':reverse('workout_map_view',kwargs={'id':id}), 'name': 'Map' } @@ -3789,7 +3610,7 @@ def workout_uploadimage_view(request,id): 'name': w.name }, { - 'url':reverse(workout_uploadimage_view,kwargs={'id':id}), + 'url':reverse('workout_uploadimage_view',kwargs={'id':id}), 'name': 'Upload Image' } @@ -3825,7 +3646,7 @@ def workout_uploadimage_view(request,id): message = "Not a valid image" messages.error(request,message) os.remove(path_and_filename) - url = reverse(workout_uploadimage_view, + url = reverse('workout_uploadimage_view', kwargs = {'id':id}) if is_ajax: @@ -3847,7 +3668,7 @@ def workout_uploadimage_view(request,id): return HttpResponseRedirect(url) else: messages.error(request,'Something went wrong - no file attached') - url = reverse(workout_uploadimage_view, + url = reverse('workout_uploadimage_view', kwargs = {'id':id}) if is_ajax: @@ -3929,7 +3750,7 @@ def workout_toggle_ranking(request,id=0): return response else: - url = reverse(workouts_view) + url = reverse('workouts_view') response = HttpResponseRedirect(url) return response @@ -3961,7 +3782,7 @@ def workout_upload_view(request, 'name':'Workouts' }, { - 'url': reverse(workout_upload_view), + 'url': reverse('workout_upload_view'), 'name': 'Upload' } ] @@ -4063,7 +3884,7 @@ def workout_upload_view(request, else: messages.error(request, "Something went wrong - no file attached") - url = reverse(workout_upload_view) + url = reverse('workout_upload_view') if is_ajax: return JSONResponse({'result':0,'url':0}) else: @@ -4151,7 +3972,7 @@ def workout_upload_view(request, messages.info( request, "The file was too large to process in real time. It will be processed in a background process. You will receive an email when it is ready") - url = reverse(workout_upload_view) + url = reverse('workout_upload_view') if is_ajax: return JSONResponse({'result':1,'url':url}) else: @@ -4160,7 +3981,7 @@ def workout_upload_view(request, if not id: messages.error(request,message) - url = reverse(workout_upload_view) + url = reverse('workout_upload_view') if is_ajax: return JSONResponse({'result':0,'url':url}) else: @@ -4169,7 +3990,7 @@ def workout_upload_view(request, elif id == -1: message = 'The zip archive will be processed in the background. The files in the archive will only be uploaded without the extra actions. You will receive email when the workouts are ready.' messages.info(request,message) - url = reverse(workout_upload_view) + url = reverse('workout_upload_view') if is_ajax: return JSONResponse({'result':1,'url':url}) else: @@ -4179,7 +4000,7 @@ def workout_upload_view(request, if message: messages.error(request,message) - url = reverse(workout_edit_view, + url = reverse('workout_edit_view', kwargs = { 'id':int(id), }) @@ -4387,7 +4208,7 @@ def team_workout_upload_view(request,message="", 'name':'Workouts' }, { - 'url': reverse(team_workout_upload_view), + 'url': reverse('team_workout_upload_view'), 'name': 'Team Upload' } ] @@ -4477,20 +4298,20 @@ def team_workout_upload_view(request,message="", ) - url = reverse(team_workout_upload_view) + url = reverse('team_workout_upload_view') response = HttpResponseRedirect(url) return response if not id: messages.error(request,message) - url = reverse(team_workout_upload_view) + url = reverse('team_workout_upload_view') response = HttpResponseRedirect(url) return response elif id == -1: message = 'The zip archive will be processed in the background. The files in the archive will only be uploaded without the extra actions. You will receive email when the workouts are ready.' messages.info(request,message) - url = reverse(team_workout_upload_view) + url = reverse('team_workout_upload_view') response = HttpResponseRedirect(url) return response @@ -4498,7 +4319,7 @@ def team_workout_upload_view(request,message="", successmessage = "The workout was added to the user's account" messages.info(request,successmessage) - url = reverse(team_workout_upload_view) + url = reverse('team_workout_upload_view') response = HttpResponseRedirect(url) w = Workout.objects.get(id=id) @@ -4546,7 +4367,7 @@ def team_workout_upload_view(request,message="", # A page with all the recent graphs (searchable on workout name) @login_required() def graphs_view(request): - request.session['referer'] = reverse(graphs_view) + request.session['referer'] = reverse('graphs_view') try: r = getrower(request.user) workouts = Workout.objects.filter(user=r).order_by("-date", "-starttime") @@ -4611,7 +4432,7 @@ def graph_show_view(request,id): 'name': w.name }, { - 'url':reverse(graph_show_view,kwargs={'id':id}), + 'url':reverse('graph_show_view',kwargs={'id':id}), 'name': 'Chart' } @@ -4679,7 +4500,7 @@ def workout_summary_restore_view(request,id,message="",successmessage=""): messages.info(request,'Original Interval Data Restored') - url = reverse(workout_summary_edit_view, + url = reverse('workout_summary_edit_view', kwargs={ 'id':int(id), } @@ -4703,7 +4524,8 @@ def workout_split_view(request,id=id): 'name': row.name }, { - 'url':reverse(graph_show_view,kwargs={'id':id}), + 'url':reverse('graph_show_view', + kwargs={'id':id}), 'name': 'Chart' } @@ -4729,7 +4551,7 @@ def workout_split_view(request,id=id): if request.user == r: - url = reverse(workouts_view) + url = reverse('workouts_view') else: mgrids = [team.id for team in Team.objects.filter(manager=request.user)] rwrids = [team.id for team in r.team.all()] @@ -4737,13 +4559,13 @@ def workout_split_view(request,id=id): if len(teamids) > 0: teamid = teamids[0] - url = reverse(workouts_view, + url = reverse('workouts_view', kwargs={ 'teamid':int(teamid), } ) else: - url = reverse(workouts_view) + url = reverse('workouts_view') rowname = row.name if isinstance(rowname,unicode): @@ -4817,7 +4639,7 @@ def workout_fusion_view(request,id1=0,id2=1): successmessage = 'Data fused' messages.info(request,message) - url = reverse(workout_edit_view, + url = reverse('workout_edit_view', kwargs={ 'id':idnew, }) @@ -4836,11 +4658,12 @@ def workout_fusion_view(request,id1=0,id2=1): 'name': str(w1.id) }, { - 'url':reverse(workout_fusion_list,kwargs={'id':id1}), + 'url':reverse('workout_fusion_list',kwargs={'id':id1}), 'name': 'Sensor Fusion' }, { - 'url':reverse(workout_fusion_view,kwargs={'id1':id1,'id2':id2}), + 'url':reverse('workout_fusion_view', + kwargs={'id1':id1,'id2':id2}), 'name': str(w2.id) } @@ -4874,7 +4697,7 @@ def workout_summary_edit_view(request,id,message="",successmessage="" 'name': row.name }, { - 'url':reverse(workout_summary_edit_view,kwargs={'id':id}), + 'url':reverse('workout_summary_edit_view',kwargs={'id':id}), 'name': 'Edit Intervals' } @@ -5296,7 +5119,7 @@ class GraphDelete(DeleteView): 'name': str(self.object.workout.id) }, { - 'url':reverse(graph_show_view,kwargs={'id':self.object.pk}), + 'url':reverse('graph_show_view',kwargs={'id':self.object.pk}), 'name': 'Chart' }, { 'url':reverse('graph_delete',kwargs={'pk':str(self.object.pk)}), @@ -5314,7 +5137,7 @@ class GraphDelete(DeleteView): def get_success_url(self): w = self.object.workout - return reverse(workout_edit_view,kwargs={'id':str(w.id)}) + return reverse('workout_edit_view',kwargs={'id':str(w.id)}) def get_object(self, *args, **kwargs): obj = super(GraphDelete, self).get_object(*args, **kwargs) @@ -5368,7 +5191,7 @@ class WorkoutDelete(DeleteView): def get_success_url(self): - return reverse(workouts_view) + return reverse('workouts_view') def get_object(self, *args, **kwargs): obj = super(WorkoutDelete, self).get_object(*args, **kwargs) From 10519ca558e08996ab283dbf595a9265da969244 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Fri, 8 Feb 2019 11:39:25 +0100 Subject: [PATCH 13/16] refund policy updated --- rowers/forms.py | 1 + rowers/templates/billing.html | 3 +- rowers/templates/legal.html | 82 ++++++++++++++++++++++++++-- rowers/templates/paidplans.html | 6 +- rowers/templates/paymentconfirm.html | 6 +- rowers/templates/privacypolicy.html | 5 +- rowers/templates/refunds.html | 33 ++++++++--- rowers/views/paymentviews.py | 12 +++- 8 files changed, 129 insertions(+), 19 deletions(-) diff --git a/rowers/forms.py b/rowers/forms.py index ffef4328..88ce4888 100644 --- a/rowers/forms.py +++ b/rowers/forms.py @@ -44,6 +44,7 @@ class BillingForm(forms.Form): max_digits=8) plan = forms.IntegerField(widget=forms.HiddenInput()) payment_method_nonce = forms.CharField(max_length=255,required=True) + tac= forms.BooleanField(required=True,initial=False) # login form diff --git a/rowers/templates/billing.html b/rowers/templates/billing.html index d6035180..ef442cb3 100644 --- a/rowers/templates/billing.html +++ b/rowers/templates/billing.html @@ -22,7 +22,8 @@ as a price per year. You can downgrade or cancel your plan at any time, through the settings page. Please refer to our terms and conditions for our - payments and refunds policy. Accepted payment methods are the payment methods offered + payments and refunds policy. + Accepted payment methods are the payment methods offered by Braintree through us. If you have any questions about our payments and refunds policy, please contact diff --git a/rowers/templates/legal.html b/rowers/templates/legal.html index 037e3253..66bc90fa 100644 --- a/rowers/templates/legal.html +++ b/rowers/templates/legal.html @@ -3,6 +3,52 @@ {% block title %}Legal{% endblock title %} {% block main %} +

Welcome to Rowsandall

+ +

Welcome to Rowsandall. We want you to know and understand your rights and our rights relating + to the provision o fthe Services (as defined below). Please review them carefully. + Here are a few highlights: +

+ +

+

+

+

Terms and Conditions

Credit

@@ -32,7 +78,12 @@

-

Acceptable use

+

Acceptable use

+ +

You must not use this website to copy, store, host, transmit, sned, use, publish or + distribute any material which is illegal, obscene, defamatory, threatening, harassing, abusive, + or hateful or that advocates violence. +

You must not use this website in any way that causes, or may cause, damage to the website or impairment of the availability or accessibility of the website; or in any way which is unlawful, illegal, fraudulent or harmful, or in connection with any unlawful, illegal, fraudulent or harmful purpose or activity.

@@ -51,7 +102,7 @@

rowsandall.com may disable your user ID and password in rowsandall.com’s sole discretion without notice or explanation.

-

User content

+

User content

In these terms and conditions, your user content means material (including without limitation text, images, audio material, video material and audio-visual material) that you submit to this website, for whatever purpose.

@@ -78,7 +129,7 @@

Nothing on this website constitutes, or is meant to constitute, advice of any kind. If you require advice in relation to any legal, financial or medica] matter you should consult an appropriate professional.

-

Limitations of liability

+

Limitations of liability

rowsandall.com will not be liable to you (whether under the law of contact, the law of torts or otherwise) in relation to the contents of, or use of, or otherwise in connection with, this website: @@ -136,6 +187,27 @@

If a provision of these terms and conditions is determined by any court or other competent authority to be unlawful and/or unenforceable, the other provisions will continue in effect. If any unlawful and/or unenforceable provision would be lawful or enforceable if part of it were deleted, that part will be deemed to be deleted, and the rest of the provision will continue in effect.

+

Termination

+ +

+ You agree that Rowsandall may, under certain circumstances and without prior notice, + immediately terminate your accountand/or access to the site. Cause for such termination + shall include, but not be limited to, (a) breaches or violations of the Terms or + other incorporated agreements, policies, or guidelines, (b) requests by law enforcement + or other government agencies, (c) a request by you (self-initiated account deletions), + (d) discontinuance or material modification to the services (or any portion thereof), (e) + unexpected technical or security issues or problems, f) extended periods of inactivity, + and/or (g) nonpayment of any fees owed by you in connection with the Services. + Termination of your account may include (x) removal of access to all offerings within the + Services, (y) deletion of your information, files and Content associated with your account, + and (z) barring of further use of the Services. Further, you agree that all terminations + for cause shall be made in Rowsandall’s sole discretion and that Strava shall not be liable + to you or any third party for any termination of your account or access to the Services. + The following Sections shall survive termination of your account + and/or the Terms: Member Content Submitted to the Services, Proprietary Rights, + Your Feedback, Disclaimer of Warranties and Liability, Indemnity, Applicable Laws and General. +

+

Entire agreement

These terms and conditions constitute the entire agreement between you and rowsandall.com in relation to your use of this website, and supersede all previous agreements in respect of your use of this website.

@@ -145,7 +217,7 @@

These terms and conditions will be governed by and construed in accordance with Czech Law and any disputes relating to these terms and conditions will be subject to the exclusive jurisdiction of the courts of The Czech Republic.

-

rowsandall.com’s details

+

rowsandall.com’s details

The rowsandall.com site is owned by Rowsandall s.r.o., Nové sady 988/2, Staré Brno, 602 00 Brno, Czech Republic (company identification number 070 48 572)

@@ -156,7 +228,7 @@ {% include "refunds.html" %} -

Privacy Policy

+

Privacy Policy

{% include "privacypolicy.html" %} diff --git a/rowers/templates/paidplans.html b/rowers/templates/paidplans.html index cd174ccd..0b2f55cb 100644 --- a/rowers/templates/paidplans.html +++ b/rowers/templates/paidplans.html @@ -302,7 +302,11 @@

Terms and Conditions, Contact Information

-

Our paid plans follow the Terms and Conditions.

+

+ Before purchasing any of our paid plans, you must + review and acknowledge our Terms and Conditions, + and Refunds and Returns Policy +

Payments are made to "Rowsandall s.r.o.", with the following contact information:

Rowsandall s.r.o.
diff --git a/rowers/templates/paymentconfirm.html b/rowers/templates/paymentconfirm.html index 149a1355..d757f65a 100644 --- a/rowers/templates/paymentconfirm.html +++ b/rowers/templates/paymentconfirm.html @@ -93,6 +93,11 @@ +

+ I have taken note of the + Refund and Cancellation + Policy and agree with the Terms of Service. +

{% csrf_token %} @@ -101,7 +106,6 @@ {% include 'braintreedropin.html' %} - {% endblock %} {% block sidebar %} diff --git a/rowers/templates/privacypolicy.html b/rowers/templates/privacypolicy.html index 944660ed..d90051f8 100644 --- a/rowers/templates/privacypolicy.html +++ b/rowers/templates/privacypolicy.html @@ -116,7 +116,7 @@ posts.

-

Data Deletion

+

Membership Cancellation and Data Deletion

If you have previously consented to allow rowsandall.com to store and process your personal data in accordance with this privacy policy, and you wish to withdraw your conent, @@ -237,7 +237,7 @@ edit your heart rate and power settings, as well as functional threshold information and the account information accessible on your settings page under the header "Account Information". The team manager is not able to access or change your passwords, team memberships, favorite charts, export settings, workflow layout, or secret tokens. Also, the team manager is not able to download all your data, - not can he deactivate or delete your account. + nor can he deactivate or delete your account.

@@ -274,6 +274,7 @@ has suitable GDPR compliant measures in place.

+

Inactive Users - accounts are deleted after 18 months

diff --git a/rowers/templates/refunds.html b/rowers/templates/refunds.html index faf7140d..ff8cb547 100644 --- a/rowers/templates/refunds.html +++ b/rowers/templates/refunds.html @@ -1,5 +1,5 @@ -

Thank you for shopping at Rowsandall.

+

Thank you for shopping at Rowsandall.

Digital products

@@ -9,19 +9,36 @@ of the plan, you can cancel the recurring payment. We do not issue refunds for payments regarding the current plan period.

-

We do not issue refunds for digital products once the order is - confirmed and the product is sent.

+

If you are not 100% satisfied with your purchase, you can get a refund or + exhchange the product for another one. +

+ +

You can return a product for up to 30 days from the date you purchased it. + To be eligible for a refund, you need to contact us using the contact information + below. To improve our service, we ask you to explain how the product did not meet + your expectations. If your refund is approved, we will initiate a refund to your + credit card (or original method of payment). You will receive the credit within + a certain amount of days, depending on your card issuer's policies. +

We recommend contacting us for assistance if you experience any issues receiving our products.

-

Upgrades and Downgrades

+

Upgrades and Downgrades, Cancellations

-Upgrades and downgrades between paid plans are effective immediately, but the billing cycle -is not changed. Upgrades are charged a pro-rated amount for the current billing cycle. Downgrades -will result in a credit on our accounts, leading to a lower charge at the beginning of the -subsequent billing cycle. +

+ Upgrades and downgrades between paid plans are effective immediately, but the billing cycle + is not changed. Upgrades are charged a pro-rated amount for the current billing cycle. Downgrades + will result in a credit on our accounts, leading to a lower charge at the beginning of the + subsequent billing cycle. +

+ +

+ With the exception of an approved refund within 30 days of purchase (see above), we do not + issue refunds upon cancellation of the plan. If you are eligible for a refund, contact + us within 30 days of your purchase. +

Contact us

diff --git a/rowers/views/paymentviews.py b/rowers/views/paymentviews.py index 0e7b88f9..5183ca27 100644 --- a/rowers/views/paymentviews.py +++ b/rowers/views/paymentviews.py @@ -316,6 +316,7 @@ def checkouts_view(request): return HttpResponseRedirect(url) form = BillingForm(request.POST) + print request.POST if form.is_valid(): data = form.cleaned_data success,amount = braintreestuff.create_subscription(r,data) @@ -329,7 +330,16 @@ def checkouts_view(request): messages.error(request,"There was a problem with your payment") url = reverse(billing_view) return HttpResponseRedirect(url) - + elif 'tac' not in request.POST: + try: + planid = int(request.POST['plan']) + url = reverse('payment_confirm_view',kwargs={'planid':planid}) + messages.error(request,"You must review and acknowledge the terms and conditions") + return HttpResponseRedirect(url) + except IndexError: + messages.error(request,"There was an error in the payment form") + url = reverse('billing_view') + return HttpResponseRedirect(url) else: messages.error(request,"There was an error in the payment form") url = reverse(billing_view) From 319ce56e07bd2cd48a510cfd1a20d60831689270 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Fri, 8 Feb 2019 12:39:31 +0100 Subject: [PATCH 14/16] added downgrades & upgrades --- rowers/templates/downgradeconfirm.html | 5 + rowers/templates/upgradeconfirm.html | 5 + rowers/tests/test_payments.py | 136 +++++++++++++++++++++++++ rowers/views/paymentviews.py | 23 ++++- 4 files changed, 167 insertions(+), 2 deletions(-) diff --git a/rowers/templates/downgradeconfirm.html b/rowers/templates/downgradeconfirm.html index 1e027c34..4b76a6d9 100644 --- a/rowers/templates/downgradeconfirm.html +++ b/rowers/templates/downgradeconfirm.html @@ -72,6 +72,11 @@ +

+ I have taken note of the + Refund and Cancellation + Policy and agree with the Terms of Service. +

{% csrf_token %} diff --git a/rowers/templates/upgradeconfirm.html b/rowers/templates/upgradeconfirm.html index 2427117a..e3b0aa98 100644 --- a/rowers/templates/upgradeconfirm.html +++ b/rowers/templates/upgradeconfirm.html @@ -93,6 +93,11 @@ +

+ I have taken note of the + Refund and Cancellation + Policy and agree with the Terms of Service. +

{% csrf_token %} diff --git a/rowers/tests/test_payments.py b/rowers/tests/test_payments.py index 61a9e5c6..794416d5 100644 --- a/rowers/tests/test_payments.py +++ b/rowers/tests/test_payments.py @@ -366,6 +366,7 @@ class PaymentTest(TestCase): 'amount':'15.00', 'plan': plans[1].id, 'payment_method_nonce': 'aap', + 'tac':'tac', } form = BillingForm(form_data) @@ -410,6 +411,7 @@ class PaymentTest(TestCase): 'amount':'15.00', 'plan': plans[1].id, 'payment_method_nonce': 'aap', + 'tac':'tac', } form = BillingForm(form_data) @@ -453,6 +455,7 @@ class PaymentTest(TestCase): 'amount':'15.00', 'plan': plans[1].id, 'payment_method_nonce': 'aap', + 'tac':'tac', } form = BillingForm(form_data) @@ -470,3 +473,136 @@ class PaymentTest(TestCase): expected_url = '/rowers/downgradecompleted/', status_code=302,target_status_code=200) + @patch('rowers.views.braintreestuff.create_subscription', side_effect=mock_create_subscription) + def test_checkouts_view(self,mock_subscription): + u = UserFactory() + r = Rower.objects.create(user=u, + birthdate=faker.profile()['birthdate'], + gdproptin=True, + gdproptindate=timezone.now(), + rowerplan='coach', + paymentprocessor='braintree', + street_address = faker.street_address(), + city = faker.city(), + postal_code = faker.postalcode(), + country = faker.country(), + ) + + r.save() + u.set_password(self.password) + u.save() + + plans = PaidPlan.objects.all().order_by('price') + plan = plans[1] + + form_data = { + 'amount':'15.00', + 'plan': plans[1].id, + 'payment_method_nonce': 'aap', + } + + + login = self.c.login(username=u.username, password=self.password) + self.assertTrue(login) + + url = '/rowers/checkouts/' + + response = self.c.post(url, form_data,follow=True) + self.assertEqual(response.status_code,200) + + self.assertRedirects(response, + expected_url = '/rowers/checkout/{planid}'.format( + planid=plans[1].id), + status_code=302,target_status_code=200) + + + @patch('rowers.views.braintreestuff.update_subscription', side_effect=mock_update_subscription) + def test_upgrade_checkouts_view(self,mock_subscription): + u = UserFactory() + r = Rower.objects.create(user=u, + birthdate=faker.profile()['birthdate'], + gdproptin=True, + gdproptindate=timezone.now(), + rowerplan='coach', + paymentprocessor='braintree', + street_address = faker.street_address(), + city = faker.city(), + postal_code = faker.postalcode(), + country = faker.country(), + ) + + r.save() + u.set_password(self.password) + u.save() + + plans = PaidPlan.objects.all().order_by('price') + plan = plans[1] + + form_data = { + 'amount':'15.00', + 'plan': plans[1].id, + 'payment_method_nonce': 'aap', + # 'tac':'tac', + } + + form = BillingForm(form_data) + self.assertTrue(form.is_valid()) + + login = self.c.login(username=u.username, password=self.password) + self.assertTrue(login) + + url = '/rowers/upgradecheckouts/' + + response = self.c.post(url, form_data,follow=True) + self.assertEqual(response.status_code,200) + + self.assertRedirects(response, + expected_url = '/rowers/upgradecheckout/{planid}'.format( + planid=plans[1].id), + status_code=302,target_status_code=200) + + @patch('rowers.views.braintreestuff.update_subscription', side_effect=mock_update_subscription) + def test_downgrade_checkouts_view(self,mock_subscription): + u = UserFactory() + r = Rower.objects.create(user=u, + birthdate=faker.profile()['birthdate'], + gdproptin=True, + gdproptindate=timezone.now(), + rowerplan='coach', + paymentprocessor='braintree', + street_address = faker.street_address(), + city = faker.city(), + postal_code = faker.postalcode(), + country = faker.country(), + ) + + r.save() + u.set_password(self.password) + u.save() + + plans = PaidPlan.objects.all().order_by('price') + plan = plans[1] + + form_data = { + 'amount':'15.00', + 'plan': plans[1].id, + 'payment_method_nonce': 'aap', + # 'tac':'tac', + } + + form = BillingForm(form_data) + self.assertTrue(form.is_valid()) + + login = self.c.login(username=u.username, password=self.password) + self.assertTrue(login) + + url = '/rowers/downgradecheckouts/' + + response = self.c.post(url, form_data,follow=True) + self.assertEqual(response.status_code,200) + + self.assertRedirects(response, + expected_url = '/rowers/downgradecheckout/{planid}'.format( + planid=plans[1].id), + status_code=302,target_status_code=200) + diff --git a/rowers/views/paymentviews.py b/rowers/views/paymentviews.py index 5183ca27..2f680fe6 100644 --- a/rowers/views/paymentviews.py +++ b/rowers/views/paymentviews.py @@ -316,7 +316,6 @@ def checkouts_view(request): return HttpResponseRedirect(url) form = BillingForm(request.POST) - print request.POST if form.is_valid(): data = form.cleaned_data success,amount = braintreestuff.create_subscription(r,data) @@ -333,7 +332,7 @@ def checkouts_view(request): elif 'tac' not in request.POST: try: planid = int(request.POST['plan']) - url = reverse('payment_confirm_view',kwargs={'planid':planid}) + url = reverse('downgrade_confirm_view',kwargs={'planid':planid}) messages.error(request,"You must review and acknowledge the terms and conditions") return HttpResponseRedirect(url) except IndexError: @@ -376,6 +375,16 @@ def upgrade_checkouts_view(request): url = reverse(upgrade_view) return HttpResponseRedirect(url) + elif 'tac' not in request.POST: + try: + planid = int(request.POST['plan']) + url = reverse('upgrade_confirm_view',kwargs={'planid':planid}) + messages.error(request,"You must review and acknowledge the terms and conditions") + return HttpResponseRedirect(url) + except IndexError: + messages.error(request,"There was an error in the payment form") + url = reverse('billing_view') + return HttpResponseRedirect(url) else: messages.error(request,"There was an error in the payment form") url = reverse(upgrade_view) @@ -409,6 +418,16 @@ def downgrade_checkouts_view(request): messages.error(request,"There was a problem with your transaction") url = reverse(upgrade_view) return HttpResponseRedirect(url) + elif 'tac' not in request.POST: + try: + planid = int(request.POST['plan']) + url = reverse('payment_confirm_view',kwargs={'planid':planid}) + messages.error(request,"You must review and acknowledge the terms and conditions") + return HttpResponseRedirect(url) + except IndexError: + messages.error(request,"There was an error in the payment form") + url = reverse('billing_view') + return HttpResponseRedirect(url) else: messages.error(request,"There was an error in the payment form") From fa9632afb1492ae38b0d350693081275d3654608 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Fri, 8 Feb 2019 13:01:19 +0100 Subject: [PATCH 15/16] testing refund acknowledgement --- rowers/datautils.py | 11 ++++++----- rowers/templates/payment_completed.html | 3 ++- rowers/templates/paymentconfirm.html | 2 +- rowers/templates/upgradeconfirm.html | 2 +- rowers/tests/test_payments.py | 12 +++++++----- rowers/tests/testdata/testdata.csv.gz | Bin 11457 -> 11426 bytes rowers/views/paymentviews.py | 4 ++-- 7 files changed, 19 insertions(+), 15 deletions(-) diff --git a/rowers/datautils.py b/rowers/datautils.py index b19a7597..4048fd24 100644 --- a/rowers/datautils.py +++ b/rowers/datautils.py @@ -304,15 +304,16 @@ def getmaxwattinterval(tt,ww,i): if len(w_roll): # now goes with # data points - should be fixed seconds indexmax = w_roll.idxmax(axis=1) - indexmaxpos = indexmax.get_loc(indexmax) + # indexmaxpos = indexmax.get_loc(indexmax) + indexmaxpos = indexmax try: - t_0 = tt.iloc[indexmaxpos] - t_1 = tt.iloc[indexmaxpos-i] - deltas = tt.iloc[indexmaxpos-i:indexmaxpos].diff().dropna() + t_0 = tt.ix[indexmaxpos] + t_1 = tt.ix[indexmaxpos-i] + deltas = tt.ix[indexmaxpos-i:indexmaxpos].diff().dropna() testres = 1.0e-3*deltas.max() < 30. if testres: deltat = 1.0e-3*(t_0-t_1) - wmax = w_roll.iloc[indexmaxpos] + wmax = w_roll.ix[indexmaxpos] #if wmax > 800 or wmax*5.0e-4*deltat > 800.0: # wmax = 0 else: diff --git a/rowers/templates/payment_completed.html b/rowers/templates/payment_completed.html index dbfc9c9a..653907dc 100644 --- a/rowers/templates/payment_completed.html +++ b/rowers/templates/payment_completed.html @@ -24,7 +24,8 @@ as a price per year. You can downgrade or cancel your plan at any time, through the settings page. Please refer to our terms and conditions for our - payments and refunds policy. Accepted payment methods are the payment methods offered + payments and refunds policy. + Accepted payment methods are the payment methods offered by Braintree through us. If you have any questions about our payments and refunds policy, please contact diff --git a/rowers/templates/paymentconfirm.html b/rowers/templates/paymentconfirm.html index d757f65a..1f616834 100644 --- a/rowers/templates/paymentconfirm.html +++ b/rowers/templates/paymentconfirm.html @@ -20,7 +20,7 @@

- Payments will be procesed by Braintree (A PayPal service): + Payments will be processed by Braintree (A PayPal service):

diff --git a/rowers/templates/upgradeconfirm.html b/rowers/templates/upgradeconfirm.html index e3b0aa98..ec4221c3 100644 --- a/rowers/templates/upgradeconfirm.html +++ b/rowers/templates/upgradeconfirm.html @@ -20,7 +20,7 @@

- Payments will be procesed by Braintree (A PayPal service): + Payments will be processed by Braintree (A PayPal service):

diff --git a/rowers/tests/test_payments.py b/rowers/tests/test_payments.py index 794416d5..ab67cfb7 100644 --- a/rowers/tests/test_payments.py +++ b/rowers/tests/test_payments.py @@ -501,6 +501,8 @@ class PaymentTest(TestCase): 'payment_method_nonce': 'aap', } + form = BillingForm(form_data) + self.assertTrue(not form.is_valid()) login = self.c.login(username=u.username, password=self.password) self.assertTrue(login) @@ -511,7 +513,7 @@ class PaymentTest(TestCase): self.assertEqual(response.status_code,200) self.assertRedirects(response, - expected_url = '/rowers/checkout/{planid}'.format( + expected_url = '/rowers/checkout/{planid}/'.format( planid=plans[1].id), status_code=302,target_status_code=200) @@ -546,7 +548,7 @@ class PaymentTest(TestCase): } form = BillingForm(form_data) - self.assertTrue(form.is_valid()) + self.assertTrue(not form.is_valid()) login = self.c.login(username=u.username, password=self.password) self.assertTrue(login) @@ -557,7 +559,7 @@ class PaymentTest(TestCase): self.assertEqual(response.status_code,200) self.assertRedirects(response, - expected_url = '/rowers/upgradecheckout/{planid}'.format( + expected_url = '/rowers/upgradecheckout/{planid}/'.format( planid=plans[1].id), status_code=302,target_status_code=200) @@ -591,7 +593,7 @@ class PaymentTest(TestCase): } form = BillingForm(form_data) - self.assertTrue(form.is_valid()) + self.assertTrue(not form.is_valid()) login = self.c.login(username=u.username, password=self.password) self.assertTrue(login) @@ -602,7 +604,7 @@ class PaymentTest(TestCase): self.assertEqual(response.status_code,200) self.assertRedirects(response, - expected_url = '/rowers/downgradecheckout/{planid}'.format( + expected_url = '/rowers/downgradecheckout/{planid}/'.format( planid=plans[1].id), status_code=302,target_status_code=200) diff --git a/rowers/tests/testdata/testdata.csv.gz b/rowers/tests/testdata/testdata.csv.gz index 7dc55d87c477e548aa76296844e2bf2104abde41..58b6f988584997088913c77aec29bc418eb0152f 100644 GIT binary patch literal 11426 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≧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 bE%OG|dV_eM5SB%T-Ol@e0koFQfOh}@SgjW_ diff --git a/rowers/views/paymentviews.py b/rowers/views/paymentviews.py index 2f680fe6..f72c16ca 100644 --- a/rowers/views/paymentviews.py +++ b/rowers/views/paymentviews.py @@ -332,7 +332,7 @@ def checkouts_view(request): elif 'tac' not in request.POST: try: planid = int(request.POST['plan']) - url = reverse('downgrade_confirm_view',kwargs={'planid':planid}) + url = reverse('payment_confirm_view',kwargs={'planid':planid}) messages.error(request,"You must review and acknowledge the terms and conditions") return HttpResponseRedirect(url) except IndexError: @@ -421,7 +421,7 @@ def downgrade_checkouts_view(request): elif 'tac' not in request.POST: try: planid = int(request.POST['plan']) - url = reverse('payment_confirm_view',kwargs={'planid':planid}) + url = reverse('downgrade_confirm_view',kwargs={'planid':planid}) messages.error(request,"You must review and acknowledge the terms and conditions") return HttpResponseRedirect(url) except IndexError: From ba837d16b1ab935ac70bc44b07f0b56d5d5c5018 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Fri, 8 Feb 2019 13:27:04 +0100 Subject: [PATCH 16/16] tests passed --- rowers/tests/testdata/testdata.csv.gz | Bin 11426 -> 11426 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/rowers/tests/testdata/testdata.csv.gz b/rowers/tests/testdata/testdata.csv.gz index 58b6f988584997088913c77aec29bc418eb0152f..b9981412cb1b0a6c6235ccf6db153fa6e7cd3489 100644 GIT binary patch delta 15 WcmZ1!xhRrNzMF&NUCBnaIXVC>wgs2~ delta 15 WcmZ1!xhRrNzMF%CDSso|9322C6$GaM