1714 lines
55 KiB
Python
1714 lines
55 KiB
Python
from __future__ import unicode_literals
|
|
|
|
from django.db import models
|
|
from django.contrib.auth.models import User
|
|
from django.core.validators import validate_email
|
|
from django.core.exceptions import ValidationError
|
|
from django import forms
|
|
from django.forms import ModelForm
|
|
from django.dispatch import receiver
|
|
from django.forms.widgets import SplitDateTimeWidget
|
|
from django.forms.extras.widgets import SelectDateWidget
|
|
from django.forms.formsets import BaseFormSet
|
|
from django.contrib.admin.widgets import AdminDateWidget
|
|
from datetimewidget.widgets import DateTimeWidget
|
|
from django.core.validators import validate_email
|
|
import os
|
|
import twitter
|
|
import re
|
|
import pytz
|
|
from django.conf import settings
|
|
from sqlalchemy import create_engine
|
|
import sqlalchemy as sa
|
|
from sqlite3 import OperationalError
|
|
from django.utils import timezone
|
|
import pandas as pd
|
|
from dateutil import parser
|
|
import datetime
|
|
from django.core.exceptions import ValidationError
|
|
from rowers.rows import validate_file_extension
|
|
from collections import OrderedDict
|
|
|
|
import types
|
|
|
|
from rowsandall_app.settings import (
|
|
TWEET_ACCESS_TOKEN_KEY,
|
|
TWEET_ACCESS_TOKEN_SECRET,
|
|
TWEET_CONSUMER_KEY,
|
|
TWEET_CONSUMER_SECRET,
|
|
)
|
|
|
|
tweetapi = twitter.Api(consumer_key=TWEET_CONSUMER_KEY,
|
|
consumer_secret=TWEET_CONSUMER_SECRET,
|
|
access_token_key=TWEET_ACCESS_TOKEN_KEY,
|
|
access_token_secret=TWEET_ACCESS_TOKEN_SECRET)
|
|
|
|
user = settings.DATABASES['default']['USER']
|
|
password = settings.DATABASES['default']['PASSWORD']
|
|
database_name = settings.DATABASES['default']['NAME']
|
|
host = settings.DATABASES['default']['HOST']
|
|
port = settings.DATABASES['default']['PORT']
|
|
|
|
database_url = 'mysql://{user}:{password}@{host}:{port}/{database_name}'.format(
|
|
user=user,
|
|
password=password,
|
|
database_name=database_name,
|
|
host=host,
|
|
port=port,
|
|
)
|
|
|
|
if settings.DEBUG or user=='':
|
|
database_url = 'sqlite:///db.sqlite3'
|
|
|
|
timezones = (
|
|
(x,x) for x in pytz.common_timezones
|
|
)
|
|
|
|
class UserFullnameChoiceField(forms.ModelChoiceField):
|
|
def label_from_instance(self,obj):
|
|
return obj.get_full_name()
|
|
|
|
# model for configurable template field
|
|
class TemplateListField(models.TextField):
|
|
def __init__(self, *args, **kwargs):
|
|
self.token = kwargs.pop('token',',')
|
|
super(TemplateListField, self).__init__(*args, **kwargs)
|
|
|
|
def to_python(self, value):
|
|
if not value: return
|
|
if isinstance(value, list):
|
|
return value
|
|
# remove double quotes and brackets
|
|
value = re.sub(r'u\"','',value)
|
|
value = re.sub(r'u\'','',value)
|
|
value = re.sub(r'\\','',value)
|
|
value = re.sub(r'\"','',value)
|
|
value = re.sub(r'\'','',value)
|
|
value = re.sub(r'\[','',value)
|
|
value = re.sub(r'\]','',value)
|
|
value = re.sub(r'\[\[','[',value)
|
|
value = re.sub(r'\]\]',']',value)
|
|
value = re.sub(r'\ \ ',' ',value)
|
|
value = re.sub(r', ',',',value)
|
|
|
|
return value.split(self.token)
|
|
|
|
def from_db_value(self,value, expression, connection, context):
|
|
if value is None:
|
|
return value
|
|
if isinstance(value, list):
|
|
return value
|
|
return value.split(self.token)
|
|
|
|
def get_db_prep_value(self, value, connection, prepared=False):
|
|
if not value: return
|
|
assert(isinstance(value, list) or isinstance(value, tuple))
|
|
return self.token.join([unicode(s) for s in value])
|
|
|
|
def value_to_string(self, obj):
|
|
value = self._get_val_from_obj(obj)
|
|
return self.get_deb_prep_value(value)
|
|
|
|
# model for Power Zone names
|
|
class PowerZonesField(models.TextField):
|
|
# __metaclass__ = models.SubfieldBase
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
self.token = kwargs.pop('token',',')
|
|
super(PowerZonesField, self).__init__(*args, **kwargs)
|
|
|
|
def to_python(self, value):
|
|
if not value: return
|
|
if isinstance(value, list):
|
|
return value
|
|
# remove double quotes and brackets
|
|
value = re.sub(r'u\"','',value)
|
|
value = re.sub(r'u\'','',value)
|
|
value = re.sub(r'\\','',value)
|
|
value = re.sub(r'\"','',value)
|
|
value = re.sub(r'\'','',value)
|
|
value = re.sub(r'\[','',value)
|
|
value = re.sub(r'\]','',value)
|
|
value = re.sub(r'\[\[','[',value)
|
|
value = re.sub(r'\]\]',']',value)
|
|
value = re.sub(r'\ \ ',' ',value)
|
|
value = re.sub(r', ',',',value)
|
|
|
|
return value.split(self.token)
|
|
|
|
def from_db_value(self,value, expression, connection, context):
|
|
if value is None:
|
|
return value
|
|
if isinstance(value, list):
|
|
return value
|
|
return value.split(self.token)
|
|
|
|
def get_db_prep_value(self, value, connection, prepared=False):
|
|
if not value: return
|
|
assert(isinstance(value, list) or isinstance(value, tuple))
|
|
return self.token.join([unicode(s) for s in value])
|
|
|
|
def value_to_string(self, obj):
|
|
value = self._get_val_from_obj(obj)
|
|
return self.get_deb_prep_value(value)
|
|
|
|
|
|
c2url = 'http://www.concept2.com/indoor-rowers/racing/records/world?machine=1&event=All&gender=All&age=All&weight=All'
|
|
|
|
def update_records(url=c2url):
|
|
try:
|
|
dfs = pd.read_html(url,attrs={'class':'views-table'})
|
|
df = dfs[0]
|
|
df.columns = df.columns.str.strip()
|
|
success = 1
|
|
except:
|
|
df = pd.DataFrame()
|
|
|
|
if not df.empty:
|
|
C2WorldClassAgePerformance.objects.all().delete()
|
|
|
|
df.Gender = df.Gender.apply(lambda x: 'male' if x=='M' else 'female')
|
|
df['Distance'] = df['Event']
|
|
df['Duration'] = 0
|
|
|
|
for nr,row in df.iterrows():
|
|
if 'm' in row['Record']:
|
|
df.ix[nr,'Distance'] = row['Record'][:-1]
|
|
df.ix[nr,'Duration'] = 60*row['Event']
|
|
else:
|
|
df.ix[nr,'Distance'] = row['Event']
|
|
try:
|
|
tobj = datetime.datetime.strptime(row['Record'],'%M:%S.%f')
|
|
except ValueError:
|
|
tobj = datetime.datetime.strptime(row['Record'],'%H:%M:%S.%f')
|
|
df.ix[nr,'Duration'] = 3600.*tobj.hour+60.*tobj.minute+tobj.second+tobj.microsecond/1.e6
|
|
|
|
print row.Duration
|
|
for nr,row in df.iterrows():
|
|
try:
|
|
weightcategory = row.Weight.lower()
|
|
except AttributeError:
|
|
weightcategory = 'hwt'
|
|
|
|
sex = row.Gender
|
|
name = row.Name
|
|
age = int(row.Age)
|
|
distance = int(row.Distance)
|
|
duration = float(row.Duration)
|
|
season = int(row.Season)
|
|
|
|
velo = distance/duration
|
|
power = int(2.8*velo**3)
|
|
|
|
record = C2WorldClassAgePerformance(
|
|
age = age,
|
|
weightcategory = weightcategory,
|
|
sex=sex,
|
|
distance = distance,
|
|
duration = duration,
|
|
power = power,
|
|
season = season,
|
|
name = name,
|
|
)
|
|
try:
|
|
record.save()
|
|
except:
|
|
print record
|
|
|
|
|
|
class CalcAgePerformance(models.Model):
|
|
weightcategories = (
|
|
('hwt','heavy-weight'),
|
|
('lwt','light-weight'),
|
|
)
|
|
|
|
sexcategories = (
|
|
('male','male'),
|
|
('female','female'),
|
|
)
|
|
|
|
weightcategory = models.CharField(default="hwt",
|
|
max_length=30,
|
|
choices=weightcategories)
|
|
|
|
sex = models.CharField(default="female",
|
|
max_length=30,
|
|
choices=sexcategories)
|
|
|
|
age = models.IntegerField(default=19,verbose_name="Age")
|
|
|
|
duration = models.FloatField(default=1,blank=True)
|
|
power = models.IntegerField(default=200)
|
|
|
|
class Meta:
|
|
db_table = 'calcagegrouprecords'
|
|
|
|
class PowerTimeFitnessMetric(models.Model):
|
|
modechoices = (
|
|
('rower','Rower'),
|
|
('water','On the water')
|
|
)
|
|
|
|
date = models.DateField(default=timezone.now)
|
|
last_workout = models.IntegerField(default=0)
|
|
user = models.ForeignKey(User)
|
|
PowerFourMin = models.FloatField(default=0)
|
|
PowerTwoK = models.FloatField(default=0)
|
|
PowerOneHour = models.FloatField(default=0)
|
|
workoutmode = models.CharField(default='rower',choices=modechoices,
|
|
max_length=40)
|
|
class Meta:
|
|
db_table = 'powertimefitnessmetric'
|
|
|
|
class C2WorldClassAgePerformance(models.Model):
|
|
weightcategories = (
|
|
('hwt','heavy-weight'),
|
|
('lwt','light-weight'),
|
|
)
|
|
|
|
sexcategories = (
|
|
('male','male'),
|
|
('female','female'),
|
|
)
|
|
|
|
weightcategory = models.CharField(default="hwt",
|
|
max_length=30,
|
|
choices=weightcategories)
|
|
|
|
sex = models.CharField(default="female",
|
|
max_length=30,
|
|
choices=sexcategories)
|
|
|
|
age = models.IntegerField(default=19,verbose_name="Age")
|
|
|
|
distance = models.IntegerField(default=2000)
|
|
name = models.CharField(max_length=200,blank=True)
|
|
duration = models.FloatField(default=1,blank=True)
|
|
season = models.IntegerField(default=2013)
|
|
power = models.IntegerField(default=200)
|
|
|
|
class Meta:
|
|
unique_together = ('age','sex','weightcategory','distance')
|
|
|
|
def __unicode__(self):
|
|
return self.sex+' '+self.weightcategory+' '+self.name+':'+str(self.age)+' ('+str(self.season)+')'
|
|
|
|
# For future Team functionality
|
|
class Team(models.Model):
|
|
choices = (
|
|
('private','private'),
|
|
('open','open'),
|
|
)
|
|
|
|
viewchoices = (
|
|
('coachonly','Coach Only'),
|
|
('allmembers','All Members')
|
|
)
|
|
|
|
name = models.CharField(max_length=150,unique=True,verbose_name='Team Name')
|
|
notes = models.CharField(blank=True,max_length=200,verbose_name='Team Purpose')
|
|
manager = models.ForeignKey(User, null=True)
|
|
private = models.CharField(max_length=30,choices=choices,default='open',
|
|
verbose_name='Team Type')
|
|
|
|
viewing = models.CharField(max_length=30,choices=viewchoices,default='allmembers',verbose_name='Sharing Behavior')
|
|
|
|
def __unicode__(self):
|
|
return self.name
|
|
|
|
class TeamForm(ModelForm):
|
|
class Meta:
|
|
model = Team
|
|
fields = ['name','notes','private','viewing']
|
|
widgets = {
|
|
'notes': forms.Textarea,
|
|
}
|
|
|
|
class TeamInvite(models.Model):
|
|
team = models.ForeignKey(Team)
|
|
user = models.ForeignKey(User,null=True)
|
|
issuedate = models.DateField(default=timezone.now)
|
|
code = models.CharField(max_length=150,unique=True)
|
|
email = models.CharField(max_length=150,null=True,blank=True)
|
|
|
|
class TeamInviteForm(ModelForm):
|
|
user = UserFullnameChoiceField(queryset=User.objects.all(),required=False)
|
|
class Meta:
|
|
model = TeamInvite
|
|
fields = ['user','email']
|
|
|
|
class TeamRequest(models.Model):
|
|
team = models.ForeignKey(Team)
|
|
user = models.ForeignKey(User,null=True)
|
|
issuedate = models.DateField(default=timezone.now)
|
|
code = models.CharField(max_length=150,unique=True)
|
|
|
|
from utils import (
|
|
workflowleftpanel,workflowmiddlepanel,
|
|
defaultleft,defaultmiddle,landingpages
|
|
)
|
|
|
|
# Extension of User with rowing specific data
|
|
class Rower(models.Model):
|
|
weightcategories = (
|
|
('hwt','heavy-weight'),
|
|
('lwt','light-weight'),
|
|
)
|
|
|
|
sexcategories = (
|
|
('male','male'),
|
|
('female','female'),
|
|
('not specified','not specified'),
|
|
)
|
|
|
|
stravatypes = (
|
|
('Ride','Ride'),
|
|
('Kitesurf','Kitesurf'),
|
|
('Run','Run'),
|
|
('NordicSki','NordicSki'),
|
|
('Swim','Swim'),
|
|
('RockClimbing','RockClimbing'),
|
|
('Hike','Hike'),
|
|
('RollerSki','RollerSki'),
|
|
('Walk','Walk'),
|
|
('Rowing','Rowing'),
|
|
('AlpineSki','AlpineSki'),
|
|
('Snowboard','Snowboard'),
|
|
('BackcountrySki','BackcountrySki'),
|
|
('Snowshoe','Snowshoe'),
|
|
('Canoeing','Canoeing'),
|
|
('StairStepper','StairStepper'),
|
|
('Crossfit','Crossfit'),
|
|
('StandUpPaddling','StandUpPaddling'),
|
|
('EBikeRide','EBikeRide'),
|
|
('Surfing','Surfing'),
|
|
('Elliptical','Elliptical'),
|
|
('VirtualRide','VirtualRide'),
|
|
('IceSkate','IceSkate'),
|
|
('WeightTraining','WeightTraining'),
|
|
('InlineSkate','InlineSkate'),
|
|
('Windsurf','Windsurf'),
|
|
('Kayaking','Kayaking'),
|
|
('Workout','Workout'),
|
|
('Yoga','Yoga'),
|
|
)
|
|
user = models.OneToOneField(User)
|
|
|
|
# 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")
|
|
|
|
# Weight Category (for sync to C2)
|
|
weightcategory = models.CharField(default="hwt",
|
|
max_length=30,
|
|
choices=weightcategories)
|
|
|
|
sex = models.CharField(default="not specified",
|
|
max_length=30,
|
|
choices=sexcategories)
|
|
|
|
birthdate = models.DateField(null=True,blank=True)
|
|
# Power Zone Data
|
|
ftp = models.IntegerField(default=226,verbose_name="Functional Threshold Power")
|
|
|
|
p0 = models.FloatField(default=1.0,verbose_name="CP p1")
|
|
p1 = models.FloatField(default=1.0,verbose_name="CP p2")
|
|
p2 = models.FloatField(default=1.0,verbose_name="CP p3")
|
|
p3 = models.FloatField(default=1.0,verbose_name="CP p4")
|
|
cpratio = models.FloatField(default=1.0,verbose_name="CP fit ratio")
|
|
|
|
|
|
ep0 = models.FloatField(default=1.0,verbose_name="erg CP p1")
|
|
ep1 = models.FloatField(default=1.0,verbose_name="erg CP p2")
|
|
ep2 = models.FloatField(default=1.0,verbose_name="erg CP p3")
|
|
ep3 = models.FloatField(default=1.0,verbose_name="erg CP p4")
|
|
ecpratio = models.FloatField(default=1.0,verbose_name="erg CP fit ratio")
|
|
|
|
|
|
|
|
otwslack = models.IntegerField(default=0,verbose_name="OTW Power slack")
|
|
|
|
pw_ut2 = models.IntegerField(default=124,verbose_name="UT2 Power")
|
|
pw_ut1 = models.IntegerField(default=171,verbose_name="UT1 Power")
|
|
pw_at = models.IntegerField(default=203,verbose_name="AT Power")
|
|
pw_tr = models.IntegerField(default=237,verbose_name="TR Power")
|
|
pw_an = models.IntegerField(default=273,verbose_name="AN Power")
|
|
|
|
powerzones = PowerZonesField(default=['Rest',
|
|
'Pwr UT2',
|
|
'Pwr UT1',
|
|
'Pwr AT',
|
|
'Pwr TR',
|
|
'Pwr AN'])
|
|
|
|
# Site Settings
|
|
workflowleftpanel = TemplateListField(default=defaultleft)
|
|
workflowmiddlepanel = TemplateListField(default=defaultmiddle)
|
|
defaultlandingpage = models.CharField(default='workout_edit_view',
|
|
max_length=200,
|
|
choices=landingpages,
|
|
verbose_name="Default Landing Page")
|
|
|
|
# Access tokens
|
|
c2token = models.CharField(default='',max_length=200,blank=True,null=True)
|
|
tokenexpirydate = models.DateTimeField(blank=True,null=True)
|
|
c2refreshtoken = models.CharField(default='',max_length=200,blank=True,null=True)
|
|
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)
|
|
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)
|
|
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)
|
|
|
|
stravatoken = models.CharField(default='',max_length=200,blank=True,null=True)
|
|
stravaexportas = models.CharField(default="Rowing",
|
|
max_length=30,
|
|
choices=stravatypes,
|
|
verbose_name="Export Workouts to Strava as")
|
|
|
|
runkeepertoken = models.CharField(default='',max_length=200,
|
|
blank=True,null=True)
|
|
|
|
# Plan
|
|
plans = (
|
|
('basic','basic'),
|
|
('pro','pro'),
|
|
('plan','plan'),
|
|
('coach','coach')
|
|
)
|
|
|
|
privacychoices = (
|
|
('visible','Visible'),
|
|
('hidden','Hidden'),
|
|
)
|
|
|
|
getemailnotifications = models.BooleanField(default=False,
|
|
verbose_name='Receive email notifications')
|
|
|
|
rowerplan = models.CharField(default='basic',max_length=30,
|
|
choices=plans)
|
|
|
|
planexpires = models.DateField(default=timezone.now)
|
|
teamplanexpires = models.DateField(default=timezone.now)
|
|
clubsize = models.IntegerField(default=0)
|
|
protrialexpires = models.DateField(blank=True,null=True)
|
|
plantrialexpires = models.DateField(blank=True,null=True)
|
|
|
|
|
|
# Friends/Team
|
|
friends = models.ManyToManyField("self",blank=True)
|
|
privacy = models.CharField(default='visible',max_length=30,
|
|
choices=privacychoices)
|
|
|
|
team = models.ManyToManyField(Team,blank=True)
|
|
|
|
# Export and Time Zone Settings
|
|
defaulttimezone = models.CharField(default='UTC',max_length=100,
|
|
choices=timezones,
|
|
verbose_name='Default Time Zone')
|
|
|
|
# Show flex chart notes
|
|
showfavoritechartnotes = models.BooleanField(default=True,
|
|
verbose_name='Show Notes for Favorite Charts')
|
|
|
|
def __str__(self):
|
|
return self.user.firstname+' '+self.user.last_name
|
|
|
|
def __unicode__(self):
|
|
return self.user.first_name+' '+self.user.last_name
|
|
|
|
def clean_email(self):
|
|
return self.user.email.lower()
|
|
|
|
@receiver(models.signals.post_save,sender=Rower)
|
|
def auto_delete_teams_on_change(sender, instance, **kwargs):
|
|
if instance.rowerplan != 'coach':
|
|
teams = Team.objects.filter(manager=instance.user)
|
|
for team in teams:
|
|
team.delete()
|
|
|
|
from rowers.metrics import axlabels
|
|
favchartlabelsx = axlabels.copy()
|
|
favchartlabelsy1 = axlabels.copy()
|
|
favchartlabelsy2 = axlabels.copy()
|
|
favchartlabelsy1.pop('None')
|
|
parchoicesy1 = list(sorted(favchartlabelsy1.items(), key = lambda x:x[1]))
|
|
parchoicesy2 = list(sorted(favchartlabelsy2.items(), key = lambda x:x[1]))
|
|
parchoicesx = list(sorted(favchartlabelsx.items(), key = lambda x:x[1]))
|
|
|
|
# Saving a chart as a favorite chart
|
|
class FavoriteChart(models.Model):
|
|
workouttypechoices = (
|
|
('ote','Erg/SkiErg'),
|
|
('otw','On The Water'),
|
|
('both','both')
|
|
)
|
|
|
|
plottypes = (
|
|
('line','Line Chart'),
|
|
('scatter','Scatter Chart')
|
|
)
|
|
|
|
yparam1 = models.CharField(max_length=50,choices=parchoicesy1,verbose_name='Y1')
|
|
yparam2 = models.CharField(max_length=50,choices=parchoicesy2,verbose_name='Y2',default='None',blank=True)
|
|
xparam = models.CharField(max_length=50,choices=parchoicesx,verbose_name='X')
|
|
plottype = models.CharField(max_length=50,choices=plottypes,
|
|
default='line',
|
|
verbose_name='Chart Type')
|
|
workouttype = models.CharField(max_length=50,choices=workouttypechoices,
|
|
default='both',
|
|
verbose_name='Workout Type')
|
|
reststrokes = models.BooleanField(default=True,verbose_name="Incl. Rest")
|
|
notes = models.CharField(max_length=300,verbose_name='Chart Notes',
|
|
default='Flex Chart Notes',blank=True)
|
|
user = models.ForeignKey(Rower)
|
|
|
|
|
|
class FavoriteForm(ModelForm):
|
|
class Meta:
|
|
model = FavoriteChart
|
|
fields = ['xparam','yparam1','yparam2',
|
|
'plottype','workouttype','reststrokes','notes']
|
|
# widgets = {
|
|
# 'notes': forms.Textarea,
|
|
# }
|
|
|
|
|
|
# To generate favorite chart forms on the fly
|
|
class BaseFavoriteFormSet(BaseFormSet):
|
|
def clean(self):
|
|
if any(self.errors):
|
|
return
|
|
|
|
for form in self.forms:
|
|
if form.cleaned_data:
|
|
xparam = form.cleaned_data['xparam']
|
|
yparam1 = form.cleaned_data['yparam1']
|
|
yparam2 = form.cleaned_data['yparam2']
|
|
plottype = form.cleaned_data['plottype']
|
|
reststrokes = form.cleaned_data['reststrokes']
|
|
|
|
if not xparam:
|
|
raise forms.ValidationError(
|
|
'Must have x parameter.',
|
|
code='missing_xparam'
|
|
)
|
|
|
|
if not yparam1:
|
|
raise forms.ValidationError(
|
|
'Must have Y1 parameter.',
|
|
code='missing_yparam1'
|
|
)
|
|
|
|
if not yparam2:
|
|
yparam2 = 'None'
|
|
|
|
class BasePlannedSessionFormSet(BaseFormSet):
|
|
def clean(self):
|
|
if any(self.serrors):
|
|
return
|
|
|
|
|
|
# Check if workout is owned by this user
|
|
def checkworkoutuser(user,workout):
|
|
try:
|
|
r = Rower.objects.get(user=user)
|
|
teams = workout.team.all()
|
|
if workout.user == r:
|
|
return True
|
|
elif teams:
|
|
for team in teams:
|
|
if user == team.manager:
|
|
return True
|
|
else:
|
|
return False
|
|
except Rower.DoesNotExist:
|
|
return False
|
|
|
|
# Check if user is coach or rower
|
|
def checkaccessuser(user,rower):
|
|
try:
|
|
r = Rower.objects.get(user=user)
|
|
teams = Team.objects.filter(manager=user)
|
|
if rower == r:
|
|
return True
|
|
elif teams:
|
|
for team in teams:
|
|
if team in rower.team.all():
|
|
return True
|
|
else:
|
|
return False
|
|
except Rower.DoesNotExist:
|
|
return False
|
|
|
|
timezones = (
|
|
(x,x) for x in pytz.common_timezones
|
|
)
|
|
|
|
# models related to geo data (points, polygon, courses)
|
|
|
|
class GeoCourse(models.Model):
|
|
manager = models.ForeignKey(Rower)
|
|
name = models.CharField(max_length=150,blank=True)
|
|
|
|
|
|
class GeoPolygon(models.Model):
|
|
course = models.ForeignKey(GeoCourse, blank=True)
|
|
order_in_course = models.IntegerField(default=0)
|
|
|
|
# Need error checking to insert new polygons into existing course (all later polygons
|
|
# increase there order_in_course number
|
|
|
|
class GeoPoint(models.Model):
|
|
latitude = models.FloatField(default=0)
|
|
longitude = models.FloatField(default=0)
|
|
polygon = models.ForeignKey(GeoPolygon,blank=True)
|
|
order_in_poly = models.IntegerField(default=0)
|
|
|
|
# need error checking to "insert" new point into existing polygon? This affects order_in_poly
|
|
# of multiple GeoPoint instances
|
|
|
|
|
|
def half_year_from_now():
|
|
return timezone.now()+timezone.timedelta(days=182)
|
|
|
|
def a_week_from_now():
|
|
return timezone.now()+timezone.timedelta(days=7)
|
|
|
|
# models related to training planning - draft
|
|
# Do we need a separate class TestTarget?
|
|
class TrainingTarget(models.Model):
|
|
rower = models.ForeignKey(Rower)
|
|
name = models.CharField(max_length=150,blank=True)
|
|
date = models.DateField(
|
|
default=half_year_from_now)
|
|
notes = models.TextField(max_length=300,blank=True)
|
|
|
|
class TrainingTargetForm(ModelForm):
|
|
class Meta:
|
|
model = TrainingTarget
|
|
fields = ['name','date','notes']
|
|
|
|
widgets = {
|
|
'date': SelectDateWidget(
|
|
years=range(
|
|
timezone.now().year-1,timezone.now().year+1)),
|
|
}
|
|
|
|
# SportTracks has a TrainingGoal like this
|
|
#class TrainingGoal(models.Model):
|
|
# rower = models.ForeignKey(Rower)
|
|
# name = models.CharField(max_length=150,blank=True)
|
|
# startdate = models.DateField(default=timezone.now)
|
|
# enddate = models.DateField(
|
|
# default=timezone.now()+datetime.timedelta(days=28))
|
|
# goalmetric = models.CharField(max_length=150,default='rower',
|
|
# choices = modechoices)
|
|
# value = models.IntegerValue(default=1)
|
|
|
|
# I think we can use PlannedSession for that (in challenge mode)
|
|
# although such a TrainingGoal could have automatically calculated
|
|
# values without needing the user to assign
|
|
|
|
|
|
class TrainingPlan(models.Model):
|
|
rower = models.ForeignKey(Rower)
|
|
name = models.CharField(max_length=150,blank=True)
|
|
target = models.ForeignKey(TrainingTarget,blank=True)
|
|
startdate = models.DateField(default=timezone.now)
|
|
enddate = models.DateField(
|
|
default=half_year_from_now)
|
|
|
|
class TrainingPlanForm(ModelForm):
|
|
class Meta:
|
|
model = TrainingPlan
|
|
fields = ['name','target','startdate','enddate']
|
|
|
|
widgets = {
|
|
'startdate': SelectDateWidget(
|
|
years=range(
|
|
timezone.now().year-1,timezone.now().year+1)),
|
|
'enddate': SelectDateWidget(
|
|
years=range(
|
|
timezone.now().year-1,timezone.now().year+1)),
|
|
}
|
|
|
|
|
|
cycletypechoices = (
|
|
('filler','System Defined'),
|
|
('userdefined','User Defined')
|
|
)
|
|
|
|
class TrainingMacroCycle(models.Model):
|
|
plan = models.ForeignKey(TrainingPlan)
|
|
name = models.CharField(max_length=150,blank=True)
|
|
startdate = models.DateField(default=timezone.now)
|
|
enddate = models.DateField(
|
|
default=half_year_from_now)
|
|
notes = models.TextField(max_length=300,blank=True)
|
|
type = models.CharField(default='filler',
|
|
choices=cycletypechoices,
|
|
max_length=150)
|
|
|
|
class TrainingMesoCycle(models.Model):
|
|
plan = models.ForeignKey(TrainingMacroCycle)
|
|
name = models.CharField(max_length=150,blank=True)
|
|
startdate = models.DateField(default=timezone.now)
|
|
enddate = models.DateField(
|
|
default=half_year_from_now)
|
|
notes = models.TextField(max_length=300,blank=True)
|
|
type = models.CharField(default='filler',
|
|
choices=cycletypechoices,
|
|
max_length=150)
|
|
|
|
|
|
class TrainingMicroCycle(models.Model):
|
|
plan = models.ForeignKey(TrainingMesoCycle)
|
|
name = models.CharField(max_length=150,blank=True)
|
|
startdate = models.DateField(default=timezone.now)
|
|
enddate = models.DateField(
|
|
default=half_year_from_now)
|
|
notes = models.TextField(max_length=300,blank=True)
|
|
type = models.CharField(default='filler',
|
|
choices=cycletypechoices,
|
|
max_length=150)
|
|
|
|
|
|
# Needs some error checking
|
|
# - Microcycles should not overlap with other microcycles, same for MesoCycles, MacroCycles
|
|
# - When a TrainingPlan is created, it should create 1 "collector" Macro, Meso & MicroCycle - this is invisible for users who choose to not use cycles
|
|
# - When a new Microcycle is inserted, the "collector" cycle is automatically adjusted to "go out of the way" of the new MicroCycle - and similar for Macro & Meso
|
|
# - If the entire MesoCycle is filled with user defined MicroCycles - there are no "filler" MicroCycles
|
|
# - Sessions are automatically linked to the correct Cycles based on their start/end date - no need for a hard link
|
|
|
|
# Cycle error checking goes in forms
|
|
|
|
# model for Planned Session (Workout, Challenge, Test)
|
|
class PlannedSession(models.Model):
|
|
|
|
sessiontypechoices = (
|
|
('session','Training Session'),
|
|
('challenge','Challenge'),
|
|
('test','Mandatory Test'),
|
|
('cycletarget','Cycle Target'),
|
|
)
|
|
|
|
sessionmodechoices = (
|
|
('distance','Distance'),
|
|
('time','Time'),
|
|
('rScore','rScore'),
|
|
('TRIMP','TRIMP'),
|
|
)
|
|
|
|
criteriumchoices = (
|
|
('none','Approximately'),
|
|
('minimum','At Least'),
|
|
('exact','Exactly'),
|
|
)
|
|
|
|
verificationchoices = (
|
|
('none','None'),
|
|
('automatic','Automatic'),
|
|
('manual','Manual')
|
|
)
|
|
|
|
sessionunitchoices = (
|
|
('min','minutes'),
|
|
('km','km'),
|
|
('m','meters'),
|
|
('None',None),
|
|
)
|
|
|
|
manager = models.ForeignKey(User)
|
|
|
|
name = models.CharField(max_length=150,blank=True,
|
|
verbose_name='Name')
|
|
|
|
comment = models.TextField(max_length=500,blank=True,
|
|
)
|
|
|
|
startdate = models.DateField(default=timezone.now,
|
|
verbose_name='On or After')
|
|
|
|
enddate = models.DateField(default=a_week_from_now,
|
|
verbose_name='On or Before')
|
|
|
|
sessiontype = models.CharField(default='session',
|
|
choices=sessiontypechoices,
|
|
max_length=150,
|
|
verbose_name='Session Type')
|
|
|
|
sessionvalue = models.IntegerField(default=60,verbose_name='Value')
|
|
|
|
max_nr_of_workouts = models.IntegerField(
|
|
default=0,verbose_name='Maximum number of workouts'
|
|
)
|
|
|
|
sessionunit = models.CharField(
|
|
default='min',choices=sessionunitchoices,
|
|
max_length=150,
|
|
verbose_name='Unit')
|
|
|
|
criterium = models.CharField(
|
|
default='none',
|
|
choices=criteriumchoices,
|
|
max_length=150)
|
|
|
|
verification = models.CharField(
|
|
default='none',
|
|
max_length=150,
|
|
choices=verificationchoices
|
|
)
|
|
|
|
team = models.ManyToManyField(Team,blank=True)
|
|
rower = models.ManyToManyField(Rower,blank=True)
|
|
|
|
sessionmode = models.CharField(default='time',
|
|
choices=sessionmodechoices,
|
|
max_length=150,
|
|
verbose_name='Session Mode')
|
|
|
|
hasranking = models.BooleanField(default=False)
|
|
|
|
def __unicode__(self):
|
|
|
|
name = self.name
|
|
startdate = self.startdate
|
|
enddate = self.enddate
|
|
|
|
stri = u'{n} {s} - {e}'.format(
|
|
s = startdate.strftime('%Y-%m-%d'),
|
|
e = enddate.strftime('%Y-%m-%d'),
|
|
n = name,
|
|
)
|
|
|
|
return stri
|
|
|
|
def save(self, *args, **kwargs):
|
|
# sort units
|
|
if self.sessionmode == 'distance':
|
|
if self.sessionunit not in ['m','km']:
|
|
self.sessionunit = 'm'
|
|
elif self.sessionmode == 'time':
|
|
self.sessionunit = 'min'
|
|
else:
|
|
self.sessionunit = 'None'
|
|
|
|
super(PlannedSession,self).save(*args, **kwargs)
|
|
|
|
# Date input utility
|
|
class DateInput(forms.DateInput):
|
|
input_type = 'date'
|
|
|
|
class PlannedSessionForm(ModelForm):
|
|
|
|
class Meta:
|
|
model = PlannedSession
|
|
fields = ['startdate',
|
|
'enddate',
|
|
'name',
|
|
'sessiontype',
|
|
'sessionmode',
|
|
'criterium',
|
|
'sessionvalue',
|
|
'sessionunit',
|
|
'comment',
|
|
]
|
|
|
|
dateTimeOptions = {
|
|
'format': 'yyyy-mm-dd',
|
|
'autoclose': True,
|
|
}
|
|
|
|
widgets = {
|
|
'comment': forms.Textarea,
|
|
'startdate': AdminDateWidget(),
|
|
'enddate': AdminDateWidget(),
|
|
}
|
|
|
|
class PlannedSessionFormSmall(ModelForm):
|
|
|
|
class Meta:
|
|
model = PlannedSession
|
|
fields = ['startdate',
|
|
'enddate',
|
|
'name',
|
|
'sessiontype',
|
|
'sessionmode',
|
|
'criterium',
|
|
'sessionvalue',
|
|
'sessionunit',
|
|
'manager',
|
|
'comment',
|
|
]
|
|
|
|
dateTimeOptions = {
|
|
'format': 'yyyy-mm-dd',
|
|
'autoclose': True,
|
|
}
|
|
|
|
widgets = {
|
|
'startdate': DateInput(attrs={'size':10}),
|
|
'enddate': DateInput(attrs={'size':10}),
|
|
'name': forms.TextInput(attrs={'size':10}),
|
|
'comment': forms.TextInput(attrs={'size':10}),
|
|
'sessionvalue': forms.TextInput(attrs={'style':'width:5em',
|
|
'type':'number'}),
|
|
'manager': forms.HiddenInput(),
|
|
}
|
|
|
|
|
|
# Workout
|
|
class Workout(models.Model):
|
|
workouttypes = types.workouttypes
|
|
workoutsources = types.workoutsources
|
|
boattypes = types.boattypes
|
|
privacychoices = types.privacychoices
|
|
|
|
user = models.ForeignKey(Rower)
|
|
team = models.ManyToManyField(Team,blank=True)
|
|
plannedsession = models.ForeignKey(PlannedSession, blank=True,null=True)
|
|
name = models.CharField(max_length=150)
|
|
date = models.DateField()
|
|
workouttype = models.CharField(choices=workouttypes,max_length=50)
|
|
workoutsource = models.CharField(choices=workoutsources,max_length=100,
|
|
default='unknown')
|
|
boattype = models.CharField(choices=boattypes,max_length=50,
|
|
default='1x',
|
|
verbose_name = 'Boat Type')
|
|
starttime = models.TimeField(blank=True,null=True)
|
|
startdatetime = models.DateTimeField(blank=True,null=True)
|
|
timezone = models.CharField(default='UTC',
|
|
choices=timezones,
|
|
max_length=100)
|
|
distance = models.IntegerField(default=0,blank=True)
|
|
duration = models.TimeField(default=1,blank=True)
|
|
weightcategory = models.CharField(default="hwt",max_length=10)
|
|
weightvalue = models.FloatField(default=80.0,blank=True,verbose_name = 'Average Crew Weight (kg)')
|
|
csvfilename = models.CharField(blank=True,max_length=150)
|
|
uploadedtoc2 = models.IntegerField(default=0)
|
|
averagehr = models.IntegerField(blank=True,null=True)
|
|
maxhr = models.IntegerField(blank=True,null=True)
|
|
uploadedtostrava = models.IntegerField(default=0)
|
|
uploadedtosporttracks = models.IntegerField(default=0)
|
|
uploadedtounderarmour = models.IntegerField(default=0)
|
|
uploadedtotp = models.IntegerField(default=0)
|
|
uploadedtorunkeeper = models.IntegerField(default=0)
|
|
forceunit = models.CharField(default='lbs',
|
|
choices = (
|
|
('lbs','lbs'),
|
|
('N','N')
|
|
),
|
|
max_length=100)
|
|
|
|
# empower stuff
|
|
inboard = models.FloatField(default=0.88)
|
|
oarlength = models.FloatField(default=2.89)
|
|
|
|
|
|
notes = models.CharField(blank=True,null=True,max_length=1000)
|
|
summary = models.TextField(blank=True)
|
|
privacy = models.CharField(default='visible',max_length=30,
|
|
choices=privacychoices)
|
|
rankingpiece = models.BooleanField(default=False,verbose_name='Ranking Piece')
|
|
|
|
def __unicode__(self):
|
|
|
|
date = self.date
|
|
name = self.name
|
|
distance = str(self.distance)
|
|
ownerfirst = self.user.user.first_name
|
|
ownerlast = self.user.user.last_name
|
|
duration = self.duration
|
|
boattype = self.boattype
|
|
workouttype = self.workouttype
|
|
|
|
if workouttype != 'water':
|
|
stri = u'{d} {n} {dist}m {duration:%H:%M:%S} {workouttype} {ownerfirst} {ownerlast}'.format(
|
|
d = date.strftime('%Y-%m-%d'),
|
|
n = name,
|
|
dist = distance,
|
|
duration = duration,
|
|
workouttype = workouttype,
|
|
ownerfirst = ownerfirst,
|
|
ownerlast = ownerlast,
|
|
)
|
|
else:
|
|
stri = u'{d} {n} {dist}m {duration:%H:%M:%S} {workouttype} {boattype} {ownerfirst} {ownerlast}'.format(
|
|
d = date.strftime('%Y-%m-%d'),
|
|
n = name,
|
|
dist = distance,
|
|
duration = duration,
|
|
workouttype = workouttype,
|
|
boattype=boattype,
|
|
ownerfirst = ownerfirst,
|
|
ownerlast = ownerlast,
|
|
)
|
|
|
|
return stri
|
|
|
|
# delete files belonging to workout instance
|
|
# related GraphImage objects should be deleted automatically
|
|
@receiver(models.signals.post_delete,sender=Workout)
|
|
def auto_delete_file_on_delete(sender, instance, **kwargs):
|
|
# delete CSV file
|
|
if instance.csvfilename:
|
|
if os.path.isfile(instance.csvfilename):
|
|
os.remove(instance.csvfilename)
|
|
if instance.csvfilename+'.gz':
|
|
if os.path.isfile(instance.csvfilename+'.gz'):
|
|
os.remove(instance.csvfilename+'.gz')
|
|
|
|
# Delete stroke data from the database when a workout is deleted
|
|
@receiver(models.signals.post_delete,sender=Workout)
|
|
def auto_delete_strokedata_on_delete(sender, instance, **kwargs):
|
|
if instance.id:
|
|
query = sa.text('DELETE FROM strokedata WHERE workoutid={id};'.format(
|
|
id=instance.id,
|
|
))
|
|
engine = create_engine(database_url, echo=False)
|
|
with engine.connect() as conn, conn.begin():
|
|
try:
|
|
result = conn.execute(query)
|
|
except:
|
|
print "Database Locked"
|
|
conn.close()
|
|
engine.dispose()
|
|
|
|
from rowers.metrics import rowingmetrics
|
|
|
|
strokedatafields = {
|
|
'workoutid':models.IntegerField(null=True),
|
|
'workoutstate':models.IntegerField(null=True,default=1),
|
|
'ftime':models.CharField(max_length=30),
|
|
'fpace':models.CharField(max_length=30),
|
|
'hr_ut2':models.IntegerField(null=True),
|
|
'hr_ut1':models.IntegerField(null=True),
|
|
'hr_at':models.IntegerField(null=True),
|
|
'hr_tr':models.IntegerField(null=True),
|
|
'hr_an':models.IntegerField(null=True),
|
|
'hr_max':models.IntegerField(null=True),
|
|
'hr_bottom':models.IntegerField(null=True),
|
|
'x_right':models.FloatField(null=True),
|
|
'ergpace':models.FloatField(null=True),
|
|
'nowindpace':models.FloatField(null=True),
|
|
'equivergpower':models.FloatField(null=True),
|
|
'fergpace':models.CharField(max_length=30),
|
|
'fnowindpace':models.CharField(max_length=30),
|
|
}
|
|
|
|
for name,d in rowingmetrics:
|
|
if d['numtype'] == 'float':
|
|
try:
|
|
strokedatafields[name] = models.FloatField(
|
|
null=d['null'],
|
|
default=d['default'],
|
|
verbose_name=d['verbose_name'])
|
|
except KeyError:
|
|
strokedatafields[name] = models.FloatField(
|
|
null=d['null'],
|
|
verbose_name=d['verbose_name'])
|
|
elif d['numtype'] == 'integer':
|
|
try:
|
|
strokedatafields[name] = models.IntegerField(
|
|
null=d['null'],
|
|
default=d['default'],
|
|
verbose_name=d['verbose_name'])
|
|
except KeyError:
|
|
strokedatafields[name] = models.IntegerField(
|
|
null=d['null'],
|
|
verbose_name=d['verbose_name'])
|
|
class Meta:
|
|
db_table = 'strokedata'
|
|
index_together = ['workoutid']
|
|
app_label = 'rowers'
|
|
|
|
attrs = {'__module__': '', 'Meta': Meta}
|
|
attrs.update(strokedatafields)
|
|
|
|
# Model of StrokeData table
|
|
# the definition here is used only to enable easy Django migration
|
|
# when the StrokeData are expanded.
|
|
# No Django Instances of this model are managed. Strokedata table is
|
|
# accesssed directly with SQL commands
|
|
StrokeData = type(str('StrokeData'), (models.Model,),
|
|
attrs
|
|
)
|
|
|
|
# Storing data for the OTW CP chart
|
|
class cpdata(models.Model):
|
|
delta = models.IntegerField(default=0)
|
|
cp = models.FloatField(default=0)
|
|
user = models.IntegerField(default=0)
|
|
|
|
class Meta:
|
|
db_table = 'cpdata'
|
|
index_together = ['user']
|
|
app_label = 'rowers'
|
|
|
|
|
|
# Storing data for the OTW CP chart
|
|
class cpergdata(models.Model):
|
|
delta = models.IntegerField(default=0)
|
|
cp = models.FloatField(default=0)
|
|
user = models.IntegerField(default=0)
|
|
|
|
class Meta:
|
|
db_table = 'cpergdata'
|
|
index_together = ['user']
|
|
app_label = 'rowers'
|
|
|
|
|
|
|
|
# Storing data for the OTW CP chart
|
|
class ergcpdata(models.Model):
|
|
delta = models.IntegerField(default=0)
|
|
cp = models.FloatField(default=0)
|
|
distance = models.FloatField(default=0)
|
|
user = models.IntegerField(default=0)
|
|
|
|
class Meta:
|
|
db_table = 'ergcpdata'
|
|
index_together = ['user']
|
|
app_label = 'rowers'
|
|
|
|
# A wrapper around the png files
|
|
class GraphImage(models.Model):
|
|
filename = models.CharField(default='',max_length=150,blank=True,null=True)
|
|
creationdatetime = models.DateTimeField()
|
|
workout = models.ForeignKey(Workout)
|
|
width = models.IntegerField(default=1200)
|
|
height = models.IntegerField(default=600)
|
|
|
|
def __str__(self):
|
|
return self.filename
|
|
|
|
|
|
# delete related file object when image is deleted
|
|
@receiver(models.signals.post_delete,sender=GraphImage)
|
|
def auto_delete_image_on_delete(sender,instance, **kwargs):
|
|
if instance.filename:
|
|
if os.path.isfile(instance.filename):
|
|
others = GraphImage.objects.filter(filename=instance.filename)
|
|
if len(others) == 0:
|
|
os.remove(instance.filename)
|
|
else:
|
|
print "couldn't find the file "+instance.filename
|
|
|
|
|
|
# Form to update Workout data
|
|
class WorkoutForm(ModelForm):
|
|
duration = forms.TimeInput(format='%H:%M:%S.%f')
|
|
class Meta:
|
|
model = Workout
|
|
fields = ['name','date','starttime','timezone','duration','distance','workouttype','notes','privacy','rankingpiece','boattype']
|
|
widgets = {
|
|
'date': AdminDateWidget(),
|
|
'notes': forms.Textarea,
|
|
'duration': forms.TimeInput(format='%H:%M:%S.%f'),
|
|
}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super(WorkoutForm, self).__init__(*args, **kwargs)
|
|
# this line to be removed
|
|
del self.fields['privacy']
|
|
# self.fields['timezone'] = forms.ChoiceField(choices=[
|
|
# (x,x) for x in pytz.common_timezones
|
|
# ],
|
|
# initial='UTC',
|
|
# label='Time Zone')
|
|
|
|
if self.instance.workouttype != 'water':
|
|
del self.fields['boattype']
|
|
|
|
fieldorder = (
|
|
'name',
|
|
'date',
|
|
'starttime',
|
|
'timezone',
|
|
'duration',
|
|
'distance',
|
|
'workouttype',
|
|
'notes',
|
|
'rankingpiece',
|
|
'boattype'
|
|
)
|
|
|
|
fields = OrderedDict()
|
|
for key in fieldorder:
|
|
try:
|
|
fields[key] = self.fields.pop(key)
|
|
except KeyError:
|
|
pass
|
|
for key, valye in self.fields.items():
|
|
fields[key] = value
|
|
self.fields = fields
|
|
|
|
# Used for the rowing physics calculations
|
|
class AdvancedWorkoutForm(ModelForm):
|
|
quick_calc = forms.BooleanField(initial=True,required=False)
|
|
|
|
class Meta:
|
|
model = Workout
|
|
fields = ['boattype','weightvalue','quick_calc']
|
|
|
|
class RowerExportForm(ModelForm):
|
|
class Meta:
|
|
model = Rower
|
|
fields = ['stravaexportas']
|
|
|
|
# Simple form to set rower's Functional Threshold Power
|
|
class RowerPowerForm(ModelForm):
|
|
class Meta:
|
|
model = Rower
|
|
fields = ['ftp','otwslack']
|
|
|
|
# Form to set rower's Power zones, including test routines
|
|
# to enable consistency
|
|
class RowerPowerZonesForm(ModelForm):
|
|
|
|
powerzones = ['UT3','UT2','UT1','AT','TR','AN']
|
|
ut3name = forms.CharField(initial=powerzones[0])
|
|
ut2name = forms.CharField(initial=powerzones[1])
|
|
ut1name = forms.CharField(initial=powerzones[2])
|
|
atname = forms.CharField(initial=powerzones[3])
|
|
trname = forms.CharField(initial=powerzones[4])
|
|
anname = forms.CharField(initial=powerzones[5])
|
|
|
|
def __init__(self, *args,**kwargs):
|
|
super(RowerPowerZonesForm, self).__init__(*args, **kwargs)
|
|
|
|
if 'instance' in kwargs:
|
|
powerzones = kwargs['instance'].powerzones
|
|
else:
|
|
powerzones = ['UT3','UT2','UT1','AT','TR','AN']
|
|
|
|
self.fields['ut3name'].initial = powerzones[0]
|
|
self.fields['ut2name'].initial = powerzones[1]
|
|
self.fields['ut1name'].initial = powerzones[2]
|
|
self.fields['atname'].initial = powerzones[3]
|
|
self.fields['trname'].initial = powerzones[4]
|
|
self.fields['anname'].initial = powerzones[5]
|
|
|
|
class Meta:
|
|
model = Rower
|
|
fields = ['pw_ut2','pw_ut1','pw_at','pw_tr','pw_an']
|
|
|
|
def clean(self):
|
|
cleaned_data = super(RowerPowerZonesForm, self).clean()
|
|
|
|
try:
|
|
pw_ut2 = cleaned_data['pw_ut2']
|
|
except KeyError:
|
|
raise ValidationError("Value cannot be empty")
|
|
except:
|
|
pw_ut2 = int(self.data['pw_ut2'])
|
|
try:
|
|
pw_ut1 = cleaned_data['pw_ut1']
|
|
except KeyError:
|
|
raise ValidationError("Value cannot be empty")
|
|
except:
|
|
pw_ut1 = int(self.data['pw_ut1'])
|
|
try:
|
|
pw_at = cleaned_data['pw_at']
|
|
except KeyError:
|
|
raise ValidationError("Value cannot be empty")
|
|
except:
|
|
pw_at = int(self.data['pw_at'])
|
|
try:
|
|
pw_tr = cleaned_data['pw_tr']
|
|
except KeyError:
|
|
raise ValidationError("Value cannot be empty")
|
|
except:
|
|
pw_tr = int(self.data['pw_tr'])
|
|
try:
|
|
pw_an = cleaned_data['pw_an']
|
|
except KeyError:
|
|
raise ValidationError("Value cannot be empty")
|
|
except:
|
|
pw_an = int(self.data['pw_an'])
|
|
|
|
try:
|
|
ut3name = cleaned_data['ut3name']
|
|
except:
|
|
ut2name = 'UT3'
|
|
cleaned_data['ut3name'] = 'UT3'
|
|
try:
|
|
ut2name = cleaned_data['ut2name']
|
|
except:
|
|
ut2name = 'UT2'
|
|
cleaned_data['ut2name'] = 'UT2'
|
|
try:
|
|
ut1name = cleaned_data['ut1name']
|
|
except:
|
|
ut1name = 'UT1'
|
|
cleaned_data['ut1name'] = 'UT1'
|
|
try:
|
|
atname = cleaned_data['atname']
|
|
except:
|
|
atname = 'AT'
|
|
cleaned_data['atname'] = 'AT'
|
|
try:
|
|
trname = cleaned_data['trname']
|
|
except:
|
|
trname = 'TR'
|
|
cleaned_data['ut1name'] = 'TR'
|
|
try:
|
|
anname = cleaned_data['anname']
|
|
except:
|
|
anname = 'AN'
|
|
cleaned_data['ut1name'] = 'AN'
|
|
|
|
|
|
try:
|
|
ut3name = cleaned_data['ut3name']
|
|
except:
|
|
ut2name = 'UT3'
|
|
cleaned_data['ut3name'] = 'UT3'
|
|
try:
|
|
ut2name = cleaned_data['ut2name']
|
|
except:
|
|
ut2name = 'UT2'
|
|
cleaned_data['ut2name'] = 'UT2'
|
|
try:
|
|
ut1name = cleaned_data['ut1name']
|
|
except:
|
|
ut1name = 'UT1'
|
|
cleaned_data['ut1name'] = 'UT1'
|
|
try:
|
|
atname = cleaned_data['atname']
|
|
except:
|
|
atname = 'AT'
|
|
cleaned_data['atname'] = 'AT'
|
|
try:
|
|
trname = cleaned_data['trname']
|
|
except:
|
|
trname = 'TR'
|
|
cleaned_data['ut1name'] = 'TR'
|
|
try:
|
|
anname = cleaned_data['anname']
|
|
except:
|
|
anname = 'AN'
|
|
cleaned_data['ut1name'] = 'AN'
|
|
|
|
|
|
if pw_ut1 <= pw_ut2:
|
|
e = "{ut1name} should be higher than {ut2name}".format(
|
|
ut1name = ut1name,
|
|
ut2name= ut2name,
|
|
)
|
|
raise forms.ValidationError(e)
|
|
if pw_at <= pw_ut1:
|
|
e = "{atname} should be higher than {ut1name}".format(
|
|
atname = atname,
|
|
ut1name= ut1name,
|
|
)
|
|
raise forms.ValidationError(e)
|
|
if pw_tr <= pw_at:
|
|
e = "{trname} should be higher than {atname}".format(
|
|
atname = atname,
|
|
trname= trname,
|
|
)
|
|
raise forms.ValidationError(e)
|
|
if pw_an <= pw_tr:
|
|
e = "{anname} should be higher than {trname}".format(
|
|
anname = anname,
|
|
trname= trname,
|
|
)
|
|
raise forms.ValidationError(e)
|
|
|
|
return cleaned_data
|
|
|
|
# Form to set rower's Email and Weight category
|
|
class AccountRowerForm(ModelForm):
|
|
class Meta:
|
|
model = Rower
|
|
fields = ['sex','birthdate','weightcategory','getemailnotifications',
|
|
'defaulttimezone','showfavoritechartnotes',
|
|
'defaultlandingpage']
|
|
|
|
widgets = {
|
|
'birthdate': SelectDateWidget(
|
|
years=range(
|
|
timezone.now().year-100,timezone.now().year-10)),
|
|
}
|
|
|
|
|
|
def clean_email(self):
|
|
email = self.cleaned_data.get('email')
|
|
|
|
try:
|
|
validate_email(email)
|
|
except ValidationError:
|
|
raise forms.ValidationError(
|
|
'Please enter a valid email address')
|
|
|
|
try:
|
|
match = User.objects.get(email__iexact=email)
|
|
if self.instance.user == match:
|
|
return email
|
|
except User.DoesNotExist:
|
|
return email
|
|
|
|
raise forms.ValidationError('This email address is not allowed')
|
|
|
|
|
|
|
|
class UserForm(ModelForm):
|
|
class Meta:
|
|
model = User
|
|
fields = ['first_name','last_name','email']
|
|
|
|
def clean_first_name(self):
|
|
first_name = self.cleaned_data.get('first_name')
|
|
|
|
if len(first_name):
|
|
return first_name
|
|
|
|
raise forms.ValidationError('Please fill in your first name')
|
|
|
|
def clean_email(self):
|
|
email = self.cleaned_data.get('email')
|
|
|
|
try:
|
|
validate_email(email)
|
|
except ValidationError:
|
|
raise forms.ValidationError(
|
|
'Please enter a valid email address')
|
|
|
|
try:
|
|
match = User.objects.filter(email__iexact=email)
|
|
if self.instance in match:
|
|
return email
|
|
except User.DoesNotExist:
|
|
return email
|
|
|
|
raise forms.ValidationError('This email address is not allowed')
|
|
|
|
|
|
# Form to set rower's Heart Rate zones, including test routines
|
|
# to enable consistency
|
|
class RowerForm(ModelForm):
|
|
class Meta:
|
|
model = Rower
|
|
fields = ['rest','ut2','ut1','at','tr','an','max']
|
|
|
|
|
|
def clean_rest(self):
|
|
rest = self.cleaned_data['rest']
|
|
|
|
if rest<10:
|
|
self.data['rest']=10
|
|
raise forms.ValidationError("Resting heart rate should be higher than 10 bpm")
|
|
|
|
|
|
if rest>250:
|
|
self.data['rest'] = 250
|
|
raise forms.ValidationError("Resting heart rate should be lower than 250 bpm")
|
|
|
|
|
|
return rest
|
|
|
|
def clean_ut2(self):
|
|
ut2 = self.cleaned_data['ut2']
|
|
|
|
if ut2<10:
|
|
raise forms.ValidationError("UT2 heart rate should be higher than 10 bpm")
|
|
|
|
if ut2>250:
|
|
raise forms.ValidationError("UT2 heart rate should be lower than 250 bpm")
|
|
|
|
return ut2
|
|
|
|
def clean_ut1(self):
|
|
ut1 = self.cleaned_data['ut1']
|
|
|
|
if ut1<10:
|
|
raise forms.ValidationError("UT1 heart rate should be higher than 10 bpm")
|
|
|
|
if ut1>250:
|
|
raise forms.ValidationError("Resting heart rate should be lower than 250 bpm")
|
|
|
|
return ut1
|
|
|
|
def clean_at(self):
|
|
at = self.cleaned_data['at']
|
|
|
|
if at<10:
|
|
raise forms.ValidationError("AT heart rate should be higher than 10 bpm")
|
|
|
|
if at>250:
|
|
raise forms.ValidationError("AT heart rate should be lower than 250 bpm")
|
|
|
|
return at
|
|
|
|
def clean_tr(self):
|
|
tr = self.cleaned_data['tr']
|
|
|
|
if tr<10:
|
|
raise forms.ValidationError("TR heart rate should be higher than 10 bpm")
|
|
|
|
if tr>250:
|
|
raise forms.ValidationError("TR heart rate should be lower than 250 bpm")
|
|
|
|
return tr
|
|
|
|
def clean_an(self):
|
|
an = self.cleaned_data['an']
|
|
|
|
if an<10:
|
|
raise forms.ValidationError("AN heart rate should be higher than 10 bpm")
|
|
|
|
if an>250:
|
|
raise forms.ValidationError("AN heart rate should be lower than 250 bpm")
|
|
|
|
return an
|
|
|
|
def clean_max(self):
|
|
max = self.cleaned_data['max']
|
|
|
|
if max<10:
|
|
raise forms.ValidationError("Max heart rate should be higher than 10 bpm")
|
|
|
|
if max>250:
|
|
raise forms.ValidationError("Max heart rate should be lower than 250 bpm")
|
|
|
|
return max
|
|
|
|
|
|
def clean(self):
|
|
|
|
|
|
try:
|
|
rest = self.cleaned_data['rest']
|
|
except:
|
|
try:
|
|
rest = int(self.data['rest'])
|
|
except ValueError:
|
|
rest = 0
|
|
|
|
try:
|
|
ut2 = self.cleaned_data['ut2']
|
|
except:
|
|
try:
|
|
ut2 = self.data['ut2']
|
|
except ValueError:
|
|
ut2 = 0
|
|
|
|
try:
|
|
ut1 = self.cleaned_data['ut1']
|
|
except:
|
|
try:
|
|
ut1 = self.data['ut1']
|
|
except ValueError:
|
|
ut1 = 0
|
|
|
|
try:
|
|
at = self.cleaned_data['at']
|
|
except:
|
|
try:
|
|
at = self.data['at']
|
|
except ValueError:
|
|
at = 0
|
|
|
|
try:
|
|
an = self.cleaned_data['an']
|
|
except:
|
|
try:
|
|
an = self.data['an']
|
|
except ValueError:
|
|
an = 0
|
|
|
|
try:
|
|
tr = self.cleaned_data['tr']
|
|
except:
|
|
try:
|
|
tr = self.data['tr']
|
|
except ValueError:
|
|
tr = 0
|
|
|
|
try:
|
|
max = self.cleaned_data['max']
|
|
except:
|
|
try:
|
|
max = self.data['max']
|
|
except ValueError:
|
|
max = 0
|
|
|
|
if rest>=ut2:
|
|
raise forms.ValidationError("Resting heart rate should be lower than UT2")
|
|
if ut2>=ut1:
|
|
raise forms.ValidationError("UT2 should be lower than UT1")
|
|
if ut2>=ut1:
|
|
raise forms.ValidationError("UT2 should be lower than UT1")
|
|
if ut1>=at:
|
|
raise forms.ValidationError("UT1 should be lower than AT")
|
|
if at>=tr:
|
|
raise forms.ValidationError("AT should be lower than TR")
|
|
if tr>=an:
|
|
raise forms.ValidationError("TR should be lower than AN")
|
|
if an>=max:
|
|
raise forms.ValidationError("AN should be lower than Max")
|
|
|
|
|
|
# An announcement that goes to the right of the workouts list
|
|
# optionally sends a tweet to our twitter account
|
|
class SiteAnnouncement(models.Model):
|
|
created = models.DateField(default=timezone.now)
|
|
announcement = models.TextField(max_length=280)
|
|
expires = models.DateField(default=timezone.now)
|
|
modified = models.DateField(default=timezone.now)
|
|
dotweet = models.BooleanField(default=False)
|
|
|
|
def save(self, *args, **kwargs):
|
|
if not self.id:
|
|
self.created = timezone.now()
|
|
self.expires = timezone.now()+datetime.timedelta(days=10)
|
|
self.modified = timezone.now()
|
|
if self.dotweet:
|
|
try:
|
|
status = tweetapi.PostUpdate(self.announcement)
|
|
except:
|
|
try:
|
|
status = tweetapi.PostUpdate(self.announcement[:135])
|
|
except:
|
|
pass
|
|
return super(SiteAnnouncement,self).save(*args, **kwargs)
|
|
|
|
# A comment by a user on a training
|
|
class WorkoutComment(models.Model):
|
|
comment = models.TextField(max_length=300)
|
|
created = models.DateTimeField(default=timezone.now)
|
|
read = models.BooleanField(default=False)
|
|
notification = models.BooleanField(default=True,verbose_name="Subscribe to new comment notifications")
|
|
user = models.ForeignKey(User)
|
|
workout = models.ForeignKey(Workout)
|
|
|
|
def __unicode__(self):
|
|
return u'Comment to: {w} by {u1} {u2}'.format(
|
|
w=self.workout,
|
|
u1 = self.user.first_name,
|
|
u2 = self.user.last_name,
|
|
)
|
|
|
|
|
|
class WorkoutCommentForm(ModelForm):
|
|
class Meta:
|
|
model = WorkoutComment
|
|
fields = ['comment','notification']
|
|
widgets = {
|
|
'comment': forms.Textarea,
|
|
}
|
|
|