2837 lines
90 KiB
Python
2837 lines
90 KiB
Python
from __future__ import unicode_literals
|
|
|
|
from django.db import models
|
|
from django.contrib.auth.models import User
|
|
from django.core.validators import validate_email
|
|
from django.core.exceptions import ValidationError
|
|
from django import forms
|
|
from django.forms import ModelForm
|
|
from django.dispatch import receiver
|
|
from django.forms.widgets import SplitDateTimeWidget
|
|
from django.forms.extras.widgets import SelectDateWidget
|
|
from django.forms.formsets import BaseFormSet
|
|
from django.contrib.admin.widgets import AdminDateWidget,AdminTimeWidget,AdminSplitDateTime
|
|
from datetimewidget.widgets import DateTimeWidget
|
|
from django.core.validators import validate_email
|
|
import os
|
|
import twitter
|
|
import re
|
|
import pytz
|
|
|
|
from scipy.interpolate import splprep, splev, CubicSpline
|
|
import numpy as np
|
|
|
|
from django.conf import settings
|
|
from sqlalchemy import create_engine
|
|
import sqlalchemy as sa
|
|
from sqlite3 import OperationalError
|
|
from django.utils import timezone
|
|
import pandas as pd
|
|
from dateutil import parser
|
|
import datetime
|
|
from django.core.exceptions import ValidationError
|
|
from rowers.rows import validate_file_extension
|
|
from collections import OrderedDict
|
|
from timezonefinder import TimezoneFinder
|
|
|
|
import types
|
|
from matplotlib import path
|
|
|
|
from rowsandall_app.settings import (
|
|
TWEET_ACCESS_TOKEN_KEY,
|
|
TWEET_ACCESS_TOKEN_SECRET,
|
|
TWEET_CONSUMER_KEY,
|
|
TWEET_CONSUMER_SECRET,
|
|
)
|
|
|
|
tweetapi = twitter.Api(consumer_key=TWEET_CONSUMER_KEY,
|
|
consumer_secret=TWEET_CONSUMER_SECRET,
|
|
access_token_key=TWEET_ACCESS_TOKEN_KEY,
|
|
access_token_secret=TWEET_ACCESS_TOKEN_SECRET)
|
|
|
|
from rowers.database import *
|
|
|
|
timezones = (
|
|
(x,x) for x in pytz.common_timezones
|
|
)
|
|
|
|
class UserFullnameChoiceField(forms.ModelChoiceField):
|
|
def label_from_instance(self,obj):
|
|
return obj.get_full_name()
|
|
|
|
# model for configurable template field
|
|
class TemplateListField(models.TextField):
|
|
def __init__(self, *args, **kwargs):
|
|
self.token = kwargs.pop('token',',')
|
|
super(TemplateListField, self).__init__(*args, **kwargs)
|
|
|
|
def to_python(self, value):
|
|
if not value: return
|
|
if isinstance(value, list):
|
|
return value
|
|
# remove double quotes and brackets
|
|
value = re.sub(r'u\"','',value)
|
|
value = re.sub(r'u\'','',value)
|
|
value = re.sub(r'\\','',value)
|
|
value = re.sub(r'\"','',value)
|
|
value = re.sub(r'\'','',value)
|
|
value = re.sub(r'\[','',value)
|
|
value = re.sub(r'\]','',value)
|
|
value = re.sub(r'\[\[','[',value)
|
|
value = re.sub(r'\]\]',']',value)
|
|
value = re.sub(r'\ \ ',' ',value)
|
|
value = re.sub(r', ',',',value)
|
|
|
|
return value.split(self.token)
|
|
|
|
def from_db_value(self,value, expression, connection, context):
|
|
if value is None:
|
|
return value
|
|
if isinstance(value, list):
|
|
return value
|
|
return value.split(self.token)
|
|
|
|
def get_db_prep_value(self, value, connection, prepared=False):
|
|
if not value: return
|
|
assert(isinstance(value, list) or isinstance(value, tuple))
|
|
return self.token.join([unicode(s) for s in value])
|
|
|
|
def value_to_string(self, obj):
|
|
value = self._get_val_from_obj(obj)
|
|
return self.get_deb_prep_value(value)
|
|
|
|
# model for Power Zone names
|
|
class PowerZonesField(models.TextField):
|
|
# __metaclass__ = models.SubfieldBase
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
self.token = kwargs.pop('token',',')
|
|
super(PowerZonesField, self).__init__(*args, **kwargs)
|
|
|
|
def to_python(self, value):
|
|
if not value: return
|
|
if isinstance(value, list):
|
|
return value
|
|
# remove double quotes and brackets
|
|
value = re.sub(r'u\"','',value)
|
|
value = re.sub(r'u\'','',value)
|
|
value = re.sub(r'\\','',value)
|
|
value = re.sub(r'\"','',value)
|
|
value = re.sub(r'\'','',value)
|
|
value = re.sub(r'\[','',value)
|
|
value = re.sub(r'\]','',value)
|
|
value = re.sub(r'\[\[','[',value)
|
|
value = re.sub(r'\]\]',']',value)
|
|
value = re.sub(r'\ \ ',' ',value)
|
|
value = re.sub(r', ',',',value)
|
|
|
|
return value.split(self.token)
|
|
|
|
def from_db_value(self,value, expression, connection, context):
|
|
if value is None:
|
|
return value
|
|
if isinstance(value, list):
|
|
return value
|
|
return value.split(self.token)
|
|
|
|
def get_db_prep_value(self, value, connection, prepared=False):
|
|
if not value: return
|
|
assert(isinstance(value, list) or isinstance(value, tuple))
|
|
return self.token.join([unicode(s) for s in value])
|
|
|
|
def value_to_string(self, obj):
|
|
value = self._get_val_from_obj(obj)
|
|
return self.get_deb_prep_value(value)
|
|
|
|
|
|
c2url = 'http://www.concept2.com/indoor-rowers/racing/records/world?machine=1&event=All&gender=All&age=All&weight=All'
|
|
|
|
def update_records(url=c2url):
|
|
try:
|
|
dfs = pd.read_html(url,attrs={'class':'views-table'})
|
|
df = dfs[0]
|
|
df.columns = df.columns.str.strip()
|
|
success = 1
|
|
except:
|
|
df = pd.DataFrame()
|
|
|
|
if not df.empty:
|
|
C2WorldClassAgePerformance.objects.all().delete()
|
|
|
|
df.Gender = df.Gender.apply(lambda x: 'male' if x=='M' else 'female')
|
|
df['Distance'] = df['Event']
|
|
df['Duration'] = 0
|
|
|
|
for nr,row in df.iterrows():
|
|
if 'm' in row['Record']:
|
|
df.ix[nr,'Distance'] = row['Record'][:-1]
|
|
df.ix[nr,'Duration'] = 60*row['Event']
|
|
else:
|
|
df.ix[nr,'Distance'] = row['Event']
|
|
try:
|
|
tobj = datetime.datetime.strptime(row['Record'],'%M:%S.%f')
|
|
except ValueError:
|
|
tobj = datetime.datetime.strptime(row['Record'],'%H:%M:%S.%f')
|
|
df.ix[nr,'Duration'] = 3600.*tobj.hour+60.*tobj.minute+tobj.second+tobj.microsecond/1.e6
|
|
|
|
for nr,row in df.iterrows():
|
|
try:
|
|
weightcategory = row.Weight.lower()
|
|
except AttributeError:
|
|
weightcategory = 'hwt'
|
|
|
|
sex = row.Gender
|
|
name = row.Name
|
|
age = int(row.Age)
|
|
distance = int(row.Distance)
|
|
duration = float(row.Duration)
|
|
season = int(row.Season)
|
|
|
|
velo = distance/duration
|
|
power = int(2.8*velo**3)
|
|
|
|
record = C2WorldClassAgePerformance(
|
|
age = age,
|
|
weightcategory = weightcategory,
|
|
sex=sex,
|
|
distance = distance,
|
|
duration = duration,
|
|
power = power,
|
|
season = season,
|
|
name = name,
|
|
)
|
|
try:
|
|
record.save()
|
|
except:
|
|
print record
|
|
|
|
|
|
class CalcAgePerformance(models.Model):
|
|
weightcategories = (
|
|
('hwt','heavy-weight'),
|
|
('lwt','light-weight'),
|
|
)
|
|
|
|
sexcategories = (
|
|
('male','male'),
|
|
('female','female'),
|
|
)
|
|
|
|
weightcategory = models.CharField(default="hwt",
|
|
max_length=30,
|
|
choices=weightcategories)
|
|
|
|
sex = models.CharField(default="female",
|
|
max_length=30,
|
|
choices=sexcategories)
|
|
|
|
age = models.IntegerField(default=19,verbose_name="Age")
|
|
|
|
duration = models.FloatField(default=1,blank=True)
|
|
power = models.IntegerField(default=200)
|
|
|
|
class Meta:
|
|
db_table = 'calcagegrouprecords'
|
|
|
|
class PowerTimeFitnessMetric(models.Model):
|
|
modechoices = (
|
|
('rower','Rower'),
|
|
('water','On the water')
|
|
)
|
|
|
|
date = models.DateField(default=timezone.now)
|
|
last_workout = models.IntegerField(default=0)
|
|
user = models.ForeignKey(User)
|
|
PowerFourMin = models.FloatField(default=0)
|
|
PowerTwoK = models.FloatField(default=0)
|
|
PowerOneHour = models.FloatField(default=0)
|
|
workoutmode = models.CharField(default='rower',choices=modechoices,
|
|
max_length=40)
|
|
class Meta:
|
|
db_table = 'powertimefitnessmetric'
|
|
|
|
class C2WorldClassAgePerformance(models.Model):
|
|
weightcategories = (
|
|
('hwt','heavy-weight'),
|
|
('lwt','light-weight'),
|
|
)
|
|
|
|
sexcategories = (
|
|
('male','male'),
|
|
('female','female'),
|
|
)
|
|
|
|
weightcategory = models.CharField(default="hwt",
|
|
max_length=30,
|
|
choices=weightcategories)
|
|
|
|
sex = models.CharField(default="female",
|
|
max_length=30,
|
|
choices=sexcategories)
|
|
|
|
age = models.IntegerField(default=19,verbose_name="Age")
|
|
|
|
distance = models.IntegerField(default=2000)
|
|
name = models.CharField(max_length=200,blank=True)
|
|
duration = models.FloatField(default=1,blank=True)
|
|
season = models.IntegerField(default=2013)
|
|
power = models.IntegerField(default=200)
|
|
|
|
class Meta:
|
|
unique_together = ('age','sex','weightcategory','distance')
|
|
|
|
def __unicode__(self):
|
|
return self.sex+' '+self.weightcategory+' '+self.name+':'+str(self.age)+' ('+str(self.season)+')'
|
|
|
|
# For future Team functionality
|
|
class Team(models.Model):
|
|
choices = (
|
|
('private','private'),
|
|
('open','open'),
|
|
)
|
|
|
|
viewchoices = (
|
|
('coachonly','Coach Only'),
|
|
('allmembers','All Members')
|
|
)
|
|
|
|
name = models.CharField(max_length=150,unique=True,verbose_name='Team Name')
|
|
notes = models.CharField(blank=True,max_length=200,verbose_name='Team Purpose')
|
|
manager = models.ForeignKey(User, null=True)
|
|
private = models.CharField(max_length=30,choices=choices,default='open',
|
|
verbose_name='Team Type')
|
|
|
|
viewing = models.CharField(max_length=30,choices=viewchoices,default='allmembers',verbose_name='Sharing Behavior')
|
|
|
|
def __unicode__(self):
|
|
return self.name
|
|
|
|
class TeamForm(ModelForm):
|
|
class Meta:
|
|
model = Team
|
|
fields = ['name','notes','private','viewing']
|
|
widgets = {
|
|
'notes': forms.Textarea,
|
|
}
|
|
|
|
class TeamInvite(models.Model):
|
|
team = models.ForeignKey(Team)
|
|
user = models.ForeignKey(User,null=True)
|
|
issuedate = models.DateField(default=timezone.now)
|
|
code = models.CharField(max_length=150,unique=True)
|
|
email = models.CharField(max_length=150,null=True,blank=True)
|
|
|
|
class TeamInviteForm(ModelForm):
|
|
user = UserFullnameChoiceField(queryset=User.objects.all(),required=False)
|
|
class Meta:
|
|
model = TeamInvite
|
|
fields = ['user','email']
|
|
|
|
class TeamRequest(models.Model):
|
|
team = models.ForeignKey(Team)
|
|
user = models.ForeignKey(User,null=True)
|
|
issuedate = models.DateField(default=timezone.now)
|
|
code = models.CharField(max_length=150,unique=True)
|
|
|
|
from utils import (
|
|
workflowleftpanel,workflowmiddlepanel,
|
|
defaultleft,defaultmiddle,landingpages
|
|
)
|
|
|
|
from utils import geo_distance
|
|
|
|
def polygon_coord_center(polygon):
|
|
|
|
points = GeoPoint.objects.filter(polygon=polygon).order_by("order_in_poly")
|
|
|
|
latitudes = pd.Series([p.latitude for p in points])
|
|
longitudes = pd.Series([p.longitude for p in points])
|
|
|
|
return latitudes.mean(), longitudes.mean()
|
|
|
|
|
|
|
|
def polygon_to_path(polygon):
|
|
points = GeoPoint.objects.filter(polygon=polygon).order_by("order_in_poly")
|
|
s = []
|
|
for point in points:
|
|
s.append([point.latitude,point.longitude])
|
|
|
|
p = path.Path(s[:-1])
|
|
|
|
return p
|
|
|
|
from rowers.courseutils import coordinate_in_path
|
|
|
|
def course_spline(coordinates):
|
|
latitudes = coordinates['latitude'].values
|
|
longitudes = coordinates['longitude'].values
|
|
|
|
# spline parameters
|
|
s = 1.0
|
|
k = min([5,len(latitudes)-1])
|
|
nest = -1
|
|
|
|
t = np.linspace(0,1,len(latitudes))
|
|
tnew = np.linspace(0,1,100)
|
|
|
|
latnew = CubicSpline(t,latitudes,bc_type='clamped')(tnew)
|
|
lonnew = CubicSpline(t,longitudes,bc_type='clamped')(tnew)
|
|
# latnew = CubicSpline(t,latitudes,bc_type='natural')(tnew)
|
|
# lonnew = CubicSpline(t,longitudes,bc_type='natural')(tnew)
|
|
|
|
# tckp,u = splprep([t,latitudes,longitudes],s=s,k=k,nest=nest)
|
|
# tnew,latnew,lonnew = splev(np.linspace(0,1,100),tckp)
|
|
|
|
newcoordinates = pd.DataFrame({
|
|
'latitude':latnew,
|
|
'longitude':lonnew,
|
|
})
|
|
|
|
return newcoordinates
|
|
|
|
def course_coord_center(course):
|
|
|
|
polygons = GeoPolygon.objects.filter(course=course).order_by("order_in_course")
|
|
|
|
latitudes = []
|
|
longitudes = []
|
|
|
|
for p in polygons:
|
|
latitude,longitude = polygon_coord_center(p)
|
|
latitudes.append(latitude)
|
|
longitudes.append(longitude)
|
|
|
|
latitude = pd.Series(latitudes).median()
|
|
longitude = pd.Series(longitudes).median()
|
|
|
|
|
|
coordinates = pd.DataFrame({
|
|
'latitude':latitudes,
|
|
'longitude':longitudes,
|
|
})
|
|
|
|
|
|
return latitude,longitude,coordinates
|
|
|
|
def course_coord_maxmin(course):
|
|
|
|
polygons = GeoPolygon.objects.filter(course=course).order_by("order_in_course")
|
|
|
|
latitudes = []
|
|
longitudes = []
|
|
|
|
for p in polygons:
|
|
latitude,longitude = polygon_coord_center(p)
|
|
latitudes.append(latitude)
|
|
longitudes.append(longitude)
|
|
|
|
lat_min = pd.Series(latitudes).min()
|
|
lat_max = pd.Series(latitudes).max()
|
|
long_min = pd.Series(longitudes).min()
|
|
long_max = pd.Series(longitudes).max()
|
|
|
|
|
|
return lat_min,lat_max,long_min,long_max
|
|
|
|
def get_dir_vector(polygon1,polygon2):
|
|
lat1,lon1 = polygon_coord_center(polygon1)
|
|
lat2,lon2 = polygon_coord_center(polygon2)
|
|
|
|
return [lat2-lat1,lon2-lon1]
|
|
|
|
def get_delta(vector,polygon):
|
|
x = pd.Series(range(10000))/9999.
|
|
vlat = vector[0]
|
|
vlon = vector[1]
|
|
|
|
lat1,lon1 = polygon_coord_center(polygon)
|
|
|
|
lat = x.apply(lambda x:lat1+x*vlat)
|
|
lon = x.apply(lambda x:lon1+x*vlon)
|
|
|
|
totdist,bearing = geo_distance(lat1,lon1,lat1+vlat,lon1+vlon)
|
|
|
|
dist = x*totdist
|
|
|
|
p = polygon_to_path(polygon)
|
|
|
|
f = lambda x: coordinate_in_path(x['lat'],x['lon'],p)
|
|
|
|
df = pd.DataFrame({'x':x,
|
|
'lat':lat,
|
|
'lon':lon,
|
|
'dist':dist,
|
|
})
|
|
|
|
df['inpolygon'] = df.apply(f,axis=1)
|
|
|
|
|
|
b = (~df['inpolygon']).shift(-1)+df['inpolygon']
|
|
|
|
|
|
if len(df[b==2]):
|
|
return 1.0e3*df[b==2]['dist'].min()
|
|
|
|
else:
|
|
return 0
|
|
|
|
|
|
def get_delta_start(course):
|
|
polygons = GeoPolygon.objects.filter(course=course).order_by("order_in_course")
|
|
vector = get_dir_vector(polygons[0],polygons[1])
|
|
delta = get_delta(vector,polygons[0])
|
|
|
|
return delta
|
|
|
|
def get_delta_finish(course):
|
|
polygons = GeoPolygon.objects.filter(course=course).order_by("-order_in_course")
|
|
vector = get_dir_vector(polygons[0],polygons[1])
|
|
delta = get_delta(vector,polygons[0])
|
|
|
|
return delta
|
|
|
|
def course_length(course):
|
|
polygons = GeoPolygon.objects.filter(course=course).order_by("order_in_course")
|
|
|
|
totaldist = 0
|
|
for i in range(len(polygons)-1):
|
|
latitude1,longitude1 = polygon_coord_center(polygons[i])
|
|
latitude2,longitude2 = polygon_coord_center(polygons[i+1])
|
|
|
|
dist = geo_distance(latitude1,longitude1,
|
|
latitude2,longitude2,)
|
|
|
|
totaldist += 1000.*dist[0]
|
|
|
|
vector = get_dir_vector(polygons[0],polygons[1])
|
|
deltastart = get_delta(vector,polygons[0])
|
|
|
|
polygons = polygons.reverse()
|
|
vector = get_dir_vector(polygons[0],polygons[1])
|
|
deltafinish = get_delta(vector,polygons[0])
|
|
|
|
return int(totaldist-deltastart-deltafinish)
|
|
|
|
sexcategories = (
|
|
('male','male'),
|
|
('female','female'),
|
|
('not specified','not specified'),
|
|
)
|
|
weightcategories = (
|
|
('hwt','heavy-weight'),
|
|
('lwt','light-weight'),
|
|
)
|
|
|
|
|
|
# Extension of User with rowing specific data
|
|
class Rower(models.Model):
|
|
|
|
stravatypes = (
|
|
('Ride','Ride'),
|
|
('Kitesurf','Kitesurf'),
|
|
('Run','Run'),
|
|
('NordicSki','NordicSki'),
|
|
('Swim','Swim'),
|
|
('RockClimbing','RockClimbing'),
|
|
('Hike','Hike'),
|
|
('RollerSki','RollerSki'),
|
|
('Walk','Walk'),
|
|
('Rowing','Rowing'),
|
|
('AlpineSki','AlpineSki'),
|
|
('Snowboard','Snowboard'),
|
|
('BackcountrySki','BackcountrySki'),
|
|
('Snowshoe','Snowshoe'),
|
|
('Canoeing','Canoeing'),
|
|
('StairStepper','StairStepper'),
|
|
('Crossfit','Crossfit'),
|
|
('StandUpPaddling','StandUpPaddling'),
|
|
('EBikeRide','EBikeRide'),
|
|
('Surfing','Surfing'),
|
|
('Elliptical','Elliptical'),
|
|
('VirtualRide','VirtualRide'),
|
|
('IceSkate','IceSkate'),
|
|
('WeightTraining','WeightTraining'),
|
|
('InlineSkate','InlineSkate'),
|
|
('Windsurf','Windsurf'),
|
|
('Kayaking','Kayaking'),
|
|
('Workout','Workout'),
|
|
('Yoga','Yoga'),
|
|
)
|
|
user = models.OneToOneField(User)
|
|
gdproptin = models.BooleanField(default=False)
|
|
gdproptindate = models.DateTimeField(blank=True,null=True)
|
|
|
|
# Heart Rate Zone data
|
|
max = models.IntegerField(default=192,verbose_name="Max Heart Rate")
|
|
rest = models.IntegerField(default=48,verbose_name="Resting Heart Rate")
|
|
ut2 = models.IntegerField(default=105,verbose_name="UT2 band lower HR")
|
|
ut1 = models.IntegerField(default=146,verbose_name="UT1 band lower HR")
|
|
at = models.IntegerField(default=160,verbose_name="AT band lower HR")
|
|
tr = models.IntegerField(default=167,verbose_name="TR band lower HR")
|
|
an = models.IntegerField(default=180,verbose_name="AN band lower HR")
|
|
hrftp = models.IntegerField(default=0,verbose_name="FTP heart rate")
|
|
|
|
# Weight Category (for sync to C2)
|
|
weightcategory = models.CharField(default="hwt",
|
|
max_length=30,
|
|
choices=weightcategories)
|
|
|
|
sex = models.CharField(default="not specified",
|
|
max_length=30,
|
|
choices=sexcategories)
|
|
|
|
birthdate = models.DateField(null=True,blank=True)
|
|
# Power Zone Data
|
|
ftp = models.IntegerField(default=226,verbose_name="Functional Threshold Power")
|
|
|
|
p0 = models.FloatField(default=1.0,verbose_name="CP p1")
|
|
p1 = models.FloatField(default=1.0,verbose_name="CP p2")
|
|
p2 = models.FloatField(default=1.0,verbose_name="CP p3")
|
|
p3 = models.FloatField(default=1.0,verbose_name="CP p4")
|
|
cpratio = models.FloatField(default=1.0,verbose_name="CP fit ratio")
|
|
|
|
|
|
ep0 = models.FloatField(default=1.0,verbose_name="erg CP p1")
|
|
ep1 = models.FloatField(default=1.0,verbose_name="erg CP p2")
|
|
ep2 = models.FloatField(default=1.0,verbose_name="erg CP p3")
|
|
ep3 = models.FloatField(default=1.0,verbose_name="erg CP p4")
|
|
ecpratio = models.FloatField(default=1.0,verbose_name="erg CP fit ratio")
|
|
|
|
|
|
|
|
otwslack = models.IntegerField(default=0,verbose_name="OTW Power slack")
|
|
|
|
pw_ut2 = models.IntegerField(default=124,verbose_name="UT2 Power")
|
|
pw_ut1 = models.IntegerField(default=171,verbose_name="UT1 Power")
|
|
pw_at = models.IntegerField(default=203,verbose_name="AT Power")
|
|
pw_tr = models.IntegerField(default=237,verbose_name="TR Power")
|
|
pw_an = models.IntegerField(default=273,verbose_name="AN Power")
|
|
|
|
powerzones = PowerZonesField(default=['Rest',
|
|
'Pwr UT2',
|
|
'Pwr UT1',
|
|
'Pwr AT',
|
|
'Pwr TR',
|
|
'Pwr AN'])
|
|
|
|
# Site Settings
|
|
workflowleftpanel = TemplateListField(default=defaultleft)
|
|
workflowmiddlepanel = TemplateListField(default=defaultmiddle)
|
|
defaultlandingpage = models.CharField(default='workout_edit_view',
|
|
max_length=200,
|
|
choices=landingpages,
|
|
verbose_name="Default Landing Page")
|
|
|
|
# Access tokens
|
|
c2token = models.CharField(default='',max_length=200,blank=True,null=True)
|
|
tokenexpirydate = models.DateTimeField(blank=True,null=True)
|
|
c2refreshtoken = models.CharField(default='',max_length=200,blank=True,null=True)
|
|
c2_auto_export = models.BooleanField(default=False)
|
|
c2_auto_import = models.BooleanField(default=False)
|
|
sporttrackstoken = models.CharField(default='',max_length=200,blank=True,null=True)
|
|
sporttrackstokenexpirydate = models.DateTimeField(blank=True,null=True)
|
|
sporttracksrefreshtoken = models.CharField(default='',max_length=200,
|
|
blank=True,null=True)
|
|
sporttracks_auto_export = models.BooleanField(default=False)
|
|
underarmourtoken = models.CharField(default='',max_length=200,blank=True,null=True)
|
|
underarmourtokenexpirydate = models.DateTimeField(blank=True,null=True)
|
|
underarmourrefreshtoken = models.CharField(default='',max_length=200,
|
|
blank=True,null=True)
|
|
mapmyfitness_auto_export = models.BooleanField(default=False)
|
|
tptoken = models.CharField(default='',max_length=1000,blank=True,null=True)
|
|
tptokenexpirydate = models.DateTimeField(blank=True,null=True)
|
|
tprefreshtoken = models.CharField(default='',max_length=1000,
|
|
blank=True,null=True)
|
|
trainingpeaks_auto_export = models.BooleanField(default=False)
|
|
|
|
polartoken = models.CharField(default='',max_length=1000,blank=True,null=True)
|
|
polartokenexpirydate = models.DateTimeField(blank=True,null=True)
|
|
polarrefreshtoken = models.CharField(default='',max_length=1000,
|
|
blank=True,null=True)
|
|
polaruserid = models.IntegerField(default=0)
|
|
polar_auto_import = models.BooleanField(default=False)
|
|
|
|
stravatoken = models.CharField(default='',max_length=200,blank=True,null=True)
|
|
stravaexportas = models.CharField(default="Rowing",
|
|
max_length=30,
|
|
choices=stravatypes,
|
|
verbose_name="Export Workouts to Strava as")
|
|
|
|
strava_auto_export = models.BooleanField(default=False)
|
|
strava_auto_import = models.BooleanField(default=False)
|
|
runkeepertoken = models.CharField(default='',max_length=200,
|
|
blank=True,null=True)
|
|
runkeeper_auto_export = models.BooleanField(default=False)
|
|
|
|
# Plan
|
|
plans = (
|
|
('basic','basic'),
|
|
('pro','pro'),
|
|
('plan','plan'),
|
|
('coach','coach')
|
|
)
|
|
|
|
privacychoices = (
|
|
('visible','Visible'),
|
|
('hidden','Hidden'),
|
|
)
|
|
|
|
getemailnotifications = models.BooleanField(default=False,
|
|
verbose_name='Receive email notifications')
|
|
emailbounced = models.BooleanField(default=False,
|
|
verbose_name='Email Address Bounced')
|
|
|
|
getimportantemails = models.BooleanField(default=True,
|
|
verbose_name='Get Important Emails')
|
|
|
|
rowerplan = models.CharField(default='basic',max_length=30,
|
|
choices=plans)
|
|
|
|
planexpires = models.DateField(default=timezone.now)
|
|
teamplanexpires = models.DateField(default=timezone.now)
|
|
clubsize = models.IntegerField(default=0)
|
|
protrialexpires = models.DateField(blank=True,null=True)
|
|
plantrialexpires = models.DateField(blank=True,null=True)
|
|
|
|
|
|
# Friends/Team
|
|
friends = models.ManyToManyField("self",blank=True)
|
|
privacy = models.CharField(default='visible',max_length=30,
|
|
choices=privacychoices)
|
|
|
|
team = models.ManyToManyField(Team,blank=True)
|
|
|
|
# Export and Time Zone Settings
|
|
defaulttimezone = models.CharField(default='UTC',max_length=100,
|
|
choices=timezones,
|
|
verbose_name='Default Time Zone')
|
|
|
|
# Show flex chart notes
|
|
showfavoritechartnotes = models.BooleanField(default=True,
|
|
verbose_name='Show Notes for Favorite Charts')
|
|
|
|
def __str__(self):
|
|
return self.user.first_name+' '+self.user.last_name
|
|
|
|
def __unicode__(self):
|
|
return self.user.first_name+' '+self.user.last_name
|
|
|
|
def clean_email(self):
|
|
return self.user.email.lower()
|
|
|
|
class DeactivateUserForm(forms.ModelForm):
|
|
class Meta:
|
|
model = User
|
|
fields = ['is_active']
|
|
|
|
class DeleteUserForm(forms.ModelForm):
|
|
delete_user = forms.BooleanField(initial=False,
|
|
label='Remove my account and all data')
|
|
|
|
class Meta:
|
|
model = User
|
|
fields = []
|
|
|
|
@receiver(models.signals.post_save,sender=Rower)
|
|
def auto_delete_teams_on_change(sender, instance, **kwargs):
|
|
if instance.rowerplan != 'coach':
|
|
teams = Team.objects.filter(manager=instance.user)
|
|
for team in teams:
|
|
team.delete()
|
|
|
|
from rowers.metrics import axlabels
|
|
favchartlabelsx = axlabels.copy()
|
|
favchartlabelsy1 = axlabels.copy()
|
|
favchartlabelsy2 = axlabels.copy()
|
|
favchartlabelsy1.pop('None')
|
|
parchoicesy1 = list(sorted(favchartlabelsy1.items(), key = lambda x:x[1]))
|
|
parchoicesy2 = list(sorted(favchartlabelsy2.items(), key = lambda x:x[1]))
|
|
parchoicesx = list(sorted(favchartlabelsx.items(), key = lambda x:x[1]))
|
|
|
|
# Saving a chart as a favorite chart
|
|
class FavoriteChart(models.Model):
|
|
workouttypechoices = (
|
|
('ote','Erg/SkiErg'),
|
|
('otw','On The Water'),
|
|
('both','both')
|
|
)
|
|
|
|
plottypes = (
|
|
('line','Line Chart'),
|
|
('scatter','Scatter Chart')
|
|
)
|
|
|
|
yparam1 = models.CharField(max_length=50,choices=parchoicesy1,verbose_name='Y1')
|
|
yparam2 = models.CharField(max_length=50,choices=parchoicesy2,verbose_name='Y2',default='None',blank=True)
|
|
xparam = models.CharField(max_length=50,choices=parchoicesx,verbose_name='X')
|
|
plottype = models.CharField(max_length=50,choices=plottypes,
|
|
default='line',
|
|
verbose_name='Chart Type')
|
|
workouttype = models.CharField(max_length=50,choices=workouttypechoices,
|
|
default='both',
|
|
verbose_name='Workout Type')
|
|
reststrokes = models.BooleanField(default=True,verbose_name="Incl. Rest")
|
|
notes = models.CharField(max_length=300,verbose_name='Chart Notes',
|
|
default='Flex Chart Notes',blank=True)
|
|
user = models.ForeignKey(Rower)
|
|
|
|
|
|
class FavoriteForm(ModelForm):
|
|
class Meta:
|
|
model = FavoriteChart
|
|
fields = ['xparam','yparam1','yparam2',
|
|
'plottype','workouttype','reststrokes','notes']
|
|
# widgets = {
|
|
# 'notes': forms.Textarea,
|
|
# }
|
|
|
|
|
|
# To generate favorite chart forms on the fly
|
|
class BaseFavoriteFormSet(BaseFormSet):
|
|
def clean(self):
|
|
if any(self.errors):
|
|
return
|
|
|
|
for form in self.forms:
|
|
if form.cleaned_data:
|
|
xparam = form.cleaned_data['xparam']
|
|
yparam1 = form.cleaned_data['yparam1']
|
|
yparam2 = form.cleaned_data['yparam2']
|
|
plottype = form.cleaned_data['plottype']
|
|
reststrokes = form.cleaned_data['reststrokes']
|
|
|
|
if not xparam:
|
|
raise forms.ValidationError(
|
|
'Must have x parameter.',
|
|
code='missing_xparam'
|
|
)
|
|
|
|
if not yparam1:
|
|
raise forms.ValidationError(
|
|
'Must have Y1 parameter.',
|
|
code='missing_yparam1'
|
|
)
|
|
|
|
if not yparam2:
|
|
yparam2 = 'None'
|
|
|
|
class BasePlannedSessionFormSet(BaseFormSet):
|
|
def clean(self):
|
|
if any(self.serrors):
|
|
return
|
|
|
|
|
|
# Check if workout is owned by this user
|
|
def checkworkoutuser(user,workout):
|
|
if user.is_anonymous():
|
|
return False
|
|
try:
|
|
r = Rower.objects.get(user=user)
|
|
teams = workout.team.all()
|
|
if workout.user == r:
|
|
return True
|
|
elif teams:
|
|
for team in teams:
|
|
if user == team.manager:
|
|
return True
|
|
else:
|
|
return False
|
|
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)
|
|
distance = models.IntegerField(default=0)
|
|
name = models.CharField(max_length=150,blank=True)
|
|
country = models.CharField(max_length=150,blank=True)
|
|
notes = models.CharField(blank=True,max_length=200,verbose_name='Course Notes')
|
|
def __unicode__(self):
|
|
name = self.name
|
|
country = self.country
|
|
d = self.distance
|
|
if d == 0:
|
|
self.distance = course_length(self)
|
|
self.save()
|
|
d = self.distance
|
|
|
|
return u'{country} - {name} - {d}m'.format(
|
|
name=name,
|
|
country=country,
|
|
d = d,
|
|
)
|
|
|
|
class GeoCourseEditForm(ModelForm):
|
|
class Meta:
|
|
model = GeoCourse
|
|
fields = ['name','country','notes']
|
|
|
|
widgets = {
|
|
'notes': forms.Textarea,
|
|
}
|
|
|
|
class GeoPolygon(models.Model):
|
|
name = models.CharField(max_length=150,blank=True)
|
|
course = models.ForeignKey(GeoCourse, blank=True)
|
|
order_in_course = models.IntegerField(default=0)
|
|
|
|
def __unicode__(self):
|
|
name = self.name
|
|
coursename = self.course.name
|
|
|
|
return u'{coursename} - {name}'.format(
|
|
name=name,
|
|
coursename=coursename
|
|
)
|
|
|
|
|
|
# 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)
|
|
|
|
def a_week_from_now():
|
|
return timezone.now()+timezone.timedelta(days=7)
|
|
|
|
# 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)
|
|
|
|
def __unicode__(self):
|
|
date = self.date
|
|
name = self.name
|
|
ownerfirst = self.rower.user.first_name
|
|
ownerlast = self.rower.user.last_name
|
|
|
|
stri = u'{ownerfirst} {ownerlast} {d} {n}'.format(
|
|
ownerfirst = ownerfirst,
|
|
ownerlast = ownerlast,
|
|
d = date.strftime('%Y-%m-%d'),
|
|
n = name
|
|
)
|
|
|
|
return stri
|
|
|
|
class TrainingTargetForm(ModelForm):
|
|
class Meta:
|
|
model = TrainingTarget
|
|
fields = ['name','date','notes']
|
|
|
|
widgets = {
|
|
'date': AdminDateWidget()
|
|
}
|
|
|
|
|
|
# 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,null=True)
|
|
startdate = models.DateField(default=timezone.now)
|
|
enddate = models.DateField(
|
|
default=half_year_from_now)
|
|
|
|
def __unicode__(self):
|
|
name = self.name
|
|
startdate = self.startdate
|
|
enddate = self.enddate
|
|
firstname = self.rower.user.first_name
|
|
lastname = self.rower.user.last_name
|
|
|
|
stri = u'Training Plan for {firstname} {lastname} {s} - {e}: {name}'.format(
|
|
s = startdate.strftime('%Y-%m-%d'),
|
|
e = enddate.strftime('%Y-%m-%d'),
|
|
firstname = firstname,
|
|
lastname = lastname,
|
|
name=name
|
|
)
|
|
|
|
return stri
|
|
|
|
def save(self, *args, **kwargs):
|
|
if self.enddate < self.startdate:
|
|
startdate = self.startdate
|
|
enddate = self.enddate
|
|
self.startdate = enddate
|
|
self.enddate = startdate
|
|
|
|
otherplans = TrainingPlan.objects.filter(rower=self.rower).exclude(pk=self.pk).order_by("-startdate")
|
|
|
|
for otherplan in otherplans:
|
|
if otherplan.startdate <= self.enddate and otherplan.startdate >= self.startdate:
|
|
self.enddate = otherplan.startdate-datetime.timedelta(days=1)
|
|
if otherplan.enddate >= self.startdate and otherplan.enddate <= self.enddate:
|
|
self.startdate = otherplan.enddate+datetime.timedelta(days=1)
|
|
|
|
if not self.enddate <= self.startdate:
|
|
super(TrainingPlan,self).save(*args, **kwargs)
|
|
|
|
macrocycles = TrainingMacroCycle.objects.filter(plan = self)
|
|
if not macrocycles:
|
|
m = TrainingMacroCycle(
|
|
plan = self,
|
|
name = 'Filler',
|
|
startdate = self.startdate,
|
|
enddate = self.enddate,
|
|
)
|
|
|
|
m.save()
|
|
|
|
else:
|
|
createmacrofillers(self)
|
|
|
|
class TrainingPlanForm(ModelForm):
|
|
class Meta:
|
|
model = TrainingPlan
|
|
fields = ['name','target','startdate','enddate']
|
|
|
|
widgets = {
|
|
'startdate': AdminDateWidget(),
|
|
'enddate': AdminDateWidget()
|
|
}
|
|
|
|
def __init__(self,*args, **kwargs):
|
|
targets = kwargs.pop('targets',None)
|
|
super(TrainingPlanForm, self).__init__(*args, **kwargs)
|
|
|
|
if targets:
|
|
targetchoices = [(x.id,x) for x in targets]
|
|
targetchoices.append((None,'---'))
|
|
self.fields['target'].choices = targetchoices
|
|
|
|
|
|
cycletypechoices = (
|
|
('filler','System Defined'),
|
|
('userdefined','User Defined')
|
|
)
|
|
|
|
def createmacrofillers(plan):
|
|
fillers = TrainingMacroCycle.objects.filter(
|
|
plan = plan, type = 'filler'
|
|
)
|
|
|
|
for f in fillers:
|
|
f.delete()
|
|
|
|
cycles = TrainingMacroCycle.objects.filter(
|
|
plan = plan
|
|
).order_by("-startdate")
|
|
|
|
if not cycles:
|
|
macr = TrainingMacroCycle(
|
|
plan=plan,
|
|
startdate = plan.startdate,
|
|
enddate = plan.enddate,
|
|
type='filler',
|
|
name='Filler'
|
|
)
|
|
macr.save()
|
|
|
|
thedate = plan.enddate
|
|
while cycles:
|
|
if cycles[0].enddate < thedate:
|
|
macr = TrainingMacroCycle(
|
|
plan=plan,
|
|
startdate = cycles[0].enddate+datetime.timedelta(days=1),
|
|
enddate = thedate,
|
|
type='filler',
|
|
name='Filler'
|
|
)
|
|
macr.save()
|
|
thedate = cycles[0].startdate-datetime.timedelta(days=1)
|
|
cycles = cycles[1:]
|
|
|
|
cycles = TrainingMacroCycle.objects.filter(
|
|
plan = plan
|
|
).order_by("startdate")
|
|
|
|
if cycles[0].startdate > plan.startdate:
|
|
macr = TrainingMacroCycle(
|
|
plan=plan,
|
|
startdate = plan.startdate,
|
|
enddate = cycles[0].startdate-datetime.timedelta(days=1),
|
|
type='filler',
|
|
name='Filler'
|
|
)
|
|
macr.save()
|
|
|
|
def createmesofillers(plan):
|
|
fillers = TrainingMesoCycle.objects.filter(
|
|
plan = plan, type = 'filler'
|
|
)
|
|
|
|
for f in fillers:
|
|
f.delete()
|
|
|
|
cycles = TrainingMesoCycle.objects.filter(
|
|
plan = plan
|
|
).order_by("-startdate")
|
|
|
|
if not cycles:
|
|
macr = TrainingMesoCycle(
|
|
plan = plan,
|
|
startdate = plan.startdate,
|
|
enddate = plan.enddate,
|
|
type='filler',
|
|
name='Filler'
|
|
)
|
|
macr.save()
|
|
|
|
thedate = plan.enddate
|
|
while cycles:
|
|
if cycles[0].enddate < thedate:
|
|
macr = TrainingMesoCycle(
|
|
plan=plan,
|
|
startdate = cycles[0].enddate+datetime.timedelta(days=1),
|
|
enddate = thedate,
|
|
type='filler',
|
|
name='Filler'
|
|
)
|
|
macr.save()
|
|
thedate = cycles[0].startdate-datetime.timedelta(days=1)
|
|
cycles = cycles[1:]
|
|
|
|
cycles = TrainingMesoCycle.objects.filter(
|
|
plan = plan
|
|
).order_by("startdate")
|
|
|
|
if cycles[0].startdate > plan.startdate:
|
|
macr = TrainingMesoCycle(
|
|
plan=plan,
|
|
startdate = plan.startdate,
|
|
enddate = cycles[0].startdate-datetime.timedelta(days=1),
|
|
type='filler',
|
|
name='Filler'
|
|
)
|
|
macr.save()
|
|
|
|
|
|
def createmicrofillers(plan):
|
|
fillers = TrainingMicroCycle.objects.filter(
|
|
plan = plan, type = 'filler'
|
|
)
|
|
|
|
for f in fillers:
|
|
f.delete()
|
|
|
|
cycles = TrainingMicroCycle.objects.filter(
|
|
plan = plan
|
|
).order_by("-startdate")
|
|
|
|
if not cycles:
|
|
macr = TrainingMicroCycle(
|
|
plan=plan,
|
|
startdate = plan.startdate,
|
|
enddate = plan.enddate,
|
|
type='filler',
|
|
name='Filler'
|
|
)
|
|
macr.save()
|
|
|
|
thedate = plan.enddate
|
|
while cycles:
|
|
if cycles[0].enddate < thedate:
|
|
macr = TrainingMicroCycle(
|
|
plan=plan,
|
|
startdate = cycles[0].enddate+datetime.timedelta(days=1),
|
|
enddate = thedate,
|
|
type='filler',
|
|
name='Filler'
|
|
)
|
|
macr.save()
|
|
thedate = cycles[0].startdate-datetime.timedelta(days=1)
|
|
cycles = cycles[1:]
|
|
|
|
cycles = TrainingMicroCycle.objects.filter(
|
|
plan = plan
|
|
).order_by("startdate")
|
|
|
|
if cycles[0].startdate > plan.startdate:
|
|
macr = TrainingMicroCycle(
|
|
plan=plan,
|
|
startdate = plan.startdate,
|
|
enddate = cycles[0].startdate-datetime.timedelta(days=1),
|
|
type='filler',
|
|
name='Filler'
|
|
)
|
|
macr.save()
|
|
|
|
def microcyclecheckdates(plan):
|
|
cycles = TrainingMicroCycle.objects.filter(
|
|
plan=plan
|
|
).order_by("-startdate")
|
|
|
|
thedate = plan.enddate
|
|
while cycles:
|
|
if cycles[0].enddate < plan.startdate:
|
|
cycles[0].delete()
|
|
if cycles[0].startdate > plan.enddate:
|
|
cycles[0].delete()
|
|
if cycles[0].enddate > thedate:
|
|
cycles[0].enddate = thedate
|
|
cycles[0].save()
|
|
thedate = cycles[0].startdate-datetime.timedelta(days=1)
|
|
cycles = cycles[1:]
|
|
|
|
cycles = TrainingMicroCycle.objects.filter(
|
|
plan=plan
|
|
).order_by("startdate")
|
|
|
|
thedate = plan.startdate
|
|
while cycles:
|
|
if cycles[0].startdate < thedate:
|
|
cycles[0].startdate = thedate
|
|
cycles[0].save()
|
|
try:
|
|
thedate = cycles[1].startdate-datetime.timedelta(days=1)
|
|
except IndexError:
|
|
pass
|
|
cycles = cycles[1:]
|
|
|
|
def mesocyclecheckdates(plan):
|
|
cycles = TrainingMesoCycle.objects.filter(
|
|
plan=plan
|
|
).order_by("-startdate")
|
|
|
|
thedate = plan.enddate
|
|
while cycles:
|
|
if cycles[0].enddate < plan.startdate:
|
|
cycles[0].delete()
|
|
if cycles[0].startdate > plan.enddate:
|
|
cycles[0].delete()
|
|
if cycles[0].enddate > thedate:
|
|
cycles[0].enddate = thedate
|
|
cycles[0].save()
|
|
thedate = cycles[0].startdate-datetime.timedelta(days=1)
|
|
cycles = cycles[1:]
|
|
|
|
cycles = TrainingMesoCycle.objects.filter(
|
|
plan=plan
|
|
).order_by("startdate")
|
|
|
|
thedate = plan.startdate
|
|
while cycles:
|
|
if cycles[0].startdate < thedate:
|
|
cycles[0].startdate = thedate
|
|
cycles[0].save()
|
|
try:
|
|
thedate = cycles[1].startdate-datetime.timedelta(days=1)
|
|
except IndexError:
|
|
pass
|
|
cycles = cycles[1:]
|
|
|
|
def macrocyclecheckdates(plan):
|
|
cycles = TrainingMacroCycle.objects.filter(
|
|
plan=plan
|
|
).order_by("-startdate")
|
|
|
|
thedate = plan.enddate
|
|
while cycles:
|
|
if cycles[0].enddate < plan.startdate:
|
|
cycles[0].delete()
|
|
if cycles[0].startdate > plan.enddate:
|
|
cycles[0].delete()
|
|
if cycles[0].enddate > thedate:
|
|
cycles[0].enddate = thedate
|
|
cycles[0].save()
|
|
thedate = cycles[0].startdate-datetime.timedelta(days=1)
|
|
cycles = cycles[1:]
|
|
|
|
cycles = TrainingMacroCycle.objects.filter(
|
|
plan=plan
|
|
).order_by("startdate")
|
|
|
|
thedate = plan.startdate
|
|
while cycles:
|
|
if cycles[0].startdate < thedate:
|
|
cycles[0].startdate = thedate
|
|
cycles[0].save()
|
|
try:
|
|
thedate = cycles[1].startdate-datetime.timedelta(days=1)
|
|
except IndexError:
|
|
pass
|
|
cycles = cycles[1:]
|
|
|
|
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)
|
|
|
|
plantime = models.IntegerField(default=0,verbose_name='Planned Duration')
|
|
plandistance = models.IntegerField(default=0,verbose_name='Planned Distance')
|
|
planrscore = models.IntegerField(default=0,verbose_name='Planned rScore')
|
|
plantrimp = models.IntegerField(default=0,verbose_name='Planned TRIMP')
|
|
|
|
actualtime = models.IntegerField(default=0,verbose_name='Actual Duration')
|
|
actualdistance = models.IntegerField(default=0,verbose_name='Actual Distance')
|
|
actualrscore = models.IntegerField(default=0,verbose_name='Actual rScore')
|
|
actualtrimp = models.IntegerField(default=0,verbose_name='Actual TRIMP')
|
|
|
|
def __unicode__(self):
|
|
stri = 'Macro Cycle - {n} ({sd} - {ed})'.format(
|
|
n = self.name,
|
|
sd = self.startdate,
|
|
ed = self.enddate,
|
|
)
|
|
|
|
return stri
|
|
|
|
def save(self, *args, **kwargs):
|
|
if self.enddate < self.startdate:
|
|
startdate = self.startdate
|
|
enddate = self.enddate
|
|
self.startdate = enddate
|
|
self.enddate = startdate
|
|
|
|
fillers = TrainingMacroCycle.objects.filter(
|
|
plan=self.plan,type='filler')
|
|
for f in fillers:
|
|
f.delete()
|
|
|
|
if self.enddate > self.plan.enddate:
|
|
self.enddate = self.plan.enddate
|
|
|
|
if self.startdate < self.plan.startdate:
|
|
self.startdate = self.plan.startdate
|
|
|
|
othercycles = TrainingMacroCycle.objects.filter(
|
|
plan=self.plan).exclude(pk=self.pk).order_by("-startdate")
|
|
|
|
for othercycle in othercycles:
|
|
if othercycle.startdate <= self.enddate and othercycle.startdate >= self.startdate:
|
|
self.enddate = othercycle.startdate-datetime.timedelta(days=1)
|
|
|
|
if othercycle.enddate >= self.startdate and othercycle.enddate <= self.enddate:
|
|
self.startdate = othercycle.enddate+datetime.timedelta(days=1)
|
|
|
|
if not self.enddate <= self.startdate:
|
|
super(TrainingMacroCycle,self).save(*args, **kwargs)
|
|
|
|
|
|
mesocycles = TrainingMesoCycle.objects.filter(plan = self)
|
|
if not mesocycles:
|
|
meso = TrainingMesoCycle(
|
|
plan = self,
|
|
name = 'Filler',
|
|
startdate = self.startdate,
|
|
enddate = self.enddate,
|
|
)
|
|
|
|
meso.save()
|
|
else:
|
|
createmesofillers(self)
|
|
|
|
class TrainingMacroCycleForm(ModelForm):
|
|
class Meta:
|
|
model = TrainingMacroCycle
|
|
fields = ['name','startdate','enddate','notes']
|
|
|
|
widgets = {
|
|
'startdate': AdminDateWidget(),
|
|
'enddate': AdminDateWidget()
|
|
}
|
|
|
|
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)
|
|
|
|
plantime = models.IntegerField(default=0,verbose_name='Planned Duration')
|
|
plandistance = models.IntegerField(default=0,verbose_name='Planned Distance')
|
|
planrscore = models.IntegerField(default=0,verbose_name='Planned rScore')
|
|
plantrimp = models.IntegerField(default=0,verbose_name='Planned TRIMP')
|
|
|
|
actualtime = models.IntegerField(default=0,verbose_name='Actual Duration')
|
|
actualdistance = models.IntegerField(default=0,verbose_name='Actual Distance')
|
|
actualrscore = models.IntegerField(default=0,verbose_name='Actual rScore')
|
|
actualtrimp = models.IntegerField(default=0,verbose_name='Actual TRIMP')
|
|
|
|
|
|
def __unicode__(self):
|
|
stri = 'Meso Cycle - {n} ({sd} - {ed})'.format(
|
|
n = self.name,
|
|
sd = self.startdate,
|
|
ed = self.enddate,
|
|
)
|
|
|
|
return stri
|
|
|
|
def save(self, *args, **kwargs):
|
|
if self.enddate < self.startdate:
|
|
startdate = self.startdate
|
|
enddate = self.enddate
|
|
self.startdate = enddate
|
|
self.enddate = startdate
|
|
|
|
fillers = TrainingMesoCycle.objects.filter(
|
|
plan=self.plan,type='filler')
|
|
for f in fillers:
|
|
f.delete()
|
|
|
|
if self.enddate > self.plan.enddate:
|
|
self.enddate = self.plan.enddate
|
|
|
|
if self.startdate < self.plan.startdate:
|
|
self.startdate = self.plan.startdate
|
|
|
|
othercycles = TrainingMesoCycle.objects.filter(
|
|
plan=self.plan).exclude(pk=self.pk).order_by("-startdate")
|
|
|
|
for othercycle in othercycles:
|
|
if othercycle.startdate <= self.enddate and othercycle.startdate >= self.startdate:
|
|
self.enddate = othercycle.startdate-datetime.timedelta(days=1)
|
|
|
|
if othercycle.enddate >= self.startdate and othercycle.enddate <= self.enddate:
|
|
self.startdate = othercycle.enddate+datetime.timedelta(days=1)
|
|
|
|
if not self.enddate <= self.startdate:
|
|
super(TrainingMesoCycle,self).save(*args, **kwargs)
|
|
|
|
microcycles = TrainingMicroCycle.objects.filter(plan = self)
|
|
if not microcycles:
|
|
micro = TrainingMicroCycle(
|
|
plan = self,
|
|
name = 'Filler',
|
|
startdate = self.startdate,
|
|
enddate = self.enddate,
|
|
)
|
|
|
|
micro.save()
|
|
else:
|
|
createmicrofillers(self)
|
|
|
|
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)
|
|
|
|
plantime = models.IntegerField(default=0,verbose_name='Planned Duration')
|
|
plandistance = models.IntegerField(default=0,verbose_name='Planned Distance')
|
|
planrscore = models.IntegerField(default=0,verbose_name='Planned rScore')
|
|
plantrimp = models.IntegerField(default=0,verbose_name='Planned TRIMP')
|
|
|
|
actualtime = models.IntegerField(default=0,verbose_name='Actual Duration')
|
|
actualdistance = models.IntegerField(default=0,verbose_name='Actual Distance')
|
|
actualrscore = models.IntegerField(default=0,verbose_name='Actual rScore')
|
|
actualtrimp = models.IntegerField(default=0,verbose_name='Actual TRIMP')
|
|
|
|
|
|
|
|
|
|
def __unicode__(self):
|
|
stri = 'Micro Cycle - {n} ({sd} - {ed})'.format(
|
|
n = self.name,
|
|
sd = self.startdate,
|
|
ed = self.enddate,
|
|
)
|
|
|
|
return stri
|
|
|
|
def save(self, *args, **kwargs):
|
|
if self.enddate < self.startdate:
|
|
startdate = self.startdate
|
|
enddate = self.enddate
|
|
self.startdate = enddate
|
|
self.enddate = startdate
|
|
|
|
fillers = TrainingMicroCycle.objects.filter(
|
|
plan=self.plan,type='filler')
|
|
for f in fillers:
|
|
f.delete()
|
|
|
|
if self.enddate > self.plan.enddate:
|
|
self.enddate = self.plan.enddate
|
|
|
|
if self.startdate < self.plan.startdate:
|
|
self.startdate = self.plan.startdate
|
|
|
|
othercycles = TrainingMicroCycle.objects.filter(
|
|
plan=self.plan).exclude(pk=self.pk).order_by("-startdate")
|
|
|
|
for othercycle in othercycles:
|
|
if othercycle.startdate <= self.enddate and othercycle.startdate >= self.startdate:
|
|
self.enddate = othercycle.startdate-datetime.timedelta(days=1)
|
|
|
|
if othercycle.enddate >= self.startdate and othercycle.enddate <= self.enddate:
|
|
self.startdate = othercycle.enddate+datetime.timedelta(days=1)
|
|
|
|
|
|
|
|
if not self.enddate <= self.startdate:
|
|
super(TrainingMicroCycle,self).save(*args, **kwargs)
|
|
|
|
class TrainingMesoCycleForm(ModelForm):
|
|
class Meta:
|
|
model = TrainingMesoCycle
|
|
fields = ['name','startdate','enddate','notes']
|
|
|
|
widgets = {
|
|
'startdate': AdminDateWidget(),
|
|
'enddate': AdminDateWidget()
|
|
}
|
|
|
|
class TrainingMicroCycleForm(ModelForm):
|
|
class Meta:
|
|
model = TrainingMicroCycle
|
|
fields = ['name','startdate','enddate','notes']
|
|
|
|
widgets = {
|
|
'startdate': AdminDateWidget(),
|
|
'enddate': AdminDateWidget()
|
|
}
|
|
|
|
|
|
# model for Planned Session (Workout, Challenge, Test)
|
|
class PlannedSession(models.Model):
|
|
|
|
sessiontypechoices = (
|
|
('session','Training Session'),
|
|
('challenge','Challenge'),
|
|
('test','Mandatory Test'),
|
|
('cycletarget','Cycle Target'),
|
|
('coursetest','OTW test over a course'),
|
|
('race','Virtual Race'),
|
|
)
|
|
|
|
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'),
|
|
('m','meters'),
|
|
('None',None),
|
|
)
|
|
|
|
manager = models.ForeignKey(User)
|
|
course = models.ForeignKey(GeoCourse,blank=True,null=True,
|
|
verbose_name='OTW Course')
|
|
|
|
name = models.CharField(max_length=150,blank=True,
|
|
verbose_name='Name')
|
|
|
|
comment = models.TextField(max_length=500,blank=True,
|
|
)
|
|
|
|
startdate = models.DateField(default=timezone.now,
|
|
verbose_name='On or After')
|
|
|
|
enddate = models.DateField(default=a_week_from_now,
|
|
verbose_name='On or Before')
|
|
|
|
preferreddate = models.DateField(default=a_week_from_now,
|
|
verbose_name='Preferred 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,
|
|
verbose_name='Criteria')
|
|
|
|
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='time',
|
|
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
|
|
|
|
def save(self, *args, **kwargs):
|
|
# sort units
|
|
if self.sessionmode == 'distance':
|
|
if self.sessionunit not in ['m','km']:
|
|
self.sessionunit = 'm'
|
|
elif self.sessionmode == 'time':
|
|
self.sessionunit = 'min'
|
|
else:
|
|
self.sessionunit = 'None'
|
|
|
|
if self.sessiontype == 'test':
|
|
if self.sessionmode not in ['distance','time']:
|
|
if self.sessionvalue < 100:
|
|
self.sessionmode = 'time'
|
|
self.sessionunit = 'min'
|
|
else:
|
|
self.sessionmode = 'distance'
|
|
self.sessionunit = 'm'
|
|
self.criterium = 'exact'
|
|
if self.sessiontype == 'coursetest' or self.sessiontype == 'race':
|
|
self.sessionmode = 'distance'
|
|
self.sessionunit = 'm'
|
|
self.criterium = 'none'
|
|
if self.course == None:
|
|
self.course = GeoCourse.objects.all()[0]
|
|
self.sessionvalue = self.course.distance
|
|
elif self.sessiontype != 'coursetest' and self.sessiontype != 'race':
|
|
self.course = None
|
|
|
|
if self.enddate < self.startdate:
|
|
self.enddate = self.startdate
|
|
|
|
if self.preferreddate > self.enddate:
|
|
self.preferreddate = self.enddate
|
|
if self.preferreddate < self.startdate:
|
|
self.preferreddate = self.startdate
|
|
|
|
super(PlannedSession,self).save(*args, **kwargs)
|
|
|
|
from django.core.validators import RegexValidator,validate_email
|
|
|
|
registerchoices = (
|
|
('windowstart','Start of Race Window'),
|
|
('windowend','End of Race Window'),
|
|
('deadline','Evaluation Closure Deadline'),
|
|
('manual','Manual - select below'),
|
|
)
|
|
|
|
class VirtualRace(PlannedSession):
|
|
# has_registration = models.BooleanField(default=False)
|
|
registration_form = models.CharField(
|
|
max_length=100,
|
|
default='windowstart',
|
|
choices=registerchoices,
|
|
verbose_name='Registration Closure Quick Selector'
|
|
)
|
|
registration_closure = models.DateTimeField(blank=True,null=True)
|
|
evaluation_closure = models.DateTimeField(blank=True,null=True)
|
|
start_time = models.TimeField(blank=True,null=True)
|
|
end_time = models.TimeField(blank=True,null=True)
|
|
country = models.CharField(max_length=100,blank=True)
|
|
|
|
timezone = models.CharField(default='UTC',
|
|
choices=timezones,
|
|
max_length=100)
|
|
|
|
phone_regex = RegexValidator(
|
|
regex=r'^\+?1?\d{9,15}$',
|
|
message="Phone number must be entered in the format: '+999999999'. Up to 15 digits allowed."
|
|
)
|
|
|
|
contact_phone = models.CharField(validators=[phone_regex], max_length=17, blank=True)
|
|
|
|
contact_email = models.EmailField(max_length=254,
|
|
validators=[validate_email],blank=True)
|
|
|
|
def __unicode__(self):
|
|
|
|
name = self.name
|
|
startdate = self.startdate
|
|
enddate = self.enddate
|
|
|
|
stri = u'Virtual Race {n}'.format(
|
|
n = name,
|
|
)
|
|
|
|
return stri
|
|
|
|
|
|
def save(self, *args, **kwargs):
|
|
# test race window logic
|
|
|
|
start_time = self.start_time
|
|
start_date = self.startdate
|
|
startdatetime = datetime.datetime.combine(start_date,start_time)
|
|
startdatetime = pytz.timezone(self.timezone).localize(
|
|
startdatetime
|
|
)
|
|
|
|
end_time = self.end_time
|
|
end_date = self.enddate
|
|
enddatetime = datetime.datetime.combine(end_date,end_time)
|
|
enddatetime = pytz.timezone(self.timezone).localize(
|
|
enddatetime
|
|
)
|
|
|
|
if startdatetime > enddatetime:
|
|
self.start_time = end_time
|
|
self.startdate = end_date
|
|
self.end_time = start_time
|
|
self.enddate = start_date
|
|
enddatetime = startdatetime
|
|
|
|
if self.evaluation_closure < enddatetime:
|
|
self.evaluation_closure = enddatetime + timezone.timedelta(days=1)
|
|
|
|
super(VirtualRace,self).save(*args, **kwargs)
|
|
|
|
|
|
# Date input utility
|
|
class DateInput(forms.DateInput):
|
|
input_type = 'date'
|
|
|
|
class PlannedSessionForm(ModelForm):
|
|
|
|
class Meta:
|
|
model = PlannedSession
|
|
fields = ['startdate',
|
|
'enddate',
|
|
'preferreddate',
|
|
'name',
|
|
'sessiontype',
|
|
'sessionmode',
|
|
'criterium',
|
|
'sessionvalue',
|
|
'sessionunit',
|
|
'course',
|
|
'comment',
|
|
]
|
|
|
|
dateTimeOptions = {
|
|
'format': 'yyyy-mm-dd',
|
|
'autoclose': True,
|
|
}
|
|
|
|
widgets = {
|
|
'comment': forms.Textarea,
|
|
'startdate': AdminDateWidget(),
|
|
'enddate': AdminDateWidget(),
|
|
'preferreddate': AdminDateWidget(),
|
|
}
|
|
|
|
def __init__(self,*args,**kwargs):
|
|
super(PlannedSessionForm, self).__init__(*args, **kwargs)
|
|
self.fields['course'].queryset = GeoCourse.objects.all().order_by("country","name")
|
|
|
|
def get_course_timezone(course):
|
|
polygons = GeoPolygon.objects.filter(course = course)
|
|
points = GeoPoint.objects.filter(polygon = polygons[0])
|
|
lat = points[0].latitude
|
|
lon = points[0].longitude
|
|
|
|
tf = TimezoneFinder()
|
|
try:
|
|
timezone_str = tf.timezone_at(lng=lon,lat=lat)
|
|
except ValueError:
|
|
timezone_str = 'UTC'
|
|
|
|
if timezone_str is None:
|
|
timezone_str = tf.closest_timezone_at(lng=lon,lat=lat)
|
|
if timezone_str is None:
|
|
timezone_str = 'UTC'
|
|
|
|
return timezone_str
|
|
|
|
|
|
class VirtualRaceForm(ModelForm):
|
|
course = forms.ModelChoiceField(queryset = GeoCourse.objects, empty_label=None)
|
|
registration_closure = forms.SplitDateTimeField(widget=AdminSplitDateTime(),required=False)
|
|
evaluation_closure = forms.SplitDateTimeField(widget=AdminSplitDateTime(),required=True)
|
|
|
|
|
|
class Meta:
|
|
model = VirtualRace
|
|
fields = [
|
|
'name',
|
|
'startdate',
|
|
'start_time',
|
|
'enddate',
|
|
'end_time',
|
|
# 'has_registration',
|
|
'registration_form',
|
|
'registration_closure',
|
|
'evaluation_closure',
|
|
'course',
|
|
'comment',
|
|
'contact_phone',
|
|
'contact_email',
|
|
]
|
|
|
|
dateTimeOptions = {
|
|
'format': 'yyyy-mm-dd',
|
|
'autoclose': True,
|
|
}
|
|
|
|
widgets = {
|
|
'comment': forms.Textarea,
|
|
'startdate': AdminDateWidget(),
|
|
'enddate': AdminDateWidget(),
|
|
'start_time': AdminTimeWidget(),
|
|
'end_time': AdminTimeWidget(),
|
|
'registration_closure':AdminSplitDateTime(),
|
|
'evaluation_closure':AdminSplitDateTime(),
|
|
}
|
|
|
|
def __init__(self,*args,**kwargs):
|
|
super(VirtualRaceForm, self).__init__(*args, **kwargs)
|
|
self.fields['course'].queryset = GeoCourse.objects.all().order_by("country","name")
|
|
|
|
|
|
def clean(self):
|
|
cd = self.cleaned_data
|
|
course = cd['course']
|
|
geocourse = GeoCourse.objects.get(id=course.id)
|
|
timezone_str = get_course_timezone(geocourse)
|
|
|
|
start_time = cd['start_time']
|
|
if start_time is None:
|
|
raise forms.ValidationError(
|
|
'Must have start time',
|
|
code='missing_yparam1'
|
|
)
|
|
|
|
start_date = cd['startdate']
|
|
startdatetime = datetime.datetime.combine(start_date,start_time)
|
|
startdatetime = pytz.timezone(timezone_str).localize(
|
|
startdatetime
|
|
)
|
|
|
|
end_time = cd['end_time']
|
|
if end_time is None:
|
|
raise forms.ValidationError(
|
|
'Must have end time',
|
|
code='missing endtime'
|
|
)
|
|
|
|
end_date = cd['enddate']
|
|
enddatetime = datetime.datetime.combine(end_date,end_time)
|
|
enddatetime = pytz.timezone(timezone_str).localize(
|
|
enddatetime
|
|
)
|
|
|
|
registration_closure = cd['registration_closure']
|
|
|
|
registration_form = cd['registration_form']
|
|
|
|
try:
|
|
evaluation_closure = cd['evaluation_closure']
|
|
except KeyError:
|
|
evaluation_closure = enddatetime+datetime.timedelta(days=1)
|
|
cd['evaluation_closure'] = evaluation_closure
|
|
|
|
if registration_form == 'manual':
|
|
try:
|
|
registration_closure = pytz.timezone(
|
|
timezone_str
|
|
).localize(
|
|
registration_closure.replace(tzinfo=None)
|
|
)
|
|
except AttributeError:
|
|
registration_closure = startdatetime
|
|
elif registration_form == 'windowstart':
|
|
registration_closure = startdatetime
|
|
elif registration_form == 'windowend':
|
|
registration_closure = enddatetime
|
|
else:
|
|
registration_closure = evaluation_closure
|
|
|
|
|
|
if registration_closure <= timezone.now():
|
|
raise forms.ValidationError("Registration Closure cannot be in the past")
|
|
|
|
|
|
if startdatetime > enddatetime:
|
|
raise forms.ValidationError("The Start of the Race Window should be before the End of the Race Window")
|
|
|
|
|
|
if cd['evaluation_closure'] <= enddatetime:
|
|
raise forms.ValidationError("Evaluation closure deadline should be after the Race Window closes")
|
|
|
|
if cd['evaluation_closure'] <= timezone.now():
|
|
raise forms.ValidationError("Evaluation closure cannot be in the past")
|
|
|
|
|
|
return cd
|
|
|
|
class PlannedSessionFormSmall(ModelForm):
|
|
|
|
class Meta:
|
|
model = PlannedSession
|
|
fields = ['startdate',
|
|
'enddate',
|
|
'preferreddate',
|
|
'name',
|
|
'sessiontype',
|
|
'sessionmode',
|
|
'criterium',
|
|
'sessionvalue',
|
|
'sessionunit',
|
|
'manager',
|
|
]
|
|
|
|
dateTimeOptions = {
|
|
'format': 'yyyy-mm-dd',
|
|
'autoclose': True,
|
|
}
|
|
|
|
widgets = {
|
|
'startdate': DateInput(attrs={'size':10}),
|
|
'enddate': DateInput(attrs={'size':10}),
|
|
'preferreddate': DateInput(attrs={'size':10}),
|
|
'name': forms.TextInput(attrs={'size':10}),
|
|
'sessionvalue': forms.TextInput(attrs={'style':'width:5em',
|
|
'type':'number'}),
|
|
'manager': forms.HiddenInput(),
|
|
}
|
|
|
|
boattypes = types.boattypes
|
|
|
|
# Workout
|
|
class Workout(models.Model):
|
|
workouttypes = types.workouttypes
|
|
workoutsources = types.workoutsources
|
|
privacychoices = types.privacychoices
|
|
|
|
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,
|
|
verbose_name='Exercise/Boat Class')
|
|
workoutsource = models.CharField(max_length=100,
|
|
default='unknown')
|
|
boattype = models.CharField(choices=boattypes,max_length=50,
|
|
default='1x',
|
|
verbose_name = 'Boat Type')
|
|
starttime = models.TimeField(blank=True,null=True)
|
|
startdatetime = models.DateTimeField(blank=True,null=True)
|
|
timezone = models.CharField(default='UTC',
|
|
choices=timezones,
|
|
max_length=100)
|
|
distance = models.IntegerField(default=0,blank=True)
|
|
duration = models.TimeField(default=1,blank=True)
|
|
trimp = models.IntegerField(default=-1,blank=True)
|
|
rscore = models.IntegerField(default=-1,blank=True)
|
|
hrtss = models.IntegerField(default=-1,blank=True)
|
|
normp = models.IntegerField(default=-1,blank=True)
|
|
normv = models.FloatField(default=-1,blank=True)
|
|
normw = models.FloatField(default=-1,blank=True)
|
|
weightcategory = models.CharField(
|
|
default="hwt",
|
|
max_length=10,
|
|
choices=weightcategories,
|
|
verbose_name='Weight Category')
|
|
weightvalue = models.FloatField(default=80.0,blank=True,verbose_name = 'Average Crew Weight (kg)')
|
|
csvfilename = models.CharField(blank=True,max_length=150)
|
|
uploadedtoc2 = models.IntegerField(default=0)
|
|
averagehr = models.IntegerField(blank=True,null=True)
|
|
maxhr = models.IntegerField(blank=True,null=True)
|
|
uploadedtostrava = models.IntegerField(default=0)
|
|
uploadedtosporttracks = models.IntegerField(default=0)
|
|
uploadedtounderarmour = models.IntegerField(default=0)
|
|
uploadedtotp = models.IntegerField(default=0)
|
|
uploadedtorunkeeper = models.IntegerField(default=0)
|
|
forceunit = models.CharField(default='lbs',
|
|
choices = (
|
|
('lbs','lbs'),
|
|
('N','N')
|
|
),
|
|
max_length=100)
|
|
|
|
# empower stuff
|
|
inboard = models.FloatField(default=0.88)
|
|
oarlength = models.FloatField(default=2.89)
|
|
|
|
|
|
notes = models.CharField(blank=True,null=True,max_length=1000)
|
|
summary = models.TextField(blank=True)
|
|
privacy = models.CharField(default='visible',max_length=30,
|
|
choices=privacychoices)
|
|
rankingpiece = models.BooleanField(default=False,verbose_name='Ranking Piece')
|
|
|
|
def __unicode__(self):
|
|
|
|
date = self.date
|
|
name = self.name
|
|
distance = str(self.distance)
|
|
ownerfirst = self.user.user.first_name
|
|
ownerlast = self.user.user.last_name
|
|
duration = self.duration
|
|
boattype = self.boattype
|
|
workouttype = self.workouttype
|
|
|
|
if workouttype != 'water':
|
|
stri = u'{d} {n} {dist}m {duration:%H:%M:%S} {workouttype} {ownerfirst} {ownerlast}'.format(
|
|
d = date.strftime('%Y-%m-%d'),
|
|
n = name,
|
|
dist = distance,
|
|
duration = duration,
|
|
workouttype = workouttype,
|
|
ownerfirst = ownerfirst,
|
|
ownerlast = ownerlast,
|
|
)
|
|
else:
|
|
stri = u'{d} {n} {dist}m {duration:%H:%M:%S} {workouttype} {boattype} {ownerfirst} {ownerlast}'.format(
|
|
d = date.strftime('%Y-%m-%d'),
|
|
n = name,
|
|
dist = distance,
|
|
duration = duration,
|
|
workouttype = workouttype,
|
|
boattype=boattype,
|
|
ownerfirst = ownerfirst,
|
|
ownerlast = ownerlast,
|
|
)
|
|
|
|
return stri
|
|
|
|
# delete files belonging to workout instance
|
|
# related GraphImage objects should be deleted automatically
|
|
@receiver(models.signals.post_delete,sender=Workout)
|
|
def auto_delete_file_on_delete(sender, instance, **kwargs):
|
|
# delete CSV file
|
|
if instance.csvfilename:
|
|
if os.path.isfile(instance.csvfilename):
|
|
os.remove(instance.csvfilename)
|
|
if instance.csvfilename+'.gz':
|
|
if os.path.isfile(instance.csvfilename+'.gz'):
|
|
os.remove(instance.csvfilename+'.gz')
|
|
|
|
# Delete stroke data from the database when a workout is deleted
|
|
@receiver(models.signals.post_delete,sender=Workout)
|
|
def auto_delete_strokedata_on_delete(sender, instance, **kwargs):
|
|
if instance.id:
|
|
query = sa.text('DELETE FROM strokedata WHERE workoutid={id};'.format(
|
|
id=instance.id,
|
|
))
|
|
engine = create_engine(database_url, echo=False)
|
|
with engine.connect() as conn, conn.begin():
|
|
try:
|
|
result = conn.execute(query)
|
|
except:
|
|
print "Database Locked"
|
|
conn.close()
|
|
engine.dispose()
|
|
|
|
# Virtual Race results (for keeping results when workouts are deleted)
|
|
class VirtualRaceResult(models.Model):
|
|
boatclasses = (type for type in types.workouttypes if type[0] in types.otwtypes)
|
|
userid = models.IntegerField(default=0)
|
|
teamname = models.CharField(max_length=80,verbose_name = 'Team Name',
|
|
blank=True,null=True)
|
|
username = models.CharField(max_length=150)
|
|
workoutid = models.IntegerField(null=True)
|
|
weightcategory = models.CharField(default="hwt",max_length=10,
|
|
choices=weightcategories,
|
|
verbose_name='Weight Category')
|
|
race = models.ForeignKey(VirtualRace)
|
|
duration = models.TimeField(default=datetime.time(1,0))
|
|
distance = models.IntegerField(default=0)
|
|
boatclass = models.CharField(choices=boatclasses,
|
|
max_length=40,
|
|
default='water',
|
|
verbose_name = 'Boat Class')
|
|
boattype = models.CharField(choices=boattypes,max_length=40,
|
|
default='1x',
|
|
verbose_name = 'Boat Type'
|
|
)
|
|
coursecompleted = models.BooleanField(default=False)
|
|
sex = models.CharField(default="not specified",
|
|
max_length=30,
|
|
choices=sexcategories,
|
|
verbose_name='Gender')
|
|
|
|
age = models.IntegerField(null=True)
|
|
|
|
def __unicode__(self):
|
|
rr = Rower.objects.get(id=self.userid)
|
|
name = '{u1} {u2}'.format(
|
|
u1 = rr.user.first_name,
|
|
u2 = rr.user.last_name,
|
|
)
|
|
if self.teamname:
|
|
return u'Entry for {n} for "{r}" in {c} {d} with {t} ({s})'.format(
|
|
n = name,
|
|
r = self.race,
|
|
d = self.boattype,
|
|
c = self.boatclass,
|
|
t = self.teamname,
|
|
s = self.sex,
|
|
)
|
|
else:
|
|
return u'Entry for {n} for "{r}" in {c} {d} ({s})'.format(
|
|
n = name,
|
|
r = self.race,
|
|
d = self.boattype,
|
|
c = self.boatclass,
|
|
s = self.sex,
|
|
)
|
|
|
|
|
|
class CourseTestResult(models.Model):
|
|
userid = models.IntegerField(default=0)
|
|
workoutid = models.IntegerField(null=True)
|
|
plannedsession = models.ForeignKey(PlannedSession)
|
|
duration = models.TimeField(default=datetime.time(1,0))
|
|
distance = models.IntegerField(default=0)
|
|
coursecompleted = models.BooleanField(default=False)
|
|
|
|
class VirtualRaceResultForm(ModelForm):
|
|
class Meta:
|
|
model = VirtualRaceResult
|
|
fields = ['teamname','weightcategory','boatclass','boattype','age']
|
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
boattypes = kwargs.pop('boattypes',None)
|
|
super(VirtualRaceResultForm, self).__init__(*args, **kwargs)
|
|
|
|
if boattypes:
|
|
self.fields['boattype'].choices = boattypes
|
|
|
|
self.fields['mix'] = forms.BooleanField(initial=False,
|
|
required=False,
|
|
label='Mixed Gender')
|
|
|
|
from rowers.metrics import rowingmetrics
|
|
|
|
strokedatafields = {
|
|
'workoutid':models.IntegerField(null=True),
|
|
'workoutstate':models.IntegerField(null=True,default=1),
|
|
'ftime':models.CharField(max_length=30),
|
|
'fpace':models.CharField(max_length=30),
|
|
'hr_ut2':models.IntegerField(null=True),
|
|
'hr_ut1':models.IntegerField(null=True),
|
|
'hr_at':models.IntegerField(null=True),
|
|
'hr_tr':models.IntegerField(null=True),
|
|
'hr_an':models.IntegerField(null=True),
|
|
'hr_max':models.IntegerField(null=True),
|
|
'hr_bottom':models.IntegerField(null=True),
|
|
'x_right':models.FloatField(null=True),
|
|
'ergpace':models.FloatField(null=True),
|
|
'nowindpace':models.FloatField(null=True),
|
|
'equivergpower':models.FloatField(null=True),
|
|
'fergpace':models.CharField(max_length=30),
|
|
'fnowindpace':models.CharField(max_length=30),
|
|
}
|
|
|
|
for name,d in rowingmetrics:
|
|
if d['numtype'] == 'float':
|
|
try:
|
|
strokedatafields[name] = models.FloatField(
|
|
null=d['null'],
|
|
default=d['default'],
|
|
verbose_name=d['verbose_name'])
|
|
except KeyError:
|
|
strokedatafields[name] = models.FloatField(
|
|
null=d['null'],
|
|
verbose_name=d['verbose_name'])
|
|
elif d['numtype'] == 'integer':
|
|
try:
|
|
strokedatafields[name] = models.IntegerField(
|
|
null=d['null'],
|
|
default=d['default'],
|
|
verbose_name=d['verbose_name'])
|
|
except KeyError:
|
|
strokedatafields[name] = models.IntegerField(
|
|
null=d['null'],
|
|
verbose_name=d['verbose_name'])
|
|
class Meta:
|
|
db_table = 'strokedata'
|
|
index_together = ['workoutid']
|
|
app_label = 'rowers'
|
|
|
|
attrs = {'__module__': '', 'Meta': Meta}
|
|
attrs.update(strokedatafields)
|
|
|
|
# Model of StrokeData table
|
|
# the definition here is used only to enable easy Django migration
|
|
# when the StrokeData are expanded.
|
|
# No Django Instances of this model are managed. Strokedata table is
|
|
# accesssed directly with SQL commands
|
|
StrokeData = type(str('StrokeData'), (models.Model,),
|
|
attrs
|
|
)
|
|
|
|
# Storing data for the OTW CP chart
|
|
class cpdata(models.Model):
|
|
delta = models.IntegerField(default=0)
|
|
cp = models.FloatField(default=0)
|
|
user = models.IntegerField(default=0)
|
|
|
|
class Meta:
|
|
db_table = 'cpdata'
|
|
index_together = ['user']
|
|
app_label = 'rowers'
|
|
|
|
|
|
# Storing data for the OTW CP chart
|
|
class cpergdata(models.Model):
|
|
delta = models.IntegerField(default=0)
|
|
cp = models.FloatField(default=0)
|
|
user = models.IntegerField(default=0)
|
|
|
|
class Meta:
|
|
db_table = 'cpergdata'
|
|
index_together = ['user']
|
|
app_label = 'rowers'
|
|
|
|
|
|
|
|
# Storing data for the OTW CP chart
|
|
class ergcpdata(models.Model):
|
|
delta = models.IntegerField(default=0)
|
|
cp = models.FloatField(default=0)
|
|
distance = models.FloatField(default=0)
|
|
user = models.IntegerField(default=0)
|
|
|
|
class Meta:
|
|
db_table = 'ergcpdata'
|
|
index_together = ['user']
|
|
app_label = 'rowers'
|
|
|
|
# A wrapper around the png files
|
|
class GraphImage(models.Model):
|
|
filename = models.CharField(default='',max_length=150,blank=True,null=True)
|
|
creationdatetime = models.DateTimeField()
|
|
workout = models.ForeignKey(Workout)
|
|
width = models.IntegerField(default=1200)
|
|
height = models.IntegerField(default=600)
|
|
|
|
def __str__(self):
|
|
return self.filename
|
|
|
|
|
|
# delete related file object when image is deleted
|
|
@receiver(models.signals.post_delete,sender=GraphImage)
|
|
def auto_delete_image_on_delete(sender,instance, **kwargs):
|
|
if instance.filename:
|
|
if os.path.isfile(instance.filename):
|
|
others = GraphImage.objects.filter(filename=instance.filename)
|
|
if len(others) == 0:
|
|
os.remove(instance.filename)
|
|
else:
|
|
print "couldn't find the file "+instance.filename
|
|
|
|
|
|
# Form to update Workout data
|
|
class WorkoutForm(ModelForm):
|
|
# duration = forms.TimeInput(format='%H:%M:%S.%f')
|
|
class Meta:
|
|
model = Workout
|
|
fields = ['name','date','starttime','timezone','duration','distance','workouttype','boattype','weightcategory','notes','rankingpiece']
|
|
widgets = {
|
|
'date': AdminDateWidget(),
|
|
'notes': forms.Textarea,
|
|
'duration': forms.TimeInput(format='%H:%M:%S.%f'),
|
|
}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(WorkoutForm, self).__init__(*args, **kwargs)
|
|
self.fields['private'] = forms.BooleanField(initial=False,
|
|
required=False,
|
|
label='Private')
|
|
|
|
self.fields['timezone'] = forms.ChoiceField(
|
|
choices = [(x,x) for x in pytz.common_timezones]
|
|
)
|
|
|
|
if 'instance' in kwargs:
|
|
if kwargs['instance'].privacy == 'visible':
|
|
self.fields['private'].initial = False
|
|
else:
|
|
self.fields['private'].initial = True
|
|
|
|
# Used for the rowing physics calculations
|
|
class AdvancedWorkoutForm(ModelForm):
|
|
quick_calc = forms.BooleanField(initial=True,required=False)
|
|
|
|
class Meta:
|
|
model = Workout
|
|
fields = ['boattype','weightvalue','quick_calc']
|
|
|
|
class RowerExportForm(ModelForm):
|
|
class Meta:
|
|
model = Rower
|
|
fields = [
|
|
'stravaexportas',
|
|
'polar_auto_import',
|
|
'c2_auto_export',
|
|
'c2_auto_import',
|
|
'mapmyfitness_auto_export',
|
|
'runkeeper_auto_export',
|
|
'sporttracks_auto_export',
|
|
'strava_auto_export',
|
|
'strava_auto_import',
|
|
'trainingpeaks_auto_export',
|
|
]
|
|
|
|
# Simple form to set rower's Functional Threshold Power
|
|
class RowerPowerForm(ModelForm):
|
|
class Meta:
|
|
model = Rower
|
|
fields = ['hrftp','ftp','otwslack']
|
|
|
|
# Form to set rower's Power zones, including test routines
|
|
# to enable consistency
|
|
class RowerPowerZonesForm(ModelForm):
|
|
|
|
powerzones = ['UT3','UT2','UT1','AT','TR','AN']
|
|
ut3name = forms.CharField(initial=powerzones[0])
|
|
ut2name = forms.CharField(initial=powerzones[1])
|
|
ut1name = forms.CharField(initial=powerzones[2])
|
|
atname = forms.CharField(initial=powerzones[3])
|
|
trname = forms.CharField(initial=powerzones[4])
|
|
anname = forms.CharField(initial=powerzones[5])
|
|
|
|
def __init__(self, *args,**kwargs):
|
|
super(RowerPowerZonesForm, self).__init__(*args, **kwargs)
|
|
|
|
if 'instance' in kwargs:
|
|
powerzones = kwargs['instance'].powerzones
|
|
else:
|
|
powerzones = ['UT3','UT2','UT1','AT','TR','AN']
|
|
|
|
self.fields['ut3name'].initial = powerzones[0]
|
|
self.fields['ut2name'].initial = powerzones[1]
|
|
self.fields['ut1name'].initial = powerzones[2]
|
|
self.fields['atname'].initial = powerzones[3]
|
|
self.fields['trname'].initial = powerzones[4]
|
|
self.fields['anname'].initial = powerzones[5]
|
|
|
|
class Meta:
|
|
model = Rower
|
|
fields = ['pw_ut2','pw_ut1','pw_at','pw_tr','pw_an']
|
|
|
|
def clean(self):
|
|
cleaned_data = super(RowerPowerZonesForm, self).clean()
|
|
|
|
try:
|
|
pw_ut2 = cleaned_data['pw_ut2']
|
|
except KeyError:
|
|
raise ValidationError("Value cannot be empty")
|
|
except:
|
|
pw_ut2 = int(self.data['pw_ut2'])
|
|
try:
|
|
pw_ut1 = cleaned_data['pw_ut1']
|
|
except KeyError:
|
|
raise ValidationError("Value cannot be empty")
|
|
except:
|
|
pw_ut1 = int(self.data['pw_ut1'])
|
|
try:
|
|
pw_at = cleaned_data['pw_at']
|
|
except KeyError:
|
|
raise ValidationError("Value cannot be empty")
|
|
except:
|
|
pw_at = int(self.data['pw_at'])
|
|
try:
|
|
pw_tr = cleaned_data['pw_tr']
|
|
except KeyError:
|
|
raise ValidationError("Value cannot be empty")
|
|
except:
|
|
pw_tr = int(self.data['pw_tr'])
|
|
try:
|
|
pw_an = cleaned_data['pw_an']
|
|
except KeyError:
|
|
raise ValidationError("Value cannot be empty")
|
|
except:
|
|
pw_an = int(self.data['pw_an'])
|
|
|
|
try:
|
|
ut3name = cleaned_data['ut3name']
|
|
except:
|
|
ut2name = 'UT3'
|
|
cleaned_data['ut3name'] = 'UT3'
|
|
try:
|
|
ut2name = cleaned_data['ut2name']
|
|
except:
|
|
ut2name = 'UT2'
|
|
cleaned_data['ut2name'] = 'UT2'
|
|
try:
|
|
ut1name = cleaned_data['ut1name']
|
|
except:
|
|
ut1name = 'UT1'
|
|
cleaned_data['ut1name'] = 'UT1'
|
|
try:
|
|
atname = cleaned_data['atname']
|
|
except:
|
|
atname = 'AT'
|
|
cleaned_data['atname'] = 'AT'
|
|
try:
|
|
trname = cleaned_data['trname']
|
|
except:
|
|
trname = 'TR'
|
|
cleaned_data['ut1name'] = 'TR'
|
|
try:
|
|
anname = cleaned_data['anname']
|
|
except:
|
|
anname = 'AN'
|
|
cleaned_data['ut1name'] = 'AN'
|
|
|
|
|
|
try:
|
|
ut3name = cleaned_data['ut3name']
|
|
except:
|
|
ut2name = 'UT3'
|
|
cleaned_data['ut3name'] = 'UT3'
|
|
try:
|
|
ut2name = cleaned_data['ut2name']
|
|
except:
|
|
ut2name = 'UT2'
|
|
cleaned_data['ut2name'] = 'UT2'
|
|
try:
|
|
ut1name = cleaned_data['ut1name']
|
|
except:
|
|
ut1name = 'UT1'
|
|
cleaned_data['ut1name'] = 'UT1'
|
|
try:
|
|
atname = cleaned_data['atname']
|
|
except:
|
|
atname = 'AT'
|
|
cleaned_data['atname'] = 'AT'
|
|
try:
|
|
trname = cleaned_data['trname']
|
|
except:
|
|
trname = 'TR'
|
|
cleaned_data['ut1name'] = 'TR'
|
|
try:
|
|
anname = cleaned_data['anname']
|
|
except:
|
|
anname = 'AN'
|
|
cleaned_data['ut1name'] = 'AN'
|
|
|
|
|
|
if pw_ut1 <= pw_ut2:
|
|
e = "{ut1name} should be higher than {ut2name}".format(
|
|
ut1name = ut1name,
|
|
ut2name= ut2name,
|
|
)
|
|
raise forms.ValidationError(e)
|
|
if pw_at <= pw_ut1:
|
|
e = "{atname} should be higher than {ut1name}".format(
|
|
atname = atname,
|
|
ut1name= ut1name,
|
|
)
|
|
raise forms.ValidationError(e)
|
|
if pw_tr <= pw_at:
|
|
e = "{trname} should be higher than {atname}".format(
|
|
atname = atname,
|
|
trname= trname,
|
|
)
|
|
raise forms.ValidationError(e)
|
|
if pw_an <= pw_tr:
|
|
e = "{anname} should be higher than {trname}".format(
|
|
anname = anname,
|
|
trname= trname,
|
|
)
|
|
raise forms.ValidationError(e)
|
|
|
|
return cleaned_data
|
|
|
|
# Form to set rower's Auto Import and Export settings
|
|
class RowerImportExportForm(ModelForm):
|
|
class Meta:
|
|
model = Rower
|
|
fields = [
|
|
'polar_auto_import',
|
|
'c2_auto_export',
|
|
'c2_auto_import',
|
|
'mapmyfitness_auto_export',
|
|
'runkeeper_auto_export',
|
|
'sporttracks_auto_export',
|
|
'strava_auto_export',
|
|
'strava_auto_import',
|
|
'trainingpeaks_auto_export',
|
|
]
|
|
|
|
|
|
|
|
# Form to set rower's Email and Weight category
|
|
class AccountRowerForm(ModelForm):
|
|
class Meta:
|
|
model = Rower
|
|
fields = ['sex','birthdate','weightcategory',
|
|
'getemailnotifications',
|
|
'getimportantemails',
|
|
'defaulttimezone','showfavoritechartnotes',
|
|
'defaultlandingpage']
|
|
|
|
widgets = {
|
|
'birthdate': SelectDateWidget(
|
|
years=range(
|
|
timezone.now().year-100,timezone.now().year-10)),
|
|
}
|
|
|
|
|
|
def clean_email(self):
|
|
email = self.cleaned_data.get('email')
|
|
|
|
try:
|
|
validate_email(email)
|
|
except ValidationError:
|
|
raise forms.ValidationError(
|
|
'Please enter a valid email address')
|
|
|
|
try:
|
|
match = User.objects.get(email__iexact=email)
|
|
if self.instance.user == match:
|
|
return email
|
|
except User.DoesNotExist:
|
|
return email
|
|
|
|
raise forms.ValidationError('This email address is not allowed')
|
|
|
|
|
|
|
|
class UserForm(ModelForm):
|
|
class Meta:
|
|
model = User
|
|
fields = ['first_name','last_name','email']
|
|
|
|
def clean_first_name(self):
|
|
first_name = self.cleaned_data.get('first_name')
|
|
|
|
if len(first_name):
|
|
return first_name
|
|
|
|
raise forms.ValidationError('Please fill in your first name')
|
|
|
|
def clean_email(self):
|
|
email = self.cleaned_data.get('email')
|
|
|
|
try:
|
|
validate_email(email)
|
|
except ValidationError:
|
|
raise forms.ValidationError(
|
|
'Please enter a valid email address')
|
|
|
|
try:
|
|
match = User.objects.filter(email__iexact=email)
|
|
if self.instance in match:
|
|
return email
|
|
except User.DoesNotExist:
|
|
return email
|
|
|
|
raise forms.ValidationError('This email address is not allowed')
|
|
|
|
|
|
# Form to set rower's Heart Rate zones, including test routines
|
|
# to enable consistency
|
|
class RowerForm(ModelForm):
|
|
class Meta:
|
|
model = Rower
|
|
fields = ['rest','ut2','ut1','at','tr','an','max']
|
|
|
|
|
|
def clean_rest(self):
|
|
rest = self.cleaned_data['rest']
|
|
|
|
if rest<10:
|
|
self.data['rest']=10
|
|
raise forms.ValidationError("Resting heart rate should be higher than 10 bpm")
|
|
|
|
|
|
if rest>250:
|
|
self.data['rest'] = 250
|
|
raise forms.ValidationError("Resting heart rate should be lower than 250 bpm")
|
|
|
|
|
|
return rest
|
|
|
|
def clean_ut2(self):
|
|
ut2 = self.cleaned_data['ut2']
|
|
|
|
if ut2<10:
|
|
raise forms.ValidationError("UT2 heart rate should be higher than 10 bpm")
|
|
|
|
if ut2>250:
|
|
raise forms.ValidationError("UT2 heart rate should be lower than 250 bpm")
|
|
|
|
return ut2
|
|
|
|
def clean_ut1(self):
|
|
ut1 = self.cleaned_data['ut1']
|
|
|
|
if ut1<10:
|
|
raise forms.ValidationError("UT1 heart rate should be higher than 10 bpm")
|
|
|
|
if ut1>250:
|
|
raise forms.ValidationError("Resting heart rate should be lower than 250 bpm")
|
|
|
|
return ut1
|
|
|
|
def clean_at(self):
|
|
at = self.cleaned_data['at']
|
|
|
|
if at<10:
|
|
raise forms.ValidationError("AT heart rate should be higher than 10 bpm")
|
|
|
|
if at>250:
|
|
raise forms.ValidationError("AT heart rate should be lower than 250 bpm")
|
|
|
|
return at
|
|
|
|
def clean_tr(self):
|
|
tr = self.cleaned_data['tr']
|
|
|
|
if tr<10:
|
|
raise forms.ValidationError("TR heart rate should be higher than 10 bpm")
|
|
|
|
if tr>250:
|
|
raise forms.ValidationError("TR heart rate should be lower than 250 bpm")
|
|
|
|
return tr
|
|
|
|
def clean_an(self):
|
|
an = self.cleaned_data['an']
|
|
|
|
if an<10:
|
|
raise forms.ValidationError("AN heart rate should be higher than 10 bpm")
|
|
|
|
if an>250:
|
|
raise forms.ValidationError("AN heart rate should be lower than 250 bpm")
|
|
|
|
return an
|
|
|
|
def clean_max(self):
|
|
max = self.cleaned_data['max']
|
|
|
|
if max<10:
|
|
raise forms.ValidationError("Max heart rate should be higher than 10 bpm")
|
|
|
|
if max>250:
|
|
raise forms.ValidationError("Max heart rate should be lower than 250 bpm")
|
|
|
|
return max
|
|
|
|
|
|
def clean(self):
|
|
|
|
|
|
try:
|
|
rest = self.cleaned_data['rest']
|
|
except:
|
|
try:
|
|
rest = int(self.data['rest'])
|
|
except ValueError:
|
|
rest = 0
|
|
|
|
try:
|
|
ut2 = self.cleaned_data['ut2']
|
|
except:
|
|
try:
|
|
ut2 = self.data['ut2']
|
|
except ValueError:
|
|
ut2 = 0
|
|
|
|
try:
|
|
ut1 = self.cleaned_data['ut1']
|
|
except:
|
|
try:
|
|
ut1 = self.data['ut1']
|
|
except ValueError:
|
|
ut1 = 0
|
|
|
|
try:
|
|
at = self.cleaned_data['at']
|
|
except:
|
|
try:
|
|
at = self.data['at']
|
|
except ValueError:
|
|
at = 0
|
|
|
|
try:
|
|
an = self.cleaned_data['an']
|
|
except:
|
|
try:
|
|
an = self.data['an']
|
|
except ValueError:
|
|
an = 0
|
|
|
|
try:
|
|
tr = self.cleaned_data['tr']
|
|
except:
|
|
try:
|
|
tr = self.data['tr']
|
|
except ValueError:
|
|
tr = 0
|
|
|
|
try:
|
|
max = self.cleaned_data['max']
|
|
except:
|
|
try:
|
|
max = self.data['max']
|
|
except ValueError:
|
|
max = 0
|
|
|
|
if rest>=ut2:
|
|
raise forms.ValidationError("Resting heart rate should be lower than UT2")
|
|
if ut2>=ut1:
|
|
raise forms.ValidationError("UT2 should be lower than UT1")
|
|
if ut2>=ut1:
|
|
raise forms.ValidationError("UT2 should be lower than UT1")
|
|
if ut1>=at:
|
|
raise forms.ValidationError("UT1 should be lower than AT")
|
|
if at>=tr:
|
|
raise forms.ValidationError("AT should be lower than TR")
|
|
if tr>=an:
|
|
raise forms.ValidationError("TR should be lower than AN")
|
|
if an>=max:
|
|
raise forms.ValidationError("AN should be lower than Max")
|
|
|
|
|
|
# An announcement that goes to the right of the workouts list
|
|
# optionally sends a tweet to our twitter account
|
|
class SiteAnnouncement(models.Model):
|
|
created = models.DateField(default=timezone.now)
|
|
announcement = models.TextField(max_length=280)
|
|
expires = models.DateField(default=timezone.now)
|
|
modified = models.DateField(default=timezone.now)
|
|
dotweet = models.BooleanField(default=False)
|
|
|
|
def save(self, *args, **kwargs):
|
|
if not self.id:
|
|
self.created = timezone.now()
|
|
self.expires = timezone.now()+datetime.timedelta(days=10)
|
|
self.modified = timezone.now()
|
|
if self.dotweet:
|
|
try:
|
|
status = tweetapi.PostUpdate(self.announcement)
|
|
except:
|
|
try:
|
|
status = tweetapi.PostUpdate(self.announcement[:270])
|
|
except:
|
|
pass
|
|
return super(SiteAnnouncement,self).save(*args, **kwargs)
|
|
|
|
# A comment by a user on a training
|
|
class WorkoutComment(models.Model):
|
|
comment = models.TextField(max_length=300)
|
|
created = models.DateTimeField(default=timezone.now)
|
|
read = models.BooleanField(default=False)
|
|
notification = models.BooleanField(default=True,verbose_name="Subscribe to new comment notifications")
|
|
user = models.ForeignKey(User)
|
|
workout = models.ForeignKey(Workout)
|
|
|
|
def __unicode__(self):
|
|
return u'Comment to: {w} by {u1} {u2}'.format(
|
|
w=self.workout,
|
|
u1 = self.user.first_name,
|
|
u2 = self.user.last_name,
|
|
)
|
|
|
|
|
|
class WorkoutCommentForm(ModelForm):
|
|
class Meta:
|
|
model = WorkoutComment
|
|
fields = ['comment','notification']
|
|
widgets = {
|
|
'comment': forms.Textarea,
|
|
}
|
|
|