Private
Public Access
1
0

Merge branch 'feature/newpermissions' into develop

This commit is contained in:
Sander Roosendaal
2019-02-17 15:21:23 +01:00
36 changed files with 2604 additions and 216 deletions
+2 -2
View File
@@ -24,7 +24,7 @@ class RowerInline(admin.StackedInline):
('Billing Details', ('Billing Details',
{'fields':('street_address','city','postal_code','country','paymentprocessor','customer_id')}), {'fields':('street_address','city','postal_code','country','paymentprocessor','customer_id')}),
('Rower Plan', ('Rower Plan',
{'fields':('paidplan','rowerplan','paymenttype','planexpires','teamplanexpires','clubsize','protrialexpires','plantrialexpires',)}), {'fields':('paidplan','rowerplan','paymenttype','planexpires','teamplanexpires','protrialexpires','plantrialexpires',)}),
('Rower Settings', ('Rower Settings',
{'fields': {'fields':
('gdproptin','gdproptindate','weightcategory','sex','adaptiveclass','birthdate','getemailnotifications', ('gdproptin','gdproptindate','weightcategory','sex','adaptiveclass','birthdate','getemailnotifications',
@@ -128,7 +128,7 @@ class IndoorVirtualRaceResultAdmin(admin.ModelAdmin):
search_fields = ['race__name','username'] search_fields = ['race__name','username']
class PaidPlanAdmin(admin.ModelAdmin): 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.unregister(User)
admin.site.register(User,UserAdmin) admin.site.register(User,UserAdmin)
+17 -1
View File
@@ -44,7 +44,7 @@ else:
) )
from rowers.models import Rower,PaidPlan from rowers.models import Rower,PaidPlan, CoachingGroup
from rowers.utils import ProcessorCustomerError from rowers.utils import ProcessorCustomerError
def create_customer(rower,force=False): def create_customer(rower,force=False):
@@ -154,6 +154,7 @@ def update_subscription(rower,data,method='up'):
return False,0 return False,0
if result.is_success: if result.is_success:
yesterday = (timezone.now()-datetime.timedelta(days=1)).date()
rower.paidplan = plan rower.paidplan = plan
rower.planexpires = result.subscription.billing_period_end_date rower.planexpires = result.subscription.billing_period_end_date
rower.teamplanexpires = 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.paymenttype = plan.paymenttype
rower.rowerplan = plan.shortname rower.rowerplan = plan.shortname
rower.subscription_id = result.subscription.id rower.subscription_id = result.subscription.id
rower.protrialexpires = yesterday
rower.plantrialexpires = yesterday
rower.save() rower.save()
name = '{f} {l}'.format( name = '{f} {l}'.format(
f = rower.user.first_name, f = rower.user.first_name,
l = rower.user.last_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': if method == 'up':
transactions = result.subscription.transactions transactions = result.subscription.transactions
+20 -17
View File
@@ -692,10 +692,12 @@ def runcpupdate(
theids = [w.id for w in theworkouts] theids = [w.id for w in theworkouts]
if settings.DEBUG: job = myqueue(
job = handle_updatecp.delay(rower.id,theids,debug=True,table=table) queue,
else: handle_updatecp,
job = queue.enqueue(handle_updatecp,rower.id,theids,table=table) rower.id,
theids,
table=table)
return job return job
@@ -704,10 +706,11 @@ def fetchcperg(rower,theworkouts):
thefilenames = [w.csvfilename for w in theworkouts] thefilenames = [w.csvfilename for w in theworkouts]
cpdf = getcpdata_sql(rower.id,table='ergcpdata') cpdf = getcpdata_sql(rower.id,table='ergcpdata')
if settings.DEBUG: job = myqueue(
res = handle_updateergcp.delay(rower.id,thefilenames,debug=True) queue,
else: handle_updateergcp,
res = queue.enqueue(handle_updateergcp,rower.id,thefilenames) rower.id,
thefilenames)
return cpdf return cpdf
@@ -738,10 +741,12 @@ def fetchcp(rower,theworkouts,table='cpdata'):
if not cpdf.empty: if not cpdf.empty:
return cpdf['delta'],cpdf['cp'],avgpower2 return cpdf['delta'],cpdf['cp'],avgpower2
else: else:
if settings.DEBUG: job = myqueue(queue,
res = handle_updatecp.delay(rower.id,theids,debug=True,table=table) handle_updatecp,
else: rower.id,
res = queue.enqueue(handle_updatecp,rower.id,theids,table=table) theids,
table=table)
return [],[],avgpower2 return [],[],avgpower2
@@ -1315,13 +1320,11 @@ def new_workout_from_file(r, f2,
message = "We couldn't recognize the file type" message = "We couldn't recognize the file type"
f4 = f2[:-5]+'a'+f2[-5:] f4 = f2[:-5]+'a'+f2[-5:]
copyfile(f2,f4) copyfile(f2,f4)
if settings.DEBUG: job = myqueue(queuehigh,
res = handle_sendemail_unrecognized.delay(f4, handle_sendemail_unrecognized,
f4,
r.user.email) r.user.email)
else:
res = queuehigh.enqueue(handle_sendemail_unrecognized,
f4, r.user.email)
return (0, message, f2) return (0, message, f2)
# handle non-Painsled by converting it to painsled compatible CSV # handle non-Painsled by converting it to painsled compatible CSV
+2 -2
View File
@@ -752,7 +752,7 @@ class PlanSelectForm(forms.Form):
).exclude( ).exclude(
shortname="basic" shortname="basic"
).order_by( ).order_by(
"price","clubsize","shortname" "price","shortname"
) )
if rower and not includeall: if rower and not includeall:
try: try:
@@ -765,7 +765,7 @@ class PlanSelectForm(forms.Form):
).exclude( ).exclude(
price__lte=amount price__lte=amount
).order_by( ).order_by(
"price","clubsize","shortname" "price","shortname"
) )
+111 -19
View File
@@ -30,7 +30,7 @@ from django.utils import timezone
import pandas as pd import pandas as pd
from dateutil import parser from dateutil import parser
import datetime import datetime
from django.core.exceptions import ValidationError
from rowers.rows import validate_file_extension from rowers.rows import validate_file_extension
from collections import OrderedDict from collections import OrderedDict
from timezonefinder import TimezoneFinder from timezonefinder import TimezoneFinder
@@ -313,6 +313,13 @@ class C2WorldClassAgePerformance(models.Model):
return thestring return thestring
def is_not_basic(user):
if user.rower.rowerplan == 'basic':
raise ValidationError(
"Basic user cannot be team manager"
)
# For future Team functionality # For future Team functionality
class Team(models.Model): class Team(models.Model):
choices = ( choices = (
@@ -327,15 +334,34 @@ class Team(models.Model):
name = models.CharField(max_length=150,unique=True,verbose_name='Team Name') name = models.CharField(max_length=150,unique=True,verbose_name='Team Name')
notes = models.CharField(blank=True,max_length=200,verbose_name='Team Purpose') 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', private = models.CharField(max_length=30,choices=choices,default='open',
verbose_name='Team Type') verbose_name='Team Type')
viewing = models.CharField(max_length=30,choices=viewchoices,default='allmembers',verbose_name='Sharing Behavior') viewing = models.CharField(max_length=30,choices=viewchoices,default='allmembers',verbose_name='Sharing Behavior')
def __unicode__(self): def __unicode__(self):
return self.name 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 TeamForm(ModelForm):
class Meta: class Meta:
model = Team model = Team
@@ -361,7 +387,6 @@ class TeamInviteForm(ModelForm):
class TeamRequest(models.Model): class TeamRequest(models.Model):
team = models.ForeignKey(Team) team = models.ForeignKey(Team)
user = models.ForeignKey(User,null=True) user = models.ForeignKey(User,null=True)
@@ -607,6 +632,14 @@ class PaidPlan(models.Model):
paymentprocessor = self.paymentprocessor, 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 # Extension of User with rowing specific data
class Rower(models.Model): class Rower(models.Model):
@@ -671,8 +704,8 @@ class Rower(models.Model):
planexpires = models.DateField(default=current_day) planexpires = models.DateField(default=current_day)
teamplanexpires = models.DateField(default=current_day) teamplanexpires = models.DateField(default=current_day)
clubsize = models.IntegerField(default=0) clubsize = models.IntegerField(default=0)
protrialexpires = models.DateField(blank=True,null=True) protrialexpires = models.DateField(default=datetime.date(1970,1,1))
plantrialexpires = models.DateField(blank=True,null=True) plantrialexpires = models.DateField(default=datetime.date(1970,1,1))
# Privacy Data # Privacy Data
@@ -806,6 +839,8 @@ class Rower(models.Model):
# Friends/Team # Friends/Team
friends = models.ManyToManyField("self",blank=True) 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, privacy = models.CharField(default='visible',max_length=30,
choices=privacychoices) choices=privacychoices)
@@ -829,6 +864,7 @@ class Rower(models.Model):
def clean_email(self): def clean_email(self):
return self.user.email.lower() return self.user.email.lower()
class DeactivateUserForm(forms.ModelForm): class DeactivateUserForm(forms.ModelForm):
class Meta: class Meta:
model = User model = User
@@ -842,12 +878,44 @@ class DeleteUserForm(forms.ModelForm):
model = User model = User
fields = [] fields = []
@receiver(models.signals.post_save,sender=Rower) # requestor is user
def auto_delete_teams_on_change(sender, instance, **kwargs): class CoachRequest(models.Model):
if instance.rowerplan != 'coach': coach = models.ForeignKey(Rower)
teams = Team.objects.filter(manager=instance.user) user = models.ForeignKey(User,null=True)
for team in teams: issuedate = models.DateField(default=current_day)
team.delete() 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
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 from rowers.metrics import axlabels
favchartlabelsx = axlabels.copy() favchartlabelsx = axlabels.copy()
@@ -941,12 +1009,14 @@ def checkworkoutuser(user,workout):
return False return False
try: try:
r = Rower.objects.get(user=user) r = Rower.objects.get(user=user)
teams = workout.team.all()
if workout.user == r: if workout.user == r:
return True return True
elif teams: coaches = []
for team in teams: for group in workout.user.coachinggroups.all():
if user == team.manager and workout.privacy == 'visible': coach = Rower.objects.get(mycoachgroup=group)
coaches.append(coach)
for coach in coaches:
if user.rower == coach and workout.privacy == 'visible':
return True return True
else: else:
return False return False
@@ -958,12 +1028,14 @@ def checkworkoutuser(user,workout):
def checkaccessuser(user,rower): def checkaccessuser(user,rower):
try: try:
r = Rower.objects.get(user=user) r = Rower.objects.get(user=user)
teams = Team.objects.filter(manager=user)
if rower == r: if rower == r:
return True return True
elif teams: coaches = []
for team in teams: for group in rower.coachinggroups.all():
if team in rower.team.all(): coach = Rower.objects.get(mycoachgroup=group)
coaches.append(coach)
for coach in coaches:
if user.rower == coach:
return True return True
else: else:
return False return False
@@ -1134,6 +1206,12 @@ class TrainingPlan(models.Model):
return stri return stri
def save(self, *args, **kwargs): 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: if self.enddate < self.startdate:
startdate = self.startdate startdate = self.startdate
enddate = self.enddate enddate = self.enddate
@@ -1179,6 +1257,7 @@ class TrainingPlan(models.Model):
else: else:
createmacrofillers(self) createmacrofillers(self)
class TrainingPlanForm(ModelForm): class TrainingPlanForm(ModelForm):
class Meta: class Meta:
model = TrainingPlan model = TrainingPlan
@@ -1549,6 +1628,8 @@ class TrainingMacroCycle(models.Model):
else: else:
createmesofillers(self) createmesofillers(self)
class TrainingMacroCycleForm(ModelForm): class TrainingMacroCycleForm(ModelForm):
class Meta: class Meta:
model = TrainingMacroCycle model = TrainingMacroCycle
@@ -1634,6 +1715,7 @@ class TrainingMesoCycle(models.Model):
else: else:
createmicrofillers(self) createmicrofillers(self)
class TrainingMicroCycle(models.Model): class TrainingMicroCycle(models.Model):
plan = models.ForeignKey(TrainingMesoCycle) plan = models.ForeignKey(TrainingMesoCycle)
name = models.CharField(max_length=150,blank=True) name = models.CharField(max_length=150,blank=True)
@@ -1849,6 +1931,15 @@ class PlannedSession(models.Model):
if self.sessionvalue <= 0: if self.sessionvalue <= 0:
self.sessionvalue = 1 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 # sort units
if self.sessionmode == 'distance': if self.sessionmode == 'distance':
if self.sessionunit not in ['m','km']: if self.sessionunit not in ['m','km']:
@@ -3368,3 +3459,4 @@ class PlannedSessionCommentForm(ModelForm):
'comment': forms.Textarea, 'comment': forms.Textarea,
} }
+51
View File
@@ -1699,6 +1699,57 @@ def handle_makeplot(f1, f2, t, hrdata, plotnr, imagename,
# Team related remote tasks # 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 <info@rowsandall.com>'
siteurl = SITE_URL
if debug:
siteurl = SITE_URL_DEV
d = {
'name':name,
'coach':coachname,
'code':code,
'siteurl':siteurl
}
form_email = 'Rowsandall <info@rowsandall.com>'
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 <info@rowsandall.com>'
siteurl = SITE_URL
if debug:
siteurl = SITE_URL_DEV
d = {
'name':name,
'coach':coachname,
'code':code,
'siteurl':siteurl
}
form_email = 'Rowsandall <info@rowsandall.com>'
res = send_template_email(from_email,[fullemail],
subject,'coacheerequestemail.html',d,
**kwargs)
return 1
@app.task @app.task
def handle_sendemail_invite(email, name, code, teamname, manager, def handle_sendemail_invite(email, name, code, teamname, manager,
+232 -18
View File
@@ -17,7 +17,8 @@ queuelow = django_rq.get_queue('low')
queuehigh = django_rq.get_queue('low') queuehigh = django_rq.get_queue('low')
from rowers.models import ( from rowers.models import (
Rower, Workout, Team, TeamInvite,User,TeamRequest Rower, Workout, Team, TeamInvite,User,TeamRequest, CoachRequest, CoachOffer,
CoachingGroup
) )
from rowers.tasks import ( from rowers.tasks import (
@@ -26,8 +27,11 @@ from rowers.tasks import (
handle_sendemail_member_dropped,handle_sendemail_request_accept, handle_sendemail_member_dropped,handle_sendemail_request_accept,
handle_sendemail_request_reject,handle_sendemail_invite_reject, handle_sendemail_request_reject,handle_sendemail_invite_reject,
handle_sendemail_invite_accept,handle_sendemail_team_removed, 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 # Low level functions - to be called by higher level methods
inviteduration = 14 # days 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'): def create_team(name,manager,private='open',notes='',viewing='allmembers'):
# needs some error testing # 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: try:
t = Team(name=name,manager=manager,notes=notes, t = Team(name=name,manager=manager,notes=notes,
private=private,viewing=viewing) private=private,viewing=viewing)
@@ -77,29 +89,47 @@ def remove_team(id):
send_team_delete_mail(t,r) send_team_delete_mail(t,r)
return t.delete() return t.delete()
def set_teamplanexpires(rower): #def set_teamplanexpires(rower):
ts = Team.objects.filter(rower=rower) # ts = Team.objects.filter(rower=rower)
texp = datetime.date(timezone.now()) # texp = datetime.date(timezone.now())
for t in ts: # for t in ts:
mr = Rower.objects.get(user=t.manager) # print t.name
if mr.teamplanexpires > texp: # mr = Rower.objects.get(user=t.manager)
rower.teamplanexpires = mr.teamplanexpires # if mr.teamplanexpires > texp:
# rower.teamplanexpires = mr.teamplanexpires
t.save() # t.save()
return (1,'Updated rower team expiry') 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): def add_member(id,rower):
t= Team.objects.get(id=id) t= Team.objects.get(id=id)
try:
rower.team.add(t) rower.team.add(t)
except ValidationError:
return(0,"Couldn't add member")
# code to add all workouts # code to add all workouts
ws = Workout.objects.filter(user=rower) ws = Workout.objects.filter(user=rower)
res = handle_add_workouts_team(ws,t) res = handle_add_workouts_team(ws,t)
set_teamplanexpires(rower) # set_teamplanexpires(rower)
return (id,'Member added') return (id,'Member added')
@@ -111,9 +141,47 @@ def remove_member(id,rower):
res = handle_remove_workouts_team(ws,t) res = handle_remove_workouts_team(ws,t)
set_teamplanexpires(rower) # set_teamplanexpires(rower)
return (id,'Member removed') 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): def mgr_remove_member(id,manager,rower):
t = Team.objects.get(id=id) t = Team.objects.get(id=id)
if t.manager == manager: if t.manager == manager:
@@ -141,13 +209,58 @@ def count_club_members(manager):
# Medium level functionality # 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): def create_request(team,user):
r2 = Rower.objects.get(user=user) r2 = Rower.objects.get(user=user)
r = Rower.objects.get(user=team.manager) r = Rower.objects.get(user=team.manager)
if r2 in Rower.objects.filter(team=team): if r2 in Rower.objects.filter(team=team):
return (0,'Already a member of that team') return (0,'Already a member of that team')
if count_club_members(team.manager)+count_invites(team.manager) <= r.clubsize: # if count_club_members(team.manager)+count_invites(team.manager) <= r.clubsize:
codes = [i.code for i in TeamRequest.objects.all()] codes = [i.code for i in TeamRequest.objects.all()]
code = uuid.uuid4().hex[:10].upper() code = uuid.uuid4().hex[:10].upper()
# prevent duplicates # prevent duplicates
@@ -161,11 +274,35 @@ def create_request(team,user):
send_request_email(rekwest) send_request_email(rekwest)
return (rekwest.id,'The request was created') return (rekwest.id,'The request was created')
else:
return (0,'That team has reached its maximum number of members')
return (0,'Something went wrong in create_request') 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=''): def create_invite(team,manager,user=None,email=''):
r = Rower.objects.get(user=manager) r = Rower.objects.get(user=manager)
if team.manager != manager: if team.manager != manager:
@@ -189,7 +326,7 @@ def create_invite(team,manager,user=None,email=''):
except Rower.MultipleObjectsReturned: except Rower.MultipleObjectsReturned:
return (0,'There is more than one user with that email address') return (0,'There is more than one user with that email address')
if count_club_members(team.manager)+count_invites(team.manager) <= r.clubsize: # if count_club_members(team.manager)+count_invites(team.manager) <= r.clubsize:
codes = [i.code for i in TeamInvite.objects.all()] codes = [i.code for i in TeamInvite.objects.all()]
code = uuid.uuid4().hex[:10].upper() code = uuid.uuid4().hex[:10].upper()
# prevent duplicates # prevent duplicates
@@ -201,9 +338,6 @@ def create_invite(team,manager,user=None,email=''):
return (invite.id,'Invitation created') return (invite.id,'Invitation created')
else:
return (0,'You are at your club size limit')
return (0,'Nothing done') return (0,'Nothing done')
def revoke_request(user,id): def revoke_request(user,id):
@@ -220,6 +354,36 @@ def revoke_request(user,id):
else: else:
return (0,'You are not the requestor') 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): def revoke_invite(manager,id):
try: try:
invite = TeamInvite.objects.get(id=id) 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') return (0,'You are not the manager of this team')
result = add_member(t.id,r) result = add_member(t.id,r)
if not result:
return (result,"The member couldn't be added")
send_request_accept_email(rekwest) send_request_accept_email(rekwest)
rekwest.delete() rekwest.delete()
return result return result
@@ -421,6 +591,10 @@ def process_invite_code(user,code):
t = invitation.team t = invitation.team
result = add_member(t.id,r) result = add_member(t.id,r)
if not result:
return (result,"The member couldn't be added")
send_invite_accept_email(invitation) send_invite_accept_email(invitation)
invitation.delete() invitation.delete()
return result return result
@@ -433,3 +607,43 @@ def remove_expired_invites():
revoke_invite(i.team.manager,i.id) revoke_invite(i.team.manager,i.id)
return (1,'Expired invitations deleted') 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
+32
View File
@@ -0,0 +1,32 @@
{% extends "emailbase.html" %}
{% block body %}
<p>Dear <strong>{{ name }}</strong>,</p>
<p>
{{ coachname }} is offering to become your coach on rowsandall.com.
</p>
<p>
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.
</p>
<p>
By accepting the invite, you are agreeing with the sharing
of personal data according to our privacy policy.
</p>
<p>
To accept, login to the
site and you will find the invitation here on the Teams page:
<a href="{{ siteurl }}/rowers/me/teams">{{ siteurl }}/rowers/me/teams</a>
</p>
<p>
You can also click the direct link:
<a href="{{ siteurl }}/rowers/me/coachoffer/{{ code }}/accept/">
{{ siteurl }}/rowers/me/coachoffer/{{ code }}/accept</a>
</p>
<p>
Best Regards, the Rowsandall Team
</p>
{% endblock %}
+33
View File
@@ -0,0 +1,33 @@
{% extends "emailbase.html" %}
{% block body %}
<p>Dear <strong>{{ coachname }}</strong>,</p>
<p>
{{ name }} is inviting you to become his/her coach
on rowsandall.com
</p>
<p>
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 }}.
</p>
<p>
By accepting the invite, you are agreeing with the sharing
of personal data according to our privacy policy.
</p>
<p>
To accept the login to the
site and you will find the invitation here on the Teams page:
<a href="{{ siteurl }}/rowers/me/teams">{{ siteurl }}/rowers/me/teams</a>
</p>
<p>
You can also click the direct link:
<a href="{{ siteurl }}/rowers/me/coachrequest/{{ code }}/accept/">
{{ siteurl }}/rowers/me/coachrequest/{{ code }}/accept/</a>
</p>
<p>
Best Regards, the Rowsandall Team
</p>
{% endblock %}
+33
View File
@@ -0,0 +1,33 @@
{% extends "newbase.html" %}
{% load staticfiles %}
{% load rowerfilters %}
{% block title %}Remove Athlete {% endblock %}
{% block main %}
<h1>Confirm removing {{ athlete.user.first_name }} {{ athlete.user.last_name }}</h1>
<ul class="main-content">
<li class="grid_2">
<p>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.
</p>
<p>
<a class="button green small" href="/rowers/me/teams">Cancel</a>
</p>
<p>
<a class="button red small" href="/rowers/coaches/{{ athlete.id }}/drop">Drop Athlete</a>
</p>
</li>
</ul>
{% endblock %}
{% block sidebar %}
{% include 'menu_teams.html' %}
{% endblock %}
+35
View File
@@ -0,0 +1,35 @@
{% extends "newbase.html" %}
{% load staticfiles %}
{% load rowerfilters %}
{% block title %}Remove Coach {% endblock %}
{% block main %}
<h1>Confirm removing {{ coach.user.first_name }} {{ coach.user.last_name }}</h1>
<ul class="main-content">
<li class="grid_2">
<p>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.
</p>
<p>
<a class="button green small" href="/rowers/me/teams">Cancel</a>
</p>
<p>
<a class="button red small" href="/rowers/coaches/{{ coach.id }}/dropcoach">Drop Coach
</a>
</p>
</li>
</ul>
{% endblock %}
{% block sidebar %}
{% include 'menu_teams.html' %}
{% endblock %}
+5 -5
View File
@@ -71,21 +71,21 @@
{% if user.is_authenticated and user|is_manager %} {% if user.is_authenticated and user|is_manager %}
<p>&nbsp;</p> <p>&nbsp;</p>
{% if user|team_members %} {% if user|coach_rowers %}
<ul class="cd-accordion-menu animated"> <ul class="cd-accordion-menu animated">
<li class="has-children" id="athletes"> <li class="has-children" id="athletes">
<input type="checkbox" name="athlete-selector" id="athlete-selector"> <input type="checkbox" name="athlete-selector" id="athlete-selector">
<label for="athlete-selector"><i class="fas fa-users fa-fw"></i>&nbsp;Athletes</label> <label for="athlete-selector"><i class="fas fa-users fa-fw"></i>&nbsp;Athletes</label>
<ul> <ul>
{% for member in user|team_members %} {% for member in user|coach_rowers %}
<a href={{ request.path|userurl:member }}> <a href={{ request.path|userurl:member.user }}>
<i class="fas fa-user fa-fw"></i> <i class="fas fa-user fa-fw"></i>
{% if member == rower.user %} {% if member.user == rower.user %}
&bull; &bull;
{% else %} {% else %}
&nbsp; &nbsp;
{% endif %} {% endif %}
{{ member.first_name }} {{ member.last_name }} {{ member.user.first_name }} {{ member.user.last_name }}
</a> </a>
{% endfor %} {% endfor %}
</ul> </ul>
+5 -5
View File
@@ -87,21 +87,21 @@
{% if user.is_authenticated and user|is_manager %} {% if user.is_authenticated and user|is_manager %}
<p>&nbsp;</p> <p>&nbsp;</p>
{% if user|team_members %} {% if user|coach_rowers %}
<ul class="cd-accordion-menu animated"> <ul class="cd-accordion-menu animated">
<li class="has-children" id="athletes"> <li class="has-children" id="athletes">
<input type="checkbox" name="athlete-selector" id="athlete-selector"> <input type="checkbox" name="athlete-selector" id="athlete-selector">
<label for="athlete-selector"><i class="fas fa-users fa-fw"></i>&nbsp;Athletes</label> <label for="athlete-selector"><i class="fas fa-users fa-fw"></i>&nbsp;Athletes</label>
<ul> <ul>
{% for member in user|team_members %} {% for member in user|coach_rowers %}
<a href={{ request.path|userurl:member }}?when={{ timeperiod }}> <a href={{ request.path|userurl:member.user }}?when={{ timeperiod }}>
<i class="fas fa-user fa-fw"></i> <i class="fas fa-user fa-fw"></i>
{% if member == rower.user %} {% if member.user == rower.user %}
&bull; &bull;
{% else %} {% else %}
&nbsp; &nbsp;
{% endif %} {% endif %}
{{ member.first_name }} {{ member.last_name }} {{ member.user.first_name }} {{ member.user.last_name }}
</a> </a>
{% endfor %} {% endfor %}
</ul> </ul>
+5 -5
View File
@@ -39,21 +39,21 @@
{% if user.is_authenticated and user|is_manager %} {% if user.is_authenticated and user|is_manager %}
<p>&nbsp;</p> <p>&nbsp;</p>
{% if user|team_members %} {% if user|coach_rowers %}
<ul class="cd-accordion-menu animated"> <ul class="cd-accordion-menu animated">
<li class="has-children" id="athletes"> <li class="has-children" id="athletes">
<input type="checkbox" name="athlete-selector" id="athlete-selector"> <input type="checkbox" name="athlete-selector" id="athlete-selector">
<label for="athlete-selector"><i class="fas fa-users fa-fw"></i>&nbsp;Athletes</label> <label for="athlete-selector"><i class="fas fa-users fa-fw"></i>&nbsp;Athletes</label>
<ul> <ul>
{% for member in user|team_members %} {% for member in user|coach_rowers %}
<a href={{ request.path|userurl:member }}> <a href={{ request.path|userurl:member.user }}>
<i class="fas fa-user fa-fw"></i> <i class="fas fa-user fa-fw"></i>
{% if member == rower.user %} {% if member.user == rower.user %}
&bull; &bull;
{% else %} {% else %}
&nbsp; &nbsp;
{% endif %} {% endif %}
{{ member.first_name }} {{ member.last_name }} {{ member.user.first_name }} {{ member.user.last_name }}
</a> </a>
{% endfor %} {% endfor %}
</ul> </ul>
+1 -3
View File
@@ -8,13 +8,11 @@
<i class="fas fa-cog fa-fw"></i>&nbsp;Overview <i class="fas fa-cog fa-fw"></i>&nbsp;Overview
</a> </a>
</li> </li>
{% if user|is_manager %}
<li id="create"> <li id="create">
<a href="/rowers/team/create/"> <a href="/rowers/team/create/">
<i class="fas fa-plus fa-fw"></i>&nbsp;New Team <i class="fas fa-plus fa-fw"></i>&nbsp;New Training Group
</a> </a>
</li> </li>
{% endif %}
</ul><!-- cd-accordion-menu --> </ul><!-- cd-accordion-menu -->
+6 -6
View File
@@ -47,22 +47,22 @@
{% if user.is_authenticated and user|is_manager %} {% if user.is_authenticated and user|is_manager %}
<p>&nbsp;</p> <p>&nbsp;</p>
{% if user|team_members %} {% if user|coach_rowers %}
<ul class="cd-accordion-menu animated"> <ul class="cd-accordion-menu animated">
<li class="has-children" id="athletes"> <li class="has-children" id="athletes">
<input type="checkbox" name="athlete-selector" id="athlete-selector"> <input type="checkbox" name="athlete-selector" id="athlete-selector">
<label for="athlete-selector"><i class="fas fa-users fa-fw"></i>&nbsp;Athletes</label> <label for="athlete-selector"><i class="fas fa-users fa-fw"></i>&nbsp;Athletes</label>
<ul> <ul>
{% for member in user|team_members %} {% for member in user|coach_rowers %}
<li> <li>
<a href={{ request.path|userurl:member }}> <a href={{ request.path|userurl:member.user }}>
<i class="fas fa-user fa-fw"></i> <i class="fas fa-user fa-fw"></i>
{% if member == rower.user and not team %} {% if member.user == rower.user and not team %}
&bull; &bull;
{% else %} {% else %}
&nbsp; &nbsp;
{% endif %} {% endif %}
{{ member.first_name }} {{ member.last_name }} {{ member.user.first_name }} {{ member.user.last_name }}
</a> </a>
</li> </li>
{% endfor %} {% endfor %}
@@ -77,7 +77,7 @@
<ul class="cd-accordion-menu animated"> <ul class="cd-accordion-menu animated">
<li class="has-children" id="teams"> <li class="has-children" id="teams">
<input type="checkbox" name="team-selector" id="team-selector"> <input type="checkbox" name="team-selector" id="team-selector">
<label for="team-selector"><i class="fas fa-bullhorn fa-fw"></i>&nbsp;Teams</label> <label for="team-selector"><i class="fas fa-bullhorn fa-fw"></i>&nbsp;Groups</label>
<ul> <ul>
{% for tteam in teams %} {% for tteam in teams %}
<li> <li>
-13
View File
@@ -257,19 +257,6 @@
{% endif %} {% endif %}
</button> </button>
</td> </td>
{% elif rower and rower.rowerplan == 'coach' and rower.clubsize < 100 %}
<td>&nbsp;</td>
<td>&nbsp;</td>
<td>
<button style="width:100%">
{% if user|existing_customer %}
<a href="/rowers/upgrade/">UPGRADE NOW</a>
{% else %}
<a href="/rowers/billing/">BUY NOW</a>
{% endif %}
</button>
</td>
{% else %}
<td colspan=3> <td colspan=3>
&nbsp; &nbsp;
</td> </td>
+34 -29
View File
@@ -59,13 +59,13 @@
other fitness sites. You can actually revoke these at any time. other fitness sites. You can actually revoke these at any time.
<li>User preferences as shown on the user settings page <li>User preferences as shown on the user settings page
<li>Your favorite Flex Charts if defined <li>Your favorite Flex Charts if defined
<li>The teams you are a member of. <li>The teams or groups you are a member of.
<li>Estimated four minute, 2k and 1 hour ergometer and OTW power values, <li>Estimated four minute, 2k and 1 hour ergometer and OTW power values,
based on the workouts you upload, and their evolution during your based on the workouts you upload, and their evolution during your
usage of the site usage of the site
<li>For members on the Coach plan, the names and purposes of teams. Names <li>For members on the Coach plan, the names and purposes of teams or groups. Names
of team members. (Members who delete their account will be erased from of team or group members. (Members who delete their account will be erased from
existing teams.) existing teams or groups.)
<li>Any rowing courses you uploaded <li>Any rowing courses you uploaded
<li>Training targets and training plans <li>Training targets and training plans
<li>Your uploaded workouts, their names, boat type, start time and date, <li>Your uploaded workouts, their names, boat type, start time and date,
@@ -212,57 +212,62 @@
</p> </p>
<h3>Team Functionality</h3> <h3>Team Or Group Functionality</h3>
<p> <p>
On rowsandall.com, users with the paid "Coach" plan can establish teams and invite other users to become part of the team. The purpose 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 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: 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:
<ul> <ul>
<li>"All Members" - This is the default team type. All members can see workouts of all other members, except those workouts that the members have <li>"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". marked as "private".
<li>"Coach Only" - With this setting, each individual team member is sharing his workout data only with the team manager. Other members cannot see <li>"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. his workouts.
</ul> </ul>
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.
</p> </p>
<p> <p>
By accepting an "invitation" to become a member of a team, or by requesting to become part of a team, you agree to automatically 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) to the team manager (coach) and, 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 policy, to other members of the team. When you leave depending to the team or group policy, to other members of the team or group. 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 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. As a member of a team, you grant the team manager 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 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 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, 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 manager is not able to download all your data, 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. nor can he deactivate or delete your account.
</p> </p>
<p> <p>
Each team member is bound by this privacy policy and the GDPR regulation of the European Union regarding the personal data of other team 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, the new member agrees to limit the use of these data strictly to the 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. allowed use according to this privacy policy and the GDPR.
</p> </p>
<p> <p>
Team managers can access requests of users to be added to one of their teams. By accepting the invitation, the manager accepts the responsibilities Team Or Group managers can access requests of users to be added to one of their teams or groups.
and duties associated with access to personal data of the new team member. He is bound by this privacy policy and the GDPR regulation 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. of the European Union regarding the personal data that he has access to.
</p> </p>
<p> <p>
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 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 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 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 members with the new sharing policy. The team manager must remove team members who did not give their active consent 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. If a team member has not responded within 7 days of being notified, the team manager will understand this as "no 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 member. and remove the team or group member.
</p> </p>
<p> <p>
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 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 membership within less than 7 days of being notified. revoke his team or group membership within less than 7 days of being notified.
</p> </p>
<h3>Third Party Sharing</h3> <h3>Third Party Sharing</h3>
+1 -1
View File
@@ -52,7 +52,7 @@
{% endif %} {% endif %}
</table> </table>
{% csrf_token %} {% csrf_token %}
{% if rower.clubsize < 100 and rower.user == user %} {% if rower.rowerplan != 'coach' and rower.user == user %}
<p> <p>
<a href="/rowers/paidplans/">Upgrade</a> <a href="/rowers/paidplans/">Upgrade</a>
</p> </p>
+1 -1
View File
@@ -39,7 +39,7 @@
{% for member in members %} {% for member in members %}
<tr> <tr>
{% if team.manager == user %} {% if team.manager == user %}
<td><a href="/rowers/me/preferences/{{ member.id }}/"> {{ member.user.first_name }} {{ member.user.last_name }}</a></td> <td><a href="/rowers/me/preferences/user/{{ member.user.id }}/"> {{ member.user.first_name }} {{ member.user.last_name }}</a></td>
<td><a class="button red small" href="/rowers/me/team/{{ team.id }}/drop/{{ member.user.id }}/">Drop</a></td> <td><a class="button red small" href="/rowers/me/team/{{ team.id }}/drop/{{ member.user.id }}/">Drop</a></td>
{% else %} {% else %}
<td>{{ member.user.first_name }} {{ member.user.last_name }}</td> <td>{{ member.user.first_name }} {{ member.user.last_name }}</td>
+192 -20
View File
@@ -1,12 +1,12 @@
{% extends "newbase.html" %} {% extends "newbase.html" %}
{% block title %}Teams {% endblock %} {% block title %}Groups {% endblock %}
{% block main %} {% block main %}
<ul class="main-content"> <ul class="main-content">
{% if teams %} {% if teams %}
<li > <li class="grid_2">
<h2>My Teams</h2> <h2>My Groups</h2>
<table width="100%" class="listtable"> <table width="100%" class="listtable">
<thead> <thead>
<tr> <tr>
@@ -31,8 +31,8 @@
{% endif %} {% endif %}
{% if otherteams %} {% if otherteams %}
<li > <li class="grid_2">
<h2>Other Teams</h2> <h2>Open Groups</h2>
<table width="100%" class="listtable"> <table width="100%" class="listtable">
<thead> <thead>
<tr> <tr>
@@ -56,17 +56,14 @@
</li> </li>
{% endif %} {% endif %}
{% if user.rower.rowerplan == 'coach' %} <li class="grid_2">
<li > <h2>Groups I manage</h2>
<h2>Teams I manage</h2>
<p>Number of members: {{ clubsize }}</p>
<p>Maximum club size: {{ max_clubsize }}</p>
{% if myteams %} {% if myteams %}
<table width="100%" class="listtable"> <table width="100%" class="listtable">
<thead> <thead>
<tr> <tr>
<th>Name</th> <th>Name</th>
<th>Manager</th> <th>&nbsp;</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -83,26 +80,77 @@
</tbody> </tbody>
</table> </table>
{% endif %} {% endif %}
<a class="button green" href="/rowers/team/create/">New Team</a> <a href="/rowers/team/create/">Create New Training Group</a>
</li>
{% if coaches %}
<li class="grid_2">
<h2>My Coaches</h2>
<table width="100%" class="listtable">
<thead>
<tr>
<th>Coach</th>
</tr>
</thead>
<tbody>
{% for coach in coaches %}
<tr>
<td>
{{ coach.user.first_name }} {{ coach.user.last_name }}
</td>
<td>
<a class="button small red"
href="/rowers/coaches/{{ coach.id }}/dropcoachconfirm/">Remove
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</li>
{% endif %}
{% if coachees %}
<li class="grid_2">
<h2>My Rowers</h2>
<table width="100%" class="listtable">
<thead>
<tr>
<th>Name</th>
</tr>
</thead>
<tbody>
{% for coachee in coachees %}
<tr>
<td>
{{ coachee.user.first_name }} {{ coachee.user.last_name }}
</td>
<td>
<a class="button small red"
href="/rowers/coaches/{{ coachee.id }}/dropconfirm/">Remove
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</li> </li>
{% endif %} {% endif %}
{% if invites or requests or myrequests or myinvites %} {% if invites or requests or myrequests or myinvites %}
<li class="grid_2"> <li class="grid_2">
<h2>Invitations and Requests</h2> <h2>Group Invitations and Requests</h2>
<p>This section lists open invites to join a team. By accepting <p>This section lists open invites to join a group. By accepting
a team invite, you are agreeing with the sharing a group invite, you are agreeing with the sharing
of personal data between team members and coaches according to of personal data between group members and coaches according to
our <a href="/rowers/legal/">privacy policy</a>. our <a href="/rowers/legal/">privacy policy</a>.
</p> </p>
<p>As a team manager, by accepting a team invite, you are agreeing <p>As a group manager, by accepting a group invite, you are agreeing
with our <a href="/rowers/legal/">privacy policy</a> regarding teams and with our <a href="/rowers/legal/">privacy policy</a> regarding groups and
personal data owned by team members.</p> personal data owned by group members.</p>
<table width="90%" class="listtable"> <table width="90%" class="listtable">
<thead> <thead>
<tr> <tr>
<th>Team</th> <th>Group</th>
<th>Manager</th> <th>Manager</th>
<th>User</th> <th>User</th>
<th>Action</th> <th>Action</th>
@@ -173,7 +221,131 @@
<input class="button green" type="submit" value="Submit"> <input class="button green" type="submit" value="Submit">
</form> </form>
{% endif %} {% endif %}
{% if mycoachrequests or mycoachoffers or coachoffers or coachrequests %}
<li class="grid_2">
<h2>Coaching Offers and Requests</h2>
<p>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 <a href="/rowers/legal/">privacy policy</a>.
</p>
<table width="90%" class="listtable">
<thead>
<tr>
<th>Coach</th>
<th>User</th>
<th>Action</th>
<th>&nbsp;</th>
</tr>
</thead>
<tbody>
{% for i in coachrequests %}
<tr>
<td>{{ i.coach.user.first_name }} {{ i.coach.user.last_name }}</td>
<td>{{ i.user.first_name }} {{ i.user.last_name }}</td>
<td><a
class="button small green"
href="/rowers/me/coachrequest/{{ i.code }}/accept">Accept</a>
</td>
<td>
<a class="button small red" href="/rowers/me/coachrequest/{{ i.id }}/reject/">Reject</a>
</td>
</tr>
{% endfor %}
{% for i in mycoachoffers %}
<tr>
<td>{{ i.coach.user.first_name }} {{ i.coach.user.last_name }}</td>
<td>{{ i.user.first_name }} {{ i.user.last_name }}</td>
<td>&nbsp;
</td>
<td>
<a class="button small red" href="/rowers/me/coachoffer/{{ i.id }}/revoke/">Revoke</a>
</td>
</tr>
{% endfor %}
{% for i in mycoachrequests %}
<tr>
<td>{{ i.coach.user.first_name }} {{ i.coach.user.last_name }}</td>
<td>{{ i.user.first_name }} {{ i.user.last_name }}</td>
<td>&nbsp;</td>
<td>
<a
class="button small red"
href="/rowers/me/coachrequest/{{ i.id }}/revoke/">Revoke</a>
</td>
</tr>
{% endfor %}
{% for i in coachoffers %}
<tr>
<td>{{ i.coach.user.first_name }} {{ i.coach.user.last_name }}</td>
<td>{{ i.user.first_name }} {{ i.user.last_name }}</td>
<td><a
class="button small green"
href="/rowers/me/coachoffer/{{ i.code }}/accept/">Accept</a>
</td>
<td>
<a
class="button small red"
href="/rowers/me/coachoffer/{{ i.id }}/revoke/">Reject</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</li> </li>
{% if potentialathletes %}
<li class="grid_2">
<h2>Rowers you could coach</h2>
<table width="90%" class="listtable">
<thead>
<tr>
<th>User</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{% for a in potentialathletes %}
<tr>
<td>{{ a.user.first_name }} {{ a.user.last_name }}</td>
<td>
<a
class="button small green"
href="/rowers/me/coachoffer/{{ a.user.id }}/">Offer Coaching</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</li>
{% endif %}
{% if potentialcoaches %}
<li class="grid_2">
<h2>Coaches who could coach you</h2>
<table width="90%" class="listtable">
<thead>
<tr>
<th>User</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{% for c in potentialcoaches %}
<tr>
<td>{{ c.first_name }} {{ c.last_name }}</td>
<td>
<a
class="button small green"
href="/rowers/me/coachrequest/{{ c.id }}/">Request Coaching</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</li>
{% endif %}
</ul> </ul>
+5
View File
@@ -376,6 +376,11 @@ def team_rowers(user):
return [] return []
from rowers.teams import coach_getcoachees
@register.filter
def coach_rowers(user):
return coach_getcoachees(user.rower)
@register.filter @register.filter
def verbosetimeperiod(timeperiod): def verbosetimeperiod(timeperiod):
+10 -8
View File
@@ -69,12 +69,14 @@ class VirtualEventViewTest(TestCase):
yesterday = nu-datetime.timedelta(days=1) yesterday = nu-datetime.timedelta(days=1)
tomorrow = nu+datetime.timedelta(days=1) tomorrow = nu+datetime.timedelta(days=1)
nextweek = nu+datetime.timedelta(days=7) nextweek = nu+datetime.timedelta(days=7)
intwoweeks = nu+datetime.timedelta(days=14)
lastweek = nu-datetime.timedelta(days=7) lastweek = nu-datetime.timedelta(days=7)
self.yesterday = yesterday self.yesterday = yesterday
self.tomorrow = tomorrow self.tomorrow = tomorrow
self.nextweek = nextweek self.nextweek = nextweek
self.lastweek = lastweek self.lastweek = lastweek
self.intwoweeks = intwoweeks
# erg races # erg races
@@ -396,8 +398,8 @@ class VirtualEventViewTest(TestCase):
'registration_form':'deadline', 'registration_form':'deadline',
'registration_closure_0': self.nextweek.strftime('%Y-%m-%d'), 'registration_closure_0': self.nextweek.strftime('%Y-%m-%d'),
'registration_closure_1': self.nextweek.strftime('%H:%M:%S'), 'registration_closure_1': self.nextweek.strftime('%H:%M:%S'),
'evaluation_closure_0': self.nextweek.strftime('%Y-%m-%d'), 'evaluation_closure_0': self.intwoweeks.strftime('%Y-%m-%d'),
'evaluation_closure_1': self.nextweek.strftime('%H:%M:%S'), 'evaluation_closure_1': self.intwoweeks.strftime('%H:%M:%S'),
'contact_phone': '', 'contact_phone': '',
'contact_email': self.u.email, 'contact_email': self.u.email,
'timezone': 'UTC' 'timezone': 'UTC'
@@ -440,8 +442,8 @@ class VirtualEventViewTest(TestCase):
'registration_form':'deadline', 'registration_form':'deadline',
'registration_closure_0': self.nextweek.strftime('%Y-%m-%d'), 'registration_closure_0': self.nextweek.strftime('%Y-%m-%d'),
'registration_closure_1': self.nextweek.strftime('%H:%M:%S'), 'registration_closure_1': self.nextweek.strftime('%H:%M:%S'),
'evaluation_closure_0': self.nextweek.strftime('%Y-%m-%d'), 'evaluation_closure_0': self.intwoweeks.strftime('%Y-%m-%d'),
'evaluation_closure_1': self.nextweek.strftime('%H:%M:%S'), 'evaluation_closure_1': self.intwoweeks.strftime('%H:%M:%S'),
'contact_phone': '', 'contact_phone': '',
'contact_email': self.u.email, 'contact_email': self.u.email,
'timezone': 'UTC' 'timezone': 'UTC'
@@ -485,8 +487,8 @@ class VirtualEventViewTest(TestCase):
'registration_form':'deadline', 'registration_form':'deadline',
'registration_closure_0': self.nextweek.strftime('%Y-%m-%d'), 'registration_closure_0': self.nextweek.strftime('%Y-%m-%d'),
'registration_closure_1': self.nextweek.strftime('%H:%M:%S'), 'registration_closure_1': self.nextweek.strftime('%H:%M:%S'),
'evaluation_closure_0': self.nextweek.strftime('%Y-%m-%d'), 'evaluation_closure_0': self.intwoweeks.strftime('%Y-%m-%d'),
'evaluation_closure_1': self.nextweek.strftime('%H:%M:%S'), 'evaluation_closure_1': self.intwoweeks.strftime('%H:%M:%S'),
'contact_phone': '', 'contact_phone': '',
'contact_email': self.u.email, 'contact_email': self.u.email,
} }
@@ -522,8 +524,8 @@ class VirtualEventViewTest(TestCase):
'registration_form':'deadline', 'registration_form':'deadline',
'registration_closure_0': self.nextweek.strftime('%Y-%m-%d'), 'registration_closure_0': self.nextweek.strftime('%Y-%m-%d'),
'registration_closure_1': self.nextweek.strftime('%H:%M:%S'), 'registration_closure_1': self.nextweek.strftime('%H:%M:%S'),
'evaluation_closure_0': self.nextweek.strftime('%Y-%m-%d'), 'evaluation_closure_0': self.intwoweeks.strftime('%Y-%m-%d'),
'evaluation_closure_1': self.nextweek.strftime('%H:%M:%S'), 'evaluation_closure_1': self.intwoweeks.strftime('%H:%M:%S'),
'contact_phone': '', 'contact_phone': '',
'contact_email': self.u.email, 'contact_email': self.u.email,
} }
File diff suppressed because it is too large Load Diff
+3 -1
View File
@@ -131,7 +131,7 @@ class TrainingPlanTest(TestCase):
login = self.c.login(username=self.u.username, password=self.password) login = self.c.login(username=self.u.username, password=self.password)
self.assertTrue(login) self.assertTrue(login)
url = '/rowers/sessions/create/' url = reverse('plannedsession_create_view')
startdate = nu.date() startdate = nu.date()
enddate = (nu+datetime.timedelta(days=3)).date() enddate = (nu+datetime.timedelta(days=3)).date()
@@ -1051,6 +1051,8 @@ class PlannedSessionsView(TestCase):
manager = self.u, manager = self.u,
) )
self.team.save()
self.r.team.add(self.team) self.r.team.add(self.team)
self.r2.team.add(self.team) self.r2.team.add(self.team)
self.r.save() self.r.save()
-9
View File
@@ -349,13 +349,4 @@ class TeamTestLowLevel(TestCase):
id, comment = create_invite(self.t, self.users[3],self.users[4]) id, comment = create_invite(self.t, self.users[3],self.users[4])
self.assertEqual(id,0) 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)
Binary file not shown.
+24
View File
@@ -410,6 +410,30 @@ urlpatterns = [
url(r'^team/(?P<id>\d+)/leave/$',views.team_leave_view,name='team_leave_view'), url(r'^team/(?P<id>\d+)/leave/$',views.team_leave_view,name='team_leave_view'),
url(r'^team/(?P<id>\d+)/deleteconfirm/$',views.team_deleteconfirm_view,name='team_deleteconfirm_view'), url(r'^team/(?P<id>\d+)/deleteconfirm/$',views.team_deleteconfirm_view,name='team_deleteconfirm_view'),
url(r'^team/(?P<teamid>\d+)/requestmembership/(?P<userid>\d+)/$',views.team_requestmembership_view,name='team_requestmembership_view'), url(r'^team/(?P<teamid>\d+)/requestmembership/(?P<userid>\d+)/$',views.team_requestmembership_view,name='team_requestmembership_view'),
url(r'^me/coachrequest/(?P<id>\d+)/reject/$',views.reject_revoke_coach_request,
name='reject_revoke_coach_request'),
url(r'^coaches/(?P<id>\d+)/dropconfirm/$',views.coach_drop_athlete_confirm_view,
name='coach_drop_athlete_confirm_view'),
url(r'^coaches/(?P<id>\d+)/drop/$',views.coach_drop_athlete_view,
name='coach_drop_athlete_view'),
url(r'^coaches/(?P<id>\d+)/dropcoachconfirm/$',views.athlete_drop_coach_confirm_view,
name='athlete_drop_coach_confirm_view'),
url(r'^coaches/(?P<id>\d+)/dropcoach/$',views.athlete_drop_coach_view,
name='athlete_drop_coach_view'),
url(r'^me/coachrequest/(?P<id>\d+)/revoke/$',views.reject_revoke_coach_request,
name='reject_revoke_coach_request'),
url(r'^me/coachoffer/(?P<id>\d+)/reject/$',views.reject_revoke_coach_offer,
name='reject_revoke_coach_offer'),
url(r'^me/coachoffer/(?P<id>\d+)/revoke/$',views.reject_revoke_coach_offer,
name='reject_revoke_coach_offer'),
url(r'^me/coachrequest/(?P<coachid>\d+)/$',views.request_coaching_view,
name='request_coaching_view'),
url(r'^me/coachoffer/(?P<userid>\d+)/$',views.offer_coaching_view,
name='offer_coaching_view'),
url(r'^me/coachrequest/(?P<code>\w+.*)/accept/$',views.coach_accept_coachrequest_view,
name='coach_accept_coachrequest_view'),
url(r'^me/coachoffer/(?P<code>\w+.*)/accept/$',views.rower_accept_coachoffer_view,
name='rower_accept_coachoffer_view'),
url(r'^team/(?P<id>\d+)/delete/$',views.team_delete_view,name='team_delete_view'), url(r'^team/(?P<id>\d+)/delete/$',views.team_delete_view,name='team_delete_view'),
url(r'^team/create/$',views.team_create_view,name='team_create_view'), url(r'^team/create/$',views.team_create_view,name='team_create_view'),
url(r'^me/team/(?P<teamid>\d+)/drop/(?P<userid>\d+)/$',views.manager_member_drop_view,name='manager_member_drop_view'), url(r'^me/team/(?P<teamid>\d+)/drop/(?P<userid>\d+)/$',views.manager_member_drop_view,name='manager_member_drop_view'),
+10 -1
View File
@@ -298,11 +298,14 @@ def myqueue(queue,function,*args,**kwargs):
if settings.TESTING: if settings.TESTING:
return MockJob() return MockJob()
if settings.DEBUG: if settings.CELERY:
kwargs['debug'] = True kwargs['debug'] = True
job = function.delay(*args,**kwargs) job = function.delay(*args,**kwargs)
else: else:
if settings.DEBUG:
kwargs['debug'] = True
job_id = str(uuid.uuid4()) job_id = str(uuid.uuid4())
kwargs['job_id'] = job_id kwargs['job_id'] = job_id
kwargs['jobkey'] = job_id kwargs['jobkey'] = job_id
@@ -405,6 +408,12 @@ def isprorower(r):
return result return result
def iscoach(m,r):
result = False
result = m in r.coaches
return result
# Exponentially weighted moving average # Exponentially weighted moving average
# Used for data smoothing of the jagged data obtained by Strava # Used for data smoothing of the jagged data obtained by Strava
# See bitbucket issue 72 # See bitbucket issue 72
+18 -1
View File
@@ -88,7 +88,7 @@ from rowers.models import (
microcyclecheckdates,mesocyclecheckdates,macrocyclecheckdates, microcyclecheckdates,mesocyclecheckdates,macrocyclecheckdates,
TrainingMesoCycleForm, TrainingMicroCycleForm, TrainingMesoCycleForm, TrainingMicroCycleForm,
RaceLogo,RowerBillingAddressForm,PaidPlan, RaceLogo,RowerBillingAddressForm,PaidPlan,
PlannedSessionComment, PlannedSessionComment,CoachRequest,CoachOffer,
) )
from rowers.models import ( from rowers.models import (
RowerPowerForm,RowerForm,GraphImage,AdvancedWorkoutForm, RowerPowerForm,RowerForm,GraphImage,AdvancedWorkoutForm,
@@ -928,6 +928,23 @@ def iscoachmember(user):
return result return result
def cancreateteam(user):
if user.is_anonymous():
return False
try:
r = Rower.objects.get(user=user)
except Rower.DoesNotExist:
r = Rower(user=user)
r.save()
if user.is_authenticated() and (r.rowerplan=='coach'):
return True
elif user.is_athenticated() and r.rowerplan in ['plan','pro']:
ts = Team.objects.filter(manager=user)
if len(otherteams) >= 1:
return False
# Check if a user can create planned sessions # Check if a user can create planned sessions
def hasplannedsessions(user): def hasplannedsessions(user):
if not user.is_anonymous(): if not user.is_anonymous():
+260 -17
View File
@@ -167,13 +167,38 @@ def rower_teams_view(request,message='',successmessage=''):
myteams, memberteams, otherteams = get_teams(request) myteams, memberteams, otherteams = get_teams(request)
teams.remove_expired_invites() teams.remove_expired_invites()
invites = TeamInvite.objects.filter(user=request.user) invites = TeamInvite.objects.filter(user=request.user)
requests = TeamRequest.objects.filter(user=request.user) requests = TeamRequest.objects.filter(user=request.user)
myrequests = TeamRequest.objects.filter(team__in=myteams) myrequests = TeamRequest.objects.filter(team__in=myteams)
myinvites = TeamInvite.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 # 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 = teams.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
).exclude(coachinggroups__in=[request.user.rower.mycoachgroup])
# clubsize = teams.count_invites(request.user)+teams.count_club_members(request.user)
# max_clubsize = r.clubsize
messages.info(request,successmessage) messages.info(request,successmessage)
messages.error(request,message) messages.error(request,message)
@@ -185,13 +210,12 @@ def rower_teams_view(request,message='',successmessage=''):
} }
] ]
return render(request, 'teams.html', return render(request, 'teams.html',
{ {
'teams':ts, 'teams':ts,
'active':'nav-teams', 'active':'nav-teams',
'breadcrumbs':breadcrumbs, 'breadcrumbs':breadcrumbs,
'clubsize':clubsize,
'max_clubsize':max_clubsize,
'myteams':myteams, 'myteams':myteams,
'memberteams':memberteams, 'memberteams':memberteams,
'invites':invites, 'invites':invites,
@@ -200,9 +224,17 @@ def rower_teams_view(request,message='',successmessage=''):
'myrequests':myrequests, 'myrequests':myrequests,
'form':form, 'form':form,
'myinvites':myinvites, 'myinvites':myinvites,
'mycoachrequests':mycoachrequests,
'mycoachoffers':mycoachoffers,
'coachrequests':coachrequests,
'coachoffers':coachoffers,
'coaches':coaches,
'potentialcoaches':potentialcoaches,
'coachees':coachees,
'potentialathletes':potentialathletes,
}) })
@user_passes_test(iscoachmember,login_url="/rowers/paidplans",redirect_field_name=None) @login_required()
def invitation_revoke_view(request,id): def invitation_revoke_view(request,id):
res,text = teams.revoke_invite(request.user,id) res,text = teams.revoke_invite(request.user,id)
if res: if res:
@@ -216,7 +248,7 @@ def invitation_revoke_view(request,id):
return HttpResponseRedirect(url) 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, def manager_member_drop_view(request,teamid,userid,
message='',successmessage=''): message='',successmessage=''):
rower = Rower.objects.get(user__id=userid) rower = Rower.objects.get(user__id=userid)
@@ -230,7 +262,7 @@ def manager_member_drop_view(request,teamid,userid,
return HttpResponseRedirect(url) 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=''): def manager_requests_view(request,code=None,message='',successmessage=''):
if code: if code:
res,text = teams.process_request_code(request.user,code) res,text = teams.process_request_code(request.user,code)
@@ -247,6 +279,101 @@ def manager_requests_view(request,code=None,message='',successmessage=''):
}) })
return HttpResponseRedirect(url) 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() @login_required()
def team_requestmembership_view(request,teamid,userid): def team_requestmembership_view(request,teamid,userid):
@@ -255,19 +382,94 @@ def team_requestmembership_view(request,teamid,userid):
except Team.DoesNotExist: except Team.DoesNotExist:
raise Http404("Team doesn't exist") 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) res,text = teams.create_request(t,userid)
if res: if res:
messages.info(request,text) messages.info(request,text)
else: else:
messages.error(request,text) messages.error(request,text)
url = reverse(team_view,kwargs={
url = reverse('team_view',kwargs={
'id':int(teamid), 'id':int(teamid),
}) })
return HttpResponseRedirect(url) 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_offer(coach,u)
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_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() @login_required()
def request_revoke_view(request,id=0): def request_revoke_view(request,id=0):
res,text = teams.revoke_request(request.user,id) res,text = teams.revoke_request(request.user,id)
@@ -282,7 +484,7 @@ def request_revoke_view(request,id=0):
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
@user_passes_test(iscoachmember,login_url="/rowers/paidplans",redirect_field_name=None) @login_required()
def request_reject_view(request,id=0): def request_reject_view(request,id=0):
res,text = teams.reject_request(request.user,id) res,text = teams.reject_request(request.user,id)
@@ -295,7 +497,7 @@ def request_reject_view(request,id=0):
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
@user_passes_test(iscoachmember,login_url="/rowers/paidplans",redirect_field_name=None) @login_required()
def invitation_reject_view(request,id=0): def invitation_reject_view(request,id=0):
res,text = teams.reject_invitation(request.user,id) res,text = teams.reject_invitation(request.user,id)
@@ -331,7 +533,7 @@ def rower_invitations_view(request,code=None,message='',successmessage=''):
}) })
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
@user_passes_test(iscoachmember,login_url="/rowers/paidplans",redirect_field_name=None) @login_required()
def team_edit_view(request,id=0): def team_edit_view(request,id=0):
try: try:
t = Team.objects.get(id=id) t = Team.objects.get(id=id)
@@ -395,8 +597,19 @@ def team_edit_view(request,id=0):
'team':t, '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): 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': if request.method == 'POST':
teamcreateform = TeamForm(request.POST) teamcreateform = TeamForm(request.POST)
if teamcreateform.is_valid(): if teamcreateform.is_valid():
@@ -408,7 +621,13 @@ def team_create_view(request):
viewing = cd['viewing'] viewing = cd['viewing']
res,message=teams.create_team(name,manager,private,notes, res,message=teams.create_team(name,manager,private,notes,
viewing) viewing)
url = reverse(rower_teams_view)
if not res:
messages.error(request,message)
url = reverse('paidplans')
return HttpResponseRedirect(url)
url = reverse('rower_teams_view')
response = HttpResponseRedirect(url) response = HttpResponseRedirect(url)
return response return response
@@ -438,7 +657,7 @@ def team_create_view(request):
'breadcrumbs':breadcrumbs, 'breadcrumbs':breadcrumbs,
}) })
@user_passes_test(iscoachmember,login_url="/rowers/paidplans",redirect_field_name=None) @login_required()
def team_deleteconfirm_view(request,id): def team_deleteconfirm_view(request,id):
r = getrower(request.user) r = getrower(request.user)
try: try:
@@ -474,7 +693,7 @@ def team_deleteconfirm_view(request,id):
'active':'nav-teams', 'active':'nav-teams',
}) })
@user_passes_test(iscoachmember,login_url="/rowers/paidplans",redirect_field_name=None) @login_required()
def team_delete_view(request,id): def team_delete_view(request,id):
r = getrower(request.user) r = getrower(request.user)
try: try:
@@ -490,7 +709,7 @@ def team_delete_view(request,id):
response = HttpResponseRedirect(url) response = HttpResponseRedirect(url)
return response return response
@user_passes_test(iscoachmember,login_url="/rowers/paidplans",redirect_field_name=None) @login_required()
def team_members_stats_view(request,id): def team_members_stats_view(request,id):
r = getrower(request.user) r = getrower(request.user)
try: try:
@@ -534,3 +753,27 @@ def team_members_stats_view(request,id):
}) })
return response 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:
messages.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)
+2 -2
View File
@@ -4,7 +4,7 @@ from statements import *
def start_trial_view(request): def start_trial_view(request):
r = getrower(request.user) 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') messages.error(request,'You do not qualify for a trial')
url = '/rowers/paidplans' url = '/rowers/paidplans'
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
@@ -31,7 +31,7 @@ def start_trial_view(request):
def start_plantrial_view(request): def start_plantrial_view(request):
r = getrower(request.user) 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') messages.error(request,'You do not qualify for a trial')
url = '/rowers/paidplans' url = '/rowers/paidplans'
return HttpResponseRedirect(url) return HttpResponseRedirect(url)
+6 -4
View File
@@ -1,5 +1,5 @@
from statements import * from statements import *
import rowers.teams as teams
# Show the EMpower Oarlock generated Stroke Profile # Show the EMpower Oarlock generated Stroke Profile
@user_passes_test(ispromember,login_url="/rowers/paidplans/", @user_passes_test(ispromember,login_url="/rowers/paidplans/",
@@ -4235,16 +4235,18 @@ def team_workout_upload_view(request,message="",
rowerform = TeamInviteForm(request.POST) rowerform = TeamInviteForm(request.POST)
rowerform.fields.pop('email') rowerform.fields.pop('email')
rowerform.fields['user'].queryset = User.objects.filter(rower__isnull=False,rower__team__in=myteams).distinct() rowers = Rower.objects.filter(coachinggroups__in=[r.mycoachgroup]).distinct()
rowerform.fields['user'].queryset = User.objects.filter(rower__in=rowers).distinct()
if form.is_valid(): if form.is_valid():
f = request.FILES['file'] f = request.FILES['file']
res = handle_uploaded_file(f) res = handle_uploaded_file(f)
t = form.cleaned_data['title'] t = form.cleaned_data['title']
offline = form.cleaned_data['offline'] offline = form.cleaned_data['offline']
boattype = form.cleaned_data['boattype'] boattype = form.cleaned_data['boattype']
workouttype = form.cleaned_data['workouttype']
if rowerform.is_valid(): if rowerform.is_valid():
u = rowerform.cleaned_data['user'] u = rowerform.cleaned_data['user']
if u: if u and request.user.rower in teams.rower_get_coaches(u.rower):
r = getrower(u) r = getrower(u)
else: else:
message = 'Please select a rower' message = 'Please select a rower'
@@ -4331,7 +4333,7 @@ def team_workout_upload_view(request,message="",
url = reverse('team_workout_upload_view') url = reverse('team_workout_upload_view')
response = HttpResponseRedirect(url) response = HttpResponseRedirect(url)
w = Workout.objects.get(id=encoder.decode_hex(id)) w = Workout.objects.get(id=id)
r = getrower(request.user) r = getrower(request.user)
if (make_plot): if (make_plot):
+6
View File
@@ -467,3 +467,9 @@ try:
OPAQUE_SECRET_KEY = CFG['opaque_secret_key'] OPAQUE_SECRET_KEY = CFG['opaque_secret_key']
except KeyError: except KeyError:
OPAQUE_SECRET_KEY = 0xa193443a OPAQUE_SECRET_KEY = 0xa193443a
# Celery or RQ
try:
CELERY = CFG['use_celery']
except KeyError:
CELERY = False
+1 -1
View File
@@ -216,7 +216,7 @@
</li> </li>
<li id="nav-teams"> <li id="nav-teams">
<a href="/rowers/me/teams/"> <a href="/rowers/me/teams/">
<i class="fas fa-bullhorn"></i>&nbsp;Teams <i class="fas fa-bullhorn"></i>&nbsp;Groups
</a> </a>
</li> </li>
</ul> </ul>
+1 -1
View File
@@ -227,7 +227,7 @@
</li> </li>
<li id="nav-teams"> <li id="nav-teams">
<a href="/rowers/me/teams/"> <a href="/rowers/me/teams/">
<i class="fas fa-bullhorn"></i>&nbsp;Teams <i class="fas fa-bullhorn"></i>&nbsp;Groups
</a> </a>
</li> </li>
</ul> </ul>