Stores job id in a cookie. Upon reload, checks if job is still busy. If busy, no new jobs are started. Some messaging for users to give them information about the job status.
1146 lines
37 KiB
Python
1146 lines
37 KiB
Python
from __future__ import unicode_literals
|
|
|
|
from django.db import models
|
|
from django.contrib.auth.models import User
|
|
from django import forms
|
|
from django.forms import ModelForm
|
|
from django.dispatch import receiver
|
|
from django.forms.widgets import SplitDateTimeWidget
|
|
from django.forms.formsets import BaseFormSet
|
|
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 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)
|
|
|
|
# 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)
|
|
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'),
|
|
)
|
|
|
|
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)
|
|
|
|
# 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)
|
|
|
|
# runkeepertokenexpirydate = models.DateTimeField(blank=True,null=True)
|
|
# runkeeperrefreshtoken = models.CharField(default='',max_length=200,
|
|
# blank=True,null=True)
|
|
|
|
# Plan
|
|
plans = (
|
|
('basic','basic'),
|
|
('pro','pro'),
|
|
('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)
|
|
|
|
|
|
# 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.username
|
|
|
|
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'
|
|
|
|
# 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
|
|
|
|
timezones = (
|
|
(x,x) for x in pytz.common_timezones
|
|
)
|
|
|
|
|
|
# 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)
|
|
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):
|
|
os.remove(instance.filename)
|
|
else:
|
|
print "couldn't find the file "+instance.filename
|
|
|
|
# Date input utility
|
|
class DateInput(forms.DateInput):
|
|
input_type = 'date'
|
|
|
|
# Form to update Workout data
|
|
class WorkoutForm(ModelForm):
|
|
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': DateInput(),
|
|
'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):
|
|
class Meta:
|
|
model = Workout
|
|
fields = ['boattype','weightvalue']
|
|
|
|
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 = ['weightcategory','getemailnotifications',
|
|
'defaulttimezone','showfavoritechartnotes',
|
|
'defaultlandingpage']
|
|
|
|
class UserForm(ModelForm):
|
|
class Meta:
|
|
model = User
|
|
fields = ['first_name','last_name','email']
|
|
|
|
|
|
def clean(self):
|
|
cleaned_data = super(UserForm, self).clean()
|
|
|
|
# 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:
|
|
rest = int(self.data['rest'])
|
|
|
|
try:
|
|
ut2 = self.cleaned_data['ut2']
|
|
except:
|
|
ut2 = self.data['ut2']
|
|
|
|
try:
|
|
ut1 = self.cleaned_data['ut1']
|
|
except:
|
|
ut1 = self.data['ut1']
|
|
|
|
try:
|
|
at = self.cleaned_data['at']
|
|
except:
|
|
at = self.data['at']
|
|
|
|
try:
|
|
an = self.cleaned_data['an']
|
|
except:
|
|
an = self.data['an']
|
|
|
|
try:
|
|
tr = self.cleaned_data['tr']
|
|
except:
|
|
tr = self.data['tr']
|
|
|
|
try:
|
|
max = self.cleaned_data['max']
|
|
except:
|
|
max = self.data['max']
|
|
|
|
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=140)
|
|
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,
|
|
}
|
|
|