diff --git a/rowers/admin.py b/rowers/admin.py index 14599dc5..b6e9bbbe 100644 --- a/rowers/admin.py +++ b/rowers/admin.py @@ -6,7 +6,7 @@ from .models import ( Rower, Workout,GraphImage,FavoriteChart,SiteAnnouncement, Team,TeamInvite,TeamRequest, WorkoutComment,C2WorldClassAgePerformance,PlannedSession, - GeoCourse,GeoPolygon,GeoPoint, + GeoCourse,GeoPolygon,GeoPoint,VirtualRace, ) # Register your models here so you can use them in the Admin module @@ -60,6 +60,8 @@ class GeoCourseAdmin(admin.ModelAdmin): inlines = (GeoPolygonInline,) +class VirtualRaceAdmin(admin.ModelAdmin): + list_display = ('manager','name','startdate','country') admin.site.unregister(User) admin.site.register(User,UserAdmin) @@ -75,3 +77,4 @@ admin.site.register(C2WorldClassAgePerformance, C2WorldClassAgePerformanceAdmin) admin.site.register(PlannedSession,PlannedSessionAdmin) admin.site.register(GeoCourse, GeoCourseAdmin) +admin.site.register(VirtualRace, VirtualRaceAdmin) diff --git a/rowers/courses.py b/rowers/courses.py index 8a2b07df..58085405 100644 --- a/rowers/courses.py +++ b/rowers/courses.py @@ -16,6 +16,7 @@ import xml.etree.ElementTree as et import pandas as pd import numpy as np +from timezonefinder import TimezoneFinder import dataprep from rowers.utils import geo_distance @@ -38,6 +39,24 @@ class InvalidTrajectoryError(Exception): def __str__(self): return repr(self.value) +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 def polygon_to_path(polygon): points = GeoPoint.objects.filter(polygon=polygon).order_by("order_in_poly") diff --git a/rowers/dataprep.py b/rowers/dataprep.py index d34076fe..b8b054a4 100644 --- a/rowers/dataprep.py +++ b/rowers/dataprep.py @@ -809,34 +809,7 @@ def create_row_df(r,distance,duration,startdatetime, return (id, message) - -def totaltime_sec_to_string(totaltime): - hours = int(totaltime / 3600.) - if hours > 23: - message = 'Warning: The workout duration was longer than 23 hours. ' - hours = 23 - - minutes = int((totaltime - 3600. * hours) / 60.) - if minutes > 59: - minutes = 59 - if not message: - message = 'Warning: there is something wrong with the workout duration' - - seconds = int(totaltime - 3600. * hours - 60. * minutes) - if seconds > 59: - seconds = 59 - if not message: - message = 'Warning: there is something wrong with the workout duration' - - tenths = int(10 * (totaltime - 3600. * hours - 60. * minutes - seconds)) - if tenths > 9: - tenths = 9 - if not message: - message = 'Warning: there is something wrong with the workout duration' - - duration = "%s:%s:%s.%s" % (hours, minutes, seconds, tenths) - - return duration +from utils import totaltime_sec_to_string # Processes painsled CSV file to database def save_workout_database(f2, r, dosmooth=True, workouttype='rower', diff --git a/rowers/forms.py b/rowers/forms.py index 2c74247a..19665e58 100644 --- a/rowers/forms.py +++ b/rowers/forms.py @@ -695,6 +695,19 @@ class WorkoutSessionSelectForm(forms.Form): widget = forms.CheckboxSelectMultiple, ) +class WorkoutRaceSelectForm(forms.Form): + + def __init__(self, workoutdata, *args, **kwargs): + + super(WorkoutRaceSelectForm, self).__init__(*args, **kwargs) + + self.fields['workouts'] = forms.ChoiceField( + label='Workouts', + choices = workoutdata['choices'], + initial=workoutdata['initial'], + widget=forms.RadioSelect, + ) + class PlannedSessionTeamForm(forms.Form): team = forms.ModelMultipleChoiceField( queryset=Team.objects.all(), @@ -716,5 +729,39 @@ class PlannedSessionTeamMemberForm(forms.Form): super(PlannedSessionTeamMemberForm,self).__init__(*args,**kwargs) self.fields['members'].queryset = thesession.rower.all() - - + +from rowers.models import VirtualRace,GeoCourse + +def get_countries(): + countries = VirtualRace.objects.order_by('country').values_list('country').distinct() + countries = tuple([(c[0],c[0]) for c in countries]) + countries = countries+(('All','All'),) + return countries + + + +class VirtualRaceSelectForm(forms.Form): + regattatypechoices = ( + ('upcoming','Upcoming Races'), + ('ongoing','Ongoing Races'), + ('previous','Previous Races'), + ('my','My Races'), + ('all','All Races'), + ) + + regattatype = forms.ChoiceField( + label='Type', + choices = regattatypechoices, + initial = 'upcoming', + ) + + country = forms.ChoiceField( + label='Country', + choices = get_countries() + ) + + def __init__(self, *args, **kwargs): + super(VirtualRaceSelectForm, self).__init__(*args, **kwargs) + self.fields['country'] = forms.ChoiceField( + choices = get_countries(),initial='All' + ) diff --git a/rowers/models.py b/rowers/models.py index c35331e1..25749a58 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -10,7 +10,7 @@ from django.dispatch import receiver from django.forms.widgets import SplitDateTimeWidget from django.forms.extras.widgets import SelectDateWidget from django.forms.formsets import BaseFormSet -from django.contrib.admin.widgets import AdminDateWidget +from django.contrib.admin.widgets import AdminDateWidget,AdminTimeWidget,AdminSplitDateTime from datetimewidget.widgets import DateTimeWidget from django.core.validators import validate_email import os @@ -422,18 +422,19 @@ def course_length(course): return int(totaldist) +sexcategories = ( + ('male','male'), + ('female','female'), + ('not specified','not specified'), +) +weightcategories = ( + ('hwt','heavy-weight'), + ('lwt','light-weight'), +) + + # Extension of User with rowing specific data class Rower(models.Model): - weightcategories = ( - ('hwt','heavy-weight'), - ('lwt','light-weight'), - ) - - sexcategories = ( - ('male','male'), - ('female','female'), - ('not specified','not specified'), - ) stravatypes = ( ('Ride','Ride'), @@ -1056,6 +1057,44 @@ class PlannedSession(models.Model): super(PlannedSession,self).save(*args, **kwargs) +from django.core.validators import RegexValidator,validate_email + +class VirtualRace(PlannedSession): + has_registration = models.BooleanField(default=False) + 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 __unicode__(self): + + name = self.name + startdate = self.startdate + enddate = self.enddate + + stri = u'Virtual Race {n}'.format( + n = name, + ) + + return stri + + + # Date input utility class DateInput(forms.DateInput): input_type = 'date' @@ -1093,6 +1132,49 @@ class PlannedSessionForm(ModelForm): super(PlannedSessionForm, self).__init__(*args, **kwargs) self.fields['course'].queryset = GeoCourse.objects.all().order_by("country","name") +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=False) + + class Meta: + model = VirtualRace + fields = [ + 'name', + 'startdate', + 'start_time', + 'enddate', + 'end_time', + 'has_registration', + '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") + + + class PlannedSessionFormSmall(ModelForm): class Meta: @@ -1124,12 +1206,12 @@ class PlannedSessionFormSmall(ModelForm): 'manager': forms.HiddenInput(), } +boattypes = types.boattypes # Workout class Workout(models.Model): workouttypes = types.workouttypes workoutsources = types.workoutsources - boattypes = types.boattypes privacychoices = types.privacychoices user = models.ForeignKey(Rower) @@ -1242,6 +1324,36 @@ def auto_delete_strokedata_on_delete(sender, instance, **kwargs): conn.close() engine.dispose() +# Virtual Race results (for keeping results when workouts are deleted) +class VirtualRaceResult(models.Model): + user = models.ForeignKey(Rower) + teamname = models.CharField(max_length=20,verbose_name = 'Team Name', + blank=True,null=True) + username = models.CharField(max_length=150) + workout = models.ForeignKey(Workout,blank=True,null=True) + weightcategory = models.CharField(default="hwt",max_length=10, + choices=weightcategories, + verbose_name='Weight Category') + race = models.ForeignKey(VirtualRace) + duration = models.TimeField(default=datetime.time(1,0)) + 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) + +class VirtualRaceResultForm(ModelForm): + class Meta: + model = VirtualRaceResult + fields = ['teamname','weightcategory','boattype','age'] + + from rowers.metrics import rowingmetrics strokedatafields = { diff --git a/rowers/plannedsessions.py b/rowers/plannedsessions.py index da26d0a5..c5b69c77 100644 --- a/rowers/plannedsessions.py +++ b/rowers/plannedsessions.py @@ -7,8 +7,8 @@ import time from django.db import IntegrityError import uuid from django.conf import settings - -from utils import myqueue +import pytz +from utils import myqueue,calculate_age,totaltime_sec_to_string import django_rq queue = django_rq.get_queue('default') @@ -18,7 +18,7 @@ queuehigh = django_rq.get_queue('low') from rowers.models import ( Rower, Workout,Team, GeoCourse, TrainingMicroCycle,TrainingMesoCycle,TrainingMacroCycle, - TrainingPlan,PlannedSession, + TrainingPlan,PlannedSession,VirtualRaceResult ) import metrics @@ -86,6 +86,30 @@ def timefield_to_seconds_duration(t): return duration + +def get_virtualrace_times(virtualrace): + geocourse = GeoCourse.objects.get(id = virtualrace.course.id) + timezone_str = courses.get_course_timezone(geocourse) + + startdatetime = datetime.datetime.combine( + virtualrace.startdate,virtualrace.start_time) + enddatetime = datetime.datetime.combine( + virtualrace.enddate,virtualrace.end_time) + + startdatetime = pytz.timezone(timezone_str).localize( + startdatetime + ) + enddatetime = pytz.timezone(timezone_str).localize( + enddatetime + ) + + return { + 'startdatetime':startdatetime, + 'enddatetime':enddatetime, + 'evaluation_closure':virtualrace.evaluation_closure, + 'registration_closure':virtualrace.registration_closure, + } + def get_session_metrics(ps): rowers = ps.rower.all() rscore = [] @@ -401,6 +425,13 @@ def get_sessions(r,startdate=date.today(), return sps +def get_my_session_ids(r): + sps = PlannedSession.objects.filter( + rower__in=[r] + ).order_by("preferreddate","startdate","enddate") + + return [ps.id for ps in sps] + def get_workouts_session(r,ps): ws = Workout.objects.filter(user=r,plannedsession=ps) @@ -416,3 +447,269 @@ def update_plannedsession(ps,cd): ps.save() return 1,'Planned Session Updated' + +def update_virtualrace(ps,cd): + for attr, value in cd.items(): + if attr == 'comment': + value.replace("\r\n", " "); + value.replace("\n", " "); + setattr(ps, attr, value) + + # correct times + + course = cd['course'] + geocourse = GeoCourse.objects.get(id= course.id) + timezone_str = courses.get_course_timezone(geocourse) + + startdatetime = datetime.combine(cd['startdate'],cd['start_time']) + enddatetime = datetime.combine(cd['enddate'],cd['end_time']) + + startdatetime = pytz.timezone(timezone_str).localize( + startdatetime + ) + enddatetime = pytz.timezone(timezone_str).localize( + enddatetime + ) + ps.evaluation_closure = pytz.timezone(timezone_str).localize( + ps.evaluation_closure.replace(tzinfo=None) + ) + ps.registration_closure = pytz.timezone(timezone_str).localize( + ps.registration_closure.replace(tzinfo=None) + ) + + ps.timezone = timezone_str + + ps.save() + + return 1,'Virtual Race Updated' + +def race_rower_status(r,race): + + ws = Workout.objects.filter(user=r,plannedsession=race) + + is_complete = is_session_complete_ws(ws,race)[1] + + has_registered = r in race.rower.all() + + return is_complete,has_registered + +def race_can_edit(r,race): + if r.user != race.manager: + return False + else: + start_time = race.start_time + start_date = race.startdate + startdatetime = datetime.combine(start_date,start_time) + startdatetime = pytz.timezone(race.timezone).localize( + startdatetime + ) + if timezone.now() startdatetime and timezone.now() < evaluation_closure: + is_complete,has_registered = race_rower_status(r,race) + if is_complete == 'not done': + return True + else: + return False + else: + return False + + return False + +def race_can_resubmit(r,race): + if r not in race.rower.all(): + return False + + start_time = race.start_time + start_date = race.startdate + startdatetime = datetime.combine(start_date,start_time) + startdatetime = pytz.timezone(race.timezone).localize( + startdatetime + ) + evaluation_closure = race.evaluation_closure + + + if timezone.now() > startdatetime and timezone.now() < evaluation_closure: + is_complete,has_registered = race_rower_status(r,race) + if is_complete in ['partial','completed']: + return True + else: + return False + else: + return False + + return False + +def race_can_withdraw(r,race): + if r not in race.rower.all(): + return False + + start_time = race.start_time + start_date = race.startdate + startdatetime = datetime.combine(start_date,start_time) + startdatetime = pytz.timezone(race.timezone).localize( + startdatetime + ) + + registration_closure = race.registration_closure + if registration_closure is not None and registration_closure != '': + if timezone.now() > registration_closure: + return False + elif timezone.now() > startdatetime: + return False + elif timezone.now() > startdatetime: + return False + + return True + +def race_can_register(r,race): + if r in race.rower.all(): + return False + + start_time = race.start_time + start_date = race.startdate + startdatetime = datetime.combine(start_date,start_time) + startdatetime = pytz.timezone(race.timezone).localize( + startdatetime + ) + + registration_closure = race.registration_closure + if registration_closure is not None and registration_closure != '': + if timezone.now() > registration_closure: + return False + elif timezone.now() > startdatetime: + return False + elif timezone.now() > startdatetime: + return False + + return True + +def add_rower_race(r,race): + race.rower.add(r) + race.save() + + return 1 + +def remove_rower_race(r,race): + race.rower.remove(r) + + records = VirtualRaceResult.objects.filter(user=r, + workout__isnull=True, + race=race) + + for r in records: + r.delete() + + return 1 + +# Low Level functions - to be called by higher level methods +def add_workout_race(ws,race,r): + result = 0 + comments = [] + errors = [] + + start_time = race.start_time + start_date = race.startdate + startdatetime = datetime.combine(start_date,start_time) + startdatetime = pytz.timezone(race.timezone).localize( + startdatetime + ) + + end_time = race.end_time + end_date = race.enddate + enddatetime = datetime.combine(end_date,end_time) + enddatetime = pytz.timezone(race.timezone).localize( + enddatetime + ) + + # check if all sessions have same date + dates = [w.date for w in ws] + if (not all(d == dates[0] for d in dates)) and race.sessiontype not in ['challenge','cycletarget']: + errors.append('For tests and training sessions, selected workouts must all be done on the same date') + return result,comments,errors + + if len(ws)>1 and race.sessiontype == 'test': + errors.append('For tests, you can only attach one workout') + return result,comments,errors + + + + ids = [w.id for w in ws] + ids = list(set(ids)) + + if len(ids)>1 and race.sessiontype in ['test','coursetest']: + errors.append('For tests, you can only attach one workout') + return result,comments,errors + + # start adding sessions + for w in ws: + if w.startdatetime>=startdatetime and w.startdatetime<=enddatetime: + w.plannedsession = race + w.save() + result += 1 + + comments.append('Your result has been submitted') + else: + errors.append('Workout %i did not match the race window' % w.id) + return result,comments,errors + + if result>0: + username = r.user.first_name+' '+r.user.last_name + if r.birthdate: + age = calculate_age(r.birthdate) + else: + age = None + ( + coursetime, + coursemeters, + coursecompleted + ) = courses.get_time_course(ws,race.course) + if not coursecompleted: + errors.append('Your trajectory did not match the race course') + return result,comments,errors + + duration = totaltime_sec_to_string(coursetime) + + record = VirtualRaceResult( + user=r, + username=username, + workout = ws[0], + race = race, + coursecompleted=coursecompleted, + duration = duration, + boattype = ws[0].boattype, + sex = r.sex, + age = age, + ) + + record.save() + + + + return result,comments,errors + +def delete_race_result(workout,race): + results = VirtualRaceResult.objects.filter(workout=workout,race=race) + for r in results: + r.delete() + + diff --git a/rowers/templates/list_workouts.html b/rowers/templates/list_workouts.html index d4904466..11e7cef6 100644 --- a/rowers/templates/list_workouts.html +++ b/rowers/templates/list_workouts.html @@ -63,11 +63,11 @@ {% csrf_token %} -
+
{% if user.is_authenticated and user|is_manager %} -