Private
Public Access
1
0

Merge branch 'release/v5.78'

This commit is contained in:
Sander Roosendaal
2018-02-08 15:40:19 +01:00
29 changed files with 1987 additions and 73 deletions

View File

@@ -5,7 +5,7 @@ from django.contrib.auth.models import User
from .models import (
Rower, Workout,GraphImage,FavoriteChart,SiteAnnouncement,
Team,TeamInvite,TeamRequest,
WorkoutComment,C2WorldClassAgePerformance,
WorkoutComment,C2WorldClassAgePerformance,PlannedSession,
)
# Register your models here so you can use them in the Admin module
@@ -43,11 +43,17 @@ class TeamRequestAdmin(admin.ModelAdmin):
class WorkoutCommentAdmin(admin.ModelAdmin):
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.register(User,UserAdmin)
admin.site.register(Workout,WorkoutAdmin)
admin.site.register(GraphImage)
admin.site.register(GraphImage,GraphImageAdmin)
admin.site.register(Team,TeamAdmin)
admin.site.register(FavoriteChart,FavoriteChartAdmin)
admin.site.register(SiteAnnouncement,SiteAnnouncementAdmin)
@@ -56,3 +62,5 @@ admin.site.register(TeamRequest,TeamRequestAdmin)
admin.site.register(WorkoutComment,WorkoutCommentAdmin)
admin.site.register(C2WorldClassAgePerformance,
C2WorldClassAgePerformanceAdmin)
admin.site.register(PlannedSession,PlannedSessionAdmin)

73
rowers/courses.py Normal file
View 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

View File

@@ -36,7 +36,7 @@ from rowingdata import (
summarydata, get_file_type,
)
from rowers.metrics import axes
from rowers.metrics import axes,calc_trimp
from async_messages import messages as a_messages
import os
import zipfile
@@ -2212,3 +2212,40 @@ def dataprep(rowdatadf, id=0, bands=True, barchart=True, otwpower=True,
conn.close()
engine.dispose()
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

View File

@@ -602,3 +602,30 @@ class FusionMetricChoiceForm(ModelForm):
metricchoices = list(sorted(formaxlabels2.items(), key = lambda x:x[1]))
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,
)

View File

@@ -307,6 +307,7 @@ This value should be fairly constant across all stroke rates.""",
},
)
def calc_trimp(df,sex,hrmax,hrmin):
if sex == 'male':
f = 1.92
@@ -321,6 +322,7 @@ def calc_trimp(df,sex,hrmax,hrmin):
return trimp
def getagegrouprecord(age,sex='male',weightcategory='hwt',
distance=2000,duration=None,indf=pd.DataFrame()):

View File

@@ -483,6 +483,7 @@ class Rower(models.Model):
plans = (
('basic','basic'),
('pro','pro'),
('plan','plan'),
('coach','coach')
)
@@ -623,10 +624,284 @@ def checkworkoutuser(user,workout):
except Rower.DoesNotExist:
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 = (
(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
class Workout(models.Model):
@@ -637,6 +912,7 @@ class Workout(models.Model):
user = models.ForeignKey(Rower)
team = models.ManyToManyField(Team,blank=True)
plannedsession = models.ForeignKey(PlannedSession, blank=True,null=True)
name = models.CharField(max_length=150)
date = models.DateField()
workouttype = models.CharField(choices=workouttypes,max_length=50)
@@ -864,9 +1140,6 @@ def auto_delete_image_on_delete(sender,instance, **kwargs):
else:
print "couldn't find the file "+instance.filename
# Date input utility
class DateInput(forms.DateInput):
input_type = 'date'
# Form to update Workout data
class WorkoutForm(ModelForm):
@@ -1327,7 +1600,7 @@ class RowerForm(ModelForm):
# optionally sends a tweet to our twitter account
class SiteAnnouncement(models.Model):
created = models.DateField(default=timezone.now)
announcement = models.TextField(max_length=140)
announcement = models.TextField(max_length=280)
expires = models.DateField(default=timezone.now)
modified = models.DateField(default=timezone.now)
dotweet = models.BooleanField(default=False)
@@ -1371,4 +1644,4 @@ class WorkoutCommentForm(ModelForm):
widgets = {
'comment': forms.Textarea,
}

233
rowers/plannedsessions.py Normal file
View 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'

View File

@@ -90,15 +90,9 @@ def handle_uploaded_image(i):
break
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):
# cases: image don't have getexif
pass
exif = {'orientation':0}
if image.mode not in ("L", "RGB"):
image = image.convert("RGB")
@@ -108,6 +102,12 @@ def handle_uploaded_image(i):
hsize = int((float(image.size[1])*float(wpercent)))
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'

View File

@@ -185,7 +185,8 @@ def get_strava_workout(user,stravaid):
if nr_rows == 0:
return (0,"Error: Time data had zero length")
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:
return (0,"something went wrong with the Strava import")

View File

@@ -40,4 +40,4 @@
</div>
{% endblock %}
{% endblock %}

View File

@@ -50,7 +50,6 @@
<div class="grid_12">
Select start and end date for a date range:
<div class="grid_4 alpha">
{% if team %}
@@ -64,9 +63,25 @@
</table>
{% csrf_token %}
</div>
<div class="grid_2 suffix_6 alpha">
<div class="grid_2">
<input name='daterange' class="button green" type="submit" value="Submit"> </form>
</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 %}
&nbsp;
{% endif %}
</div>
@@ -77,11 +92,12 @@
{% endif %}
<div class="grid_12">
<div id="workouts_table" class="grid_8 alpha">
{% if team %}
<h3>{{ team.name }} Team Workouts</h3>
{% else %}
<h3>My Workouts</h3>
<h3>Workouts of {{ rower.user.first_name }} {{ rower.user.last_name }}</h3>
{% endif %}
{% if workouts %}
@@ -154,7 +170,10 @@
</td>
{% else %}
<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>
{% endif %}
<td> <a class="small" href="/rowers/workout/{{ workout.id }}/flexchart">Flex</a> </td>

View 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 %}

View 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>&nbsp;</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 %}

View 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 %}

View 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>&nbsp;</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 %}

View 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>&nbsp;</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 %}
&nbsp;
{% 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 }}">&nbsp;</a>
{% elif completeness|lookup:ps.id == 'completed' %}
<a class="green dot" href="/rowers/sessions/manage/{{ timeperiod }}/rower/{{ rower.id }}/session/{{ ps.id }}">&nbsp;</a>
{% elif completeness|lookup:ps.id == 'partial' %}
<a class="orange dot" href="/rowers/sessions/manage/{{ timeperiod }}/rower/{{ rower.id }}/session/{{ ps.id }}">&nbsp;</a>
{% else %}
<a class="red dot" href="/rowers/sessions/manage/{{ timeperiod }}/rower/{{ rower.id }}/session/{{ ps.id }}">&nbsp;</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 %}

View 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 %}

View 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 %}

View 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 %}

View 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>

View File

@@ -2,9 +2,9 @@
{% extends "base.html" %}
{% block title %}About us{% endblock title %}
{% block content %}
<h2>Pro Membership</h2>
<div class="grid_6 alpha">
<h2>Pro Membership</h2>
<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
@@ -27,7 +27,7 @@ You will be taken to the secure PayPal payment site.
</div>
<div class="grid_6 omega">
<h3>Recurring Payment</h2>
<h2>Recurring Payment</h2>
<p>You need a Paypal account for this</p>
<form action="https://www.paypal.com/cgi-bin/webscr" method="post" target="_top">
<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">
</form>
<h3>One Year Subscription</h3>
<h2>One Year Subscription</h2>
<p>Only a credit card needed. Will not automatically renew</p>
<form action="https://www.paypal.com/cgi-bin/webscr" method="post" target="_top">
<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">
</form>
<h3>Payment Processing</h3>
<h2>Payment Processing</h2>
<p>After you do the payment, we will manually change your membership to
"Pro". Depending on our availability, this may take some time
(typically one working day). Don't hesitate to contact us

View File

@@ -151,3 +151,4 @@ def team_members(user):
return []
return []

View File

@@ -137,6 +137,10 @@ urlpatterns = [
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+)/$',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/$',views.workouts_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'^analysis', TemplateView.as_view(template_name='analysis.html'),name='analysis'),
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'^legal', TemplateView.as_view(template_name='legal.html'),name='legal'),
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'^test\_callback',views.rower_process_testcallback),
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:

View File

@@ -291,3 +291,16 @@ from datetime import date
def calculate_age(born):
today = date.today()
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

View File

@@ -30,7 +30,7 @@ from rowers.forms import (
LoginForm,DocumentsForm,UploadOptionsForm,ImageForm,
TeamUploadOptionsForm,WorkFlowLeftPanelForm,WorkFlowMiddlePanelForm,
WorkFlowLeftPanelElement,WorkFlowMiddlePanelElement,
LandingPageForm,
LandingPageForm,PlannedSessionSelectForm,WorkoutSessionSelectForm
)
from django.core.urlresolvers import reverse
from django.core.exceptions import PermissionDenied
@@ -50,13 +50,16 @@ from rowers.forms import (
FusionMetricChoiceForm,BoxPlotChoiceForm,MultiFlexChoiceForm,
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 (
RowerPowerForm,RowerForm,GraphImage,AdvancedWorkoutForm,
RowerPowerZonesForm,AccountRowerForm,UserForm,StrokeData,
Team,TeamForm,TeamInviteForm,TeamInvite,TeamRequest,
WorkoutComment,WorkoutCommentForm,RowerExportForm,
CalcAgePerformance,PowerTimeFitnessMetric,
CalcAgePerformance,PowerTimeFitnessMetric,PlannedSessionForm
)
from rowers.models import FavoriteForm,BaseFavoriteFormSet,SiteAnnouncement
from rowers.metrics import rowingmetrics,defaultfavoritecharts
@@ -104,6 +107,7 @@ import json
from rest_framework.renderers import JSONRenderer
from rest_framework.parsers import JSONParser
from rowers.rows import handle_uploaded_file,handle_uploaded_image
from rowers.plannedsessions import *
from rowers.tasks import handle_makeplot,handle_otwsetpower,handle_sendemailtcx,handle_sendemailcsv
from rowers.tasks import (
handle_sendemail_unrecognized,handle_sendemailnewcomment,
@@ -602,6 +606,7 @@ def rowhascoordinates(row):
if rowdata != 0:
try:
latitude = rowdata.df[' latitude']
if not latitude.std():
hascoordinates = 0
except KeyError,AttributeError:
@@ -737,12 +742,12 @@ from utils import (
geo_distance,serialize_list,deserialize_list,uniqify,
str2bool,range_to_color_hex,absolute,myqueue,get_call,
calculate_age,rankingdistances,rankingdurations,
is_ranking_piece
is_ranking_piece,my_dict_from_instance
)
import datautils
from rowers.models import checkworkoutuser
from rowers.models import checkworkoutuser,checkaccessuser
# Check if a user is a Coach member
def iscoachmember(user):
@@ -759,7 +764,20 @@ def iscoachmember(user):
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):
try:
@@ -781,7 +799,7 @@ def ispromember(user):
r = Rower(user=user)
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:
result = False
return result
@@ -923,7 +941,7 @@ def add_workout_from_strokedata(user,importid,data,strokedata,
workouttype = 'rower'
if workouttype not in [x[0] for x in Workout.workouttypes]:
workouttype = 'water'
workouttype = 'other'
try:
comments = data['comments']
except:
@@ -1070,7 +1088,7 @@ def add_workout_from_runkeeperdata(user,importid,data):
# To Do - add utcoffset to time
workouttype = data['type']
if workouttype not in [x[0] for x in Workout.workouttypes]:
workouttype = 'water'
workouttype = 'other'
try:
comments = data['notes']
except:
@@ -1243,7 +1261,7 @@ def add_workout_from_runkeeperdata(user,importid,data):
def add_workout_from_stdata(user,importid,data):
workouttype = data['type']
if workouttype not in [x[0] for x in Workout.workouttypes]:
workouttype = 'water'
workouttype = 'other'
try:
comments = data['comments']
except:
@@ -1273,11 +1291,16 @@ def add_workout_from_stdata(user,importid,data):
try:
res = splitstdata(data['distance'])
distance = res[1]
times_distance = res[0]
except KeyError:
return (0,"No distance data in the workout")
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")
distance = res[1]
times_distance = res[0]
try:
l = data['location']
@@ -6032,18 +6055,31 @@ def workouts_view(request,message='',successmessage='',
startdatestring="",enddatestring="",
startdate=timezone.now()-datetime.timedelta(days=365),
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']
try:
r = getrower(request.user)
if rowerid != 0:
r = Rower.objects.get(id=rowerid)
elif userid != 0:
u = User.objects.get(id=userid)
r = getrower(u)
else:
r = getrower(request.user)
except Rower.DoesNotExist:
raise Http404("Rower doesn't exist")
# 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':
dateform = DateRangeForm(request.POST)
if dateform.is_valid():
startdate = dateform.cleaned_data['startdate']
enddate = dateform.cleaned_data['enddate']
startdatestring = None
enddatestring = None
else:
dateform = DateRangeForm(initial={
'startdate':startdate,
@@ -7490,7 +7526,6 @@ def workout_stats_view(request,id=0,message="",successmessage=""):
if w.workouttype in ('water','coastal'):
ftp = ftp*(100.-r.otwslack)/100.
intensityfactor = datadf['power'].mean()/float(ftp)
intensityfactor = normp/float(ftp)
tss = 100.*((duration*normp*intensityfactor)/(3600.*ftp))
@@ -11674,3 +11709,422 @@ def agegrouprecordview(request,sex='male',weightcategory='hwt',
'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',
]
}
)

View File

@@ -291,6 +291,19 @@ th.rotate > div > span {
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 {
font: 1.1em/1.5em sans-serif;
text-decoration: none;

View File

@@ -1,42 +1,42 @@
{% extends "basebase.html" %}
{% block filters %}
{% load rowerfilters %}
{% load rowerfilters %}
{% endblock %}
{% block teams %}
{% if user.is_authenticated and user|has_teams %}
<div class="grid_1 alpha dropdown">
<button class="grid_1 alpha button gray small dropbtn">
Teams
</button>
<div class="dropdown-content">
<a class="button gray small" href="/rowers/me/teams/">Manage Teams</a>
{% if user|is_manager %}
<a class="button gray small" href="/rowers/workout/upload/team/">Upload Team Member Workout</a>
{% endif %}
{% for t in user|user_teams %}
<a class="button gray small" href="/rowers/list-workouts/team/{{ t.id }}/">{{ t.name }}</a>
{% endfor %}
</div>
</div>
<span class="tooltiptext">See recent workouts for your team</span>
{% elif user.is_authenticated and user.rower.team.all %}
<div class="grid_1 alpha dropdown">
<button class="grid_1 alpha button gray small dropbtn">
Teams
</button>
<div class="dropdown-content">
{% for t in user.rower.team.all %}
<a class="button gray small" href="/rowers/list-workouts/team/{{ t.id }}/">{{ t.name }}</a>
{% endfor %}
</div>
</div>
<span class="tooltiptext">See recent workouts for your team</span>
{% else %}
<p>&nbsp;</p>
{% endif %}
{% if user.is_authenticated and user|has_teams %}
<div class="grid_1 alpha dropdown">
<button class="grid_1 alpha button gray small dropbtn">
Teams
</button>
<div class="dropdown-content">
<a class="button gray small" href="/rowers/me/teams/">Manage Teams</a>
{% if user|is_manager %}
<a class="button gray small" href="/rowers/workout/upload/team/">Upload Team Member Workout</a>
{% endif %}
{% for t in user|user_teams %}
<a class="button gray small" href="/rowers/list-workouts/team/{{ t.id }}/">{{ t.name }}</a>
{% endfor %}
</div>
</div>
<span class="tooltiptext">See recent workouts for your team</span>
{% elif user.is_authenticated and user.rower.team.all %}
<div class="grid_1 alpha dropdown">
<button class="grid_1 alpha button gray small dropbtn">
Teams
</button>
<div class="dropdown-content">
{% for t in user.rower.team.all %}
<a class="button gray small" href="/rowers/list-workouts/team/{{ t.id }}/">{{ t.name }}</a>
{% endfor %}
</div>
</div>
<span class="tooltiptext">See recent workouts for your team</span>
{% else %}
<p>&nbsp;</p>
{% endif %}
{% endblock %}
{% block content %}

View File

@@ -201,6 +201,17 @@
{% block teams %}
{% endblock %}
</div>
<div class="grid_1 tooltip">
{% if user.is_authenticated %}
<p><a class="button gray small" href="/rowers/sessions/">Plans</a></p>
{% else %}
<p>&nbsp;</p>
{% endif %}
</div>
<div class="grid_1 tooltip">
{% block challenges %}
{% endblock %}
</div>
</div>

BIN
temporary

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB