from __future__ import absolute_import from __future__ import division from __future__ import print_function from __future__ import unicode_literals from __future__ import unicode_literals, absolute_import from django.utils.encoding import python_2_unicode_compatible from django.db import models,IntegrityError from django.contrib.auth.models import User from django.core.validators import validate_email from django.core.exceptions import ValidationError from django import forms from django.forms import ModelForm from django.dispatch import receiver from django.forms.widgets import SplitDateTimeWidget,SelectDateWidget #from django.forms.extras.widgets import SelectDateWidget from django.forms.formsets import BaseFormSet from django.contrib.admin.widgets import AdminDateWidget,AdminTimeWidget,AdminSplitDateTime from datetimewidget.widgets import DateTimeWidget from django.core.validators import validate_email import os import twitter import re import pytz from django_countries.fields import CountryField from scipy.interpolate import splprep, splev, CubicSpline import numpy as np 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 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, ) tweetapi = twitter.Api(consumer_key=TWEET_CONSUMER_KEY, consumer_secret=TWEET_CONSUMER_SECRET, access_token_key=TWEET_ACCESS_TOKEN_KEY, access_token_secret=TWEET_ACCESS_TOKEN_SECRET) from rowers.database import * timezones = ( (x,x) for x in pytz.common_timezones ) def half_year_from_now(): return (datetime.datetime.now(tz=timezone.utc)+timezone.timedelta(days=182)).date() def a_week_from_now(): return (datetime.datetime.now(tz=timezone.utc)+timezone.timedelta(days=7)).date() def current_day(): return (datetime.datetime.now(tz=timezone.utc)).date() def current_time(): return datetime.datetime.now(tz=timezone.utc) class UserFullnameChoiceField(forms.ModelChoiceField): def label_from_instance(self,obj): return obj.get_full_name() # model for configurable template field class TemplateListField(models.TextField): def __init__(self, *args, **kwargs): self.token = kwargs.pop('token',',') super(TemplateListField, self).__init__(*args, **kwargs) def to_python(self, value): if not value: return if isinstance(value, list): return value # remove double quotes and brackets value = re.sub(r'u\"','',value) value = re.sub(r'u\'','',value) value = re.sub(r'\\','',value) value = re.sub(r'\"','',value) value = re.sub(r'\'','',value) value = re.sub(r'\[','',value) value = re.sub(r'\]','',value) value = re.sub(r'\[\[','[',value) value = re.sub(r'\]\]',']',value) value = re.sub(r'\ \ ',' ',value) value = re.sub(r', ',',',value) return value.split(self.token) def from_db_value(self,value, expression, connection, context): if value is None: return value if isinstance(value, list): return value return value.split(self.token) def get_db_prep_value(self, value, connection, prepared=False): if not value: return assert(isinstance(value, list) or isinstance(value, tuple)) return self.token.join([str(s) for s in value]) def value_to_string(self, obj): value = self._get_val_from_obj(obj) return self.get_deb_prep_value(value) # model for Power Zone names class PowerZonesField(models.TextField): # __metaclass__ = models.SubfieldBase def __init__(self, *args, **kwargs): self.token = kwargs.pop('token',',') super(PowerZonesField, self).__init__(*args, **kwargs) def to_python(self, value): if not value: return if isinstance(value, list): return value # remove double quotes and brackets value = re.sub(r'u\"','',value) value = re.sub(r'u\'','',value) value = re.sub(r'\\','',value) value = re.sub(r'\"','',value) value = re.sub(r'\'','',value) value = re.sub(r'\[','',value) value = re.sub(r'\]','',value) value = re.sub(r'\[\[','[',value) value = re.sub(r'\]\]',']',value) value = re.sub(r'\ \ ',' ',value) value = re.sub(r', ',',',value) return value.split(self.token) def from_db_value(self,value, expression, connection, context): if value is None: return value if isinstance(value, list): return value return value.split(self.token) def get_db_prep_value(self, value, connection, prepared=False): if not value: return assert(isinstance(value, list) or isinstance(value, tuple)) return self.token.join([str(s) for s in value]) def value_to_string(self, obj): value = self._get_val_from_obj(obj) return self.get_deb_prep_value(value) c2url = 'http://www.concept2.com/indoor-rowers/racing/records/world?machine=1&event=All&gender=All&age=All&weight=All' def update_records(url=c2url,verbose=True): try: dfs = pd.read_html(url,attrs={'class':'views-table'}) df = dfs[0] df.columns = df.columns.str.strip() success = 1 except: df = pd.DataFrame() if not df.empty: C2WorldClassAgePerformance.objects.all().delete() df.Gender = df.Gender.apply(lambda x: 'male' if x=='M' else 'female') df['Distance'] = df['Event'] df['Duration'] = 0 for nr,row in df.iterrows(): if 'm' in row['Record']: df.ix[nr,'Distance'] = row['Record'][:-1] df.ix[nr,'Duration'] = 60*row['Event'] else: df.ix[nr,'Distance'] = row['Event'] try: tobj = datetime.datetime.strptime(row['Record'],'%M:%S.%f') except ValueError: tobj = datetime.datetime.strptime(row['Record'],'%H:%M:%S.%f') df.ix[nr,'Duration'] = 3600.*tobj.hour+60.*tobj.minute+tobj.second+tobj.microsecond/1.e6 for nr,row in df.iterrows(): try: weightcategory = row.Weight.lower() except AttributeError: weightcategory = 'hwt' sex = row.Gender name = row.Name age = int(row.Age) distance = int(row.Distance) duration = float(row.Duration) season = int(row.Season) velo = distance/duration power = int(2.8*velo**3) record = C2WorldClassAgePerformance( age = age, weightcategory = weightcategory, sex=sex, distance = distance, duration = duration, power = power, season = season, name = name, ) try: if verbose: print(record) record.save() except: if verbose: print(record,'*') else: pass class CalcAgePerformance(models.Model): weightcategories = ( ('hwt','heavy-weight'), ('lwt','light-weight'), ) sexcategories = ( ('male','male'), ('female','female'), ) weightcategory = models.CharField(default="hwt", max_length=30, choices=weightcategories) sex = models.CharField(default="female", max_length=30, choices=sexcategories) age = models.IntegerField(default=19,verbose_name="Age") duration = models.FloatField(default=1,blank=True) power = models.IntegerField(default=200) class Meta: db_table = 'calcagegrouprecords' class PowerTimeFitnessMetric(models.Model): modechoices = ( ('rower','Rower'), ('water','On the water') ) date = models.DateField(default=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=40) class Meta: db_table = 'powertimefitnessmetric' @python_2_unicode_compatible class C2WorldClassAgePerformance(models.Model): weightcategories = ( ('hwt','heavy-weight'), ('lwt','light-weight'), ) sexcategories = ( ('male','male'), ('female','female'), ) weightcategory = models.CharField(default="hwt", max_length=30, choices=weightcategories) sex = models.CharField(default="female", max_length=30, choices=sexcategories) age = models.IntegerField(default=19,verbose_name="Age") distance = models.IntegerField(default=2000) name = models.CharField(max_length=200,blank=True) duration = models.FloatField(default=1,blank=True) season = models.IntegerField(default=2013) power = models.IntegerField(default=200) class Meta: unique_together = ('age','sex','weightcategory','distance') def __str__(self): 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 def is_not_basic(user): if user.rower.rowerplan == 'basic': raise ValidationError( "Basic user cannot be team manager" ) # For future Team functionality @python_2_unicode_compatible 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) # validators=[is_not_basic]) 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 manager.rower.rowerplan == 'basic': if manager.rower.protrialexpires < datetime.date.today() and manager.rower.plantrialexpires < datetime.date.today(): raise ValidationError( "Basic user cannot be team manager" ) if manager.rower.rowerplan in ['plan','pro']: otherteams = Team.objects.filter(manager=manager) if len(otherteams) >= 1: 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) 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) from rowers.utils import ( workflowleftpanel,workflowmiddlepanel, defaultleft,defaultmiddle,landingpages ) from rowers.utils import geo_distance 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): 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 from rowers.courseutils import coordinate_in_path def course_spline(coordinates): latitudes = coordinates['latitude'].values longitudes = coordinates['longitude'].values # spline parameters s = 1.0 k = min([5,len(latitudes)-1]) nest = -1 t = np.linspace(0,1,len(latitudes)) tnew = np.linspace(0,1,100) latnew = CubicSpline(t,latitudes,bc_type='clamped')(tnew) lonnew = CubicSpline(t,longitudes,bc_type='clamped')(tnew) # latnew = CubicSpline(t,latitudes,bc_type='natural')(tnew) # lonnew = CubicSpline(t,longitudes,bc_type='natural')(tnew) # tckp,u = splprep([t,latitudes,longitudes],s=s,k=k,nest=nest) # tnew,latnew,lonnew = splev(np.linspace(0,1,100),tckp) 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) f = lambda x: 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: return 0 def get_delta_start(course): 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): 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: return 0 for i in range(len(polygons)-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] 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]) return int(totaldist-deltastart-deltafinish) sexcategories = ( ('male','male'), ('female','female'), ('not specified','not specified'), ) weightcategories = ( ('hwt','heavy-weight'), ('lwt','light-weight'), ) # Plan plans = ( ('basic','basic'), ('pro','pro'), ('plan','plan'), ('coach','coach') ) paymenttypes = ( ('single','single'), ('recurring','recurring') ) paymentprocessors = ( ('paypal','PayPal'), ('braintree','BrainTree') ) @python_2_unicode_compatible 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, ) @python_2_unicode_compatible class CoachingGroup(models.Model): name = models.CharField(default='group',max_length=30,null=True,blank=True) def __str__(self): return 'Coaching Group {id}: {name}'.format( id = self.pk, name = self.name ) # Extension of User with rowing specific data @python_2_unicode_compatible class Rower(models.Model): adaptivetypes = mytypes.adaptivetypes stravatypes = ( ('Ride','Ride'), ('Kitesurf','Kitesurf'), ('Run','Run'), ('NordicSki','NordicSki'), ('Swim','Swim'), ('RockClimbing','RockClimbing'), ('Hike','Hike'), ('RollerSki','RollerSki'), ('Walk','Walk'), ('Rowing','Rowing'), ('AlpineSki','AlpineSki'), ('Snowboard','Snowboard'), ('BackcountrySki','BackcountrySki'), ('Snowshoe','Snowshoe'), ('Canoeing','Canoeing'), ('StairStepper','StairStepper'), ('Crossfit','Crossfit'), ('StandUpPaddling','StandUpPaddling'), ('EBikeRide','EBikeRide'), ('Surfing','Surfing'), ('Elliptical','Elliptical'), ('VirtualRide','VirtualRide'), ('IceSkate','IceSkate'), ('WeightTraining','WeightTraining'), ('InlineSkate','InlineSkate'), ('Windsurf','Windsurf'), ('Kayaking','Kayaking'), ('Workout','Workout'), ('Yoga','Yoga'), ) user = models.OneToOneField(User,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') 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)) offercoaching = models.BooleanField(default=False, verbose_name='Offer Remote Coaching') # Privacy Data gdproptin = models.BooleanField(default=False) gdproptindate = models.DateTimeField(blank=True,null=True) # 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) 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') birthdate = models.DateField(null=True,blank=True) # Power Zone Data ftp = models.IntegerField(default=226,verbose_name="Functional Threshold Power") p0 = models.FloatField(default=1.0,verbose_name="CP p1") p1 = models.FloatField(default=1.0,verbose_name="CP p2") p2 = models.FloatField(default=1.0,verbose_name="CP p3") p3 = models.FloatField(default=1.0,verbose_name="CP p4") cpratio = models.FloatField(default=1.0,verbose_name="CP fit ratio") ep0 = models.FloatField(default=1.0,verbose_name="erg CP p1") ep1 = models.FloatField(default=1.0,verbose_name="erg CP p2") ep2 = models.FloatField(default=1.0,verbose_name="erg CP p3") ep3 = models.FloatField(default=1.0,verbose_name="erg CP p4") ecpratio = models.FloatField(default=1.0,verbose_name="erg CP fit ratio") otwslack = models.IntegerField(default=0,verbose_name="OTW Power slack") pw_ut2 = models.IntegerField(default=124,verbose_name="UT2 Power") pw_ut1 = models.IntegerField(default=171,verbose_name="UT1 Power") pw_at = models.IntegerField(default=203,verbose_name="AT Power") pw_tr = models.IntegerField(default=237,verbose_name="TR Power") pw_an = models.IntegerField(default=273,verbose_name="AN Power") powerzones = PowerZonesField(default=['Rest', 'Pwr UT2', 'Pwr UT1', 'Pwr AT', 'Pwr TR', 'Pwr AN']) # Site Settings workflowleftpanel = TemplateListField(default=defaultleft) workflowmiddlepanel = TemplateListField(default=defaultmiddle) defaultlandingpage = models.CharField(default='workout_edit_view', max_length=200, choices=landingpages, verbose_name="Default Landing Page") # Access tokens c2token = models.CharField(default='',max_length=200,blank=True,null=True) tokenexpirydate = models.DateTimeField(blank=True,null=True) c2refreshtoken = models.CharField(default='',max_length=200,blank=True,null=True) 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) underarmourtoken = models.CharField(default='',max_length=200,blank=True,null=True) underarmourtokenexpirydate = models.DateTimeField(blank=True,null=True) underarmourrefreshtoken = models.CharField(default='',max_length=200, blank=True,null=True) mapmyfitness_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) 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) 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="Rowing", max_length=30, choices=stravatypes, verbose_name="Export Workouts to Strava as") strava_auto_export = models.BooleanField(default=False) strava_auto_import = models.BooleanField(default=False) runkeepertoken = models.CharField(default='',max_length=200, blank=True,null=True) runkeeper_auto_export = models.BooleanField(default=False) privacychoices = ( ('visible','Visible'), ('hidden','Hidden'), ) getemailnotifications = models.BooleanField(default=False, verbose_name='Receive email notifications') emailbounced = models.BooleanField(default=False, verbose_name='Email Address Bounced') getimportantemails = models.BooleanField(default=True, verbose_name='Get Important Emails') # 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) # Export and Time Zone Settings defaulttimezone = models.CharField(default='UTC',max_length=100, choices=timezones, verbose_name='Default Time Zone') # Show flex chart notes showfavoritechartnotes = models.BooleanField(default=True, verbose_name='Show Notes for Favorite Charts') def __str__(self): return self.user.first_name+' '+self.user.last_name def clean_email(self): return self.user.email.lower() 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 = [] # 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) from django.db.models.signals import m2m_changed 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' and instance.rowerplan=='basic': if instance.protrialexpires < datetime.date.today() and instance.plantrialexpires < datetime.date.today(): for id in pk_set: team = Team.objects.get(id=id) if team.manager.rower.rowerplan not in ['coach']: raise ValidationError( "You cannot join a team led by a Pro 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() from rowers.metrics import axlabels favchartlabelsx = axlabels.copy() favchartlabelsy1 = axlabels.copy() favchartlabelsy2 = axlabels.copy() favchartlabelsy1.pop('None') parchoicesy1 = list(sorted(favchartlabelsy1.items(), key = lambda x:x[1])) parchoicesy2 = list(sorted(favchartlabelsy2.items(), key = lambda x:x[1])) parchoicesx = list(sorted(favchartlabelsx.items(), key = lambda x:x[1])) # Saving a chart as a favorite chart class FavoriteChart(models.Model): workouttypechoices = [ ('ote','Erg/SkiErg'), ('otw','On The Water'), ('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): if any(self.errors): return for form in self.forms: if form.cleaned_data: xparam = form.cleaned_data['xparam'] yparam1 = form.cleaned_data['yparam1'] yparam2 = form.cleaned_data['yparam2'] plottype = form.cleaned_data['plottype'] reststrokes = form.cleaned_data['reststrokes'] if not xparam: raise forms.ValidationError( 'Must have x parameter.', code='missing_xparam' ) if not yparam1: raise forms.ValidationError( 'Must have Y1 parameter.', code='missing_yparam1' ) if not yparam2: yparam2 = 'None' class BasePlannedSessionFormSet(BaseFormSet): def clean(self): if any(self.serrors): return # Check if workout is owned by this user def checkworkoutuser(user,workout): if user.is_anonymous: return False try: r = Rower.objects.get(user=user) if workout.user == r: return True coaches = [] for group in workout.user.coachinggroups.all(): coach = Rower.objects.get(mycoachgroup=group) coaches.append(coach) for coach in coaches: if user.rower == coach and workout.privacy == 'visible': return True else: return False except Rower.DoesNotExist: return False # Check if workout may be viewed by this user def checkworkoutuserview(user,workout): if user.is_anonymous: return False try: r = Rower.objects.get(user=user) if workout.user == r: return True teams = workout.user.team.all() for team in teams: if team in r.team.all(): return True return False except Rower.DoesNotExist: return False return False def checkviewworkouts(user,rower): try: r = user.rower if rower == r: return True teams = Team.objects.filter(manager=user) if rower in Rower.objects.filter(team__in=teams): return True if rower in Rower.objects.filter(coachinggroups__in=[user.rower.mycoachgroup]): return True except Rower.DoesNotExist: return False # check if user is plan and rower is in his group def checkaccessplanuser(user,rower): try: r = Rower.objects.get(user=user) if rower == r: return True team_managers = [t.manager for t in rower.team.all() if t.manager.rower.rowerplan in ['plan','coach']] if user.rower.rowerplan != 'basic': return user in team_managers else: return False return False except Rower.DoesNotExist: return False # Check if user is coach or rower def checkaccessuser(user,rower): try: r = Rower.objects.get(user=user) if rower == r: return True coaches = [] for group in rower.coachinggroups.all(): coach = Rower.objects.get(mycoachgroup=group) coaches.append(coach) for coach in coaches: if user.rower == coach: return True else: return False except Rower.DoesNotExist: return False timezones = ( (x,x) for x in pytz.common_timezones ) # models related to geo data (points, polygon, courses) @python_2_unicode_compatible 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: self.distance = course_length(self) self.save() d = self.distance return u'{country} - {name} - {d}m'.format( name=name, country=country, d = d, ) class GeoCourseEditForm(ModelForm): class Meta: model = GeoCourse fields = ['name','country','notes'] widgets = { 'notes': forms.Textarea, } @python_2_unicode_compatible class GeoPolygon(models.Model): name = models.CharField(max_length=150,blank=True) course = models.ForeignKey(GeoCourse, blank=True,on_delete=models.CASCADE) order_in_course = models.IntegerField(default=0) def __str__(self): 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) 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: 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 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: 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") @python_2_unicode_compatible 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) 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) 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 manager.rowerplan in ['basic','pro']: raise ValidationError( "Basic user cannot have a training plan" ) if self.enddate < self.startdate: startdate = self.startdate enddate = self.enddate self.startdate = enddate self.enddate = startdate if not self.enddate <= self.startdate: super(TrainingPlan,self).save(*args, **kwargs) if self.status: otherplans = TrainingPlan.objects.filter( status=True).exclude( pk=self.pk).order_by( "-startdate") for otherplan in otherplans: if otherplan.startdate <= self.enddate and otherplan.startdate >= self.startdate: for rower in self.rowers.all(): 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) class TrainingPlanForm(ModelForm): class Meta: model = TrainingPlan fields = ['name','target','startdate','enddate','status','rowers'] widgets = { 'startdate': AdminDateWidget(), 'enddate': AdminDateWidget() } 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: 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: 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: 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: 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: 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: 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: 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: macr = TrainingMicroCycle( plan=plan, startdate = plan.startdate, enddate = cycles[0].startdate-datetime.timedelta(days=1), type='filler', name='Filler' ) macr.save() def microcyclecheckdates(plan): 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): 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): 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:] @python_2_unicode_compatible 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): 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: 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: self.enddate = self.plan.enddate if self.startdate < self.plan.startdate: self.startdate = self.plan.startdate othercycles = TrainingMacroCycle.objects.filter( plan=self.plan).exclude(pk=self.pk).order_by("-startdate") for othercycle in othercycles: 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: createmesofillers(self) class TrainingMacroCycleForm(ModelForm): class Meta: model = TrainingMacroCycle fields = ['name','startdate','enddate','notes'] widgets = { 'startdate': AdminDateWidget(), 'enddate': AdminDateWidget() } @python_2_unicode_compatible 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): 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: 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: f.delete() if self.enddate > self.plan.enddate: self.enddate = self.plan.enddate if self.startdate < self.plan.startdate: self.startdate = self.plan.startdate othercycles = TrainingMesoCycle.objects.filter( plan=self.plan).exclude(pk=self.pk).order_by("-startdate") for othercycle in othercycles: 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) microcycles = TrainingMicroCycle.objects.filter(plan = self) if not microcycles: micro = TrainingMicroCycle( plan = self, name = 'Filler', startdate = self.startdate, enddate = self.enddate, ) micro.save() else: createmicrofillers(self) @python_2_unicode_compatible 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): 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: 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: f.delete() if self.enddate > self.plan.enddate: self.enddate = self.plan.enddate if self.startdate < self.plan.startdate: self.startdate = self.plan.startdate othercycles = TrainingMicroCycle.objects.filter( plan=self.plan).exclude(pk=self.pk).order_by("-startdate") for othercycle in othercycles: 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'), ) # model for Planned Session (Workout, Challenge, Test) @python_2_unicode_compatible 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'), ('race','Virtual Race'), ('indoorrace','Indoor Virtual Race'), ) regularsessiontypechoices = ( ('session','Training Session'), ('challenge','Challenge'), ('test','Mandatory Test'), ('cycletarget','Total for a time period'), ('coursetest','OTW test over a course'), ) 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) 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=500,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') sessionvalue = models.IntegerField(default=60,verbose_name='Value') max_nr_of_workouts = models.IntegerField( default=0,verbose_name='Maximum number of workouts' ) sessionunit = models.CharField( default='min',choices=sessionunitchoices, max_length=150, verbose_name='Unit') criterium = models.CharField( default='none', choices=criteriumchoices, max_length=150, 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) 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 save(self, *args, **kwargs): if self.sessionvalue <= 0: self.sessionvalue = 1 manager = self.manager if self.sessiontype not in ['race','indoorrace']: if manager.rower.rowerplan in ['basic','pro']: if manager.rower.plantrialexpires < timezone.now().date(): raise ValidationError( "You must be a Self-Coach user or higher to create a planned session" ) # sort units if self.sessionmode == 'distance': if self.sessionunit not in ['m','km']: self.sessionunit = 'm' elif self.sessionmode == 'time': self.sessionunit = 'min' else: self.sessionunit = 'None' if self.sessiontype == 'test' or self.sessiontype == 'indoorrace': if self.sessionmode not in ['distance','time']: if self.sessionvalue < 100: self.sessionmode = 'time' self.sessionunit = 'min' else: 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 == None: 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: self.enddate = self.startdate if self.preferreddate > self.enddate: self.preferreddate = self.enddate if self.preferreddate < self.startdate: self.preferreddate = self.startdate super(PlannedSession,self).save(*args, **kwargs) from django.core.validators import RegexValidator,validate_email registerchoices = ( ('windowstart','Start of Race Window'), ('windowend','End of Race Window'), ('deadline','Evaluation Closure Deadline'), ('manual','Manual - select below'), ) @python_2_unicode_compatible 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) def __str__(self): name = self.name startdate = self.startdate enddate = self.enddate stri = u'Virtual Race {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: 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: 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): return self.filename def delete(self, *args, **kwargs): os.remove(self.filename) print('file deleted') 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', 'sessiontype', 'sessionmode', 'criterium', 'sessionvalue', 'sessionunit', 'course', 'comment', ] dateTimeOptions = { 'format': 'yyyy-mm-dd', 'autoclose': True, } widgets = { 'comment': forms.Textarea, 'startdate': AdminDateWidget(), 'enddate': AdminDateWidget(), 'preferreddate': AdminDateWidget(), } 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 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: timezone_str = 'UTC' if timezone_str is None: 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', '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 def clean(self): cd = self.cleaned_data timezone_str = cd['timezone'] value = cd['sessionvalue'] if value <= 0: raise forms.ValidationError('The Value must be a positive, non-zero value') unit = cd['sessionunit'] if unit == 'm' and value < 100: raise forms.ValidationError('Minimum distance is 100m') start_time = cd['start_time'] if start_time is None: 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: 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: evaluation_closure = enddatetime+datetime.timedelta(days=1) cd['evaluation_closure'] = evaluation_closure if registration_form == 'manual': try: registration_closure = pytz.timezone( timezone_str ).localize( registration_closure.replace(tzinfo=None) ) except AttributeError: registration_closure = startdatetime elif registration_form == 'windowstart': registration_closure = startdatetime elif registration_form == 'windowend': registration_closure = enddatetime else: registration_closure = evaluation_closure if registration_closure <= timezone.now(): raise forms.ValidationError("Registration Closure cannot be in the past") if startdatetime > enddatetime: raise forms.ValidationError("The Start of the Race Window should be before the End of the Race Window") if cd['evaluation_closure'] <= enddatetime: raise forms.ValidationError("Evaluation closure deadline should be after the Race Window closes") if cd['evaluation_closure'] <= timezone.now(): raise forms.ValidationError("Evaluation closure cannot be in the past") return cd class VirtualRaceForm(ModelForm): course = forms.ModelChoiceField(queryset = GeoCourse.objects, empty_label=None) 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', '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") 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: 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: 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: evaluation_closure = enddatetime+datetime.timedelta(days=1) cd['evaluation_closure'] = evaluation_closure if registration_form == 'manual': try: registration_closure = pytz.timezone( timezone_str ).localize( registration_closure.replace(tzinfo=None) ) except AttributeError: registration_closure = startdatetime elif registration_form == 'windowstart': registration_closure = startdatetime elif registration_form == 'windowend': registration_closure = enddatetime else: registration_closure = evaluation_closure if registration_closure <= timezone.now(): raise forms.ValidationError("Registration Closure cannot be in the past") if startdatetime > enddatetime: raise forms.ValidationError("The Start of the Race Window should be before the End of the Race Window") if cd['evaluation_closure'] <= enddatetime: raise forms.ValidationError("Evaluation closure deadline should be after the Race Window closes") if cd['evaluation_closure'] <= timezone.now(): 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'), ) class Meta: model = PlannedSession fields = ['startdate', 'enddate', 'preferreddate', 'name', 'sessiontype', 'sessionmode', 'criterium', 'sessionvalue', 'sessionunit', 'manager', ] dateTimeOptions = { 'format': 'yyyy-mm-dd', 'autoclose': True, } widgets = { 'startdate': DateInput(attrs={'size':10}), 'enddate': DateInput(attrs={'size':10}), 'preferreddate': DateInput(attrs={'size':10}), '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 class Workout(models.Model): workouttypes = mytypes.workouttypes workoutsources = mytypes.workoutsources privacychoices = mytypes.privacychoices adaptivetypes = mytypes.adaptivetypes 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() 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') adaptiveclass = models.CharField(choices=adaptivetypes,max_length=50, default='None', verbose_name='Adaptive Classification') starttime = models.TimeField(blank=True,null=True) startdatetime = models.DateTimeField(blank=True,null=True) timezone = models.CharField(default='UTC', choices=timezones, max_length=100) distance = models.IntegerField(default=0,blank=True) duration = models.TimeField(default=1,blank=True) dragfactor = models.IntegerField(default=0,blank=True) 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) 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) uploadedtounderarmour = models.BigIntegerField(default=0) uploadedtotp = models.BigIntegerField(default=0) uploadedtorunkeeper = 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') 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': stri = u'{d} {n} {dist}m {duration:%H:%M:%S} {workouttype} {ownerfirst} {ownerlast}'.format( d = date.strftime('%Y-%m-%d'), n = name, dist = distance, duration = duration, workouttype = workouttype, ownerfirst = ownerfirst, ownerlast = ownerlast, ) else: stri = u'{d} {n} {dist}m {duration:%H:%M:%S} {workouttype} {boattype} {ownerfirst} {ownerlast}'.format( d = date.strftime('%Y-%m-%d'), n = name, dist = distance, duration = duration, workouttype = workouttype, boattype=boattype, ownerfirst = ownerfirst, ownerlast = ownerlast, ) return stri 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) uploadedtounderarmour = models.BigIntegerField(default=0) uploadedtotp = models.BigIntegerField(default=0) uploadedtorunkeeper = 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, uploadedtounderarmour = instance.uploadedtounderarmour, uploadedtotp = instance.uploadedtotp, uploadedtorunkeeper = instance.uploadedtorunkeeper, ) t.save() # delete files belonging to workout instance # related GraphImage objects should be deleted automatically @receiver(models.signals.post_delete,sender=Workout) def auto_delete_file_on_delete(sender, instance, **kwargs): # delete CSV file if instance.csvfilename: if os.path.isfile(instance.csvfilename): os.remove(instance.csvfilename) if instance.csvfilename+'.gz': if os.path.isfile(instance.csvfilename+'.gz'): os.remove(instance.csvfilename+'.gz') @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: 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) enddatetime = ww.startdatetime+delta if enddatetime > d.startdatetime: ws2.append(ww) 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() # Virtual Race results (for keeping results when workouts are deleted) @python_2_unicode_compatible 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") race = models.ForeignKey(VirtualRace,on_delete=models.CASCADE) duration = models.TimeField(default=datetime.time(1,0)) distance = models.IntegerField(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 race notifications by email') 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: 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: 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) @python_2_unicode_compatible 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") race = models.ForeignKey(VirtualRace,on_delete=models.CASCADE) duration = models.TimeField(default=datetime.time(1,0)) distance = models.IntegerField(default=0) boatclass = models.CharField(choices=boatclasses, max_length=40, default='rower', verbose_name = 'Ergometer Class') 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 race notifications by email') 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: 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: 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) class IndoorVirtualRaceResultForm(ModelForm): class Meta: model = IndoorVirtualRaceResult fields = ['teamname','weightcategory','boatclass','age','adaptiveclass'] def __init__(self, *args, **kwargs): super(IndoorVirtualRaceResultForm, self).__init__(*args, **kwargs) class VirtualRaceResultForm(ModelForm): class Meta: model = VirtualRaceResult fields = ['teamname','weightcategory','boatclass','boattype', 'age','adaptiveclass'] def __init__(self, *args, **kwargs): boattypes = kwargs.pop('boattypes',None) super(VirtualRaceResultForm, self).__init__(*args, **kwargs) if boattypes: self.fields['boattype'].choices = boattypes self.fields['mix'] = forms.BooleanField(initial=False, required=False, label='Mixed Gender') from rowers.metrics import rowingmetrics strokedatafields = { 'workoutid':models.IntegerField(null=True), 'workoutstate':models.IntegerField(null=True,default=1), 'ftime':models.CharField(max_length=30), 'fpace':models.CharField(max_length=30), 'hr_ut2':models.IntegerField(null=True), 'hr_ut1':models.IntegerField(null=True), 'hr_at':models.IntegerField(null=True), 'hr_tr':models.IntegerField(null=True), 'hr_an':models.IntegerField(null=True), 'hr_max':models.IntegerField(null=True), 'hr_bottom':models.IntegerField(null=True), 'x_right':models.FloatField(null=True), 'ergpace':models.FloatField(null=True), 'nowindpace':models.FloatField(null=True), 'equivergpower':models.FloatField(null=True), 'fergpace':models.CharField(max_length=30), 'fnowindpace':models.CharField(max_length=30), } for name,d in rowingmetrics: if d['numtype'] == 'float': try: strokedatafields[name] = models.FloatField( null=d['null'], default=d['default'], verbose_name=d['verbose_name']) except KeyError: strokedatafields[name] = models.FloatField( null=d['null'], verbose_name=d['verbose_name']) elif d['numtype'] == 'integer': try: strokedatafields[name] = models.IntegerField( null=d['null'], default=d['default'], verbose_name=d['verbose_name']) except KeyError: strokedatafields[name] = models.IntegerField( null=d['null'], verbose_name=d['verbose_name']) class Meta: db_table = 'strokedata' index_together = ['workoutid'] app_label = 'rowers' attrs = {'__module__': '', 'Meta': Meta} attrs.update(strokedatafields) # Model of StrokeData table # the definition here is used only to enable easy Django migration # when the StrokeData are expanded. # No Django Instances of this model are managed. Strokedata table is # accesssed directly with SQL commands StrokeData = type(str('StrokeData'), (models.Model,), attrs ) # Storing data for the OTW CP chart class cpdata(models.Model): delta = models.IntegerField(default=0) cp = models.FloatField(default=0) user = models.IntegerField(default=0) class Meta: db_table = 'cpdata' index_together = ['user'] app_label = 'rowers' # Storing data for the OTW CP chart class cpergdata(models.Model): delta = models.IntegerField(default=0) cp = models.FloatField(default=0) user = models.IntegerField(default=0) class Meta: db_table = 'cpergdata' index_together = ['user'] app_label = 'rowers' # Storing data for the OTW CP chart class ergcpdata(models.Model): delta = models.IntegerField(default=0) cp = models.FloatField(default=0) distance = models.FloatField(default=0) user = models.IntegerField(default=0) class Meta: db_table = 'ergcpdata' index_together = ['user'] app_label = 'rowers' # A wrapper around the png files @python_2_unicode_compatible 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): return self.filename # delete related file object when image is deleted @receiver(models.signals.post_delete,sender=GraphImage) def auto_delete_image_on_delete(sender,instance, **kwargs): if instance.filename: if os.path.isfile(instance.filename): others = GraphImage.objects.filter(filename=instance.filename) if len(others) == 0: os.remove(instance.filename) else: print("couldn't find the file "+instance.filename) # Form to update Workout data class WorkoutForm(ModelForm): # duration = forms.TimeInput(format='%H:%M:%S.%f') class Meta: model = Workout fields = ['name', 'date', 'starttime', 'timezone', 'duration', 'distance', 'workouttype', 'boattype', 'dragfactor', 'weightcategory', 'adaptiveclass', 'notes', 'rankingpiece', 'duplicate', 'plannedsession'] widgets = { 'date': AdminDateWidget(), 'notes': forms.Textarea, 'duration': forms.TimeInput(format='%H:%M:%S.%f'), } def __init__(self, *args, **kwargs): super(WorkoutForm, self).__init__(*args, **kwargs) 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: 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: self.fields['plannedsession'].queryset = sps else: del self.fields['plannedsession'] # Used for the rowing physics calculations class AdvancedWorkoutForm(ModelForm): quick_calc = forms.BooleanField(initial=True,required=False) class Meta: model = Workout fields = ['boattype','weightvalue','quick_calc'] class RowerExportForm(ModelForm): class Meta: model = Rower fields = [ 'stravaexportas', 'polar_auto_import', 'c2_auto_export', 'c2_auto_import', 'mapmyfitness_auto_export', 'runkeeper_auto_export', 'sporttracks_auto_export', 'strava_auto_export', 'strava_auto_import', 'trainingpeaks_auto_export', ] # Simple form to set rower's Functional Threshold Power class RowerPowerForm(ModelForm): class Meta: model = Rower fields = ['hrftp','ftp','otwslack'] # Form to set rower's Power zones, including test routines # to enable consistency class RowerPowerZonesForm(ModelForm): powerzones = ['UT3','UT2','UT1','AT','TR','AN'] ut3name = forms.CharField(initial=powerzones[0]) ut2name = forms.CharField(initial=powerzones[1]) ut1name = forms.CharField(initial=powerzones[2]) atname = forms.CharField(initial=powerzones[3]) trname = forms.CharField(initial=powerzones[4]) anname = forms.CharField(initial=powerzones[5]) def __init__(self, *args,**kwargs): super(RowerPowerZonesForm, self).__init__(*args, **kwargs) if 'instance' in kwargs: powerzones = kwargs['instance'].powerzones else: powerzones = ['UT3','UT2','UT1','AT','TR','AN'] self.fields['ut3name'].initial = powerzones[0] self.fields['ut2name'].initial = powerzones[1] self.fields['ut1name'].initial = powerzones[2] self.fields['atname'].initial = powerzones[3] self.fields['trname'].initial = powerzones[4] self.fields['anname'].initial = powerzones[5] class Meta: model = Rower fields = ['pw_ut2','pw_ut1','pw_at','pw_tr','pw_an'] def clean(self): cleaned_data = super(RowerPowerZonesForm, self).clean() try: pw_ut2 = cleaned_data['pw_ut2'] except KeyError: raise ValidationError("Value cannot be empty") except: pw_ut2 = int(self.data['pw_ut2']) try: pw_ut1 = cleaned_data['pw_ut1'] except KeyError: raise ValidationError("Value cannot be empty") except: pw_ut1 = int(self.data['pw_ut1']) try: pw_at = cleaned_data['pw_at'] except KeyError: raise ValidationError("Value cannot be empty") except: pw_at = int(self.data['pw_at']) try: pw_tr = cleaned_data['pw_tr'] except KeyError: raise ValidationError("Value cannot be empty") except: pw_tr = int(self.data['pw_tr']) try: pw_an = cleaned_data['pw_an'] except KeyError: raise ValidationError("Value cannot be empty") except: pw_an = int(self.data['pw_an']) try: ut3name = cleaned_data['ut3name'] except: ut2name = 'UT3' cleaned_data['ut3name'] = 'UT3' try: ut2name = cleaned_data['ut2name'] except: ut2name = 'UT2' cleaned_data['ut2name'] = 'UT2' try: ut1name = cleaned_data['ut1name'] except: ut1name = 'UT1' cleaned_data['ut1name'] = 'UT1' try: atname = cleaned_data['atname'] except: atname = 'AT' cleaned_data['atname'] = 'AT' try: trname = cleaned_data['trname'] except: trname = 'TR' cleaned_data['ut1name'] = 'TR' try: anname = cleaned_data['anname'] except: anname = 'AN' cleaned_data['ut1name'] = 'AN' try: ut3name = cleaned_data['ut3name'] except: ut2name = 'UT3' cleaned_data['ut3name'] = 'UT3' try: ut2name = cleaned_data['ut2name'] except: ut2name = 'UT2' cleaned_data['ut2name'] = 'UT2' try: ut1name = cleaned_data['ut1name'] except: ut1name = 'UT1' cleaned_data['ut1name'] = 'UT1' try: atname = cleaned_data['atname'] except: atname = 'AT' cleaned_data['atname'] = 'AT' try: trname = cleaned_data['trname'] except: trname = 'TR' cleaned_data['ut1name'] = 'TR' try: anname = cleaned_data['anname'] except: anname = 'AN' cleaned_data['ut1name'] = 'AN' if pw_ut1 <= pw_ut2: e = "{ut1name} should be higher than {ut2name}".format( ut1name = ut1name, ut2name= ut2name, ) raise forms.ValidationError(e) if pw_at <= pw_ut1: e = "{atname} should be higher than {ut1name}".format( atname = atname, ut1name= ut1name, ) raise forms.ValidationError(e) if pw_tr <= pw_at: e = "{trname} should be higher than {atname}".format( atname = atname, trname= trname, ) raise forms.ValidationError(e) if pw_an <= pw_tr: e = "{anname} should be higher than {trname}".format( anname = anname, trname= trname, ) raise forms.ValidationError(e) return cleaned_data # Form to set rower's Auto Import and Export settings class RowerImportExportForm(ModelForm): class Meta: model = Rower fields = [ 'polar_auto_import', 'c2_auto_export', 'c2_auto_import', 'mapmyfitness_auto_export', 'runkeeper_auto_export', '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', 'defaulttimezone','showfavoritechartnotes', 'defaultlandingpage', 'offercoaching'] 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 self.instance.rowerplan != 'coach': self.fields.pop('offercoaching') class UserForm(ModelForm): class Meta: model = User fields = ['first_name','last_name','email'] def clean_first_name(self): first_name = self.cleaned_data.get('first_name') if len(first_name): return first_name raise forms.ValidationError('Please fill in your first name') def clean_email(self): email = self.cleaned_data.get('email') try: validate_email(email) except ValidationError: raise forms.ValidationError( 'Please enter a valid email address') try: match = User.objects.filter(email__iexact=email) if self.instance in match: return email except User.DoesNotExist: return email raise forms.ValidationError('This email address is not allowed') # Form to set rower's Heart Rate zones, including test routines # to enable consistency class RowerForm(ModelForm): class Meta: model = Rower fields = ['rest','ut2','ut1','at','tr','an','max'] def clean_rest(self): rest = self.cleaned_data['rest'] if rest<10: self.data['rest']=10 raise forms.ValidationError("Resting heart rate should be higher than 10 bpm") if rest>250: self.data['rest'] = 250 raise forms.ValidationError("Resting heart rate should be lower than 250 bpm") return rest def clean_ut2(self): ut2 = self.cleaned_data['ut2'] if ut2<10: raise forms.ValidationError("UT2 heart rate should be higher than 10 bpm") if ut2>250: raise forms.ValidationError("UT2 heart rate should be lower than 250 bpm") return ut2 def clean_ut1(self): ut1 = self.cleaned_data['ut1'] if ut1<10: raise forms.ValidationError("UT1 heart rate should be higher than 10 bpm") if ut1>250: raise forms.ValidationError("Resting heart rate should be lower than 250 bpm") return ut1 def clean_at(self): at = self.cleaned_data['at'] if at<10: raise forms.ValidationError("AT heart rate should be higher than 10 bpm") if at>250: raise forms.ValidationError("AT heart rate should be lower than 250 bpm") return at def clean_tr(self): tr = self.cleaned_data['tr'] if tr<10: raise forms.ValidationError("TR heart rate should be higher than 10 bpm") if tr>250: raise forms.ValidationError("TR heart rate should be lower than 250 bpm") return tr def clean_an(self): an = self.cleaned_data['an'] if an<10: raise forms.ValidationError("AN heart rate should be higher than 10 bpm") if an>250: raise forms.ValidationError("AN heart rate should be lower than 250 bpm") return an def clean_max(self): max = self.cleaned_data['max'] if max<10: raise forms.ValidationError("Max heart rate should be higher than 10 bpm") if max>250: raise forms.ValidationError("Max heart rate should be lower than 250 bpm") return max def clean(self): try: rest = self.cleaned_data['rest'] except: try: rest = int(self.data['rest']) except ValueError: rest = 0 try: ut2 = self.cleaned_data['ut2'] except: try: ut2 = self.data['ut2'] except ValueError: ut2 = 0 try: ut1 = self.cleaned_data['ut1'] except: try: ut1 = self.data['ut1'] except ValueError: ut1 = 0 try: at = self.cleaned_data['at'] except: try: at = self.data['at'] except ValueError: at = 0 try: an = self.cleaned_data['an'] except: try: an = self.data['an'] except ValueError: an = 0 try: tr = self.cleaned_data['tr'] except: try: tr = self.data['tr'] except ValueError: tr = 0 try: max = self.cleaned_data['max'] except: try: max = self.data['max'] except ValueError: max = 0 if rest>=ut2: raise forms.ValidationError("Resting heart rate should be lower than UT2") if ut2>=ut1: raise forms.ValidationError("UT2 should be lower than UT1") if ut2>=ut1: raise forms.ValidationError("UT2 should be lower than UT1") if ut1>=at: raise forms.ValidationError("UT1 should be lower than AT") if at>=tr: raise forms.ValidationError("AT should be lower than TR") if tr>=an: raise forms.ValidationError("TR should be lower than AN") if an>=max: raise forms.ValidationError("AN should be lower than Max") # An announcement that goes to the right of the workouts list # optionally sends a tweet to our twitter account class SiteAnnouncement(models.Model): created = models.DateField(default=current_day) announcement = models.TextField(max_length=280) expires = models.DateField(default=current_day) modified = models.DateField(default=current_day) dotweet = models.BooleanField(default=False) def save(self, *args, **kwargs): if not self.id: self.created = timezone.now() self.expires = timezone.now()+datetime.timedelta(days=10) self.modified = timezone.now() if self.dotweet: try: status = tweetapi.PostUpdate(self.announcement) except: try: status = tweetapi.PostUpdate(self.announcement[:270]) except: pass return super(SiteAnnouncement,self).save(*args, **kwargs) # A comment by a user on a training @python_2_unicode_compatible 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): 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 @python_2_unicode_compatible 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): 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, }