Merge branch 'release/v5.78'
This commit is contained in:
@@ -5,7 +5,7 @@ from django.contrib.auth.models import User
|
|||||||
from .models import (
|
from .models import (
|
||||||
Rower, Workout,GraphImage,FavoriteChart,SiteAnnouncement,
|
Rower, Workout,GraphImage,FavoriteChart,SiteAnnouncement,
|
||||||
Team,TeamInvite,TeamRequest,
|
Team,TeamInvite,TeamRequest,
|
||||||
WorkoutComment,C2WorldClassAgePerformance,
|
WorkoutComment,C2WorldClassAgePerformance,PlannedSession,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Register your models here so you can use them in the Admin module
|
# Register your models here so you can use them in the Admin module
|
||||||
@@ -44,10 +44,16 @@ class TeamRequestAdmin(admin.ModelAdmin):
|
|||||||
class WorkoutCommentAdmin(admin.ModelAdmin):
|
class WorkoutCommentAdmin(admin.ModelAdmin):
|
||||||
list_display = ('created','user','workout')
|
list_display = ('created','user','workout')
|
||||||
|
|
||||||
|
class PlannedSessionAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('name','startdate','enddate','manager','sessionvalue','sessionunit')
|
||||||
|
|
||||||
|
class GraphImageAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('creationdatetime','workout','filename')
|
||||||
|
|
||||||
admin.site.unregister(User)
|
admin.site.unregister(User)
|
||||||
admin.site.register(User,UserAdmin)
|
admin.site.register(User,UserAdmin)
|
||||||
admin.site.register(Workout,WorkoutAdmin)
|
admin.site.register(Workout,WorkoutAdmin)
|
||||||
admin.site.register(GraphImage)
|
admin.site.register(GraphImage,GraphImageAdmin)
|
||||||
admin.site.register(Team,TeamAdmin)
|
admin.site.register(Team,TeamAdmin)
|
||||||
admin.site.register(FavoriteChart,FavoriteChartAdmin)
|
admin.site.register(FavoriteChart,FavoriteChartAdmin)
|
||||||
admin.site.register(SiteAnnouncement,SiteAnnouncementAdmin)
|
admin.site.register(SiteAnnouncement,SiteAnnouncementAdmin)
|
||||||
@@ -56,3 +62,5 @@ admin.site.register(TeamRequest,TeamRequestAdmin)
|
|||||||
admin.site.register(WorkoutComment,WorkoutCommentAdmin)
|
admin.site.register(WorkoutComment,WorkoutCommentAdmin)
|
||||||
admin.site.register(C2WorldClassAgePerformance,
|
admin.site.register(C2WorldClassAgePerformance,
|
||||||
C2WorldClassAgePerformanceAdmin)
|
C2WorldClassAgePerformanceAdmin)
|
||||||
|
admin.site.register(PlannedSession,PlannedSessionAdmin)
|
||||||
|
|
||||||
|
|||||||
73
rowers/courses.py
Normal file
73
rowers/courses.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
# All the Courses related methods
|
||||||
|
|
||||||
|
# Python
|
||||||
|
from django.utils import timezone
|
||||||
|
from datetime import datetime
|
||||||
|
from datetime import timedelta
|
||||||
|
import time
|
||||||
|
from django.db import IntegrityError
|
||||||
|
import uuid
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
from utils import myqueue
|
||||||
|
|
||||||
|
from matplotlib import path
|
||||||
|
|
||||||
|
import django_rq
|
||||||
|
queue = django_rq.get_queue('default')
|
||||||
|
queuelow = django_rq.get_queue('low')
|
||||||
|
queuehigh = django_rq.get_queue('low')
|
||||||
|
|
||||||
|
from rowers.models import (
|
||||||
|
Rower, Workout,
|
||||||
|
GeoPoint,GeoPolygon, GeoCourse,
|
||||||
|
)
|
||||||
|
|
||||||
|
# low level methods
|
||||||
|
class InvalidTrajectoryError(Exception):
|
||||||
|
def __init__(self,value):
|
||||||
|
self.value=value
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return repr(self.value)
|
||||||
|
|
||||||
|
def polygon_to_path(polygon):
|
||||||
|
points = GeoPoint.objects.filter(polygon==polygon).order_by(order_in_polygon)
|
||||||
|
s = []
|
||||||
|
for point in points:
|
||||||
|
s.append([point.latitude,point.longitude])
|
||||||
|
|
||||||
|
p = path.Path(np.array(s))
|
||||||
|
|
||||||
|
return p
|
||||||
|
|
||||||
|
def coordinate_in_polygon(latitude,longitude, polygon):
|
||||||
|
p = polygon_to_path(polygon)
|
||||||
|
|
||||||
|
retun p.contains_points([(latitude,longitude)])[0]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def time_in_polygon(df,polygon,maxmin='max'):
|
||||||
|
# df has timestamp, latitude, longitude
|
||||||
|
p = polygon_to_path(polygon)
|
||||||
|
|
||||||
|
latitude = df.latitude
|
||||||
|
longitude = df.longitude
|
||||||
|
|
||||||
|
f = lambda x: coordinate_in_polygon(x['latitude'],x['longitude'],polygon)
|
||||||
|
|
||||||
|
df['inpolygon'] = df.apply(f,axis=1)
|
||||||
|
|
||||||
|
mask = df['inpolygon'] == True
|
||||||
|
|
||||||
|
if df[mask].empty():
|
||||||
|
raise InvalidTrajectoryError
|
||||||
|
|
||||||
|
if maxmin == 'max':
|
||||||
|
time = df[mask]['time'].max()
|
||||||
|
else:
|
||||||
|
time = df[mask]['time'].min()
|
||||||
|
|
||||||
|
|
||||||
|
return time
|
||||||
@@ -36,7 +36,7 @@ from rowingdata import (
|
|||||||
summarydata, get_file_type,
|
summarydata, get_file_type,
|
||||||
)
|
)
|
||||||
|
|
||||||
from rowers.metrics import axes
|
from rowers.metrics import axes,calc_trimp
|
||||||
from async_messages import messages as a_messages
|
from async_messages import messages as a_messages
|
||||||
import os
|
import os
|
||||||
import zipfile
|
import zipfile
|
||||||
@@ -2212,3 +2212,40 @@ def dataprep(rowdatadf, id=0, bands=True, barchart=True, otwpower=True,
|
|||||||
conn.close()
|
conn.close()
|
||||||
engine.dispose()
|
engine.dispose()
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
def workout_trimp(workout):
|
||||||
|
r = workout.user
|
||||||
|
df,row = getrowdata_db(id=workout.id)
|
||||||
|
df = clean_df_stats(df)
|
||||||
|
if df.empty:
|
||||||
|
df,row = getrowdata_db(id=workout.id)
|
||||||
|
df = clean_df_stats(df,workstrokesonly=False)
|
||||||
|
trimp = calc_trimp(df,r.sex,r.max,r.rest)
|
||||||
|
trimp = int(trimp)
|
||||||
|
|
||||||
|
return trimp
|
||||||
|
|
||||||
|
def workout_rscore(w):
|
||||||
|
r = workout.user
|
||||||
|
df,row = getrowdata_db(id=workout.id)
|
||||||
|
df = clean_df_stats(df)
|
||||||
|
if df.empty:
|
||||||
|
df,row = getrowdata_db(id=workout.id)
|
||||||
|
df = clean_df_stats(df,workstrokesonly=False)
|
||||||
|
|
||||||
|
duration = df['time'].max()-df['time'].min()
|
||||||
|
duration /= 1.0e3
|
||||||
|
pwr4 = df['power']**(4.0)
|
||||||
|
normp = (pwr4.mean())**(0.25)
|
||||||
|
if not np.isnan(normp):
|
||||||
|
ftp = float(r.ftp)
|
||||||
|
if w.workouttype in ('water','coastal'):
|
||||||
|
ftp = ftp*(100.-r.otwslack)/100.
|
||||||
|
|
||||||
|
intensityfactor = df['power'].mean()/float(ftp)
|
||||||
|
intensityfactor = normp/float(ftp)
|
||||||
|
tss = 100.*((duration*normp*intensityfactor)/(3600.*ftp))
|
||||||
|
else:
|
||||||
|
tss = 0
|
||||||
|
|
||||||
|
return tss
|
||||||
|
|||||||
@@ -602,3 +602,30 @@ class FusionMetricChoiceForm(ModelForm):
|
|||||||
metricchoices = list(sorted(formaxlabels2.items(), key = lambda x:x[1]))
|
metricchoices = list(sorted(formaxlabels2.items(), key = lambda x:x[1]))
|
||||||
self.fields['columns'].choices = metricchoices
|
self.fields['columns'].choices = metricchoices
|
||||||
|
|
||||||
|
class PlannedSessionSelectForm(forms.Form):
|
||||||
|
|
||||||
|
def __init__(self, sessionchoices, *args, **kwargs):
|
||||||
|
initialsession = kwargs.pop('initialsession',None)
|
||||||
|
super(PlannedSessionSelectForm, self).__init__(*args,**kwargs)
|
||||||
|
|
||||||
|
self.fields['plannedsession'] = forms.ChoiceField(
|
||||||
|
label='Sessions',
|
||||||
|
choices = sessionchoices,
|
||||||
|
widget = forms.RadioSelect,
|
||||||
|
initial=initialsession
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class WorkoutSessionSelectForm(forms.Form):
|
||||||
|
|
||||||
|
def __init__(self, workoutdata, *args, **kwargs):
|
||||||
|
|
||||||
|
super(WorkoutSessionSelectForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
self.fields['workouts'] = forms.MultipleChoiceField(
|
||||||
|
label='Workouts',
|
||||||
|
choices = workoutdata['choices'],
|
||||||
|
initial=workoutdata['initial'],
|
||||||
|
widget = forms.CheckboxSelectMultiple,
|
||||||
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -307,6 +307,7 @@ This value should be fairly constant across all stroke rates.""",
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def calc_trimp(df,sex,hrmax,hrmin):
|
def calc_trimp(df,sex,hrmax,hrmin):
|
||||||
if sex == 'male':
|
if sex == 'male':
|
||||||
f = 1.92
|
f = 1.92
|
||||||
@@ -321,6 +322,7 @@ def calc_trimp(df,sex,hrmax,hrmin):
|
|||||||
|
|
||||||
return trimp
|
return trimp
|
||||||
|
|
||||||
|
|
||||||
def getagegrouprecord(age,sex='male',weightcategory='hwt',
|
def getagegrouprecord(age,sex='male',weightcategory='hwt',
|
||||||
distance=2000,duration=None,indf=pd.DataFrame()):
|
distance=2000,duration=None,indf=pd.DataFrame()):
|
||||||
|
|
||||||
|
|||||||
281
rowers/models.py
281
rowers/models.py
@@ -483,6 +483,7 @@ class Rower(models.Model):
|
|||||||
plans = (
|
plans = (
|
||||||
('basic','basic'),
|
('basic','basic'),
|
||||||
('pro','pro'),
|
('pro','pro'),
|
||||||
|
('plan','plan'),
|
||||||
('coach','coach')
|
('coach','coach')
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -623,10 +624,284 @@ def checkworkoutuser(user,workout):
|
|||||||
except Rower.DoesNotExist:
|
except Rower.DoesNotExist:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# Check if user is coach or rower
|
||||||
|
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
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
except Rower.DoesNotExist:
|
||||||
|
return False
|
||||||
|
|
||||||
timezones = (
|
timezones = (
|
||||||
(x,x) for x in pytz.common_timezones
|
(x,x) for x in pytz.common_timezones
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# models related to geo data (points, polygon, courses)
|
||||||
|
|
||||||
|
class GeoCourse(models.Model):
|
||||||
|
manager = models.ForeignKey(Rower)
|
||||||
|
name = models.CharField(max_length=150,blank=True)
|
||||||
|
|
||||||
|
|
||||||
|
class GeoPolygon(models.Model):
|
||||||
|
course = models.ForeignKey(GeoCourse, blank=True)
|
||||||
|
order_in_course = models.IntegerField(default=0)
|
||||||
|
|
||||||
|
# Need error checking to insert new polygons into existing course (all later polygons
|
||||||
|
# increase there order_in_course number
|
||||||
|
|
||||||
|
class GeoPoint(models.Model):
|
||||||
|
latitude = models.FloatField(default=0)
|
||||||
|
longitude = models.FloatField(default=0)
|
||||||
|
polygon = models.ForeignKey(GeoPolygon,blank=True)
|
||||||
|
order_in_poly = models.IntegerField(default=0)
|
||||||
|
|
||||||
|
# need error checking to "insert" new point into existing polygon? This affects order_in_poly
|
||||||
|
# of multiple GeoPoint instances
|
||||||
|
|
||||||
|
|
||||||
|
def half_year_from_now():
|
||||||
|
return timezone.now()+timezone.timedelta(days=182)
|
||||||
|
|
||||||
|
|
||||||
|
# models related to training planning - draft
|
||||||
|
# Do we need a separate class TestTarget?
|
||||||
|
class TrainingTarget(models.Model):
|
||||||
|
rower = models.ForeignKey(Rower)
|
||||||
|
name = models.CharField(max_length=150,blank=True)
|
||||||
|
date = models.DateField(
|
||||||
|
default=half_year_from_now)
|
||||||
|
notes = models.TextField(max_length=300,blank=True)
|
||||||
|
|
||||||
|
class TrainingTargetForm(ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = TrainingTarget
|
||||||
|
fields = ['name','date','notes']
|
||||||
|
|
||||||
|
widgets = {
|
||||||
|
'date': SelectDateWidget(
|
||||||
|
years=range(
|
||||||
|
timezone.now().year-1,timezone.now().year+1)),
|
||||||
|
}
|
||||||
|
|
||||||
|
# SportTracks has a TrainingGoal like this
|
||||||
|
#class TrainingGoal(models.Model):
|
||||||
|
# rower = models.ForeignKey(Rower)
|
||||||
|
# name = models.CharField(max_length=150,blank=True)
|
||||||
|
# startdate = models.DateField(default=timezone.now)
|
||||||
|
# enddate = models.DateField(
|
||||||
|
# default=timezone.now()+datetime.timedelta(days=28))
|
||||||
|
# goalmetric = models.CharField(max_length=150,default='rower',
|
||||||
|
# choices = modechoices)
|
||||||
|
# value = models.IntegerValue(default=1)
|
||||||
|
|
||||||
|
# I think we can use PlannedSession for that (in challenge mode)
|
||||||
|
# although such a TrainingGoal could have automatically calculated
|
||||||
|
# values without needing the user to assign
|
||||||
|
|
||||||
|
|
||||||
|
class TrainingPlan(models.Model):
|
||||||
|
rower = models.ForeignKey(Rower)
|
||||||
|
name = models.CharField(max_length=150,blank=True)
|
||||||
|
target = models.ForeignKey(TrainingTarget,blank=True)
|
||||||
|
startdate = models.DateField(default=timezone.now)
|
||||||
|
enddate = models.DateField(
|
||||||
|
default=half_year_from_now)
|
||||||
|
|
||||||
|
class TrainingPlanForm(ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = TrainingPlan
|
||||||
|
fields = ['name','target','startdate','enddate']
|
||||||
|
|
||||||
|
widgets = {
|
||||||
|
'startdate': SelectDateWidget(
|
||||||
|
years=range(
|
||||||
|
timezone.now().year-1,timezone.now().year+1)),
|
||||||
|
'enddate': SelectDateWidget(
|
||||||
|
years=range(
|
||||||
|
timezone.now().year-1,timezone.now().year+1)),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
cycletypechoices = (
|
||||||
|
('filler','System Defined'),
|
||||||
|
('userdefined','User Defined')
|
||||||
|
)
|
||||||
|
|
||||||
|
class TrainingMacroCycle(models.Model):
|
||||||
|
plan = models.ForeignKey(TrainingPlan)
|
||||||
|
name = models.CharField(max_length=150,blank=True)
|
||||||
|
startdate = models.DateField(default=timezone.now)
|
||||||
|
enddate = models.DateField(
|
||||||
|
default=half_year_from_now)
|
||||||
|
notes = models.TextField(max_length=300,blank=True)
|
||||||
|
type = models.CharField(default='filler',
|
||||||
|
choices=cycletypechoices,
|
||||||
|
max_length=150)
|
||||||
|
|
||||||
|
class TrainingMesoCycle(models.Model):
|
||||||
|
plan = models.ForeignKey(TrainingMacroCycle)
|
||||||
|
name = models.CharField(max_length=150,blank=True)
|
||||||
|
startdate = models.DateField(default=timezone.now)
|
||||||
|
enddate = models.DateField(
|
||||||
|
default=half_year_from_now)
|
||||||
|
notes = models.TextField(max_length=300,blank=True)
|
||||||
|
type = models.CharField(default='filler',
|
||||||
|
choices=cycletypechoices,
|
||||||
|
max_length=150)
|
||||||
|
|
||||||
|
|
||||||
|
class TrainingMicroCycle(models.Model):
|
||||||
|
plan = models.ForeignKey(TrainingMesoCycle)
|
||||||
|
name = models.CharField(max_length=150,blank=True)
|
||||||
|
startdate = models.DateField(default=timezone.now)
|
||||||
|
enddate = models.DateField(
|
||||||
|
default=half_year_from_now)
|
||||||
|
notes = models.TextField(max_length=300,blank=True)
|
||||||
|
type = models.CharField(default='filler',
|
||||||
|
choices=cycletypechoices,
|
||||||
|
max_length=150)
|
||||||
|
|
||||||
|
|
||||||
|
# Needs some error checking
|
||||||
|
# - Microcycles should not overlap with other microcycles, same for MesoCycles, MacroCycles
|
||||||
|
# - When a TrainingPlan is created, it should create 1 "collector" Macro, Meso & MicroCycle - this is invisible for users who choose to not use cycles
|
||||||
|
# - When a new Microcycle is inserted, the "collector" cycle is automatically adjusted to "go out of the way" of the new MicroCycle - and similar for Macro & Meso
|
||||||
|
# - If the entire MesoCycle is filled with user defined MicroCycles - there are no "filler" MicroCycles
|
||||||
|
# - Sessions are automatically linked to the correct Cycles based on their start/end date - no need for a hard link
|
||||||
|
|
||||||
|
# Cycle error checking goes in forms
|
||||||
|
|
||||||
|
# model for Planned Session (Workout, Challenge, Test)
|
||||||
|
class PlannedSession(models.Model):
|
||||||
|
|
||||||
|
sessiontypechoices = (
|
||||||
|
('session','Training Session'),
|
||||||
|
('challenge','Challenge'),
|
||||||
|
('test','Mandatory Test'),
|
||||||
|
)
|
||||||
|
|
||||||
|
sessionmodechoices = (
|
||||||
|
('distance','Distance'),
|
||||||
|
('time','Time'),
|
||||||
|
('rScore','rScore'),
|
||||||
|
('TRIMP','TRIMP'),
|
||||||
|
)
|
||||||
|
|
||||||
|
criteriumchoices = (
|
||||||
|
('none','Approximately'),
|
||||||
|
('minimum','At Least'),
|
||||||
|
('exact','Exactly'),
|
||||||
|
)
|
||||||
|
|
||||||
|
verificationchoices = (
|
||||||
|
('none','None'),
|
||||||
|
('automatic','Automatic'),
|
||||||
|
('manual','Manual')
|
||||||
|
)
|
||||||
|
|
||||||
|
sessionunitchoices = (
|
||||||
|
('min','minutes'),
|
||||||
|
('km','km'),
|
||||||
|
('m','meters'),
|
||||||
|
('None',None),
|
||||||
|
)
|
||||||
|
|
||||||
|
manager = models.ForeignKey(User)
|
||||||
|
|
||||||
|
name = models.CharField(max_length=150,blank=True)
|
||||||
|
|
||||||
|
comment = models.TextField(max_length=300,blank=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
startdate = models.DateField(default=timezone.now,
|
||||||
|
verbose_name='Start Date')
|
||||||
|
|
||||||
|
enddate = models.DateField(default=timezone.now,
|
||||||
|
verbose_name='End Date')
|
||||||
|
|
||||||
|
sessiontype = models.CharField(default='session',
|
||||||
|
choices=sessiontypechoices,
|
||||||
|
max_length=150,
|
||||||
|
verbose_name='Session Type')
|
||||||
|
|
||||||
|
sessionvalue = models.IntegerField(default=60,verbose_name='Value')
|
||||||
|
|
||||||
|
max_nr_of_workouts = models.IntegerField(
|
||||||
|
default=0,verbose_name='Maximum number of workouts'
|
||||||
|
)
|
||||||
|
|
||||||
|
sessionunit = models.CharField(
|
||||||
|
default='min',choices=sessionunitchoices,
|
||||||
|
max_length=150,
|
||||||
|
verbose_name='Unit')
|
||||||
|
|
||||||
|
criterium = models.CharField(
|
||||||
|
default='none',
|
||||||
|
choices=criteriumchoices,
|
||||||
|
max_length=150)
|
||||||
|
|
||||||
|
verification = models.CharField(
|
||||||
|
default='none',
|
||||||
|
max_length=150,
|
||||||
|
choices=verificationchoices
|
||||||
|
)
|
||||||
|
|
||||||
|
team = models.ManyToManyField(Team,blank=True)
|
||||||
|
rower = models.ManyToManyField(Rower,blank=True)
|
||||||
|
|
||||||
|
sessionmode = models.CharField(default='distance',
|
||||||
|
choices=sessionmodechoices,
|
||||||
|
max_length=150,
|
||||||
|
verbose_name='Session Mode')
|
||||||
|
|
||||||
|
hasranking = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
|
||||||
|
name = self.name
|
||||||
|
startdate = self.startdate
|
||||||
|
enddate = self.enddate
|
||||||
|
|
||||||
|
stri = u'{n} {s} - {e}'.format(
|
||||||
|
s = startdate.strftime('%Y-%m-%d'),
|
||||||
|
e = enddate.strftime('%Y-%m-%d'),
|
||||||
|
n = name,
|
||||||
|
)
|
||||||
|
|
||||||
|
return stri
|
||||||
|
|
||||||
|
# Date input utility
|
||||||
|
class DateInput(forms.DateInput):
|
||||||
|
input_type = 'date'
|
||||||
|
|
||||||
|
class PlannedSessionForm(ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = PlannedSession
|
||||||
|
fields = ['startdate',
|
||||||
|
'enddate',
|
||||||
|
'name',
|
||||||
|
'sessiontype',
|
||||||
|
'sessionmode',
|
||||||
|
'criterium',
|
||||||
|
'sessionvalue',
|
||||||
|
'sessionunit',
|
||||||
|
'comment',
|
||||||
|
]
|
||||||
|
widgets = {
|
||||||
|
'comment': forms.Textarea,
|
||||||
|
'startdate': DateInput(),
|
||||||
|
'enddate': DateInput(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# Workout
|
# Workout
|
||||||
class Workout(models.Model):
|
class Workout(models.Model):
|
||||||
@@ -637,6 +912,7 @@ class Workout(models.Model):
|
|||||||
|
|
||||||
user = models.ForeignKey(Rower)
|
user = models.ForeignKey(Rower)
|
||||||
team = models.ManyToManyField(Team,blank=True)
|
team = models.ManyToManyField(Team,blank=True)
|
||||||
|
plannedsession = models.ForeignKey(PlannedSession, blank=True,null=True)
|
||||||
name = models.CharField(max_length=150)
|
name = models.CharField(max_length=150)
|
||||||
date = models.DateField()
|
date = models.DateField()
|
||||||
workouttype = models.CharField(choices=workouttypes,max_length=50)
|
workouttype = models.CharField(choices=workouttypes,max_length=50)
|
||||||
@@ -864,9 +1140,6 @@ def auto_delete_image_on_delete(sender,instance, **kwargs):
|
|||||||
else:
|
else:
|
||||||
print "couldn't find the file "+instance.filename
|
print "couldn't find the file "+instance.filename
|
||||||
|
|
||||||
# Date input utility
|
|
||||||
class DateInput(forms.DateInput):
|
|
||||||
input_type = 'date'
|
|
||||||
|
|
||||||
# Form to update Workout data
|
# Form to update Workout data
|
||||||
class WorkoutForm(ModelForm):
|
class WorkoutForm(ModelForm):
|
||||||
@@ -1327,7 +1600,7 @@ class RowerForm(ModelForm):
|
|||||||
# optionally sends a tweet to our twitter account
|
# optionally sends a tweet to our twitter account
|
||||||
class SiteAnnouncement(models.Model):
|
class SiteAnnouncement(models.Model):
|
||||||
created = models.DateField(default=timezone.now)
|
created = models.DateField(default=timezone.now)
|
||||||
announcement = models.TextField(max_length=140)
|
announcement = models.TextField(max_length=280)
|
||||||
expires = models.DateField(default=timezone.now)
|
expires = models.DateField(default=timezone.now)
|
||||||
modified = models.DateField(default=timezone.now)
|
modified = models.DateField(default=timezone.now)
|
||||||
dotweet = models.BooleanField(default=False)
|
dotweet = models.BooleanField(default=False)
|
||||||
|
|||||||
233
rowers/plannedsessions.py
Normal file
233
rowers/plannedsessions.py
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
# Python
|
||||||
|
from django.utils import timezone
|
||||||
|
from datetime import datetime
|
||||||
|
from datetime import timedelta
|
||||||
|
from datetime import date
|
||||||
|
import time
|
||||||
|
from django.db import IntegrityError
|
||||||
|
import uuid
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
from utils import myqueue
|
||||||
|
|
||||||
|
import django_rq
|
||||||
|
queue = django_rq.get_queue('default')
|
||||||
|
queuelow = django_rq.get_queue('low')
|
||||||
|
queuehigh = django_rq.get_queue('low')
|
||||||
|
|
||||||
|
from rowers.models import (
|
||||||
|
Rower, Workout,
|
||||||
|
GeoCourse, TrainingMicroCycle,TrainingMesoCycle,TrainingMacroCycle,
|
||||||
|
TrainingPlan,PlannedSession,
|
||||||
|
)
|
||||||
|
|
||||||
|
import metrics
|
||||||
|
import numpy as np
|
||||||
|
import dataprep
|
||||||
|
|
||||||
|
# Low Level functions - to be called by higher level methods
|
||||||
|
def add_workouts_plannedsession(ws,ps):
|
||||||
|
result = 0
|
||||||
|
comments = []
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
# check if all sessions have same date
|
||||||
|
dates = [w.date for w in ws]
|
||||||
|
if (not all(d == dates[0] for d in dates)) and ps.sessiontype != 'challenge':
|
||||||
|
errors.append('For tests and training sessions, selected workouts must all be done on the same date')
|
||||||
|
return result,comments,errors
|
||||||
|
|
||||||
|
# start adding sessions
|
||||||
|
for w in ws:
|
||||||
|
if w.date>=ps.startdate and w.date<=ps.enddate:
|
||||||
|
w.plannedsession = ps
|
||||||
|
w.save()
|
||||||
|
result += 1
|
||||||
|
comments.append('Attached workout %i to session' % w.id)
|
||||||
|
else:
|
||||||
|
errors.append('Workout %i did not match session dates' % w.id)
|
||||||
|
|
||||||
|
return result,comments,errors
|
||||||
|
|
||||||
|
|
||||||
|
def remove_workout_plannedsession(w,ps):
|
||||||
|
if w.plannedsession == ps:
|
||||||
|
w.plannedsession = None
|
||||||
|
w.save()
|
||||||
|
return 1
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def clone_planned_session(ps):
|
||||||
|
ps.save()
|
||||||
|
ps.pk = None # creates new instance
|
||||||
|
ps.save()
|
||||||
|
|
||||||
|
def timefield_to_seconds_duration(t):
|
||||||
|
duration = t.hour*3600.
|
||||||
|
duration += t.minute * 60.
|
||||||
|
duration += t.second
|
||||||
|
duration += t.microsecond/1.e6
|
||||||
|
|
||||||
|
return duration
|
||||||
|
|
||||||
|
def is_session_complete(r,ps):
|
||||||
|
status = 'not done'
|
||||||
|
|
||||||
|
ws = Workout.objects.filter(user=r,plannedsession=ps)
|
||||||
|
|
||||||
|
if len(ws)==0:
|
||||||
|
today = date.today()
|
||||||
|
if today > ps.enddate:
|
||||||
|
status = 'missed'
|
||||||
|
ratio = 0
|
||||||
|
return ratio,status
|
||||||
|
else:
|
||||||
|
return 0,'not done'
|
||||||
|
|
||||||
|
score = 0
|
||||||
|
for w in ws:
|
||||||
|
if ps.sessionmode == 'distance':
|
||||||
|
score += w.distance
|
||||||
|
elif ps.sessionmode == 'time':
|
||||||
|
durationseconds = timefield_to_seconds_duration(w.duration)
|
||||||
|
score += durationseconds
|
||||||
|
elif ps.sessionmode == 'TRIMP':
|
||||||
|
trimp = dataprep.workout_trimp(w)
|
||||||
|
score += trimp
|
||||||
|
elif ps.sessionmode == 'rScore':
|
||||||
|
rscore = dataprep.workout_rscore(w)
|
||||||
|
score += rscore
|
||||||
|
|
||||||
|
value = ps.sessionvalue
|
||||||
|
if ps.sessionunit == 'min':
|
||||||
|
value *= 60.
|
||||||
|
elif ps.sessionunit == 'km':
|
||||||
|
value *= 1000.
|
||||||
|
|
||||||
|
ratio = score/float(value)
|
||||||
|
|
||||||
|
status = 'partial'
|
||||||
|
|
||||||
|
if ps.sessiontype == 'session':
|
||||||
|
if ps.criterium == 'exact':
|
||||||
|
if ratio == 1.0:
|
||||||
|
return ratio,'completed'
|
||||||
|
else:
|
||||||
|
return ratio,'partial'
|
||||||
|
elif ps.criterium == 'minimum':
|
||||||
|
if ratio > 1.0:
|
||||||
|
return ratio,'completed'
|
||||||
|
else:
|
||||||
|
return ratio,'partial'
|
||||||
|
else:
|
||||||
|
if ratio>0.8 and ratio<1.2:
|
||||||
|
return ratio,'completed'
|
||||||
|
else:
|
||||||
|
return ratio,'partial'
|
||||||
|
elif ps.sessiontype == 'test':
|
||||||
|
if ratio==1.0:
|
||||||
|
return ratio,'completed'
|
||||||
|
else:
|
||||||
|
return ratio,'partial'
|
||||||
|
elif ps.sessiontype == 'challenge':
|
||||||
|
if ps.criterium == 'exact':
|
||||||
|
if ratio == 1.0:
|
||||||
|
return ratio,'completed'
|
||||||
|
else:
|
||||||
|
return ratio,'partial'
|
||||||
|
elif ps.criterium == 'minimum':
|
||||||
|
if ratio > 1.0:
|
||||||
|
return ratio,'completed'
|
||||||
|
else:
|
||||||
|
return ratio,'partial'
|
||||||
|
else:
|
||||||
|
return ratio,'partial'
|
||||||
|
|
||||||
|
else:
|
||||||
|
return ratio,status
|
||||||
|
|
||||||
|
def rank_results(ps):
|
||||||
|
return 1
|
||||||
|
|
||||||
|
def add_team_session(t,ps):
|
||||||
|
ps.team.add(t)
|
||||||
|
ps.save()
|
||||||
|
|
||||||
|
return 1
|
||||||
|
|
||||||
|
def add_rower_session(r,ps):
|
||||||
|
ps.rower.add(r)
|
||||||
|
ps.save()
|
||||||
|
|
||||||
|
return 1
|
||||||
|
|
||||||
|
def remove_team_session(t,ps):
|
||||||
|
ps.team.remove(t)
|
||||||
|
|
||||||
|
return 1
|
||||||
|
|
||||||
|
def remove_rower_session(r,ps):
|
||||||
|
ps.rower.remove(r)
|
||||||
|
|
||||||
|
return 1
|
||||||
|
|
||||||
|
def get_dates_timeperiod(timeperiod):
|
||||||
|
# set start end date according timeperiod
|
||||||
|
if timeperiod=='today':
|
||||||
|
startdate=date.today()
|
||||||
|
enddate=date.today()
|
||||||
|
elif timeperiod=='tomorrow':
|
||||||
|
startdate=date.today()+timezone.timedelta(days=1)
|
||||||
|
enddate=date.today()+timezone.timedelta(days=1)
|
||||||
|
elif timeperiod=='thisweek':
|
||||||
|
today = date.today()
|
||||||
|
startdate = date.today()-timezone.timedelta(days=today.weekday())
|
||||||
|
enddate = startdate+timezone.timedelta(days=6)
|
||||||
|
elif timeperiod=='thismonth':
|
||||||
|
today = date.today()
|
||||||
|
startdate = today.replace(day=1)
|
||||||
|
enddate = startdate+timezone.timedelta(days=32)
|
||||||
|
enddate = enddate.replace(day=1)
|
||||||
|
enddate = enddate-timezone.timedelta(days=1)
|
||||||
|
elif timeperiod=='lastweek':
|
||||||
|
today = date.today()
|
||||||
|
enddate = today-timezone.timedelta(days=today.weekday())-timezone.timedelta(days=1)
|
||||||
|
startdate = enddate-timezone.timedelta(days=6)
|
||||||
|
elif timeperiod=='lastmonth':
|
||||||
|
today = date.today()
|
||||||
|
startdate = today.replace(day=1)
|
||||||
|
startdate = startdate-timezone.timedelta(days=3)
|
||||||
|
startdate = startdate.replace(day=1)
|
||||||
|
enddate = startdate+timezone.timedelta(days=32)
|
||||||
|
enddate = enddate.replace(day=1)
|
||||||
|
enddate = enddate-timezone.timedelta(days=1)
|
||||||
|
else:
|
||||||
|
startdate = date.today()
|
||||||
|
enddate = date.today()
|
||||||
|
|
||||||
|
return startdate,enddate
|
||||||
|
|
||||||
|
def get_sessions(r,startdate=date.today(),
|
||||||
|
enddate=date.today()+timezone.timedelta(+1000)):
|
||||||
|
|
||||||
|
sps = PlannedSession.objects.filter(
|
||||||
|
rower__in=[r],
|
||||||
|
startdate__lte=enddate,
|
||||||
|
enddate__gte=startdate,
|
||||||
|
).order_by("startdate","enddate")
|
||||||
|
|
||||||
|
return sps
|
||||||
|
|
||||||
|
def get_workouts_session(r,ps):
|
||||||
|
ws = Workout.objects.filter(user=r,plannedsession=ps)
|
||||||
|
|
||||||
|
return ws
|
||||||
|
|
||||||
|
def update_plannedsession(ps,cd):
|
||||||
|
for attr, value in cd.items():
|
||||||
|
setattr(ps, attr, value)
|
||||||
|
|
||||||
|
ps.save()
|
||||||
|
|
||||||
|
return 1,'Planned Session Updated'
|
||||||
@@ -90,15 +90,9 @@ def handle_uploaded_image(i):
|
|||||||
break
|
break
|
||||||
exif=dict(image._getexif().items())
|
exif=dict(image._getexif().items())
|
||||||
|
|
||||||
if exif[orientation] == 3:
|
|
||||||
image=image.rotate(180, expand=True)
|
|
||||||
elif exif[orientation] == 6:
|
|
||||||
image=image.rotate(270, expand=True)
|
|
||||||
elif exif[orientation] == 8:
|
|
||||||
image=image.rotate(90, expand=True)
|
|
||||||
except (AttributeError, KeyError, IndexError):
|
except (AttributeError, KeyError, IndexError):
|
||||||
# cases: image don't have getexif
|
# cases: image don't have getexif
|
||||||
pass
|
exif = {'orientation':0}
|
||||||
|
|
||||||
if image.mode not in ("L", "RGB"):
|
if image.mode not in ("L", "RGB"):
|
||||||
image = image.convert("RGB")
|
image = image.convert("RGB")
|
||||||
@@ -108,6 +102,12 @@ def handle_uploaded_image(i):
|
|||||||
hsize = int((float(image.size[1])*float(wpercent)))
|
hsize = int((float(image.size[1])*float(wpercent)))
|
||||||
image = image.resize((basewidth,hsize), Image.ANTIALIAS)
|
image = image.resize((basewidth,hsize), Image.ANTIALIAS)
|
||||||
|
|
||||||
|
if exif[orientation] == 3:
|
||||||
|
image=image.rotate(180, expand=True)
|
||||||
|
elif exif[orientation] == 6:
|
||||||
|
image=image.rotate(270, expand=True)
|
||||||
|
elif exif[orientation] == 8:
|
||||||
|
image=image.rotate(90, expand=True)
|
||||||
|
|
||||||
filename = hashlib.md5(imagefile.getvalue()).hexdigest()+'.jpg'
|
filename = hashlib.md5(imagefile.getvalue()).hexdigest()+'.jpg'
|
||||||
|
|
||||||
|
|||||||
@@ -185,7 +185,8 @@ def get_strava_workout(user,stravaid):
|
|||||||
if nr_rows == 0:
|
if nr_rows == 0:
|
||||||
return (0,"Error: Time data had zero length")
|
return (0,"Error: Time data had zero length")
|
||||||
except IndexError:
|
except IndexError:
|
||||||
return (0,"Error: No Distance information in the Strava data")
|
d = 0*t
|
||||||
|
# return (0,"Error: No Distance information in the Strava data")
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return (0,"something went wrong with the Strava import")
|
return (0,"something went wrong with the Strava import")
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,6 @@
|
|||||||
|
|
||||||
<div class="grid_12">
|
<div class="grid_12">
|
||||||
|
|
||||||
Select start and end date for a date range:
|
|
||||||
<div class="grid_4 alpha">
|
<div class="grid_4 alpha">
|
||||||
|
|
||||||
{% if team %}
|
{% if team %}
|
||||||
@@ -64,9 +63,25 @@
|
|||||||
</table>
|
</table>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
</div>
|
</div>
|
||||||
<div class="grid_2 suffix_6 alpha">
|
<div class="grid_2">
|
||||||
<input name='daterange' class="button green" type="submit" value="Submit"> </form>
|
<input name='daterange' class="button green" type="submit" value="Submit"> </form>
|
||||||
</div>
|
</div>
|
||||||
|
{% if user.is_authenticated and user|is_manager %}
|
||||||
|
<div class="grid_2 omega dropdown">
|
||||||
|
<button class="grid_2 alpha button green small dropbtn">
|
||||||
|
Change Rower
|
||||||
|
</button>
|
||||||
|
<div class="dropdown-content">
|
||||||
|
{% for member in user|team_members %}
|
||||||
|
<a class="button green small" href="/rowers/u/{{ member.id }}/list-workouts/{{ startdate|date:"Y-m-d" }}/{{ enddate|date:"Y-m-d" }}">{{ member.first_name }} {{ member.last_name }}</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -77,11 +92,12 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="grid_12">
|
<div class="grid_12">
|
||||||
|
|
||||||
<div id="workouts_table" class="grid_8 alpha">
|
<div id="workouts_table" class="grid_8 alpha">
|
||||||
{% if team %}
|
{% if team %}
|
||||||
<h3>{{ team.name }} Team Workouts</h3>
|
<h3>{{ team.name }} Team Workouts</h3>
|
||||||
{% else %}
|
{% else %}
|
||||||
<h3>My Workouts</h3>
|
<h3>Workouts of {{ rower.user.first_name }} {{ rower.user.last_name }}</h3>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if workouts %}
|
{% if workouts %}
|
||||||
@@ -154,7 +170,10 @@
|
|||||||
</td>
|
</td>
|
||||||
{% else %}
|
{% else %}
|
||||||
<td colspan="2">
|
<td colspan="2">
|
||||||
{{ workout.user.user.first_name }} {{ workout.user.user.last_name }}
|
<a class="small" href="/rowers/{{ workout.user.id }}/list-workouts">
|
||||||
|
{{ workout.user.user.first_name }}
|
||||||
|
{{ workout.user.user.last_name }}
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td> <a class="small" href="/rowers/workout/{{ workout.id }}/flexchart">Flex</a> </td>
|
<td> <a class="small" href="/rowers/workout/{{ workout.id }}/flexchart">Flex</a> </td>
|
||||||
|
|||||||
57
rowers/templates/planmembership.html
Normal file
57
rowers/templates/planmembership.html
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}About us{% endblock title %}
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="grid_6 alpha">
|
||||||
|
<h2>Coach and Self-Coach Membership</h2>
|
||||||
|
|
||||||
|
<p>You have arrived at this page, because you tried to create a
|
||||||
|
training plan for yourself.</p>
|
||||||
|
|
||||||
|
<p>Currently, training planning is restricted to "coach" members with
|
||||||
|
"team" functionality.</p>
|
||||||
|
|
||||||
|
<p>If you are interested in becoming a coach and planning sessions
|
||||||
|
for a group of rowers on rowsandall.com, contact me through the contact
|
||||||
|
form.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>If you would like to find a coach who helps you plan your training
|
||||||
|
through rowsandall.com, contact me throught the contact form.</p>
|
||||||
|
|
||||||
|
<p>For self-coached rowers who would like to add the training planning
|
||||||
|
functionality, we will soon establish a "Self-Coach" plan, which will enable you to do so.</p>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid_6 omega">
|
||||||
|
<h2>What training planning functionality do we offer?</h2>
|
||||||
|
|
||||||
|
<p>Over the spring of 2018, we will gradually expand this functionality.
|
||||||
|
Our current roadmap is to deploy the following and more:</li>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<ul>
|
||||||
|
<li>Create Planned Sessions (trainings, tests, challenges) for yourself
|
||||||
|
and for your team members (coach plan)</li>
|
||||||
|
<li>Track your performance against plan.
|
||||||
|
Match workouts to planned sessions.
|
||||||
|
Get feedback on plan adherence.</li>
|
||||||
|
<li>Track your teams performance against plan. See how well each
|
||||||
|
of your team members adhere to their (team or personalized) plan.</li>
|
||||||
|
<li>See test outcomes ranked by performance.</li>
|
||||||
|
<li>Attach courses to your OTW tests. This advanced functionality
|
||||||
|
allows you, for example, to assign "Row the 6km from bridge A to
|
||||||
|
bridge B on Saturday" to your team members. The resulting workout
|
||||||
|
tracks will be evaluated against the course, and you will receive
|
||||||
|
a results table for the net time spent between the start and finish
|
||||||
|
points on the course. It's like a mini head race.
|
||||||
|
</ul>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock content %}
|
||||||
112
rowers/templates/plannedsessioncreate.html
Normal file
112
rowers/templates/plannedsessioncreate.html
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% load staticfiles %}
|
||||||
|
|
||||||
|
{% block title %}New Planned Session{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="grid_12 alpha">
|
||||||
|
{% include "planningbuttons.html" %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="grid_12 alpha">
|
||||||
|
<div id="left" class="grid_6 alpha">
|
||||||
|
<h1>Create Session for {{ rower.user.first_name }} {{ rower.user.last_name }}</h1>
|
||||||
|
</div>
|
||||||
|
<div id="timeperiod" class="grid_2 dropdown">
|
||||||
|
<button class="grid_2 alpha button gray small dropbtn">Time Period</button>
|
||||||
|
<div class="dropdown-content">
|
||||||
|
<a class="button gray small alpha"
|
||||||
|
href="/rowers/sessions/create/today/rower/{{ rower.id }}">
|
||||||
|
Today
|
||||||
|
</a>
|
||||||
|
<a class="button gray small alpha"
|
||||||
|
href="/rowers/sessions/create/thisweek/rower/{{ rower.id }}">
|
||||||
|
This Week
|
||||||
|
</a>
|
||||||
|
<a class="button gray small alpha"
|
||||||
|
href="/rowers/sessions/create/thismonth/rower/{{ rower.id }}">
|
||||||
|
This Month
|
||||||
|
</a>
|
||||||
|
<a class="button gray small alpha"
|
||||||
|
href="/rowers/sessions/create/lastweek/rower/{{ rower.id }}">
|
||||||
|
Last Week
|
||||||
|
</a>
|
||||||
|
<a class="button gray small alpha"
|
||||||
|
href="/rowers/sessions/create/lastmonth/rower/{{ rower.id }}">
|
||||||
|
Last Month
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid_12 alpha">
|
||||||
|
<div class="grid_6 alpha">
|
||||||
|
<form enctype="multipart/form-data" action="{{ formloc }}" method="post">
|
||||||
|
{% if form.errors %}
|
||||||
|
<p style="color: red;">
|
||||||
|
Please correct the error{{ form.errors|pluralize }} below.
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<table>
|
||||||
|
{{ form.as_table }}
|
||||||
|
</table>
|
||||||
|
{% csrf_token %}
|
||||||
|
<div id="formbutton" class="grid_1 prefix_4 suffix_1">
|
||||||
|
<input class="button green" type="submit" value="Submit">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="right" class="grid_6 omega">
|
||||||
|
<h1>Plan</h1>
|
||||||
|
<p>
|
||||||
|
Click on session name to view
|
||||||
|
</p>
|
||||||
|
<table class="listtable shortpadded" width="80%">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>After</th>
|
||||||
|
<th>Before</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Value</th>
|
||||||
|
<th> </th>
|
||||||
|
<th>Edit</th>
|
||||||
|
<th>Clone</th>
|
||||||
|
<th>Delete</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for ps in plannedsessions %}
|
||||||
|
<tr>
|
||||||
|
<td> {{ ps.startdate|date:"Y-m-d" }} </td>
|
||||||
|
<td> {{ ps.enddate|date:"Y-m-d" }} </td>
|
||||||
|
<td>
|
||||||
|
{% if ps.name != '' %}
|
||||||
|
<a class="small"
|
||||||
|
href="/rowers/sessions/{{ ps.id }}">{{ ps.name }}</a>
|
||||||
|
{% else %}
|
||||||
|
<a class="small"
|
||||||
|
href="/rowers/sessions/{{ ps.id }}">Unnamed Session</a>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td> {{ ps.sessionvalue }} </td>
|
||||||
|
<td> {{ ps.sessionunit }} </td>
|
||||||
|
<td>
|
||||||
|
<a class="small" href="/rowers/sessions/{{ ps.id }}/edit">Edit</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a class="small" href="/rowers/sessions/{{ ps.id }}/clone">Clone</a>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<a class="small" href="/rowers/sessions/{{ ps.id }}/deleteconfirm">Delete</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
45
rowers/templates/plannedsessiondeleteconfirm.html
Normal file
45
rowers/templates/plannedsessiondeleteconfirm.html
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% load staticfiles %}
|
||||||
|
|
||||||
|
{% block title %}Planned Session{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="grid_12 alpha">
|
||||||
|
{% include "planningbuttons.html" %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div id="left" class="grid_6 alpha">
|
||||||
|
<h1>Confirm Delete</h1>
|
||||||
|
<p>This will permanently delete the planned session</p>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="grid_2 alpha">
|
||||||
|
<p>
|
||||||
|
<a class="button green small" href="/rowers/sessions/">Cancel</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid_2">
|
||||||
|
<p>
|
||||||
|
<a class="button red small" href="/rowers/sessions/{{ ps.id }}/delete">Delete</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div id="right" class="grid_6 omega">
|
||||||
|
<h1>Session {{ psdict.name.1 }}</h1>
|
||||||
|
<table class="listtable shortpadded">
|
||||||
|
{% for attr in attrs %}
|
||||||
|
{% for key,value in psdict.items %}
|
||||||
|
{% if key == attr %}
|
||||||
|
<tr>
|
||||||
|
<td><b>{{ value.0 }}</b></td><td>{{ value.1 }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
123
rowers/templates/plannedsessionedit.html
Normal file
123
rowers/templates/plannedsessionedit.html
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% load staticfiles %}
|
||||||
|
|
||||||
|
{% block title %}Update Planned Session{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="grid_12 alpha">
|
||||||
|
{% include "planningbuttons.html" %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="grid_12 alpha">
|
||||||
|
<div id="left" class="grid_6 alpha">
|
||||||
|
<h1>Edit Session {{ thesession.name }}</h1>
|
||||||
|
</div>
|
||||||
|
<div id="timeperiod" class="grid_2 dropdown">
|
||||||
|
<button class="grid_2 alpha button gray small dropbtn">Time Period</button>
|
||||||
|
<div class="dropdown-content">
|
||||||
|
<a class="button gray small alpha"
|
||||||
|
href="/rowers/sessions/create/today/rower/{{ rower.id }}">
|
||||||
|
Today
|
||||||
|
</a>
|
||||||
|
<a class="button gray small alpha"
|
||||||
|
href="/rowers/sessions/create/thisweek/rower/{{ rower.id }}">
|
||||||
|
This Week
|
||||||
|
</a>
|
||||||
|
<a class="button gray small alpha"
|
||||||
|
href="/rowers/sessions/create/thismonth/rower/{{ rower.id }}">
|
||||||
|
This Month
|
||||||
|
</a>
|
||||||
|
<a class="button gray small alpha"
|
||||||
|
href="/rowers/sessions/create/lastweek/rower/{{ rower.id }}">
|
||||||
|
Last Week
|
||||||
|
</a>
|
||||||
|
<a class="button gray small alpha"
|
||||||
|
href="/rowers/sessions/create/lastmonth/rower/{{ rower.id }}">
|
||||||
|
Last Month
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid_12 alpha">
|
||||||
|
<div class="grid_6 alpha">
|
||||||
|
<form enctype="multipart/form-data" action="{{ formloc }}" method="post">
|
||||||
|
{% if form.errors %}
|
||||||
|
<p style="color: red;">
|
||||||
|
Please correct the error{{ form.errors|pluralize }} below.
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<table>
|
||||||
|
{{ form.as_table }}
|
||||||
|
</table>
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="grid_1 prefix_2 alpha">
|
||||||
|
<a class="red button small" href="/rowers/sessions/{{ thesession.id }}/deleteconfirm">Delete</a>
|
||||||
|
</div>
|
||||||
|
<div class="grid_1">
|
||||||
|
<a class="gray button small" href="/rowers/sessions/{{ thesession.id }}/clone">Clone</a>
|
||||||
|
</div>
|
||||||
|
<div id="formbutton" class="grid_1 suffix_1 omega">
|
||||||
|
<input class="button green" type="submit" value="Save">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="right" class="grid_6 omega">
|
||||||
|
<h1>Plan</h1>
|
||||||
|
<p>
|
||||||
|
Click on session name to view
|
||||||
|
</p>
|
||||||
|
<table class="listtable shortpadded" width="80%">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>After</th>
|
||||||
|
<th>Before</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Value</th>
|
||||||
|
<th> </th>
|
||||||
|
<th>Edit</th>
|
||||||
|
<th>Clone</th>
|
||||||
|
<th>Delete</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for ps in plannedsessions %}
|
||||||
|
<tr>
|
||||||
|
<td> {{ ps.startdate|date:"Y-m-d" }} </td>
|
||||||
|
<td> {{ ps.enddate|date:"Y-m-d" }} </td>
|
||||||
|
<td>
|
||||||
|
{% if ps.name != '' %}
|
||||||
|
<a class="small"
|
||||||
|
href="/rowers/sessions/{{ ps.id }}">{{ ps.name }}</a>
|
||||||
|
{% else %}
|
||||||
|
<a class="small"
|
||||||
|
href="/rowers/sessions/{{ ps.id }}">Unnamed Session</a>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td> {{ ps.sessionvalue }} </td>
|
||||||
|
<td> {{ ps.sessionunit }} </td>
|
||||||
|
<td>
|
||||||
|
{% if timeperiod and rower %}
|
||||||
|
<a class="small" href="/rowers/sessions/{{ ps.id }}/edit/{{ timeperiod }}/rower/{{ rower.id }}">Edit</a>
|
||||||
|
{% elif timeperiod %}
|
||||||
|
<a class="small" href="/rowers/sessions/{{ ps.id }}/edit/{{ timeperiod }}">Edit</a>
|
||||||
|
{% else %}
|
||||||
|
<a class="small" href="/rowers/sessions/{{ ps.id }}/edit">Edit</a>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a class="small" href="/rowers/sessions/{{ ps.id }}/clone">Clone</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a class="small" href="/rowers/sessions/{{ ps.id }}/deleteconfirm">Delete</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
109
rowers/templates/plannedsessions.html
Normal file
109
rowers/templates/plannedsessions.html
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% load staticfiles %}
|
||||||
|
{% load rowerfilters %}
|
||||||
|
|
||||||
|
{% block title %}Planned Sessions{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="grid_12 alpha">
|
||||||
|
{% include "planningbuttons.html" %}
|
||||||
|
</div>
|
||||||
|
<div class="grid_6 alpha">
|
||||||
|
<h1>Plan for {{ rower.user.first_name }} {{ rower.user.last_name }}</h1>
|
||||||
|
</div>
|
||||||
|
<div id="timeperiod" class="grid_2 dropdown">
|
||||||
|
<button class="grid_2 alpha button gray small dropbtn">Time Period</button>
|
||||||
|
<div class="dropdown-content">
|
||||||
|
<a class="button gray small alpha"
|
||||||
|
href="/rowers/sessions/today/rower/{{ rower.id }}">
|
||||||
|
Today
|
||||||
|
</a>
|
||||||
|
<a class="button gray small alpha"
|
||||||
|
href="/rowers/sessions/thisweek/rower/{{ rower.id }}">
|
||||||
|
This Week
|
||||||
|
</a>
|
||||||
|
<a class="button gray small alpha"
|
||||||
|
href="/rowers/sessions/thismonth/rower/{{ rower.id }}">
|
||||||
|
This Month
|
||||||
|
</a>
|
||||||
|
<a class="button gray small alpha"
|
||||||
|
href="/rowers/sessions/lastweek/rower/{{ rower.id }}">
|
||||||
|
Last Week
|
||||||
|
</a>
|
||||||
|
<a class="button gray small alpha"
|
||||||
|
href="/rowers/sessions/lastmonth/rower/{{ rower.id }}">
|
||||||
|
Last Month
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid_12 alpha">
|
||||||
|
{% if plannedsessions %}
|
||||||
|
<p>
|
||||||
|
Click on session name to view, edit to change the session and on the
|
||||||
|
traffic light symbol to add workouts to the session
|
||||||
|
</p>
|
||||||
|
<table width="80%" class="listtable shortpadded">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>After</th>
|
||||||
|
<th>Before</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Edit</th>
|
||||||
|
<th>Value</th>
|
||||||
|
<th> </th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for ps in plannedsessions %}
|
||||||
|
<tr>
|
||||||
|
<td> {{ ps.startdate|date:"Y-m-d" }} </td>
|
||||||
|
<td> {{ ps.enddate|date:"Y-m-d" }} </td>
|
||||||
|
<td>
|
||||||
|
{% if ps.name != '' %}
|
||||||
|
<a class="small"
|
||||||
|
href="/rowers/sessions/{{ ps.id }}">{{ ps.name }}</a>
|
||||||
|
{% else %}
|
||||||
|
<a class="small"
|
||||||
|
href="/rowers/sessions/{{ ps.id }}">Unnamed Session</a>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if ps.manager == request.user %}
|
||||||
|
<a class="small"
|
||||||
|
href="/rowers/sessions/{{ ps.id }}/edit">Edit</a>
|
||||||
|
{% else %}
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td> {{ ps.sessionvalue }} </td>
|
||||||
|
<td> {{ ps.sessionunit }} </td>
|
||||||
|
<td> {{ ps.sessiontype }} </td>
|
||||||
|
<td>
|
||||||
|
{% if completeness|lookup:ps.id == 'not done' %}
|
||||||
|
<a class="white dot" href="/rowers/sessions/manage/{{ timeperiod }}/rower/{{ rower.id }}/session/{{ ps.id }}"> </a>
|
||||||
|
{% elif completeness|lookup:ps.id == 'completed' %}
|
||||||
|
<a class="green dot" href="/rowers/sessions/manage/{{ timeperiod }}/rower/{{ rower.id }}/session/{{ ps.id }}"> </a>
|
||||||
|
{% elif completeness|lookup:ps.id == 'partial' %}
|
||||||
|
<a class="orange dot" href="/rowers/sessions/manage/{{ timeperiod }}/rower/{{ rower.id }}/session/{{ ps.id }}"> </a>
|
||||||
|
{% else %}
|
||||||
|
<a class="red dot" href="/rowers/sessions/manage/{{ timeperiod }}/rower/{{ rower.id }}/session/{{ ps.id }}"> </a>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
You have no planned workouts for this period. Planned workouts are created
|
||||||
|
by your coach if you are part of a team. You can create your own
|
||||||
|
planned workouts by purchasing the "Coach" or "Self-Coach" plans.
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
121
rowers/templates/plannedsessionsmanage.html
Normal file
121
rowers/templates/plannedsessionsmanage.html
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% load staticfiles %}
|
||||||
|
{% load rowerfilters %}
|
||||||
|
|
||||||
|
{% block title %}Planned Sessions{% endblock %}
|
||||||
|
|
||||||
|
{% block meta %}
|
||||||
|
<script type='text/javascript'
|
||||||
|
src='https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js'>
|
||||||
|
</script>
|
||||||
|
<script type='text/javascript'
|
||||||
|
src='https://ajax.aspnetcdn.com/ajax/jquery.validate/1.14.0/jquery.validate.min.js'>
|
||||||
|
</script>
|
||||||
|
<script>
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="grid_12 alpha">
|
||||||
|
{% include "planningbuttons.html" %}
|
||||||
|
</div>
|
||||||
|
<div class="grid_6 alpha">
|
||||||
|
<h1>Manage Plan Execution for {{ rower.user.first_name }} {{ rower.user.last_name }}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="timeperiod" class="grid_2 dropdown">
|
||||||
|
<button class="grid_2 alpha button gray small dropbtn">Time Period</button>
|
||||||
|
<div class="dropdown-content">
|
||||||
|
<a class="button gray small alpha"
|
||||||
|
href="/rowers/sessions/manage/today/rower/{{ rower.id }}">
|
||||||
|
Today
|
||||||
|
</a>
|
||||||
|
<a class="button gray small alpha"
|
||||||
|
href="/rowers/sessions/manage/thisweek/rower/{{ rower.id }}">
|
||||||
|
This Week
|
||||||
|
</a>
|
||||||
|
<a class="button gray small alpha"
|
||||||
|
href="/rowers/sessions/manage/thismonth/rower/{{ rower.id }}">
|
||||||
|
This Month
|
||||||
|
</a>
|
||||||
|
<a class="button gray small alpha"
|
||||||
|
href="/rowers/sessions/manage/lastweek/rower/{{ rower.id }}">
|
||||||
|
Last Week
|
||||||
|
</a>
|
||||||
|
<a class="button gray small alpha"
|
||||||
|
href="/rowers/sessions/manage/lastmonth/rower/{{ rower.id }}">
|
||||||
|
Last Month
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid_12 alpha">
|
||||||
|
<p>Select one session on the left, and one or more workouts on the right
|
||||||
|
to match the workouts to the session. For tests and training sessions,
|
||||||
|
the selected workouts must be done on the same date. For all sessions,
|
||||||
|
the workout dates must be between the start and end date for the
|
||||||
|
session.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
If you select a workout that has already been matched to another session,
|
||||||
|
it will change to match this session.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
We will make this form smarter in the near future.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<form id="session_form" action="/rowers/sessions/manage/{{ timeperiod }}/rower/{{ rower.id }}"
|
||||||
|
method="post">
|
||||||
|
<div class="grid_12 alpha">
|
||||||
|
<div class="grid_6 alpha">
|
||||||
|
{{ ps_form.as_table}}
|
||||||
|
</div>
|
||||||
|
<div class="grid_6 omega">
|
||||||
|
{{ w_form.as_table}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid_2 prefix_2 suffix_8">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input class="button green" type="submit" value="Submit">
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
|
||||||
|
<script>
|
||||||
|
$(document).ready(function(){
|
||||||
|
$('#id_plannedsession').on('click', function(evt) {
|
||||||
|
var selectedsession = $("input:radio[name='plannedsession']:checked").val();
|
||||||
|
|
||||||
|
var url = window.location.pathname;
|
||||||
|
if (url.indexOf("/session/") >= 0) {
|
||||||
|
url = url.replace(/\/session\/\d+/g, "/session/"+selectedsession);
|
||||||
|
} else {
|
||||||
|
url += "/session/"+selectedsession
|
||||||
|
}
|
||||||
|
// window.location.replace(url);
|
||||||
|
$.getJSON(url, function(json) {
|
||||||
|
var workouts = json['workouts'];
|
||||||
|
for (i=0; i < workouts.length; i++) {
|
||||||
|
var wid = workouts[i][0];
|
||||||
|
var wcheck = workouts[i][2];
|
||||||
|
if (wcheck) {
|
||||||
|
$(":checkbox").filter(function() {
|
||||||
|
return this.value == wid;
|
||||||
|
}).prop("checked",true);
|
||||||
|
} else {
|
||||||
|
$(":checkbox").filter(function() {
|
||||||
|
return this.value == wid;
|
||||||
|
}).prop("checked",false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
59
rowers/templates/plannedsessionview.html
Normal file
59
rowers/templates/plannedsessionview.html
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% load staticfiles %}
|
||||||
|
{% load rowerfilters %}
|
||||||
|
|
||||||
|
{% block title %}Planned Session{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="grid_12 alpha">
|
||||||
|
{% include "planningbuttons.html" %}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div id="left" class="grid_6 alpha">
|
||||||
|
<h1>Session {{ psdict.name.1 }}</h1>
|
||||||
|
<table class="listtable shortpadded" width="80%">
|
||||||
|
{% for attr in attrs %}
|
||||||
|
{% for key,value in psdict.items %}
|
||||||
|
{% if key == attr %}
|
||||||
|
<tr>
|
||||||
|
<td><b>{{ value.0 }}</b></td><td>{{ value.1 }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
<h1>Result</h1>
|
||||||
|
<p>Status: {{ status }}</p>
|
||||||
|
<p>Percentage complete: {{ ratio }} </p>
|
||||||
|
</div>
|
||||||
|
<div id="right" class="grid_6 omega">
|
||||||
|
<h1>Workouts attached</h1>
|
||||||
|
<table class="listtable shortpadded" width="80%">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Distance</th>
|
||||||
|
<th>Duration</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for workout in workouts %}
|
||||||
|
<tr>
|
||||||
|
<td> {{ workout.date|date:"Y-m-d" }} </td>
|
||||||
|
<td>
|
||||||
|
<a href={% url manager.defaultlandingpage id=workout.id %}>
|
||||||
|
{{ workout.name }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td> {{ workout.distance }}m</td>
|
||||||
|
<td> {{ workout.duration |durationprint:"%H:%M:%S.%f" }} </td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
48
rowers/templates/plannedsssionsmanage.html
Normal file
48
rowers/templates/plannedsssionsmanage.html
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% load staticfiles %}
|
||||||
|
{% load rowerfilters %}
|
||||||
|
|
||||||
|
{% block title %}Planned Sessions{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="grid_12 alpha">
|
||||||
|
{% include "planningbuttons.html" %}
|
||||||
|
</div>
|
||||||
|
<div class="grid_6 alpha">
|
||||||
|
<h1>Plan for {{ rower.user.first_name }} {{ rower.user.last_name }}</h1>
|
||||||
|
</div>
|
||||||
|
<div id="timeperiod" class="grid_2 dropdown">
|
||||||
|
<button class="grid_2 alpha button gray small dropbtn">Time Period</button>
|
||||||
|
<div class="dropdown-content">
|
||||||
|
<a class="button gray small alpha"
|
||||||
|
href="/rowers/sessions/today/rower/{{ rower.id }}">
|
||||||
|
Today
|
||||||
|
</a>
|
||||||
|
<a class="button gray small alpha"
|
||||||
|
href="/rowers/sessions/thisweek/rower/{{ rower.id }}">
|
||||||
|
This Week
|
||||||
|
</a>
|
||||||
|
<a class="button gray small alpha"
|
||||||
|
href="/rowers/sessions/thismonth/rower/{{ rower.id }}">
|
||||||
|
This Month
|
||||||
|
</a>
|
||||||
|
<a class="button gray small alpha"
|
||||||
|
href="/rowers/sessions/lastweek/rower/{{ rower.id }}">
|
||||||
|
Last Week
|
||||||
|
</a>
|
||||||
|
<a class="button gray small alpha"
|
||||||
|
href="/rowers/sessions/lastmonth/rower/{{ rower.id }}">
|
||||||
|
Last Month
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid_12 alpha">
|
||||||
|
<p>
|
||||||
|
Click on session name to view
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
28
rowers/templates/planningbuttons.html
Normal file
28
rowers/templates/planningbuttons.html
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{% load rowerfilters %}
|
||||||
|
<div class="grid_2 alpha">
|
||||||
|
<p>
|
||||||
|
{% if timeperiod and rower %}
|
||||||
|
<a class="button gray small" href="/rowers/sessions/{{ timeperiod }}/rower/{{ rower.id }}">Plan Overview</a>
|
||||||
|
{% elif timeperiod %}
|
||||||
|
<a class="button gray small" href="/rowers/sessions/{{ timeperiod }}">Plan Overview</a>
|
||||||
|
{% else %}
|
||||||
|
<a class="button gray small" href="/rowers/sessions">Plan Overview</a>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="grid_2">
|
||||||
|
<p>
|
||||||
|
{% if timeperiod and rower %}
|
||||||
|
<a class="button gray small" href="/rowers/sessions/manage/{{ timeperiod }}/rower/{{ rower.id }}">Manage Sessions</a>
|
||||||
|
{% elif timeperiod %}
|
||||||
|
<a class="button gray small" href="/rowers/sessions/manage/{{ timeperiod }}">Manage Sessions</a>
|
||||||
|
{% else %}
|
||||||
|
<a class="button gray small" href="/rowers/sessions/manage">Manage Sessions</a>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="grid_2 suffix_6 omega">
|
||||||
|
<p>
|
||||||
|
<a class="button gray small" href="/rowers/sessions/create">Add Session</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
@@ -2,9 +2,9 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}About us{% endblock title %}
|
{% block title %}About us{% endblock title %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h2>Pro Membership</h2>
|
|
||||||
|
|
||||||
<div class="grid_6 alpha">
|
<div class="grid_6 alpha">
|
||||||
|
<h2>Pro Membership</h2>
|
||||||
|
|
||||||
<p>Donations are welcome to keep this web site going. To help cover the hosting
|
<p>Donations are welcome to keep this web site going. To help cover the hosting
|
||||||
costs, I have created a <q>Pro</q> membership option (for only 15 EURO per year). Once I process your
|
costs, I have created a <q>Pro</q> membership option (for only 15 EURO per year). Once I process your
|
||||||
@@ -27,7 +27,7 @@ You will be taken to the secure PayPal payment site.
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid_6 omega">
|
<div class="grid_6 omega">
|
||||||
<h3>Recurring Payment</h2>
|
<h2>Recurring Payment</h2>
|
||||||
<p>You need a Paypal account for this</p>
|
<p>You need a Paypal account for this</p>
|
||||||
<form action="https://www.paypal.com/cgi-bin/webscr" method="post" target="_top">
|
<form action="https://www.paypal.com/cgi-bin/webscr" method="post" target="_top">
|
||||||
<input type="hidden" name="cmd" value="_s-xclick">
|
<input type="hidden" name="cmd" value="_s-xclick">
|
||||||
@@ -40,7 +40,7 @@ You will be taken to the secure PayPal payment site.
|
|||||||
<img alt="" border="0" src="https://www.paypalobjects.com/en_US/i/scr/pixel.gif" width="1" height="1">
|
<img alt="" border="0" src="https://www.paypalobjects.com/en_US/i/scr/pixel.gif" width="1" height="1">
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<h3>One Year Subscription</h3>
|
<h2>One Year Subscription</h2>
|
||||||
<p>Only a credit card needed. Will not automatically renew</p>
|
<p>Only a credit card needed. Will not automatically renew</p>
|
||||||
<form action="https://www.paypal.com/cgi-bin/webscr" method="post" target="_top">
|
<form action="https://www.paypal.com/cgi-bin/webscr" method="post" target="_top">
|
||||||
<input type="hidden" name="cmd" value="_s-xclick">
|
<input type="hidden" name="cmd" value="_s-xclick">
|
||||||
@@ -49,7 +49,7 @@ You will be taken to the secure PayPal payment site.
|
|||||||
<img alt="" border="0" src="https://www.paypalobjects.com/en_US/i/scr/pixel.gif" width="1" height="1">
|
<img alt="" border="0" src="https://www.paypalobjects.com/en_US/i/scr/pixel.gif" width="1" height="1">
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<h3>Payment Processing</h3>
|
<h2>Payment Processing</h2>
|
||||||
<p>After you do the payment, we will manually change your membership to
|
<p>After you do the payment, we will manually change your membership to
|
||||||
"Pro". Depending on our availability, this may take some time
|
"Pro". Depending on our availability, this may take some time
|
||||||
(typically one working day). Don't hesitate to contact us
|
(typically one working day). Don't hesitate to contact us
|
||||||
|
|||||||
@@ -151,3 +151,4 @@ def team_members(user):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|||||||
@@ -137,6 +137,10 @@ urlpatterns = [
|
|||||||
url(r'^list-workouts/ranking$',views.workouts_view,{'rankingonly':True}),
|
url(r'^list-workouts/ranking$',views.workouts_view,{'rankingonly':True}),
|
||||||
url(r'^list-workouts/team/(?P<teamid>\d+)/(?P<startdatestring>\w+.*)/(?P<enddatestring>\w+.*)$',views.workouts_view),
|
url(r'^list-workouts/team/(?P<teamid>\d+)/(?P<startdatestring>\w+.*)/(?P<enddatestring>\w+.*)$',views.workouts_view),
|
||||||
url(r'^list-workouts/team/(?P<teamid>\d+)/$',views.workouts_view),
|
url(r'^list-workouts/team/(?P<teamid>\d+)/$',views.workouts_view),
|
||||||
|
url(r'^(?P<rowerid>\d+)/list-workouts/$',views.workouts_view),
|
||||||
|
url(r'^(?P<rowerid>\d+)/list-workouts/(?P<startdatestring>\w+.*)/(?P<enddatestring>\w+.*)$',views.workouts_view),
|
||||||
|
url(r'^u/(?P<userid>\d+)/list-workouts/$',views.workouts_view),
|
||||||
|
url(r'^u/(?P<userid>\d+)/list-workouts/(?P<startdatestring>\w+.*)/(?P<enddatestring>\w+.*)$',views.workouts_view),
|
||||||
url(r'^list-workouts/(?P<startdatestring>\w+.*)/(?P<enddatestring>\w+.*)$',views.workouts_view),
|
url(r'^list-workouts/(?P<startdatestring>\w+.*)/(?P<enddatestring>\w+.*)$',views.workouts_view),
|
||||||
url(r'^list-workouts/$',views.workouts_view),
|
url(r'^list-workouts/$',views.workouts_view),
|
||||||
url(r'^addmanual/$',views.addmanual_view),
|
url(r'^addmanual/$',views.addmanual_view),
|
||||||
@@ -378,6 +382,7 @@ urlpatterns = [
|
|||||||
url(r'^videos', TemplateView.as_view(template_name='videos.html'),name='videos'),
|
url(r'^videos', TemplateView.as_view(template_name='videos.html'),name='videos'),
|
||||||
url(r'^analysis', TemplateView.as_view(template_name='analysis.html'),name='analysis'),
|
url(r'^analysis', TemplateView.as_view(template_name='analysis.html'),name='analysis'),
|
||||||
url(r'^promembership', TemplateView.as_view(template_name='promembership.html'),name='promembership'),
|
url(r'^promembership', TemplateView.as_view(template_name='promembership.html'),name='promembership'),
|
||||||
|
url(r'^planmembership', TemplateView.as_view(template_name='planmembership.html'),name='planmembership'),
|
||||||
url(r'^paypaltest', TemplateView.as_view(template_name='paypaltest.html'),name='paypaltest'),
|
url(r'^paypaltest', TemplateView.as_view(template_name='paypaltest.html'),name='paypaltest'),
|
||||||
url(r'^legal', TemplateView.as_view(template_name='legal.html'),name='legal'),
|
url(r'^legal', TemplateView.as_view(template_name='legal.html'),name='legal'),
|
||||||
url(r'^register$',views.rower_register_view),
|
url(r'^register$',views.rower_register_view),
|
||||||
@@ -392,6 +397,48 @@ urlpatterns = [
|
|||||||
url(r'^workout/compare/(?P<id1>\d+)/(?P<id2>\d+)/(?P<xparam>\w+.*)/(?P<yparam>[\w\ ]+.*)/$',views.workout_comparison_view2),
|
url(r'^workout/compare/(?P<id1>\d+)/(?P<id2>\d+)/(?P<xparam>\w+.*)/(?P<yparam>[\w\ ]+.*)/$',views.workout_comparison_view2),
|
||||||
url(r'^test\_callback',views.rower_process_testcallback),
|
url(r'^test\_callback',views.rower_process_testcallback),
|
||||||
url(r'^workout/(?P<id>\d+)/test\_strokedata$',views.strokedataform),
|
url(r'^workout/(?P<id>\d+)/test\_strokedata$',views.strokedataform),
|
||||||
|
url(r'^sessions/create$',views.plannedsession_create_view),
|
||||||
|
url(r'^sessions/create/rower/(?P<rowerid>\d+)$',
|
||||||
|
views.plannedsession_create_view),
|
||||||
|
url(
|
||||||
|
r'^sessions/create/(?P<timeperiod>[\w\ ]+.*)/rower/(?P<rowerid>\d+)$',
|
||||||
|
views.plannedsession_create_view),
|
||||||
|
url(r'^sessions/create/(?P<timeperiod>[\w\ ]+.*)$',
|
||||||
|
views.plannedsession_create_view),
|
||||||
|
|
||||||
|
url(r'^sessions/(?P<id>\d+)/edit$',views.plannedsession_edit_view),
|
||||||
|
url(r'^sessions/(?P<id>\d+)/edit/(?P<timeperiod>[\w\ ]+.*)/rower/(?P<rowerid>\d+)$',views.plannedsession_edit_view),
|
||||||
|
url(r'^sessions/(?P<id>\d+)/edit/(?P<timeperiod>[\w\ ]+.*)$',views.plannedsession_edit_view),
|
||||||
|
|
||||||
|
url(r'^sessions/(?P<id>\d+)/clone$',views.plannedsession_clone_view),
|
||||||
|
url(r'^sessions/(?P<id>\d+)/clone/(?P<timeperiod>[\w\ ]+.*)/rower/(?P<rowerid>\d+)$',views.plannedsession_clone_view),
|
||||||
|
url(r'^sessions/(?P<id>\d+)/clone/(?P<timeperiod>[\w\ ]+.*)$',views.plannedsession_clone_view),
|
||||||
|
|
||||||
|
url(r'^sessions/(?P<id>\d+)$',views.plannedsession_view),
|
||||||
|
url(r'^sessions/(?P<id>\d+)/deleteconfirm$',views.plannedsession_deleteconfirm_view),
|
||||||
|
url(r'^sessions/(?P<id>\d+)/delete$',views.plannedsession_delete_view),
|
||||||
|
url(r'^sessions/manage/session/(?P<initialsession>\d+)$',
|
||||||
|
views.plannedsessions_manage_view),
|
||||||
|
url(r'^sessions/manage/rower/(?P<rowerid>\d+)/session/(?P<initialsession>\d+)$',
|
||||||
|
views.plannedsessions_manage_view),
|
||||||
|
url(r'^sessions/manage/(?P<timeperiod>[\w\ ]+.*)/rower/(?P<rowerid>\d+)/session/(?P<initialsession>\d+)$',
|
||||||
|
views.plannedsessions_manage_view),
|
||||||
|
url(r'^sessions/manage/(?P<timeperiod>[\w\ ]+.*)/session/(?P<initialsession>\d+)$',
|
||||||
|
views.plannedsessions_manage_view),
|
||||||
|
|
||||||
|
|
||||||
|
url(r'^sessions/manage/?$',
|
||||||
|
views.plannedsessions_manage_view),
|
||||||
|
url(r'^sessions/manage/rower/(?P<rowerid>\d+)$',
|
||||||
|
views.plannedsessions_manage_view),
|
||||||
|
url(r'^sessions/manage/(?P<timeperiod>[\w\ ]+.*)/rower/(?P<rowerid>\d+)$',
|
||||||
|
views.plannedsessions_manage_view),
|
||||||
|
url(r'^sessions/manage/(?P<timeperiod>[\w\ ]+.*)$',
|
||||||
|
views.plannedsessions_manage_view),
|
||||||
|
url(r'^sessions/?$',views.plannedsessions_view),
|
||||||
|
url(r'^sessions/rower/(?P<rowerid>\d+)$',views.plannedsessions_view),
|
||||||
|
url(r'^sessions/(?P<timeperiod>[\w\ ]+.*)/rower/(?P<rowerid>\d+)$',views.plannedsessions_view),
|
||||||
|
url(r'^sessions/(?P<timeperiod>[\w\ ]+.*)$',views.plannedsessions_view),
|
||||||
]
|
]
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
|
|||||||
@@ -291,3 +291,16 @@ from datetime import date
|
|||||||
def calculate_age(born):
|
def calculate_age(born):
|
||||||
today = date.today()
|
today = date.today()
|
||||||
return today.year - born.year - ((today.month, today.day) < (born.month, born.day))
|
return today.year - born.year - ((today.month, today.day) < (born.month, born.day))
|
||||||
|
|
||||||
|
def my_dict_from_instance(instance,model):
|
||||||
|
thedict = {}
|
||||||
|
|
||||||
|
for attr, value in instance.__dict__.iteritems():
|
||||||
|
try:
|
||||||
|
verbose_name = model._meta.get_field(attr).verbose_name
|
||||||
|
except:
|
||||||
|
verbose_name = attr
|
||||||
|
|
||||||
|
thedict[attr] = (verbose_name,value)
|
||||||
|
|
||||||
|
return thedict
|
||||||
|
|||||||
482
rowers/views.py
482
rowers/views.py
@@ -30,7 +30,7 @@ from rowers.forms import (
|
|||||||
LoginForm,DocumentsForm,UploadOptionsForm,ImageForm,
|
LoginForm,DocumentsForm,UploadOptionsForm,ImageForm,
|
||||||
TeamUploadOptionsForm,WorkFlowLeftPanelForm,WorkFlowMiddlePanelForm,
|
TeamUploadOptionsForm,WorkFlowLeftPanelForm,WorkFlowMiddlePanelForm,
|
||||||
WorkFlowLeftPanelElement,WorkFlowMiddlePanelElement,
|
WorkFlowLeftPanelElement,WorkFlowMiddlePanelElement,
|
||||||
LandingPageForm,
|
LandingPageForm,PlannedSessionSelectForm,WorkoutSessionSelectForm
|
||||||
)
|
)
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
@@ -50,13 +50,16 @@ from rowers.forms import (
|
|||||||
FusionMetricChoiceForm,BoxPlotChoiceForm,MultiFlexChoiceForm,
|
FusionMetricChoiceForm,BoxPlotChoiceForm,MultiFlexChoiceForm,
|
||||||
TrendFlexModalForm,WorkoutSplitForm,WorkoutJoinParamForm,
|
TrendFlexModalForm,WorkoutSplitForm,WorkoutJoinParamForm,
|
||||||
)
|
)
|
||||||
from rowers.models import Workout, User, Rower, WorkoutForm,FavoriteChart
|
from rowers.models import (
|
||||||
|
Workout, User, Rower, WorkoutForm,FavoriteChart,
|
||||||
|
PlannedSession
|
||||||
|
)
|
||||||
from rowers.models import (
|
from rowers.models import (
|
||||||
RowerPowerForm,RowerForm,GraphImage,AdvancedWorkoutForm,
|
RowerPowerForm,RowerForm,GraphImage,AdvancedWorkoutForm,
|
||||||
RowerPowerZonesForm,AccountRowerForm,UserForm,StrokeData,
|
RowerPowerZonesForm,AccountRowerForm,UserForm,StrokeData,
|
||||||
Team,TeamForm,TeamInviteForm,TeamInvite,TeamRequest,
|
Team,TeamForm,TeamInviteForm,TeamInvite,TeamRequest,
|
||||||
WorkoutComment,WorkoutCommentForm,RowerExportForm,
|
WorkoutComment,WorkoutCommentForm,RowerExportForm,
|
||||||
CalcAgePerformance,PowerTimeFitnessMetric,
|
CalcAgePerformance,PowerTimeFitnessMetric,PlannedSessionForm
|
||||||
)
|
)
|
||||||
from rowers.models import FavoriteForm,BaseFavoriteFormSet,SiteAnnouncement
|
from rowers.models import FavoriteForm,BaseFavoriteFormSet,SiteAnnouncement
|
||||||
from rowers.metrics import rowingmetrics,defaultfavoritecharts
|
from rowers.metrics import rowingmetrics,defaultfavoritecharts
|
||||||
@@ -104,6 +107,7 @@ import json
|
|||||||
from rest_framework.renderers import JSONRenderer
|
from rest_framework.renderers import JSONRenderer
|
||||||
from rest_framework.parsers import JSONParser
|
from rest_framework.parsers import JSONParser
|
||||||
from rowers.rows import handle_uploaded_file,handle_uploaded_image
|
from rowers.rows import handle_uploaded_file,handle_uploaded_image
|
||||||
|
from rowers.plannedsessions import *
|
||||||
from rowers.tasks import handle_makeplot,handle_otwsetpower,handle_sendemailtcx,handle_sendemailcsv
|
from rowers.tasks import handle_makeplot,handle_otwsetpower,handle_sendemailtcx,handle_sendemailcsv
|
||||||
from rowers.tasks import (
|
from rowers.tasks import (
|
||||||
handle_sendemail_unrecognized,handle_sendemailnewcomment,
|
handle_sendemail_unrecognized,handle_sendemailnewcomment,
|
||||||
@@ -602,6 +606,7 @@ def rowhascoordinates(row):
|
|||||||
if rowdata != 0:
|
if rowdata != 0:
|
||||||
try:
|
try:
|
||||||
latitude = rowdata.df[' latitude']
|
latitude = rowdata.df[' latitude']
|
||||||
|
|
||||||
if not latitude.std():
|
if not latitude.std():
|
||||||
hascoordinates = 0
|
hascoordinates = 0
|
||||||
except KeyError,AttributeError:
|
except KeyError,AttributeError:
|
||||||
@@ -737,12 +742,12 @@ from utils import (
|
|||||||
geo_distance,serialize_list,deserialize_list,uniqify,
|
geo_distance,serialize_list,deserialize_list,uniqify,
|
||||||
str2bool,range_to_color_hex,absolute,myqueue,get_call,
|
str2bool,range_to_color_hex,absolute,myqueue,get_call,
|
||||||
calculate_age,rankingdistances,rankingdurations,
|
calculate_age,rankingdistances,rankingdurations,
|
||||||
is_ranking_piece
|
is_ranking_piece,my_dict_from_instance
|
||||||
)
|
)
|
||||||
|
|
||||||
import datautils
|
import datautils
|
||||||
|
|
||||||
from rowers.models import checkworkoutuser
|
from rowers.models import checkworkoutuser,checkaccessuser
|
||||||
|
|
||||||
# Check if a user is a Coach member
|
# Check if a user is a Coach member
|
||||||
def iscoachmember(user):
|
def iscoachmember(user):
|
||||||
@@ -759,7 +764,20 @@ def iscoachmember(user):
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
# Check if a user can create planned sessions
|
||||||
|
def hasplannedsessions(user):
|
||||||
|
if not user.is_anonymous():
|
||||||
|
try:
|
||||||
|
r = Rower.objects.get(user=user)
|
||||||
|
except Rower.DoesNotExist:
|
||||||
|
r = Rower(user=user)
|
||||||
|
r.save()
|
||||||
|
|
||||||
|
result = user.is_authenticated() and (r.rowerplan=='coach' or r.rowerplan=='plan')
|
||||||
|
else:
|
||||||
|
result = False
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
def getrower(user):
|
def getrower(user):
|
||||||
try:
|
try:
|
||||||
@@ -781,7 +799,7 @@ def ispromember(user):
|
|||||||
r = Rower(user=user)
|
r = Rower(user=user)
|
||||||
r.save()
|
r.save()
|
||||||
|
|
||||||
result = user.is_authenticated() and (r.rowerplan=='pro' or r.rowerplan=='coach')
|
result = user.is_authenticated() and (r.rowerplan=='pro' or r.rowerplan=='coach' or r.rowerplan=='plan')
|
||||||
else:
|
else:
|
||||||
result = False
|
result = False
|
||||||
return result
|
return result
|
||||||
@@ -923,7 +941,7 @@ def add_workout_from_strokedata(user,importid,data,strokedata,
|
|||||||
workouttype = 'rower'
|
workouttype = 'rower'
|
||||||
|
|
||||||
if workouttype not in [x[0] for x in Workout.workouttypes]:
|
if workouttype not in [x[0] for x in Workout.workouttypes]:
|
||||||
workouttype = 'water'
|
workouttype = 'other'
|
||||||
try:
|
try:
|
||||||
comments = data['comments']
|
comments = data['comments']
|
||||||
except:
|
except:
|
||||||
@@ -1070,7 +1088,7 @@ def add_workout_from_runkeeperdata(user,importid,data):
|
|||||||
# To Do - add utcoffset to time
|
# To Do - add utcoffset to time
|
||||||
workouttype = data['type']
|
workouttype = data['type']
|
||||||
if workouttype not in [x[0] for x in Workout.workouttypes]:
|
if workouttype not in [x[0] for x in Workout.workouttypes]:
|
||||||
workouttype = 'water'
|
workouttype = 'other'
|
||||||
try:
|
try:
|
||||||
comments = data['notes']
|
comments = data['notes']
|
||||||
except:
|
except:
|
||||||
@@ -1243,7 +1261,7 @@ def add_workout_from_runkeeperdata(user,importid,data):
|
|||||||
def add_workout_from_stdata(user,importid,data):
|
def add_workout_from_stdata(user,importid,data):
|
||||||
workouttype = data['type']
|
workouttype = data['type']
|
||||||
if workouttype not in [x[0] for x in Workout.workouttypes]:
|
if workouttype not in [x[0] for x in Workout.workouttypes]:
|
||||||
workouttype = 'water'
|
workouttype = 'other'
|
||||||
try:
|
try:
|
||||||
comments = data['comments']
|
comments = data['comments']
|
||||||
except:
|
except:
|
||||||
@@ -1273,11 +1291,16 @@ def add_workout_from_stdata(user,importid,data):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
res = splitstdata(data['distance'])
|
res = splitstdata(data['distance'])
|
||||||
except KeyError:
|
|
||||||
return (0,"No distance data in the workout")
|
|
||||||
|
|
||||||
distance = res[1]
|
distance = res[1]
|
||||||
times_distance = res[0]
|
times_distance = res[0]
|
||||||
|
except KeyError:
|
||||||
|
try:
|
||||||
|
res = splitstdata(data['heartrate'])
|
||||||
|
times_distance = res[0]
|
||||||
|
distance = 0*times_distance
|
||||||
|
except KeyError:
|
||||||
|
return (0,"No distance or heart rate data in the workout")
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
l = data['location']
|
l = data['location']
|
||||||
@@ -6032,18 +6055,31 @@ def workouts_view(request,message='',successmessage='',
|
|||||||
startdatestring="",enddatestring="",
|
startdatestring="",enddatestring="",
|
||||||
startdate=timezone.now()-datetime.timedelta(days=365),
|
startdate=timezone.now()-datetime.timedelta(days=365),
|
||||||
enddate=timezone.now()+datetime.timedelta(days=1),
|
enddate=timezone.now()+datetime.timedelta(days=1),
|
||||||
teamid=0,rankingonly=False):
|
teamid=0,rankingonly=False,rowerid=0,userid=0):
|
||||||
request.session['referer'] = absolute(request)['PATH']
|
request.session['referer'] = absolute(request)['PATH']
|
||||||
try:
|
try:
|
||||||
|
if rowerid != 0:
|
||||||
|
r = Rower.objects.get(id=rowerid)
|
||||||
|
elif userid != 0:
|
||||||
|
u = User.objects.get(id=userid)
|
||||||
|
r = getrower(u)
|
||||||
|
else:
|
||||||
r = getrower(request.user)
|
r = getrower(request.user)
|
||||||
|
|
||||||
except Rower.DoesNotExist:
|
except Rower.DoesNotExist:
|
||||||
raise Http404("Rower doesn't exist")
|
raise Http404("Rower doesn't exist")
|
||||||
|
|
||||||
|
# check if access is allowed
|
||||||
|
if not checkaccessuser(request.user,r):
|
||||||
|
raise Http404("You are not allowed access to these data")
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
dateform = DateRangeForm(request.POST)
|
dateform = DateRangeForm(request.POST)
|
||||||
if dateform.is_valid():
|
if dateform.is_valid():
|
||||||
startdate = dateform.cleaned_data['startdate']
|
startdate = dateform.cleaned_data['startdate']
|
||||||
enddate = dateform.cleaned_data['enddate']
|
enddate = dateform.cleaned_data['enddate']
|
||||||
|
startdatestring = None
|
||||||
|
enddatestring = None
|
||||||
else:
|
else:
|
||||||
dateform = DateRangeForm(initial={
|
dateform = DateRangeForm(initial={
|
||||||
'startdate':startdate,
|
'startdate':startdate,
|
||||||
@@ -7490,7 +7526,6 @@ def workout_stats_view(request,id=0,message="",successmessage=""):
|
|||||||
if w.workouttype in ('water','coastal'):
|
if w.workouttype in ('water','coastal'):
|
||||||
ftp = ftp*(100.-r.otwslack)/100.
|
ftp = ftp*(100.-r.otwslack)/100.
|
||||||
|
|
||||||
intensityfactor = datadf['power'].mean()/float(ftp)
|
|
||||||
intensityfactor = normp/float(ftp)
|
intensityfactor = normp/float(ftp)
|
||||||
tss = 100.*((duration*normp*intensityfactor)/(3600.*ftp))
|
tss = 100.*((duration*normp*intensityfactor)/(3600.*ftp))
|
||||||
|
|
||||||
@@ -11674,3 +11709,422 @@ def agegrouprecordview(request,sex='male',weightcategory='hwt',
|
|||||||
'the_div':div,
|
'the_div':div,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# Individual user creates training for himself
|
||||||
|
@user_passes_test(hasplannedsessions,login_url="/rowers/planmembership/",
|
||||||
|
redirect_field_name=None)
|
||||||
|
def plannedsession_create_view(request,timeperiod='today',rowerid=0):
|
||||||
|
|
||||||
|
if rowerid==0:
|
||||||
|
r = getrower(request.user)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
r = Rower.objects.get(id=rowerid)
|
||||||
|
except Rower.DoesNotExist:
|
||||||
|
raise Http404("This rower doesn't exist")
|
||||||
|
if not checkaccessuser(request.user,r):
|
||||||
|
raise Http404("You don't have access to this plan")
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
sessioncreateform = PlannedSessionForm(request.POST)
|
||||||
|
if sessioncreateform.is_valid():
|
||||||
|
cd = sessioncreateform.cleaned_data
|
||||||
|
startdate = cd['startdate']
|
||||||
|
enddate = cd['enddate']
|
||||||
|
sessiontype = cd['sessiontype']
|
||||||
|
sessionmode = cd['sessionmode']
|
||||||
|
criterium = cd['criterium']
|
||||||
|
sessionvalue = cd['sessionvalue']
|
||||||
|
sessionunit = cd['sessionunit']
|
||||||
|
comment = cd['comment']
|
||||||
|
name = cd['name']
|
||||||
|
|
||||||
|
if sessionunit == 'min':
|
||||||
|
sessionmode = 'time'
|
||||||
|
elif sessionunit in ['km','m']:
|
||||||
|
sessionmode = 'distance'
|
||||||
|
|
||||||
|
ps = PlannedSession(
|
||||||
|
name=name,
|
||||||
|
startdate=startdate,
|
||||||
|
enddate=enddate,
|
||||||
|
sessiontype=sessiontype,
|
||||||
|
sessionmode=sessionmode,
|
||||||
|
sessionvalue=sessionvalue,
|
||||||
|
sessionunit=sessionunit,
|
||||||
|
comment=comment,
|
||||||
|
criterium=criterium,
|
||||||
|
manager=request.user)
|
||||||
|
|
||||||
|
ps.save()
|
||||||
|
|
||||||
|
add_rower_session(r,ps)
|
||||||
|
|
||||||
|
url = reverse(plannedsession_create_view)
|
||||||
|
return HttpResponseRedirect(url)
|
||||||
|
else:
|
||||||
|
sessioncreateform = PlannedSessionForm()
|
||||||
|
|
||||||
|
startdate,enddate = get_dates_timeperiod(timeperiod)
|
||||||
|
sps = get_sessions(r,startdate=startdate,enddate=enddate)
|
||||||
|
|
||||||
|
return render(request,'plannedsessioncreate.html',
|
||||||
|
{
|
||||||
|
'teams':get_my_teams(request.user),
|
||||||
|
'form':sessioncreateform,
|
||||||
|
'plannedsessions':sps,
|
||||||
|
'rower':r,
|
||||||
|
})
|
||||||
|
|
||||||
|
@login_required()
|
||||||
|
def plannedsessions_view(request,timeperiod='today',rowerid=0):
|
||||||
|
|
||||||
|
if rowerid==0:
|
||||||
|
r = getrower(request.user)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
r = Rower.objects.get(id=rowerid)
|
||||||
|
except Rower.DoesNotExist:
|
||||||
|
raise Http404("This rower doesn't exist")
|
||||||
|
if not checkaccessuser(request.user,r):
|
||||||
|
raise Http404("You don't have access to this plan")
|
||||||
|
|
||||||
|
startdate,enddate = get_dates_timeperiod(timeperiod)
|
||||||
|
|
||||||
|
sps = get_sessions(r,startdate=startdate,enddate=enddate)
|
||||||
|
|
||||||
|
completeness = {}
|
||||||
|
|
||||||
|
for ps in sps:
|
||||||
|
ratio,status = is_session_complete(r,ps)
|
||||||
|
completeness[ps.id] = status
|
||||||
|
|
||||||
|
return render(request,'plannedsessions.html',
|
||||||
|
{
|
||||||
|
'teams':get_my_teams(request.user),
|
||||||
|
'plannedsessions':sps,
|
||||||
|
'rower':r,
|
||||||
|
'timeperiod':timeperiod,
|
||||||
|
'completeness':completeness,
|
||||||
|
})
|
||||||
|
|
||||||
|
@login_required()
|
||||||
|
def plannedsessions_manage_view(request,timeperiod='today',rowerid=0,
|
||||||
|
initialsession=0):
|
||||||
|
|
||||||
|
is_ajax = False
|
||||||
|
if request.is_ajax():
|
||||||
|
is_ajax = True
|
||||||
|
|
||||||
|
if rowerid==0:
|
||||||
|
r = getrower(request.user)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
r = Rower.objects.get(id=rowerid)
|
||||||
|
except Rower.DoesNotExist:
|
||||||
|
raise Http404("This rower doesn't exist")
|
||||||
|
if not checkaccessuser(request.user,r):
|
||||||
|
raise Http404("You don't have access to this plan")
|
||||||
|
|
||||||
|
startdate,enddate = get_dates_timeperiod(timeperiod)
|
||||||
|
|
||||||
|
sps = get_sessions(r,startdate=startdate,enddate=enddate)
|
||||||
|
if initialsession==0:
|
||||||
|
initialsession=sps[0].id
|
||||||
|
|
||||||
|
ps0 = PlannedSession.objects.get(id=initialsession)
|
||||||
|
|
||||||
|
ws = Workout.objects.filter(
|
||||||
|
user=r,date__gte=startdate,
|
||||||
|
date__lte=enddate
|
||||||
|
).order_by(
|
||||||
|
"date","id"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
initialworkouts = [w.id for w in Workout.objects.filter(user=r,plannedsession=ps0)]
|
||||||
|
|
||||||
|
plannedsessionstuple = []
|
||||||
|
|
||||||
|
for ps in sps:
|
||||||
|
sessiontpl = (ps.id,ps.__unicode__())
|
||||||
|
plannedsessionstuple.append(sessiontpl)
|
||||||
|
|
||||||
|
plannedsessionstuple = tuple(plannedsessionstuple)
|
||||||
|
|
||||||
|
workoutdata = {}
|
||||||
|
workoutdata['initial'] = []
|
||||||
|
|
||||||
|
choices = []
|
||||||
|
|
||||||
|
for w in ws:
|
||||||
|
wtpl = (w.id, w.__unicode__())
|
||||||
|
choices.append(wtpl)
|
||||||
|
if w.id in initialworkouts:
|
||||||
|
workoutdata['initial'].append(w.id)
|
||||||
|
|
||||||
|
workoutdata['choices'] = tuple(choices)
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
ps_form = PlannedSessionSelectForm(plannedsessionstuple,request.POST)
|
||||||
|
w_form = WorkoutSessionSelectForm(workoutdata,request.POST)
|
||||||
|
|
||||||
|
if ps_form.is_valid():
|
||||||
|
ps = PlannedSession.objects.get(id=ps_form.cleaned_data['plannedsession'])
|
||||||
|
if w_form.is_valid():
|
||||||
|
selectedworkouts = w_form.cleaned_data['workouts']
|
||||||
|
else:
|
||||||
|
selectedworkouts = []
|
||||||
|
|
||||||
|
|
||||||
|
if selectedworkouts:
|
||||||
|
workouts = Workout.objects.filter(user=r,id__in=selectedworkouts)
|
||||||
|
for w in ws:
|
||||||
|
if w.id not in selectedworkouts:
|
||||||
|
remove_workout_plannedsession(w,ps)
|
||||||
|
|
||||||
|
result,comments,errors = add_workouts_plannedsession(workouts,ps)
|
||||||
|
for c in comments:
|
||||||
|
messages.info(request,c)
|
||||||
|
for er in errors:
|
||||||
|
messages.error(request,er)
|
||||||
|
|
||||||
|
|
||||||
|
ps_form = PlannedSessionSelectForm(plannedsessionstuple,
|
||||||
|
initialsession=initialsession)
|
||||||
|
w_form = WorkoutSessionSelectForm(workoutdata=workoutdata)
|
||||||
|
|
||||||
|
|
||||||
|
if is_ajax:
|
||||||
|
ajax_workouts = []
|
||||||
|
for id,name in workoutdata['choices']:
|
||||||
|
if id in initialworkouts:
|
||||||
|
ajax_workouts.append((id,name,True))
|
||||||
|
else:
|
||||||
|
ajax_workouts.append((id,name,False))
|
||||||
|
|
||||||
|
ajax_response = {
|
||||||
|
'workouts':ajax_workouts,
|
||||||
|
'plannedsessionstuple':plannedsessionstuple,
|
||||||
|
}
|
||||||
|
return JSONResponse(ajax_response)
|
||||||
|
|
||||||
|
|
||||||
|
return render(request,'plannedsessionsmanage.html',
|
||||||
|
{
|
||||||
|
'teams':get_my_teams(request.user),
|
||||||
|
'plannedsessions':sps,
|
||||||
|
'workouts':ws,
|
||||||
|
'timeperiod':timeperiod,
|
||||||
|
'rower':r,
|
||||||
|
'ps_form':ps_form,
|
||||||
|
'w_form':w_form,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
# Clone an existing planned session
|
||||||
|
@user_passes_test(hasplannedsessions,login_url="/rowers/planmembership/",
|
||||||
|
redirect_field_name=None)
|
||||||
|
def plannedsession_clone_view(request,id=0,rowerid=0,
|
||||||
|
timeperiod='today'):
|
||||||
|
if rowerid==0:
|
||||||
|
r = getrower(request.user)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
r = Rower.objects.get(id=rowerid)
|
||||||
|
except Rower.DoesNotExist:
|
||||||
|
raise Http404("This rower doesn't exist")
|
||||||
|
if not checkaccessuser(request.user,r):
|
||||||
|
raise Http404("You don't have access to this plan")
|
||||||
|
|
||||||
|
startdate,enddate = get_dates_timeperiod(timeperiod)
|
||||||
|
|
||||||
|
try:
|
||||||
|
ps = PlannedSession.objects.get(id=id)
|
||||||
|
except PlannedSession.DoesNotExist:
|
||||||
|
raise Http404("Planned Session does not exist")
|
||||||
|
|
||||||
|
if ps.manager != request.user:
|
||||||
|
raise Http404("You are not allowed to clone this planned session")
|
||||||
|
|
||||||
|
ps.pk = None
|
||||||
|
|
||||||
|
ps.startdate = timezone.now()
|
||||||
|
ps.enddate = timezone.now()
|
||||||
|
ps.name += ' (copy)'
|
||||||
|
|
||||||
|
ps.save()
|
||||||
|
|
||||||
|
add_rower_session(r,ps)
|
||||||
|
|
||||||
|
url = reverse(plannedsession_edit_view,
|
||||||
|
kwargs = {
|
||||||
|
'id':ps.id,
|
||||||
|
'timeperiod':timeperiod,
|
||||||
|
'rowerid':r.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return HttpResponseRedirect(url)
|
||||||
|
|
||||||
|
|
||||||
|
# Edit an existing planned session
|
||||||
|
@user_passes_test(hasplannedsessions,login_url="/rowers/planmembership/",
|
||||||
|
redirect_field_name=None)
|
||||||
|
def plannedsession_edit_view(request,id=0,timeperiod='today',rowerid=0):
|
||||||
|
|
||||||
|
if rowerid==0:
|
||||||
|
r = getrower(request.user)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
r = Rower.objects.get(id=rowerid)
|
||||||
|
except Rower.DoesNotExist:
|
||||||
|
raise Http404("This rower doesn't exist")
|
||||||
|
if not checkaccessuser(request.user,r):
|
||||||
|
raise Http404("You don't have access to this plan")
|
||||||
|
|
||||||
|
startdate,enddate = get_dates_timeperiod(timeperiod)
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
ps = PlannedSession.objects.get(id=id)
|
||||||
|
except PlannedSession.DoesNotExist:
|
||||||
|
raise Http404("Planned Session does not exist")
|
||||||
|
|
||||||
|
if ps.manager != request.user:
|
||||||
|
raise Http404("You are not allowed to edit this planned session")
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
sessioncreateform = PlannedSessionForm(request.POST,instance=ps)
|
||||||
|
if sessioncreateform.is_valid():
|
||||||
|
cd = sessioncreateform.cleaned_data
|
||||||
|
|
||||||
|
if cd['sessionunit'] == 'min':
|
||||||
|
cd['sessionmode'] = 'time'
|
||||||
|
elif cd['sessionunit'] in ['km','m']:
|
||||||
|
cd['sessionmode'] = 'distance'
|
||||||
|
|
||||||
|
|
||||||
|
res,message = update_plannedsession(ps,cd)
|
||||||
|
|
||||||
|
if res:
|
||||||
|
messages.info(request,message)
|
||||||
|
else:
|
||||||
|
messages.error(request,message)
|
||||||
|
|
||||||
|
url = reverse(plannedsession_edit_view,
|
||||||
|
kwargs={
|
||||||
|
'id':int(ps.id),
|
||||||
|
'timeperiod':timeperiod,
|
||||||
|
'rowerid':r.id,
|
||||||
|
})
|
||||||
|
return HttpResponseRedirect(url)
|
||||||
|
else:
|
||||||
|
sessioncreateform = PlannedSessionForm(instance=ps)
|
||||||
|
|
||||||
|
sps = get_sessions(r,startdate=startdate,enddate=enddate)
|
||||||
|
|
||||||
|
return render(request,'plannedsessionedit.html',
|
||||||
|
{
|
||||||
|
'teams':get_my_teams(request.user),
|
||||||
|
'form':sessioncreateform,
|
||||||
|
'plannedsessions':sps,
|
||||||
|
'thesession':ps,
|
||||||
|
'rower':r,
|
||||||
|
'timeperiod':timeperiod,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@login_required()
|
||||||
|
def plannedsession_view(request,id=0,rowerid=0):
|
||||||
|
|
||||||
|
m = getrower(request.user)
|
||||||
|
|
||||||
|
if not rowerid:
|
||||||
|
r = m
|
||||||
|
else:
|
||||||
|
r = Rower.objects.get(id=rowerid)
|
||||||
|
|
||||||
|
try:
|
||||||
|
ps = PlannedSession.objects.get(id=id)
|
||||||
|
except PlannedSession.DoesNotExist:
|
||||||
|
raise Http404("Planned Session does not exist")
|
||||||
|
|
||||||
|
|
||||||
|
if ps.manager != request.user:
|
||||||
|
raise Http404("You are not allowed to delete this planned session")
|
||||||
|
|
||||||
|
|
||||||
|
psdict = my_dict_from_instance(ps,PlannedSession)
|
||||||
|
|
||||||
|
ws = get_workouts_session(r,ps)
|
||||||
|
|
||||||
|
ratio,status = is_session_complete(r,ps)
|
||||||
|
|
||||||
|
ratio = int(100.*ratio)
|
||||||
|
|
||||||
|
return render(request,'plannedsessionview.html',
|
||||||
|
{
|
||||||
|
'psdict': psdict,
|
||||||
|
'attrs':[
|
||||||
|
'name','startdate','enddate','sessiontype',
|
||||||
|
'sessionvalue','sessionunit'
|
||||||
|
],
|
||||||
|
'workouts': ws,
|
||||||
|
'manager':m,
|
||||||
|
'rower':r,
|
||||||
|
'ratio':ratio,
|
||||||
|
'status':status
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@login_required()
|
||||||
|
def plannedsession_delete_view(request,id=0):
|
||||||
|
r = getrower(request.user)
|
||||||
|
|
||||||
|
try:
|
||||||
|
ps = PlannedSession.objects.get(id=id)
|
||||||
|
except PlannedSession.DoesNotExist:
|
||||||
|
raise Http404("Planned Session does not exist")
|
||||||
|
|
||||||
|
|
||||||
|
if ps.manager != request.user:
|
||||||
|
raise Http404("You are not allowed to delete this planned session")
|
||||||
|
|
||||||
|
ws = Workout.objects.filter(plannedsession=ps)
|
||||||
|
for w in ws:
|
||||||
|
w.plannedsession=None
|
||||||
|
w.save()
|
||||||
|
|
||||||
|
ps.delete()
|
||||||
|
|
||||||
|
url = reverse(plannedsessions_view)
|
||||||
|
|
||||||
|
return HttpResponseRedirect(url)
|
||||||
|
|
||||||
|
@login_required()
|
||||||
|
def plannedsession_deleteconfirm_view(request,id=0):
|
||||||
|
|
||||||
|
r = getrower(request.user)
|
||||||
|
|
||||||
|
try:
|
||||||
|
ps = PlannedSession.objects.get(id=id)
|
||||||
|
except PlannedSession.DoesNotExist:
|
||||||
|
raise Http404("Planned Session does not exist")
|
||||||
|
|
||||||
|
|
||||||
|
if ps.manager != request.user:
|
||||||
|
raise Http404("You are not allowed to delete this planned session")
|
||||||
|
|
||||||
|
|
||||||
|
psdict = my_dict_from_instance(ps,PlannedSession)
|
||||||
|
|
||||||
|
return render(request,'plannedsessiondeleteconfirm.html',
|
||||||
|
{
|
||||||
|
'ps':ps,
|
||||||
|
'psdict': psdict,
|
||||||
|
'attrs':[
|
||||||
|
'name','startdate','enddate','sessiontype',
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|||||||
@@ -291,6 +291,19 @@ th.rotate > div > span {
|
|||||||
border: solid 1px #333;
|
border: solid 1px #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
border-radius: 50%;
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
width: 25px;
|
||||||
|
height: 25px;
|
||||||
|
border: solid 1px #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
font: 1.1em/1.5em sans-serif;
|
font: 1.1em/1.5em sans-serif;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|||||||
@@ -201,6 +201,17 @@
|
|||||||
{% block teams %}
|
{% block teams %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="grid_1 tooltip">
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
<p><a class="button gray small" href="/rowers/sessions/">Plans</a></p>
|
||||||
|
{% else %}
|
||||||
|
<p> </p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="grid_1 tooltip">
|
||||||
|
{% block challenges %}
|
||||||
|
{% endblock %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user