From e70bd6a627b492516c0c52cb120f843408e9af90 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Thu, 14 Feb 2019 13:52:47 +0100 Subject: [PATCH 01/18] empty file with requirements --- rowers/tests/test_permissions.py | 87 ++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 rowers/tests/test_permissions.py diff --git a/rowers/tests/test_permissions.py b/rowers/tests/test_permissions.py new file mode 100644 index 00000000..df1228d1 --- /dev/null +++ b/rowers/tests/test_permissions.py @@ -0,0 +1,87 @@ +from statements import * + +# set up + +## Users - Pro, Basic, Coach & Self Coach + +## Couple of workouts + +## TeamPro, TeamCoach, TeamSelfCoach + +# Requirements + +## Low level + +## Coach can have any number of groups + +## Basic athletes can be member of Coach led group + +## Coach can create planned sessions and team planned sessions + +## Self coach can create one group + +## Self coach cannot create more than one group + +## Pro users (and higher) can join group led by other Pro (or higher) user + +## Self Coach can create planned sessions and team planned sessions + +## Pro can have one group + +## Pro cannot create more than one group + +## Pro cannot create planned sessions or team planned sessions + +## Basic cannot join groups led by Pro or Self Coach + +## Basic can join group led by Coach + +# View based + +## Coach can have any number of groups + +## Basic athletes can be member of Coach led group + +## Coach can create planned sessions and team planned sessions + +## Coach can edit on behalf of athlete + +## Coach can run analytics for athlete + +## Coach can upload on behalf of athlete + +## Coach can edit athlete's workout + +## Self coach can create one group + +## Self coach cannot create more than one group + +## Pro users (and higher) can join group led by other Pro (or higher) user + +## Self Coach can create planned sessions and team planned sessions + +## Self Coach cannot edit on behalf of athlete + +## Self Coach cannot run analytics on behalf of athlete + +## Self Coach cannot upload on behalf of athlete + +## Pro can have one group + +## Pro cannot create more than one group + +## Pro cannot create planned sessions or team planned sessions + +## Pro can create planned sessions and team planned sessions + +## Pro cannot edit on behalf of athlete + +## Pro cannot run analytics on behalf of athlete + +## Basic cannot join groups from Pro or Self Coach users (redirects to paid plans) + +## Pro users can see team members' workout, but not edit + +## Self Coach users can see team members' workout, but not edit + +## Basic users can see team members' workout, but not edit From 95afb04dd291ce50af40f5a547515f0c476b3192 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Thu, 14 Feb 2019 17:45:19 +0100 Subject: [PATCH 02/18] it's a start --- rowers/models.py | 11 +++- rowers/tests/test_permissions.py | 108 ++++++++++++++++++++++++------- 2 files changed, 94 insertions(+), 25 deletions(-) diff --git a/rowers/models.py b/rowers/models.py index 6846f06f..82bad1d8 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -30,7 +30,7 @@ from django.utils import timezone import pandas as pd from dateutil import parser import datetime -from django.core.exceptions import ValidationError + from rowers.rows import validate_file_extension from collections import OrderedDict from timezonefinder import TimezoneFinder @@ -313,6 +313,13 @@ class C2WorldClassAgePerformance(models.Model): return thestring +def is_not_basic(user): + if user.rower.rowerplan == 'basic': + raise ValidationError( + "Basic user cannot be team manager" + ) + + # For future Team functionality class Team(models.Model): choices = ( @@ -327,7 +334,7 @@ class Team(models.Model): name = models.CharField(max_length=150,unique=True,verbose_name='Team Name') notes = models.CharField(blank=True,max_length=200,verbose_name='Team Purpose') - manager = models.ForeignKey(User, null=True) + manager = models.ForeignKey(User, null=True, validators=[is_not_basic]) private = models.CharField(max_length=30,choices=choices,default='open', verbose_name='Team Type') diff --git a/rowers/tests/test_permissions.py b/rowers/tests/test_permissions.py index df1228d1..6ca94c97 100644 --- a/rowers/tests/test_permissions.py +++ b/rowers/tests/test_permissions.py @@ -1,40 +1,102 @@ from statements import * +from django.utils import timezone +nu = datetime.datetime.now(tz=timezone.utc) # set up -## Users - Pro, Basic, Coach & Self Coach +import rowers.teams as teams +import rowers.plannedsessions as plannedsessions -## Couple of workouts -## TeamPro, TeamCoach, TeamSelfCoach +@override_settings(TESTING=True) +class PermissionsBasicsTests(TestCase): + def setUp: + self.c = Client() + ## Users - Pro, Basic, Coach & Self Coach -# Requirements + self.ucoach = UserFactory(username='coachuser') + self.rcoach = Rower.objects.create(user=self.ucoach, + birthdate=faker.profile()['birthdate'], + gdproptin=True,gdproptindate=timezone.now(), + rowerplan='coach') -## Low level + self.ucoach_workouts = WorkoutFactory.create_batch(5, user=self.rcoach) + self.factory = RequestFactory() + self.password = faker.word() + self.ucoach.set_password(self.password) + self.ucoach.save() + + self.uplan = UserFactory(username='planuser') + self.rplan = Rower.objects.create(user=self.uplan, + birthdate=faker.profile()['birthdate'], + gdproptin=True,gdproptindate=timezone.now(), + rowerplan='plan') -## Coach can have any number of groups + self.uselfplan_workouts = WorkoutFactory.create_batch(5, user=self.rselfplan) + self.factory = RequestFactory() + self.password = faker.word() + self.uselfplan.set_password(self.password) + self.uselfplan.save() + + self.upro = UserFactory(username='prouser') + self.rpro = Rower.objects.create(user=self.upro, + birthdate=faker.profile()['birthdate'], + gdproptin=True,gdproptindate=timezone.now(), + rowerplan='pro') -## Basic athletes can be member of Coach led group + self.upro_workouts = WorkoutFactory.create_batch(5, user=self.rpro) + self.factory = RequestFactory() + self.password = faker.word() + self.upro.set_password(self.password) + self.upro.save() -## Coach can create planned sessions and team planned sessions + self.ubasic = UserFactory(username='basicuser') + self.rbasic = Rower.objects.create(user=self.ubasic, + birthdate=faker.profile()['birthdate'], + gdproptin=True,gdproptindate=timezone.now(), + rowerplan='basic') -## Self coach can create one group + self.ubasic_workouts = WorkoutFactory.create_batch(5, user=self.rbasic) + self.factory = RequestFactory() + self.password = faker.word() + self.ubasic.set_password(self.password) + self.ubasic.save() + + + + ## TeamPro, TeamCoach, TeamSelfCoach -## Self coach cannot create more than one group + # Requirements + + ## Low level + + ## Coach can have any number of groups + + ## Basic athletes can be member of Coach led group + + ## Coach can create planned sessions and team planned sessions + + ## Self coach can create one group + + ## Self coach cannot create more than one group + + ## Pro users (and higher) can join group led by other Pro (or higher) user + + ## Self Coach can create planned sessions and team planned sessions + + ## Pro can have one group + + ## Pro cannot create more than one group + + ## Pro cannot create planned sessions or team planned sessions + + ## Basic cannot join groups led by Pro or Self Coach + + ## Basic can join group led by Coach -## Pro users (and higher) can join group led by other Pro (or higher) user - -## Self Coach can create planned sessions and team planned sessions - -## Pro can have one group - -## Pro cannot create more than one group - -## Pro cannot create planned sessions or team planned sessions - -## Basic cannot join groups led by Pro or Self Coach - -## Basic can join group led by Coach + ## Basic cannot manage a group + def test_basic_groupmanager(self): + self.assertRaises ... # View based From 90f1638675303e361a145693c85a131624e3b2e5 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Thu, 14 Feb 2019 22:33:07 +0100 Subject: [PATCH 03/18] passing 1 test --- rowers/models.py | 10 +++++++++- rowers/tests/test_permissions.py | 16 +++++++++++----- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/rowers/models.py b/rowers/models.py index dceeb586..687bcaa6 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -334,7 +334,7 @@ class Team(models.Model): name = models.CharField(max_length=150,unique=True,verbose_name='Team Name') notes = models.CharField(blank=True,max_length=200,verbose_name='Team Purpose') - manager = models.ForeignKey(User, null=True, validators=[is_not_basic]) + manager = models.ForeignKey(User, null=True,) # validators=[is_not_basic]) private = models.CharField(max_length=30,choices=choices,default='open', verbose_name='Team Type') @@ -343,6 +343,14 @@ class Team(models.Model): def __unicode__(self): return self.name + def save(self, *args, **kwargs): + manager = self.manager + if manager.rower.rowerplan == 'basic': + raise ValidationError( + "Basic user cannot be team manager" + ) + + class TeamForm(ModelForm): class Meta: model = Team diff --git a/rowers/tests/test_permissions.py b/rowers/tests/test_permissions.py index 6ca94c97..88f64e22 100644 --- a/rowers/tests/test_permissions.py +++ b/rowers/tests/test_permissions.py @@ -10,7 +10,7 @@ import rowers.plannedsessions as plannedsessions @override_settings(TESTING=True) class PermissionsBasicsTests(TestCase): - def setUp: + def setUp(self): self.c = Client() ## Users - Pro, Basic, Coach & Self Coach @@ -32,11 +32,11 @@ class PermissionsBasicsTests(TestCase): gdproptin=True,gdproptindate=timezone.now(), rowerplan='plan') - self.uselfplan_workouts = WorkoutFactory.create_batch(5, user=self.rselfplan) + self.uplan_workouts = WorkoutFactory.create_batch(5, user=self.rplan) self.factory = RequestFactory() self.password = faker.word() - self.uselfplan.set_password(self.password) - self.uselfplan.save() + self.uplan.set_password(self.password) + self.uplan.save() self.upro = UserFactory(username='prouser') self.rpro = Rower.objects.create(user=self.upro, @@ -96,7 +96,13 @@ class PermissionsBasicsTests(TestCase): ## Basic cannot manage a group def test_basic_groupmanager(self): - self.assertRaises ... + with self.assertRaises(ValidationError): + team = Team.objects.create( + name = 'ThisTeamShouldNotExist', + notes = faker.text(), + manager = self.ubasic, + private = 'open', + viewing = 'allmembers') # View based From 9bb7c3bef21940dff469f6027a9bcdfedb4712ff Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Fri, 15 Feb 2019 14:51:15 +0100 Subject: [PATCH 04/18] test writing (initial set) complete might need to add permissions for viewin --- rowers/models.py | 58 +- rowers/tests/test_aavirtualevents.py | 18 +- rowers/tests/test_permissions.py | 822 +++++++++++++++++++++++++- rowers/tests/test_plans.py | 4 +- rowers/tests/testdata/testdata.csv.gz | Bin 11457 -> 11426 bytes rowers/views/teamviews.py | 4 +- rowers/views/workoutviews.py | 2 +- 7 files changed, 859 insertions(+), 49 deletions(-) diff --git a/rowers/models.py b/rowers/models.py index 687bcaa6..7f7d5300 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -349,6 +349,15 @@ class Team(models.Model): raise ValidationError( "Basic user cannot be team manager" ) + + if manager.rower.rowerplan in ['plan','pro']: + otherteams = Team.objects.filter(manager=manager) + if len(otherteams) >= 1: + raise ValidationError( + "Pro and Self-Coach users cannot have more than one team" + ) + + super(Team, self).save(*args,**kwargs) class TeamForm(ModelForm): @@ -844,6 +853,7 @@ class Rower(models.Model): def clean_email(self): return self.user.email.lower() + class DeactivateUserForm(forms.ModelForm): class Meta: model = User @@ -856,13 +866,31 @@ class DeleteUserForm(forms.ModelForm): class Meta: model = User fields = [] + + +from django.db.models.signals import m2m_changed -@receiver(models.signals.post_save,sender=Rower) -def auto_delete_teams_on_change(sender, instance, **kwargs): - if instance.rowerplan != 'coach': - teams = Team.objects.filter(manager=instance.user) - for team in teams: - team.delete() +def check_teams_on_change(sender, **kwargs): + instance = kwargs.pop('instance', None) + action = kwargs.pop('action', None) + pk_set = kwargs.pop('pk_set',None) + if action == 'pre_add' and instance.rowerplan=='basic': + for id in pk_set: + team = Team.objects.get(id=id) + if team.manager.rower.rowerplan not in ['coach']: + raise ValidationError( + "You cannot join a team led by a Pro or Self-Coach user" + ) + +m2m_changed.connect(check_teams_on_change, sender=Rower.team.through) + + +#@receiver(models.signals.post_save,sender=Rower) +#def auto_delete_teams_on_change(sender, instance, **kwargs): +# if instance.rowerplan != 'coach': +# teams = Team.objects.filter(manager=instance.user) +# for team in teams: +# team.delete() from rowers.metrics import axlabels favchartlabelsx = axlabels.copy() @@ -1149,6 +1177,12 @@ class TrainingPlan(models.Model): return stri def save(self, *args, **kwargs): + manager = self.manager + if manager.rowerplan in ['basic','pro']: + raise ValidationError( + "Basic user cannot have a training plan" + ) + if self.enddate < self.startdate: startdate = self.startdate enddate = self.enddate @@ -1194,6 +1228,7 @@ class TrainingPlan(models.Model): else: createmacrofillers(self) + class TrainingPlanForm(ModelForm): class Meta: model = TrainingPlan @@ -1563,6 +1598,8 @@ class TrainingMacroCycle(models.Model): meso.save() else: createmesofillers(self) + + class TrainingMacroCycleForm(ModelForm): class Meta: @@ -1649,6 +1686,7 @@ class TrainingMesoCycle(models.Model): else: createmicrofillers(self) + class TrainingMicroCycle(models.Model): plan = models.ForeignKey(TrainingMesoCycle) name = models.CharField(max_length=150,blank=True) @@ -1863,6 +1901,14 @@ class PlannedSession(models.Model): def save(self, *args, **kwargs): if self.sessionvalue <= 0: self.sessionvalue = 1 + + manager = self.manager + if self.sessiontype not in ['race','indoorrace']: + if manager.rower.rowerplan in ['basic','pro']: + raise ValidationError( + "Basic user cannot be team manager" + ) + # sort units if self.sessionmode == 'distance': diff --git a/rowers/tests/test_aavirtualevents.py b/rowers/tests/test_aavirtualevents.py index 73ad8504..2ca74dbb 100644 --- a/rowers/tests/test_aavirtualevents.py +++ b/rowers/tests/test_aavirtualevents.py @@ -69,12 +69,14 @@ class VirtualEventViewTest(TestCase): yesterday = nu-datetime.timedelta(days=1) tomorrow = nu+datetime.timedelta(days=1) nextweek = nu+datetime.timedelta(days=7) + intwoweeks = nu+datetime.timedelta(days=14) lastweek = nu-datetime.timedelta(days=7) self.yesterday = yesterday self.tomorrow = tomorrow self.nextweek = nextweek self.lastweek = lastweek + self.intwoweeks = intwoweeks # erg races @@ -396,8 +398,8 @@ class VirtualEventViewTest(TestCase): 'registration_form':'deadline', 'registration_closure_0': self.nextweek.strftime('%Y-%m-%d'), 'registration_closure_1': self.nextweek.strftime('%H:%M:%S'), - 'evaluation_closure_0': self.nextweek.strftime('%Y-%m-%d'), - 'evaluation_closure_1': self.nextweek.strftime('%H:%M:%S'), + 'evaluation_closure_0': self.intwoweeks.strftime('%Y-%m-%d'), + 'evaluation_closure_1': self.intwoweeks.strftime('%H:%M:%S'), 'contact_phone': '', 'contact_email': self.u.email, 'timezone': 'UTC' @@ -440,8 +442,8 @@ class VirtualEventViewTest(TestCase): 'registration_form':'deadline', 'registration_closure_0': self.nextweek.strftime('%Y-%m-%d'), 'registration_closure_1': self.nextweek.strftime('%H:%M:%S'), - 'evaluation_closure_0': self.nextweek.strftime('%Y-%m-%d'), - 'evaluation_closure_1': self.nextweek.strftime('%H:%M:%S'), + 'evaluation_closure_0': self.intwoweeks.strftime('%Y-%m-%d'), + 'evaluation_closure_1': self.intwoweeks.strftime('%H:%M:%S'), 'contact_phone': '', 'contact_email': self.u.email, 'timezone': 'UTC' @@ -485,8 +487,8 @@ class VirtualEventViewTest(TestCase): 'registration_form':'deadline', 'registration_closure_0': self.nextweek.strftime('%Y-%m-%d'), 'registration_closure_1': self.nextweek.strftime('%H:%M:%S'), - 'evaluation_closure_0': self.nextweek.strftime('%Y-%m-%d'), - 'evaluation_closure_1': self.nextweek.strftime('%H:%M:%S'), + 'evaluation_closure_0': self.intwoweeks.strftime('%Y-%m-%d'), + 'evaluation_closure_1': self.intwoweeks.strftime('%H:%M:%S'), 'contact_phone': '', 'contact_email': self.u.email, } @@ -522,8 +524,8 @@ class VirtualEventViewTest(TestCase): 'registration_form':'deadline', 'registration_closure_0': self.nextweek.strftime('%Y-%m-%d'), 'registration_closure_1': self.nextweek.strftime('%H:%M:%S'), - 'evaluation_closure_0': self.nextweek.strftime('%Y-%m-%d'), - 'evaluation_closure_1': self.nextweek.strftime('%H:%M:%S'), + 'evaluation_closure_0': self.intwoweeks.strftime('%Y-%m-%d'), + 'evaluation_closure_1': self.intwoweeks.strftime('%H:%M:%S'), 'contact_phone': '', 'contact_email': self.u.email, } diff --git a/rowers/tests/test_permissions.py b/rowers/tests/test_permissions.py index 88f64e22..8c6e2618 100644 --- a/rowers/tests/test_permissions.py +++ b/rowers/tests/test_permissions.py @@ -2,6 +2,8 @@ from statements import * from django.utils import timezone nu = datetime.datetime.now(tz=timezone.utc) +from django.db import transaction + # set up import rowers.teams as teams @@ -50,6 +52,30 @@ class PermissionsBasicsTests(TestCase): self.upro.set_password(self.password) self.upro.save() + self.uplan2 = UserFactory(username='planuser2') + self.rplan2 = Rower.objects.create(user=self.uplan2, + birthdate=faker.profile()['birthdate'], + gdproptin=True,gdproptindate=timezone.now(), + rowerplan='plan') + + self.uplan2_workouts = WorkoutFactory.create_batch(5, user=self.rplan2) + self.factory = RequestFactory() + self.password = faker.word() + self.uplan2.set_password(self.password) + self.uplan2.save() + + self.upro2 = UserFactory(username='prouser2') + self.rpro2 = Rower.objects.create(user=self.upro2, + birthdate=faker.profile()['birthdate'], + gdproptin=True,gdproptindate=timezone.now(), + rowerplan='pro') + + self.upro2_workouts = WorkoutFactory.create_batch(5, user=self.rpro2) + self.factory = RequestFactory() + self.password = faker.word() + self.upro2.set_password(self.password) + self.upro2.save() + self.ubasic = UserFactory(username='basicuser') self.rbasic = Rower.objects.create(user=self.ubasic, birthdate=faker.profile()['birthdate'], @@ -64,35 +90,169 @@ class PermissionsBasicsTests(TestCase): - ## TeamPro, TeamCoach, TeamSelfCoach + ## TeamPro, TeamCoach, TeamSelfCoach + + self.teampro = Team.objects.create( + name=faker.word(), + notes=faker.text(), + manager=self.upro2) + + self.teamplan = Team.objects.create( + name=faker.word(), + notes=faker.text(), + manager=self.uplan2) + + self.teamcoach = Team.objects.create( + name=faker.word(), + notes=faker.text(), + manager=self.ucoach) # Requirements ## Low level ## Coach can have any number of groups + def test_plan_groupmanager(self): + team1 = Team.objects.create( + name = 'FirstTeam', + notes = faker.text(), + manager = self.ucoach, + ) + + self.assertEqual(team1.manager,self.ucoach) + + team2 = Team.objects.create( + name = 'SecondTeam', + notes = faker.text(), + manager = self.ucoach, + ) + + self.assertEqual(team2.manager,self.ucoach) + + + team3 = Team.objects.create( + name = 'SecondTeam', + notes = faker.text(), + manager = self.ucoach, + ) + + self.assertEqual(team3.manager,self.ucoach) + + ## Basic athletes can be member of Coach led group + def test_add_coach(self): + self.rbasic.team.add(self.teamcoach) + self.assertIn(self.teamcoach,self.rbasic.team.all()) - ## Coach can create planned sessions and team planned sessions ## Self coach can create one group - ## Self coach cannot create more than one group + def test_plan_groupmanager(self): + team1 = Team.objects.create( + name = 'FirstTeam', + notes = faker.text(), + manager = self.uplan, + ) + + self.assertEqual(team1.manager,self.uplan) + + with self.assertRaises(ValidationError): + team2 = Team.objects.create( + name = 'SecondTeam', + notes = faker.text(), + manager = self.uplan, + ) + + ## Pro users (and higher) can join group led by other Pro (or higher) user + def test_add_proplan_pro_or_plan(self): + self.rpro.team.add(self.teamplan) + self.assertIn(self.teamplan,self.rpro.team.all()) + + self.rpro.team.add(self.teampro) + self.assertIn(self.teampro,self.rpro.team.all()) + + self.rplan.team.add(self.teamplan) + self.assertIn(self.teamplan,self.rplan.team.all()) + + self.rplan.team.add(self.teampro) + self.assertIn(self.teampro,self.rplan.team.all()) + + self.rcoach.team.add(self.teamplan) + self.assertIn(self.teamplan,self.rcoach.team.all()) + + self.rcoach.team.add(self.teampro) + self.assertIn(self.teampro,self.rcoach.team.all()) + - ## Self Coach can create planned sessions and team planned sessions + + + ## Coach can create planned sessions and team planned sessions + ## Self Coach and higher can create planned sessions and team planned sessions + def test_plan_create_session(self): + ps = PlannedSession.objects.create( + manager=self.uplan, + name=faker.word(), + comment=faker.text() + ) + self.assertEqual(ps.manager,self.uplan) + + def test_coach_create_session(self): + ps = PlannedSession.objects.create( + manager=self.ucoach, + name=faker.word(), + comment=faker.text() + ) + self.assertEqual(ps.manager,self.ucoach) ## Pro can have one group - ## Pro cannot create more than one group + def test_pro_groupmanager(self): + team1 = Team.objects.create( + name = 'FirstTeam', + notes = faker.text(), + manager = self.upro, + ) + + self.assertEqual(team1.manager,self.upro) + + with self.assertRaises(ValidationError): + team2 = Team.objects.create( + name = 'SecondTeam', + notes = faker.text(), + manager = self.upro, + ) - ## Pro cannot create planned sessions or team planned sessions + + ## Pro or Basic cannot create planned sessions or team planned sessions + def test_pro_create_plannedsession(self): + with self.assertRaises(ValidationError): + ps = PlannedSession.objects.create( + manager=self.upro, + name = faker.word(), + comment = faker.text() + ) + + def test_basic_create_plannedsession(self): + with self.assertRaises(ValidationError): + ps = PlannedSession.objects.create( + manager=self.ubasic, + name = faker.word(), + comment = faker.text() + ) ## Basic cannot join groups led by Pro or Self Coach + def test_add_basic_pro_or_plan(self): + with transaction.atomic(): + with self.assertRaises(ValidationError): + self.rbasic.team.add(self.teamplan) + + with transaction.atomic(): + with self.assertRaises(ValidationError): + self.rbasic.team.add(self.teampro) - ## Basic can join group led by Coach ## Basic cannot manage a group def test_basic_groupmanager(self): @@ -104,52 +264,652 @@ class PermissionsBasicsTests(TestCase): private = 'open', viewing = 'allmembers') + ## On downgrade, Coach users lose all but their oldest team + # View based +@override_settings(TESTING=True) +class PermissionsViewTests(TestCase): + def setUp(self): + self.c = Client() + ## Users - Pro, Basic, Coach & Self Coach -## Coach can have any number of groups + self.ucoach = UserFactory(username='coachuser') + self.rcoach = Rower.objects.create(user=self.ucoach, + birthdate=faker.profile()['birthdate'], + gdproptin=True,gdproptindate=timezone.now(), + rowerplan='coach') -## Basic athletes can be member of Coach led group + self.ucoach_workouts = WorkoutFactory.create_batch(5, user=self.rcoach) + self.factory = RequestFactory() + self.ucoachpassword = faker.word() + self.ucoach.set_password(self.ucoachpassword) + self.ucoach.save() + + self.uplan = UserFactory(username='planuser') + self.rplan = Rower.objects.create(user=self.uplan, + birthdate=faker.profile()['birthdate'], + gdproptin=True,gdproptindate=timezone.now(), + rowerplan='plan') -## Coach can create planned sessions and team planned sessions + self.uplan_workouts = WorkoutFactory.create_batch(5, user=self.rplan) + self.factory = RequestFactory() + self.uplanpassword = faker.word() + self.uplan.set_password(self.uplanpassword) + self.uplan.save() + + self.upro = UserFactory(username='prouser') + self.rpro = Rower.objects.create(user=self.upro, + birthdate=faker.profile()['birthdate'], + gdproptin=True,gdproptindate=timezone.now(), + rowerplan='pro') -## Coach can edit on behalf of athlete + self.upro_workouts = WorkoutFactory.create_batch(5, user=self.rpro) + self.factory = RequestFactory() + self.upropassword = faker.word() + self.upro.set_password(self.upropassword) + self.upro.save() -## Coach can run analytics for athlete + self.uplan2 = UserFactory(username='planuser2') + self.rplan2 = Rower.objects.create(user=self.uplan2, + birthdate=faker.profile()['birthdate'], + gdproptin=True,gdproptindate=timezone.now(), + rowerplan='plan') -## Coach can upload on behalf of athlete + self.uplan2_workouts = WorkoutFactory.create_batch(5, user=self.rplan2) + self.factory = RequestFactory() + self.uplan2password = faker.word() + self.uplan2.set_password(self.uplan2password) + self.uplan2.save() + + self.upro2 = UserFactory(username='prouser2') + self.rpro2 = Rower.objects.create(user=self.upro2, + birthdate=faker.profile()['birthdate'], + gdproptin=True,gdproptindate=timezone.now(), + rowerplan='pro') -## Coach can edit athlete's workout + self.upro2_workouts = WorkoutFactory.create_batch(5, user=self.rpro2) + self.factory = RequestFactory() + self.upro2password = faker.word() + self.upro2.set_password(self.upro2password) + self.upro2.save() -## Self coach can create one group + self.ubasic = UserFactory(username='basicuser') + self.rbasic = Rower.objects.create(user=self.ubasic, + birthdate=faker.profile()['birthdate'], + gdproptin=True,gdproptindate=timezone.now(), + rowerplan='basic') -## Self coach cannot create more than one group + self.ubasic_workouts = WorkoutFactory.create_batch(5, user=self.rbasic) + self.factory = RequestFactory() + self.ubasicpassword = faker.word() + self.ubasic.set_password(self.ubasicpassword) + self.ubasic.save() + + + + ## TeamPro, TeamCoach, TeamSelfCoach -## Pro users (and higher) can join group led by other Pro (or higher) user + self.teampro = Team.objects.create( + name=faker.word(), + notes=faker.text(), + manager=self.upro2) -## Self Coach can create planned sessions and team planned sessions + self.teamplan = Team.objects.create( + name=faker.word(), + notes=faker.text(), + manager=self.uplan2) -## Self Coach cannot edit on behalf of athlete + self.teamcoach = Team.objects.create( + name=faker.word(), + notes=faker.text(), + manager=self.ucoach) -## Self Coach cannot run analytics on behalf of athlete -## Self Coach cannot upload on behalf of athlete + ## Coach can have any number of groups + def test_coach_groups_create(self): + login = self.c.login(username=self.ucoach.username, password=self.ucoachpassword) + self.assertTrue(login) + + url = reverse('team_create_view') -## Pro can have one group + response = self.c.get(url) + self.assertTrue(response.status_code,200) -## Pro cannot create more than one group + # Create 1st new team + form_data = { + 'name': faker.word(), + 'notes': faker.text(), + 'private': 'open', + 'viewing': 'allmembers' + } -## Pro cannot create planned sessions or team planned sessions + form = TeamForm(form_data) + if not form.is_valid(): + print form.errors + + self.assertTrue(form.is_valid()) -## Pro can create planned sessions and team planned sessions + expected_url = reverse('rower_teams_view') -## Pro cannot edit on behalf of athlete + response = self.c.post(url,form_data,follow=True) + self.assertRedirects(response, + expected_url=expected_url, + status_code=302,target_status_code=200) -## Pro cannot run analytics on behalf of athlete -## Basic cannot join groups from Pro or Self Coach users (redirects to paid plans) + # Create 2nd new team + form_data = { + 'name': faker.word(), + 'notes': faker.text(), + 'private': 'open', + 'viewing': 'allmembers' + } -## Pro users can see team members' workout, but not edit + form = TeamForm(form_data) + if not form.is_valid(): + print form.errors + + self.assertTrue(form.is_valid()) -## Self Coach users can see team members' workout, but not edit + expected_url = reverse('rower_teams_view') + + response = self.c.post(url,form_data,follow=True) + self.assertRedirects(response, + expected_url=expected_url, + status_code=302,target_status_code=200) + + ## Basic athletes can be member of Coach led group + + ## Coach can create planned sessions and team planned sessions + def test_coach_create_session(self): + login = self.c.login(username=self.ucoach.username, password=self.ucoachpassword) + self.assertTrue(login) + + url = reverse('plannedsession_create_view') + + startdate = nu.date() + enddate = (nu+datetime.timedelta(days=3)).date() + preferreddate = startdate + + response = self.c.get(url) + self.assertEqual(response.status_code,200) + + post_data = { + 'comment': faker.text(), + 'criterium': 'none', + 'enddate': enddate.strftime("%Y-%m-%d"), + 'preferreddate': preferreddate.strftime("%Y-%m-%d"), + 'startdate': startdate.strftime("%Y-%m-%d"), + 'sessionmode':'time', + 'sessiontype':'session', + 'sessionunit':'min', + 'sessionvalue': '60', + 'name': faker.word(), + } + + print 'posting to sessions/create' + + form = PlannedSessionForm(post_data) + self.assertTrue(form.is_valid()) + + response = self.c.post(url,post_data) + self.assertEqual(response.status_code,200) + + + + ## Coach can edit on behalf of athlete + def test_coach_edit_athlete_settings(self): + self.rbasic.team.add(self.teamcoach) + + login = self.c.login(username=self.ucoach.username, password=self.ucoachpassword) + self.assertTrue(login) + + url = reverse('rower_prefs_view',kwargs={'userid':self.ubasic.id}) + + response = self.c.get(url) + self.assertEqual(response.status_code,200) + + ## Coach can run analytics for athlete + @patch('rowers.dataprep.read_cols_df_sql', side_effect = mocked_read_df_cols_sql_multistats) + def test_coach_edit_athlete_analysis(self,mocked_df): + self.rbasic.team.add(self.teamcoach) + + login = self.c.login(username=self.ucoach.username, password=self.ucoachpassword) + self.assertTrue(login) + + + url = reverse('cumstats', + kwargs={ + 'theuser':self.ubasic.id, + } + ) + + response = self.c.get(url) + + self.assertEqual(response.status_code,200) + + + ## Coach can upload on behalf of athlete + @patch('rowers.dataprep.create_engine') + @patch('rowers.dataprep.getsmallrowdata_db',side_effect=mocked_getsmallrowdata_db) + def test_coach_edit_athlete_upload(self,mocked_sqlalchemy,mocked_getsmallrowdata_db): + self.rbasic.team.add(self.teamcoach) + + login = self.c.login(username=self.ucoach.username, password=self.ucoachpassword) + self.assertTrue(login) + + url = reverse('team_workout_upload_view') + + aantal = len(Workout.objects.filter(user=self.rbasic)) + + response = self.c.get(url) + self.assertEqual(response.status_code,200) + + filename = 'rowers/tests/testdata/testdata.csv' + f = open(filename,'rb') + file_data = {'file': f} + form_data = { + 'title':'test', + 'workouttype':'rower', + 'boattype':'1x', + 'notes':'aap noot mies', + 'make_plot':False, + 'upload_to_c2':False, + 'plottype':'timeplot', + 'file': f, + 'user': self.ubasic.id + } + + response = self.c.post(url, form_data, follow=True) + f.close() + + self.assertEqual(response.status_code,200) + + self.assertRedirects(response, + expected_url = url, + status_code=302,target_status_code=200) + + aantal2 = len(Workout.objects.filter(user=self.rbasic)) + + self.assertEqual(aantal2,aantal+1) + + ## Coach can edit athlete's workout + def test_coach_edit_athlete_workout(self): + self.rbasic.team.add(self.teamcoach) + + login = self.c.login(username=self.ucoach.username, password=self.ucoachpassword) + self.assertTrue(login) + + url = reverse('workout_edit_view', + kwargs={'id':encoder.encode_hex(self.ubasic_workouts[0].id)} + ) + + response = self.c.get(url) + self.assertEqual(response.status_code,200) + + ## Self coach can create one group + ## Self coach cannot create more than one group + def test_plan_groups_create(self): + login = self.c.login(username=self.uplan.username, password=self.uplanpassword) + self.assertTrue(login) + + url = reverse('team_create_view') + + response = self.c.get(url) + self.assertTrue(response.status_code,200) + + # Create 1st new team + form_data = { + 'name': faker.word(), + 'notes': faker.text(), + 'private': 'open', + 'viewing': 'allmembers' + } + + form = TeamForm(form_data) + if not form.is_valid(): + print form.errors + + self.assertTrue(form.is_valid()) + + expected_url = reverse('rower_teams_view') + + response = self.c.post(url,form_data,follow=True) + self.assertRedirects(response, + expected_url=expected_url, + status_code=302,target_status_code=200) + + + # Create 2nd new team - should redirect to paid plans + form_data = { + 'name': faker.word(), + 'notes': faker.text(), + 'private': 'open', + 'viewing': 'allmembers' + } + + form = TeamForm(form_data) + if not form.is_valid(): + print form.errors + + self.assertTrue(form.is_valid()) + + expected_url = reverse('paidplans') + + response = self.c.post(url,form_data,follow=True) + self.assertRedirects(response, + expected_url=expected_url, + status_code=302,target_status_code=200) + + + ## Self Coach can create planned sessions and team planned sessions + def test_plan_create_session(self): + login = self.c.login(username=self.uplan2.username, password=self.uplan2password) + self.assertTrue(login) + + url = reverse('plannedsession_create_view') + + startdate = nu.date() + enddate = (nu+datetime.timedelta(days=3)).date() + preferreddate = startdate + + response = self.c.get(url) + self.assertEqual(response.status_code,200) + + post_data = { + 'comment': faker.text(), + 'criterium': 'none', + 'enddate': enddate.strftime("%Y-%m-%d"), + 'preferreddate': preferreddate.strftime("%Y-%m-%d"), + 'startdate': startdate.strftime("%Y-%m-%d"), + 'sessionmode':'time', + 'sessiontype':'session', + 'sessionunit':'min', + 'sessionvalue': '60', + 'name': faker.word(), + } + + print 'posting to sessions/create' + + form = PlannedSessionForm(post_data) + self.assertTrue(form.is_valid()) + + response = self.c.post(url,post_data) + self.assertEqual(response.status_code,200) + + + + ## Self Coach cannot edit on behalf of athlete + def test_plan_edit_athlete_settings(self): + self.rbasic.team.add(self.teamplan) + + login = self.c.login(username=self.uplan2.username, password=self.uplan2password) + self.assertTrue(login) + + url = reverse('rower_prefs_view',kwargs={'userid':self.ubasic.id}) + + response = self.c.get(url) + self.assertEqual(response.status_code,404) + + ## Self Coach cannot run analytics on behalf of athlete + @patch('rowers.dataprep.read_cols_df_sql', side_effect = mocked_read_df_cols_sql_multistats) + def test_plan_edit_athlete_analysis(self,mocked_df): + self.rbasic.team.add(self.teamplan) + + login = self.c.login(username=self.uplan2.username, password=self.uplan2password) + self.assertTrue(login) + + + url = reverse('cumstats', + kwargs={ + 'theuser':self.ubasic.id, + } + ) + + response = self.c.get(url) + + self.assertEqual(response.status_code,404) + + ## Self Coach cannot upload on behalf of athlete + @patch('rowers.dataprep.create_engine') + @patch('rowers.dataprep.getsmallrowdata_db',side_effect=mocked_getsmallrowdata_db) + def test_plan_edit_athlete_upload(self,mocked_sqlalchemy,mocked_getsmallrowdata_db): + self.rbasic.team.add(self.teamplan) + + login = self.c.login(username=self.uplan2.username, password=self.uplan2password) + self.assertTrue(login) + + url = reverse('team_workout_upload_view') + + response = self.c.get(url) + self.assertEqual(response.status_code,404) + + + ## Pro can have one group + ## Pro cannot create more than one group + def test_pro_groups_create(self): + login = self.c.login(username=self.upro.username, password=self.upropassword) + self.assertTrue(login) + + url = reverse('team_create_view') + + response = self.c.get(url) + self.assertTrue(response.status_code,200) + + # Create 1st new team + form_data = { + 'name': faker.word(), + 'notes': faker.text(), + 'private': 'open', + 'viewing': 'allmembers' + } + + form = TeamForm(form_data) + if not form.is_valid(): + print form.errors + + self.assertTrue(form.is_valid()) + + expected_url = reverse('rower_teams_view') + + response = self.c.post(url,form_data,follow=True) + self.assertRedirects(response, + expected_url=expected_url, + status_code=302,target_status_code=200) + + + # Create 2nd new team - should redirect to paid plans + form_data = { + 'name': faker.word(), + 'notes': faker.text(), + 'private': 'open', + 'viewing': 'allmembers' + } + + form = TeamForm(form_data) + if not form.is_valid(): + print form.errors + + self.assertTrue(form.is_valid()) + + expected_url = reverse('paidplans') + + response = self.c.post(url,form_data,follow=True) + self.assertRedirects(response, + expected_url=expected_url, + status_code=302,target_status_code=200) + + + ## Pro cannot create planned sessions or team planned sessions + def test_pro_create_session(self): + login = self.c.login(username=self.upro2.username, password=self.upro2password) + self.assertTrue(login) + + url = reverse('plannedsession_create_view') + + startdate = nu.date() + enddate = (nu+datetime.timedelta(days=3)).date() + preferreddate = startdate + + response = self.c.get(url,follow=True) + self.assertEqual(response.status_code,200) + + expected_url = reverse('paidplans') + + self.assertRedirects(response, + expected_url=expected_url, + status_code=302,target_status_code=200) + + + ## Pro cannot edit on behalf of athlete + def test_pro_edit_athlete_settings(self): + self.rbasic.team.add(self.teampro) + + login = self.c.login(username=self.upro2.username, password=self.upro2password) + self.assertTrue(login) + + url = reverse('rower_prefs_view',kwargs={'userid':self.ubasic.id}) + + response = self.c.get(url) + self.assertEqual(response.status_code,404) + + ## Pro cannot run analytics on behalf of athlete + @patch('rowers.dataprep.read_cols_df_sql', side_effect = mocked_read_df_cols_sql_multistats) + def test_pro_edit_athlete_analysis(self,mocked_df): + self.rbasic.team.add(self.teampro) + + login = self.c.login(username=self.upro2.username, password=self.upro2password) + self.assertTrue(login) + + + url = reverse('cumstats', + kwargs={ + 'theuser':self.ubasic.id, + } + ) + + response = self.c.get(url) + + self.assertEqual(response.status_code,404) + + ## Self Coach cannot upload on behalf of athlete + @patch('rowers.dataprep.create_engine') + @patch('rowers.dataprep.getsmallrowdata_db',side_effect=mocked_getsmallrowdata_db) + def test_plan_edit_athlete_upload(self,mocked_sqlalchemy,mocked_getsmallrowdata_db): + self.rbasic.team.add(self.teamplan) + + login = self.c.login(username=self.uplan2.username, password=self.uplan2password) + self.assertTrue(login) + + url = reverse('team_workout_upload_view') + + response = self.c.get(url) + self.assertEqual(response.status_code,404) + + + ## Pro users can see team members' workout, but not edit + def test_coach_edit_athlete_workout(self,mocked_sqlalchemy,mocked_getsmallrowdata_db): + self.rbasic.team.add(self.proplan) + self.rpro2.team.add(self.proplan) + + login = self.c.login(username=self.upro2.username, password=self.upro2password) + self.assertTrue(login) + + url = reverse('workout_edit_view', + kwargs={'id':encoder.encode_hex(self.ubasic_workouts[0].id)} + ) + + response = self.c.get(url) + self.assertEqual(response.status_code,404) + + url = reverse('workout_view', + kwargs={'id':encoder.encode_hex(self.ubasic_workouts[0].id)} + ) + + response = self.c.get(url) + self.assertEqual(response.status_code,200) + + + ## Self Coach users can see team members' workout, but not edit + def test_coach_edit_athlete_workout(self,mocked_sqlalchemy,mocked_getsmallrowdata_db): + self.rbasic.team.add(self.teamplan) + + login = self.c.login(username=self.uplan2.username, password=self.uplan2password) + self.assertTrue(login) + + url = reverse('workout_edit_view', + kwargs={'id':encoder.encode_hex(self.ubasic_workouts[0].id)} + ) + + response = self.c.get(url) + self.assertEqual(response.status_code,200) + + url = reverse('workout_view', + kwargs={'id':encoder.encode_hex(self.ubasic_workouts[0].id)} + ) + + response = self.c.get(url) + self.assertEqual(response.status_code,200) + + + ## Basic users can see team members' workout, but not edit + def test_basic_edit_athlete_workout(self,mocked_sqlalchemy,mocked_getsmallrowdata_db): + self.rbasic.team.add(self.teamplan) + self.rplan2.team.add(self.teamplan) + + login = self.c.login(username=self.ubasic.username, password=self.ubasicpassword) + self.assertTrue(login) + + url = reverse('workout_edit_view', + kwargs={'id':encoder.encode_hex(self.uplan2_workouts[0].id)} + ) + + response = self.c.get(url) + self.assertEqual(response.status_code,404) + + url = reverse('workout_view', + kwargs={'id':encoder.encode_hex(self.uplan2_workouts[0].id)} + ) + + response = self.c.get(url) + self.assertEqual(response.status_code,200) + + ## Pro users (and higher) can join group led by other Pro (or higher) user + def test_team_member_request_pro_pro(self): + login = self.c.login(username=self.upro.username,password=self.upropassword) + self.assertTrue(login) + + url = reverse('team_requestmembership_view', + kwargs = { + 'teamid':self.teampro.id, + 'userid':self.upro.id + }) + + response = self.c.get(url,follow=True) + self.assertEqual(response.status_code,200) + + expected_url = reverse('team_view',kwargs={'id':self.teampro.id}) + + self.assertRedirects(response, + expected_url = expected_url, + status_code=302,target_status_code=200) + + ## Basic cannot join groups from Pro or Self Coach users (redirects to paid plans) + def test_team_member_request_basic_pro(self): + login = self.c.login(username=self.ubasic.username,password=self.ubasicpassword) + self.assertTrue(login) + + url = reverse('team_requestmembership_view', + kwargs = { + 'teamid':self.teampro.id, + 'userid':self.upro.id + }) + + response = self.c.get(url,follow=True) + self.assertEqual(response.status_code,200) + + expected_url = reverse('paidplans') + self.assertRedirects(response, + expected_url = expected_url, + status_code=302,target_status_code=200) -## Basic users can see team members' workout, but not edit diff --git a/rowers/tests/test_plans.py b/rowers/tests/test_plans.py index 51395c25..87f138c6 100644 --- a/rowers/tests/test_plans.py +++ b/rowers/tests/test_plans.py @@ -131,7 +131,7 @@ class TrainingPlanTest(TestCase): login = self.c.login(username=self.u.username, password=self.password) self.assertTrue(login) - url = '/rowers/sessions/create/' + url = reverse('plannedsession_create_view') startdate = nu.date() enddate = (nu+datetime.timedelta(days=3)).date() @@ -1051,6 +1051,8 @@ class PlannedSessionsView(TestCase): manager = self.u, ) + self.team.save() + self.r.team.add(self.team) self.r2.team.add(self.team) self.r.save() diff --git a/rowers/tests/testdata/testdata.csv.gz b/rowers/tests/testdata/testdata.csv.gz index b35d3210b753187692b309cae0d57a22a5a32737..3c6cc8df3385d74d5ae9969f9e7fe46cce2ec6f9 100644 GIT binary patch literal 11426 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≧H45L=aOTS%qgk;V1wi1PKj;iI4c2}S5$G_DS@M}T$s}xDsfQ_sca9yNYo=a zPCDCE?WUOLbsW;&j4N!2yBlA32(1x-az>deb;w6}&P@rx&*e-V&cRRt648*#_75Dy zT4g1C6i+RsUPhh^ASdTCo4q`y<&{)rrG&-KlAJum)%)=7#Ml|3X)%q% z$m;O-#!U@GOuUfF{g?2NW=Lgw2v)Bbg$0Klu9ZXr~pf z%YpfZTG`$e=~bC3%1urd&FKqjapk_V>r>De|-oD$0_Pzw-=hE%tE=yyMaABBi|M&FiaP37cU z(Y17#+*a*33V;R z)xD!E#a9 zpI2U%mQ+8l!&aPar|&{iKDuAb;rHu2Li9`C>~2?InU`)#95vf{sEZ}Y1|mRN^+-f;H`vLzPeU|`aXinILeml z_6@Umy|8HeT$yUSOVqKQB{>alNP_$thPdvsxiobi_d&GnRS}4kTuspSeGpva;VhX4 z!MdKqOVo8c2M%}-zc&@wilSPs2*wYZ>YShP*3`Zuxhq0HH_(sv;Hj$T2KsIft{W$~ z!kOxJ4RiW5RFNTR%q3E*XD%I&%F>Kil<7Odciz=cho2w)-bXUM7oyIF?^=jaGJS6e z=M-1j3(gYQv~icXu~a_r&9z~sn{VIn43~-1a~y|ynmEplWzJr(mKnz|?VPBl{c5_`=nIkf zmg>b!Z6tRU&+o0_oG8*$;2@T|Qi3B#by<;*k)CEpzG{S!E!-8<&fACVGQvRX8C zLid3B8q2TL)O&dt*cb!~TZeb>M5V1eKR&yyS(n@L{o#4udARu2WkvdN=i%YDkV+fZ zqw0r7moG9@9ctOwIL#|;+$PX949TKhRwO@bnU0Ox;9JCZVc&Ka&d252>F_Jgu98{j zN@e4mu<^Ft>dz{&A>EtAnQb}e+th6md{44VQBWsxJjJ$bs7`UTB=q-W00%VZ2 zh-}a_UIu>h&LZE(%M99w#bHbZxv^9O%T#KLjp$8Be4fuvnzL%Mg1vowC8V@T4ts069FTfdKzA#eG< z0^oKe4wvi7$0Xdouo&V$Yb(CxvM1pzGBz20VX+{IY)EbU2inBtSGh1UoOc#4bRUXK z3!Q08==SbR&cXia*;5LUEFUJL{XM0uXNH!$f9-?&LNGQ4nw`s&VahT4P z;=U1E=ptu}NKOmpkIu-Wqck2Pjz&1VHaPa@rIN>pqacZFNVZ)>jlSJmbR8}fYG@cc zujYUQ?L5UH__eB^)U@WM4}p!Z68r}6{x`R_8D!2CK!Me$z~7f**MM1u*k*i-}J^ z=_X!HZ+{q+Te<$)&N=RNh=|)*W{_6w?HsP&a)Zd>l|?rs+kS%A_pOtKr!aLLbbodA zY`>iF?1IO1-?x$b=e}24`@s_T5t~QKI^{d zhGg4WSh?Zx!ldc&mR-=y^4`T#O|D)clrJmi&&B?u!Sk-uoNf9pL%Ka-FB-UEU%;Zq0na)7mAJ;ls6= z!X&yO_3bUVukWcp6I~9g@hN6s<*a<}ElV-Xvr_FHoh$ptrHgHiS?$6Ef)w7K#wg`z zS9AD*)g1)_#3Z^Q_3bZk5^Zr0{sPa#EO5l}$<;pLk;UaP9DiITnAF>hTl7wEmdLW) zbPo~OT-Xi}yu9d!)VIUv^(szqr)4H}thp-a@cNZk=tiueDLgN$`y%t>`gup^;c+>} zNs$)DS+h4ckBOnkKIic6CLxJzNPYVYwuLO2q9MzbUihAKYoDvJ9_O6%k!Lk_cbpy(JRyl}NPW8umiAn4c|$bRy6E=*yutyy zx}3WCZ)(hvdQ0Vwk8$RsSJzhQ?r4Zjj-zRnkp&RWsa2Jr_=l{#bwW&{8`9W5W7bs= z;GJSAX#;l5Ykc#b%B`ze;^_DU>5im0gEF^6yOzT4)QmTH6}H=sp3b8^tUuH-Reu`W zU9hgbj+K|>RSh@ISl8OYNT<&FoaO5Jug&N3sq62plX_g_eO@tkv`!F?4sMG3Uu#H< zZbYXL2lD8_hyK(P!%#T3u4~~4Nn}I1m!UH!Cs##)$sDhuBlCP5LqjJ=l&6{KD?}dc zbL)Q`m*e&`MCR1+=+B#>41TTg4=l+X6Ozb=bZ`P{y*Z_mfp%ejMaU*F!i0cx#yqk{Yr*^us4>C9^|rso#b|I`R2 z$~O~)&OV~Mw-9_=dgO2IS*6F$I}*9kxY9SD_0MDFT?`Z1knVkHpez3O7Il9~V+2#x w5V5Hb^v`{+m2+%aeUWT@R0@@t-4%V^czmKoJ`|zXw{o|*PKYYm8F_~jqIeE#(3mm0$Nzx(iwz4Vv-FZ}6OAHIG0 zLBIQgm;Ch0??3*)Px<|?e*XN`*MIzxFZtc4|9QcG{LhCkUw+a%zJrXHpT7F~&-js_ z{^QHbw~+QrLtw&w_37syzU=?_^~aySe2|n2(yu>%{L9O~z5MW}FaP!WxAr*SJMiDX zeDmoqFQ5N<{NX=7eg5@{Ov;Z>E|z4!Y?ly{8zvI`TIYa#pj!%y=O@Q0ZCBjU$Le9llG z5&s?;f9d@(8-81d-k9;y()B@nVVt?e9z%NI|DQR`&>ktwXJ{>#N5QKo2A}_n=s4yx z6~TxPKx&VepAnxtp9L?P3rlLk8D@GUysSdPGvl=-;_tok?Iq!}TgN|Q&}+vit(TE1 zUN^?Gg;LXaq&c4hufUIm&qZlOlrBUwJ!3V+JjQe8M~Xy&_eDrI?cmK-h-&PQ{D9O# zL=fE^P=3xwgEanDFO8v$7W2f1)o1E$)bYqMJjy3EuIR|#_Ktayx^pvZ}z@iEy% z5l?YEW9x^%d*X#z%hU!QapB3~(GAb+^-+NbTHs^MMEy*?8|z3sW8s;t$B|}je5i&8 zNeS|?$3_k%HiI*ldg_MLfqcQi?>g733_NITP=8-*m*Zn_Ik8`%^0we)Lo?>mP9d5u zG#fu9{-TuF17fbyFLtnJ^;ij;K9n6rxU(?XaLm zGpUpIybxipb0@GIvFo%@9c# z5e-L#r%FNugKW>1`KegG7M`)}H3Ooy5XHx&j;a=I3@%z*fC}F_FF+}^5n?USbi{t2 zc@!*KhEn1jlQ!Q?76Hu*5bSE27s4Covq5%+V}PG~;!Un1egMQ&IvMI{+NyC2MR*l% zLT+fhd9PY56sm7k~pD? zjtZ{Dyb*qx0b9D~rw=7`07Nb@F9$S%L#@fuMU-68b*43HbOS+V@{+E)00T!jp8!_W9IsLnLj zwT@*$%hFg}&{`_Jl{^D>W5#~LCdOI<8mtH&Do|x{ixjl%&DBGp_N^r-t;6->zxd>>(8vQeFX$$U>9#~6D~_y*@`bPfss zg~%ba0ds)L5a$$-jmWqxkwWGUo~F1gpwUUw37CuLU&@BY-zI}b!|blJ_F#Yj>l3ud zZIFUPgMXg|3kdeg*nc3QIBA-&wG@Uyt4pMyxu?&hcp-!BfwLeE5z)|UmQ995E(V^L zs|ke;A&UpHF6Y{d-kB~7=+q`uGRcGZJvPq(>(l`xGzM8fTOx(bJw8pa88oav{0&ty zxT;vsw3S9^l|Gp3*&gyO@-W(H?ehD>9;94e7CLJ{lLtOGgGDlU9u|OCAQDX$TlFPT z$lUeA9NR)R$)3;?!$l&C_}r~Dl0g-+AsQz#@Yb=MrZlN5<;ylf%m{xA_Bc!c%iOWK zps9-G!K#!h;~Yz*0#RpxLWgLPD58l#rePSGr8Jn7Ln}ih@$4c#E$Db_p5`~CX~ULl zDz$?O`r$XYp0RoM8&U~6+te9G54^7eCf0L`&{JG8h0S>Yi{7(g%MZrO*j|XR&8zje zv1MD(q>T-BC^xlL=O`IKa*l#`F4+DvD++T9#R~~p97V4#nZo8w0Ha)ctWk)`Bu&oXiCjoY*ojCE<4t7~l!1&b3D_1W zV9o@UOeb88#sx{K&P4>RpTN=r2B*X%$YLQPOOC9qce)J>m@_sSg7*x)bm*<7S4L2m zNFj3~K(dh+w9XuYjA;=KIV!z^Ru)O6aYCcgMp!n$@S&d^H5#&O&;e|q()}avc~pe@ zsbX)+2HFxSXwC#M$za5eU&4{&>C#XfNG7;k2bW)=1{Hy)6cz9ZFV}H7wIOQ1Y@kNi z9;9KT=Yl9ZU$lL2&D3>$t47Y%ucI`!~bTQ8q_ zQ>=#cfvJ2-QHwjUFmyAA?*LDYGD9VWGVIB1AOvX*VO~WzPG~tSk%H!IKu2Wp zEq*x@GrJ7hOk#JsLdVt(+eq}C!gktdbGL?v+J=BV2$u^VGH+fDCR!$m|DhEyi9+UF zfLNrhvQB2ERTv^i@nB{hY?1qNYl_x79RZQmdfGu)3tE-wv}-}JWw3^kdyQ@tIs@sj zf`qm1Gy|x_L<*Wy0jx5c!^OX>+@m1bC^T=SHKS02hJV(-&F*wO;qYt))IFy5nimTy z9TnoXq6N)Mrm#5~z$(L18|5kI%*==lTzds>E`HBS4Pt&zZ_B)$-!pQ9)Vmo1MRwX~ zV(};pDXuJ;0_TJ<8W9K_2pt>j=>D&eMp5VzF|Z;J;* z5Qh8?rjGOv0X%R^adn9lHm8K4vWBqn(;7qm-1Aagxq^=_GhNt_N59=~X+bjrz81AN zc%G4=KR`nq9qNR#sJ9y6(S)rnk;3MjfY;>ZKqzn0o#c^sP}ojv%$9`>A2)A_XmfBD zPfiCn0-}#H;=LNwnBUKo#PU#!$rLyz1=e3JXyY$wKdg9`&GX}x;^N}@65hl{bH_&U z9<_nX8=QYomW=(4q&=m3)CSQeQ?*!Ug?Z&w{AGl*28UQZCM$bbZlsGAl7*CbiE@PA zPtAq3tX!7p;E)c_hByRKX+{uKTG6NO;LRS;BP1<>;Y!kJw(UV&C*On4Hu+>3vIBA_XluG?sr1TC(&zkd6Fq`*}*B6v>t+z zNxnL_l`ZGE7j@X8mc{i&+|b*Z_>~pE<$E*^YO0{o!W~Bh42;@68Yei(6u9Ex=2iYL zR#WQusV(BF`OqA})xG2Hse(J;p@_Fc&q_-vFp1fiDlmZq3}}u!4!NkTfeay$!gi6v zKm@QjmA}LTW8ar**dj}53mXH=X4nSSwzQicsV6ybRouzZMk_QHXy))Qc}^t$$bcbZ zAY&~HT)n_G2UjSJ=g+vVfwEt))w#w+4(FL%H8&Z2~4SFmP`&De46G0>*alZGc zX(bRW5(&12N?j5Ktvj^7i@N+JObL-t>AMJ0cbKH0w{2|LljEv1u-?WEH_iXbxnWJq zrMHM+1Qdnk%gBO9E%a**OEg(MW#C)3Oo6+|0UmwVb&C~DY1m_0AH~Zc0;kZP;#ip@ zxbbe*td5yS<&#E?__wka&R)z&EK=4d;(*qKZ7h+(HXR!c-kLSy8;YHa7*AlV*AL4! zA2qx!pO%BQhJY!6FR5Zfbe86CtOcXUwBChGBGnZOj;ykvppJyf9qi3vgKh_Ru4r7? z;5n+*j&5O6@fFS9KtPIVrCloRTv4Q1aLei-;2BM%usJUzwI|u}RfPK_vbo4UpLURy za$te-qb4s?u9*BnrUyA#VIpTPQ>6wVE68i0G~`cM=&Je*9@``en6pAwoX)neO24x| zY7UF&dp(gwWgKMNnw_q8)|`%gc0}Z@VS~do8Ik)wgF%ADh9Yw2_jxC#)fs{HzAj`* z&)Qf--N|3?2eL92GOqX0LD<>fJc2h%=&h{JlzwCeqaM38(h0J8t-zbai0YNk94fUAl$DUBwY)?M zn$rOTK2k&00Lw6H{qvIY29)$532;DD%HEl?G=<@*kFGHeXv#zio3lY>q1CWOK0Esg&H72{)C%j&E|EN2i%X)QIUh7;5eAJVwayp~pQVS7nptva zWkJIpY~^p2Xe`!i&Q%CB2CTF*G$>5SDT;DH>bv8UYfk|<=(2Y7W{ zn~hDkq~&VbEbQVR$XF(1Of>L$t$7!+oz*}qR34rUyBT3*j2vM)3zdgw!(U9IpgAG5 zRSk^l?T*U$ur2?j!p(s#FKl!l{Z@L8SABOxEvQcqVJ+dnU%QF(=Umt-JQyO4FzS7~ zHCFSZMv9;-lPPdc39QLZ0|z`T@{b3ig$2&L-UO#o1^xtE=;79pPZzi@mBHq?Ba0qo z^o|fyHEX!if}ctA2yc*VMTL}Y%hXVqlR_5{2ppTdjiFMHWgElZab!z&_T$oAk8TZk z>TARTi&NE-kMAjtTPShfrct$2p`gtrQP7+eMzR)BRG)ng#=e}PnPv>_NM0^zczYKs zw7uaD!s;uzxYE(uQbx91)$G73v3P3zW$=I2Y-kTlrocHX4As~KE^0EcvME``){NYj z+Gxm=MHKccksNzRVMA*Rn%U{qQAnc<=JE85P$wdD{GNqMh!2H!%15i&(D6-4Ev z`JR_VL337^$y!8Fecmu>vq;rcGMn^#$8O84?ob#uq-deGQ*ZUyZO)}&#DE3G0@ZW3 z-N+QGGMNJByuikg`ChhqV`9$KSFa~{G%j$F^&M_*Ruw!5nTzP-BKmggC?MQ#Y?&4| z?Fx5U;FR^Pr2u7_Oo4M^fMtf2l{O844$i5yWv4ZtE+j8-{H%Mbkeur^LS3qZyAcDI zG9;GU3OFc$Wd*J+nF4n)1L9-^dj^h^PhEC8l_CLL7^mh~7SoR7z^${^)?8lBx5>fz zj?1+ns|pI=n2d}ZlRO;baaluOB86=_w#-2>VIz*ftY!pFYNc^Sy@J*jG{mnXc=WcR z^%aN=E$Zca2j>A1y|Dva>F7ZQ&|5^NS@1^fxUocPG&eZ5##Bkzn1?1tlOt-R8ra%v zaE#w3UT`adgsTW*W3=Xl7BnQpfX!nCnPF$i?8!IS1*j})!nV&PQP8488`e)y@g*gW z@v@dwgzTbIA!EIc$nYq`Xkpjeyg)OG#mK&*bAS{I4-TkL#UUuzY1Ph|O3y}6eFhx^!5*l1 zZq_d!S^^U(Y{jv)?Rqu9km-7v+eE1?)q}EFVqky-e#WM{x!xn{t$eInE^sr=gcXOA zV?&Y6k&b){&oemZZl_Y90+T6l)xk|GimU0K;Lj0Z4jZDmflIHz-Fe{__Rd?Q;|8u3 z9+*ig*n6V`x*FFI&A4b`LUgnmO=kMeY5-P7T{|plNC&pYBCMow!v7pXJ)jxY`|J^i zE%HRNta(bOdiL#@)JShzq_B0zR&1o0E*q2@X?lnc)<9U<;|FUE;Z-FSa7?7IIV;dE4I0C@ zN?ptqkW*s^GF4+)qe+h`PH1O5ZY}0z2g9fE#{S>vDJ^D*rUU|>u!C8m9`l!Y^>8q{3hGjz-&kchemu_$U#%= zGcTC}=d1v`Tpa}l-w69Sy4Bhmy=w6Umkb=&Vb)vvcPy!8jz@1ErceuOY~`$uS57sT zXlKoU$7BkevjR*rTR_fH|0Z&Uz3)2j1+IzhYq$9|e3rAqZePviLaOymt~xKGiOM5p zRg{$xPDC&kI|lH18=FK5o3lc_k}S^V0zF?i#wu8baboMN7AxJvXK7X6*>N_BceK9| zlQbUO;9S*F;5ysifNe4b&S|0YETDm73wd%I&ADr$?WVZ8z#&JN;|8wyswxC2&3hhm za%4O*@8*^ZY>PgP38JbHhQF0z`f-rp24^mD`~ zAFZP9J&&{X=dB_zE7&9OsHGMm>=a(&&jR9!4nXn4uf4%?Ub66Lt8WLNtu>B36^1VcHIXIg|D*K z&Mq5Y!?HRvSZ{;Rx|C-P^s7#e4Uyrrt*HaD4YQV{E(|ArWD%E8gXjDJ3sqs22Zv!u zPtBo}wBYrd`c!WxJlbNq?Yo=$>@3pdO#++(Li0x5d#Rj9NlKe?CIVzq4WDyFQ(~&a zslZp1Lj`jO#8%fE=-G>M8Tk*~TZS zhR>OTW5+stt>UU_4>w0k1c+dfs8Q)qk(&`f{v}09&ym<1~+U0-V0S+5H}% zflvb{R&VQz1Z;3L+0IOsE!F@!X@HTdd<#zpBZlT&H-a&(5(iR_^^I&GQ6m(6kcM^e zmjZ{4lzN4!@qCfa3os^T`8RUX{Z?TTAd_nNoHr0Ht_^61_u1^gqbviy*+*jwLpzEO zs|B8?{l;Uaws@;61!Pe0DXAPYwQzwP=Pnv#5grq2@SHio-i@?cQqj?8Rd1s-rCtgZqb# zq>Ydtc~qXZjr8RNEz))ks->ADvkVPurOAFiM+RHec$_`&HFX~SmP039~GVm@X{AzY2OCX!bsq=!FT4C1N4X`rGyF}S91HUipW=+GTr-<_~{ z9fAhDY)!KqhjMo)#zlVO-yk)Jd4~Uyj#OI0C`3A39dPH))VQY;VDY8 z`Ns-l8MXuSmTCa218f(-rVBa*aMlGdU-_C;&ejzHl0n+V!gmuf0y>7~QYz3?QBVGe zu?Dlj9=idoTdDzU4p8?b2w>Ob4BJv{^O%$I_*tXA2Cz+?{l8_D?L-)ERi8F8k~3uZ z`HY~CqL1*HOoP`ic-ZCBf-D{^U>Tb={BrUZ{#md2`RJr9ryA6WByNgOsK(g z2FdHxZPN5U=e4Q0kXH;d=BpjpcATG@=b7sklFI|O+o2M~`AB#XJj+A#!myrVx*sYL zBok{0okDm+#BwoRJ+7|GP+30OGxdeAUpuhB@qwFzcaCuFz$g-?f9G)DfEUC9!{L?C zEs;xY3pISsAsk$>IGQ7I$nLx9xOE+ocMgum!DDl98n3;~jTXm2%SXA8WK;MnunP;Il#8OOuCZc<)w!eFLZkt7b9%9m~};rE!t?~4Nov!Y!Wz`UgzKxdM+F0@OUcka5oDk6Vf zQ$=&WT4M!+E!+K38{mD7+iF^0(O`kjBG3G_^B5bf=E;_{Cb{gUtT`_$aWZMjFom$G zN1^VoIbk^qUt?ZFmRGu&+gTD6lkEPCbx($8n@xuRIkc^bg>Me9!B{>g zNGV-=(76O5PFQ=8a|uoRzF1K-s}*r$<56}#V#l#ZJw0_;3zye8EEI+bX|U-)b4bZ=d#>%j(4UTix}*JsduZ=yX#voqN_+o zSUQOJwr}KwTvE|ofb7~FU8?3Z(yyKk-;Ql-m4mvNIObD-S@#@5A(ni;>tjano7oo4 z!G6JKnqcKEg{;=(>+u?yBNp@z?CWRzXjFT5}>s;*x~TQPe(c@D|{ z=yth^-JNs`_xJv5SKEDObh=4%l+?}zzFmGLK;tSX6ekgm+L{3~C=0qPk0&*EN$}(l z-dMTw;dMm2pBd-8T?&xi%ZKlm0(4$qYXI|>Y5<)?K-bw~L7w^L0R&xIGgj{)Umix& z?uM{wLb2XFOmhv+HK}|dMBI`Z?3j_3SAEYb71mWbdz3F0=a8ALW|?(6fHL8FmCZvv zOM)6q(T=jKGs3!~Xj||+?(wM3!U$k;DoybG9uFRn;vAg?)4E>8%jU&Ngah%z?%V~w ztmv3WGs7zjwpEznvf%EK;Ewjs8v>5D=4ry`4FTTPTsMX6BebzNh0JLaU|CM4GB=5} zocVeFAYJ!g2FKU80(m8d>VwD& z^HSi9uNFA%gx!vC&v+W}Somx_@XCk#n21(Dyms!LbR6s;Dn6xHQB^0z`&HDxPI0Hm zZ9WAMTUFPAaKx%C5A!<8npc!%C%Jon1JLFdkD2~kl*J=&H8gc`eW-sOXDHi(ufyEH z$3Rl}I#|S#pStdCc}sD#EQ+)9-1EE-bLp?k!}Q}m%!A^Pt~VY(E3X=Jb`Ydo3!EJv zC!U3mr!jPOMr@&N50maSRl`RMu~Xew)1i6i(8dMp86*^}W1-T{;h~-GO>oxd4zs*@h}hH4AWr?8{w;>)#r)mQ;`<=*N%SAcbhlk z-W?t#RM6~DbLhBu_iPUfVc&NR#O6$ryPfFBx=Oif6g+aE&o3Rk(}4@l^xy zC=3?fLDhNZ!@Yy(Vhy3QNvRh?Y=@>f*$BRP+J!#zb{maNeCbAKO}f7t6kIihb>iu` ziz6WJV@X4n;wPSX1ezza3}W7L4Wjc2uXJ3i3`S|{is)|iVpWBFLTno%%L;Znyua(HaZA6Zh7~6K(t-v;GO>ox83loOUOL1p8FTWbwe41We{CxH z(&4z3sLNTRp0{gMk*H1tGKm$1^Z4$0jjAx2T!ZM8(sZkZAo6}KtG@B{rBO=HcaLqo z8FSh5)`l1t#HM+{bPR2c+Yrq-nvF=LmTdqNLQfc{u$cwS2L0mpV zbKQQ{O*O%?!!J8Lp58Vh;<{Mph`ti1rMqT^IP=m2?`FSUOioB9)^3`2QfaFM$2*T7 zg$IbZ&ZDn;B(CpUxDZ|eqMaZw^W2G>RnqNt)f+?|cZGqxx?WY7Os+w6R^gorR$^c> z%|Sgr&Frg`mdbVD z=M}6moGs!LV(OhMh5tqMU*TdqNL zV(DcULU=}KMs?nKJkucdou}vqw4q5vEBD*M^4nJ)7(xrP^D$zJlrzkdtTB5`M+Nq1 zhjnHN$;28$=M}cFERjm!6<)-^he=}nNNTg!RXBn*9&+C#55 zYdP{@*u{p-yW-5{3_Ajq^*t}HM7bP`61iX78;CO>mpZm1fM^LE%n;&s zsbk}p(3EB+VPZ152GOZyq@B+5Uc#w+7P#wIh|}|V>txbE=kkhn*!=!w2>XbJHOI$$ zUXdg8HSk)RN@*#C^*s%jT!ZM`0ux>tGrtois1?cz>5|gw39&ASybdwik@FVMs#o-J z@1yPtUjtXen{UQ#-1}B{B_@+=5S?3K!rgoh_E!znzD{CoCoc@Eke4-dy3!RpcV6q; z%CN;p=d9>i8TRhjEgK*t6Ke>aT3B8*d*;pKF!`)+pGYf&XuCtJn;@sgPHE3H`fwZh zKKhGwl@5;v-sC8rj*qg~!Wq4y(igvw&DuM}WOD7hLY!M>S%nAQB^RSkA&D2y{U)5&iiy1P<7-rAPg9z1$aw-&d)JZ4^Xui>=9diz>pUYJ-k%r!$< zM>a-0ZQ15d*A&A#iJsp~F<5)292do(SIZsk9fY)lC;07KL*kZe5HDJRDebgA^NV1* z()ta(^T$gT?m7erZ8etjy3lP}igwr%JmabaZL3v45)JnomwBXXkNzIBM0Os+x9 z4l!R^#G8`c&o-AA*Z7tuw zzk>}w*smq_)R~DjgxwL=T^r%Fvu*EbU)q>;TiC5l7Nf;qB+>D@3hOgd>DK9n?`-f< zA?bz{C%v=PO`Vxo12`Pu*rgHhMYSO*F4l2;<7}iDLY_Yc^InHNt|O4r)?pHT!&7jC*}h{`chFQuKSfQOJz+l2W?t7Ev@YMJ@A|t4&k12D bE%OG|dV_eM5SB%T-Ol@e0koFQfOh}@@q`w` diff --git a/rowers/views/teamviews.py b/rowers/views/teamviews.py index 06e8e894..a248c24b 100644 --- a/rowers/views/teamviews.py +++ b/rowers/views/teamviews.py @@ -261,7 +261,7 @@ def team_requestmembership_view(request,teamid,userid): else: messages.error(request,text) - url = reverse(team_view,kwargs={ + url = reverse('team_view',kwargs={ 'id':int(teamid), }) @@ -408,7 +408,7 @@ def team_create_view(request): viewing = cd['viewing'] res,message=teams.create_team(name,manager,private,notes, viewing) - url = reverse(rower_teams_view) + url = reverse('rower_teams_view') response = HttpResponseRedirect(url) return response diff --git a/rowers/views/workoutviews.py b/rowers/views/workoutviews.py index 9f1c51cd..546405a3 100644 --- a/rowers/views/workoutviews.py +++ b/rowers/views/workoutviews.py @@ -4331,7 +4331,7 @@ def team_workout_upload_view(request,message="", url = reverse('team_workout_upload_view') response = HttpResponseRedirect(url) - w = Workout.objects.get(id=encoder.decode_hex(id)) + w = Workout.objects.get(id=id) r = getrower(request.user) if (make_plot): From 948fbf76ee4efe7ce3d337b0bfdd157599eb0fb5 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Fri, 15 Feb 2019 21:11:00 +0100 Subject: [PATCH 05/18] passing tests - teams can be created --- rowers/models.py | 27 +++++----- rowers/teams.py | 29 ++++++---- rowers/templates/teams.html | 2 - rowers/tests/test_permissions.py | 73 ++++++++++++++------------ rowers/tests/testdata/testdata.csv.gz | Bin 11426 -> 12525 bytes rowers/views/statements.py | 17 ++++++ rowers/views/teamviews.py | 32 +++++++++-- 7 files changed, 120 insertions(+), 60 deletions(-) diff --git a/rowers/models.py b/rowers/models.py index 7f7d5300..b261d323 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -346,8 +346,9 @@ class Team(models.Model): def save(self, *args, **kwargs): manager = self.manager if manager.rower.rowerplan == 'basic': - raise ValidationError( - "Basic user cannot be team manager" + if manager.rower.protrialexpires < datetime.date.today() and manager.rower.plantrialexpires < datetime.date.today(): + raise ValidationError( + "Basic user cannot be team manager" ) if manager.rower.rowerplan in ['plan','pro']: @@ -695,8 +696,8 @@ class Rower(models.Model): planexpires = models.DateField(default=current_day) teamplanexpires = models.DateField(default=current_day) clubsize = models.IntegerField(default=0) - protrialexpires = models.DateField(blank=True,null=True) - plantrialexpires = models.DateField(blank=True,null=True) + protrialexpires = models.DateField(default=datetime.date(1970,1,1)) + plantrialexpires = models.DateField(default=datetime.date(1970,1,1)) # Privacy Data @@ -875,11 +876,12 @@ def check_teams_on_change(sender, **kwargs): action = kwargs.pop('action', None) pk_set = kwargs.pop('pk_set',None) if action == 'pre_add' and instance.rowerplan=='basic': - for id in pk_set: - team = Team.objects.get(id=id) - if team.manager.rower.rowerplan not in ['coach']: - raise ValidationError( - "You cannot join a team led by a Pro or Self-Coach user" + if instance.protrialexpires < datetime.date.today() and instance.plantrialexpires < datetime.date.today(): + for id in pk_set: + team = Team.objects.get(id=id) + if team.manager.rower.rowerplan not in ['coach']: + raise ValidationError( + "You cannot join a team led by a Pro or Self-Coach user" ) m2m_changed.connect(check_teams_on_change, sender=Rower.team.through) @@ -1905,9 +1907,10 @@ class PlannedSession(models.Model): manager = self.manager if self.sessiontype not in ['race','indoorrace']: if manager.rower.rowerplan in ['basic','pro']: - raise ValidationError( - "Basic user cannot be team manager" - ) + if manager.rower.plantrialexpires < timezone.now().date(): + raise ValidationError( + "You must be a Self-Coach user or higher to create a planned session" + ) # sort units diff --git a/rowers/teams.py b/rowers/teams.py index 6bdbf566..b6a7bfc2 100644 --- a/rowers/teams.py +++ b/rowers/teams.py @@ -61,6 +61,14 @@ def update_team(t,name,manager,private,notes,viewing): def create_team(name,manager,private='open',notes='',viewing='allmembers'): # needs some error testing + if manager.rower.rowerplan == 'basic': + if manager.rower.protrialexpires < timezone.now().date() and manager.rower.plantrialexpires < timezone.now().date(): + return (0,'You need to upgrade to a paid plan to establish a team') + elif manager.rower.rowerplan != 'coach': + ts = Team.objects.filter(manager=manager) + if len(ts)>=1: + return (0,'You need to upgrade to the Coach plan to have more than one team') + try: t = Team(name=name,manager=manager,notes=notes, private=private,viewing=viewing) @@ -77,17 +85,18 @@ def remove_team(id): send_team_delete_mail(t,r) return t.delete() -def set_teamplanexpires(rower): - ts = Team.objects.filter(rower=rower) +#def set_teamplanexpires(rower): +# ts = Team.objects.filter(rower=rower) - texp = datetime.date(timezone.now()) +# texp = datetime.date(timezone.now()) - for t in ts: - mr = Rower.objects.get(user=t.manager) - if mr.teamplanexpires > texp: - rower.teamplanexpires = mr.teamplanexpires +# for t in ts: +# print t.name +# mr = Rower.objects.get(user=t.manager) +# if mr.teamplanexpires > texp: +# rower.teamplanexpires = mr.teamplanexpires - t.save() +# t.save() return (1,'Updated rower team expiry') @@ -99,7 +108,7 @@ def add_member(id,rower): res = handle_add_workouts_team(ws,t) - set_teamplanexpires(rower) +# set_teamplanexpires(rower) return (id,'Member added') @@ -111,7 +120,7 @@ def remove_member(id,rower): res = handle_remove_workouts_team(ws,t) - set_teamplanexpires(rower) +# set_teamplanexpires(rower) return (id,'Member removed') def mgr_remove_member(id,manager,rower): diff --git a/rowers/templates/teams.html b/rowers/templates/teams.html index 7db3247f..2f5a4396 100644 --- a/rowers/templates/teams.html +++ b/rowers/templates/teams.html @@ -56,7 +56,6 @@ {% endif %} - {% if user.rower.rowerplan == 'coach' %}
  • Teams I manage

    Number of members: {{ clubsize }}

    @@ -85,7 +84,6 @@ {% endif %} New Team
  • - {% endif %} {% if invites or requests or myrequests or myinvites %}
  • Invitations and Requests

    diff --git a/rowers/tests/test_permissions.py b/rowers/tests/test_permissions.py index 8c6e2618..408cf6ff 100644 --- a/rowers/tests/test_permissions.py +++ b/rowers/tests/test_permissions.py @@ -112,7 +112,7 @@ class PermissionsBasicsTests(TestCase): ## Low level ## Coach can have any number of groups - def test_plan_groupmanager(self): + def test_coach_groupmanager(self): team1 = Team.objects.create( name = 'FirstTeam', notes = faker.text(), @@ -131,7 +131,7 @@ class PermissionsBasicsTests(TestCase): team3 = Team.objects.create( - name = 'SecondTeam', + name = 'ThirdTeam', notes = faker.text(), manager = self.ucoach, ) @@ -447,8 +447,6 @@ class PermissionsViewTests(TestCase): 'name': faker.word(), } - print 'posting to sessions/create' - form = PlannedSessionForm(post_data) self.assertTrue(form.is_valid()) @@ -545,7 +543,7 @@ class PermissionsViewTests(TestCase): ) response = self.c.get(url) - self.assertEqual(response.status_code,200) + self.assertEqual(response.status_code,403) ## Self coach can create one group ## Self coach cannot create more than one group @@ -629,7 +627,6 @@ class PermissionsViewTests(TestCase): 'name': faker.word(), } - print 'posting to sessions/create' form = PlannedSessionForm(post_data) self.assertTrue(form.is_valid()) @@ -641,7 +638,7 @@ class PermissionsViewTests(TestCase): ## Self Coach cannot edit on behalf of athlete def test_plan_edit_athlete_settings(self): - self.rbasic.team.add(self.teamplan) + self.rpro.team.add(self.teamplan) login = self.c.login(username=self.uplan2.username, password=self.uplan2password) self.assertTrue(login) @@ -649,12 +646,12 @@ class PermissionsViewTests(TestCase): url = reverse('rower_prefs_view',kwargs={'userid':self.ubasic.id}) response = self.c.get(url) - self.assertEqual(response.status_code,404) + self.assertEqual(response.status_code,403) ## Self Coach cannot run analytics on behalf of athlete @patch('rowers.dataprep.read_cols_df_sql', side_effect = mocked_read_df_cols_sql_multistats) def test_plan_edit_athlete_analysis(self,mocked_df): - self.rbasic.team.add(self.teamplan) + self.rpro.team.add(self.teamplan) login = self.c.login(username=self.uplan2.username, password=self.uplan2password) self.assertTrue(login) @@ -668,13 +665,13 @@ class PermissionsViewTests(TestCase): response = self.c.get(url) - self.assertEqual(response.status_code,404) + self.assertEqual(response.status_code,403) ## Self Coach cannot upload on behalf of athlete @patch('rowers.dataprep.create_engine') @patch('rowers.dataprep.getsmallrowdata_db',side_effect=mocked_getsmallrowdata_db) def test_plan_edit_athlete_upload(self,mocked_sqlalchemy,mocked_getsmallrowdata_db): - self.rbasic.team.add(self.teamplan) + self.rpro.team.add(self.teamplan) login = self.c.login(username=self.uplan2.username, password=self.uplan2password) self.assertTrue(login) @@ -682,7 +679,7 @@ class PermissionsViewTests(TestCase): url = reverse('team_workout_upload_view') response = self.c.get(url) - self.assertEqual(response.status_code,404) + self.assertEqual(response.status_code,403) ## Pro can have one group @@ -763,7 +760,7 @@ class PermissionsViewTests(TestCase): ## Pro cannot edit on behalf of athlete def test_pro_edit_athlete_settings(self): - self.rbasic.team.add(self.teampro) + self.rpro.team.add(self.teampro) login = self.c.login(username=self.upro2.username, password=self.upro2password) self.assertTrue(login) @@ -771,12 +768,12 @@ class PermissionsViewTests(TestCase): url = reverse('rower_prefs_view',kwargs={'userid':self.ubasic.id}) response = self.c.get(url) - self.assertEqual(response.status_code,404) + self.assertEqual(response.status_code,403) ## Pro cannot run analytics on behalf of athlete @patch('rowers.dataprep.read_cols_df_sql', side_effect = mocked_read_df_cols_sql_multistats) def test_pro_edit_athlete_analysis(self,mocked_df): - self.rbasic.team.add(self.teampro) + self.rpro.team.add(self.teampro) login = self.c.login(username=self.upro2.username, password=self.upro2password) self.assertTrue(login) @@ -790,40 +787,45 @@ class PermissionsViewTests(TestCase): response = self.c.get(url) - self.assertEqual(response.status_code,404) + self.assertEqual(response.status_code,403) ## Self Coach cannot upload on behalf of athlete @patch('rowers.dataprep.create_engine') @patch('rowers.dataprep.getsmallrowdata_db',side_effect=mocked_getsmallrowdata_db) def test_plan_edit_athlete_upload(self,mocked_sqlalchemy,mocked_getsmallrowdata_db): - self.rbasic.team.add(self.teamplan) + self.rpro.team.add(self.teamplan) login = self.c.login(username=self.uplan2.username, password=self.uplan2password) self.assertTrue(login) url = reverse('team_workout_upload_view') - response = self.c.get(url) - self.assertEqual(response.status_code,404) + response = self.c.get(url,follow=True) + self.assertEqual(response.status_code,200) + + expected_url = reverse('paidplans') + self.assertRedirects(response, + expected_url = expected_url, + status_code=302,target_status_code=200) ## Pro users can see team members' workout, but not edit - def test_coach_edit_athlete_workout(self,mocked_sqlalchemy,mocked_getsmallrowdata_db): - self.rbasic.team.add(self.proplan) - self.rpro2.team.add(self.proplan) + def test_pro_edit_athlete_workout(self): + self.rpro.team.add(self.teampro) + self.rpro2.team.add(self.teampro) login = self.c.login(username=self.upro2.username, password=self.upro2password) self.assertTrue(login) url = reverse('workout_edit_view', - kwargs={'id':encoder.encode_hex(self.ubasic_workouts[0].id)} + kwargs={'id':encoder.encode_hex(self.upro_workouts[0].id)} ) response = self.c.get(url) - self.assertEqual(response.status_code,404) + self.assertEqual(response.status_code,403) url = reverse('workout_view', - kwargs={'id':encoder.encode_hex(self.ubasic_workouts[0].id)} + kwargs={'id':encoder.encode_hex(self.upro_workouts[0].id)} ) response = self.c.get(url) @@ -831,8 +833,8 @@ class PermissionsViewTests(TestCase): ## Self Coach users can see team members' workout, but not edit - def test_coach_edit_athlete_workout(self,mocked_sqlalchemy,mocked_getsmallrowdata_db): - self.rbasic.team.add(self.teamplan) + def test_plan_edit_athlete_workout(self): + self.rpro.team.add(self.teamplan) login = self.c.login(username=self.uplan2.username, password=self.uplan2password) self.assertTrue(login) @@ -842,7 +844,7 @@ class PermissionsViewTests(TestCase): ) response = self.c.get(url) - self.assertEqual(response.status_code,200) + self.assertEqual(response.status_code,403) url = reverse('workout_view', kwargs={'id':encoder.encode_hex(self.ubasic_workouts[0].id)} @@ -853,9 +855,9 @@ class PermissionsViewTests(TestCase): ## Basic users can see team members' workout, but not edit - def test_basic_edit_athlete_workout(self,mocked_sqlalchemy,mocked_getsmallrowdata_db): - self.rbasic.team.add(self.teamplan) - self.rplan2.team.add(self.teamplan) + def test_basic_edit_athlete_workout(self): + self.rbasic.team.add(self.teamcoach) + self.rplan2.team.add(self.teamcoach) login = self.c.login(username=self.ubasic.username, password=self.ubasicpassword) self.assertTrue(login) @@ -865,7 +867,7 @@ class PermissionsViewTests(TestCase): ) response = self.c.get(url) - self.assertEqual(response.status_code,404) + self.assertEqual(response.status_code,403) url = reverse('workout_view', kwargs={'id':encoder.encode_hex(self.uplan2_workouts[0].id)} @@ -902,7 +904,7 @@ class PermissionsViewTests(TestCase): url = reverse('team_requestmembership_view', kwargs = { 'teamid':self.teampro.id, - 'userid':self.upro.id + 'userid':self.ubasic.id }) response = self.c.get(url,follow=True) @@ -913,3 +915,8 @@ class PermissionsViewTests(TestCase): expected_url = expected_url, status_code=302,target_status_code=200) +# Race related + +## Basic and Pro users can create races + +## Basic users can subscribe to any race diff --git a/rowers/tests/testdata/testdata.csv.gz b/rowers/tests/testdata/testdata.csv.gz index 3c6cc8df3385d74d5ae9969f9e7fe46cce2ec6f9..a3190522b7f7acdec42e8bc8e99c1df0b34d1093 100644 GIT binary patch literal 12525 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

    E>@Zpm<_Jc%b( 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 DGZ9{a literal 11426 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≧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 w5V= 1: + return False + # Check if a user can create planned sessions def hasplannedsessions(user): if not user.is_anonymous(): diff --git a/rowers/views/teamviews.py b/rowers/views/teamviews.py index a248c24b..54c8736e 100644 --- a/rowers/views/teamviews.py +++ b/rowers/views/teamviews.py @@ -254,6 +254,15 @@ def team_requestmembership_view(request,teamid,userid): t = Team.objects.get(id=teamid) except Team.DoesNotExist: raise Http404("Team doesn't exist") + + r = getrequestrower(request,userid=userid) + + if t.manager.rower.rowerplan in ['plan','pro'] and r.rowerplan == 'basic': + messages.error(request, + "You have to be on a paid plan (Pro or higher) to join this team. As a basic user you can only join teams managed by users on the Coach plan.") + + url = reverse('paidplans') + return HttpResponseRedirect(url) res,text = teams.create_request(t,userid) if res: @@ -395,8 +404,19 @@ def team_edit_view(request,id=0): 'team':t, }) -@user_passes_test(iscoachmember,login_url="/rowers/paidplans",redirect_field_name=None) +#@user_passes_test(cancreateteam,login_url="/rowers/paidplans",redirect_field_name=None) +@login_required() def team_create_view(request): + r = getrequestrower(request) + + if r.rowerplan == 'basic': + if r.protrialexpires < timezone.now().date() and r.plantrialexpires < timezone.now().date(): + messages.error(request,"You must upgrade to Pro or higher to create teams/training groups") + url = reverse('paidplans') + return HttpResponseRedirect(url) + + + if request.method == 'POST': teamcreateform = TeamForm(request.POST) if teamcreateform.is_valid(): @@ -408,6 +428,12 @@ def team_create_view(request): viewing = cd['viewing'] res,message=teams.create_team(name,manager,private,notes, viewing) + + if not res: + messages.error(request,message) + url = reverse('paidplans') + return HttpResponseRedirect(url) + url = reverse('rower_teams_view') response = HttpResponseRedirect(url) return response @@ -438,7 +464,7 @@ def team_create_view(request): 'breadcrumbs':breadcrumbs, }) -@user_passes_test(iscoachmember,login_url="/rowers/paidplans",redirect_field_name=None) +@login_required() def team_deleteconfirm_view(request,id): r = getrower(request.user) try: @@ -474,7 +500,7 @@ def team_deleteconfirm_view(request,id): 'active':'nav-teams', }) -@user_passes_test(iscoachmember,login_url="/rowers/paidplans",redirect_field_name=None) +@login_required() def team_delete_view(request,id): r = getrower(request.user) try: From 14c7296c9b2cde1218b90136097270d91407e18e Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Fri, 15 Feb 2019 21:44:30 +0100 Subject: [PATCH 06/18] working, passing, everything --- rowers/teams.py | 2 +- rowers/tests/testdata/testdata.csv.gz | Bin 12525 -> 12525 bytes rowers/views/userviews.py | 4 ++-- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/rowers/teams.py b/rowers/teams.py index b6a7bfc2..f0fa0e0b 100644 --- a/rowers/teams.py +++ b/rowers/teams.py @@ -64,7 +64,7 @@ def create_team(name,manager,private='open',notes='',viewing='allmembers'): if manager.rower.rowerplan == 'basic': if manager.rower.protrialexpires < timezone.now().date() and manager.rower.plantrialexpires < timezone.now().date(): return (0,'You need to upgrade to a paid plan to establish a team') - elif manager.rower.rowerplan != 'coach': + if manager.rower.rowerplan != 'coach': ts = Team.objects.filter(manager=manager) if len(ts)>=1: return (0,'You need to upgrade to the Coach plan to have more than one team') diff --git a/rowers/tests/testdata/testdata.csv.gz b/rowers/tests/testdata/testdata.csv.gz index a3190522b7f7acdec42e8bc8e99c1df0b34d1093..796c938ab7663b782b917936ae7a2777307546e5 100644 GIT binary patch delta 15 WcmaEx_%@MEzMF&Nzw$=5=LP^aB?e6Z delta 15 WcmaEx_%@MEzMF$%oA^ey=LP^YGX?kn diff --git a/rowers/views/userviews.py b/rowers/views/userviews.py index c69b0957..45bf65b4 100644 --- a/rowers/views/userviews.py +++ b/rowers/views/userviews.py @@ -4,7 +4,7 @@ from statements import * def start_trial_view(request): r = getrower(request.user) - if r.protrialexpires is not None: + if r.protrialexpires > datetime.date(1970,1,1): messages.error(request,'You do not qualify for a trial') url = '/rowers/paidplans' return HttpResponseRedirect(url) @@ -31,7 +31,7 @@ def start_trial_view(request): def start_plantrial_view(request): r = getrower(request.user) - if r.plantrialexpires is not None: + if r.plantrialexpires > datetime.date(1970,1,1): messages.error(request,'You do not qualify for a trial') url = '/rowers/paidplans' return HttpResponseRedirect(url) From e9043733de656da575de8e0f6ff05c1bc8ff7ddb Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Fri, 15 Feb 2019 21:55:05 +0100 Subject: [PATCH 07/18] all good except club size --- rowers/tests/testdata/testdata.csv.gz | Bin 12525 -> 12525 bytes rowers/views/teamviews.py | 14 +++++++------- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/rowers/tests/testdata/testdata.csv.gz b/rowers/tests/testdata/testdata.csv.gz index 796c938ab7663b782b917936ae7a2777307546e5..804bb97abf72e774a1cf074b2704bd236aaf6698 100644 GIT binary patch delta 15 WcmaEx_%@MEzMF%CRedAda{~Y{T?JPF delta 15 WcmaEx_%@MEzMF&Nzw$=5=LP^aB?e6Z diff --git a/rowers/views/teamviews.py b/rowers/views/teamviews.py index 54c8736e..fab4debe 100644 --- a/rowers/views/teamviews.py +++ b/rowers/views/teamviews.py @@ -202,7 +202,7 @@ def rower_teams_view(request,message='',successmessage=''): 'myinvites':myinvites, }) -@user_passes_test(iscoachmember,login_url="/rowers/paidplans",redirect_field_name=None) +@login_required() def invitation_revoke_view(request,id): res,text = teams.revoke_invite(request.user,id) if res: @@ -216,7 +216,7 @@ def invitation_revoke_view(request,id): return HttpResponseRedirect(url) -@user_passes_test(iscoachmember,login_url="/rowers/paidplans",redirect_field_name=None) +@login_required() def manager_member_drop_view(request,teamid,userid, message='',successmessage=''): rower = Rower.objects.get(user__id=userid) @@ -230,7 +230,7 @@ def manager_member_drop_view(request,teamid,userid, return HttpResponseRedirect(url) -@user_passes_test(iscoachmember,login_url="/rowers/paidplans",redirect_field_name=None) +@login_required() def manager_requests_view(request,code=None,message='',successmessage=''): if code: res,text = teams.process_request_code(request.user,code) @@ -291,7 +291,7 @@ def request_revoke_view(request,id=0): return HttpResponseRedirect(url) -@user_passes_test(iscoachmember,login_url="/rowers/paidplans",redirect_field_name=None) +@login_required() def request_reject_view(request,id=0): res,text = teams.reject_request(request.user,id) @@ -304,7 +304,7 @@ def request_reject_view(request,id=0): return HttpResponseRedirect(url) -@user_passes_test(iscoachmember,login_url="/rowers/paidplans",redirect_field_name=None) +@login_required() def invitation_reject_view(request,id=0): res,text = teams.reject_invitation(request.user,id) @@ -340,7 +340,7 @@ def rower_invitations_view(request,code=None,message='',successmessage=''): }) return HttpResponseRedirect(url) -@user_passes_test(iscoachmember,login_url="/rowers/paidplans",redirect_field_name=None) +@login_required() def team_edit_view(request,id=0): try: t = Team.objects.get(id=id) @@ -516,7 +516,7 @@ def team_delete_view(request,id): response = HttpResponseRedirect(url) return response -@user_passes_test(iscoachmember,login_url="/rowers/paidplans",redirect_field_name=None) +@login_required() def team_members_stats_view(request,id): r = getrower(request.user) try: From 170c267e0e9fabb963bc4c7c8add07b4e599590d Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sat, 16 Feb 2019 09:20:54 +0100 Subject: [PATCH 08/18] removed club size --- rowers/admin.py | 4 +-- rowers/forms.py | 4 +-- rowers/teams.py | 43 ++++++++++++++------------------ rowers/templates/paidplans.html | 15 +---------- rowers/templates/rower_form.html | 2 +- rowers/templates/teams.html | 2 -- rowers/views/teamviews.py | 6 ++--- 7 files changed, 27 insertions(+), 49 deletions(-) diff --git a/rowers/admin.py b/rowers/admin.py index 4c2c861c..fad8795b 100644 --- a/rowers/admin.py +++ b/rowers/admin.py @@ -24,7 +24,7 @@ class RowerInline(admin.StackedInline): ('Billing Details', {'fields':('street_address','city','postal_code','country','paymentprocessor','customer_id')}), ('Rower Plan', - {'fields':('paidplan','rowerplan','paymenttype','planexpires','teamplanexpires','clubsize','protrialexpires','plantrialexpires',)}), + {'fields':('paidplan','rowerplan','paymenttype','planexpires','teamplanexpires','protrialexpires','plantrialexpires',)}), ('Rower Settings', {'fields': ('gdproptin','gdproptindate','weightcategory','sex','adaptiveclass','birthdate','getemailnotifications', @@ -128,7 +128,7 @@ class IndoorVirtualRaceResultAdmin(admin.ModelAdmin): search_fields = ['race__name','username'] class PaidPlanAdmin(admin.ModelAdmin): - list_display = ('name','shortname','price','paymenttype','paymentprocessor','clubsize','external_id') + list_display = ('name','shortname','price','paymenttype','paymentprocessor','external_id') admin.site.unregister(User) admin.site.register(User,UserAdmin) diff --git a/rowers/forms.py b/rowers/forms.py index 88ce4888..acabc5b4 100644 --- a/rowers/forms.py +++ b/rowers/forms.py @@ -752,7 +752,7 @@ class PlanSelectForm(forms.Form): ).exclude( shortname="basic" ).order_by( - "price","clubsize","shortname" + "price","shortname" ) if rower and not includeall: try: @@ -765,7 +765,7 @@ class PlanSelectForm(forms.Form): ).exclude( price__lte=amount ).order_by( - "price","clubsize","shortname" + "price","shortname" ) diff --git a/rowers/teams.py b/rowers/teams.py index f0fa0e0b..ad4c8414 100644 --- a/rowers/teams.py +++ b/rowers/teams.py @@ -156,22 +156,20 @@ def create_request(team,user): if r2 in Rower.objects.filter(team=team): return (0,'Already a member of that team') - if count_club_members(team.manager)+count_invites(team.manager) <= r.clubsize: - codes = [i.code for i in TeamRequest.objects.all()] + # if count_club_members(team.manager)+count_invites(team.manager) <= r.clubsize: + codes = [i.code for i in TeamRequest.objects.all()] + code = uuid.uuid4().hex[:10].upper() + # prevent duplicates + while code in codes: code = uuid.uuid4().hex[:10].upper() - # prevent duplicates - while code in codes: - code = uuid.uuid4().hex[:10].upper() - u = User.objects.get(id=user) - rekwest = TeamRequest(team=team,user=u,code=code) - rekwest.save() - - send_request_email(rekwest) + u = User.objects.get(id=user) + rekwest = TeamRequest(team=team,user=u,code=code) + rekwest.save() + + send_request_email(rekwest) - return (rekwest.id,'The request was created') - else: - return (0,'That team has reached its maximum number of members') + return (rekwest.id,'The request was created') return (0,'Something went wrong in create_request') @@ -198,21 +196,18 @@ def create_invite(team,manager,user=None,email=''): except Rower.MultipleObjectsReturned: return (0,'There is more than one user with that email address') - if count_club_members(team.manager)+count_invites(team.manager) <= r.clubsize: - codes = [i.code for i in TeamInvite.objects.all()] + # if count_club_members(team.manager)+count_invites(team.manager) <= r.clubsize: + codes = [i.code for i in TeamInvite.objects.all()] + code = uuid.uuid4().hex[:10].upper() + # prevent duplicates + while code in codes: code = uuid.uuid4().hex[:10].upper() - # prevent duplicates - while code in codes: - code = uuid.uuid4().hex[:10].upper() - invite = TeamInvite(team=team,code=code,user=user,email=email) - invite.save() - return (invite.id,'Invitation created') + invite = TeamInvite(team=team,code=code,user=user,email=email) + invite.save() + return (invite.id,'Invitation created') - else: - return (0,'You are at your club size limit') - return (0,'Nothing done') def revoke_request(user,id): diff --git a/rowers/templates/paidplans.html b/rowers/templates/paidplans.html index 0b2f55cb..b091b9e5 100644 --- a/rowers/templates/paidplans.html +++ b/rowers/templates/paidplans.html @@ -257,23 +257,10 @@ {% endif %} - {% elif rower and rower.rowerplan == 'coach' and rower.clubsize < 100 %} -   -   - - - - {% else %}   - {% endif %} + {% endif %} diff --git a/rowers/templates/rower_form.html b/rowers/templates/rower_form.html index df55dc5b..23c1c456 100644 --- a/rowers/templates/rower_form.html +++ b/rowers/templates/rower_form.html @@ -52,7 +52,7 @@ {% endif %} {% csrf_token %} - {% if rower.clubsize < 100 and rower.user == user %} + {% if rower.rowerplan != 'coach' and rower.user == user %}

    Upgrade

    diff --git a/rowers/templates/teams.html b/rowers/templates/teams.html index 2f5a4396..ba43dec4 100644 --- a/rowers/templates/teams.html +++ b/rowers/templates/teams.html @@ -58,8 +58,6 @@
  • Teams I manage

    -

    Number of members: {{ clubsize }}

    -

    Maximum club size: {{ max_clubsize }}

    {% if myteams %} diff --git a/rowers/views/teamviews.py b/rowers/views/teamviews.py index fab4debe..f947b1b4 100644 --- a/rowers/views/teamviews.py +++ b/rowers/views/teamviews.py @@ -172,8 +172,8 @@ def rower_teams_view(request,message='',successmessage=''): 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 + # clubsize = teams.count_invites(request.user)+teams.count_club_members(request.user) + # max_clubsize = r.clubsize messages.info(request,successmessage) messages.error(request,message) @@ -190,8 +190,6 @@ def rower_teams_view(request,message='',successmessage=''): 'teams':ts, 'active':'nav-teams', 'breadcrumbs':breadcrumbs, - 'clubsize':clubsize, - 'max_clubsize':max_clubsize, 'myteams':myteams, 'memberteams':memberteams, 'invites':invites, From 259d6f28bbdd956402bfbbb2b9cc3b075f8b4da4 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sat, 16 Feb 2019 09:30:33 +0100 Subject: [PATCH 09/18] checking of team permissions at code acceptance --- rowers/teams.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/rowers/teams.py b/rowers/teams.py index ad4c8414..a8fdad1a 100644 --- a/rowers/teams.py +++ b/rowers/teams.py @@ -28,6 +28,8 @@ from rowers.tasks import ( handle_sendemail_invite_accept,handle_sendemail_team_removed, ) +from rowers.models import ValidationError + # Low level functions - to be called by higher level methods inviteduration = 14 # days @@ -102,7 +104,11 @@ def remove_team(id): def add_member(id,rower): t= Team.objects.get(id=id) - rower.team.add(t) + try: + rower.team.add(t) + except ValidationError: + return(0,"Couldn't add member") + # code to add all workouts ws = Workout.objects.filter(user=rower) @@ -406,7 +412,12 @@ def process_request_code(manager,code): return (0,'You are not the manager of this team') result = add_member(t.id,r) + if not result: + return (result,"The member couldn't be added") + send_request_accept_email(rekwest) + + rekwest.delete() return result @@ -425,6 +436,9 @@ def process_invite_code(user,code): t = invitation.team result = add_member(t.id,r) + if not result: + return (result,"The member couldn't be added") + send_invite_accept_email(invitation) invitation.delete() return result From 6afde11b7c5c13477bf800b29d17ac72a76d1e13 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sat, 16 Feb 2019 10:31:47 +0100 Subject: [PATCH 10/18] athlete edit permissions in place --- rowers/braintreestuff.py | 5 ++ rowers/models.py | 20 ++++---- rowers/teams.py | 7 +++ rowers/templatetags/rowerfilters.py | 9 ++++ rowers/tests/test_permissions.py | 80 ++++++++++++++++++++++++++++- rowers/utils.py | 6 +++ rowers/views/teamviews.py | 1 + rowers/views/workoutviews.py | 6 ++- 8 files changed, 121 insertions(+), 13 deletions(-) diff --git a/rowers/braintreestuff.py b/rowers/braintreestuff.py index bfe978bc..1dd572a0 100644 --- a/rowers/braintreestuff.py +++ b/rowers/braintreestuff.py @@ -167,6 +167,11 @@ def update_subscription(rower,data,method='up'): l = rower.user.last_name, ) + if rower.paidplan != 'coach': + coachees = Rower.objects.filter(coach__in=[rower]).distinct() + for coachee in coachees: + coachee.coaches.remove(rower) + if method == 'up': transactions = result.subscription.transactions diff --git a/rowers/models.py b/rowers/models.py index b261d323..ca7f3869 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -340,6 +340,7 @@ class Team(models.Model): viewing = models.CharField(max_length=30,choices=viewchoices,default='allmembers',verbose_name='Sharing Behavior') + def __unicode__(self): return self.name @@ -831,6 +832,7 @@ class Rower(models.Model): # Friends/Team friends = models.ManyToManyField("self",blank=True) + coaches = models.ManyToManyField("self",blank=True) privacy = models.CharField(default='visible',max_length=30, choices=privacychoices) @@ -986,12 +988,12 @@ def checkworkoutuser(user,workout): return False try: r = Rower.objects.get(user=user) - teams = workout.team.all() + coaches = user.rower.coaches.filter(rowerplan='coach') if workout.user == r: return True - elif teams: - for team in teams: - if user == team.manager and workout.privacy == 'visible': + elif coaches: + for coach in coaches: + if user.rower == coach and workout.privacy == 'visible': return True else: return False @@ -1003,17 +1005,17 @@ def checkworkoutuser(user,workout): def checkaccessuser(user,rower): try: r = Rower.objects.get(user=user) - teams = Team.objects.filter(manager=user) if rower == r: return True - elif teams: - for team in teams: - if team in rower.team.all(): + coaches = rower.coaches.filter(rowerplan='coach') + if coaches: + for coach in coaches: + if user.rower == coach: return True else: return False except Rower.DoesNotExist: - return False + return False timezones = ( (x,x) for x in pytz.common_timezones diff --git a/rowers/teams.py b/rowers/teams.py index a8fdad1a..e6604f98 100644 --- a/rowers/teams.py +++ b/rowers/teams.py @@ -102,6 +102,11 @@ def remove_team(id): return (1,'Updated rower team expiry') +def add_coach(manager,rower): + rower.coaches.add(m) + + return (1,"Added Coach") + def add_member(id,rower): t= Team.objects.get(id=id) try: @@ -414,6 +419,7 @@ def process_request_code(manager,code): result = add_member(t.id,r) if not result: return (result,"The member couldn't be added") + send_request_accept_email(rekwest) @@ -439,6 +445,7 @@ def process_invite_code(user,code): if not result: return (result,"The member couldn't be added") + send_invite_accept_email(invitation) invitation.delete() return result diff --git a/rowers/templatetags/rowerfilters.py b/rowers/templatetags/rowerfilters.py index 11758611..be4413fb 100644 --- a/rowers/templatetags/rowerfilters.py +++ b/rowers/templatetags/rowerfilters.py @@ -376,6 +376,15 @@ def team_rowers(user): return [] +@register.filter +def coach_rowers(user): + try: + members = Rower.objects.filter(coaches__in=user.rower).distinct().order_by( + "user__last_name","user__last_name" + ) + return members + except TypeError: + return [] @register.filter def verbosetimeperiod(timeperiod): diff --git a/rowers/tests/test_permissions.py b/rowers/tests/test_permissions.py index 408cf6ff..7b30f9e9 100644 --- a/rowers/tests/test_permissions.py +++ b/rowers/tests/test_permissions.py @@ -455,9 +455,10 @@ class PermissionsViewTests(TestCase): - ## Coach can edit on behalf of athlete + ## Coach can edit on behalf of athlete if permitted def test_coach_edit_athlete_settings(self): self.rbasic.team.add(self.teamcoach) + self.rbasic.coaches.add(self.rcoach) login = self.c.login(username=self.ucoach.username, password=self.ucoachpassword) self.assertTrue(login) @@ -467,10 +468,22 @@ class PermissionsViewTests(TestCase): response = self.c.get(url) self.assertEqual(response.status_code,200) + def test_coach_edit_athlete_settings_not(self): + self.rbasic.team.add(self.teamcoach) + + login = self.c.login(username=self.ucoach.username, password=self.ucoachpassword) + self.assertTrue(login) + + url = reverse('rower_prefs_view',kwargs={'userid':self.ubasic.id}) + + response = self.c.get(url) + self.assertEqual(response.status_code,403) + ## Coach can run analytics for athlete @patch('rowers.dataprep.read_cols_df_sql', side_effect = mocked_read_df_cols_sql_multistats) def test_coach_edit_athlete_analysis(self,mocked_df): self.rbasic.team.add(self.teamcoach) + self.rbasic.coaches.add(self.rcoach) login = self.c.login(username=self.ucoach.username, password=self.ucoachpassword) self.assertTrue(login) @@ -486,12 +499,31 @@ class PermissionsViewTests(TestCase): self.assertEqual(response.status_code,200) + @patch('rowers.dataprep.read_cols_df_sql', side_effect = mocked_read_df_cols_sql_multistats) + def test_coach_edit_athlete_analysis_not(self,mocked_df): + self.rbasic.team.add(self.teamcoach) - ## Coach can upload on behalf of athlete + login = self.c.login(username=self.ucoach.username, password=self.ucoachpassword) + self.assertTrue(login) + + + url = reverse('cumstats', + kwargs={ + 'theuser':self.ubasic.id, + } + ) + + response = self.c.get(url) + + self.assertEqual(response.status_code,403) + + + ## Coach can upload on behalf of athlete - if team allows @patch('rowers.dataprep.create_engine') @patch('rowers.dataprep.getsmallrowdata_db',side_effect=mocked_getsmallrowdata_db) def test_coach_edit_athlete_upload(self,mocked_sqlalchemy,mocked_getsmallrowdata_db): self.rbasic.team.add(self.teamcoach) + self.rbasic.coaches.add(self.rcoach) login = self.c.login(username=self.ucoach.username, password=self.ucoachpassword) self.assertTrue(login) @@ -531,6 +563,50 @@ class PermissionsViewTests(TestCase): self.assertEqual(aantal2,aantal+1) + ## Coach can upload on behalf of athlete - if team allows + @patch('rowers.dataprep.create_engine') + @patch('rowers.dataprep.getsmallrowdata_db',side_effect=mocked_getsmallrowdata_db) + def test_coach_edit_athlete_uploadnot(self,mocked_sqlalchemy,mocked_getsmallrowdata_db): + self.rbasic.team.add(self.teamcoach) + + login = self.c.login(username=self.ucoach.username, password=self.ucoachpassword) + self.assertTrue(login) + + url = reverse('team_workout_upload_view') + + aantal = len(Workout.objects.filter(user=self.rbasic)) + + response = self.c.get(url) + self.assertEqual(response.status_code,200) + + filename = 'rowers/tests/testdata/testdata.csv' + f = open(filename,'rb') + file_data = {'file': f} + form_data = { + 'title':'test', + 'workouttype':'rower', + 'boattype':'1x', + 'notes':'aap noot mies', + 'make_plot':False, + 'upload_to_c2':False, + 'plottype':'timeplot', + 'file': f, + 'user': self.ubasic.id + } + + response = self.c.post(url, form_data, follow=True) + f.close() + + self.assertEqual(response.status_code,200) + + self.assertRedirects(response, + expected_url = url, + status_code=302,target_status_code=200) + + aantal2 = len(Workout.objects.filter(user=self.rbasic)) + + self.assertEqual(aantal2,aantal) + ## Coach can edit athlete's workout def test_coach_edit_athlete_workout(self): self.rbasic.team.add(self.teamcoach) diff --git a/rowers/utils.py b/rowers/utils.py index 53194091..3cc90707 100644 --- a/rowers/utils.py +++ b/rowers/utils.py @@ -404,6 +404,12 @@ def isprorower(r): result = r.rowerplan == 'basic' and r.protrialexpires >= datetime.date.today() return result + +def iscoach(m,r): + result = False + result = m in r.coaches + + return result # Exponentially weighted moving average # Used for data smoothing of the jagged data obtained by Strava diff --git a/rowers/views/teamviews.py b/rowers/views/teamviews.py index f947b1b4..0bce82e3 100644 --- a/rowers/views/teamviews.py +++ b/rowers/views/teamviews.py @@ -267,6 +267,7 @@ def team_requestmembership_view(request,teamid,userid): messages.info(request,text) else: messages.error(request,text) + url = reverse('team_view',kwargs={ 'id':int(teamid), diff --git a/rowers/views/workoutviews.py b/rowers/views/workoutviews.py index 546405a3..e6528084 100644 --- a/rowers/views/workoutviews.py +++ b/rowers/views/workoutviews.py @@ -4235,16 +4235,18 @@ def team_workout_upload_view(request,message="", rowerform = TeamInviteForm(request.POST) rowerform.fields.pop('email') - rowerform.fields['user'].queryset = User.objects.filter(rower__isnull=False,rower__team__in=myteams).distinct() + rowers = Rower.objects.filter(coaches__in=[request.user.rower]).distinct() + rowerform.fields['user'].queryset = User.objects.filter(rower__in=rowers).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'] + workouttype = form.cleaned_data['workouttype'] if rowerform.is_valid(): u = rowerform.cleaned_data['user'] - if u: + if u and request.user.rower in u.rower.coaches.all(): r = getrower(u) else: message = 'Please select a rower' From 0d3763ebb33cbf2ecb6103aeeb574743305e2a06 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sat, 16 Feb 2019 11:08:41 +0100 Subject: [PATCH 11/18] updated t&c, --- rowers/braintreestuff.py | 3 ++ rowers/templates/menu_teams.html | 2 - rowers/templates/privacypolicy.html | 63 ++++++++++++---------- rowers/templates/teams.html | 74 ++++++++++++++++++++++---- rowers/tests/test_permissions.py | 15 ++++-- rowers/tests/test_team.py | 9 ---- rowers/tests/testdata/testdata.csv.gz | Bin 12525 -> 12525 bytes templates/newbase.html | 2 +- 8 files changed, 111 insertions(+), 57 deletions(-) diff --git a/rowers/braintreestuff.py b/rowers/braintreestuff.py index 1dd572a0..4de44b42 100644 --- a/rowers/braintreestuff.py +++ b/rowers/braintreestuff.py @@ -154,6 +154,7 @@ def update_subscription(rower,data,method='up'): return False,0 if result.is_success: + yesterday = (timezone.now()-datetime.timedelta(days=1)).date() rower.paidplan = plan rower.planexpires = result.subscription.billing_period_end_date rower.teamplanexpires = result.subscription.billing_period_end_date @@ -161,6 +162,8 @@ def update_subscription(rower,data,method='up'): rower.paymenttype = plan.paymenttype rower.rowerplan = plan.shortname rower.subscription_id = result.subscription.id + rower.protrialexpires = yesterday + rower.plantrialexpires = yesterday rower.save() name = '{f} {l}'.format( f = rower.user.first_name, diff --git a/rowers/templates/menu_teams.html b/rowers/templates/menu_teams.html index 84551097..9be35ba0 100644 --- a/rowers/templates/menu_teams.html +++ b/rowers/templates/menu_teams.html @@ -8,13 +8,11 @@  Overview - {% if user|is_manager %}
  •  New Team
  • - {% endif %} diff --git a/rowers/templates/privacypolicy.html b/rowers/templates/privacypolicy.html index efd50bd0..8a396650 100644 --- a/rowers/templates/privacypolicy.html +++ b/rowers/templates/privacypolicy.html @@ -59,13 +59,13 @@ other fitness sites. You can actually revoke these at any time.
  • User preferences as shown on the user settings page
  • Your favorite Flex Charts if defined -
  • The teams you are a member of. +
  • The teams or groups you are a member of.
  • Estimated four minute, 2k and 1 hour ergometer and OTW power values, based on the workouts you upload, and their evolution during your usage of the site -
  • For members on the Coach plan, the names and purposes of teams. Names - of team members. (Members who delete their account will be erased from - existing teams.) +
  • For members on the Coach plan, the names and purposes of teams or groups. Names + of team or group members. (Members who delete their account will be erased from + existing teams or groups.)
  • Any rowing courses you uploaded
  • Training targets and training plans
  • Your uploaded workouts, their names, boat type, start time and date, @@ -212,57 +212,62 @@

    -

    Team Functionality

    +

    Team Or Group Functionality

    - On rowsandall.com, users with the paid "Coach" plan can establish teams and invite other users to become part of the team. The purpose - of a team is to share workout and training plan data between the coach and the team members. In terms of sharing behavior, there are two types of teams: + On rowsandall.com, users with the paid "Coach" plan can establish teams or groups and invite other users to become part of the team or group. The purpose + of a team or group is to share workout and training plan data between the coach and the team or group members. In terms of sharing behavior, there are two types of teams or groups:

      -
    • "All Members" - This is the default team type. All members can see workouts of all other members, except those workouts that the members have +
    • "All Members" - This is the default team or group type. All members can see workouts of all other members, except those workouts that the members have marked as "private". -
    • "Coach Only" - With this setting, each individual team member is sharing his workout data only with the team manager. Other members cannot see +
    • "Coach Only" - With this setting, each individual team or group member is sharing his workout data only with the team or group manager. Other members cannot see his workouts.
    - The sharing behavior is chosen by the team member when he establishes the team and can be changed during the existence of the team. + The sharing behavior is chosen by the team or group member when he establishes the team or group and can be changed during the existence of the team or group.

    - By accepting an "invitation" to become a member of a team, or by requesting to become part of a team, you agree to automatically - share all your workout data (including workouts done prior to becoming a member of the team) to the team manager (coach) and, - depending to the team policy, to other members of the team. When you leave - a team, all your workout data will immediately become invisible to those who had access to it during your team membership, including - workouts that cover the period of time when you were member of the team. As a member of a team, you grant the team manager + By accepting an "invitation" to become a member of a team or group, or by requesting to become part of a team or group, you agree to automatically + share all your workout data (including workouts done prior to becoming a member of the team or group) to the team or group manager (coach) and, + depending to the team or group policy, to other members of the team or group. When you leave + a team or group, all your workout data will immediately become invisible to those who had access to it during your team or group membership, including + workouts that cover the period of time when you were member of the team or group. As a member of a team or group, you may grant the team or group manager permission to edit workout data - on your behalf, including the creation of charts and cross workout analysis. You also grant the team manager permission to + on your behalf, including the creation of charts and cross workout analysis. + This includes permission to 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, + settings page under the header "Account Information". The team or group manager is not able to access or change your passwords, team or group memberships, + favorite charts, export settings, workflow layout, or secret tokens. Also, the team or group manager is not able to download all your data, nor can he deactivate or delete your account.

    - Each team member is bound by this privacy policy and the GDPR regulation of the European Union regarding the personal data of other team - members that he has access to. By accepting an invitation to a team, the new member agrees to limit the use of these data strictly to the + Each team or group member is bound by this privacy policy and the GDPR regulation of the European Union regarding the personal data of other team or group + members that he has access to. By accepting an invitation to a team or group, the new member agrees to limit the use of these data strictly to the allowed use according to this privacy policy and the GDPR.

    - Team managers can access requests of users to be added to one of their teams. By accepting the invitation, the manager accepts the responsibilities - and duties associated with access to personal data of the new team member. He is bound by this privacy policy and the GDPR regulation + Team Or Group managers can access requests of users to be added to one of their teams or groups. + He can request or receive permission to edit an athlete's data and run analysis on an + athlete's behalf as described above. + By requesting or receiving these permissions, the manager accepts the responsibilities + and duties associated with access to personal data of the new team or group member. + He is bound by this privacy policy and the GDPR regulation of the European Union regarding the personal data that he has access to.

    - In case that a team manager wants to change the sharing behavior of one of his teams from "Coach Only" to "All Members", he has to inform all - impacted team members in due time. He shall give team members a minimum of three days to decide whether they agree with the new sharing policy, and - collect the consent of the team members with the new sharing policy. The team manager must remove team members who did not give their active consent - to the new policy from his team. If a team member has not responded within 7 days of being notified, the team manager will understand this as "no consent" - and remove the team member. + In case that a team or group manager wants to change the sharing behavior of one of his teams or groups from "Coach Only" to "All Members", he has to inform all + impacted team or group members in due time. He shall give team or group members a minimum of three days to decide whether they agree with the new sharing policy, and + collect the consent of the team or group members with the new sharing policy. The team or group manager must remove team or group members who did not give their active consent + to the new policy from his team or group. If a team or group member has not responded within 7 days of being notified, the team or group manager will understand this as "no consent" + and remove the team or group member.

    - When notified of a change in team sharing behavior by the team manager, the team member has to decide whether he agrees. In case of disagreement, he shall - revoke his team membership within less than 7 days of being notified. + When notified of a change in team or group sharing behavior by the team or group manager, the team or group member has to decide whether he agrees. In case of disagreement, he shall + revoke his team or group membership within less than 7 days of being notified.

    Third Party Sharing

    diff --git a/rowers/templates/teams.html b/rowers/templates/teams.html index ba43dec4..333e9888 100644 --- a/rowers/templates/teams.html +++ b/rowers/templates/teams.html @@ -1,12 +1,12 @@ {% extends "newbase.html" %} -{% block title %}Teams {% endblock %} +{% block title %}Groups {% endblock %} {% block main %}
      {% if teams %}
    • -

      My Teams

      +

      My Groups

  • @@ -32,7 +32,7 @@ {% if otherteams %}
  • -

    Other Teams

    +

    Open Groups

  • @@ -57,7 +57,7 @@ {% endif %}
  • -

    Teams I manage

    +

    Groups I manage

    {% if myteams %}
  • @@ -82,23 +82,75 @@ {% endif %} New Team + {% if coaches %} +
  • +

    My Coaches

    +
  • + + + + + + + {% for coach in coaches %} + + + + + {% endfor %} + +
    Coach
    + {{ coach.user.first_name }} {{ coach.user.last_name }} + + Remove + +
    +
  • + {% endif %} + {% if coachees %} +
  • +

    My Rowers

    + + + + + + + + {% for coachee in coachees %} + + + + + {% endfor %} + +
    Name
    + {{ coachee.user.first_name }} {{ coachee.user.last_name }} + + Remove + +
    +
  • + {% endif %} {% if invites or requests or myrequests or myinvites %}
  • Invitations and Requests

    -

    This section lists open invites to join a team. By accepting - a team invite, you are agreeing with the sharing - of personal data between team members and coaches according to +

    This section lists open invites to join a group. By accepting + a group invite, you are agreeing with the sharing + of personal data between group members and coaches according to our privacy policy.

    -

    As a team manager, by accepting a team invite, you are agreeing - with our privacy policy regarding teams and - personal data owned by team members.

    +

    As a group manager, by accepting a group invite, you are agreeing + with our privacy policy regarding groups and + personal data owned by group members.

    - + diff --git a/rowers/tests/test_permissions.py b/rowers/tests/test_permissions.py index 7b30f9e9..cda4b9e5 100644 --- a/rowers/tests/test_permissions.py +++ b/rowers/tests/test_permissions.py @@ -93,17 +93,17 @@ class PermissionsBasicsTests(TestCase): ## TeamPro, TeamCoach, TeamSelfCoach self.teampro = Team.objects.create( - name=faker.word(), + name=faker.word()+'1', notes=faker.text(), manager=self.upro2) self.teamplan = Team.objects.create( - name=faker.word(), + name=faker.word()+'2', notes=faker.text(), manager=self.uplan2) self.teamcoach = Team.objects.create( - name=faker.word(), + name=faker.word()+'3', notes=faker.text(), manager=self.ucoach) @@ -355,12 +355,12 @@ class PermissionsViewTests(TestCase): manager=self.upro2) self.teamplan = Team.objects.create( - name=faker.word(), + name=faker.word()+'2', notes=faker.text(), manager=self.uplan2) self.teamcoach = Team.objects.create( - name=faker.word(), + name=faker.word()+'3', notes=faker.text(), manager=self.ucoach) @@ -991,6 +991,11 @@ class PermissionsViewTests(TestCase): expected_url = expected_url, status_code=302,target_status_code=200) + +# coach related + +## coach disappears from list when downgrading + # Race related ## Basic and Pro users can create races diff --git a/rowers/tests/test_team.py b/rowers/tests/test_team.py index 9da7098e..c1043d5c 100644 --- a/rowers/tests/test_team.py +++ b/rowers/tests/test_team.py @@ -349,13 +349,4 @@ class TeamTestLowLevel(TestCase): id, comment = create_invite(self.t, self.users[3],self.users[4]) self.assertEqual(id,0) - # cannot exceed club size - for i in range(5): - id, comment = create_invite(self.t,self.u,user=self.users[i+1]) - - if i <= self.u.rower.clubsize: - self.assertEqual(comment,'Invitation created') - else: - self.assertEqual(id,0) - diff --git a/rowers/tests/testdata/testdata.csv.gz b/rowers/tests/testdata/testdata.csv.gz index 804bb97abf72e774a1cf074b2704bd236aaf6698..bf3c1415543a64f250ce74d86c5ffe9180892b41 100644 GIT binary patch delta 15 WcmaEx_%@MEzMF$X`2I$==LP^YNd_GN delta 15 WcmaEx_%@MEzMF%CRedAda{~Y{T?JPF diff --git a/templates/newbase.html b/templates/newbase.html index 74608dff..05596f32 100644 --- a/templates/newbase.html +++ b/templates/newbase.html @@ -216,7 +216,7 @@ From 74cc925e59a7af682a777ddb13da016712be3cc1 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sat, 16 Feb 2019 11:16:11 +0100 Subject: [PATCH 12/18] replacing team members with coachees in menus --- rowers/templates/menu_analytics.html | 8 ++++---- rowers/templates/menu_plan.html | 8 ++++---- rowers/templates/menu_profile.html | 8 ++++---- rowers/templates/menu_workouts.html | 10 +++++----- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/rowers/templates/menu_analytics.html b/rowers/templates/menu_analytics.html index f1475a76..ac847155 100644 --- a/rowers/templates/menu_analytics.html +++ b/rowers/templates/menu_analytics.html @@ -71,21 +71,21 @@ {% if user.is_authenticated and user|is_manager %}

     

    -{% if user|team_members %} +{% if user|coach_rowers %}
    • diff --git a/rowers/templates/menu_plan.html b/rowers/templates/menu_plan.html index c46e0c0f..fc82f23a 100644 --- a/rowers/templates/menu_plan.html +++ b/rowers/templates/menu_plan.html @@ -87,21 +87,21 @@ {% if user.is_authenticated and user|is_manager %}

       

      -{% if user|team_members %} +{% if user|coach_rowers %}
      • diff --git a/rowers/templates/menu_profile.html b/rowers/templates/menu_profile.html index 5da7d5fe..19d370b8 100644 --- a/rowers/templates/menu_profile.html +++ b/rowers/templates/menu_profile.html @@ -39,21 +39,21 @@ {% if user.is_authenticated and user|is_manager %}

         

        -{% if user|team_members %} +{% if user|coach_rowers %}
        • diff --git a/rowers/templates/menu_workouts.html b/rowers/templates/menu_workouts.html index f7e2cd66..20084c21 100644 --- a/rowers/templates/menu_workouts.html +++ b/rowers/templates/menu_workouts.html @@ -47,22 +47,22 @@ {% if user.is_authenticated and user|is_manager %}

           

          -{% if user|team_members %} +{% if user|coach_rowers %}
            • - {% for member in user|team_members %} + {% for member in user|coach_rowers %}
            • - {% if member == rower.user and not team %} + {% if member.user == rower.user and not team %} • {% else %}   {% endif %} - {{ member.first_name }} {{ member.last_name }} + {{ member.user.first_name }} {{ member.user.last_name }}
            • {% endfor %} @@ -77,7 +77,7 @@
              • - +
                  {% for tteam in teams %}
                • From ccdf24f671d2feb8c528befc23446ecf03303deb Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sat, 16 Feb 2019 17:15:25 +0100 Subject: [PATCH 13/18] not sure if this works --- rowers/models.py | 24 ++- rowers/tasks.py | 51 ++++++ rowers/teams.py | 195 +++++++++++++++++++++- rowers/templates/coacheerequestemail.html | 32 ++++ rowers/templates/coachrequestemail.html | 33 ++++ rowers/templates/menu_analytics.html | 2 +- rowers/templates/menu_plan.html | 2 +- rowers/templates/menu_profile.html | 2 +- rowers/templates/menu_teams.html | 2 +- rowers/templates/menu_workouts.html | 2 +- rowers/templates/team.html | 2 +- rowers/templates/teams.html | 128 +++++++++++++- rowers/tests/testdata/testdata.csv.gz | Bin 12525 -> 12525 bytes rowers/urls.py | 16 ++ rowers/views/statements.py | 2 +- rowers/views/teamviews.py | 123 +++++++++++++- 16 files changed, 599 insertions(+), 17 deletions(-) create mode 100644 rowers/templates/coacheerequestemail.html create mode 100644 rowers/templates/coachrequestemail.html diff --git a/rowers/models.py b/rowers/models.py index ca7f3869..b4426431 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -386,7 +386,6 @@ class TeamInviteForm(ModelForm): - class TeamRequest(models.Model): team = models.ForeignKey(Team) @@ -633,6 +632,8 @@ class PaidPlan(models.Model): paymentprocessor = self.paymentprocessor, ) +class CoachingGroup(models.Model): + pass # Extension of User with rowing specific data class Rower(models.Model): @@ -832,7 +833,8 @@ class Rower(models.Model): # Friends/Team friends = models.ManyToManyField("self",blank=True) - coaches = models.ManyToManyField("self",blank=True) + mycoachgroup = models.ForeignKey(CoachingGroup,related_name='coachingrole',null=True) + coachinggroups = models.ManyToManyField(CoachingGroup,related_name='coaches') privacy = models.CharField(default='visible',max_length=30, choices=privacychoices) @@ -870,6 +872,19 @@ class DeleteUserForm(forms.ModelForm): model = User fields = [] +# requestor is user +class CoachRequest(models.Model): + coach = models.ForeignKey(Rower) + user = models.ForeignKey(User,null=True) + issuedate = models.DateField(default=current_day) + code = models.CharField(max_length=150,unique=True) + +# requestor is coach +class CoachOffer(models.Model): + coach = models.ForeignKey(Rower) + user = models.ForeignKey(User,null=True) + issuedate = models.DateField(default=current_day) + code = models.CharField(max_length=150,unique=True) from django.db.models.signals import m2m_changed @@ -988,7 +1003,7 @@ def checkworkoutuser(user,workout): return False try: r = Rower.objects.get(user=user) - coaches = user.rower.coaches.filter(rowerplan='coach') + coaches = rower_get_coaches(user.rower) if workout.user == r: return True elif coaches: @@ -1007,7 +1022,7 @@ def checkaccessuser(user,rower): r = Rower.objects.get(user=user) if rower == r: return True - coaches = rower.coaches.filter(rowerplan='coach') + coaches = rower_get_coaches(rower) if coaches: for coach in coaches: if user.rower == coach: @@ -3434,3 +3449,4 @@ class PlannedSessionCommentForm(ModelForm): 'comment': forms.Textarea, } + diff --git a/rowers/tasks.py b/rowers/tasks.py index 7f4b756c..21531d98 100644 --- a/rowers/tasks.py +++ b/rowers/tasks.py @@ -1699,6 +1699,57 @@ def handle_makeplot(f1, f2, t, hrdata, plotnr, imagename, # Team related remote tasks +@app.task +def handle_sendemail_coachrequest(email,name,code,coachname, + debug=False,**kwargs): + + fullemail = email + subject = 'Invitation to add {n} to your athletes'.format(n=name) + + siteurl = SITE_URL + if debug: + siteurl = SITE_URL_DEV + + d = { + 'name':name, + 'coach':coachname, + 'code':code, + 'siteurl':siteurl + } + + form_email = 'Rowsandall ' + + res = send_template_email(from_email,[fullemail], + subject,'coachrequestemail.html',d, + **kwargs) + + return 1 + +@app.task +def handle_sendemail_coacheerequest(email,name,code,coachname, + debug=False,**kwargs): + + fullemail = email + subject = '{n} asks coach access to your data on rowsandall.com'.format(n=coachname) + + siteurl = SITE_URL + if debug: + siteurl = SITE_URL_DEV + + d = { + 'name':name, + 'coach':coachname, + 'code':code, + 'siteurl':siteurl + } + + form_email = 'Rowsandall ' + + res = send_template_email(from_email,[fullemail], + subject,'coacheerequestemail.html',d, + **kwargs) + + return 1 @app.task def handle_sendemail_invite(email, name, code, teamname, manager, diff --git a/rowers/teams.py b/rowers/teams.py index e6604f98..91b9e75d 100644 --- a/rowers/teams.py +++ b/rowers/teams.py @@ -17,7 +17,7 @@ queuelow = django_rq.get_queue('low') queuehigh = django_rq.get_queue('low') from rowers.models import ( - Rower, Workout, Team, TeamInvite,User,TeamRequest + Rower, Workout, Team, TeamInvite,User,TeamRequest, CoachRequest, CoachOffer ) from rowers.tasks import ( @@ -26,6 +26,7 @@ from rowers.tasks import ( handle_sendemail_member_dropped,handle_sendemail_request_accept, handle_sendemail_request_reject,handle_sendemail_invite_reject, handle_sendemail_invite_accept,handle_sendemail_team_removed, + handle_sendemail_coachrequest,handle_sendemail_coacheerequest, ) from rowers.models import ValidationError @@ -102,8 +103,17 @@ def remove_team(id): return (1,'Updated rower team expiry') -def add_coach(manager,rower): - rower.coaches.add(m) +def add_coach(coach,rower): + # get coaching group + try: + coachgroup = coach.mycoachgroup + except CoachingGroup.DoesNotExist: + coachgroup = CoachingGroup() + coachgroup.save() + coach.mycoachgroup = coachgroup + coach.save() + + rower.coachinggroups.add(coach) return (1,"Added Coach") @@ -134,6 +144,44 @@ def remove_member(id,rower): # set_teamplanexpires(rower) return (id,'Member removed') +def remove_coach(coach,rower): + try: + coachgroup = coach.mycoachgroup + except CoachingGroup.DoesNotExist: + coachgroup = CoachingGroup() + coachgroup.save() + coach.mycoachgroup = coachgroup + coach.save() + + rower.coachinggroups.remove(coachgroup) + + return (1,'Coach removed') + +def rower_get_coaches(rower): + coaches = [] + for group in rower.coachinggroups: + coach = Rower.objects.get(mycoachgroup=group) + coaches.append(coach) + + return coaches + + +def coach_getcoachees(coach): + return Rower.objects.filter(coachinggroups__in=[coach.mycoachgroup]) + +def coach_remove_athlete(coach,rower): + try: + coachgroup = coach.mycoachgroup + except CoachingGroup.DoesNotExist: + coachgroup = CoachingGroup() + coachgroup.save() + coach.mycoachgroup = coachgroup + coach.save() + + rower.coachingrgroups.remove(coachgroup) + + return (1,'Coach removed') + def mgr_remove_member(id,manager,rower): t = Team.objects.get(id=id) if t.manager == manager: @@ -161,6 +209,51 @@ def count_club_members(manager): # Medium level functionality +# request by user to be coached by coach +def create_coaching_request(coach,user): + if coach in rower_get_coaches(user.rower): + return (0,'Already coached by that coach') + + codes = [i.code for i in CoachRequest.objects.all()] + code = uuid.uuid4().hex[:10].upper() + while code in codes: + code = uuid.uuid4().hex[:10].upper() + + if coach.rowerplan == 'coach': + rekwest = CoachRequest(coach=coach,user=user,code=code) + rekwest.save() + + send_coachrequest_email(rekwest) + + return (rekwest.id,'The request was created') + + else: + return (0,'That person is not a coach') + +def send_coachrequest_email(rekwest): + name = rekwest.user.first_name + " " + rekwest.user.last_name + email = rekwest.user.email + + code = rekwest.code + + coachname = rekwest.coach.user.first_name + " " + rekwest.coach.user.last_name + + res = myqueue(queuehigh, + handle_sendemail_coachrequest, + email,name,code,coachname) + +def send_coacheerequest_email(rekwest): + name = rekwest.user.first_name + " " + rekwest.user.last_name + email = rekwest.user.email + + code = rekwest.code + + coachname = rekwest.coach.user.first_name + " " + rekwest.coach.user.last_name + + res = myqueue(queuehigh, + handle_sendemail_coacheerequest, + email,name,code,coachname) + def create_request(team,user): r2 = Rower.objects.get(user=user) r = Rower.objects.get(user=team.manager) @@ -183,7 +276,33 @@ def create_request(team,user): return (rekwest.id,'The request was created') return (0,'Something went wrong in create_request') + +# request by coach to coach user +def create_coaching_offer(coach,user): + r = user.rower + + if coach in rower_get_coaches(user.rower): + return (0,'You are already coaching this person.') + + codes = [i.code for i in CoachOffer.objects.all()] + code = uuid.uuid4().hex[:10].upper() + while code in codes: + code = uuid.uuid4().hex[:10].upper() + + if coach.rowerplan == 'coach': + rekwest = CoachOffer(coach=coach,user=user,code=code) + rekwest.save() + + send_coacheerequest_email(rekwest) + + return (rekwest.id,'The request was created') + + else: + return (0,'You are not a coach') + + + def create_invite(team,manager,user=None,email=''): r = Rower.objects.get(user=manager) if team.manager != manager: @@ -235,6 +354,36 @@ def revoke_request(user,id): else: return (0,'You are not the requestor') +def reject_revoke_coach_offer(user,id): + try: + rekwest = CoachOffer.objects.get(id=id) + except CoachOffer.DoesNotExist: + return (0,'The request is invalid') + + if rekwest.coach.user == user: + rekwest.delete() + return (1,'Request removed') + elif rekwest.user == user: + rekwest.delete() + return (1,'Request removed') + else: + return (0,'Not permitted') + +def reject_revoke_coach_request(user,id): + try: + rekwest = CoachRequest.objects.get(id=id) + except CoachRequest.DoesNotExist: + return (0,'The request is invalid') + + if rekwest.coach.user == user: + rekwest.delete() + return (1,'Request rejected') + elif rekwest.user == user: + rekwest.delete() + return (1,'Request rejected') + else: + return (0,'Not permitted') + def revoke_invite(manager,id): try: invite = TeamInvite.objects.get(id=id) @@ -458,3 +607,43 @@ def remove_expired_invites(): revoke_invite(i.team.manager,i.id) return (1,'Expired invitations deleted') + +def process_coachrequest_code(coach,code): + code = code.upper() + + try: + rekwest = CoachRequest.objects.get(code=code) + except CoachRequest.DoesNotExist: + return (0,'The request has been revoked or is invalid') + + if rekwest.coach != coach: + return (0,'The request is invalid') + + result = add_coach(coach,rekwest.user.rower) + if not result: + return (result,"Something went wrong") + + rekwest.delete() + + return result + +def process_coachoffer_code(user,code): + code = code.upper() + + try: + rekwest = CoachOffer.objects.get(code=code) + except CoachOffer.DoesNotExist: + return (0,'The request has been revoked or is invalid') + + if rekwest.user != user: + return (0,'The request is invalid') + + result = add_coach(rekwest.coach,rekwest.user.rower) + if not result: + return (result,"Something went wrong") + + rekwest.delete() + + return result + + diff --git a/rowers/templates/coacheerequestemail.html b/rowers/templates/coacheerequestemail.html new file mode 100644 index 00000000..fd0fbaa5 --- /dev/null +++ b/rowers/templates/coacheerequestemail.html @@ -0,0 +1,32 @@ +{% extends "emailbase.html" %} + +{% block body %} +

                  Dear {{ name }},

                  + +

                  + {{ coachname }} is inviting you to become your coach on rowsandall.com. +

                  +

                  + By accepting the invite, {{ coachname }} will have access to your + data and will be able to upload workouts and run analysis on rowsandall.com + on behalf of you. +

                  +

                  + By accepting the invite, you are agreeing with the sharing + of personal data according to our privacy policy. +

                  +

                  + To accept, login to the + site and you will find the invitation here on the Teams page: + {{ siteurl }}/rowers/me/teams +

                  +

                  + You can also click the direct link: + + {{ siteurl }}/rowers/me/coacheeinvitation/{{ code }} +

                  + +

                  + Best Regards, the Rowsandall Team +

                  +{% endblock %} diff --git a/rowers/templates/coachrequestemail.html b/rowers/templates/coachrequestemail.html new file mode 100644 index 00000000..4523aa66 --- /dev/null +++ b/rowers/templates/coachrequestemail.html @@ -0,0 +1,33 @@ +{% extends "emailbase.html" %} + +{% block body %} +

                  Dear {{ coachname }},

                  + +

                  + {{ name }} is inviting you to become his/her coach + on rowsandall.com +

                  +

                  + By accepting the invite, you will have access to {{ name }}'s + data and will be able to upload workouts and run analysis on rowsandall.com + on behalf of {{ name }}. +

                  +

                  + By accepting the invite, you are agreeing with the sharing + of personal data according to our privacy policy. +

                  +

                  + To accept the login to the + site and you will find the invitation here on the Teams page: + {{ siteurl }}/rowers/me/teams +

                  +

                  + You can also click the direct link: + + {{ siteurl }}/rowers/me/coachinvitation/{{ code }} +

                  + +

                  + Best Regards, the Rowsandall Team +

                  +{% endblock %} diff --git a/rowers/templates/menu_analytics.html b/rowers/templates/menu_analytics.html index ac847155..07c6d218 100644 --- a/rowers/templates/menu_analytics.html +++ b/rowers/templates/menu_analytics.html @@ -78,7 +78,7 @@
    {% if team.manager == user %} - + {% else %} diff --git a/rowers/templates/teams.html b/rowers/templates/teams.html index 333e9888..d31b6ae4 100644 --- a/rowers/templates/teams.html +++ b/rowers/templates/teams.html @@ -80,7 +80,7 @@
    TeamGroup Manager User Action
    {{ member.user.first_name }} {{ member.user.last_name }} {{ member.user.first_name }} {{ member.user.last_name }} Drop{{ member.user.first_name }} {{ member.user.last_name }}
    {% endif %} - New Team + New Training Group
  • {% if coaches %}
  • @@ -136,7 +136,7 @@ {% endif %} {% if invites or requests or myrequests or myinvites %}
  • -

    Invitations and Requests

    +

    Group Invitations and Requests

    This section lists open invites to join a group. By accepting a group invite, you are agreeing with the sharing of personal data between group members and coaches according to @@ -221,7 +221,131 @@ {% endif %} + {% if mycoachrequests or mycoachoffers or coachoffers or coachrequests %} +

  • +

    Coaching Invitations and Requests

    +

    This section lists open invites related to coaching. + By accepting a coaching invite, the coach can run + analysis, add workouts and edit settings on behalf of the athlete. + You agree to the sharing + of personal data between athletes and coaches according to + our privacy policy. +

    + + + + + + + + + + + {% for i in coachrequests %} + + + + + + + {% endfor %} + {% for i in mycoachoffers %} + + + + + + + {% endfor %} + {% for i in mycoachrequests %} + + + + + + + {% endfor %} + {% for i in coachoffers %} + + + + + + + {% endfor %} + +
    CoachUserAction 
    {{ i.coach.user.first_name }} {{ i.coach.user.last_name }}{{ i.user.first_name }} {{ i.user.last_name }}Accept + + Reject +
    {{ i.coach.user.first_name }} {{ i.coach.user.last_name }}{{ i.user.first_name }} {{ i.user.last_name }}  + + Revoke +
    {{ i.coach.user.first_name }} {{ i.coach.user.last_name }}{{ i.user.first_name }} {{ i.user.last_name }}  + Revoke +
    {{ i.coach.user.first_name }} {{ i.coach.user.last_name }}{{ i.user.first_name }} {{ i.user.last_name }}Accept + + Reject +
    + {% endif %}
  • + {% if potentialathletes %} +
  • +

    Rowers you could coach

    + + + + + + + + + {% for a in potentialathletes %} + + + + + {% endfor %} + +
    UserAction
    {{ a.user.first_name }} {{ a.user.last_name }} + Offer Coaching +
    +
  • + {% endif %} + {% if potentialcoaches %} +
  • +

    Coaches who could coach you

    + + + + + + + + + {% for c in potentialcoaches %} + + + + + {% endfor %} + +
    UserAction
    {{ c.first_name }} {{ c.last_name }} + Request Coaching +
    +
  • + {% endif %} diff --git a/rowers/tests/testdata/testdata.csv.gz b/rowers/tests/testdata/testdata.csv.gz index bf3c1415543a64f250ce74d86c5ffe9180892b41..7256d94144270203437770568d2c04f40f5a37da 100644 GIT binary patch delta 15 WcmaEx_%@MEzMF$%?URjc&kX=K{sz1N delta 15 WcmaEx_%@MEzMF$X`2I$==LP^YNd_GN diff --git a/rowers/urls.py b/rowers/urls.py index ddd2ebb1..c36f4a5b 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -410,6 +410,22 @@ urlpatterns = [ 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'^me/coachrequest/(?P\d+)/reject/$',views.reject_revoke_coach_request, + name='reject_revoke_coach_request'), + url(r'^me/coachrequest/(?P\d+)/revoke/$',views.reject_revoke_coach_request, + name='reject_revoke_coach_request'), + url(r'^me/coachoffer/(?P\d+)/reject/$',views.reject_revoke_coach_offer, + name='reject_revoke_coach_offer'), + url(r'^me/coachoffer/(?P\d+)/revoke/$',views.reject_revoke_coach_offer, + name='reject_revoke_coach_offer'), + url(r'^me/coachrequest/(?P\d+)/$',views.request_coaching_view, + name='request_coaching_view'), + url(r'^me/coachoffer/(?P\d+)/$',views.offer_coaching_view, + name='offer_coaching_view'), + url(r'^me/coachrequest/(\w+.*)/accept/$',views.coach_accept_coachrequest_view, + name='coach_accept_coachrequest_view'), + url(r'^me/coachoffer/(\w+.*)/accept/$',views.rower_accept_coachoffer_view, + name='rower_accept_coachofer_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'), diff --git a/rowers/views/statements.py b/rowers/views/statements.py index 7dff9589..1a8ee0ae 100644 --- a/rowers/views/statements.py +++ b/rowers/views/statements.py @@ -88,7 +88,7 @@ from rowers.models import ( microcyclecheckdates,mesocyclecheckdates,macrocyclecheckdates, TrainingMesoCycleForm, TrainingMicroCycleForm, RaceLogo,RowerBillingAddressForm,PaidPlan, - PlannedSessionComment, + PlannedSessionComment,CoachRequest,CoachOffer, ) from rowers.models import ( RowerPowerForm,RowerForm,GraphImage,AdvancedWorkoutForm, diff --git a/rowers/views/teamviews.py b/rowers/views/teamviews.py index 0bce82e3..77827712 100644 --- a/rowers/views/teamviews.py +++ b/rowers/views/teamviews.py @@ -166,12 +166,35 @@ def rower_teams_view(request,message='',successmessage=''): 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) + + # user invites (as coach) + mycoachoffers = CoachOffer.objects.filter(coach=r) + # user is invited (by coach) + coachoffers = CoachOffer.objects.filter(user=r.user) + + # user requests a coach + mycoachrequests = CoachRequest.objects.filter(user=r.user) + # user is requested to coach + coachrequests = CoachRequest.objects.filter(coach=r) + + invitedathletes = [rekwest.user for rekwest in mycoachoffers] + invitedcoaches = [rekwest.coach for rekwest in mycoachrequests] + + coaches = rower_get_coaches(r) + potentialcoaches = [t.manager for t in memberteams if t.manager not in coaches ] + potentialcoaches = [c for c in potentialcoaches if c.rower not in invitedcoaches] + coachees = teams.coach_getcoachees(r) + + potentialathletes = Rower.objects.filter( + team__in=myteams).exclude( + user__in=invitedathletes).exclude(user=request.user) + + # clubsize = teams.count_invites(request.user)+teams.count_club_members(request.user) # max_clubsize = r.clubsize @@ -184,6 +207,7 @@ def rower_teams_view(request,message='',successmessage=''): 'name': 'Teams' } ] + return render(request, 'teams.html', { @@ -198,6 +222,14 @@ def rower_teams_view(request,message='',successmessage=''): 'myrequests':myrequests, 'form':form, 'myinvites':myinvites, + 'mycoachrequests':mycoachrequests, + 'mycoachoffers':mycoachoffers, + 'coachrequests':coachrequests, + 'coachoffers':coachoffers, + 'coaches':coaches, + 'potentialcoaches':potentialcoaches, + 'coachees':coachees, + 'potentialathletes':potentialathletes, }) @login_required() @@ -276,6 +308,71 @@ def team_requestmembership_view(request,teamid,userid): return HttpResponseRedirect(url) +@login_required() +def request_coaching_view(request,coachid): + r = getrequestrower(request) + + coach = User.objects.get(id=coachid).rower + + if coach.rowerplan == 'coach': + res,text = teams.create_coaching_request(coach,request.user) + if res: + messages.info(request,text) + else: + messages.error(request,text) + else: + messages.error(request,'That person is not a coach') + + url = reverse('rower_teams_view') + + return HttpResponseRedirect(url) + +@user_passes_test(iscoachmember,login_url="/rowers/paidplans",redirect_field_name=None) +def offer_coaching_view(request,userid): + try: + u = User.objects.get(id=userid) + except User.DoesNotExist: + raise Http404("This user doesn't exist") + + coach = getrequestrower(request) + + res,text = teams.create_coaching_invite(coach,u) + + if res: + message.info(request,text) + else: + messages.error(request,text) + + url = reverse('rower_teams_view') + + return HttpResponseRedirecet(url) + +@login_required() +def reject_revoke_coach_request(request,id=0): + res, text = teams.reject_revoke_coach_request(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 reject_revoke_coach_offer(request,id=0): + res, text = teams.reject_revoke_coach_offer(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 request_revoke_view(request,id=0): res,text = teams.revoke_request(request.user,id) @@ -559,3 +656,27 @@ def team_members_stats_view(request,id): }) return response + +@login_required() +def rower_accept_coachoffer_view(request,code=None): + if code: + res, text = teams.process_coachoffer_code(request.user,code) + if res: + message.info(request,text) + else: + messages.error(request,text) + + url = reverse('rower_teams_view') + return HttpResponseRedirect(url) + +@login_required() +def coach_accept_coachrequest_view(request,code=None): + if code: + res, text = teams.process_coachrequest_code(request.user.rower,code) + if res: + messages.info(request,text) + else: + messages.error(request,text) + + url = reverse('rower_teams_view') + return HttpResponseRedirect(url) From 5f8fd9315a931d456cfcb8812b4ca99dab380f26 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sat, 16 Feb 2019 19:43:21 +0100 Subject: [PATCH 14/18] passing tests again - now with coachinggroups --- rowers/models.py | 26 +++++++++++++++----------- rowers/teams.py | 16 ++++++++-------- rowers/templates/teams.html | 2 +- rowers/templatetags/rowerfilters.py | 10 +++------- rowers/tests/test_permissions.py | 13 ++++++++++--- rowers/tests/testdata/testdata.csv.gz | Bin 12525 -> 12525 bytes rowers/views/teamviews.py | 14 ++++++++------ rowers/views/workoutviews.py | 6 +++--- 8 files changed, 48 insertions(+), 39 deletions(-) diff --git a/rowers/models.py b/rowers/models.py index b4426431..b02e4889 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -633,7 +633,7 @@ class PaidPlan(models.Model): ) class CoachingGroup(models.Model): - pass + name = models.CharField(default='group',max_length=30,null=True,blank=True) # Extension of User with rowing specific data class Rower(models.Model): @@ -1003,13 +1003,15 @@ def checkworkoutuser(user,workout): return False try: r = Rower.objects.get(user=user) - coaches = rower_get_coaches(user.rower) if workout.user == r: return True - elif coaches: - for coach in coaches: - if user.rower == coach and workout.privacy == 'visible': - return True + coaches = [] + for group in workout.user.coachinggroups.all(): + coach = Rower.objects.get(mycoachgroup=group) + coaches.append(coach) + for coach in coaches: + if user.rower == coach and workout.privacy == 'visible': + return True else: return False except Rower.DoesNotExist: @@ -1022,11 +1024,13 @@ def checkaccessuser(user,rower): r = Rower.objects.get(user=user) if rower == r: return True - coaches = rower_get_coaches(rower) - if coaches: - for coach in coaches: - if user.rower == coach: - return True + coaches = [] + for group in rower.coachinggroups.all(): + coach = Rower.objects.get(mycoachgroup=group) + coaches.append(coach) + for coach in coaches: + if user.rower == coach: + return True else: return False except Rower.DoesNotExist: diff --git a/rowers/teams.py b/rowers/teams.py index 91b9e75d..ad9ed73e 100644 --- a/rowers/teams.py +++ b/rowers/teams.py @@ -17,7 +17,8 @@ queuelow = django_rq.get_queue('low') queuehigh = django_rq.get_queue('low') from rowers.models import ( - Rower, Workout, Team, TeamInvite,User,TeamRequest, CoachRequest, CoachOffer + Rower, Workout, Team, TeamInvite,User,TeamRequest, CoachRequest, CoachOffer, + CoachingGroup ) from rowers.tasks import ( @@ -105,15 +106,14 @@ def remove_team(id): def add_coach(coach,rower): # get coaching group - try: - coachgroup = coach.mycoachgroup - except CoachingGroup.DoesNotExist: - coachgroup = CoachingGroup() + coachgroup = coach.mycoachgroup + if coachgroup is None: + coachgroup = CoachingGroup(name=coach.user.first_name) coachgroup.save() coach.mycoachgroup = coachgroup coach.save() - rower.coachinggroups.add(coach) + rower.coachinggroups.add(coach.mycoachgroup) return (1,"Added Coach") @@ -159,7 +159,7 @@ def remove_coach(coach,rower): def rower_get_coaches(rower): coaches = [] - for group in rower.coachinggroups: + for group in rower.coachinggroups.all(): coach = Rower.objects.get(mycoachgroup=group) coaches.append(coach) @@ -167,7 +167,7 @@ def rower_get_coaches(rower): def coach_getcoachees(coach): - return Rower.objects.filter(coachinggroups__in=[coach.mycoachgroup]) + return Rower.objects.filter(coachinggroups__in=[coach.mycoachgroup]).distinct() def coach_remove_athlete(coach,rower): try: diff --git a/rowers/templates/teams.html b/rowers/templates/teams.html index d31b6ae4..59570f7d 100644 --- a/rowers/templates/teams.html +++ b/rowers/templates/teams.html @@ -313,7 +313,7 @@ Offer Coaching + href="/rowers/me/coachoffer/{{ a.user.id }}/">Offer Coaching {% endfor %} diff --git a/rowers/templatetags/rowerfilters.py b/rowers/templatetags/rowerfilters.py index be4413fb..c37c0e6a 100644 --- a/rowers/templatetags/rowerfilters.py +++ b/rowers/templatetags/rowerfilters.py @@ -376,15 +376,11 @@ def team_rowers(user): return [] +from rowers.teams import coach_getcoachees + @register.filter def coach_rowers(user): - try: - members = Rower.objects.filter(coaches__in=user.rower).distinct().order_by( - "user__last_name","user__last_name" - ) - return members - except TypeError: - return [] + return coach_getcoachees(user.rower) @register.filter def verbosetimeperiod(timeperiod): diff --git a/rowers/tests/test_permissions.py b/rowers/tests/test_permissions.py index cda4b9e5..72c43ff1 100644 --- a/rowers/tests/test_permissions.py +++ b/rowers/tests/test_permissions.py @@ -23,6 +23,9 @@ class PermissionsBasicsTests(TestCase): rowerplan='coach') self.ucoach_workouts = WorkoutFactory.create_batch(5, user=self.rcoach) + self.coachinggroup = CoachingGroup.objects.create() + self.rcoach.mycoachgroup = self.coachinggroup + self.rcoach.save() self.factory = RequestFactory() self.password = faker.word() self.ucoach.set_password(self.password) @@ -280,6 +283,9 @@ class PermissionsViewTests(TestCase): rowerplan='coach') self.ucoach_workouts = WorkoutFactory.create_batch(5, user=self.rcoach) + self.coachinggroup = CoachingGroup.objects.create() + self.rcoach.mycoachgroup = self.coachinggroup + self.rcoach.save() self.factory = RequestFactory() self.ucoachpassword = faker.word() self.ucoach.set_password(self.ucoachpassword) @@ -458,7 +464,7 @@ class PermissionsViewTests(TestCase): ## Coach can edit on behalf of athlete if permitted def test_coach_edit_athlete_settings(self): self.rbasic.team.add(self.teamcoach) - self.rbasic.coaches.add(self.rcoach) + self.rbasic.coachinggroups.add(self.coachinggroup) login = self.c.login(username=self.ucoach.username, password=self.ucoachpassword) self.assertTrue(login) @@ -483,7 +489,7 @@ class PermissionsViewTests(TestCase): @patch('rowers.dataprep.read_cols_df_sql', side_effect = mocked_read_df_cols_sql_multistats) def test_coach_edit_athlete_analysis(self,mocked_df): self.rbasic.team.add(self.teamcoach) - self.rbasic.coaches.add(self.rcoach) + self.rbasic.coachinggroups.add(self.coachinggroup) login = self.c.login(username=self.ucoach.username, password=self.ucoachpassword) self.assertTrue(login) @@ -523,7 +529,8 @@ class PermissionsViewTests(TestCase): @patch('rowers.dataprep.getsmallrowdata_db',side_effect=mocked_getsmallrowdata_db) def test_coach_edit_athlete_upload(self,mocked_sqlalchemy,mocked_getsmallrowdata_db): self.rbasic.team.add(self.teamcoach) - self.rbasic.coaches.add(self.rcoach) + self.rbasic.coachinggroups.add(self.coachinggroup) + login = self.c.login(username=self.ucoach.username, password=self.ucoachpassword) self.assertTrue(login) diff --git a/rowers/tests/testdata/testdata.csv.gz b/rowers/tests/testdata/testdata.csv.gz index 7256d94144270203437770568d2c04f40f5a37da..39549454059f23d3023d96cba1b69c6f57433f2f 100644 GIT binary patch delta 16 XcmaEx_%@MUzMF$%UU Date: Sun, 17 Feb 2019 10:36:59 +0100 Subject: [PATCH 15/18] all the plumbing to let people select, drop, offer, coaching --- rowers/models.py | 6 ++ rowers/teams.py | 2 +- rowers/templates/coacheerequestemail.html | 6 +- rowers/templates/coachrequestemail.html | 4 +- rowers/templates/dropathleteconfirm.html | 33 ++++++++ rowers/templates/dropcoachconfirm.html | 35 ++++++++ rowers/templates/teams.html | 18 ++-- rowers/tests/testdata/testdata.csv.gz | Bin 12525 -> 12525 bytes rowers/urls.py | 8 ++ rowers/views/teamviews.py | 95 ++++++++++++++++++++++ templates/newbasefront.html | 2 +- 11 files changed, 193 insertions(+), 16 deletions(-) create mode 100644 rowers/templates/dropathleteconfirm.html create mode 100644 rowers/templates/dropcoachconfirm.html diff --git a/rowers/models.py b/rowers/models.py index b02e4889..a846a2aa 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -634,6 +634,12 @@ class PaidPlan(models.Model): class CoachingGroup(models.Model): name = models.CharField(default='group',max_length=30,null=True,blank=True) + + def __unicode__(self): + return 'Coaching Group {id}: {name}'.format( + id = self.pk, + name = self.name + ) # Extension of User with rowing specific data class Rower(models.Model): diff --git a/rowers/teams.py b/rowers/teams.py index ad9ed73e..25500897 100644 --- a/rowers/teams.py +++ b/rowers/teams.py @@ -178,7 +178,7 @@ def coach_remove_athlete(coach,rower): coach.mycoachgroup = coachgroup coach.save() - rower.coachingrgroups.remove(coachgroup) + rower.coachinggroups.remove(coachgroup) return (1,'Coach removed') diff --git a/rowers/templates/coacheerequestemail.html b/rowers/templates/coacheerequestemail.html index fd0fbaa5..27f0bea4 100644 --- a/rowers/templates/coacheerequestemail.html +++ b/rowers/templates/coacheerequestemail.html @@ -4,7 +4,7 @@

    Dear {{ name }},

    - {{ coachname }} is inviting you to become your coach on rowsandall.com. + {{ coachname }} is offering to become your coach on rowsandall.com.

    By accepting the invite, {{ coachname }} will have access to your @@ -22,8 +22,8 @@

    You can also click the direct link: - - {{ siteurl }}/rowers/me/coacheeinvitation/{{ code }} + + {{ siteurl }}/rowers/me/coachoffer/{{ code }}/accept

    diff --git a/rowers/templates/coachrequestemail.html b/rowers/templates/coachrequestemail.html index 4523aa66..89e557c6 100644 --- a/rowers/templates/coachrequestemail.html +++ b/rowers/templates/coachrequestemail.html @@ -23,8 +23,8 @@

    You can also click the direct link: - - {{ siteurl }}/rowers/me/coachinvitation/{{ code }} + + {{ siteurl }}/rowers/me/coachrequest/{{ code }}/accept/

    diff --git a/rowers/templates/dropathleteconfirm.html b/rowers/templates/dropathleteconfirm.html new file mode 100644 index 00000000..73077b69 --- /dev/null +++ b/rowers/templates/dropathleteconfirm.html @@ -0,0 +1,33 @@ +{% extends "newbase.html" %} +{% load staticfiles %} +{% load rowerfilters %} + +{% block title %}Remove Athlete {% endblock %} + +{% block main %} +

    Confirm removing {{ athlete.user.first_name }} {{ athlete.user.last_name }}

    + +
      +
    • +

      This will remove this rower from your list of athlete. The athlete can still + be in one of your training groups, but you cannot upload workouts, run analysis + or edit settings on their behalf. +

      + + +

      + Cancel +

      + +

      + Drop Athlete +

      +
    • +
    + + +{% endblock %} + +{% block sidebar %} +{% include 'menu_teams.html' %} +{% endblock %} diff --git a/rowers/templates/dropcoachconfirm.html b/rowers/templates/dropcoachconfirm.html new file mode 100644 index 00000000..863c0e2c --- /dev/null +++ b/rowers/templates/dropcoachconfirm.html @@ -0,0 +1,35 @@ +{% extends "newbase.html" %} +{% load staticfiles %} +{% load rowerfilters %} + +{% block title %}Remove Coach {% endblock %} + +{% block main %} +

    Confirm removing {{ coach.user.first_name }} {{ coach.user.last_name }}

    + +
      +
    • +

      This will remove this coach from your list of coaches. + You can still be in this coach's training groups, but she/he is losing the ability to + upload workouts for you, run analysis + or edit your settings on your behalf. +

      + + +

      + Cancel +

      + +

      + Drop Coach + +

      +
    • +
    + + +{% endblock %} + +{% block sidebar %} +{% include 'menu_teams.html' %} +{% endblock %} diff --git a/rowers/templates/teams.html b/rowers/templates/teams.html index 59570f7d..dfa4c8fa 100644 --- a/rowers/templates/teams.html +++ b/rowers/templates/teams.html @@ -5,7 +5,7 @@ {% block main %}
      {% if teams %} -
    • +
    • My Groups

      @@ -31,7 +31,7 @@ {% endif %} {% if otherteams %} -
    • +
    • Open Groups

    • @@ -56,7 +56,7 @@ {% endif %} -
    • +
    • Groups I manage

      {% if myteams %}
    • @@ -83,7 +83,7 @@ New Training Group {% if coaches %} -
    • +
    • My Coaches

    • @@ -99,7 +99,7 @@ @@ -109,7 +109,7 @@ {% endif %} {% if coachees %} -
    • +
    • My Rowers

    • Remove + href="/rowers/coaches/{{ coach.id }}/dropcoachconfirm/">Remove
      @@ -125,7 +125,7 @@ @@ -297,7 +297,7 @@ {% endif %} {% if potentialathletes %} -
    • +
    • Rowers you could coach

    • Remove + href="/rowers/coaches/{{ coachee.id }}/dropconfirm/">Remove
      @@ -322,7 +322,7 @@ {% endif %} {% if potentialcoaches %} -
    • +
    • Coaches who could coach you

    • diff --git a/rowers/tests/testdata/testdata.csv.gz b/rowers/tests/testdata/testdata.csv.gz index 39549454059f23d3023d96cba1b69c6f57433f2f..849a95e6eb515d9d8ff8ae03a64c2b58ad633c9e 100644 GIT binary patch delta 15 WcmaEx_%@MEzMF%iEovj%a{~Z0Vg?HU delta 15 WcmaEx_%@MEzMF$%Uie0~=LP^Y;|3rA diff --git a/rowers/urls.py b/rowers/urls.py index c36f4a5b..71595bbe 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -412,6 +412,14 @@ urlpatterns = [ url(r'^team/(?P\d+)/requestmembership/(?P\d+)/$',views.team_requestmembership_view,name='team_requestmembership_view'), url(r'^me/coachrequest/(?P\d+)/reject/$',views.reject_revoke_coach_request, name='reject_revoke_coach_request'), + url(r'^coaches/(?P\d+)/dropconfirm/$',views.coach_drop_athlete_confirm_view, + name='coach_drop_athlete_confirm_view'), + url(r'^coaches/(?P\d+)/drop/$',views.coach_drop_athlete_view, + name='coach_drop_athlete_view'), + url(r'^coaches/(?P\d+)/dropcoachconfirm/$',views.athlete_drop_coach_confirm_view, + name='athlete_drop_coach_confirm_view'), + url(r'^coaches/(?P\d+)/dropcoach/$',views.athlete_drop_coach_view, + name='athlete_drop_coach_view'), url(r'^me/coachrequest/(?P\d+)/revoke/$',views.reject_revoke_coach_request, name='reject_revoke_coach_request'), url(r'^me/coachoffer/(?P\d+)/reject/$',views.reject_revoke_coach_offer, diff --git a/rowers/views/teamviews.py b/rowers/views/teamviews.py index 27427c69..1546a777 100644 --- a/rowers/views/teamviews.py +++ b/rowers/views/teamviews.py @@ -279,6 +279,101 @@ def manager_requests_view(request,code=None,message='',successmessage=''): }) return HttpResponseRedirect(url) +@login_required() +def athlete_drop_coach_confirm_view(request,id): + r = getrower(request.user) + try: + coach = Rower.objects.get(id=id) + except Rower.DoesNotExist: + raise Http404("This rower doesn't exist") + if coach not in teams.rower_get_coaches(r): + raise PermissionDenied("You are not allowed to do this") + + breadcrumbs = [ + { + 'url':reverse('rower_teams_view'), + 'name': 'Teams' + }, + { + 'url':reverse('athlete_drop_coach_confirm_view',kwargs={'id':id}), + 'name': 'Confirm drop athlete' + } + ] + + return render(request,'dropcoachconfirm.html', + { + 'rower':r, + 'coach':coach + }) + +@login_required() +def coach_drop_athlete_confirm_view(request,id): + r = getrower(request.user) + try: + rower = Rower.objects.get(id=id) + except Rower.DoesNotExist: + raise Http404("This rower doesn't exist") + if rower not in teams.coach_getcoachees(r): + raise PermissionDenied("You are not allowed to do this") + + breadcrumbs = [ + { + 'url':reverse('rower_teams_view'), + 'name': 'Teams' + }, + { + 'url':reverse('coach_drop_athlete_confirm_view',kwargs={'id':id}), + 'name': 'Confirm drop athlete' + } + ] + + return render(request,'dropathleteconfirm.html', + { + 'rower':r, + 'athlete':rower + }) + +@login_required() +def coach_drop_athlete_view(request,id): + r = getrower(request.user) + try: + rower = Rower.objects.get(id=id) + except Rower.DoesNotExist: + raise Http404("This rower doesn't exist") + if rower not in teams.coach_getcoachees(r): + raise PermissionDenied("You are not allowed to do this") + + res,text = teams.coach_remove_athlete(r,rower) + + if res: + messages.info(request,'You are not coaching this athlete any more') + else: + messages.error(request,'There was an error dropping the athlete from your list') + + url = reverse('rower_teams_view') + + return HttpResponseRedirect(url) + +@login_required() +def athlete_drop_coach_view(request,id): + r = getrower(request.user) + try: + coach = Rower.objects.get(id=id) + except Rower.DoesNotExist: + raise Http404("This coach doesn't exist") + if coach not in teams.rower_get_coaches(r): + raise PermissionDenied("You are not allowed to do this") + + res,text = teams.coach_remove_athlete(coach,r) + + if res: + messages.info(request,'Removal successful') + else: + messages.error(request,'There was an error dropping the coach from your list') + + url = reverse('rower_teams_view') + + return HttpResponseRedirect(url) @login_required() def team_requestmembership_view(request,teamid,userid): diff --git a/templates/newbasefront.html b/templates/newbasefront.html index a131aeb8..791d6dd7 100644 --- a/templates/newbasefront.html +++ b/templates/newbasefront.html @@ -227,7 +227,7 @@ From 158a1e795662065f8b20797725b73dab54bf51db Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sun, 17 Feb 2019 10:58:51 +0100 Subject: [PATCH 16/18] fixed background jobs in tests --- rowers/dataprep.py | 41 ++++++++++++++------------ rowers/templates/teams.html | 10 +++---- rowers/tests/testdata/testdata.csv.gz | Bin 12525 -> 12525 bytes 3 files changed, 27 insertions(+), 24 deletions(-) diff --git a/rowers/dataprep.py b/rowers/dataprep.py index b5cb5603..066a4579 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -692,10 +692,12 @@ def runcpupdate( theids = [w.id for w in theworkouts] - if settings.DEBUG: - job = handle_updatecp.delay(rower.id,theids,debug=True,table=table) - else: - job = queue.enqueue(handle_updatecp,rower.id,theids,table=table) + job = myqueue( + queue, + handle_updatecp, + rower.id, + theids, + table=table) return job @@ -704,10 +706,11 @@ def fetchcperg(rower,theworkouts): thefilenames = [w.csvfilename for w in theworkouts] cpdf = getcpdata_sql(rower.id,table='ergcpdata') - if settings.DEBUG: - res = handle_updateergcp.delay(rower.id,thefilenames,debug=True) - else: - res = queue.enqueue(handle_updateergcp,rower.id,thefilenames) + job = myqueue( + queue, + handle_updateergcp, + rower.id, + thefilenames) return cpdf @@ -738,10 +741,12 @@ def fetchcp(rower,theworkouts,table='cpdata'): if not cpdf.empty: return cpdf['delta'],cpdf['cp'],avgpower2 else: - if settings.DEBUG: - res = handle_updatecp.delay(rower.id,theids,debug=True,table=table) - else: - res = queue.enqueue(handle_updatecp,rower.id,theids,table=table) + job = myqueue(queue, + handle_updatecp, + rower.id, + theids, + table=table) + return [],[],avgpower2 @@ -1315,13 +1320,11 @@ def new_workout_from_file(r, f2, message = "We couldn't recognize the file type" f4 = f2[:-5]+'a'+f2[-5:] copyfile(f2,f4) - if settings.DEBUG: - res = handle_sendemail_unrecognized.delay(f4, - r.user.email) - - else: - res = queuehigh.enqueue(handle_sendemail_unrecognized, - f4, r.user.email) + job = myqueue(queuehigh, + handle_sendemail_unrecognized, + f4, + r.user.email) + return (0, message, f2) # handle non-Painsled by converting it to painsled compatible CSV diff --git a/rowers/templates/teams.html b/rowers/templates/teams.html index dfa4c8fa..22e6ef47 100644 --- a/rowers/templates/teams.html +++ b/rowers/templates/teams.html @@ -63,7 +63,7 @@ - + @@ -80,7 +80,7 @@
      NameManager 
      {% endif %} - New Training Group + Create New Training Group
    • {% if coaches %}
    • @@ -223,9 +223,9 @@ {% endif %} {% if mycoachrequests or mycoachoffers or coachoffers or coachrequests %}
    • -

      Coaching Invitations and Requests

      -

      This section lists open invites related to coaching. - By accepting a coaching invite, the coach can run +

      Coaching Offers and Requests

      +

      This section lists open offers and requests related to coaching. + By accepting a coaching offer, the coach can run analysis, add workouts and edit settings on behalf of the athlete. You agree to the sharing of personal data between athletes and coaches according to diff --git a/rowers/tests/testdata/testdata.csv.gz b/rowers/tests/testdata/testdata.csv.gz index 849a95e6eb515d9d8ff8ae03a64c2b58ad633c9e..ce0b4dd78ecdd2dd12ad16a99b77568039f6de05 100644 GIT binary patch delta 16 XcmaEx_%@MUzMF%?#UOJd`!fRoH}3`s delta 16 XcmaEx_%@MUzMF%iEh=Lp`!fRoI Date: Sun, 17 Feb 2019 14:30:29 +0100 Subject: [PATCH 17/18] fixed RQ issue - bg tasks now working on linux notebook --- rowers/braintreestuff.py | 14 +++- rowers/tasks.py | 4 +- rowers/tests/test_permissions.py | 101 ++++++++++++++++++++++++++ rowers/tests/testdata/testdata.csv.gz | Bin 12525 -> 12525 bytes rowers/utils.py | 5 +- rowsandall_app/settings.py | 6 ++ 6 files changed, 124 insertions(+), 6 deletions(-) diff --git a/rowers/braintreestuff.py b/rowers/braintreestuff.py index 4de44b42..68dead51 100644 --- a/rowers/braintreestuff.py +++ b/rowers/braintreestuff.py @@ -171,9 +171,17 @@ def update_subscription(rower,data,method='up'): ) if rower.paidplan != 'coach': - coachees = Rower.objects.filter(coach__in=[rower]).distinct() - for coachee in coachees: - coachee.coaches.remove(rower) + try: + coachgroup = coach.mycoachgroup + except CoachingGroup.DoesNotExist: + coachgroup = CoachingGroup() + coachgroup.save() + rower.mycoachgroup = coachgroup + rower.save() + + athletes = Rower.objects.filter(coachinggroups__in=[rower.mycoachgroup]).distinct() + for athlete in athletes: + athlete.coachinggroups.remove(rower.mycoachgroup) if method == 'up': transactions = result.subscription.transactions diff --git a/rowers/tasks.py b/rowers/tasks.py index 21531d98..fefe1b29 100644 --- a/rowers/tasks.py +++ b/rowers/tasks.py @@ -1705,7 +1705,7 @@ def handle_sendemail_coachrequest(email,name,code,coachname, fullemail = email subject = 'Invitation to add {n} to your athletes'.format(n=name) - + from_email = 'Rowsandall ' siteurl = SITE_URL if debug: siteurl = SITE_URL_DEV @@ -1731,7 +1731,7 @@ def handle_sendemail_coacheerequest(email,name,code,coachname, fullemail = email subject = '{n} asks coach access to your data on rowsandall.com'.format(n=coachname) - + from_email = 'Rowsandall ' siteurl = SITE_URL if debug: siteurl = SITE_URL_DEV diff --git a/rowers/tests/test_permissions.py b/rowers/tests/test_permissions.py index 72c43ff1..af9101ae 100644 --- a/rowers/tests/test_permissions.py +++ b/rowers/tests/test_permissions.py @@ -999,6 +999,107 @@ class PermissionsViewTests(TestCase): status_code=302,target_status_code=200) +@override_settings(TESTING=True) +class PermissionsCoachingTests(TestCase): + def setUp(self): + self.c = Client() + ## Users - Pro, Basic, Coach & Self Coach + + self.ucoach = UserFactory(username='coachuser') + self.rcoach = Rower.objects.create(user=self.ucoach, + birthdate=faker.profile()['birthdate'], + gdproptin=True,gdproptindate=timezone.now(), + rowerplan='coach') + + self.ucoach_workouts = WorkoutFactory.create_batch(5, user=self.rcoach) + self.coachinggroup = CoachingGroup.objects.create() + self.rcoach.mycoachgroup = self.coachinggroup + self.rcoach.save() + self.factory = RequestFactory() + self.password = faker.word() + self.ucoach.set_password(self.password) + self.ucoach.save() + + self.uplan = UserFactory(username='planuser') + self.rplan = Rower.objects.create(user=self.uplan, + birthdate=faker.profile()['birthdate'], + gdproptin=True,gdproptindate=timezone.now(), + rowerplan='plan') + + self.uplan_workouts = WorkoutFactory.create_batch(5, user=self.rplan) + self.factory = RequestFactory() + self.password = faker.word() + self.uplan.set_password(self.password) + self.uplan.save() + + self.upro = UserFactory(username='prouser') + self.rpro = Rower.objects.create(user=self.upro, + birthdate=faker.profile()['birthdate'], + gdproptin=True,gdproptindate=timezone.now(), + rowerplan='pro') + + self.upro_workouts = WorkoutFactory.create_batch(5, user=self.rpro) + self.factory = RequestFactory() + self.password = faker.word() + self.upro.set_password(self.password) + self.upro.save() + + self.uplan2 = UserFactory(username='planuser2') + self.rplan2 = Rower.objects.create(user=self.uplan2, + birthdate=faker.profile()['birthdate'], + gdproptin=True,gdproptindate=timezone.now(), + rowerplan='plan') + + self.uplan2_workouts = WorkoutFactory.create_batch(5, user=self.rplan2) + self.factory = RequestFactory() + self.password = faker.word() + self.uplan2.set_password(self.password) + self.uplan2.save() + + self.upro2 = UserFactory(username='prouser2') + self.rpro2 = Rower.objects.create(user=self.upro2, + birthdate=faker.profile()['birthdate'], + gdproptin=True,gdproptindate=timezone.now(), + rowerplan='pro') + + self.upro2_workouts = WorkoutFactory.create_batch(5, user=self.rpro2) + self.factory = RequestFactory() + self.password = faker.word() + self.upro2.set_password(self.password) + self.upro2.save() + + self.ubasic = UserFactory(username='basicuser') + self.rbasic = Rower.objects.create(user=self.ubasic, + birthdate=faker.profile()['birthdate'], + gdproptin=True,gdproptindate=timezone.now(), + rowerplan='basic') + + self.ubasic_workouts = WorkoutFactory.create_batch(5, user=self.rbasic) + self.factory = RequestFactory() + self.password = faker.word() + self.ubasic.set_password(self.password) + self.ubasic.save() + + + + ## TeamPro, TeamCoach, TeamSelfCoach + + self.teampro = Team.objects.create( + name=faker.word()+'1', + notes=faker.text(), + manager=self.upro2) + + self.teamplan = Team.objects.create( + name=faker.word()+'2', + notes=faker.text(), + manager=self.uplan2) + + self.teamcoach = Team.objects.create( + name=faker.word()+'3', + notes=faker.text(), + manager=self.ucoach) + + # coach related ## coach disappears from list when downgrading diff --git a/rowers/tests/testdata/testdata.csv.gz b/rowers/tests/testdata/testdata.csv.gz index ce0b4dd78ecdd2dd12ad16a99b77568039f6de05..aa231cedb35e3f7e218f7559f7e3834af3d3c80c 100644 GIT binary patch delta 15 WcmaEx_%@MEzMF%i&SE3ma{~Y~hy~#Q delta 15 WcmaEx_%@MEzMF%?#b6`ba{~Y}1O=Y} diff --git a/rowers/utils.py b/rowers/utils.py index 3cc90707..11905555 100644 --- a/rowers/utils.py +++ b/rowers/utils.py @@ -298,11 +298,14 @@ def myqueue(queue,function,*args,**kwargs): if settings.TESTING: return MockJob() - if settings.DEBUG: + if settings.CELERY: kwargs['debug'] = True job = function.delay(*args,**kwargs) else: + if settings.DEBUG: + kwargs['debug'] = True + job_id = str(uuid.uuid4()) kwargs['job_id'] = job_id kwargs['jobkey'] = job_id diff --git a/rowsandall_app/settings.py b/rowsandall_app/settings.py index 9b66387d..145a972b 100644 --- a/rowsandall_app/settings.py +++ b/rowsandall_app/settings.py @@ -467,3 +467,9 @@ try: OPAQUE_SECRET_KEY = CFG['opaque_secret_key'] except KeyError: OPAQUE_SECRET_KEY = 0xa193443a + +# Celery or RQ +try: + CELERY = CFG['use_celery'] +except KeyError: + CELERY = False From 92334ca4efa4c4218759c0b09b2333e258608431 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Sun, 17 Feb 2019 15:20:54 +0100 Subject: [PATCH 18/18] adding coach/athlete offer request testing --- rowers/braintreestuff.py | 2 +- rowers/tests/test_permissions.py | 317 +++++++++++++++++++++++++- rowers/tests/testdata/testdata.csv.gz | Bin 12525 -> 12525 bytes rowers/urls.py | 6 +- 4 files changed, 315 insertions(+), 10 deletions(-) diff --git a/rowers/braintreestuff.py b/rowers/braintreestuff.py index 68dead51..7d6a5fbd 100644 --- a/rowers/braintreestuff.py +++ b/rowers/braintreestuff.py @@ -44,7 +44,7 @@ else: ) -from rowers.models import Rower,PaidPlan +from rowers.models import Rower,PaidPlan, CoachingGroup from rowers.utils import ProcessorCustomerError def create_customer(rower,force=False): diff --git a/rowers/tests/test_permissions.py b/rowers/tests/test_permissions.py index af9101ae..dc7ecdc2 100644 --- a/rowers/tests/test_permissions.py +++ b/rowers/tests/test_permissions.py @@ -87,8 +87,8 @@ class PermissionsBasicsTests(TestCase): self.ubasic_workouts = WorkoutFactory.create_batch(5, user=self.rbasic) self.factory = RequestFactory() - self.password = faker.word() - self.ubasic.set_password(self.password) + self.ubasicpassword = faker.word() + self.ubasic.set_password(self.ubasicpassword) self.ubasic.save() @@ -1016,8 +1016,8 @@ class PermissionsCoachingTests(TestCase): self.rcoach.mycoachgroup = self.coachinggroup self.rcoach.save() self.factory = RequestFactory() - self.password = faker.word() - self.ucoach.set_password(self.password) + self.ucoachpassword = faker.word() + self.ucoach.set_password(self.ucoachpassword) self.ucoach.save() self.uplan = UserFactory(username='planuser') @@ -1076,8 +1076,8 @@ class PermissionsCoachingTests(TestCase): self.ubasic_workouts = WorkoutFactory.create_batch(5, user=self.rbasic) self.factory = RequestFactory() - self.password = faker.word() - self.ubasic.set_password(self.password) + self.ubasicpassword = faker.word() + self.ubasic.set_password(self.ubasicpassword) self.ubasic.save() @@ -1099,6 +1099,311 @@ class PermissionsCoachingTests(TestCase): notes=faker.text(), manager=self.ucoach) + def test_athlete_request_athlete_revoke(self): + login = self.c.login(username=self.ubasic.username,password=self.ubasicpassword) + self.assertTrue(login) + + url = reverse('request_coaching_view',kwargs={'coachid':self.rcoach.id}) + + expected_url = reverse('rower_teams_view') + + response = self.c.get(url,follow=True) + self.assertRedirects(response, + expected_url=expected_url, + status_code=302,target_status_code=200) + + coachingrequests = CoachRequest.objects.filter(user=self.ubasic) + self.assertEqual(len(coachingrequests),1) + + rekwest = coachingrequests[0] + + url = reverse('reject_revoke_coach_request',kwargs={'id':rekwest.id}) + response = self.c.get(url,follow=True) + self.assertRedirects(response, + expected_url=expected_url, + status_code=302,target_status_code=200) + + coachingrequests = CoachRequest.objects.filter(user=self.ubasic) + self.assertEqual(len(coachingrequests),0) + + def test_coach_offer_coach_revoke(self): + login = self.c.login(username=self.ucoach.username,password=self.ucoachpassword) + self.assertTrue(login) + + url = reverse('offer_coaching_view',kwargs={'userid':self.ubasic.id}) + + expected_url = reverse('rower_teams_view') + + response = self.c.get(url,follow=True) + self.assertRedirects(response, + expected_url=expected_url, + status_code=302,target_status_code=200) + + coachingrequests = CoachOffer.objects.filter(user=self.ubasic) + self.assertEqual(len(coachingrequests),1) + + rekwest = coachingrequests[0] + + url = reverse('reject_revoke_coach_offer',kwargs={'id':rekwest.id}) + response = self.c.get(url,follow=True) + self.assertRedirects(response, + expected_url=expected_url, + status_code=302,target_status_code=200) + + coachingrequests = CoachOffer.objects.filter(user=self.ubasic) + self.assertEqual(len(coachingrequests),0) + + + def test_athlete_request_coach_reject(self): + login = self.c.login(username=self.ubasic.username,password=self.ubasicpassword) + self.assertTrue(login) + + url = reverse('request_coaching_view',kwargs={'coachid':self.rcoach.id}) + + expected_url = reverse('rower_teams_view') + + response = self.c.get(url,follow=True) + self.assertRedirects(response, + expected_url=expected_url, + status_code=302,target_status_code=200) + + coachingrequests = CoachRequest.objects.filter(user=self.ubasic) + self.assertEqual(len(coachingrequests),1) + + rekwest = coachingrequests[0] + + login = self.c.login(username=self.ucoach.username,password=self.ucoachpassword) + self.assertTrue(login) + + url = reverse('reject_revoke_coach_request',kwargs={'id':rekwest.id}) + response = self.c.get(url,follow=True) + self.assertRedirects(response, + expected_url=expected_url, + status_code=302,target_status_code=200) + + coachingrequests = CoachRequest.objects.filter(user=self.ubasic) + self.assertEqual(len(coachingrequests),0) + + def test_coach_offer_athlete_reject(self): + login = self.c.login(username=self.ucoach.username,password=self.ucoachpassword) + self.assertTrue(login) + + url = reverse('offer_coaching_view',kwargs={'userid':self.ubasic.id}) + + expected_url = reverse('rower_teams_view') + + response = self.c.get(url,follow=True) + self.assertRedirects(response, + expected_url=expected_url, + status_code=302,target_status_code=200) + + coachingrequests = CoachOffer.objects.filter(user=self.ubasic) + self.assertEqual(len(coachingrequests),1) + + rekwest = coachingrequests[0] + + login = self.c.login(username=self.ubasic.username,password=self.ubasicpassword) + self.assertTrue(login) + + url = reverse('reject_revoke_coach_offer',kwargs={'id':rekwest.id}) + response = self.c.get(url,follow=True) + self.assertRedirects(response, + expected_url=expected_url, + status_code=302,target_status_code=200) + + coachingrequests = CoachOffer.objects.filter(user=self.ubasic) + self.assertEqual(len(coachingrequests),0) + + + def test_athlete_request_coach_accept_coach_drop(self): + login = self.c.login(username=self.ubasic.username,password=self.ubasicpassword) + self.assertTrue(login) + + url = reverse('request_coaching_view',kwargs={'coachid':self.rcoach.id}) + + expected_url = reverse('rower_teams_view') + + response = self.c.get(url,follow=True) + self.assertRedirects(response, + expected_url=expected_url, + status_code=302,target_status_code=200) + + coachingrequests = CoachRequest.objects.filter(user=self.ubasic) + self.assertEqual(len(coachingrequests),1) + + rekwest = coachingrequests[0] + + login = self.c.login(username=self.ucoach.username,password=self.ucoachpassword) + self.assertTrue(login) + + url = reverse('coach_accept_coachrequest_view',kwargs={'code':rekwest.code}) + response = self.c.get(url,follow=True) + self.assertRedirects(response, + expected_url=expected_url, + status_code=302,target_status_code=200) + + coachingrequests = CoachRequest.objects.filter(user=self.ubasic) + self.assertEqual(len(coachingrequests),0) + + coaches = teams.rower_get_coaches(self.rbasic) + self.assertEqual(coaches[0],self.rcoach) + + url = reverse('coach_drop_athlete_confirm_view',kwargs={'id':self.rbasic.id}) + + response = self.c.get(url) + self.assertEqual(response.status_code,200) + + url = reverse('coach_drop_athlete_view',kwargs={'id':self.rbasic.id}) + + response = self.c.get(url,follow=True) + self.assertRedirects(response, + expected_url=expected_url, + status_code=302,target_status_code=200) + + coaches = teams.rower_get_coaches(self.rbasic) + self.assertEqual(len(coaches),0) + + def test_coach_offer_athlete_accept_athlete_drop(self): + login = self.c.login(username=self.ucoach.username,password=self.ucoachpassword) + self.assertTrue(login) + + url = reverse('offer_coaching_view',kwargs={'userid':self.ubasic.id}) + + expected_url = reverse('rower_teams_view') + + response = self.c.get(url,follow=True) + self.assertRedirects(response, + expected_url=expected_url, + status_code=302,target_status_code=200) + + coachingrequests = CoachOffer.objects.filter(user=self.ubasic) + self.assertEqual(len(coachingrequests),1) + + rekwest = coachingrequests[0] + + login = self.c.login(username=self.ubasic.username,password=self.ubasicpassword) + self.assertTrue(login) + + url = reverse('rower_accept_coachoffer_view',kwargs={'code':rekwest.code}) + response = self.c.get(url,follow=True) + self.assertRedirects(response, + expected_url=expected_url, + status_code=302,target_status_code=200) + + coachingrequests = CoachOffer.objects.filter(user=self.ubasic) + self.assertEqual(len(coachingrequests),0) + + coaches = teams.rower_get_coaches(self.rbasic) + self.assertEqual(coaches[0],self.rcoach) + + url = reverse('athlete_drop_coach_view',kwargs={'id':self.rcoach.id}) + + response = self.c.get(url,follow=True) + self.assertRedirects(response, + expected_url=expected_url, + status_code=302,target_status_code=200) + + coaches = teams.rower_get_coaches(self.rbasic) + self.assertEqual(len(coaches),0) + + def test_coach_offer_athlete_accept_coach_drop(self): + login = self.c.login(username=self.ucoach.username,password=self.ucoachpassword) + self.assertTrue(login) + + url = reverse('offer_coaching_view',kwargs={'userid':self.ubasic.id}) + + expected_url = reverse('rower_teams_view') + + response = self.c.get(url,follow=True) + self.assertRedirects(response, + expected_url=expected_url, + status_code=302,target_status_code=200) + + coachingrequests = CoachOffer.objects.filter(user=self.ubasic) + self.assertEqual(len(coachingrequests),1) + + rekwest = coachingrequests[0] + + login = self.c.login(username=self.ubasic.username,password=self.ubasicpassword) + self.assertTrue(login) + + url = reverse('rower_accept_coachoffer_view',kwargs={'code':rekwest.code}) + response = self.c.get(url,follow=True) + self.assertRedirects(response, + expected_url=expected_url, + status_code=302,target_status_code=200) + + coachingrequests = CoachOffer.objects.filter(user=self.ubasic) + self.assertEqual(len(coachingrequests),0) + + coaches = teams.rower_get_coaches(self.rbasic) + self.assertEqual(coaches[0],self.rcoach) + + login = self.c.login(username=self.ucoach.username,password=self.ucoachpassword) + self.assertTrue(login) + + url = reverse('coach_drop_athlete_view',kwargs={'id':self.rbasic.id}) + + response = self.c.get(url,follow=True) + self.assertRedirects(response, + expected_url=expected_url, + status_code=302,target_status_code=200) + + coaches = teams.rower_get_coaches(self.rbasic) + self.assertEqual(len(coaches),0) + + + def test_athlete_request_coach_accept_athlete_drop(self): + login = self.c.login(username=self.ubasic.username,password=self.ubasicpassword) + self.assertTrue(login) + + url = reverse('request_coaching_view',kwargs={'coachid':self.rcoach.id}) + + expected_url = reverse('rower_teams_view') + + response = self.c.get(url,follow=True) + self.assertRedirects(response, + expected_url=expected_url, + status_code=302,target_status_code=200) + + coachingrequests = CoachRequest.objects.filter(user=self.ubasic) + self.assertEqual(len(coachingrequests),1) + + rekwest = coachingrequests[0] + + login = self.c.login(username=self.ucoach.username,password=self.ucoachpassword) + self.assertTrue(login) + + url = reverse('coach_accept_coachrequest_view',kwargs={'code':rekwest.code}) + response = self.c.get(url,follow=True) + self.assertRedirects(response, + expected_url=expected_url, + status_code=302,target_status_code=200) + + coachingrequests = CoachRequest.objects.filter(user=self.ubasic) + self.assertEqual(len(coachingrequests),0) + + coaches = teams.rower_get_coaches(self.rbasic) + self.assertEqual(coaches[0],self.rcoach) + + login = self.c.login(username=self.ubasic.username,password=self.ubasicpassword) + self.assertTrue(login) + + url = reverse('athlete_drop_coach_confirm_view',kwargs={'id':self.rcoach.id}) + + response = self.c.get(url) + self.assertEqual(response.status_code,200) + + url = reverse('athlete_drop_coach_view',kwargs={'id':self.rcoach.id}) + + response = self.c.get(url,follow=True) + self.assertRedirects(response, + expected_url=expected_url, + status_code=302,target_status_code=200) + + coaches = teams.rower_get_coaches(self.rbasic) + self.assertEqual(len(coaches),0) + # coach related diff --git a/rowers/tests/testdata/testdata.csv.gz b/rowers/tests/testdata/testdata.csv.gz index aa231cedb35e3f7e218f7559f7e3834af3d3c80c..b9f3e726e4c17c10f90a6afa30578d280985ebb7 100644 GIT binary patch delta 15 WcmaEx_%@MEzMF$XH+Li3a{~Y}s|CdX delta 15 WcmaEx_%@MEzMF%i&SE3ma{~Y~hy~#Q diff --git a/rowers/urls.py b/rowers/urls.py index 71595bbe..e602430b 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -430,10 +430,10 @@ urlpatterns = [ name='request_coaching_view'), url(r'^me/coachoffer/(?P\d+)/$',views.offer_coaching_view, name='offer_coaching_view'), - url(r'^me/coachrequest/(\w+.*)/accept/$',views.coach_accept_coachrequest_view, + url(r'^me/coachrequest/(?P\w+.*)/accept/$',views.coach_accept_coachrequest_view, name='coach_accept_coachrequest_view'), - url(r'^me/coachoffer/(\w+.*)/accept/$',views.rower_accept_coachoffer_view, - name='rower_accept_coachofer_view'), + url(r'^me/coachoffer/(?P\w+.*)/accept/$',views.rower_accept_coachoffer_view, + name='rower_accept_coachoffer_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'),