Private
Public Access
1
0
Files
rowsandall/rowers/models.py
2020-07-19 09:58:10 +02:00

3987 lines
133 KiB
Python

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from __future__ import unicode_literals, absolute_import
from django.utils.encoding import python_2_unicode_compatible
from django.db import models,IntegrityError
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,SelectDateWidget
#from django.forms.extras.widgets import SelectDateWidget
from django.forms.formsets import BaseFormSet
from datetimewidget.widgets import DateTimeWidget
from django.contrib.admin.widgets import AdminDateWidget,AdminTimeWidget,AdminSplitDateTime
from django.core.validators import validate_email
import os
import twitter
import re
import pytz
from django_countries.fields import CountryField
from scipy.interpolate import splprep, splev, CubicSpline,interp1d
import numpy as np
import shutil
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 rules.contrib.models import RulesModel
from rowers.rower_rules import *
from rowers.rows import validate_file_extension
from collections import OrderedDict
from timezonefinder import TimezoneFinder
import rowers.mytypes as mytypes
from matplotlib import path
from rowsandall_app.settings import (
TWEET_ACCESS_TOKEN_KEY,
TWEET_ACCESS_TOKEN_SECRET,
TWEET_CONSUMER_KEY,
TWEET_CONSUMER_SECRET,
)
# END PERMISSIONS
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
)
def half_year_from_now():
return (datetime.datetime.now(tz=timezone.utc)+timezone.timedelta(days=182)).date()
def a_week_from_now():
return (datetime.datetime.now(tz=timezone.utc)+timezone.timedelta(days=7)).date()
def current_day():
return (datetime.datetime.now(tz=timezone.utc)).date()
def current_time():
return datetime.datetime.now(tz=timezone.utc)
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):
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([str(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):
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([str(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,verbose=True):
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:
if verbose:
print(record)
record.save()
except:
if verbose:
print(record,'*')
else:
pass
class CalcAgePerformance(models.Model):
weightcategories = mytypes.weightcategories
sexcategories = mytypes.sexcategories
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=current_day)
last_workout = models.IntegerField(default=0)
user = models.ForeignKey(User,on_delete=models.CASCADE)
PowerFourMin = models.FloatField(default=0)
PowerTwoK = models.FloatField(default=0)
PowerOneHour = models.FloatField(default=0)
workoutmode = models.CharField(default='rower',choices=modechoices,
max_length=41,)
class Meta:
db_table = 'powertimefitnessmetric'
@python_2_unicode_compatible
class C2WorldClassAgePerformance(models.Model):
weightcategories = mytypes.weightcategories
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 __str__(self):
thestring = '{s} {w} {n} age {a} ({season}) {distance}m {duration} seconds'.format(
s = self.sex,
w = self.weightcategory,
n = self.name,
a = self.age,
season = self.season,
distance = self.distance,
duration = self.duration,
)
return thestring
@python_2_unicode_compatible
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,on_delete=models.CASCADE)
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 __str__(self):
return self.name
def save(self, *args, **kwargs):
manager = self.manager
if not can_have_teams(manager):
raise ValidationError(
"Basic user cannot be team manager"
)
if not self.id:
# new model instance
if not can_add_team(manager):
raise ValidationError(
"Pro and Self-Coach users cannot have more than one team"
)
super(Team, self).save(*args,**kwargs)
class TeamForm(ModelForm):
class Meta:
model = Team
fields = ['name','notes','private','viewing']
widgets = {
'notes': forms.Textarea,
}
class TeamInvite(models.Model):
team = models.ForeignKey(Team,on_delete=models.CASCADE)
user = models.ForeignKey(User,null=True,on_delete=models.CASCADE)
issuedate = models.DateField(default=current_day)
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)
email = forms.EmailField(required=False)
class Meta:
model = TeamInvite
fields = ['user','email']
class TeamRequest(models.Model):
team = models.ForeignKey(Team,on_delete=models.CASCADE)
user = models.ForeignKey(User,null=True,on_delete=models.CASCADE)
issuedate = models.DateField(default=current_day)
code = models.CharField(max_length=150,unique=True)
from rowers.utils import (
workflowleftpanel,workflowmiddlepanel,
defaultleft,defaultmiddle,landingpages
)
from rowers.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)
try:
#latnew = CubicSpline(t,latitudes,bc_type='not-a-knot')(tnew)
#lonnew = CubicSpline(t,longitudes,bc_type='not-a-knot')(tnew)
latnew = interp1d(t,latitudes)(tnew)
lonnew = interp1d(t,longitudes)(tnew)
except ValueError:
latnew = latitudes
lonnew = longitudes
# 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
if not polygons:
return 0
for i in range(polygons.count()-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 = mytypes.weightcategories
# Plan
plans = (
('basic','basic'),
('pro','pro'),
('plan','plan'),
('coach','coach'),
('freecoach','freecoach'),
)
paymenttypes = (
('single','single'),
('recurring','recurring')
)
paymentprocessors = (
('paypal','PayPal'),
('braintree','BrainTree')
)
@python_2_unicode_compatible
class PaidPlan(models.Model):
shortname = models.CharField(max_length=50,choices=plans)
name = models.CharField(max_length=200)
external_id = models.CharField(blank=True,null=True,default=None,max_length=200)
price = models.FloatField(blank=True,null=True,default=None)
paymentprocessor = models.CharField(
max_length=50,choices=paymentprocessors,default='braintree')
paymenttype = models.CharField(
default='single',max_length=30,
verbose_name='Payment Type',
choices=paymenttypes,
)
active = models.BooleanField(default=True)
clubsize = models.IntegerField(default=0)
def __str__(self):
return '{name} - {shortname} at {price:.2f} EURO ({paymenttype} payment)'.format(
name = self.name,
shortname = self.shortname,
price = self.price,
paymenttype = self.paymenttype,
paymentprocessor = self.paymentprocessor,
)
@python_2_unicode_compatible
class CoachingGroup(models.Model):
name = models.CharField(default='group',max_length=30,null=True,blank=True)
def __str__(self):
return 'Coaching Group {id}: {name}'.format(
id = self.pk,
name = self.name
)
def __len__(self):
rs = Rower.objects.filter(coachinggroups__in=[self])
return rs.count()
def get_coaches(self):
return Rower.objects.filter(mycoachgroup=self)
# Extension of User with rowing specific data
@python_2_unicode_compatible
class Rower(models.Model):
adaptivetypes = mytypes.adaptivetypes
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'),
)
gridtypes = (
('none',None),
('both','both'),
('x','x'),
('y','y'),
)
user = models.OneToOneField(User,on_delete=models.CASCADE)
#billing details
country = CountryField(default=None, null=True, blank=True)
street_address = models.CharField(default='',blank=True,null=True,max_length=200)
city = models.CharField(default='',blank=True,null=True,max_length=200)
postal_code = models.CharField(default='',blank=True,null=True,max_length=200)
customer_id = models.CharField(default=None,null=True,blank=True,max_length=200)
subscription_id = models.CharField(default=None,null=True,
blank=True,max_length=200)
rowerplan = models.CharField(default='basic',max_length=30,
choices=plans)
paymenttype = models.CharField(
default='single',max_length=30,
verbose_name='Payment Type',
choices=paymenttypes,
)
paymentprocessor = models.CharField(max_length=50,
choices=paymentprocessors,
null=True,blank=True,
default='braintree')
paidplan = models.ForeignKey(PaidPlan,null=True,default=None,on_delete=models.SET_NULL)
planexpires = models.DateField(default=current_day)
teamplanexpires = models.DateField(default=current_day)
clubsize = models.IntegerField(default=0)
protrialexpires = models.DateField(default=datetime.date(1970,1,1))
plantrialexpires = models.DateField(default=datetime.date(1970,1,1))
offercoaching = models.BooleanField(default=False, verbose_name='Offer Remote Coaching')
# Privacy Data
gdproptin = models.BooleanField(default=False)
gdproptindate = models.DateTimeField(blank=True,null=True)
surveydone = models.BooleanField(default=False)
surveydonedate = 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)
adaptiveclass = models.CharField(choices=adaptivetypes,max_length=50,
default='None',
verbose_name='Adaptive Classification')
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)
garmintoken = models.CharField(default='',max_length=200,blank=True,null=True)
garminrefreshtoken = models.CharField(default='',max_length=1000,
blank=True,null=True)
stravatoken = models.CharField(default='',max_length=200,blank=True,null=True)
stravatokenexpirydate = models.DateTimeField(blank=True,null=True)
stravarefreshtoken = models.CharField(default='',max_length=1000,
blank=True,null=True)
stravaexportas = models.CharField(default="Rowing",
max_length=30,
choices=stravatypes,
verbose_name="Export Workouts to Strava as")
strava_owner_id = models.BigIntegerField(default=0)
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)
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')
# Friends/Team
friends = models.ManyToManyField("self",blank=True)
mycoachgroup = models.ForeignKey(CoachingGroup,related_name='coachingrole',null=True,on_delete=models.SET_NULL)
coachinggroups = models.ManyToManyField(CoachingGroup,related_name='coaches')
privacy = models.CharField(default='visible',max_length=30,
choices=privacychoices)
team = models.ManyToManyField(Team,blank=True,related_name='rower')
# 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')
# Static chart settings
staticgrids = models.CharField(default='both',choices=gridtypes,null=True,max_length=50,
verbose_name='Chart Grid')
ergpaceslow = datetime.timedelta(seconds=160)
ergpacefast = datetime.timedelta(seconds=85)
otwpaceslow = datetime.timedelta(seconds=240)
otwpacefast = datetime.timedelta(seconds=85)
slowpaceerg = models.DurationField(default=ergpaceslow,verbose_name='Slowest Erg Pace')
fastpaceerg = models.DurationField(default=ergpacefast,verbose_name='Fastest Erg Pace')
slowpaceotw = models.DurationField(default=otwpaceslow,verbose_name='Slowest OTW Pace')
fastpaceotw = models.DurationField(default=otwpacefast,verbose_name='Fastest OTW Pace')
# Auto Join
autojoin = models.BooleanField(default=False,verbose_name='Auto Join Workout Segments')
def __str__(self):
return self.user.first_name+' '+self.user.last_name
def clean_email(self):
return self.user.email.lower()
def save(self, *args, **kwargs):
try:
for group in self.coachinggroups.all():
try:
coach = Rower.objects.get(mycoachgroup=group)
if coach.rowerplan == 'freecoach':
self.coachinggroups.remove(group)
except Rower.DoesNotExist:
pass
except ValueError:
pass
super(Rower, self).save(*args, **kwargs)
def get_managed_teams(self):
return Team.objects.filter(manager=self.user)
def get_coaches(self):
coaches = []
for group in self.coachinggroups.all():
try:
coach = Rower.objects.get(mycoachgroup=group)
coaches.append(coach)
except Rower.DoesNotExist:
pass
return coaches
@property
def ispaid(self):
return self.rowerplan in ['pro','plan','coach']
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 = []
# requestor is user
class CoachRequest(models.Model):
coach = models.ForeignKey(Rower,on_delete=models.CASCADE)
user = models.ForeignKey(User,null=True,on_delete=models.CASCADE)
issuedate = models.DateField(default=current_day)
code = models.CharField(max_length=150,unique=True)
# requestor is coach
class CoachOffer(models.Model):
coach = models.ForeignKey(Rower,on_delete=models.CASCADE)
user = models.ForeignKey(User,null=True,on_delete=models.CASCADE)
issuedate = models.DateField(default=current_day)
code = models.CharField(max_length=150,unique=True)
from django.db.models.signals import m2m_changed
def check_teams_on_change(sender, **kwargs):
instance = kwargs.pop('instance', None)
action = kwargs.pop('action', None)
pk_set = kwargs.pop('pk_set',None)
if action == 'pre_add':
#if instance.protrialexpires < datetime.date.today() and instance.plantrialexpires < datetime.date.today():
for id in pk_set:
team = Team.objects.get(id=id)
if not can_join_team(instance.user,team):
raise ValidationError(
"You cannot join a team led by a Pro, Free Coach Plan or Self-Coach user"
)
m2m_changed.connect(check_teams_on_change, sender=Rower.team.through)
#@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'),
('all','All')
]
for workoutsource in mytypes.workoutsources:
workouttypechoices.append(workoutsource)
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,on_delete=models.CASCADE)
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 Condition(models.Model):
conditionchoices = (
('<','<'),
('>','>'),
('=','='),
('between','between')
)
metric = models.CharField(max_length=50,choices=parchoicesy1,verbose_name='Metric')
value1 = models.FloatField(default=0)
value2 = models.FloatField(default=0,null=True,blank=True)
condition = models.CharField(max_length=20,choices=conditionchoices,null=True)
class ConditionEditForm(ModelForm):
class Meta:
model = Condition
fields = ['metric','condition','value1','value2']
def clean(self):
cd = self.cleaned_data
try:
if cd['condition'] == 'between' and cd['value2'] is None:
raise forms.ValidationError('When using between, you must fill value 1 and value 2')
except KeyError:
pass
class BaseConditionFormSet(BaseFormSet):
def clean(self):
if any(self.errors):
return
for form in self.forms:
if form.cleaned_data:
metric = form.cleaned_data['metric']
condition = form.cleaned_data['condition']
value1 = form.cleaned_data['value1']
value2 = form.cleaned_data['value2']
rowchoices = []
for key,value in mytypes.workouttypes:
if key in mytypes.rowtypes:
rowchoices.append((key,value))
class Alert(models.Model):
name = models.CharField(max_length=150,verbose_name='Alert Name',null=True,blank=True)
manager = models.ForeignKey(User, on_delete=models.CASCADE)
rower = models.ForeignKey(Rower, on_delete=models.CASCADE)
measured = models.OneToOneField(Condition,verbose_name='Measuring',on_delete=models.CASCADE,
related_name='measured')
filter = models.ManyToManyField(Condition, related_name='filters',verbose_name='Filters')
reststrokes = models.BooleanField(default=False,null=True,verbose_name='Include Rest Strokes')
period = models.IntegerField(default=7,verbose_name='Reporting Period (days)')
next_run = models.DateField(default=timezone.now)
emailalert = models.BooleanField(default=True,verbose_name='Send email alerts')
workouttype = models.CharField(choices=rowchoices,max_length=50,
verbose_name='Exercise/Boat Class',default='water')
boattype = models.CharField(choices=mytypes.boattypes,max_length=50,
verbose_name='Boat Type',default='1x')
def __str__(self):
metricdict = {key:value for (key,value) in parchoicesy1}
stri = u'Alert {name} on {metric} for {workouttype} - running on {first_name} every {period} days'.format(
name = self.name,
metric = metricdict[self.measured.metric],
workouttype = self.workouttype,
first_name = self.rower.user.first_name,
period = self.period,
)
return stri
def metricname(self):
metricdict = {key:value for (key,value) in parchoicesy1}
return metricdict[self.measured.metric]
def description(self):
metricdict = {key:value for (key,value) in parchoicesy1}
if self.measured.condition == 'between':
description = 'This alert measures strokes where {metric} is between {value1} and {value2}.'.format(
metric = metricdict[self.measured.metric],
value1 = self.measured.value1,
value2 = self.measured.value2,
)
elif self.measured.condition == '<':
description = 'This alert measures strokes where {metric} is smaller than {value1}.'.format(
metric = metricdict[self.measured.metric],
value1 = self.measured.value1,
)
else:
description = 'This alert measures strokes where {metric} is larger than {value1}.'.format(
metric = metricdict[self.measured.metric],
value1 = self.measured.value1,
)
return description
def shortdescription(self):
metricdict = {key:value for (key,value) in parchoicesy1}
if self.measured.condition == 'between':
description = '{value1} < {metric} < {value2}'.format(
metric = self.measured.metric,
value1 = self.measured.value1,
value2 = self.measured.value2,
)
else:
description = '{metric} {condition} {value1}'.format(
metric = self.measured.metric,
value1 = self.measured.value1,
condition = self.measured.condition
)
return description
class AlertEditForm(ModelForm):
class Meta:
model = Alert
fields = ['name','reststrokes','period','emailalert','workouttype','boattype']
widgets = {
'reststrokes':forms.CheckboxInput()
}
class BasePlannedSessionFormSet(BaseFormSet):
def clean(self):
if any(self.serrors):
return
timezones = (
(x,x) for x in pytz.common_timezones
)
# models related to geo data (points, polygon, courses)
@python_2_unicode_compatible
class GeoCourse(models.Model):
manager = models.ForeignKey(Rower,null=True,on_delete=models.SET_NULL)
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 __str__(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,
)
@property
def coord(self):
return course_coord_center(self)
class GeoCourseEditForm(ModelForm):
class Meta:
model = GeoCourse
fields = ['name','country','notes']
widgets = {
'notes': forms.Textarea,
}
@python_2_unicode_compatible
class GeoPolygon(models.Model):
name = models.CharField(max_length=150,blank=True)
course = models.ForeignKey(GeoCourse, blank=True,on_delete=models.CASCADE,related_name='polygons')
order_in_course = models.IntegerField(default=0)
def __str__(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,on_delete=models.CASCADE,related_name='points')
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
# models related to training planning - draft
# Do we need a separate class TestTarget?
class TrainingTarget(models.Model):
rowers = models.ManyToManyField(Rower, related_name='targetathletes',
verbose_name='Athletes')
manager = models.ForeignKey(Rower,related_name='targetmanager',null=True,on_delete=models.CASCADE)
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 __str__(self):
date = self.date
name = self.name
id = self.pk
try:
ownerfirst = self.manager.user.first_name
ownerlast = self.manager.user.last_name
except AttributeError:
ownerfirst = ''
ownerlast = ''
stri = u'#{id}: {n} {d} {ownerfirst} {ownerlast}'.format(
ownerfirst = ownerfirst,
ownerlast = ownerlast,
d = date.strftime('%Y-%m-%d'),
n = name,
id = id,
)
return stri
def check_trainingtarget_on_change(sender,**kwargs):
instance = kwargs.pop('instance',None)
action = kwargs.pop('action',None)
pk_set = kwargs.pop('pk_set',None)
if action == 'pre_add':
for id in pk_set:
rower = Rower.objects.get(id=id)
if not can_plan_user(instance.manager.user,rower):
raise ValidationError("You cannot add this rower. Not your coachee")
m2m_changed.connect(check_trainingtarget_on_change, sender=TrainingTarget.rowers.through)
class TrainingTargetForm(ModelForm):
class Meta:
model = TrainingTarget
fields = ['name','date','notes','rowers']
widgets = {
'date': AdminDateWidget()
}
def __init__(self,*args, **kwargs):
user = kwargs.pop('user',None)
super(TrainingTargetForm, self).__init__(*args, **kwargs)
try:
teams = Team.objects.filter(manager=self.instance.manager.user)
except AttributeError:
if user:
teams = Team.objects.filter(manager=user)
else:
teams = []
if not teams:
self.fields.pop('rowers')
else:
self.fields['rowers'].queryset = Rower.objects.filter(
team__in=teams
).distinct().order_by("user__last_name","user__first_name")
@python_2_unicode_compatible
class TrainingPlan(models.Model):
statuschoices = (
('active','active'),
('deactivated','inactive'),
)
rowers = models.ManyToManyField(Rower,related_name='planathletes',
verbose_name='Athletes')
manager = models.ForeignKey(Rower,related_name='planmanager',null=True,on_delete=models.SET_NULL)
name = models.CharField(max_length=150,blank=True)
status = models.BooleanField(default=True,verbose_name='Active')
target = models.ForeignKey(TrainingTarget,blank=True,null=True,on_delete=models.SET_NULL)
startdate = models.DateField(default=current_day)
notes = models.CharField(blank=True,null=True,max_length=200,verbose_name='Plan Notes')
enddate = models.DateField(
default=half_year_from_now)
def __str__(self):
name = self.name
startdate = self.startdate
enddate = self.enddate
firstname = self.manager.user.first_name
lastname = self.manager.user.last_name
stri = u'Training Plan by {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):
manager = self.manager
if not can_add_plan(manager.user):
raise ValidationError(
"Basic user cannot have a training plan"
)
if self.enddate < self.startdate:
startdate = self.startdate
enddate = self.enddate
self.startdate = enddate
self.enddate = startdate
if not self.enddate <= self.startdate:
super(TrainingPlan,self).save(*args, **kwargs)
# only add athletes that are allowed to be added
for rower in self.rowers.all():
if can_plan_user(manager.user,rower):
self.rowers.add(rower)
else:
self.rowers.remove(rower)
if self.status:
otherplans = TrainingPlan.objects.filter(
status=True).exclude(
pk=self.pk).order_by(
"-startdate")
for otherplan in otherplans:
if otherplan.startdate <= self.enddate and otherplan.startdate >= self.startdate:
for rower in self.rowers.all():
if rower in otherplan.rowers.all():
self.status = False
self.save()
if otherplan.enddate >= self.startdate and otherplan.enddate <= self.enddate:
for rower in self.rowers.all():
if rower in otherplan.rowers.all():
self.status = False
self.save()
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)
def check_trainingplan_on_change(sender, **kwargs):
instance = kwargs.pop('instance',None)
action = kwargs.pop('action',None)
pk_set = kwargs.pop('pk_set',None)
if action == 'pre_add':
for id in pk_set:
rower = Rower.objects.get(id=id)
if not can_plan_user(instance.manager.user,rower):
raise ValidationError("You cannot add this rower. Not your coachee")
m2m_changed.connect(check_trainingplan_on_change, sender=TrainingPlan.rowers.through)
class TrainingPlanForm(ModelForm):
class Meta:
model = TrainingPlan
fields = ['name','target','startdate','enddate','status','notes','rowers']
widgets = {
'startdate': AdminDateWidget(),
'enddate': AdminDateWidget(),
'notes': forms.Textarea()
}
def __init__(self,*args, **kwargs):
targets = kwargs.pop('targets',None)
user = kwargs.pop('user',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
elif self.instance.pk is not None:
self.fields['target'].queryset = TrainingTarget.objects.filter(
manager=self.instance.manager,
date__gte=current_day()).order_by("date")
else:
self.fields.pop('target')
try:
teams = Team.objects.filter(manager=self.instance.manager.user)
except AttributeError:
if user:
teams = Team.objects.filter(manager=user)
else:
teams = []
if not teams:
self.fields.pop('rowers')
else:
self.fields['rowers'].queryset = Rower.objects.filter(
team__in=teams
).distinct().order_by("user__last_name","user__first_name")
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 and 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:]
@python_2_unicode_compatible
class TrainingMacroCycle(models.Model):
plan = models.ForeignKey(TrainingPlan,on_delete=models.CASCADE)
name = models.CharField(max_length=150,blank=True)
startdate = models.DateField(default=current_day)
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 __str__(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()
}
@python_2_unicode_compatible
class TrainingMesoCycle(models.Model):
plan = models.ForeignKey(TrainingMacroCycle,on_delete=models.CASCADE)
name = models.CharField(max_length=150,blank=True)
startdate = models.DateField(default=current_day)
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 __str__(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)
@python_2_unicode_compatible
class TrainingMicroCycle(models.Model):
plan = models.ForeignKey(TrainingMesoCycle,on_delete=models.CASCADE)
name = models.CharField(max_length=150,blank=True)
startdate = models.DateField(default=current_day)
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 __str__(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()
}
regularsessiontypechoices = (
('session','Training Session'),
('challenge','Challenge'),
('test','Mandatory Test'),
('cycletarget','Total for a time period'),
('coursetest','OTW test over a course'),
)
# model for Planned Session (Workout, Challenge, Test)
@python_2_unicode_compatible
class PlannedSession(models.Model):
sessiontypechoices = (
('session','Training Session'),
('challenge','Challenge'),
('test','Mandatory Test'),
('cycletarget','Total for a time period'),
('coursetest','OTW test over a course'),
('race','Virtual challenge'),
('indoorrace','Indoor Virtual challenge'),
)
regularsessiontypechoices = (
('session','Training Session'),
('challenge','Challenge'),
('test','Mandatory Test'),
('cycletarget','Total for a time period'),
('coursetest','OTW test over a course'),
)
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,on_delete=models.PROTECT)
course = models.ForeignKey(GeoCourse,blank=True,null=True,
verbose_name='OTW Course',on_delete=models.SET_NULL)
name = models.CharField(max_length=150,blank=True,
verbose_name='Name')
comment = models.TextField(max_length=1000,blank=True,
)
startdate = models.DateField(default=current_day,
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)
is_template = models.BooleanField(default=False)
def __str__(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):
if self.sessionvalue <= 0:
self.sessionvalue = 1
manager = self.manager
if self.sessiontype not in ['race','indoorrace']:
if not can_add_session(self.manager):
raise ValidationError(
"You must be a Self-Coach user or higher to create a planned session"
)
# 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' or self.sessiontype == 'indoorrace':
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
class StandardCollection(models.Model):
name = models.CharField(max_length=150)
manager = models.ForeignKey(User, null=True,on_delete=models.CASCADE)
notes = models.CharField(blank=True,null=True,max_length=1000)
active = models.BooleanField(default=True)
def __str__(self):
return self.name
class CourseStandard(models.Model):
name = models.CharField(max_length=150,default='')
coursedistance = models.IntegerField(default=0)
coursetime = models.CharField(max_length=100,default="")
referencespeed = models.FloatField(default=5.0) # average boat speed
agemin = models.IntegerField(default=0)
agemax = models.IntegerField(default=120)
boatclass = models.CharField(max_length=150,default='water') # corresponds to workout workouttype
boattype = models.CharField(choices=mytypes.boattypes,max_length=50,default='1x')
sex = models.CharField(max_length=150,default='male')
weightclass = models.CharField(max_length=150,default='hwt')
adaptiveclass = models.CharField(choices=mytypes.adaptivetypes,max_length=50,default="None")
skillclass = models.CharField(max_length=150,default='Open')
standardcollection = models.ForeignKey(StandardCollection,on_delete=models.CASCADE,related_name='standards')
class Meta:
unique_together = (
('name','standardcollection')
)
def __str__(self):
return self.name
registerchoices = (
('windowstart','Start of challenge Window'),
('windowend','End of challenge Window'),
('deadline','Evaluation Closure Deadline'),
('manual','Manual - select below'),
)
@python_2_unicode_compatible
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)
coursestandards = models.ForeignKey(StandardCollection,null=True,on_delete=models.SET_NULL,
verbose_name='Standard Times',blank=True,default=None)
def __str__(self):
name = self.name
startdate = self.startdate
enddate = self.enddate
stri = u'Virtual challenge {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)
class RaceLogo(models.Model):
filename = models.CharField(default='',max_length=150)
creationdatetime = models.DateTimeField()
user = models.ForeignKey(User,on_delete=models.PROTECT)
width = models.IntegerField(default=1200)
height = models.IntegerField(default=600)
race = models.ManyToManyField(VirtualRace,related_name='logos')
def __str__(self):
return self.filename
def delete(self, *args, **kwargs):
os.remove(self.filename)
print('file deleted')
super(RaceLogo,self).delete(*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")
self.fields['sessiontype'].choices = regularsessiontypechoices
class VirtualRaceAthleteForm(ModelForm):
class Meta:
model = PlannedSession
fields = ['rower']
widgets = {
'rower':forms.CheckboxSelectMultiple,
}
def __init__(self, *args, **kwargs):
super(VirtualRaceAthleteForm, self).__init__(*args, **kwargs)
self.fields['rower'].queryset = self.instance.rower.all()
self.fields['subject']= forms.CharField(max_length=255)
self.fields['message'] = forms.CharField(widget=forms.Textarea())
class PlannedSessionTemplateForm(ModelForm):
class Meta:
model = PlannedSession
fields = [
'name',
'sessiontype',
'sessionmode',
'criterium',
'sessionvalue',
'sessionunit',
'course',
'comment',
]
dateTimeOptions = {
'format': 'yyyy-mm-dd',
'autoclose': True,
}
widgets = {
'comment': forms.Textarea,
}
def __init__(self,*args,**kwargs):
super(PlannedSessionTemplateForm, self).__init__(*args, **kwargs)
self.fields['course'].queryset = GeoCourse.objects.all().order_by("country","name")
self.fields['sessiontype'].choices = regularsessiontypechoices
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 IndoorVirtualRaceForm(ModelForm):
registration_closure = forms.SplitDateTimeField(widget=AdminSplitDateTime(),required=False)
evaluation_closure = forms.SplitDateTimeField(widget=AdminSplitDateTime(),required=True)
timezone = forms.ChoiceField(initial='UTC',
choices=[(x,x) for x in pytz.common_timezones],
label='Time Zone')
class Meta:
model = VirtualRace
fields = [
'name',
'startdate',
'start_time',
'enddate',
'end_time',
'timezone',
'sessionvalue',
'sessionunit',
'registration_form',
'registration_closure',
'evaluation_closure',
'comment',
'coursestandards',
'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(),
}
labels = {
'sessionunit': 'Meters or minutes',
'sessionvalue': 'How far or how long'
}
def __init__(self,*args,**kwargs):
timezone = kwargs.pop('timezone',None)
super(IndoorVirtualRaceForm, self).__init__(*args, **kwargs)
self.fields['sessionunit'].choices = [('min','minutes'),('m','meters')]
self.fields['sessionvalue'].initial = 2000
self.fields['sessionunit'].initial = 'm'
if timezone:
self.fields['timezone'].initial = timezone
self.fields['coursestandards'].queryset = StandardCollection.objects.filter(active=True)
def clean(self):
cd = self.cleaned_data
timezone_str = cd['timezone']
value = cd['sessionvalue']
if value <= 0:
raise forms.ValidationError('The Value must be a positive, non-zero value')
unit = cd['sessionunit']
if unit == 'm' and value < 100:
raise forms.ValidationError('Minimum distance is 100m')
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 challenge Window should be before the End of the challenge Window")
if cd['evaluation_closure'] <= enddatetime:
raise forms.ValidationError("Evaluation closure deadline should be after the challenge Window closes")
if cd['evaluation_closure'] <= timezone.now():
raise forms.ValidationError("Evaluation closure cannot be in the past")
return cd
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',
'coursestandards',
'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")
self.fields['coursestandards'].queryset = StandardCollection.objects.filter(active=True)
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
)
try:
registration_closure = cd['registration_closure']
except KeyError:
registration_closure = enddatetime+datetime.timedelta(days=1)
cd['registration_closure'] = 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 challenge Window should be before the End of the challenge Window")
if cd['evaluation_closure'] <= enddatetime:
raise forms.ValidationError("Evaluation closure deadline should be after the challenge Window closes")
if cd['evaluation_closure'] <= timezone.now():
raise forms.ValidationError("Evaluation closure cannot be in the past")
return cd
class PlannedSessionFormSmall(ModelForm):
regularsessiontypechoices = (
('session','Training Session'),
('challenge','Challenge'),
('test','Mandatory Test'),
('cycletarget','Total for a time period'),
('coursetest','OTW test over a course'),
)
class Meta:
model = PlannedSession
fields = ['startdate',
'enddate',
'preferreddate',
'name',
'sessiontype',
'sessionmode',
'criterium',
'sessionvalue',
'sessionunit',
'manager',
]
dateTimeOptions = {
'format': '%Y-%m-%d',
'autoclose': True,
}
input_formats=('%Y-%m-%d')
widgets = {
'startdate': DateInput(attrs={'size':10, 'class':'datepicker'}, format='%Y-%m-%d'),
'enddate': DateInput(attrs={'size':10, 'class':'datepicker'}, format='%Y-%m-%d'),
'preferreddate': DateInput(attrs={'size':10, 'class':'datepicker'}, format='%Y-%m-%d'),
'name': forms.TextInput(attrs={'size':10}),
'sessionvalue': forms.TextInput(attrs={'style':'width:5em',
'type':'number'}),
'manager': forms.HiddenInput(),
}
def __init__(self,*args,**kwargs):
super(PlannedSessionFormSmall, self).__init__(*args, **kwargs)
self.fields['sessiontype'].choices = regularsessiontypechoices
boattypes = mytypes.boattypes
# Workout
class Workout(models.Model):
workouttypes = mytypes.workouttypes
workoutsources = mytypes.workoutsources
privacychoices = mytypes.privacychoices
adaptivetypes = mytypes.adaptivetypes
user = models.ForeignKey(Rower,on_delete=models.CASCADE)
team = models.ManyToManyField(Team,blank=True)
plannedsession = models.ForeignKey(PlannedSession, blank=True,null=True,
verbose_name='Session',on_delete=models.SET_NULL)
name = models.CharField(max_length=150,blank=True,null=True)
date = models.DateField(blank=True,null=True)
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')
adaptiveclass = models.CharField(choices=adaptivetypes,max_length=50,
default='None',
verbose_name='Adaptive Classification')
starttime = models.TimeField(default=datetime.time(12,0))
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(blank=True)
dragfactor = models.IntegerField(default=0,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.BigIntegerField(blank=True,null=True)
uploadedtostrava = models.BigIntegerField(default=0)
uploadedtosporttracks = models.BigIntegerField(default=0)
uploadedtounderarmour = models.BigIntegerField(default=0)
uploadedtotp = models.BigIntegerField(default=0)
uploadedtorunkeeper = models.BigIntegerField(default=0)
uploadedtogarmin = models.BigIntegerField(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')
duplicate = models.BooleanField(default=False,verbose_name='Duplicate Workout')
impeller = models.BooleanField(default=False,verbose_name='Impeller')
def save(self, *args, **kwargs):
user = self.user
if self.notes is not None and len(self.notes)>1000:
self.notes = self.notes[0:950]
if not can_add_workout(user.user):
raise forms.ValidationError("Free Coach User cannot have any workouts")
super(Workout, self).save(*args, **kwargs)
def __str__(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} {workouttype} {ownerfirst} {ownerlast}'.format(
d = date.strftime('%Y-%m-%d'),
n = name,
dist = distance,
duration = duration.strftime("%H:%M:%S"),
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
class TombStone(models.Model):
user = models.ForeignKey(Rower,on_delete=models.CASCADE)
uploadedtoc2 = models.IntegerField(default=0)
uploadedtostrava = models.BigIntegerField(default=0)
uploadedtosporttracks = models.BigIntegerField(default=0)
uploadedtounderarmour = models.BigIntegerField(default=0)
uploadedtotp = models.BigIntegerField(default=0)
uploadedtorunkeeper = models.BigIntegerField(default=0)
@receiver(models.signals.pre_delete,sender=Workout)
def create_tombstone_on_delete(sender, instance, **kwargs):
t = TombStone(
user=instance.user,
uploadedtoc2 = instance.uploadedtoc2,
uploadedtostrava = instance.uploadedtostrava,
uploadedtounderarmour = instance.uploadedtounderarmour,
uploadedtotp = instance.uploadedtotp,
uploadedtorunkeeper = instance.uploadedtorunkeeper,
)
t.save()
# 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')
# remove parquet file
try:
dirname = 'media/strokedata_{id}.parquet.gz'.format(id=instance.id)
shutil.rmtree(dirname)
except FileNotFoundError:
pass
@receiver(models.signals.post_delete,sender=Workout)
def update_duplicates_on_delete(sender, instance, **kwargs):
if instance.id:
duplicates = Workout.objects.filter(
user=instance.user,date=instance.date,
duplicate=True)
for d in duplicates:
t = d.duration
delta = datetime.timedelta(hours=t.hour, minutes=t.minute, seconds=t.second)
workoutenddatetime = d.startdatetime+delta
ws = Workout.objects.filter(
user=d.user,date=d.date,
).exclude(
pk__in=[instance.pk,d.pk]
).exclude(
startdatetime__gt=workoutenddatetime
)
ws2 = []
for ww in ws:
t = ww.duration
delta = datetime.timedelta(hours=t.hour, minutes=t.minute, seconds=t.second)
enddatetime = ww.startdatetime+delta
if enddatetime > d.startdatetime:
ws2.append(ww)
if len(ws2) == 0:
d.duplicate=False
d.save()
# 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()
class VirtualRaceFollower(models.Model):
user = models.ForeignKey(User,on_delete=models.CASCADE,null=True)
race = models.ForeignKey(VirtualRace,on_delete=models.CASCADE)
emailaddress = models.EmailField(max_length=254,blank=True,null=True,
verbose_name="Email Address")
class FollowerForm(ModelForm):
class Meta:
model = VirtualRaceFollower
fields = ['emailaddress']
# Virtual Race results (for keeping results when workouts are deleted)
@python_2_unicode_compatible
class VirtualRaceResult(models.Model):
boatclasses = (type for type in mytypes.workouttypes if type[0] in mytypes.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')
adaptiveclass = models.CharField(default="None",max_length=50,
choices=mytypes.adaptivetypes,
verbose_name="Adaptive Class")
skillclass = models.CharField(default="Open",max_length=50,
verbose_name="Skill Class")
race = models.ForeignKey(VirtualRace,on_delete=models.CASCADE)
duration = models.TimeField(default=datetime.time(1,0))
distance = models.IntegerField(default=0)
points = models.FloatField(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)
emailnotifications = models.BooleanField(default=True,
verbose_name = 'Receive challenge notifications by email')
startsecond = models.FloatField(default=0)
endsecond = models.FloatField(default=0)
referencespeed = models.FloatField(default=5.0)
entrycategory = models.ForeignKey(CourseStandard,null=True,on_delete=models.SET_NULL,
verbose_name='Group')
acceptsocialmedia = models.BooleanField(default=True,
verbose_name = 'I agree with sharing my name in challenge related social media posts (unchecking this does not prevent you from participation)')
def isduplicate(self,other):
if self.userid != other.userid:
return False
if self.weightcategory != other.weightcategory:
return False
if self.adaptiveclass != other.adaptiveclass:
return False
if self.skillclass != other.skillclass:
return False
if self.race != other.race:
return False
if self.boatclass != other.boatclass:
return False
if self.boattype != other.boattype:
return False
if self.sex != other.sex:
return False
if self.entrycategory is not None and other.entrycategory is not None:
if self.entrycategory != other.entrycategory:
return False
elif self.entrycategory is None and other.entrycateogry is not None:
return False
elif self.entrycategory is not None and other.entrycategory is None:
return False
return True
def __str__(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:
if self.entrycategory:
return u'Entry for {n} for "{r}" in {g} with {t}'.format(
n = name,
r = self.race,
g = self.entrycategory,
t = 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:
if self.entrycategory:
return u'Entry for {n} for "{r}" in {g}'.format(
n = name,
r = self.race,
g = self.entrycategory,
)
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,
)
# Virtual Race results (for keeping results when workouts are deleted)
@python_2_unicode_compatible
class IndoorVirtualRaceResult(models.Model):
boatclasses = (type for type in mytypes.workouttypes if type[0] in mytypes.otetypes)
userid = models.IntegerField(default=0) # ID of rower object
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')
adaptiveclass = models.CharField(default="None",max_length=50,
choices=mytypes.adaptivetypes,
verbose_name="Adaptive Class")
skillclass = models.CharField(default="Open",max_length=50,
verbose_name="Skill Class")
race = models.ForeignKey(VirtualRace,on_delete=models.CASCADE)
duration = models.TimeField(default=datetime.time(1,0))
distance = models.IntegerField(default=0)
referencespeed = models.FloatField(default=5.0)
points = models.FloatField(default=0)
boatclass = models.CharField(choices=boatclasses,
max_length=40,
default='rower',
verbose_name = 'Ergometer Class')
coursecompleted = models.BooleanField(default=False)
sex = models.CharField(default="not specified",
max_length=30,
choices=sexcategories,
verbose_name='Gender')
age = models.IntegerField(null=True)
emailnotifications = models.BooleanField(default=True,
verbose_name = 'Receive challenge notifications by email')
entrycategory = models.ForeignKey(CourseStandard,null=True,on_delete=models.SET_NULL,
verbose_name='Group')
acceptsocialmedia = models.BooleanField(default=True,
verbose_name = 'I agree with sharing my name in challenge related social media posts (unchecking this does not prevent you from participation)')
def isduplicate(self,other):
if self.userid != other.userid:
return False
if self.weightcategory != other.weightcategory:
return False
if self.adaptiveclass != other.adaptiveclass:
return False
if self.skillclass != other.skillclass:
return False
if self.race != other.race:
return False
if self.boatclass != other.boatclass:
return False
if self.sex != other.sex:
return False
if self.entrycategory is not None and other.entrycategory is not None:
if self.entrycategory != other.entrycategory:
return False
elif self.entrycategory is None and other.entrycategory is not None:
return False
elif self.entrycategory is not None and other.entrycategory is None:
return False
return True
def __str__(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:
if self.entrycategory:
return u'Entry for {n} for "{r}" in {g} with {t}'.format(
n = name,
r = self.race,
g = self.entrycategory,
t = self.teamname,
)
return u'Entry for {n} for "{r}" on {c} with {t} ({s})'.format(
n = name,
r = self.race,
t = self.teamname,
c = self.boatclass,
s = self.sex,
)
else:
if self.entrycategory:
return u'Entry for {n} for "{r}" in {g}'.format(
n = name,
r = self.race,
g = self.entrycategory,
)
return u'Entry for {n} for "{r}" on {c} ({s})'.format(
n = name,
r = self.race,
c = self.boatclass,
s = self.sex,
)
class CourseTestResult(models.Model):
userid = models.IntegerField(default=0)
workoutid = models.IntegerField(null=True)
plannedsession = models.ForeignKey(PlannedSession,on_delete=models.CASCADE)
duration = models.TimeField(default=datetime.time(1,0))
distance = models.IntegerField(default=0)
coursecompleted = models.BooleanField(default=False)
class IndoorVirtualRaceResultForm(ModelForm):
class Meta:
model = IndoorVirtualRaceResult
fields = ['teamname','weightcategory','boatclass','age','adaptiveclass',
'entrycategory','acceptsocialmedia'
]
def __init__(self, *args, **kwargs):
categories = kwargs.pop('categories',None)
super(IndoorVirtualRaceResultForm, self).__init__(*args, **kwargs)
if categories is not None:
self.fields['entrycategory'].queryset = categories
self.fields['entrycategory'].empty_label = None
else:
self.fields.pop('entrycategory')
class VirtualRaceResultForm(ModelForm):
class Meta:
model = VirtualRaceResult
fields = ['teamname','weightcategory','boatclass','boattype',
'age','adaptiveclass',
'entrycategory','acceptsocialmedia'
]
def __init__(self, *args, **kwargs):
boattypes = kwargs.pop('boattypes',None)
categories = kwargs.pop('categories',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')
if categories is not None:
self.fields['entrycategory'].queryset = categories
self.fields['entrycategory'].empty_label = None
else:
self.fields.pop('entrycategory')
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),
'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__': 'rowers.models', '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
@python_2_unicode_compatible
class GraphImage(models.Model):
filename = models.CharField(default='',max_length=150,blank=True,null=True)
creationdatetime = models.DateTimeField()
workout = models.ForeignKey(Workout,on_delete=models.CASCADE)
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 others.count() == 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',
'dragfactor',
'weightcategory',
'adaptiveclass',
'notes',
'rankingpiece',
'duplicate',
'plannedsession']
widgets = {
'date': AdminDateWidget(),
'starttime': AdminTimeWidget(),
'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
workout = self.instance
sps = PlannedSession.objects.filter(
rower__in=[workout.user],
startdate__lte=workout.date,
enddate__gte=workout.date,
).order_by("preferreddate","startdate","enddate").exclude(
sessiontype__in=['race','indoorrace'])
if not sps:
del self.fields['plannedsession']
else:
self.fields['plannedsession'].queryset = sps
else:
del self.fields['plannedsession']
# Used for the rowing physics calculations
class AdvancedWorkoutForm(ModelForm):
#quick_calc = forms.BooleanField(initial=True,required=False)
#go_service = forms.BooleanField(initial=False,required=False,label='Experimental')
class Meta:
model = Workout
fields = ['boattype','weightvalue']
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 collect rower's Billing Info
class RowerBillingAddressForm(ModelForm):
class Meta:
model = Rower
fields = [
'street_address',
'city',
'postal_code',
'country'
]
def __init__(self, *args, **kwargs):
super(RowerBillingAddressForm, self).__init__(*args, **kwargs)
self.fields['country'].required = True
# Form to set rower's Email and Weight category
class AccountRowerForm(ModelForm):
class Meta:
model = Rower
fields = ['sex','birthdate','weightcategory',
'adaptiveclass',
'getemailnotifications',
'getimportantemails',
'defaulttimezone','showfavoritechartnotes',
'defaultlandingpage',
'offercoaching','autojoin']
widgets = {
'birthdate': SelectDateWidget(
years=range(
timezone.now().year-100,timezone.now().year-10)),
}
def __init__(self, *args, **kwargs):
super(AccountRowerForm, self).__init__(*args, **kwargs)
if 'coach' not in self.instance.rowerplan:
self.fields.pop('offercoaching')
# Form to set static chart settings
class StaticChartRowerForm(ModelForm):
class Meta:
model = Rower
fields = ['staticgrids','slowpaceerg','fastpaceerg','slowpaceotw','fastpaceotw']
def __init__(self, *args, **kwargs):
super(StaticChartRowerForm, self).__init__(*args, **kwargs)
self.fields['staticgrids'].required = False
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 = int(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 = int(self.data['ut2'])
except ValueError:
ut2 = 0
try:
ut1 = self.cleaned_data['ut1']
except:
try:
ut1 = int(self.data['ut1'])
except ValueError:
ut1 = 0
try:
at = self.cleaned_data['at']
except:
try:
at = int(self.data['at'])
except ValueError:
at = 0
try:
an = self.cleaned_data['an']
except:
try:
an = int(self.data['an'])
except ValueError:
an = 0
try:
tr = self.cleaned_data['tr']
except:
try:
tr = int(self.data['tr'])
except ValueError:
tr = 0
try:
max = self.cleaned_data['max']
except:
try:
max = int(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=current_day)
announcement = models.TextField(max_length=280)
expires = models.DateField(default=current_day)
modified = models.DateField(default=current_day)
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
@python_2_unicode_compatible
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,on_delete=models.PROTECT)
workout = models.ForeignKey(Workout,on_delete=models.CASCADE)
def __str__(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,
}
# A comment by a user on a training
@python_2_unicode_compatible
class PlannedSessionComment(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,on_delete=models.PROTECT)
plannedsession = models.ForeignKey(PlannedSession,on_delete=models.CASCADE)
def __str__(self):
return u'Comment to: {w} by {u1} {u2}'.format(
w=self.workout,
u1 = self.user.first_name,
u2 = self.user.last_name,
)
class PlannedSessionCommentForm(ModelForm):
class Meta:
model = PlannedSessionComment
fields = ['comment','notification']
widgets = {
'comment': forms.Textarea,
}
class BlogPost(models.Model):
title = models.TextField(max_length=300)
link = models.TextField(max_length=300)
date = models.DateField()
defaultgroups = ['basic']
class VideoAnalysis(models.Model):
name = models.CharField(default='', max_length=150,blank=True,null=True)
video_id = models.CharField(default='',max_length=150)
delay = models.IntegerField(default=0)
workout = models.ForeignKey(Workout, on_delete=models.CASCADE)
metricsgroups = TemplateListField(default=defaultgroups)
class Meta:
unique_together = ('video_id','workout')
def __str__(self):
return self.name
class ShareKey(models.Model):
location = models.TextField() # absolute path
token = models.CharField(max_length=40, primary_key=True)
creation_date = models.DateTimeField(auto_now_add=True)
expiration_seconds = models.BigIntegerField()
@property
def expired(self):
return self.creation_date + datetime.timedelta(self.expiration_seconds) < timezone.now()
@property
def expiration_date(self):
return self.creation_date + datetime.timedelta(self.expiration_seconds)