Private
Public Access
1
0
Files
rowsandall/rowers/models.py
2024-05-26 14:51:21 +02:00

5323 lines
179 KiB
Python

from django.core.validators import RegexValidator, validate_email
from rowers.metrics import rowingmetrics
from django.db.models.signals import m2m_changed
from rowers.courseutils import coordinate_in_path
from rowers.utils import (
# workflowleftpanel, workflowmiddlepanel,
defaultleft, defaultmiddle, landingpages, landingpages2,
steps_read_fit, steps_write_fit, ps_dict_order, uniqify
)
from rowers.metrics import axlabels
from rowers.utils import geo_distance, move_one_meter
from rowers.formfields import *
from rowers.database import *
import uuid
from django.db import models, IntegrityError
from django.contrib.auth.models import User
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.formsets import BaseFormSet
from django.contrib.admin.widgets import AdminDateWidget, AdminTimeWidget, AdminSplitDateTime
import os
import json
import ssl
import re
import pytz
from django_countries.fields import CountryField
import tempfile
from scipy.interpolate import splprep, splev, CubicSpline, interp1d
import numpy as np
import shutil
import requests
from rowingdata import trainingparser
from django.conf import settings
from sqlalchemy import create_engine
import sqlalchemy as sa
from sqlite3 import OperationalError
from django.utils import timezone
from datetime import timezone as dt_timezone
import pandas as pd
from dateutil import parser
import datetime
from taggit.managers import TaggableManager
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
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(ttz=None):
if ttz is None:
return (datetime.datetime.now(tz=dt_timezone.utc)+timezone.timedelta(days=182)).date()
return (datetime.datetime.utcnow()+timezone.timedelta(days=182)).astimezone(pytz.timezone(ttz)).date() # pragma: no cover
def a_week_from_now(ttz=None):
if ttz is None:
return (datetime.datetime.now(tz=dt_timezone.utc)+timezone.timedelta(days=7)).date()
return (datetime.datetime.utcnow()+timezone.timedelta(days=7)).astimezone(pytz.timezone(ttz)).date() # pragma: no cover
def current_day(ttz=None):
if ttz is None:
return (datetime.datetime.now(tz=dt_timezone.utc)).date()
return datetime.datetime.utcnow().astimezone(pytz.timezone(ttz)).date() # pragma: no cover
def current_time(ttz=None): # pragma: no cover
if ttz is None:
return datetime.datetime.now(tz=dt_timezone.utc)
return (datetime.datetime.utcnow()).astimezone(pytz.timezone(ttz)) # pragma: no cover
class UserFullnameChoiceField(forms.ModelChoiceField):
def label_from_instance(self, obj):
return obj.get_full_name()
def get_file_path(instance, filename):
ext = filename.split('.')[-1]
filename = "%s.%s" % (uuid.uuid4(), ext)
return filename
# return os.path.join(settings.MEDIA_ROOT, filename)
# 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): # pragma: no cover
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: # pragma: no cover
return value
if isinstance(value, list): # pragma: no cover
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): # pragma: no cover
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): # pragma: no cover
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): # pragma: no cover
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: # pragma: no cover
pass
return self.token.join([str(s) for s in newlist])
def value_to_string(self, obj): # pragma: no cover
value = self._get_val_from_obj(obj)
return self.get_deb_prep_value(value)
# model for Workout Name template list
class WorkoutNameTemplateField(models.TextField):
def __init__(self, *args, **kwargs):
super(WorkoutNameTemplateField, self).__init__(*args, **kwargs)
def to_python(self, value): # pragma: no cover
if not value:
return
return json.loads(value)
def from_db_value(self, value, expression, connection):
if not value:
return
return json.loads(value)
def get_db_prep_value(self, value, connection, prepared=False):
if not value:
return
return json.dumps(value)
def value_to_string(self, obj): # pragma: no cover
value = self._get_val_from_obj(obj)
return self.get_deb_prep_value(value)
# model for Planned Session Steps
class PlannedSessionStepField(models.TextField):
def __init__(self, *args, **kwargs):
super(PlannedSessionStepField, self).__init__(*args, **kwargs)
def to_python(self, value): # pragma: no cover
if not value:
return
return json.loads(value)
def from_db_value(self, value, expression, connection):
if not value:
return
return json.loads(value)
def get_db_prep_value(self, value, connection, prepared=False):
if not value:
return
return json.dumps(value)
def value_to_string(self, obj): # pragma: no cover
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): # pragma: no cover
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: # pragma: no cover
return value
if isinstance(value, list): # pragma: no cover
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): # pragma: no cover
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'
c2url = 'https://www.concept2.com/indoor-rowers/racing/records/world?machine=rower&event=&gender=&age_category=&weight_class=&adaptive=0&op=Search&form_id=concept2_record_search_form#results'
def update_records(url=c2url, verbose=True):
ssl._create_default_https_context = ssl._create_unverified_context
try:
dfs = pd.read_html(url, attrs={'class': 'views-table'})
df = dfs[0]
df.columns = df.columns.str.strip()
except: # pragma: no cover
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)
try:
distance = int(row.Distance)
except ValueError:
distance = int(row.Distance.replace(',', ''))
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: # pragma: no cover
print(record)
record.save()
except:
if verbose: # pragma: no cover
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): # pragma: no cover
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'
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): # pragma: no cover
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
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)
def __init__(self, *args, **kwargs):
userid = kwargs.pop('userid',0)
super(TeamInviteForm, self).__init__(*args, **kwargs)
if userid:
self.fields['user'].initial = userid
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)
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, debug=False):
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
def course_spline(coordinates):
latitudes = coordinates['latitude'].values
longitudes = coordinates['longitude'].values
t = np.linspace(0, 1, len(latitudes))
tnew = np.linspace(0, 1, 100)
try:
latnew = interp1d(t, latitudes)(tnew)
lonnew = interp1d(t, longitudes)(tnew)
except ValueError: # pragma: no cover
latnew = latitudes
lonnew = longitudes
newcoordinates = pd.DataFrame({
'latitude': latnew,
'longitude': lonnew,
})
return newcoordinates
def polygon_nearest_point(polygon, latitude, longitude,debug=False):
points = GeoPoint.objects.filter(polygon=polygon)
points = sorted(points, key = lambda p: geo_distance(p.latitude, p.longitude, latitude, longitude))
#if debug:
# for p in points:
# print(p,p.latitude, p.longitude, latitude, longitude, geo_distance(p.latitude, p.longitude, latitude, longitude))
return points[0].latitude, points[0].longitude
def polygon_exit_point(polygon, lat1, lon1, lat2, lon2):
dist, bearing = geo_distance(lat1, lon1, lat2, lon2)
dirveclat = (lat2-lat1)/dist
dirveclon = (lon2-lon1)/dist
newlat, newlon = move_one_meter(lat2, lon2, bearing)
path = polygon_to_path(polygon)
while path.contains_points([(newlat, newlon)])[0]:
newlat, newlon = move_one_meter(newlat, newlon, bearing)
return newlat, newlon
def course_coord_crewnerd_navigation(course):
polygons = GeoPolygon.objects.filter(
course=course).order_by("order_in_course")
latitudes = []
longitudes = []
latitude, longitude = polygon_coord_center(polygons[0])
latitudes.append(latitude)
longitudes.append(longitude)
debug = True
for p in polygons[1:]:
oldlat = latitude
oldlon = longitude
latitude, longitude = polygon_nearest_point(p,latitude,longitude, debug=debug)
debug = False
latitudes.append(latitude)
longitudes.append(longitude)
latitude, longitude = polygon_exit_point(p, oldlat, oldlon, latitude, longitude)
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_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)
def f(x):
return 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: # pragma: no cover
return 0
def get_delta_start(course): # pragma: no cover
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): # pragma: no cover
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: # pragma: no cover
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: # pragma: no cover
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')
)
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,
)
class CoachingGroup(models.Model):
name = models.CharField(
default='group', max_length=30, null=True, blank=True)
def __str__(self): # pragma: no cover
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
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'),
)
garminsports = (
('GENERIC', 'Custom'),
('RUNNING', 'Running'),
('CYCLING', 'Cycling'),
('LAP_SWIMMING', 'Lap Swimming'),
('STRENGTH_TRAINING', 'Strength Training'),
('CARDIO_TRAINING', 'Cardio Training'),
)
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')
eurocredits = models.IntegerField(default=0)
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))
coachtrialexpires = 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)
ftpset = models.BooleanField(default=False)
surveydone = models.BooleanField(default=False)
surveydonedate = models.DateTimeField(blank=True, null=True)
birthdate = models.DateField(null=True, blank=True)
emailalternatives = AlternativeEmails(
default=[], null=True, blank=True, verbose_name='Alternative Email addresses (separate with ",")')
emailbounced = models.BooleanField(default=False,
verbose_name='Email Address Bounced')
getimportantemails = models.BooleanField(default=True,
verbose_name='Get Important Emails')
get_rpe_warnings = models.BooleanField(default=True,
verbose_name='Get missing RPE warnings')
share_course_results = models.BooleanField(default=True,
verbose_name='Share Course Results')
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')
# 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)
# Power Zone Data
ftp = models.IntegerField(
default=226, verbose_name="Functional Threshold Power")
cogganzones = models.BooleanField(verbose_name='Use Default Power Zones',default=True)
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',
'Active Recovery',
'Endurance',
'Tempo',
'Threshold',
'Anaerobic'])
hrzones = PowerZonesField(default=['Rest',
'UT2',
'UT1',
'AT',
'TR',
'AN', 'max'])
# median WpS
median_wps = models.IntegerField(
default=400, verbose_name='Median Work per Stroke (OTW)')
median_wps_erg = models.IntegerField(
default=400, verbose_name='Median Work per Stroke (ergometer)')
# 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")
defaultlandingpage2 = models.CharField(default='workout_flexchart_stacked_view',
max_length=200,
choices=landingpages2,
verbose_name="Alternative Landing Page")
defaultlandingpage3 = models.CharField(default='workout_view',
max_length=200,
choices=landingpages2,
verbose_name="Title link on workout list")
workoutnametemplate = WorkoutNameTemplateField(default=['date','name','distance','ownerfirst','ownerlast','duration','boattype','workouttype'])
# 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)
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)
rp3_auto_import = models.BooleanField(default=False)
rojabo_token = models.CharField(
default='', max_length=200, blank=True, null=True)
rojabo_refreshtoken = models.CharField(
default='', max_length=200, blank=True, null=True)
rojabo_tokenexpirydate = models.DateTimeField(blank=True, null=True)
nktoken = models.TextField(
default='', max_length=1000, blank=True, null=True)
nktokenexpirydate = models.DateTimeField(blank=True, null=True)
nkrefreshtoken = models.TextField(default='', max_length=1000,
blank=True, null=True)
nk_owner_id = models.BigIntegerField(default=0)
nk_auto_import = models.BooleanField(
default=False, verbose_name='NK Logbook auto import')
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)
garminactivity = models.CharField(default='RUNNING', max_length=200,
verbose_name='Garmin Activity for Structured Workouts',
choices=garminsports)
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)
privacychoices = (
('visible', 'Visible'),
('hidden', 'Hidden'),
)
getemailnotifications = models.BooleanField(default=False,
verbose_name='Receive email notifications')
# 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 and data 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')
dosmooth = models.BooleanField(
default=True, verbose_name='Savitzky-Golay Filter (recommended)')
erg_recalculatepower = models.BooleanField(
default=True, verbose_name='Erg Power from pace')
# Auto Join
autojoin = models.BooleanField(
default=False, verbose_name='Auto Join Workout Segments')
def __str__(self):
return self.user.first_name+' '+self.user.last_name
def clean_email(self): # pragma: no cover
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': # pragma: no cover
self.coachinggroups.remove(group)
except Rower.DoesNotExist: # pragma: no cover
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: # pragma: no cover
pass
return coaches
def can_coach(self):
if self.rowerplan not in ['coach', 'freecoach']:
return False
rs = Rower.objects.filter(coachinggroups__in=[self.mycoachgroup])
rekwests = CoachOffer.objects.filter(coach=self)
if len(rs)+len(rekwests) < self.clubsize and self.offercoaching: # pragma: no cover
return True
return False
@property
def ispaid(self): # pragma: no cover
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 = []
class UserMessage(models.Model):
receiver = models.ForeignKey(Rower, blank=True, null=True, on_delete=models.SET_NULL)
datetime = models.DateTimeField()
isread = models.BooleanField(default=False)
text = models.CharField(max_length=1000)
subject = models.CharField(max_length=100,default='Message')
def __str__(self):
return '{r1} {r2} {d} {subject}'.format(
r1 = self.receiver.user.first_name,
r2 = self.receiver.user.last_name,
d = self.datetime,
subject = self.subject
)
# 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)
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':
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()
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): # pragma: no cover
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']
pass
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)
def __str__(self):
str = 'Condition: {metric} {condition} {value1}'.format(
metric=self.metric,
condition=self.condition,
value1 = self.value1,
)
if self.condition == 'between':
str = 'Condition: {metric} between {value1} and {value2}'.format(
metric=self.metric,
condition=self.condition,
value1 = self.value1,
value2 = self.value2,
)
return str
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: # pragma: no cover
raise forms.ValidationError(
'When using between, you must fill value 1 and value 2')
except KeyError: # pragma: no cover
pass
class BaseConditionFormSet(BaseFormSet):
def clean(self):
if any(self.errors): # pragma: no cover
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']
pass
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=current_day)
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 save(self, *args, **kwargs):
if self.next_run > (timezone.now()+datetime.timedelta(days=self.period)).date():
self.next_run = (timezone.now()+datetime.timedelta(days=self.period)).date()
super(Alert, self).save(*args, **kwargs)
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): # pragma: no cover
metricdict = {key: value for (key, value) in parchoicesy1}
return metricdict[self.measured.metric]
def description(self): # pragma: no cover
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,
)
for condition in self.filter.all():
description += ' '+str(condition)+';'
return description
def shortdescription(self): # pragma: no cover
# 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): # pragma: no cover
if any(self.serrors):
return
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, null=True, on_delete=models.SET_NULL)
followers = models.ManyToManyField(Rower, related_name='followed_courses')
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')
updated = models.DateTimeField(default=timezone.now, blank=True)
def __str__(self):
name = self.name
# country = self.country
d = self.distance
if d == 0: # pragma: no cover
self.distance = course_length(self)
self.save()
d = self.distance
return u'{name} - {d}m'.format(
name=name,
# country=country,
d=d,
)
def save(self, *args, **kwargs):
self.update = timezone.now()
super(GeoCourse, self).save(*args, **kwargs)
self.followers.add(self.manager)
@property
def coord(self):
return course_coord_center(self)
@property
def with_cn_nav_waypoints(self):
polygons = GeoPolygon.objects.filter(course=self).order_by("order_in_course")
if polygons[0].name != "Start":
return False
if polygons[len(polygons)-1].name != "Finish":
return False
for i in range(1,len(polygons)-1):
if polygons[i].name[0:2].lower() != 'wp':
return False
try:
getal = float(polygons[i].name[2:])
except ValueError:
return False
return True
class GeoCourseEditForm(ModelForm):
class Meta:
model = GeoCourse
fields = ['name', 'country', 'notes']
widgets = {
'notes': forms.Textarea,
}
class GeoPolygon(models.Model):
name = models.CharField(max_length=150, blank=True)
course = models.ForeignKey(
GeoCourse, blank=True, on_delete=models.CASCADE, related_name='polygons')
order_in_course = models.IntegerField(default=0)
def __str__(self): # pragma: no cover
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: # pragma: no cover
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): # pragma: no cover
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: # pragma: no cover
teams = []
if not teams:
self.fields.pop('rowers')
else:
qs1 = Rower.objects.filter(
team__in=teams
).distinct().order_by("user__last_name", "user__first_name")
self.fields['rowers'].queryset = qs1
class InstantPlan(models.Model):
uuid = models.UUIDField(
primary_key=False, editable=True, default=uuid.uuid4)
owner = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
name = models.CharField(max_length=150, blank=True)
goal = models.CharField(max_length=150, blank=True,
verbose_name="Goal (one sentence)")
description = models.TextField(max_length=450, blank=True)
duration = models.IntegerField(
default=6, verbose_name='Duration in Calendar Days')
target = models.TextField(
max_length=450, blank=True, verbose_name='What the plan will achieve')
hoursperweek = models.IntegerField(
default=4, verbose_name='Hours Per Week')
sessionsperweek = models.IntegerField(
default=3, verbose_name='Number of sessions per week')
yaml = models.FileField(upload_to=get_file_path,
verbose_name="Plan YAML file", null=True, blank=True)
price = models.IntegerField(default=0, verbose_name="Price in EURO")
url = models.CharField(max_length=250, blank=True,
verbose_name="Link to page with more information")
private = models.BooleanField(default=False,
verbose_name="Hidden, personal")
def __str__(self): # pragma: no cover
return self.name
def save(self, *args, **kwargs):
self.yaml.open(mode="r")
yamltext = self.yaml.read()
self.yaml.close()
authorizationstring = 'Bearer '+settings.WORKOUTS_FIT_TOKEN
url = settings.WORKOUTS_FIT_URL+"/trainingplan/"
headers = {'Authorization': authorizationstring}
try:
response = requests.post(url=url, headers=headers, data=yamltext)
if response.status_code == 200:
data = response.json()
self.yaml.name = data['filename']
self.uuid = data['ID']
self.name = data['name']
self.description = data['description']
self.duration = data['duration']
self.yaml = None
except:
pass
super(InstantPlan, self).save(*args, **kwargs)
class InstantPlanForm(ModelForm):
class Meta:
model = InstantPlan
fields = [
'name',
'price',
'url',
'goal',
'description',
'duration',
'target',
'hoursperweek',
'sessionsperweek',
'yaml',
'private'
]
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,
verbose_name="Plan Name")
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): # pragma: no cover
raise ValidationError(
"Basic user cannot have a training plan"
)
if self.enddate < self.startdate: # pragma: no cover
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: # pragma: no cover
self.rowers.remove(rower)
if self.status:
otherplans = TrainingPlan.objects.filter(
status=True).exclude(
pk=self.pk).order_by(
"-startdate")
for otherplan in otherplans: # pragma: no cover
if otherplan.startdate <= self.enddate and otherplan.startdate >= self.startdate:
for rower in self.rowers.all(): # pragma: no cover
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 length(self):
startdate = self.startdate
enddate = self.enddate
return (enddate-startdate).days
def overlap(self,startdate,enddate):
is_overlapped = max(self.startdate, startdate) < min(self.enddate, enddate)
if not is_overlapped:
return 0
if startdate >= self.startdate:
if self.enddate >= enddate:
return (enddate-startdate).days
else:
return (self.enddate-startdate).days
elif startdate < self.startdate:
if enddate >= self.enddate:
return (self.enddate-self.startdate).days
else:
return (enddate-self.startdate).days
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): # pragma: no cover
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: # pragma: no cover
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: # pragma: no cover
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: # pragma: no cover
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: # pragma: no cover
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: # pragma: no cover
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: # pragma: no cover
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: # pragma: no cover
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: # pragma: no cover
macr = TrainingMicroCycle(
plan=plan,
startdate=plan.startdate,
enddate=cycles[0].startdate-datetime.timedelta(days=1),
type='filler',
name='Filler'
)
macr.save()
def microcyclecheckdates(plan): # pragma: no cover
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): # pragma: no cover
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): # pragma: no cover
cycles = TrainingMacroCycle.objects.filter(
plan=plan
).order_by("-startdate")
thedate = plan.enddate
while cycles:
if cycles[0].enddate < plan.startdate:
cycles[0].delete()
if cycles[0].startdate > plan.enddate:
cycles[0].delete()
if cycles[0].enddate > thedate:
cycles[0].enddate = thedate
cycles[0].save()
thedate = cycles[0].startdate-datetime.timedelta(days=1)
cycles = cycles[1:]
cycles = TrainingMacroCycle.objects.filter(
plan=plan
).order_by("startdate")
thedate = plan.startdate
while cycles:
if cycles[0].startdate < thedate:
cycles[0].startdate = thedate
cycles[0].save()
try:
thedate = cycles[1].startdate-datetime.timedelta(days=1)
except IndexError:
pass
cycles = cycles[1:]
class TrainingMacroCycle(models.Model):
plan = models.ForeignKey(TrainingPlan, 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): # pragma: no cover
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: # pragma: no cover
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: # pragma: no cover
self.enddate = self.plan.enddate
if self.startdate < self.plan.startdate: # pragma: no cover
self.startdate = self.plan.startdate
othercycles = TrainingMacroCycle.objects.filter(
plan=self.plan).exclude(pk=self.pk).order_by("-startdate")
for othercycle in othercycles: # pragma: no cover
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: # pragma: no cover
createmesofillers(self)
class TrainingMacroCycleForm(ModelForm):
class Meta:
model = TrainingMacroCycle
fields = ['name', 'startdate', 'enddate', 'notes']
widgets = {
'startdate': AdminDateWidget(),
'enddate': AdminDateWidget()
}
class TrainingMesoCycle(models.Model):
plan = models.ForeignKey(TrainingMacroCycle, 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): # pragma: no cover
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: # pragma: no cover
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: # pragma: no cover
f.delete()
if self.enddate > self.plan.enddate: # pragma: no cover
self.enddate = self.plan.enddate
if self.startdate < self.plan.startdate: # pragma: no cover
self.startdate = self.plan.startdate
othercycles = TrainingMesoCycle.objects.filter(
plan=self.plan).exclude(pk=self.pk).order_by("-startdate")
for othercycle in othercycles: # pragma: no cover
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)
else: # pragma: no cover
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: # pragma: no cover
createmicrofillers(self)
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): # pragma: no cover
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: # pragma: no cover
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: # pragma: no cover
f.delete()
if self.enddate > self.plan.enddate: # pragma: no cover
self.enddate = self.plan.enddate
if self.startdate < self.plan.startdate: # pragma: no cover
self.startdate = self.plan.startdate
othercycles = TrainingMicroCycle.objects.filter(
plan=self.plan).exclude(pk=self.pk).order_by("-startdate")
for othercycle in othercycles: # pragma: no cover
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)
class PlannedSessionStep(models.Model):
intensitytypes = (
("Active", "Active"),
("Rest", "Rest"),
("Warmup", "Warmup"),
("Cooldown", "Cooldown")
)
durationtypes = (
("Distance", "Distance"),
("Time", "Time"),
('RepeatUntilStepsCmplt','Repeat previous blocks n times')
)
targettypes = (
("Speed", "Speed"),
("HeartRate", "HeartRate"),
("Cadence", "Cadence"),
("Power", "Power")
)
manager = models.ForeignKey(User, null=True, on_delete=models.CASCADE)
name = models.TextField(default='',max_length=200, blank=True, null=True)
type = models.TextField(default='',max_length=200, blank=True, null=True)
durationvalue = models.FloatField(default=0, verbose_name="Duration Value")
durationtype = models.TextField(default='Time',max_length=200,
choices=durationtypes,
verbose_name='Duration Type')
targetvalue = models.IntegerField(default=0, verbose_name="Target Value")
targettype = models.TextField(default='',max_length=200, blank=True, null=True,
choices=targettypes, verbose_name="Target Type")
targetvaluelow = models.IntegerField(default=0,
verbose_name="Target Value Low")
targetvaluehigh = models.IntegerField(default=0,
verbose_name="Target Value High")
intensity = models.TextField(default='',max_length=200, blank=True, null=True,
choices=intensitytypes,
verbose_name = "Intensity")
description = models.TextField(default='',max_length=200, blank=True, null=True)
color = models.TextField(default='#ddd',max_length=200)
def save(self, *args, **kwargs):
if self.intensity == "Warmup":
self.color = "#ffcccb"
elif self.intensity == "Cooldown":
self.color = '#90ee90'
elif self.intensity == "Rest":
self.color = 'add8e6'
if self.durationtype == 'RepeatUntilStepsCmplt':
self.color = 'ffffa7'
self.durationvalue = int(self.durationvalue)
super(PlannedSessionStep, self).save(*args, **kwargs)
def asdict(self):
d = {
'wkt_step_name': self.name,
'durationType': self.durationtype,
'durationValue': self.durationvalue,
'targetType': self.targettype,
'targetValue': self.targetvalue,
'targetValueLow': self.targetvaluelow,
'targetValueHigh': self.targetvaluehigh,
'description': self.description,
'stepId': self.pk,
'intensity': self.intensity,
}
return d
class StepEditorForm(ModelForm):
class Meta:
model = PlannedSessionStep
fields = [
'name',
#'type',
'durationtype',
'durationvalue',
'targettype',
'targetvalue',
'targetvaluelow',
'targetvaluehigh',
'intensity',
'description',
]
widgets = {
'name': forms.Textarea(attrs={'rows':1, 'cols':50}),
}
def __init__(self, *args, **kwargs):
super(StepEditorForm, self).__init__(*args, **kwargs)
if self.instance.durationtype == 'Time':
self.initial['durationvalue'] = self.instance.durationvalue / 60000
elif self.instance.durationtype == 'Distance':
self.initial['durationvalue'] = self.instance.durationvalue / 100
def save(self, *args, **kwargs):
# conversions
if self.instance.durationtype == 'Time':
self.instance.durationvalue *= 60000
elif self.instance.durationtype == 'Distance':
self.instance.durationvalue *= 100
return super(StepEditorForm, self).save(*args, **kwargs)
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)
rojabo_id = models.BigIntegerField(default=0,blank=True)
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')
sessionsport = models.CharField(default='water', choices=mytypes.workouttypes,
max_length=50, verbose_name='Sport')
sessionvalue = models.IntegerField(default=60, verbose_name='Value')
approximate_distance = models.IntegerField(
default=0, verbose_name='Approximate Distance')
approximate_duration = models.IntegerField(
default=0, verbose_name='Approximate Duration')
approximate_rscore = models.IntegerField(
default=0, verbose_name='Approximate rScore')
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)
is_public = models.BooleanField(default=False)
can_be_shared = models.BooleanField(default=True)
fitfile = models.FileField(upload_to=get_file_path, blank=True, null=True)
steps = PlannedSessionStepField(default={}, null=True, max_length=1000, blank=True)
interval_string = models.TextField(max_length=1000, default=None, blank=True, null=True,
verbose_name='Interval String (optional)')
garmin_workout_id = models.BigIntegerField(default=0)
garmin_schedule_id = models.BigIntegerField(default=0)
tags = TaggableManager(blank=True)
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 update_steps(self): # pragma: no cover
# read file
if self.fitfile:
steps = steps_read_fit(settings.MEDIA_ROOT+'/'+self.fitfile.name)
self.steps = steps
self.save()
def save(self, *args, **kwargs):
if self.sessionvalue <= 0: # pragma: no cover
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"
)
# interval string
if self.interval_string: # pragma: no cover
try:
dct = trainingparser.parsetodict(self.interval_string)
dct = [item for item in dct if item['value'] != 0]
dct = trainingparser.tofitdict(dct)
for step in dct['steps']:
try:
step['targetValue'] = int(step['targetValue'])
step['targetValueLow'] = int(step['targetValueHigh'])
step['targetValueHigh'] = int(step['targetValueLow'])
except KeyError:
pass
self.steps = dct
except:
pass
# sort units
if self.sessionmode == 'distance':
if self.sessionunit not in ['m', 'km']: # pragma: no cover
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: # pragma: no cover
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 is None: # pragma: no cover
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: # pragma: no cover
self.enddate = self.startdate
if self.preferreddate > self.enddate: # pragma: no cover
self.preferreddate = self.enddate
if self.preferreddate < self.startdate: # pragma: no cover
self.preferreddate = self.startdate
if self.steps:
steps = self.steps
elif self.fitfile: # pragma: no cover
steps = steps_read_fit(os.path.join(
settings.MEDIA_ROOT, self.fitfile.name))
self.steps = steps
if self.steps and not self.fitfile:
filename = 'aap.fit'
filename = get_file_path(self, filename)
steps = self.steps
steps['filename'] = os.path.join(settings.MEDIA_ROOT, filename)
_ = steps_write_fit(steps)
self.fitfile.name = filename
self.steps = steps
# calculate approximate distance
if self.steps:
sdict, totalmeters, totalseconds, totalrscore = ps_dict_order(
self.steps)
self.approximate_distance = int(totalmeters)
self.approximate_duration = int(totalseconds/60.)
self.approximate_rscore = int(totalrscore)
self.criterium = 'none'
if self.sessionmode == 'time':
self.sessionvalue = self.approximate_duration
elif self.sessionmode == 'distance': # pragma: no cover
self.sessionvalue = self.approximate_distance
elif self.sessionmode == 'rScore': # pragma: no cover
self.sessionvalue = self.approximate_rscore
super(PlannedSession, self).save(*args, **kwargs)
@receiver(models.signals.post_delete, sender=PlannedSession)
def auto_delete_fitfile_on_delete(sender, instance, **kwargs):
# delete CSV file
if instance.fitfile: # pragma: no cover
filename = os.path.join(settings.MEDIA_ROOT, instance.fitfile.name)
if os.path.isfile(filename):
os.remove(filename)
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)
# corresponds to workout workouttype
boatclass = models.CharField(max_length=150, default='water')
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'),
)
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: # pragma: no cover
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: # pragma: no cover
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): # pragma: no cover
return self.filename
def delete(self, *args, **kwargs): # pragma: no cover
os.remove(self.filename)
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',
'sessionsport',
'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}),
# 'sessiontype': forms.Select(attrs={'style':'width:50px'})
}
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',
'sessionsport',
'sessiontype',
'sessionmode',
'criterium',
'sessionvalue',
'sessionunit',
'course',
'comment',
'interval_string',
'fitfile',
'tags',
]
dateTimeOptions = {
'format': 'yyyy-mm-dd',
'autoclose': True,
}
widgets = {
'comment': forms.Textarea,
'interval_string': forms.Textarea(attrs={'rows': 2, 'cols': 50})
}
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: # pragma: no cover
timezone_str = 'UTC'
if timezone_str is None: # pragma: no cover
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: # pragma: no cover
raise forms.ValidationError(
'The Value must be a positive, non-zero value')
unit = cd['sessionunit']
if unit == 'm' and value < 100: # pragma: no cover
raise forms.ValidationError('Minimum distance is 100m')
start_time = cd['start_time']
if start_time is None: # pragma: no cover
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: # pragma: no cover
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: # pragma: no cover
evaluation_closure = enddatetime+datetime.timedelta(days=1)
cd['evaluation_closure'] = evaluation_closure
if registration_form == 'manual': # pragma: no cover
try:
registration_closure = pytz.timezone(
timezone_str
).localize(
registration_closure.replace(tzinfo=None)
)
except AttributeError:
registration_closure = startdatetime
elif registration_form == 'windowstart': # pragma: no cover
registration_closure = startdatetime
elif registration_form == 'windowend': # pragma: no cover
registration_closure = enddatetime
else:
registration_closure = evaluation_closure
if registration_closure <= timezone.now(): # pragma: no cover
raise forms.ValidationError(
"Registration Closure cannot be in the past")
if startdatetime > enddatetime: # pragma: no cover
raise forms.ValidationError(
"The Start of the challenge Window should be before the End of the challenge Window")
if cd['evaluation_closure'] <= enddatetime: # pragma: no cover
raise forms.ValidationError(
"Evaluation closure deadline should be after the challenge Window closes")
if cd['evaluation_closure'] <= timezone.now(): # pragma: no cover
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: # pragma: no cover
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: # pragma: no cover
raise forms.ValidationError(
'Must have end time',
code='missing endtime'
)
if end_time is None: # pragma: no cover
raise forms.ValidationError(
'Must have end time',
code='missing endtime'
)
try:
end_date = cd['enddate']
except KeyError: # pragma: no cover
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: # pragma: no cover
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: # pragma: no cover
evaluation_closure = enddatetime+datetime.timedelta(days=1)
cd['evaluation_closure'] = evaluation_closure
if registration_form == 'manual': # pragma: no cover
try:
registration_closure = pytz.timezone(
timezone_str
).localize(
registration_closure.replace(tzinfo=None)
)
except AttributeError:
registration_closure = startdatetime
elif registration_form == 'windowstart': # pragma: no cover
registration_closure = startdatetime
elif registration_form == 'windowend': # pragma: no cover
registration_closure = enddatetime
else:
registration_closure = evaluation_closure
if registration_closure <= timezone.now(): # pragma: no cover
raise forms.ValidationError(
"Registration Closure cannot be in the past")
if startdatetime > enddatetime: # pragma: no cover
raise forms.ValidationError(
"The Start of the challenge Window should be before the End of the challenge Window")
if cd['evaluation_closure'] <= enddatetime: # pragma: no cover
raise forms.ValidationError(
"Evaluation closure deadline should be after the challenge Window closes")
if cd['evaluation_closure'] <= timezone.now(): # pragma: no cover
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
ergtypes = mytypes.ergtypes
# Workout
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?)'),
# 50 TSS/hour
(4, '4 Moderate (You can talk in short spurts, breathing more labored, this feels just right)'),
(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)'),
# 100 TSS / hour
(8, "8 Hard (You can barely talk, breathing heavily, hoping you won't have to this that long)"),
(9, '9 Very Hard (My goodness, please make it stop)'), # 120 TSS / hour
# 140 TSS / hour
(10, '10 Max Effort (You can barely remember your name, you would rather rip out your toenails than go through this)')
)
class Workout(models.Model):
workouttypes = mytypes.workouttypes
workoutsources = mytypes.workoutsources
privacychoices = mytypes.privacychoices
adaptivetypes = mytypes.adaptivetypes
boatbrands = mytypes.boatbrands
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+ergtypes, max_length=50,
default='1x',
verbose_name='Boat/Rower 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=timezone.now)
startdatetime = models.DateTimeField(blank=True, null=True)
timezone = models.CharField(default='UTC',
# choices=timezones,
max_length=100)
distance = models.IntegerField(default=0)
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)
uploadedtotp = models.BigIntegerField(default=0)
uploadedtogarmin = models.BigIntegerField(default=0)
uploadedtorp3 = models.BigIntegerField(default=0)
uploadedtonk = 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)
seatnumber = models.IntegerField(default=1, verbose_name='Seat Number')
boatname = models.CharField(default='', blank=True, null=True, max_length=150, verbose_name='Boat Name')
empowerside = models.CharField(default='starboard', blank=True, null=True, max_length=150, verbose_name='Empower Oarlock side')
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: # pragma: no cover
self.notes = self.notes[0:950]
if not can_add_workout(user.user):
raise forms.ValidationError(
"Free Coach User cannot have any workouts")
if self.timezone == 'tzutc()':
self.timezone = 'UTC' # pragma: no cover
if self.workouttype in mytypes.otwtypes and self.boattype in mytypes.ergtypes:
self.boattype = '1x'
elif self.workouttype in mytypes.otetypes and self.boattype in mytypes.boattypes:
self.boattype = 'static'
super(Workout, self).save(*args, **kwargs)
def __str__(self):
try:
dates = self.date.strftime('%Y-%m-%d')
except AttributeError:
dates = ''
try:
durations = self.duration.strftime("%H:%M:%S")
except AttributeError:
durations = ''
elements = dict(
date = dates,
name = self.name,
distance = str(self.distance)+'m',
ownerfirst = self.user.user.first_name,
ownerlast = self.user.user.last_name,
duration = durations,
boattype = self.boattype,
workouttype = self.workouttype,
seatnumber = 'seat '+str(self.seatnumber),
boatname = 'boat '+str(self.boatname),
empowerside = self.empowerside,
inboard = self.inboard,
oarlength = self.oarlength
)
if len(self.user.workoutnametemplate):
try:
stri = u''
for element in self.user.workoutnametemplate:
stri += u'{'+u'{element}'.format(element=element)+u'} '
stri = stri[:-1]
return stri.format(**elements)
except ValueError:
return self.name
except AttributeError:
return "No workout"
if self.workouttype not in ['water','rower']:
try:
stri = u'{date} {name} {distance} {duration} {workouttype} {ownerfirst} {ownerlast}'.format(
**elements
)
except ValueError:
stri = self.name
except AttributeError:
return "No workout"
else:
try:
stri = u'{date} {name} {distance} {duration} {workouttype} {boattype} {ownerfirst} {ownerlast}'.format(
**elements
)
except (ValueError, AttributeError):
stri = self.name
return stri
class WorkoutRPEForm(ModelForm):
class Meta:
model = Workout
fields = ['rpe']
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)
uploadedtotp = models.BigIntegerField(default=0)
uploadedtonk = 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,
uploadedtotp=instance.uploadedtotp,
uploadedtonk=instance.uploadedtonk
)
t.save()
# delete files belonging to workout instance
# related GraphImage objects should be deleted automatically
class SyncRecord(models.Model):
workout = models.ForeignKey(Workout, on_delete=models.CASCADE, blank=True, default=None, null=True) #, null=True)
rower = models.ForeignKey(Rower, on_delete=models.CASCADE, blank=True, default=None, null=True) #, null=True)
stravaid = models.BigIntegerField(unique=True,null=True,default=None)
sporttracksid = models.BigIntegerField(unique=True,null=True,default=None)
nkid = models.BigIntegerField(unique=True,null=True,default=None)
c2id = models.BigIntegerField(unique=True,null=True,default=None)
tpid = models.BigIntegerField(unique=True,null=True,default=None)
rp3id = models.BigIntegerField(unique=True,null=True,default=None)
def save(self, *args, **kwargs):
if self.workout:
self.rower = self.workout.user
return super(SyncRecord, self).save(*args, **kwargs)
def __str__(self):
str = 'SyncRecord {i} {r} {w} '.format(
i = self.id,
r = self.rower,
w = self.workout,
)
str2 = ''
for field in ['stravaid', 'sporttracksid', 'nkid', 'c2id', 'tpid']:
value = getattr(self, field, None)
if value is not None:
str2 += '{w}: {v},'.format(
w = field,
v = value
)
if str2:
str = str+'('+str2+')'
return str
def create_or_update_syncrecord(rower, workout, **kwargs):
try:
kwargs.pop('rower')
except KeyError:
pass
try:
kwargs.pop('workout')
except KeyError:
pass
if workout:
records = SyncRecord.objects.filter(workout=workout,rower=rower)
try:
record = records[0]
except IndexError:
records = SyncRecord.objects.filter(**kwargs,rower=rower)
if records.count():
record = records[0]
record.workout = workout
else:
record = SyncRecord(rower=rower, workout=workout)
else: # not workout
records = SyncRecord.objects.filter(**kwargs, rower=rower)
if records.count():
record = records[0]
else:
record = SyncRecord(rower=rower)
for field in record._meta.fields:
value = kwargs.get(field.name, None)
if value:
setattr(record, field.name, value)
try:
record.save()
except IntegrityError:
pass
return record
def get_known_ids(rower, field_name):
knownids = uniqify(
getattr(record, field_name, None) for record in SyncRecord.objects.filter(rower=rower)
)
return knownids
@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)
os.remove(dirname)
except (FileNotFoundError, IsADirectoryError):
try:
shutil.rmtree(dirname)
except:
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: # pragma: no cover
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)
try:
enddatetime = ww.startdatetime+delta
if enddatetime > d.startdatetime:
ws2.append(ww)
except TypeError:
pass
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)
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',
blank=True, null=True)
course = models.ForeignKey(
GeoCourse, on_delete=models.CASCADE, null=True, blank=True)
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): # pragma: no cover
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 is None and other.race is not None:
return False
if self.race is not None and other.race is None:
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 save(self, *args, **kwargs):
if self.race and not self.course:
self.course = self.race.course
return super(VirtualRaceResult, self).save(*args, **kwargs)
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: # pragma: no cover
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)
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, null=True, blank=True)
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): # pragma: no cover
if self.race is None and other.race is not None:
return False
if self.race is not None and other.race is None:
return False
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: # pragma: no cover
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: # pragma: no cover
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)
courseid = 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)
def save(self, *args, **kwargs):
if self.userid == 0:
w = Workout.objects.get(id=self.workoutid)
self.userid = w.user.id
super(CourseTestResult, self).save(*args, **kwargs)
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: # pragma: no cover
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: # pragma: no cover
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')
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
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): # pragma: no cover
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): # pragma: no cover
others = GraphImage.objects.filter(filename=instance.filename)
if others.count() == 0:
os.remove(instance.filename)
else:
pass
# 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',
'boatname',
'seatnumber',
'empowerside',
'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: # pragma: no cover
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: # pragma: no cover
self.fields['plannedsession'].queryset = sps
else:
del self.fields['plannedsession']
def clean(self):
if any(self.errors):
return
cd = self.cleaned_data
if cd['duration'] is None or cd['duration'] == '':
raise forms.ValidationError('Duration cannot be empty')
# Used for the rowing physics calculations
class AdvancedWorkoutForm(ModelForm):
class Meta:
model = Workout
fields = ['boattype', 'weightvalue', 'boatbrand']
class RowerExportForm(ModelForm):
class Meta:
model = Rower
fields = [
'stravaexportas',
'garminactivity',
'polar_auto_import',
'c2_auto_export',
'c2_auto_import',
'nk_auto_import',
'sporttracks_auto_export',
'strava_auto_export',
'strava_auto_import',
'strava_auto_delete',
'trainingpeaks_auto_export',
'rp3_auto_import'
]
# Simple form to set rower's Functional Threshold Power
class SimpleRowerPowerForm(ModelForm):
otwftp = forms.IntegerField(initial=0,required=True, label='FTP on water')
class Meta:
model = Rower
fields = ['ftp']
def __init__(self, *args, **kwargs):
super(SimpleRowerPowerForm, self).__init__(*args, **kwargs)
self.initial['otwftp'] = int((1-0.01*self.instance.otwslack)*self.instance.ftp)
def save(self, *args, **kwargs):
otwslack = -100.*(self.cleaned_data['otwftp']-self.cleaned_data['ftp'])/(self.cleaned_data['ftp'])
self.instance.otwslack = otwslack
return super(SimpleRowerPowerForm, self).save(*args, **kwargs)
class RowerPowerForm(ModelForm):
otwftp = forms.IntegerField(initial=0,required=False, label='FTP on water')
class Meta:
model = Rower
fields = ['hrftp', 'ftp','cogganzones']
field_order = ['hrftp', 'ftp', 'otwftp', 'cogganzones']
def __init__(self, *args, **kwargs):
super(RowerPowerForm, self).__init__(*args, **kwargs)
self.initial['otwftp'] = int((1-0.01*self.instance.otwslack)*self.instance.ftp)
def save(self, *args, **kwargs):
try:
otwslack = -100.*(self.cleaned_data['otwftp']-self.cleaned_data['ftp'])/(self.cleaned_data['ftp'])
except:
otwslack = 10.
self.instance.otwslack = otwslack
return super(RowerPowerForm, self).save(*args, **kwargs)
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): # pragma: no cover
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): # pragma: no cover
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:
ut3name = 'UT3'
cleaned_data['ut3name'] = ut3name
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',
'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',
'get_rpe_warnings',
'share_course_results',
'defaulttimezone', 'showfavoritechartnotes',
'fav_analysis',
'usersmooth',
'defaultlandingpage',
'defaultlandingpage2',
'defaultlandingpage3',
'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: # pragma: no cover
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 DataRowerForm(ModelForm):
class Meta:
model = Rower
fields = ['dosmooth', 'erg_recalculatepower', 'autojoin']
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') # pragma: no cover
def clean_email(self):
email = self.cleaned_data.get('email')
try:
validate_email(email)
except ValidationError: # pragma: no cover
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: # pragma: no cover
return email
raise forms.ValidationError(
'This email address is not allowed') # pragma: no cover
# 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: # pragma: no cover
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: # pragma: no cover
raise forms.ValidationError(
"UT2 heart rate should be higher than 10 bpm")
if ut2 > 250: # pragma: no cover
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: # pragma: no cover
raise forms.ValidationError(
"UT1 heart rate should be higher than 10 bpm")
if ut1 > 250: # pragma: no cover
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: # pragma: no cover
raise forms.ValidationError(
"AT heart rate should be higher than 10 bpm")
if at > 250: # pragma: no cover
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: # pragma: no cover
raise forms.ValidationError(
"TR heart rate should be higher than 10 bpm")
if tr > 250: # pragma: no cover
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: # pragma: no cover
raise forms.ValidationError(
"AN heart rate should be higher than 10 bpm")
if an > 250: # pragma: no cover
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: # pragma: no cover
raise forms.ValidationError(
"Max heart rate should be higher than 10 bpm")
if max > 250: # pragma: no cover
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: # pragma: no cover
rest = 0
try:
ut2 = self.cleaned_data['ut2']
except: # pragma: no cover
try:
ut2 = int(self.data['ut2'])
except ValueError:
ut2 = 0
try:
ut1 = self.cleaned_data['ut1']
except: # pragma: no cover
try:
ut1 = int(self.data['ut1'])
except ValueError:
ut1 = 0
try:
at = self.cleaned_data['at']
except: # pragma: no cover
try:
at = int(self.data['at'])
except ValueError:
at = 0
try: # pragma: no cover
an = self.cleaned_data['an']
except: # pragma: no cover
try:
an = int(self.data['an'])
except ValueError:
an = 0
try:
tr = self.cleaned_data['tr']
except: # pragma: no cover
try:
tr = int(self.data['tr'])
except ValueError:
tr = 0
try:
max = self.cleaned_data['max']
except: # pragma: no cover
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: # pragma: no cover
raise forms.ValidationError("UT2 should be lower than UT1")
if ut1 >= at: # pragma: no cover
raise forms.ValidationError("UT1 should be lower than AT")
if at >= tr: # pragma: no cover
raise forms.ValidationError("AT should be lower than TR")
if tr >= an: # pragma: no cover
raise forms.ValidationError("TR should be lower than AN")
if an >= max: # pragma: no cover
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)
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()
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, on_delete=models.PROTECT)
workout = models.ForeignKey(Workout, on_delete=models.CASCADE)
def __str__(self): # pragma: no cover
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
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): # pragma: no cover
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): # pragma: no cover
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): # pragma: no cover
return self.creation_date + datetime.timedelta(self.expiration_seconds) < timezone.now()
@property
def expiration_date(self): # pragma: no cover
return self.creation_date + datetime.timedelta(self.expiration_seconds)
class InStrokeAnalysis(models.Model):
workout = models.ForeignKey(Workout, on_delete=models.CASCADE)
rower = models.ForeignKey(Rower, on_delete=models.SET_NULL, null=True)
metric = models.CharField(max_length=140, blank=True, null=True)
name = models.CharField(max_length=150, blank=True, null=True)
date = models.DateField(blank=True, null=True)
notes = models.TextField(blank=True)
start_second = models.IntegerField(default=0)
end_second = models.IntegerField(default=3600)
spm_min = models.IntegerField(default=10)
spm_max = models.IntegerField(default=45)
average_spm = models.FloatField(default=23)
average_boatspeed = models.FloatField(default=4.0)
def __str__(self):
s = 'In-Stroke Analysis {name} ({date})'.format(name = self.name,
date = self.date)
return s
class ForceCurveAnalysis(models.Model):
workout = models.ForeignKey(Workout, on_delete=models.CASCADE)
rower = models.ForeignKey(Rower, on_delete=models.SET_NULL, null=True)
name = models.CharField(max_length=150, blank=True, null=True)
date = models.DateField(blank=True, null=True)
notes = models.TextField(blank=True)
dist_min = models.IntegerField(default=0)
dist_max = models.IntegerField(default=3600)
spm_min = models.FloatField(default=15)
spm_max = models.FloatField(default=55)
work_min = models.IntegerField(default=0)
work_max = models.IntegerField(default=1500)
average_spm = models.FloatField(default=23)
average_boatspeed = models.FloatField(default=4.0)
include_rest_strokes = models.BooleanField(default=False)
plotcircles = models.BooleanField(default=False)
plotlines = models.BooleanField(default=False)
def __str__(self):
s = 'Force Curve Analysis {name} ({date})'.format(name = self.name,
date = self.date)
return s