diff --git a/rowers/admin.py b/rowers/admin.py index 1004a06b..e1094553 100644 --- a/rowers/admin.py +++ b/rowers/admin.py @@ -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) + diff --git a/rowers/courses.py b/rowers/courses.py new file mode 100644 index 00000000..2d35363c --- /dev/null +++ b/rowers/courses.py @@ -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 diff --git a/rowers/dataprep.py b/rowers/dataprep.py index 032d51b9..9c891dd9 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -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 diff --git a/rowers/forms.py b/rowers/forms.py index 5a7c4a45..faa5d108 100644 --- a/rowers/forms.py +++ b/rowers/forms.py @@ -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, + ) + diff --git a/rowers/metrics.py b/rowers/metrics.py index bfac6b1f..c70a7639 100644 --- a/rowers/metrics.py +++ b/rowers/metrics.py @@ -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()): diff --git a/rowers/models.py b/rowers/models.py index 2838451b..d6316883 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -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, } - + diff --git a/rowers/plannedsessions.py b/rowers/plannedsessions.py new file mode 100644 index 00000000..caca9821 --- /dev/null +++ b/rowers/plannedsessions.py @@ -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' diff --git a/rowers/rows.py b/rowers/rows.py index 720fd710..6aa7401a 100644 --- a/rowers/rows.py +++ b/rowers/rows.py @@ -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' diff --git a/rowers/stravastuff.py b/rowers/stravastuff.py index e53fa108..9666ca1d 100644 --- a/rowers/stravastuff.py +++ b/rowers/stravastuff.py @@ -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") diff --git a/rowers/templates/graphimage_delete_confirm.html b/rowers/templates/graphimage_delete_confirm.html index 8b0dfa3f..04989cea 100644 --- a/rowers/templates/graphimage_delete_confirm.html +++ b/rowers/templates/graphimage_delete_confirm.html @@ -40,4 +40,4 @@ -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/rowers/templates/list_workouts.html b/rowers/templates/list_workouts.html index 4a1963a9..82b4bba2 100644 --- a/rowers/templates/list_workouts.html +++ b/rowers/templates/list_workouts.html @@ -50,7 +50,6 @@
You have arrived at this page, because you tried to create a + training plan for yourself.
+ +Currently, training planning is restricted to "coach" members with + "team" functionality.
+ +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. +
+ +If you would like to find a coach who helps you plan your training + through rowsandall.com, contact me throught the contact form.
+ +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.
+ + +Over the spring of 2018, we will gradually expand this functionality. + Our current roadmap is to deploy the following and more: + +
+
+ Click on session name to view +
+| After | +Before | +Name | +Value | ++ | Edit | +Clone | +Delete | +
|---|---|---|---|---|---|---|---|
| {{ ps.startdate|date:"Y-m-d" }} | +{{ ps.enddate|date:"Y-m-d" }} | ++ {% if ps.name != '' %} + {{ ps.name }} + {% else %} + Unnamed Session + {% endif %} + | +{{ ps.sessionvalue }} | +{{ ps.sessionunit }} | ++ Edit + | ++ Clone + | + ++ Delete + | +
This will permanently delete the planned session
+ + ++ Cancel +
+ Delete +
+| {{ value.0 }} | {{ value.1 }} | +
+ Click on session name to view +
+| After | +Before | +Name | +Value | ++ | Edit | +Clone | +Delete | +
|---|---|---|---|---|---|---|---|
| {{ ps.startdate|date:"Y-m-d" }} | +{{ ps.enddate|date:"Y-m-d" }} | ++ {% if ps.name != '' %} + {{ ps.name }} + {% else %} + Unnamed Session + {% endif %} + | +{{ ps.sessionvalue }} | +{{ ps.sessionunit }} | ++ {% if timeperiod and rower %} + Edit + {% elif timeperiod %} + Edit + {% else %} + Edit + {% endif %} + | ++ Clone + | ++ Delete + | +
+ Click on session name to view, edit to change the session and on the + traffic light symbol to add workouts to the session +
+| After | +Before | +Name | +Edit | +Value | ++ | Type | +Status | ++ |
|---|---|---|---|---|---|---|---|---|
| {{ ps.startdate|date:"Y-m-d" }} | +{{ ps.enddate|date:"Y-m-d" }} | ++ {% if ps.name != '' %} + {{ ps.name }} + {% else %} + Unnamed Session + {% endif %} + | ++ {% if ps.manager == request.user %} + Edit + {% else %} + + {% endif %} + | +{{ ps.sessionvalue }} | +{{ ps.sessionunit }} | +{{ ps.sessiontype }} | ++ {% if completeness|lookup:ps.id == 'not done' %} + + {% elif completeness|lookup:ps.id == 'completed' %} + + {% elif completeness|lookup:ps.id == 'partial' %} + + {% else %} + + {% endif %} + | +
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. +
++ If you select a workout that has already been matched to another session, + it will change to match this session. +
++ We will make this form smarter in the near future. +
+| {{ value.0 }} | {{ value.1 }} | +
Status: {{ status }}
+Percentage complete: {{ ratio }}
+| Date | +Name | +Distance | +Duration | +
|---|---|---|---|
| {{ workout.date|date:"Y-m-d" }} | ++ + {{ workout.name }} + + | +{{ workout.distance }}m | +{{ workout.duration |durationprint:"%H:%M:%S.%f" }} | +
+ Click on session name to view +
++ {% if timeperiod and rower %} + Plan Overview + {% elif timeperiod %} + Plan Overview + {% else %} + Plan Overview + {% endif %} +
++ {% if timeperiod and rower %} + Manage Sessions + {% elif timeperiod %} + Manage Sessions + {% else %} + Manage Sessions + {% endif %} +
++ Add Session +
+Donations are welcome to keep this web site going. To help cover the hosting
costs, I have created a Pro
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.
You need a Paypal account for this
-Only a credit card needed. Will not automatically renew
-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
diff --git a/rowers/templatetags/rowerfilters.py b/rowers/templatetags/rowerfilters.py
index eebaaaf3..2f1d66e3 100644
--- a/rowers/templatetags/rowerfilters.py
+++ b/rowers/templatetags/rowerfilters.py
@@ -151,3 +151,4 @@ def team_members(user):
return []
return []
+
diff --git a/rowers/urls.py b/rowers/urls.py
index f2d291f9..2d42ea30 100644
--- a/rowers/urls.py
+++ b/rowers/urls.py
@@ -137,6 +137,10 @@ urlpatterns = [
url(r'^list-workouts/ranking$',views.workouts_view,{'rankingonly':True}),
url(r'^list-workouts/team/(?P
+ {% endif %} +