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 @@
- Select start and end date for a date range:
{% if team %} @@ -64,9 +63,25 @@ {% csrf_token %}
-
+
+ {% if user.is_authenticated and user|is_manager %} + + {% else %} +   + + {% endif %} +
@@ -77,11 +92,12 @@ {% endif %}
+
{% if team %}

{{ team.name }} Team Workouts

{% else %} -

My Workouts

+

Workouts of {{ rower.user.first_name }} {{ rower.user.last_name }}

{% endif %} {% if workouts %} @@ -154,7 +170,10 @@ {% else %} - {{ workout.user.user.first_name }} {{ workout.user.user.last_name }} + + {{ workout.user.user.first_name }} + {{ workout.user.user.last_name }} + {% endif %} Flex diff --git a/rowers/templates/planmembership.html b/rowers/templates/planmembership.html new file mode 100644 index 00000000..55d599e8 --- /dev/null +++ b/rowers/templates/planmembership.html @@ -0,0 +1,57 @@ + + {% extends "base.html" %} + {% block title %}About us{% endblock title %} + {% block content %} + +
+

Coach and Self-Coach Membership

+ +

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.

+ + +
+ +
+

What training planning functionality do we offer?

+ +

Over the spring of 2018, we will gradually expand this functionality. + Our current roadmap is to deploy the following and more: + +

+

    +
  • Create Planned Sessions (trainings, tests, challenges) for yourself + and for your team members (coach plan)
  • +
  • Track your performance against plan. + Match workouts to planned sessions. + Get feedback on plan adherence.
  • +
  • Track your teams performance against plan. See how well each + of your team members adhere to their (team or personalized) plan.
  • +
  • See test outcomes ranked by performance.
  • +
  • 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. +
+

+ +
+ + + {% endblock content %} diff --git a/rowers/templates/plannedsessioncreate.html b/rowers/templates/plannedsessioncreate.html new file mode 100644 index 00000000..dd376e3e --- /dev/null +++ b/rowers/templates/plannedsessioncreate.html @@ -0,0 +1,112 @@ +{% extends "base.html" %} +{% load staticfiles %} + +{% block title %}New Planned Session{% endblock %} + +{% block content %} +
+ {% include "planningbuttons.html" %} + +
+
+
+

Create Session for {{ rower.user.first_name }} {{ rower.user.last_name }}

+
+ +
+
+
+
+ {% if form.errors %} +

+ Please correct the error{{ form.errors|pluralize }} below. +

+ {% endif %} + + + {{ form.as_table }} +
+ {% csrf_token %} +
+ +
+
+ + + + + +
+{% endblock %} diff --git a/rowers/templates/plannedsessiondeleteconfirm.html b/rowers/templates/plannedsessiondeleteconfirm.html new file mode 100644 index 00000000..0a6f3d7f --- /dev/null +++ b/rowers/templates/plannedsessiondeleteconfirm.html @@ -0,0 +1,45 @@ +{% extends "base.html" %} +{% load staticfiles %} + +{% block title %}Planned Session{% endblock %} + +{% block content %} +
+ {% include "planningbuttons.html" %} + +
+
+

Confirm Delete

+

This will permanently delete the planned session

+ + +
+

+ Cancel +

+ +
+

+ Delete +

+
+ +
+ + + + +{% endblock %} diff --git a/rowers/templates/plannedsessionedit.html b/rowers/templates/plannedsessionedit.html new file mode 100644 index 00000000..7990d726 --- /dev/null +++ b/rowers/templates/plannedsessionedit.html @@ -0,0 +1,123 @@ +{% extends "base.html" %} +{% load staticfiles %} + +{% block title %}Update Planned Session{% endblock %} + +{% block content %} +
+ {% include "planningbuttons.html" %} + +
+
+
+

Edit Session {{ thesession.name }}

+
+ +
+
+
+
+ {% if form.errors %} +

+ Please correct the error{{ form.errors|pluralize }} below. +

+ {% endif %} + + + {{ form.as_table }} +
+ {% csrf_token %} +
+ Delete +
+
+ Clone +
+
+ +
+
+ + + + + +
+{% endblock %} diff --git a/rowers/templates/plannedsessions.html b/rowers/templates/plannedsessions.html new file mode 100644 index 00000000..e660b33d --- /dev/null +++ b/rowers/templates/plannedsessions.html @@ -0,0 +1,109 @@ +{% extends "base.html" %} +{% load staticfiles %} +{% load rowerfilters %} + +{% block title %}Planned Sessions{% endblock %} + +{% block content %} +
+ {% include "planningbuttons.html" %} +
+
+

Plan for {{ rower.user.first_name }} {{ rower.user.last_name }}

+
+ +
+ {% if plannedsessions %} +

+ Click on session name to view, edit to change the session and on the + traffic light symbol to add workouts to the session +

+ + + + + + + + + + + + + + + {% for ps in plannedsessions %} + + + + + + + + + + + {% endfor %} + +
AfterBeforeNameEditValue TypeStatus +
{{ 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 %} +
+ {% 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 %} +
+ + + + +{% endblock %} diff --git a/rowers/templates/plannedsessionsmanage.html b/rowers/templates/plannedsessionsmanage.html new file mode 100644 index 00000000..7ce2b688 --- /dev/null +++ b/rowers/templates/plannedsessionsmanage.html @@ -0,0 +1,121 @@ +{% extends "base.html" %} +{% load staticfiles %} +{% load rowerfilters %} + +{% block title %}Planned Sessions{% endblock %} + +{% block meta %} + + + +{% endblock %} + + +{% block content %} +
+ {% include "planningbuttons.html" %} +
+
+

Manage Plan Execution for {{ rower.user.first_name }} {{ rower.user.last_name }}

+
+ + +
+

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. +

+
+
+
+
+ {{ ps_form.as_table}} +
+
+ {{ w_form.as_table}} +
+
+
+ {% csrf_token %} + +
+
+ + + + +{% endblock %} + +{% block scripts %} + + +{% endblock %} diff --git a/rowers/templates/plannedsessionview.html b/rowers/templates/plannedsessionview.html new file mode 100644 index 00000000..be61c544 --- /dev/null +++ b/rowers/templates/plannedsessionview.html @@ -0,0 +1,59 @@ +{% extends "base.html" %} +{% load staticfiles %} +{% load rowerfilters %} + +{% block title %}Planned Session{% endblock %} + +{% block content %} +
+ {% include "planningbuttons.html" %} + +
+
+

Session {{ psdict.name.1 }}

+ + {% for attr in attrs %} + {% for key,value in psdict.items %} + {% if key == attr %} + + + + {% endif %} + {% endfor %} + {% endfor %} +
{{ value.0 }}{{ value.1 }}
+

Result

+

Status: {{ status }}

+

Percentage complete: {{ ratio }}

+
+ + + + +{% endblock %} diff --git a/rowers/templates/plannedsssionsmanage.html b/rowers/templates/plannedsssionsmanage.html new file mode 100644 index 00000000..8be3f91b --- /dev/null +++ b/rowers/templates/plannedsssionsmanage.html @@ -0,0 +1,48 @@ +{% extends "base.html" %} +{% load staticfiles %} +{% load rowerfilters %} + +{% block title %}Planned Sessions{% endblock %} + +{% block content %} +
+ {% include "planningbuttons.html" %} +
+
+

Plan for {{ rower.user.first_name }} {{ rower.user.last_name }}

+
+ +
+

+ Click on session name to view +

+
+ + + + +{% endblock %} diff --git a/rowers/templates/planningbuttons.html b/rowers/templates/planningbuttons.html new file mode 100644 index 00000000..52d12a55 --- /dev/null +++ b/rowers/templates/planningbuttons.html @@ -0,0 +1,28 @@ +{% load rowerfilters %} +
+

+ {% 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 +

+
diff --git a/rowers/templates/promembership.html b/rowers/templates/promembership.html index 6ca7312f..bb89c13d 100644 --- a/rowers/templates/promembership.html +++ b/rowers/templates/promembership.html @@ -2,9 +2,9 @@ {% extends "base.html" %} {% block title %}About us{% endblock title %} {% block content %} -

Pro Membership

+

Pro Membership

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.

-

Recurring Payment

+

Recurring Payment

You need a Paypal account for this

@@ -40,7 +40,7 @@ You will be taken to the secure PayPal payment site.
-

One Year Subscription

+

One Year Subscription

Only a credit card needed. Will not automatically renew

@@ -49,7 +49,7 @@ You will be taken to the secure PayPal payment site.
-

Payment Processing

+

Payment Processing

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\d+)/(?P\w+.*)/(?P\w+.*)$',views.workouts_view), url(r'^list-workouts/team/(?P\d+)/$',views.workouts_view), + url(r'^(?P\d+)/list-workouts/$',views.workouts_view), + url(r'^(?P\d+)/list-workouts/(?P\w+.*)/(?P\w+.*)$',views.workouts_view), + url(r'^u/(?P\d+)/list-workouts/$',views.workouts_view), + url(r'^u/(?P\d+)/list-workouts/(?P\w+.*)/(?P\w+.*)$',views.workouts_view), url(r'^list-workouts/(?P\w+.*)/(?P\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\d+)/(?P\d+)/(?P\w+.*)/(?P[\w\ ]+.*)/$',views.workout_comparison_view2), url(r'^test\_callback',views.rower_process_testcallback), url(r'^workout/(?P\d+)/test\_strokedata$',views.strokedataform), + url(r'^sessions/create$',views.plannedsession_create_view), + url(r'^sessions/create/rower/(?P\d+)$', + views.plannedsession_create_view), + url( + r'^sessions/create/(?P[\w\ ]+.*)/rower/(?P\d+)$', + views.plannedsession_create_view), + url(r'^sessions/create/(?P[\w\ ]+.*)$', + views.plannedsession_create_view), + + url(r'^sessions/(?P\d+)/edit$',views.plannedsession_edit_view), + url(r'^sessions/(?P\d+)/edit/(?P[\w\ ]+.*)/rower/(?P\d+)$',views.plannedsession_edit_view), + url(r'^sessions/(?P\d+)/edit/(?P[\w\ ]+.*)$',views.plannedsession_edit_view), + + url(r'^sessions/(?P\d+)/clone$',views.plannedsession_clone_view), + url(r'^sessions/(?P\d+)/clone/(?P[\w\ ]+.*)/rower/(?P\d+)$',views.plannedsession_clone_view), + url(r'^sessions/(?P\d+)/clone/(?P[\w\ ]+.*)$',views.plannedsession_clone_view), + + url(r'^sessions/(?P\d+)$',views.plannedsession_view), + url(r'^sessions/(?P\d+)/deleteconfirm$',views.plannedsession_deleteconfirm_view), + url(r'^sessions/(?P\d+)/delete$',views.plannedsession_delete_view), + url(r'^sessions/manage/session/(?P\d+)$', + views.plannedsessions_manage_view), + url(r'^sessions/manage/rower/(?P\d+)/session/(?P\d+)$', + views.plannedsessions_manage_view), + url(r'^sessions/manage/(?P[\w\ ]+.*)/rower/(?P\d+)/session/(?P\d+)$', + views.plannedsessions_manage_view), + url(r'^sessions/manage/(?P[\w\ ]+.*)/session/(?P\d+)$', + views.plannedsessions_manage_view), + + + url(r'^sessions/manage/?$', + views.plannedsessions_manage_view), + url(r'^sessions/manage/rower/(?P\d+)$', + views.plannedsessions_manage_view), + url(r'^sessions/manage/(?P[\w\ ]+.*)/rower/(?P\d+)$', + views.plannedsessions_manage_view), + url(r'^sessions/manage/(?P[\w\ ]+.*)$', + views.plannedsessions_manage_view), + url(r'^sessions/?$',views.plannedsessions_view), + url(r'^sessions/rower/(?P\d+)$',views.plannedsessions_view), + url(r'^sessions/(?P[\w\ ]+.*)/rower/(?P\d+)$',views.plannedsessions_view), + url(r'^sessions/(?P[\w\ ]+.*)$',views.plannedsessions_view), ] if settings.DEBUG: diff --git a/rowers/utils.py b/rowers/utils.py index 108a670a..27fad934 100644 --- a/rowers/utils.py +++ b/rowers/utils.py @@ -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 diff --git a/rowers/views.py b/rowers/views.py index fc85c8a9..503ecdf4 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -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', + ] + } + ) diff --git a/static/css/rowsandall.css b/static/css/rowsandall.css index 8b649642..36fe4242 100644 --- a/static/css/rowsandall.css +++ b/static/css/rowsandall.css @@ -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; diff --git a/templates/base.html b/templates/base.html index 21de4904..d5912f8f 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,42 +1,42 @@ {% extends "basebase.html" %} {% block filters %} - {% load rowerfilters %} +{% load rowerfilters %} {% endblock %} {% block teams %} - {% if user.is_authenticated and user|has_teams %} -

- See recent workouts for your team - {% elif user.is_authenticated and user.rower.team.all %} - - See recent workouts for your team - {% else %} -

 

- {% endif %} +{% if user.is_authenticated and user|has_teams %} + +See recent workouts for your team +{% elif user.is_authenticated and user.rower.team.all %} + +See recent workouts for your team +{% else %} +

 

+{% endif %} {% endblock %} {% block content %} diff --git a/templates/basebase.html b/templates/basebase.html index 2a97d27c..0a96c96d 100644 --- a/templates/basebase.html +++ b/templates/basebase.html @@ -201,6 +201,17 @@ {% block teams %} {% endblock %}
+
+ {% if user.is_authenticated %} +

Plans

+ {% else %} +

 

+ {% endif %} +
+
+ {% block challenges %} + {% endblock %} +
diff --git a/temporary b/temporary deleted file mode 100644 index e6e80ce6..00000000 Binary files a/temporary and /dev/null differ