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