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/braintreestuff.py b/rowers/braintreestuff.py
index bfe978bc..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):
@@ -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,12 +162,27 @@ 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,
l = rower.user.last_name,
)
+ if rower.paidplan != 'coach':
+ 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/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/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/models.py b/rowers/models.py
index 906c959e..a846a2aa 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,15 +334,34 @@ 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')
viewing = models.CharField(max_length=30,choices=viewchoices,default='allmembers',verbose_name='Sharing Behavior')
+
def __unicode__(self):
return self.name
+ def save(self, *args, **kwargs):
+ manager = self.manager
+ if manager.rower.rowerplan == 'basic':
+ 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']:
+ 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):
class Meta:
model = Team
@@ -360,7 +386,6 @@ class TeamInviteForm(ModelForm):
-
class TeamRequest(models.Model):
team = models.ForeignKey(Team)
@@ -607,6 +632,14 @@ class PaidPlan(models.Model):
paymentprocessor = self.paymentprocessor,
)
+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):
@@ -671,8 +704,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
@@ -806,6 +839,8 @@ class Rower(models.Model):
# Friends/Team
friends = 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)
@@ -829,6 +864,7 @@ class Rower(models.Model):
def clean_email(self):
return self.user.email.lower()
+
class DeactivateUserForm(forms.ModelForm):
class Meta:
model = User
@@ -841,13 +877,45 @@ class DeleteUserForm(forms.ModelForm):
class Meta:
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
-@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':
+ 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)
+
+
+#@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()
@@ -941,13 +1009,15 @@ def checkworkoutuser(user,workout):
return False
try:
r = Rower.objects.get(user=user)
- teams = workout.team.all()
if workout.user == r:
return True
- elif teams:
- for team in teams:
- if user == team.manager 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:
@@ -958,17 +1028,19 @@ 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():
- 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:
- return False
+ return False
timezones = (
(x,x) for x in pytz.common_timezones
@@ -1134,6 +1206,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
@@ -1179,6 +1257,7 @@ class TrainingPlan(models.Model):
else:
createmacrofillers(self)
+
class TrainingPlanForm(ModelForm):
class Meta:
model = TrainingPlan
@@ -1548,6 +1627,8 @@ class TrainingMacroCycle(models.Model):
meso.save()
else:
createmesofillers(self)
+
+
class TrainingMacroCycleForm(ModelForm):
class Meta:
@@ -1634,6 +1715,7 @@ class TrainingMesoCycle(models.Model):
else:
createmicrofillers(self)
+
class TrainingMicroCycle(models.Model):
plan = models.ForeignKey(TrainingMesoCycle)
name = models.CharField(max_length=150,blank=True)
@@ -1848,6 +1930,15 @@ 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']:
+ 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
if self.sessionmode == 'distance':
@@ -3368,3 +3459,4 @@ class PlannedSessionCommentForm(ModelForm):
'comment': forms.Textarea,
}
+
diff --git a/rowers/tasks.py b/rowers/tasks.py
index 7f4b756c..fefe1b29 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)
+ from_email = 'Rowsandall '
+ 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)
+ from_email = 'Rowsandall '
+ 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 6bdbf566..25500897 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
+ Rower, Workout, Team, TeamInvite,User,TeamRequest, CoachRequest, CoachOffer,
+ CoachingGroup
)
from rowers.tasks import (
@@ -26,8 +27,11 @@ 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
+
# Low level functions - to be called by higher level methods
inviteduration = 14 # days
@@ -61,6 +65,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')
+ 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')
+
try:
t = Team(name=name,manager=manager,notes=notes,
private=private,viewing=viewing)
@@ -77,29 +89,47 @@ 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')
+def add_coach(coach,rower):
+ # get coaching group
+ 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.mycoachgroup)
+
+ return (1,"Added Coach")
+
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)
res = handle_add_workouts_team(ws,t)
- set_teamplanexpires(rower)
+# set_teamplanexpires(rower)
return (id,'Member added')
@@ -111,9 +141,47 @@ def remove_member(id,rower):
res = handle_remove_workouts_team(ws,t)
- set_teamplanexpires(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.all():
+ coach = Rower.objects.get(mycoachgroup=group)
+ coaches.append(coach)
+
+ return coaches
+
+
+def coach_getcoachees(coach):
+ return Rower.objects.filter(coachinggroups__in=[coach.mycoachgroup]).distinct()
+
+def coach_remove_athlete(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 mgr_remove_member(id,manager,rower):
t = Team.objects.get(id=id)
if t.manager == manager:
@@ -141,31 +209,100 @@ 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)
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')
+
+# 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:
@@ -189,21 +326,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):
@@ -220,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)
@@ -402,7 +566,13 @@ 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
@@ -421,6 +591,10 @@ 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
@@ -433,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..27f0bea4
--- /dev/null
+++ b/rowers/templates/coacheerequestemail.html
@@ -0,0 +1,32 @@
+{% extends "emailbase.html" %}
+
+{% block body %}
+
Dear {{ name }},
+
+
+ {{ coachname }} is offering 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
+
+ {{ 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
+
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.
+
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.
+
- {% endif %}
+ {% 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/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 %}
diff --git a/rowers/templates/team.html b/rowers/templates/team.html
index 3f31596f..242abdd5 100644
--- a/rowers/templates/team.html
+++ b/rowers/templates/team.html
@@ -39,7 +39,7 @@
{% for member in members %}
{% 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
+
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
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.
-
Team
+
Group
Manager
User
Action
@@ -173,7 +221,131 @@
{% endif %}
+ {% if mycoachrequests or mycoachoffers or coachoffers or coachrequests %}
+
+
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
+ our privacy policy.
+