5164 lines
174 KiB
Python
5164 lines
174 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
|
|
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
|
|
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=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=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=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=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 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 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")
|
|
|
|
# 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)
|
|
distance = models.IntegerField(default=0)
|
|
name = models.CharField(max_length=150, blank=True)
|
|
country = models.CharField(max_length=150, blank=True)
|
|
notes = models.CharField(blank=True, max_length=200,
|
|
verbose_name='Course Notes')
|
|
|
|
def __str__(self):
|
|
name = self.name
|
|
# country = self.country
|
|
d = self.distance
|
|
if d == 0: # 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,
|
|
)
|
|
|
|
@property
|
|
def coord(self):
|
|
return course_coord_center(self)
|
|
|
|
|
|
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}
|
|
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
|
|
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
|
|
|
|
# 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, max_length=50,
|
|
default='1x',
|
|
verbose_name='Boat Type')
|
|
boatbrand = models.CharField(choices=boatbrands, max_length=50,
|
|
default='', verbose_name='Boat Brand')
|
|
adaptiveclass = models.CharField(choices=adaptivetypes, max_length=50,
|
|
default='None',
|
|
verbose_name='Adaptive Classification')
|
|
starttime = models.TimeField(default=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)
|
|
|
|
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
|
|
|
|
super(Workout, self).save(*args, **kwargs)
|
|
|
|
def __str__(self):
|
|
|
|
date = self.date
|
|
name = self.name
|
|
distance = str(self.distance)
|
|
ownerfirst = self.user.user.first_name
|
|
ownerlast = self.user.user.last_name
|
|
duration = self.duration
|
|
boattype = self.boattype
|
|
workouttype = self.workouttype
|
|
|
|
if workouttype != 'water':
|
|
try:
|
|
stri = u'{d} {n} {dist}m {duration} {workouttype} {ownerfirst} {ownerlast}'.format(
|
|
d=date.strftime('%Y-%m-%d'),
|
|
n=name,
|
|
dist=distance,
|
|
duration=duration.strftime("%H:%M:%S"),
|
|
workouttype=workouttype,
|
|
ownerfirst=ownerfirst,
|
|
ownerlast=ownerlast,
|
|
)
|
|
except ValueError:
|
|
stri = self.name
|
|
else:
|
|
try:
|
|
stri = u'{d} {n} {dist}m {duration} {workouttype} {boattype} {ownerfirst} {ownerlast}'.format(
|
|
d=date.strftime('%Y-%m-%d'),
|
|
n=name,
|
|
dist=distance,
|
|
duration=duration.strftime("%H:%M:%S"),
|
|
workouttype=workouttype,
|
|
boattype=boattype,
|
|
ownerfirst=ownerfirst,
|
|
ownerlast=ownerlast,
|
|
)
|
|
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)
|
|
if records.count():
|
|
record = records[0]
|
|
else:
|
|
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)
|
|
shutil.rmtree(dirname)
|
|
except FileNotFoundError:
|
|
pass
|
|
|
|
# remove parquet file
|
|
try:
|
|
dirname = 'media/cpdata_{id}.parquet.gz'.format(id=instance.id)
|
|
os.remove(dirname)
|
|
except FileNotFoundError:
|
|
pass
|
|
|
|
|
|
@receiver(models.signals.post_delete, sender=Workout)
|
|
def update_duplicates_on_delete(sender, instance, **kwargs):
|
|
if instance.id:
|
|
|
|
duplicates = Workout.objects.filter(
|
|
user=instance.user, date=instance.date,
|
|
duplicate=True)
|
|
|
|
for d in duplicates: # 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)
|
|
workoutid = models.IntegerField(null=True)
|
|
plannedsession = models.ForeignKey(
|
|
PlannedSession, on_delete=models.CASCADE)
|
|
duration = models.TimeField(default=datetime.time(1, 0))
|
|
distance = models.IntegerField(default=0)
|
|
coursecompleted = models.BooleanField(default=False)
|
|
startsecond = models.FloatField(default=0)
|
|
endsecond = models.FloatField(default=0)
|
|
|
|
|
|
class IndoorVirtualRaceResultForm(ModelForm):
|
|
class Meta:
|
|
model = IndoorVirtualRaceResult
|
|
fields = ['teamname', 'weightcategory', 'boatclass', 'age', 'adaptiveclass',
|
|
'entrycategory', 'acceptsocialmedia'
|
|
]
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
categories = kwargs.pop('categories', None)
|
|
super(IndoorVirtualRaceResultForm, self).__init__(*args, **kwargs)
|
|
if categories is not None: # 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',
|
|
'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)
|
|
|
|
def __str__(self):
|
|
s = 'Force Curve Analysis {name} ({date})'.format(name = self.name,
|
|
date = self.date)
|
|
|
|
return s
|