Private
Public Access
1
0
Files
rowsandall/rowers/models.py
2021-02-13 13:14:11 +01:00

4398 lines
147 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.opaque import encoder
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 *
from rowers.formfields import *
timezones = (
(x,x) for x in pytz.common_timezones
)
favanalysischoices = (
('compare','Compare'),
('flexall','Cumulative Flex Chart'),
('histogram','Histogram'),
('stats','Statistics'),
('boxplot','Box Chart'),
('trendflex','Trend Flex'),
('cp','Critical Power'),
)
smoothingchoices = (
(1,1),
(2,2),
(4,4),
(8,8),
(16,16),
)
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 Emails field
class AlternativeEmails(models.TextField):
def __init__(self, *args, **kwargs):
self.token = kwargs.pop('token',',')
super(AlternativeEmails, 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))
newlist = []
for s in value:
try:
validate_email(s)
newlist.append(s)
except ValidationError:
pass
return self.token.join([str(s) for s in newlist])
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.loc[nr,'Distance'] = row['Record'][:-1]
df.loc[nr,'Duration'] = 60*row['Event']
else:
df.loc[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.loc[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'
def __str_(self):
stri = 'Calculated World Class Performance for {s}, {a}, {d} secs, {p} Watts'.format(
s = self.sex,
a = self.age,
d = self.duration,
p = self.power
)
return stri
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]
try:
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])
except IndexError:
deltastart = 0
deltafinish = 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 = (
('match','Variable - try to match the type'),
('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'),
)
cppresets = (
(42,'6 weeks'),
(91,'13 weeks'),
(183,'26 weeks'),
(365,'a year')
)
plotchoices = (
('timeplot','Time Plot'),
('distanceplot','Distance Plot'),
('pieplot','Heart Rate Pie Chart'),
('hrpieplot','Power Pie Chart'),
('None','None'),
)
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")
cprange = models.IntegerField(default=42,verbose_name="Range for calculation of breakthrough workouts and fitness (CP)",
choices=cppresets)
otwslack = models.IntegerField(default=0,verbose_name="OTW Power slack")
# performance manager stuff
kfit = models.IntegerField(default=42,verbose_name='Fitness Time Decay Constant (days)')
kfatigue = models.IntegerField(default=7,verbose_name='Fatigue Time Decay Constant (days)')
showfit = models.BooleanField(default=False)
showfresh = models.BooleanField(default=False)
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'])
hrzones = PowerZonesField(default=['Rest',
'UT2',
'UT1',
'AT',
'TR',
'AN','max'])
emailalternatives = AlternativeEmails(default=[],null=True,blank=True,verbose_name='Alternative Email addresses (separate with ",")')
# 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)
rp3token = models.TextField(default='',max_length=1000,blank=True,null=True)
rp3tokenexpirydate = models.DateTimeField(blank=True,null=True)
rp3refreshtoken = models.TextField(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="match",
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)
strava_auto_delete = 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')
fav_analysis = models.CharField(default='compare',choices=favanalysischoices,
max_length=100,
verbose_name='Favorite Analysis')
usersmooth = models.IntegerField(default=1,choices=smoothingchoices,
verbose_name="Chart Smoothing")
staticchartonupload = models.CharField(default='None',choices=plotchoices,
max_length=100,
verbose_name='Generate a static chart automatically on upload')
# 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'{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'),
('fastest_distance','Finds fastest time over a given distance on the water'),
('fastest_time','Finds largest distance rowed on the water over a given time'),
)
# 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'),
('fastest_distance','Finds fastest time over a given distance on the water'),
('fastest_time','Finds largest distance rowed on the water over a given time'),
('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'),
('fastest_distance','Finds fastest time over a given distance on the water'),
('fastest_time','Finds largest distance rowed on the water over a given time'),
)
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')
approximate_distance = models.IntegerField(default=10000,verbose_name='Approximate Distance')
approximate_duration = models.IntegerField(default=60,verbose_name='Approximate Duration')
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)
fitfile = models.FileField(blank=True,null=True)
json_string = models.TextField(max_length=10000,default=None,blank=True,null=True)
interval_string = models.TextField(max_length=1000,default=None,blank=True,null=True,
verbose_name='Interval String (optional)')
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 in ['test','indoorrace','fastest_distance','fastest_time']:
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',
'interval_string',
'fitfile',
]
dateTimeOptions = {
'format': 'yyyy-mm-dd',
'autoclose': True,
}
widgets = {
'comment': forms.Textarea,
'startdate': AdminDateWidget(),
'enddate': AdminDateWidget(),
'preferreddate': AdminDateWidget(),
'interval_string': forms.Textarea(attrs={'rows':2,'cols':50})
}
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 = GroupedModelChoiceField(
queryset = GeoCourse.objects, empty_label=None,
choices_groupby='country'
)
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
)
try:
end_time = cd['end_time']
except KeyError:
raise forms.ValidationError(
'Must have end time',
code='missing endtime'
)
if end_time is None:
raise forms.ValidationError(
'Must have end time',
code='missing endtime'
)
try:
end_date = cd['enddate']
except KeyError:
raise forms.ValidationError(
'Missing or invalid end date',
code='missing end date'
)
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'),
('fastest_distance','Finds fastest time over a given distance on the water'),
('fastest_time','Finds largest distance rowed on the water over a given time'),
)
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
boatbrands = mytypes.boatbrands
rpechoices = (
(0,'Not Specified'),
(1,'1 Very Easy (a walk in the park)'), # 20 TSS / hour
(2,'2 Easy (You breathe normally, it feels comfortable)'), # 30 TSS / hour
(3,'3 Somewhat easy (You can talk easily but did you notice the beautiful clouds?)'),
(4,'4 Moderate (You can talk in short spurts, breathing more labored, this feels just right)'), # 50 TSS/hour
(5,"5 (It's not that painful, you just don't want to be here all day.)"),
(6,'6 Somewhat Hard (You can say a few words if you need to)'), # 70 TSS / hour
(7,'7 Vigorous (This is starting to get painful)'),
(8,"8 Hard (You can barely talk, breathing heavily, hoping you won't have to this that long)"), # 100 TSS / hour
(9,'9 Very Hard (My goodness, please make it stop)'), # 120 TSS / hour
(10,'10 Max Effort (You can barely remember your name, you would rather rip out your toenails than go through this)') # 140 TSS / hour
)
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')
boatbrand = models.CharField(choices=boatbrands,max_length=50,
default='',verbose_name = 'Boat Brand')
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)
# scores
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)
goldmedalstandard = models.FloatField(default=-1,blank=True,verbose_name='Gold Medal Standard')
goldmedalseconds = models.IntegerField(default=0,blank=True,verbose_name='Gold Medal Seconds')
rpe = models.IntegerField(default=0,blank=True,choices=rpechoices,
verbose_name='Rate of Perceived Exertion')
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)
uploadedtorp3 = 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 url(self):
str = '/rowers/workout/{id}/'.format(
id = encoder.encode_hex(self.id)
)
url = settings.SITE_URL+str
return url
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
# remove parquet file
try:
dirname = 'media/cpdata_{id}.parquet.gz'.format(id=instance.id)
os.remove(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,related_name='entries')
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')
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')
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)')
startsecond = models.FloatField(default=0)
endsecond = models.FloatField(default=0)
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)
startsecond = models.FloatField(default=0)
endsecond = models.FloatField(default=0)
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',
'rpe',
'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','boatbrand']
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',
'strava_auto_delete',
'trainingpeaks_auto_export',
]
# Simple form to set rower's Functional Threshold Power
class RowerPowerForm(ModelForm):
class Meta:
model = Rower
fields = ['hrftp','ftp','otwslack']
class RowerCPForm(ModelForm):
class Meta:
model = Rower
fields = ['cprange','kfit','kfatigue']
# Form to set rower's Power zones, including test routines
# to enable consistency
class RowerHRZonesForm(ModelForm):
hrzones = ['Rest','UT2','UT1','AT','TR','AN','Max']
hrrestname = forms.CharField(initial=hrzones[0])
hrut2name = forms.CharField(initial=hrzones[1])
hrut1name = forms.CharField(initial=hrzones[2])
hratname = forms.CharField(initial=hrzones[3])
hrtrname = forms.CharField(initial=hrzones[4])
hranname = forms.CharField(initial=hrzones[5])
hrmaxname = forms.CharField(initial=hrzones[6])
def __init__(self, *args,**kwargs):
super(RowerHRZonesForm, self).__init__(*args, **kwargs)
if 'instance' in kwargs:
hrzones = kwargs['instance'].hrzones
else:
hrzones = ['Rest','UT2','UT1','AT','TR','AN','Max']
self.fields['hrrestname'].initial = hrzones[0]
self.fields['hrut2name'].initial = hrzones[1]
self.fields['hrut1name'].initial = hrzones[2]
self.fields['hratname'].initial = hrzones[3]
self.fields['hrtrname'].initial = hrzones[4]
self.fields['hranname'].initial = hrzones[5]
self.fields['hrmaxname'].initial = hrzones[6]
class Meta:
model = Rower
fields = ['rest','ut2','ut1','at','tr','an','max']
def clean(self):
cleaned_data = super(RowerHRZonesForm, self).clean()
try:
rest = cleaned_data['rest']
except KeyError:
raise ValidationError("Value cannot be empty")
except:
rest = int(self.data['rest'])
try:
ut2 = cleaned_data['ut2']
except KeyError:
raise ValidationError("Value cannot be empty")
except:
ut2 = int(self.data['ut2'])
try:
ut1 = cleaned_data['ut1']
except KeyError:
raise ValidationError("Value cannot be empty")
except:
ut1 = int(self.data['ut1'])
try:
at = cleaned_data['at']
except KeyError:
raise ValidationError("Value cannot be empty")
except:
at = int(self.data['at'])
try:
tr = cleaned_data['tr']
except KeyError:
raise ValidationError("Value cannot be empty")
except:
tr = int(self.data['tr'])
try:
an = cleaned_data['an']
except KeyError:
raise ValidationError("Value cannot be empty")
except:
an = int(self.data['an'])
try:
max = cleaned_data['max']
except KeyError:
raise ValidationError("Value cannot be empty")
except:
max = int(self.data['max'])
try:
hrrestname = cleaned_data['hrrestname']
except:
hrrestname = 'Rest'
cleaned_data['hrut3name'] = 'Rest'
try:
hrut2name = cleaned_data['hrut2name']
except:
hrut2name = 'UT2'
cleaned_data['hrut2name'] = 'UT2'
try:
hrut1name = cleaned_data['hrut1name']
except:
hrut1name = 'UT1'
cleaned_data['hrut1name'] = 'UT1'
try:
hratname = cleaned_data['hratname']
except:
hratname = 'AT'
cleaned_data['hratname'] = 'AT'
try:
hrtrname = cleaned_data['hrtrname']
except:
hrtrname = 'TR'
cleaned_data['hrtrname'] = 'TR'
try:
hranname = cleaned_data['hranname']
except:
hranname = 'AN'
cleaned_data['hranname'] = 'AN'
try:
hrmaxname = cleaned_data['hrmaxname']
except:
hrmaxname = 'Max'
cleaned_data['hrmaxname'] = 'Max'
if rest >= ut2:
e = "{ut2name} should be higher than {restname}".format(
restname=hrrestname,
ut2name=hrut2name
)
raise forms.ValidationError(e)
if ut1 <= ut2:
e = "{ut1name} should be higher than {ut2name}".format(
ut1name = hrut1name,
ut2name= hrut2name,
)
raise forms.ValidationError(e)
if at <= ut1:
e = "{atname} should be higher than {ut1name}".format(
atname = hratname,
ut1name= hrut1name,
)
raise forms.ValidationError(e)
if tr <= at:
e = "{trname} should be higher than {atname}".format(
atname = hratname,
trname= hrtrname,
)
raise forms.ValidationError(e)
if an <= tr:
e = "{anname} should be higher than {trname}".format(
anname = hranname,
trname= hrtrname,
)
raise forms.ValidationError(e)
if max <= an:
e = "{anname} should be lower than {maxname}".format(
anname=hranname,
maxname=hrmaxname,
)
return cleaned_data
# 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',
'fav_analysis',
'usersmooth',
'defaultlandingpage',
'offercoaching','autojoin','emailalternatives']
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')
try:
self.initial['emailalternatives'] = ', '.join(self.instance.emailalternatives)
except TypeError:
pass
def clean(self):
cd = self.cleaned_data
z = "".join(cd['emailalternatives'].split()).split(',')
emailalternatives = []
for addr in z:
try:
validate_email(addr)
match = User.objects.filter(email__iexact=addr)
if match.count() == 0:
emailalternatives.append(addr)
except ValidationError:
pass
self.cleaned_data['emailalternatives'] = emailalternatives
# Form to set static chart settings
class StaticChartRowerForm(ModelForm):
class Meta:
model = Rower
fields = ['usersmooth','staticgrids','slowpaceerg','fastpaceerg','slowpaceotw','fastpaceotw','staticchartonupload','fav_analysis']
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).exclude(id=self.instance.id)
if match.count() == 0:
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)