From e0303b90b64210dddcd63276accf3a55cfb5c512 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Tue, 27 Nov 2018 12:29:30 +0100 Subject: [PATCH 01/13] automatic expiry of non-recurring users --- rowers/emails.py | 8 +++++++- rowers/middleware.py | 24 ++++++++++++++++++++++- rowers/models.py | 7 +++++++ rowers/tasks.py | 22 +++++++++++++++++++++ rowers/templates/accountexpiredemail.html | 23 ++++++++++++++++++++++ rowsandall_app/settings.py | 1 + 6 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 rowers/templates/accountexpiredemail.html diff --git a/rowers/emails.py b/rowers/emails.py index dff06210..7dfa16d1 100644 --- a/rowers/emails.py +++ b/rowers/emails.py @@ -59,8 +59,12 @@ def send_template_email(from_email,to_email,subject, html_content = htmly.render(context) text_content = textify(html_content) + + if 'cc' in kwargs: + msg = EmailMultiAlternatives(subject, text_content, from_email, to_email,cc=kwargs['cc']) + else: + msg = EmailMultiAlternatives(subject, text_content, from_email, to_email) - msg = EmailMultiAlternatives(subject, text_content, from_email, to_email) msg.attach_alternative(html_content, "text/html") if 'attach_file' in kwargs: @@ -82,6 +86,8 @@ def send_template_email(from_email,to_email,subject, else: emailbounced = False + + if not emailbounced: res = msg.send() else: diff --git a/rowers/middleware.py b/rowers/middleware.py index 736af9c0..72c1ed7e 100644 --- a/rowers/middleware.py +++ b/rowers/middleware.py @@ -5,8 +5,9 @@ import datetime from utils import myqueue import django_rq queue = django_rq.get_queue('default') -from rowers.tasks import handle_updatefitnessmetric +from rowers.tasks import handle_updatefitnessmetric,handle_sendemail_expired from rowers.mytypes import otwtypes +from django.contrib import messages def getrower(user): try: @@ -101,3 +102,24 @@ class GDPRMiddleWare(object): return redirect( '/rowers/me/gdpr-optin/?next=%s' % nexturl ) + +class RowerPlanMiddleWare(object): + def process_request(self, request): + if request.user.is_authenticated() and request.user.rower.rowerplan != 'basic': + if request.user.rower.paymenttype == 'single': + if request.user.rower.planexpires < timezone.now().date(): + messg = 'Your paid plan has expired. We have reset you to a free basic plan.' + messages.error(request,messg) + r = getrower(request.user) + r.rowerplan = 'basic' + r.save() + # send email + job = myqueue(queue, + handle_sendemail_expired, + r.user.email, + r.user.first_name, + r.user.last_name, + str(r.planexpires)) + elif request.user.rower.planexpires-datetime.timedelta(days=5)Dear {{ first_name }},

+ +

+ Your Pro account on rowsandall.com expired on {{ expireddate }}. It + has now been automatically reset to Basic. + Let me know if you have any questions. If you want to continue using Pro membership, + just sign up again through the site and I will change your membership back to Pro. +

+ +

+ If you do not want to continue the Pro membership, I'd be interested to know why you + decided to not continue your Pro account. Did it not fulfill your expectations? + This information is valuable to improve the site for all users. Thank you! +

+ +

+ Best Regards, the Rowsandall Team +

+{% endblock %} + diff --git a/rowsandall_app/settings.py b/rowsandall_app/settings.py index 5de3f066..ab69eeca 100644 --- a/rowsandall_app/settings.py +++ b/rowsandall_app/settings.py @@ -96,6 +96,7 @@ MIDDLEWARE_CLASSES = [ 'tz_detect.middleware.TimezoneMiddleware', 'rowers.middleware.GDPRMiddleWare', 'rowers.middleware.PowerTimeFitnessMetricMiddleWare', + 'rowers.middleware.RowerPlanMiddleWare', ] ROOT_URLCONF = 'rowsandall_app.urls' From 5a10d381b534e320d87f8302886c0d13cd0c8d79 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Tue, 27 Nov 2018 12:44:09 +0100 Subject: [PATCH 02/13] warning message --- rowers/middleware.py | 4 +--- templates/newbase.html | 10 ++++++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/rowers/middleware.py b/rowers/middleware.py index 72c1ed7e..95cb5583 100644 --- a/rowers/middleware.py +++ b/rowers/middleware.py @@ -120,6 +120,4 @@ class RowerPlanMiddleWare(object): r.user.first_name, r.user.last_name, str(r.planexpires)) - elif request.user.rower.planexpires-datetime.timedelta(days=5) {% endif %} + {% if user.rower.planexpires and user.rower.rowerplan != 'basic' %} + {% if user.rower.planexpires|is_future_date %} + {% if user.rower.planexpires|date_dif|ddays < 4 %} +
  • +

    + You have {{ user.rower.planexpires|date_dif|ddays }} days left of your one year subscription. Please renew on or before {{ user.rower.planexpires }} or your plan will be reset to Basic. Click here to renew your membership.

    +
  • + {% endif %} + {% endif %} + {% endif %} {% if user.rower.protrialexpires and user.rower.protrialexpires|is_future_date %} {% if user.rower.plantrialexpires and user.rower.rowerplan != 'plan' %}
  • From ae9f097ee1c04342d8c145aa4eb8926ba727f39e Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Tue, 27 Nov 2018 13:01:58 +0100 Subject: [PATCH 03/13] added payment type check --- templates/newbase.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/newbase.html b/templates/newbase.html index 16d00c75..6f6a1c7c 100644 --- a/templates/newbase.html +++ b/templates/newbase.html @@ -225,7 +225,7 @@

  • {% endif %} - {% if user.rower.planexpires and user.rower.rowerplan != 'basic' %} + {% if user.rower.planexpires and user.rower.rowerplan != 'basic' and user.rower.paymenttype == 'single'%} {% if user.rower.planexpires|is_future_date %} {% if user.rower.planexpires|date_dif|ddays < 4 %}
  • From 2f17ca12ecfecb41b663b2d2f68fbbd16e4b19cf Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Tue, 27 Nov 2018 14:03:04 +0100 Subject: [PATCH 04/13] adding previous and next to new workout pages --- rowers/polarstuff.py | 2 +- rowers/templates/flexchart3otw.html | 11 +++++++++++ rowers/templates/summary_edit.html | 11 +++++++++++ rowers/templates/workflow.html | 11 +++++++++++ rowers/templates/workout_comments.html | 11 +++++++++++ rowers/templates/workoutstats.html | 11 +++++++++++ 6 files changed, 56 insertions(+), 1 deletion(-) diff --git a/rowers/polarstuff.py b/rowers/polarstuff.py index e6fe6025..83c1b6d1 100644 --- a/rowers/polarstuff.py +++ b/rowers/polarstuff.py @@ -299,7 +299,7 @@ def get_polar_workout(user,id,transactionid): if response.status_code == 200: exerciseurls = response.json()['exercises'] for exerciseurl in exerciseurls: - response = requests.gedt(exerciseurl,headers=headers) + response = requests.get(exerciseurl,headers=headers) if response.status_code == 200: exercise_dict = response.json() thisid = exercise_dict['id'] diff --git a/rowers/templates/flexchart3otw.html b/rowers/templates/flexchart3otw.html index ff4cfa16..e675ca2d 100644 --- a/rowers/templates/flexchart3otw.html +++ b/rowers/templates/flexchart3otw.html @@ -19,6 +19,17 @@ {{ the_script |safe }} +

    + {% if workout|previousworkout:rower.user %} + Previous  + {% endif %} + {% if workout|nextworkout:rower.user %} + Next + {% endif %} +

    +

    Flexible Chart

    diff --git a/rowers/templates/summary_edit.html b/rowers/templates/summary_edit.html index 62957cca..f0bc4592 100644 --- a/rowers/templates/summary_edit.html +++ b/rowers/templates/summary_edit.html @@ -6,6 +6,17 @@ {% block title %}Change Workout {% endblock %} {% block main %} +

    + {% if workout|previousworkout:rower.user %} + Previous  + {% endif %} + {% if workout|nextworkout:rower.user %} + Next + {% endif %} +

    +

    Edit Workout Interval Data

    • diff --git a/rowers/templates/workflow.html b/rowers/templates/workflow.html index 1a71dab0..e6e6b742 100644 --- a/rowers/templates/workflow.html +++ b/rowers/templates/workflow.html @@ -60,6 +60,17 @@ {% block main %} +

      + {% if workout|previousworkout:rower.user %} + Previous  + {% endif %} + {% if workout|nextworkout:rower.user %} + Next + {% endif %} +

      +

      {{ workout.name }}

      {% if workout.user.user != user %}

      {{ workout.user.user.first_name }} {{ workout.user.user.last_name }}

      diff --git a/rowers/templates/workout_comments.html b/rowers/templates/workout_comments.html index 0a988423..c858c8b7 100644 --- a/rowers/templates/workout_comments.html +++ b/rowers/templates/workout_comments.html @@ -10,6 +10,17 @@ {% block title %}Comment Workout {% endblock %} {% block main %} +

      + {% if workout|previousworkout:rower.user %} + Previous  + {% endif %} + {% if workout|nextworkout:rower.user %} + Next + {% endif %} +

      +

      Comments {{ workout.name }}

        diff --git a/rowers/templates/workoutstats.html b/rowers/templates/workoutstats.html index e832317f..d4976046 100644 --- a/rowers/templates/workoutstats.html +++ b/rowers/templates/workoutstats.html @@ -5,6 +5,17 @@ {% block title %}Workout Statistics{% endblock %} {% block main %} +

        + {% if workout|previousworkout:rower.user %} + Previous  + {% endif %} + {% if workout|nextworkout:rower.user %} + Next + {% endif %} +

        +

        Workout Statistics for {{ workout.name }}

        • From 35f298f63de64f240681ed88385b4b12155b7006 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Tue, 27 Nov 2018 14:10:34 +0100 Subject: [PATCH 05/13] landing page preferences link to prefs --- rowers/admin.py | 2 +- rowers/models.py | 1 + templates/newbasefront.html | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/rowers/admin.py b/rowers/admin.py index f28aecae..8b2c0d74 100644 --- a/rowers/admin.py +++ b/rowers/admin.py @@ -20,7 +20,7 @@ class RowerInline(admin.StackedInline): fieldsets = ( ('Rower Plan', - {'fields':('rowerplan','planexpires','teamplanexpires','clubsize','protrialexpires','plantrialexpires',)}), + {'fields':('rowerplan','paymenttype','planexpires','teamplanexpires','clubsize','protrialexpires','plantrialexpires',)}), ('Rower Settings', {'fields': ('gdproptin','gdproptindate','weightcategory','sex','birthdate','getemailnotifications', diff --git a/rowers/models.py b/rowers/models.py index 85b7a2fd..3a711c35 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -704,6 +704,7 @@ class Rower(models.Model): choices=plans) paymenttype = models.CharField( default='single',max_length=30, + verbose_name='Payment Type', choices=( ('single','single'), ('recurring','recurring') diff --git a/templates/newbasefront.html b/templates/newbasefront.html index 052d1ffc..97cba250 100644 --- a/templates/newbasefront.html +++ b/templates/newbasefront.html @@ -165,7 +165,7 @@
        • {% if user.is_authenticated %}
        • - + {% if user.rower.rowerplan == 'pro' %} {% elif user.rower.rowerplan == 'coach' %} From f403d1bf16f8073fb8e68918e77de2a5421818bd Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Tue, 27 Nov 2018 15:03:37 +0100 Subject: [PATCH 06/13] resolves #384 --- rowers/templates/menu_workout.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rowers/templates/menu_workout.html b/rowers/templates/menu_workout.html index 4d8a5b6f..093b9b6d 100644 --- a/rowers/templates/menu_workout.html +++ b/rowers/templates/menu_workout.html @@ -1,5 +1,5 @@ {% load rowerfilters %} -

          Workout

          +

          Workout

          • From 8162b2fc45841022ea2ef8108c77eb3dc4dc8eba Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Tue, 27 Nov 2018 17:49:02 +0100 Subject: [PATCH 07/13] initial incomplete version --- rowers/models.py | 180 ++++++++- rowers/mytypes.py | 6 + rowers/plannedsessions.py | 167 +++++++- .../templates/indoorvirtualeventcreate.html | 61 +++ rowers/templates/menu_racing.html | 5 + rowers/templates/racelist.html | 6 +- rowers/templates/virtualevent.html | 64 ++- rowers/templates/virtualeventcreate.html | 2 +- rowers/urls.py | 3 + rowers/views.py | 373 ++++++++++++++++-- 10 files changed, 815 insertions(+), 52 deletions(-) create mode 100644 rowers/templates/indoorvirtualeventcreate.html diff --git a/rowers/models.py b/rowers/models.py index 3a711c35..34d4aa1d 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -1664,6 +1664,7 @@ class PlannedSession(models.Model): ('cycletarget','Total for a time period'), ('coursetest','OTW test over a course'), ('race','Virtual Race'), + ('indoorrace','Indoor Virtual Race'), ) sessionmodechoices = ( @@ -1772,7 +1773,7 @@ class PlannedSession(models.Model): else: self.sessionunit = 'None' - if self.sessiontype == 'test': + if self.sessiontype == 'test' or self.sessiontype == 'indoorrace': if self.sessionmode not in ['distance','time']: if self.sessionvalue < 100: self.sessionmode = 'time' @@ -1937,7 +1938,126 @@ def get_course_timezone(course): 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(), + } + + def __init__(self,*args,**kwargs): + super(IndoorVirtualRaceForm, self).__init__(*args, **kwargs) + self.fields['sessionunit'].choices = [('min','minutes'),('m','meters')] + + def clean(self): + cd = self.cleaned_data + timezone_str = cd['timezone'] + + 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) @@ -2309,6 +2429,54 @@ class VirtualRaceResult(models.Model): s = self.sex, ) +# Virtual Race results (for keeping results when workouts are deleted) +class IndoorVirtualRaceResult(models.Model): + boatclasses = (type for type in mytypes.workouttypes if type[0] in mytypes.otetypes) + userid = models.IntegerField(default=0) + 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') + race = models.ForeignKey(VirtualRace) + 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) + + def __unicode__(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} 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}" in {c} ({s})'.format( + n = name, + r = self.race, + c = self.boatclass, + s = self.sex, + ) + class CourseTestResult(models.Model): userid = models.IntegerField(default=0) @@ -2318,6 +2486,16 @@ class CourseTestResult(models.Model): distance = models.IntegerField(default=0) coursecompleted = models.BooleanField(default=False) +class IndoorVirtualRaceResultForm(ModelForm): + class Meta: + model = IndoorVirtualRaceResult + fields = ['teamname','weightcategory','boatclass','age'] + + + def __init__(self, *args, **kwargs): + super(IndoorVirtualRaceResultForm, self).__init__(*args, **kwargs) + + class VirtualRaceResultForm(ModelForm): class Meta: model = VirtualRaceResult diff --git a/rowers/mytypes.py b/rowers/mytypes.py index e0b9fa7a..ad704756 100644 --- a/rowers/mytypes.py +++ b/rowers/mytypes.py @@ -225,6 +225,12 @@ otwtypes = ( 'churchboat' ) +otetypes = ( + 'rower', + 'dynamic', + 'slides' + ) + rowtypes = ( 'water', 'rower', diff --git a/rowers/plannedsessions.py b/rowers/plannedsessions.py index 3b274605..8edd5390 100644 --- a/rowers/plannedsessions.py +++ b/rowers/plannedsessions.py @@ -20,7 +20,7 @@ from rowers.models import ( Rower, Workout,Team, GeoCourse, TrainingMicroCycle,TrainingMesoCycle,TrainingMacroCycle, TrainingPlan,PlannedSession,VirtualRaceResult,CourseTestResult, - get_course_timezone + get_course_timezone, IndoorVirtualRaceResult ) from rowers.courses import get_time_course @@ -574,6 +574,56 @@ def update_plannedsession(ps,cd): return 1,'Planned Session Updated' +def update_indoorvirtualrace(ps,cd): + for attr, value in cd.items(): + if attr == 'comment': + value.replace("\r\n", " "); + value.replace("\n", " "); + setattr(ps, attr, value) + + timezone_str = cd['timezone'] + + # correct times + + 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) + ) + + registration_form = cd['registration_form'] + registration_closure = cd['registration_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 = ps.evaluation_closure + + ps.registration_closure = registration_closure + + ps.timezone = timezone_str + + ps.save() + + return 1,'Virtual Race Updated' + def update_virtualrace(ps,cd): for attr, value in cd.items(): if attr == 'comment': @@ -708,6 +758,10 @@ def race_can_resubmit(r,race): return False def race_can_adddiscipline(r,race): + + if race.sessiontype != 'race': + return False + records = VirtualRaceResult.objects.filter( userid=r.id, race=race) @@ -813,6 +867,116 @@ def remove_rower_race(r,race,recordid=None): return 1 # Low Level functions - to be called by higher level methods +def add_workout_indoorrace(ws,race,r,recordid=0): + 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,0 + + if len(ws)>1 and race.sessiontype == 'test': + errors.append('For tests, you can only attach one workout') + return result,comments,errors,0 + + + + ids = [w.id for w in ws] + ids = list(set(ids)) + + if len(ids)>1 and race.sessiontype in ['test','coursetest','race','indoorrace']: + errors.append('For tests, you can only attach one workout') + return result,comments,errors,0 + + + + username = r.user.first_name+' '+r.user.last_name + if r.birthdate: + age = calculate_age(r.birthdate) + else: + age = None + + record = IndoorVirtualRaceResult.objects.get( + userid=r.id, + race=race, + id=recordid + ) + + records = IndoorVirtualRaceResult.objects.filter( + userid=r.id, + race=race, + workoutid = ws[0].id + ) + + if not record: + errors.append("Couldn't find this entry") + return result,comments,errors,0 + + if race.sessionmode == 'distance': + if ws[0].distance != race.sessionvalue: + errors.append('Your workout did not have the correct distance') + return 0,comments, errors, 0 + else: + record.distance = ws[0].distance + record.duration = ws[0].duration + else: + t = ws[0].duration + seconds = t.second+t.minute*60.+t.hour*3600.+t.microsecond/1.e6 + if seconds != race.sessionvalue*60.: + errors.append('Your workout did not have the correct duration') + return 0, comments, errors, 0 + else: + record.distance = ws[0].distance + record.duration = ws[0].duration + + + if ws[0].weightcategory != record.weightcategory: + errors.append('Your workout weight category did not match the weight category you registered') + return 0,comments, errors,0 + + # start adding sessions + if ws[0].startdatetime>=startdatetime and ws[0].startdatetime<=enddatetime: + ws[0].plannedsession = race + ws[0].save() + result += 1 + + else: + errors.append('Workout %i did not match the race window' % ws[0].id) + return result,comments,errors,0 + + if result>0: + for otherrecord in records: + otherrecord.workoutid = None + otherrecord.coursecompleted = False + otherrecord.save() + + record.coursecompleted = True + record.workoutid = ws[0].id + record.save() + + add_workouts_plannedsession(ws,race,r) + + + return result,comments,errors,0 + + def add_workout_race(ws,race,r,splitsecond=0,recordid=0): result = 0 comments = [] @@ -895,7 +1059,6 @@ def add_workout_race(ws,race,r,splitsecond=0,recordid=0): if result>0: for otherrecord in records: - print otherrecord otherrecord.workoutid = None otherrecord.coursecompleted = False otherrecord.save() diff --git a/rowers/templates/indoorvirtualeventcreate.html b/rowers/templates/indoorvirtualeventcreate.html new file mode 100644 index 00000000..912d21fa --- /dev/null +++ b/rowers/templates/indoorvirtualeventcreate.html @@ -0,0 +1,61 @@ +{% extends "newbase.html" %} +{% load staticfiles %} +{% load rowerfilters %} + +{% block title %}New Virtual Race{% endblock %} + +{% block main %} + +

            New Indoor Virtual Race

            + +
              +
            • +

              With this form, you can create a new virtual race. After you submit + the form, the race is created and will be visible to all users. From + that moment, only the site admin can delete the race + (admin@rowsandall.com). You can still edit the race until + the start of the race window. +

              +
            • + +
            • +
              + {% if form.errors %} +

              + Please correct the error{{ form.errors|pluralize }} below. +

              + {% endif %} +

              + + {{ form.as_table }} +
              +

              +

              + {% csrf_token %} + +

              +
              +
            • +
            • +

              +

                +
              • All times are local times in the time zone you select
              • +
              • Adding a contact phone number and email is not mandatory, but we + strongly recommend it.
              • +
              • If your event has a registration closure deadline, participants + have to enter (and can withdraw) before the registration closure time.
              • +
              • Participants can submit results until the evaluation closure time.
              • +
              +

              +
            • +
            + + +{% endblock %} + +{% block scripts %} +{% endblock %} + +{% block sidebar %} +{% include 'menu_racing.html' %} +{% endblock %} diff --git a/rowers/templates/menu_racing.html b/rowers/templates/menu_racing.html index 6aabdaff..4a06ef2d 100644 --- a/rowers/templates/menu_racing.html +++ b/rowers/templates/menu_racing.html @@ -10,6 +10,11 @@  New Race
          • +
          • + +  New Indoor Race + +
          •  Courses diff --git a/rowers/templates/racelist.html b/rowers/templates/racelist.html index 2784b896..092de0d2 100644 --- a/rowers/templates/racelist.html +++ b/rowers/templates/racelist.html @@ -8,7 +8,8 @@ Event Country Course - Distance + + Click for Details @@ -26,7 +27,8 @@ {{ race.name }} {{ race.course.country }} {{ race.course.name }} - {{ race.sessionvalue }} m + {{ race.sessionvalue }} + {{ race.sessionunit }} {% if rower %} {% if race|can_register:rower %} diff --git a/rowers/templates/virtualevent.html b/rowers/templates/virtualevent.html index a731cf32..338d1805 100644 --- a/rowers/templates/virtualevent.html +++ b/rowers/templates/virtualevent.html @@ -14,6 +14,7 @@

            {{ race.name }}

              + {% if race.sessiontype == 'race' %}
            • Course

              @@ -24,6 +25,7 @@ {{ coursescript|safe }}
            • + {% endif %}
            • @@ -32,11 +34,23 @@

              + {% if race.sessiontype == 'race' %} + {% else %} - + + + + + + {% endif %} + + @@ -81,38 +95,42 @@ {% for button in buttons %} {% if button == 'registerbutton' %}

              - Register + {% if race.sessiontype == 'race' %} + Register + {% else %} + Register + {% endif %}

              {% endif %} {% if button == 'submitbutton' %} - Submit Result + Submit Result {% endif %} {% if button == 'resubmitbutton' %}

              - Submit New Result + Submit New Result

              {% endif %} {% if button == 'withdrawbutton' %}

              - Withdraw + Withdraw

              {% endif %} {% if button == 'adddisciplinebutton' %}

              - + Register New Boat

              {% endif %} {% if button == 'editbutton' %}

              - Edit Race + {% if race.sessiontype == 'race' %} + Edit Race + {% else %} + Edit Race + + {% endif %}

              {% endif %} {% endfor %} @@ -135,8 +153,10 @@ + {% if race.sessiontype == 'race' %} + {% endif %} @@ -153,8 +173,10 @@ + {% if race.sessiontype == 'race' %} + {% endif %} + {% if race.sessiontype == 'race' %} + {% endif %} {% endfor %} @@ -210,8 +234,10 @@ + {% if race.sessiontype == 'race' %} + {% endif %} @@ -221,8 +247,10 @@ + {% if race.sessiontype == 'race' %} + {% endif %} @@ -247,6 +275,14 @@ Virtual races are intended as an informal way to add a competitive element to training and as a quick way to set up and manage small regattas. +

              +

              + On the water races are rowed on the course shown. You cannot submit results rowed + on other bodies of water. +

              +

              + Indoor races are open for all, wherever you live. However, be aware of the + time zone for the race window.

              As a rowsandall.com user, you can @@ -271,6 +307,10 @@ you delete the respective workout or remove your account. By registering, you agree with this and the race rules.

              +

              + If you use a manually added workout for your indoor race result, + please attach a screenshot of the ergometer display for verification. +

              Virtual Racing on rowsandall.com is honors based. Please be a good sport, submit real results rowed by you, and make sure you set the diff --git a/rowers/templates/virtualeventcreate.html b/rowers/templates/virtualeventcreate.html index 6898709f..d2489440 100644 --- a/rowers/templates/virtualeventcreate.html +++ b/rowers/templates/virtualeventcreate.html @@ -32,7 +32,7 @@

              {% csrf_token %} - +

              diff --git a/rowers/urls.py b/rowers/urls.py index 6dbe2ea5..eeea79e5 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -143,9 +143,12 @@ urlpatterns = [ url(r'^list-workouts/(?P\d+-\d+-\d+)/(?P\d+-\d+-\d+)$',views.workouts_view), url(r'^virtualevents$',views.virtualevents_view), url(r'^virtualevent/create$',views.virtualevent_create_view), + url(r'^virtualevent/createindoor$',views.indoorvirtualevent_create_view), url(r'^virtualevent/(?P\d+)$',views.virtualevent_view), url(r'^virtualevent/(?P\d+)/edit$',views.virtualevent_edit_view), + url(r'^virtualevent/(?P\d+)/editindoor$',views.indoorvirtualevent_edit_view), url(r'^virtualevent/(?P\d+)/register$',views.virtualevent_register_view), + url(r'^virtualevent/(?P\d+)/registerindoor$',views.indoorvirtualevent_register_view), url(r'^virtualevent/(?P\d+)/adddiscipline$',views.virtualevent_addboat_view), url(r'^virtualevent/(?P\d+)/withdraw/(?P\d+)$',views.virtualevent_withdraw_view), url(r'^virtualevent/(?P\d+)/withdraw$',views.virtualevent_withdraw_view), diff --git a/rowers/views.py b/rowers/views.py index 111618d5..184a6682 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -90,7 +90,9 @@ from rowers.models import ( WorkoutComment,WorkoutCommentForm,RowerExportForm, CalcAgePerformance,PowerTimeFitnessMetric,PlannedSessionForm, PlannedSessionFormSmall,GeoCourseEditForm,VirtualRace, - VirtualRaceForm,VirtualRaceResultForm,RowerImportExportForm + VirtualRaceForm,VirtualRaceResultForm,RowerImportExportForm, + IndoorVirtualRaceResultForm,IndoorVirtualRaceResult, + IndoorVirtualRaceForm, ) from rowers.models import ( FavoriteForm,BaseFavoriteFormSet,SiteAnnouncement,BasePlannedSessionFormSet, @@ -15765,7 +15767,8 @@ def virtualevents_view(request): if country == 'All': countries = VirtualRace.objects.order_by('country').values_list('country').distinct() else: - countries = [country] + countries = [country, + 'Indoor'] if regattatype == 'upcoming': races1 = VirtualRace.objects.filter( @@ -15836,13 +15839,17 @@ def virtualevent_view(request,id=0): except VirtualRace.DoesNotExist: raise Http404("Virtual Race does not exist") - script,div = course_map(race.course) + if race.sessiontype == 'race': + script,div = course_map(race.course) + resultobj = VirtualRaceResult + else: + script = '' + div = '' + resultobj = IndoorVirtualRaceResult - - records = VirtualRaceResult.objects.filter( - race=race - ) - + records = resultobj.objects.filter(race=race) + + buttons = [] if not request.user.is_anonymous(): @@ -15881,8 +15888,11 @@ def virtualevent_view(request,id=0): try: boatclass = cd['boatclass'] except KeyError: - boatclass = [t for t in mytypes.otwtypes] - + if race.sessiontype == 'race': + boatclass = [t for t in mytypes.otwtypes] + else: + boatclass = [t for t in mytypes.otetypes] + age_min = cd['age_min'] age_max = cd['age_max'] @@ -15891,35 +15901,46 @@ def virtualevent_view(request,id=0): except KeyError: weightcategory = ['hwt','lwt'] - results = VirtualRaceResult.objects.filter( - race=race, - workoutid__isnull=False, - boatclass__in=boatclass, - boattype__in=boattype, - sex__in=sex, - weightcategory__in=weightcategory, - age__gte=age_min, - age__lte=age_max - ).order_by("duration") - - # to-do - add DNS - dns = [] - if timezone.now() > race.evaluation_closure: - dns = VirtualRaceResult.objects.filter( + if race.sessiontype == 'race': + results = resultobj.objects.filter( race=race, - workoutid__isnull=True, + workoutid__isnull=False, boatclass__in=boatclass, boattype__in=boattype, sex__in=sex, weightcategory__in=weightcategory, age__gte=age_min, age__lte=age_max + ).order_by("duration") + else: + results = resultobj.objects.filter( + race=race, + workoutid__isnull=False, + boatclass__in=boatclass, + sex__in=sex, + weightcategory__in=weightcategory, + age__gte=age_min, + age__lte=age_max + ).order_by("duration","-distance") + + + # to-do - add DNS + dns = [] + if timezone.now() > race.evaluation_closure: + dns = resultobj.objects.filter( + race=race, + workoutid__isnull=True, + boatclass__in=boatclass, + sex__in=sex, + weightcategory__in=weightcategory, + age__gte=age_min, + age__lte=age_max ) else: - results = VirtualRaceResult.objects.filter( + results = resultobj.objects.filter( race=race, workoutid__isnull=False, - ).order_by("duration") + ).order_by("duration","-distance") if results: form = RaceResultFilterForm(records=records) @@ -15929,7 +15950,7 @@ def virtualevent_view(request,id=0): # to-do - add DNS dns = [] if timezone.now() > race.evaluation_closure: - dns = VirtualRaceResult.objects.filter( + dns = resultobj.objects.filter( race=race, workoutid__isnull=True, ) @@ -16190,6 +16211,221 @@ def virtualevent_register_view(request,id=0): }) +@login_required() +def indoorvirtualevent_register_view(request,id=0): + r = getrower(request.user) + try: + race = VirtualRace.objects.get(id=id) + except VirtualRace.DoesNotExist: + raise Http404("Virtual Race does not exist") + + if not race_can_register(r,race): + messages.error(request,"You cannot register for this race") + + url = reverse(virtualevent_view, + kwargs = { + 'id':race.id + }) + + return HttpResponseRedirect(url) + + # we're still here + if request.method == 'POST': + # process form + form = IndoorVirtualRaceResultForm(request.POST) + if form.is_valid(): + cd = form.cleaned_data + teamname = cd['teamname'] + weightcategory = cd['weightcategory'] + age = cd['age'] + boatclass = cd['boatclass'] + + sex = r.sex + + if r.birthdate: + age = calculate_age(r.birthdate) + sex = r.sex + + if sex == 'not specified': + sex = 'male' + + record = IndoorVirtualRaceResult( + userid=r.id, + teamname=teamname, + race=race, + username = u'{f} {l}'.format( + f = r.user.first_name, + l = r.user.last_name + ), + weightcategory=weightcategory, + duration=datetime.time(0,0), + boatclass=boatclass, + coursecompleted=False, + sex=sex, + age=age + ) + + record.save() + + add_rower_race(r,race) + + + + messages.info( + request, + "You have successfully registered for this race. Good luck!" + ) + + url = reverse(virtualevent_view, + kwargs = { + 'id':race.id + }) + + return HttpResponseRedirect(url) + + else: + initial = { + 'age': calculate_age(r.birthdate), + 'weightcategory': r.weightcategory, + } + + form = IndoorVirtualRaceResultForm(initial=initial) + + return render(request,'virtualeventregister.html', + { + 'form':form, + 'race':race, + 'userid':r.user.id, + + }) + +@login_required() +def indoorvirtualevent_create_view(request): + r = getrower(request.user) + + if request.method == 'POST': + racecreateform = IndoorVirtualRaceForm(request.POST) + if racecreateform.is_valid(): + cd = racecreateform.cleaned_data + startdate = cd['startdate'] + start_time = cd['start_time'] + enddate = cd['enddate'] + end_time = cd['end_time'] + comment = cd['comment'] + sessionunit = cd['sessionunit'] + sessionvalue = cd['sessionvalue'] + name = cd['name'] + registration_form = cd['registration_form'] + registration_closure = cd['registration_closure'] + evaluation_closure = cd['evaluation_closure'] + contact_phone = cd['contact_phone'] + contact_email = cd['contact_email'] + + # correct times + + timezone_str = cd['timezone'] + + startdatetime = datetime.datetime.combine(startdate,start_time) + enddatetime = datetime.datetime.combine(enddate,end_time) + + + startdatetime = pytz.timezone(timezone_str).localize( + startdatetime + ) + enddatetime = pytz.timezone(timezone_str).localize( + enddatetime + ) + evaluation_closure = pytz.timezone(timezone_str).localize( + evaluation_closure.replace(tzinfo=None) + ) + + 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 sessionunit == 'min': + sessionmode = 'time' + else: + sessionmode = 'distance' + + vs = VirtualRace( + name=name, + startdate=startdate, + preferreddate = startdate, + start_time = start_time, + enddate=enddate, + end_time=end_time, + comment=comment, + sessiontype = 'indoorrace', + sessionunit = sessionunit, + sessionmode = sessionmode, + sessionvalue = sessionvalue, + course=None, + timezone=timezone_str, + evaluation_closure=evaluation_closure, + registration_closure=registration_closure, + contact_phone=contact_phone, + contact_email=contact_email, + country = 'Indoor', + manager=request.user, + ) + + vs.save() + + # create Site Announcement & Tweet + if settings.DEBUG: + dotweet = False + elif 'dev' in settings.SITE_URL: + dotweet = False + else: + dotweet = True + try: + sa = SiteAnnouncement( + announcement = "New Virtual Indoor Race on rowsandall.com: {name}".format( + name = name.encode('utf8'), + ), + dotweet = dotweet + ) + + sa.save() + except UnicodeEncodeError: + sa = SiteAnnouncement( + announcement = "New Virtual Indoor Race on rowsandall.com: {name}".format( + name = name, + ), + dotweet = dotweet + ) + + + sa.save() + + url = reverse(virtualevents_view) + return HttpResponseRedirect(url) + else: + + racecreateform = IndoorVirtualRaceForm() + + + return render(request,'indoorvirtualeventcreate.html', + { + 'form':racecreateform, + 'rower':r, + 'active':'nav-racing', + + }) + @login_required() def virtualevent_create_view(request): r = getrower(request.user) @@ -16369,6 +16605,64 @@ def virtualevent_edit_view(request,id=0): }) +@login_required() +def indoorvirtualevent_edit_view(request,id=0): + r = getrower(request.user) + + try: + race = VirtualRace.objects.get(id=id) + if race.manager != request.user: + raise PermissionDenied("Access denied") + except VirtualRace.DoesNotExist: + raise Http404("Virtual Race does not exist") + + start_time = race.start_time + start_date = race.startdate + startdatetime = datetime.datetime.combine(start_date,start_time) + startdatetime = pytz.timezone(race.timezone).localize( + startdatetime + ) + + if timezone.now() > startdatetime: + messages.error(request,"You cannot edit a race after the start of the race window") + url = reverse(virtualevent_view, + kwargs={ + 'id':race.id, + }) + + if request.method == 'POST': + racecreateform = IndoorVirtualRaceForm(request.POST,instance=race) + if racecreateform.is_valid(): + cd = racecreateform.cleaned_data + + res, message = update_indoorvirtualrace(race,cd) + + if res: + messages.info(request,message) + else: + messages.error(request,message) + + url = reverse(virtualevent_view, + kwargs = { + 'id':race.id + }) + + return HttpResponseRedirect(url) + + else: + + racecreateform = IndoorVirtualRaceForm(instance=race) + + + return render(request,'virtualeventedit.html', + { + 'form':racecreateform, + 'rower':r, + 'race':race, + + }) + + @login_required() def virtualevent_submit_result_view(request,id=0): @@ -16391,7 +16685,12 @@ def virtualevent_submit_result_view(request,id=0): can_submit = race_can_submit(r,race) or race_can_resubmit(r,race) - records = VirtualRaceResult.objects.filter( + if race.sessiontype == 'race': + resultobj = VirtualRaceResult + else: + resultobj = IndoorVirtualRaceResult + + records = resultobj.objects.filter( userid = r.id, race=race ) @@ -16462,10 +16761,16 @@ def virtualevent_submit_result_view(request,id=0): workouts = Workout.objects.filter(id=selectedworkout) - result,comments,errors,jobid = add_workout_race( - workouts,race,r, - splitsecond=splitsecond,recordid=recordid) -# if result: + if race.sessiontype == 'race': + result,comments,errors,jobid = add_workout_race( + workouts,race,r, + splitsecond=splitsecond,recordid=recordid) + else: + result,comments,errors,jobid = add_workout_indoorrace( + workouts,race,r,recordid=recordid) + + + # if result: # for w in ws: # remove_workout_plannedsession(w,race) # delete_race_result(w,race) From 1d2e1cb73f00ef7a1cf3fc22c6b87a1b34f26363 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Tue, 27 Nov 2018 21:13:41 +0100 Subject: [PATCH 08/13] form improvement --- rowers/forms.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/rowers/forms.py b/rowers/forms.py index 290e5f00..65122682 100644 --- a/rowers/forms.py +++ b/rowers/forms.py @@ -892,8 +892,11 @@ class RaceResultFilterForm(forms.Form): self.fields['boatclass'].choices = boatclasschoices # boattype - theboattypees = [record.boattype for record in records] - theboattypees = list(set(theboattypees)) + try: + theboattypees = [record.boattype for record in records] + theboattypees = list(set(theboattypees)) + except AttributeError: + theboattypees = [] if len(theboattypees)<= 1: del self.fields['boattype'] From 7c1b0626f935e7de07150a773435961f237151bb Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Tue, 27 Nov 2018 22:00:04 +0100 Subject: [PATCH 09/13] added race breadcrumbs --- rowers/models.py | 4 +- rowers/plannedsessions.py | 18 +++- rowers/views.py | 167 +++++++++++++++++++++++++++++++++++++- 3 files changed, 182 insertions(+), 7 deletions(-) diff --git a/rowers/models.py b/rowers/models.py index 34d4aa1d..88c96f5b 100644 --- a/rowers/models.py +++ b/rowers/models.py @@ -2462,7 +2462,7 @@ class IndoorVirtualRaceResult(models.Model): u2 = rr.user.last_name, ) if self.teamname: - return u'Entry for {n} for "{r}" in {c} with {t} ({s})'.format( + return u'Entry for {n} for "{r}" on {c} with {t} ({s})'.format( n = name, r = self.race, t = self.teamname, @@ -2470,7 +2470,7 @@ class IndoorVirtualRaceResult(models.Model): s = self.sex, ) else: - return u'Entry for {n} for "{r}" in {c} ({s})'.format( + return u'Entry for {n} for "{r}" on {c} ({s})'.format( n = name, r = self.race, c = self.boatclass, diff --git a/rowers/plannedsessions.py b/rowers/plannedsessions.py index 8edd5390..6cf62bda 100644 --- a/rowers/plannedsessions.py +++ b/rowers/plannedsessions.py @@ -680,8 +680,13 @@ def race_rower_status(r,race): has_registered = False is_complete = False - - vs = VirtualRaceResult.objects.filter(userid=r.id,race=race) + + if race.sessiontype == 'race': + resultobj = VirtualRaceResult + else: + resultobj = IndoorVirtualRaceResult + + vs = IndoorVirtualRaceResult.objects.filter(userid=r.id,race=race) if vs: has_registered = True is_complete = vs[0].coursecompleted @@ -852,13 +857,18 @@ def add_rower_race(r,race): def remove_rower_race(r,race,recordid=None): race.rower.remove(r) + if race.sessiontype == 'race': + recordobj = VirtualRaceResult + else: + recordobj = IndoorVirtualRaceResult + if recordid: - records = VirtualRaceResult.objects.filter(userid=r.id, + records = recordobj.objects.filter(userid=r.id, workoutid__isnull=True, race=race, id=recordid) else: - records = VirtualRaceResult.objects.filter(userid=r.id, + records = recordobj.objects.filter(userid=r.id, workoutid__isnull=True, race=race,) for r in records: diff --git a/rowers/views.py b/rowers/views.py index 184a6682..3ddb7653 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -15816,9 +15816,17 @@ def virtualevents_view(request): 'rower':r, }) + breadcrumbs = [ + { + 'url':reverse(virtualevents_view), + 'name': 'Racing' + }, + ] + return render(request,'virtualevents.html', { 'races':races, 'form':form, + 'breadcrumbs':breadcrumbs, 'active':'nav-racing', 'rower':r, } @@ -15956,12 +15964,26 @@ def virtualevent_view(request,id=0): ) - + breadcrumbs = [ + { + 'url':reverse(virtualevents_view), + 'name': 'Racing' + }, + { + 'url':reverse(virtualevent_view, + kwargs={'id':race.id} + ), + 'name': race.name + } + ] + + return render(request,'virtualevent.html', { 'coursescript':script, 'coursediv':div, + 'breadcrumbs':breadcrumbs, 'race':race, 'rower':r, 'results':results, @@ -16110,9 +16132,31 @@ def virtualevent_addboat_view(request,id=0): form = VirtualRaceResultForm(initial=initial) + breadcrumbs = [ + { + 'url':reverse(virtualevents_view), + 'name': 'Racing' + }, + { + 'url':reverse(virtualevent_view, + kwargs={'id':race.id} + ), + 'name': race.name + }, + { + 'url': reverse(virtualevent_addboat_view, + kwargs = {'id':race.id} + ), + 'name': 'Add Discipline' + } + ] + + + return render(request,'virtualeventregister.html', { 'form':form, + 'breadcrumbs':breadcrumbs, 'race':race, 'userid':r.user.id, 'active': 'nav-racing', @@ -16203,9 +16247,28 @@ def virtualevent_register_view(request,id=0): form = VirtualRaceResultForm(initial=initial) + breadcrumbs = [ + { + 'url':reverse(virtualevents_view), + 'name': 'Racing' + }, + { + 'url':reverse(virtualevent_view, + kwargs={'id':race.id} + ), + 'name': race.name + }, + { + 'url': reverse(virtualevent_register_view, + kwargs = {'id':race.id} + ), + 'name': 'Register' + } + ] return render(request,'virtualeventregister.html', { 'form':form, + 'breadcrumbs':breadcrumbs, 'race':race, 'userid':r.user.id, @@ -16291,10 +16354,30 @@ def indoorvirtualevent_register_view(request,id=0): form = IndoorVirtualRaceResultForm(initial=initial) + breadcrumbs = [ + { + 'url':reverse(virtualevents_view), + 'name': 'Racing' + }, + { + 'url':reverse(virtualevent_view, + kwargs={'id':race.id} + ), + 'name': race.name + }, + { + 'url': reverse(indoorvirtualevent_register_view, + kwargs = {'id':race.id} + ), + 'name': 'Register' + } + ] + return render(request,'virtualeventregister.html', { 'form':form, 'race':race, + 'breadcrumbs':breadcrumbs, 'userid':r.user.id, }) @@ -16418,9 +16501,22 @@ def indoorvirtualevent_create_view(request): racecreateform = IndoorVirtualRaceForm() + breadcrumbs = [ + { + 'url':reverse(virtualevents_view), + 'name': 'Racing' + }, + { + 'url':reverse(indoorvirtualevent_create_view, + ), + 'name': 'New Indoor Virtual Regatta' + }, + ] + return render(request,'indoorvirtualeventcreate.html', { 'form':racecreateform, + 'breadcrumbs':breadcrumbs, 'rower':r, 'active':'nav-racing', @@ -16540,9 +16636,21 @@ def virtualevent_create_view(request): racecreateform = VirtualRaceForm() + breadcrumbs = [ + { + 'url':reverse(virtualevents_view), + 'name': 'Racing' + }, + { + 'url':reverse(virtualevent_create_view, + ), + 'name': 'New Virtual Regatta' + }, + ] return render(request,'virtualeventcreate.html', { 'form':racecreateform, + 'breadcrumbs':breadcrumbs, 'rower':r, 'active':'nav-racing', @@ -16596,10 +16704,29 @@ def virtualevent_edit_view(request,id=0): racecreateform = VirtualRaceForm(instance=race) + breadcrumbs = [ + { + 'url':reverse(virtualevents_view), + 'name': 'Racing' + }, + { + 'url':reverse(virtualevent_view, + kwargs={'id':race.id} + ), + 'name': race.name + }, + { + 'url': reverse(virtualevent_edit_view, + kwargs = {'id':race.id} + ), + 'name': 'Edit' + } + ] return render(request,'virtualeventedit.html', { 'form':racecreateform, + 'breadcrumbs':breadcrumbs, 'rower':r, 'race':race, @@ -16654,9 +16781,28 @@ def indoorvirtualevent_edit_view(request,id=0): racecreateform = IndoorVirtualRaceForm(instance=race) + breadcrumbs = [ + { + 'url':reverse(virtualevents_view), + 'name': 'Racing' + }, + { + 'url':reverse(virtualevent_view, + kwargs={'id':race.id} + ), + 'name': race.name + }, + { + 'url': reverse(indoorvirtualevent_edit_view, + kwargs = {'id':race.id} + ), + 'name': 'Edit' + } + ] return render(request,'virtualeventedit.html', { 'form':racecreateform, + 'breadcrumbs':breadcrumbs, 'rower':r, 'race':race, @@ -16799,10 +16945,29 @@ def virtualevent_submit_result_view(request,id=0): else: w_form = WorkoutRaceSelectForm(workoutdata,entries) + breadcrumbs = [ + { + 'url':reverse(virtualevents_view), + 'name': 'Racing' + }, + { + 'url':reverse(virtualevent_view, + kwargs={'id':race.id} + ), + 'name': race.name + }, + { + 'url': reverse(virtualevent_submit_result_view, + kwargs = {'id':race.id} + ), + 'name': 'Submit Result' + } + ] return render(request,'race_submit.html', { 'race':race, 'workouts':ws, + 'breadcrumbs':breadcrumbs, 'active':'nav-racing', 'rower':r, 'w_form':w_form, From dba094475c9231f4b66f4b17f5aadced58cecec2 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 28 Nov 2018 11:46:40 +0100 Subject: [PATCH 10/13] small improvement --- rowers/plannedsessions.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/rowers/plannedsessions.py b/rowers/plannedsessions.py index 6cf62bda..04f6c194 100644 --- a/rowers/plannedsessions.py +++ b/rowers/plannedsessions.py @@ -825,7 +825,12 @@ def race_can_withdraw(r,race): return True def race_can_register(r,race): - records = VirtualRaceResult.objects.filter( + if race.sessiontype == 'race': + recordobj = VirtualRaceResult + else: + recordobj = IndoorVirtualRaceResult + + records = recordobj.objects.filter( userid=r.id, race=race) From a75b59519e3b45667648f67a59cb13210985eb43 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 28 Nov 2018 12:34:39 +0100 Subject: [PATCH 11/13] first version of disqualification --- rowers/templates/virtualevent.html | 10 ++++++++ rowers/urls.py | 2 ++ rowers/views.py | 41 ++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+) diff --git a/rowers/templates/virtualevent.html b/rowers/templates/virtualevent.html index 338d1805..07dae4e3 100644 --- a/rowers/templates/virtualevent.html +++ b/rowers/templates/virtualevent.html @@ -160,6 +160,7 @@ + @@ -182,6 +183,15 @@ + {% endfor %} {% for result in dns %} diff --git a/rowers/urls.py b/rowers/urls.py index eeea79e5..8fcd905a 100644 --- a/rowers/urls.py +++ b/rowers/urls.py @@ -154,6 +154,8 @@ urlpatterns = [ url(r'^virtualevent/(?P\d+)/withdraw$',views.virtualevent_withdraw_view), url(r'^virtualevent/(?P\d+)/submit$', views.virtualevent_submit_result_view), + url(r'^virtualevent/(?P\d+)/disqualify/(?P\d+)/', + views.virtualevent_disqualify_view), url(r'^list-workouts/$',views.workouts_view), url(r'^list-courses/$',views.courses_view), url(r'^courses/upload$',views.course_upload_view), diff --git a/rowers/views.py b/rowers/views.py index 3ddb7653..2b048024 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -15832,6 +15832,46 @@ def virtualevents_view(request): } ) +@login_required() +def virtualevent_disqualify_view(request,raceid=0,recordid=0): + + r = getrower(request.user) + + # datum moet voor race evaluation date zijn (ook in template controleren) + + + try: + race = VirtualRace.objects.get(id=raceid) + except VirtualRace.DoesNotExist: + raise Http404("Virtual Race does not exist") + + if r.user != race.manager: + raise PermissionDenied("Access denied") + + if race.sessiontype == 'race': + recordobj = VirtualRaceResult + else: + recordobj = IndoorVirtualRaceResult + + if timezone.now() > race.evaluation_closure: + try: + record = recordobj.objects.get(id=recordid) + + + messages.info(request,"We have invalidated the result for: "+str(record)) + + record.coursecompleted = False + record.save() + print record.coursecompleted + except recordobj.DoesNotExist: + messages.error(request,"We couldn't find the record") + else: + messages.error(request,"The evaluation is already closed and the results are official") + + url = reverse(virtualevent_view,kwargs={'id':raceid}) + + return HttpResponseRedirect(url) + def virtualevent_view(request,id=0): results = [] @@ -15948,6 +15988,7 @@ def virtualevent_view(request,id=0): results = resultobj.objects.filter( race=race, workoutid__isnull=False, + coursecompleted=True, ).order_by("duration","-distance") if results: From 774028497b6111d976dbdcc5a2db645cc1cea4da Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 28 Nov 2018 15:37:41 +0100 Subject: [PATCH 12/13] disqualification now with review and form --- rowers/emails.py | 2 + rowers/forms.py | 18 +++ rowers/tasks.py | 45 ++++++ rowers/templates/disqualification_view.html | 138 ++++++++++++++++++ rowers/templates/disqualificationemail.html | 37 +++++ .../templates/indoorvirtualeventcreate.html | 18 ++- rowers/templates/virtualevent.html | 19 ++- rowers/templates/virtualeventcreate.html | 20 +-- rowers/templatetags/rowerfilters.py | 8 + rowers/views.py | 112 ++++++++++++-- 10 files changed, 385 insertions(+), 32 deletions(-) create mode 100644 rowers/templates/disqualification_view.html create mode 100644 rowers/templates/disqualificationemail.html diff --git a/rowers/emails.py b/rowers/emails.py index 7dfa16d1..826d43bb 100644 --- a/rowers/emails.py +++ b/rowers/emails.py @@ -45,6 +45,8 @@ env = Environment(loader = FileSystemLoader(["rowers/templates"])) from django.contrib.staticfiles import finders + + def textify(html): # Remove html tags and continuous whitespaces text_only = re.sub('[ \t]+', ' ', strip_tags(html)) diff --git a/rowers/forms.py b/rowers/forms.py index 65122682..d6dc0cdd 100644 --- a/rowers/forms.py +++ b/rowers/forms.py @@ -30,6 +30,24 @@ class EmailForm(forms.Form): botcheck = forms.CharField(max_length=5) message = forms.CharField() +disqualificationreasons = ( + ('noimage','You did not attach a monitor screenshot or other photographic evidence as required'), + ('suspicious','We doubt that you rowed this in the right boat class or type'), + ('duplicate','This result looks like a duplicate entry'), + ('other','Other Reason'), +) + +disqualifiers = {} +for key, value in disqualificationreasons: + disqualifiers[key] = value + +class DisqualificationForm(forms.Form): + + reason = forms.ChoiceField(required=True, + choices=disqualificationreasons, + widget = forms.RadioSelect,) + + message = forms.CharField(required=True,widget=forms.Textarea) class MetricsForm(forms.Form): avghr = forms.IntegerField(required=False,label='Average Heart Rate') diff --git a/rowers/tasks.py b/rowers/tasks.py index f18c1ba8..f53d6dda 100644 --- a/rowers/tasks.py +++ b/rowers/tasks.py @@ -41,6 +41,22 @@ from django.utils.html import strip_tags from utils import deserialize_list,ewmovingaverage,wavg +from HTMLParser import HTMLParser +class MLStripper(HTMLParser): + def __init__(self): + self.reset() + self.fed = [] + def handle_data(self, d): + self.fed.append(d) + def get_data(self): + return ''.join(self.fed) + +def strip_tags(html): + s = MLStripper() + s.feed(html) + return s.get_data() + + from rowers.dataprepnodjango import ( update_strokedata, new_workout_from_file, getsmallrowdata_db, updatecpdata_sql, @@ -721,6 +737,35 @@ def handle_updatedps(useremail, workoutids, debug=False,**kwargs): # send email when a breakthrough workout is uploaded +@app.task +def handle_send_disqualification_email( + useremail,username,reason,message, racename, **kwargs): + + if 'debug' in kwargs: + debug = kwargs['debug'] + else: + debug = True + + subject = "Your result for {n} has been disqualified on rowsandall.com".format( + n = racename + ) + + from_email = 'Rowsandall ' + + d = { + 'username':username, + 'reason':reason, + 'message': strip_tags(message), + 'racename':racename, + } + + res = send_template_email(from_email,[useremail], + subject, + 'disqualificationemail.html', + d,**kwargs) + + return 1 + @app.task def handle_sendemail_expired(useremail,userfirstname,userlastname,expireddate, **kwargs): diff --git a/rowers/templates/disqualification_view.html b/rowers/templates/disqualification_view.html new file mode 100644 index 00000000..fe7b61a5 --- /dev/null +++ b/rowers/templates/disqualification_view.html @@ -0,0 +1,138 @@ +{% extends "newbase.html" %} +{% load staticfiles %} +{% load rowerfilters %} +{% block scripts %} +{% include "monitorjobs.html" %} +{% endblock %} + +{% block title %}{{ workout.name }} {% endblock %} +{% block og_title %}{{ workout.name }} {% endblock %} +{% block description %}{{ workout.name }} +{{ workout.date }} - {{ workout.distance }}m - {{ workout.duration |durationprint:"%H:%M:%S.%f" }}{% endblock %} +{% block og_description %}{{ workout.name }} +{{ workout.date }} - {{ workout.distance }}m - {{ workout.duration |durationprint:"%H:%M:%S.%f" }}{% endblock %} +{% if graphs1 %} +{% endif %} +{% for graph in graphs1 %} +{% block og_image %} +{% if graphs1 %} +{% for graph in graphs %} + + + + +{% endfor %} +{% else %} + + +{% endif %} +{% endblock %} +{% block image_src %} +{% for graph in graphs %} + +{% endfor %} +{% endblock %} + +{% endfor %} +{% block main %} + +

              Do you want to disqualify this result?

              +
                +
              • +

                + Before you reject this race result, please carefully review it + using the information below. If you still want to reject the entry, + scroll down to the rejection form. +

                +
              • +
              • +
              Course{{ race.course }}
              Distance{{ race.sessionvalue }} mIndoor RaceTo be rowed on a Concept2 ergometer
              Time Zone{{ race.timezone }}
              + {{ race.sessionmode }} challenge + {{ race.sessionvalue }} {{ race.sessionunit }} +
              Registration closure     Class BoatTime Distance Details{{ result.age }} {{ result.sex }} {{ result.weightcategory }}{{ result.boatclass }} {{ result.boattype }}{{ result.duration |durationprint:"%H:%M:%S.%f" }} {{ result.distance }} m @@ -170,8 +192,10 @@ {{ result.age }} {{ result.sex }} {{ result.weightcategory }}{{ result.boatclass }} {{ result.boattype }}DNS
              Name Team NameClass BoatAge Gender Weight Category
              {{ record.username }} {{ record.teamname }}{{ record.boatclass }} {{ record.boattype }}{{ record.age }} {{ record.sex }} {{ record.weightcategory }} Time Distance Details 
              Details + {% if race.manager == request.user %} + + Disqualify + + {% else %} +   + {% endif %} +
              + + + + + + + + + + + + + + + + + + + +
              Rower:{{ record.username }}
              Name:{{ workout.name }}
              Date:{{ workout.date }}
              Time:{{ workout.starttime }}
              Distance:{{ workout.distance }}m
              Duration:{{ workout.duration |durationprint:"%H:%M:%S.%f" }}
              Type:{{ workout.workouttype }}
              Weight Category:{{ workout.weightcategory }}
              +

            • +
            • +

              Workout Summary

              + +

              +

              +        {{ workout.summary }}
              +      
              +

              +
            • + {% for graph in graphs %} +
            • + + {{ graph.filename }} + +
            • + {% endfor %} + {% if mapdiv %} +
            • +
              + + {{ mapdiv|safe }} + + + {{ mapscript|safe }} +
              +
            • + {% endif %} +
            • + + + + {{ interactiveplot |safe }} + + {{ the_div|safe }} + +
            • +
            • +

              + Yes, I want to reject this entry +

              +

              + Please select a reason and add a comment (mandatory). + Pressing 'Reject' disqualifies the submitted result. + An email will be sent to the rower. There is no "undo". +

              +

              +

              + + {{ form.as_table }} +
              +

              +

              + {% csrf_token %} + +

              +
              +
            • +
            + +{% endblock %} + +{% block sidebar %} +{% include 'menu_racing.html' %} +{% endblock %} diff --git a/rowers/templates/disqualificationemail.html b/rowers/templates/disqualificationemail.html new file mode 100644 index 00000000..1c7dca91 --- /dev/null +++ b/rowers/templates/disqualificationemail.html @@ -0,0 +1,37 @@ +{% extends "emailbase.html" %} +{% block body %} +

            Dear {{ username }},

            + +

            + Unfortunately, the result that you have submitted + for the virtual race {{ racename }} + has been rejected by the race organizer. +

            + +

            + The reason for the rejection was: {{ reason }}. +

            + +

            + The race organizer added the following explanation: +

            + +

            + {{ message }} +

            + +

            + The decision to reject your result is the sole responsibility of + the race organizer. If you disagree with the decision, please do contact + him or her. +

            + +

            + You are still registered for the race and can submit a result. +

            + +

            + Best Regards, the Rowsandall Team +

            +{% endblock %} + diff --git a/rowers/templates/indoorvirtualeventcreate.html b/rowers/templates/indoorvirtualeventcreate.html index 912d21fa..69970198 100644 --- a/rowers/templates/indoorvirtualeventcreate.html +++ b/rowers/templates/indoorvirtualeventcreate.html @@ -39,12 +39,18 @@
            • -
            • All times are local times in the time zone you select
            • -
            • Adding a contact phone number and email is not mandatory, but we - strongly recommend it.
            • -
            • If your event has a registration closure deadline, participants - have to enter (and can withdraw) before the registration closure time.
            • -
            • Participants can submit results until the evaluation closure time.
            • +

              All times are local times in the time zone you select

              +

              Adding a contact phone number and email is not mandatory, but we + strongly recommend it.

              +

              If your event has a registration closure deadline, participants + have to enter (and can withdraw) before the registration closure time.

              +

              Participants can submit results until the evaluation closure time.

              +

              Until one hour after evaluation closure time, the race organizer + can review and reject submitted results ("disqualification"). If + you as the race organizer intend to use this functionality, it + is strongly recommended that you fill out a contact email or phone + number. +

          • diff --git a/rowers/templates/virtualevent.html b/rowers/templates/virtualevent.html index 07dae4e3..e6a86fe2 100644 --- a/rowers/templates/virtualevent.html +++ b/rowers/templates/virtualevent.html @@ -140,7 +140,11 @@
          • + {% if race|is_final %} +

            Final Results

            + {% else %}

            Results

            + {% endif %}

            {% if results or dns %} @@ -184,7 +188,7 @@ Details - {% if race.manager == request.user %} + {% if race.manager == request.user and not race|is_final %} Disqualify @@ -287,11 +291,13 @@ up and manage small regattas.

            - On the water races are rowed on the course shown. You cannot submit results rowed + On the water races are rowed on the course shown. + You cannot submit results rowed on other bodies of water.

            - Indoor races are open for all, wherever you live. However, be aware of the + Indoor races are open for all, wherever you live. + However, be aware of the time zone for the race window.

            @@ -334,7 +340,12 @@ refereed or staffed to provide for participants safety. Individual participants are entirely responsible for their safety while participating in a virtual race. -

            +

            +

            + Until the evaluation closure time, the race organizer can + review and reject entries. If you are disqualified in this + way, you will receive an email with the reason. +

          diff --git a/rowers/templates/virtualeventcreate.html b/rowers/templates/virtualeventcreate.html index d2489440..131ce9d1 100644 --- a/rowers/templates/virtualeventcreate.html +++ b/rowers/templates/virtualeventcreate.html @@ -37,15 +37,17 @@
        • -

          -

            -
          • All times are local times in the race course time zone
          • -
          • Adding a contact phone number and email is not mandatory, but we - strongly recommend it.
          • -
          • If your event has a registration closure deadline, participants - have to enter (and can withdraw) before the registration closure time.
          • -
          • Participants can submit results until the evaluation closure time.
          • -
          +

          All times are local times in the race course time zone

          +

          Adding a contact phone number and email is not mandatory, but we + strongly recommend it.

          +

          If your event has a registration closure deadline, participants + have to enter (and can withdraw) before the registration closure time.

          +

          Participants can submit results until the evaluation closure time.

          +

          Until one hour after evaluation closure time, the race organizer + can review and reject submitted results ("disqualification"). If + you as the race organizer intend to use this functionality, it + is strongly recommended that you fill out a contact email or phone + number.

        diff --git a/rowers/templatetags/rowerfilters.py b/rowers/templatetags/rowerfilters.py index 0142f292..c9b81bd3 100644 --- a/rowers/templatetags/rowerfilters.py +++ b/rowers/templatetags/rowerfilters.py @@ -370,6 +370,14 @@ def is_past_due(self): def is_not_past_due(self): return datetime.date.today() <= self.date +@register.filter +def is_closed(race): + return race.evaluation_closure < timezone.now() + +@register.filter +def is_final(race): + return race.evaluation_closure < timezone.now()-datetime.timedelta(hours=1) + @register.filter def userurl(path,member): pattern = re.compile('user\/\d+') diff --git a/rowers/views.py b/rowers/views.py index 2b048024..307ba0de 100644 --- a/rowers/views.py +++ b/rowers/views.py @@ -49,7 +49,8 @@ from rowers.forms import ( VirtualRaceSelectForm,WorkoutRaceSelectForm,CourseSelectForm, RaceResultFilterForm,PowerIntervalUpdateForm,FlexAxesForm, FlexOptionsForm,DataFrameColumnsForm,OteWorkoutTypeForm, - MetricsForm, + MetricsForm,DisqualificationForm,disqualificationreasons, + disqualifiers ) from django.core.urlresolvers import reverse, reverse_lazy @@ -156,6 +157,7 @@ from rowers.tasks import handle_makeplot,handle_otwsetpower,handle_sendemailtcx, from rowers.tasks import ( handle_sendemail_unrecognized,handle_sendemailnewcomment, handle_sendemailsummary, + handle_send_disqualification_email, handle_sendemailfile, handle_sendemailkml, handle_sendemailnewresponse, handle_updatedps, @@ -15837,9 +15839,6 @@ def virtualevent_disqualify_view(request,raceid=0,recordid=0): r = getrower(request.user) - # datum moet voor race evaluation date zijn (ook in template controleren) - - try: race = VirtualRace.objects.get(id=raceid) except VirtualRace.DoesNotExist: @@ -15853,24 +15852,111 @@ def virtualevent_disqualify_view(request,raceid=0,recordid=0): else: recordobj = IndoorVirtualRaceResult - if timezone.now() > race.evaluation_closure: - try: - record = recordobj.objects.get(id=recordid) + # datum moet voor race evaluation date zijn (ook in template controleren) + try: + record = recordobj.objects.get(id=recordid) + except recordobj.DoesNotExist: + messages.error(request,"We couldn't find the record") + if timezone.now() > race.evaluation_closure+datetime.timedelta(hours=1): + messages.error(request,"The evaluation is already closed and the results are official") + url = reverse(virtualevent_view,kwargs={'id':raceid}) + + return HttpResponseRedirect(url) + + if request.method == 'POST': + form = DisqualificationForm(request.POST) + if form.is_valid(): + message = form.cleaned_data['message'] + reason = form.cleaned_data['reason'] + disqualifier = disqualifiers[reason] + + u = User.objects.get(id=record.userid) + name = record.username + + job = myqueue(queue,handle_send_disqualification_email, + u.email, name, + disqualifier,message,race.name) + messages.info(request,"We have invalidated the result for: "+str(record)) record.coursecompleted = False record.save() - print record.coursecompleted - except recordobj.DoesNotExist: - messages.error(request,"We couldn't find the record") + + url = reverse(virtualevent_view,kwargs={'id':raceid}) + + return HttpResponseRedirect(url) + else: - messages.error(request,"The evaluation is already closed and the results are official") + form = DisqualificationForm(request.POST) - url = reverse(virtualevent_view,kwargs={'id':raceid}) + workout = Workout.objects.get(id=record.workoutid) - return HttpResponseRedirect(url) + g = GraphImage.objects.filter(workout=workout).order_by("-creationdatetime") + for i in g: + try: + width,height = Image.open(i.filename).size + i.width = width + i.height = height + i.save() + except: + pass + + script, div = interactive_chart(record.workoutid) + + f1 = workout.csvfilename + rowdata = rdata(f1) + hascoordinates = 1 + if rowdata != 0: + try: + latitude = rowdata.df[' latitude'] + if not latitude.std(): + hascoordinates = 0 + except KeyError, AttributeError: + hascoordinates = 0 + else: + hascoordinates = 0 + + if hascoordinates: + mapscript, mapdiv = leaflet_chart(rowdata.df[' latitude'], + rowdata.df[' longitude'], + workout.name) + else: + mapscript = "" + mapdiv = "" + + breadcrumbs = [ + { + 'url':reverse(virtualevents_view), + 'name': 'Racing' + }, + { + 'url':reverse(virtualevent_view, + kwargs={'id':race.id}), + 'name': race.name + }, + { + 'url':reverse(virtualevent_disqualify_view, + kwargs={'raceid':raceid, + 'recordid':recordid}), + 'name': 'Disqualify Entry' + }, + ] + + + return render(request,"disqualification_view.html", + {'workout':workout, + 'active':'nav-racing', + 'graphs':g, + 'interactiveplot':script, + 'the_div':div, + 'mapscript':mapscript, + 'mapdiv':mapdiv, + 'form':form, + 'race':race, + 'record':record, + }) def virtualevent_view(request,id=0): From 1c79a03f276203b50c079d041bfb905acab0bf39 Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 28 Nov 2018 15:39:25 +0100 Subject: [PATCH 13/13] added explanation --- rowers/templates/indoorvirtualeventcreate.html | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/rowers/templates/indoorvirtualeventcreate.html b/rowers/templates/indoorvirtualeventcreate.html index 69970198..1492d6aa 100644 --- a/rowers/templates/indoorvirtualeventcreate.html +++ b/rowers/templates/indoorvirtualeventcreate.html @@ -45,12 +45,16 @@

        If your event has a registration closure deadline, participants have to enter (and can withdraw) before the registration closure time.

        Participants can submit results until the evaluation closure time.

        -

        Until one hour after evaluation closure time, the race organizer - can review and reject submitted results ("disqualification"). If - you as the race organizer intend to use this functionality, it - is strongly recommended that you fill out a contact email or phone - number. -

        +

        Until one hour after evaluation closure time, the race organizer + can review and reject submitted results ("disqualification"). If + you as the race organizer intend to use this functionality, it + is strongly recommended that you fill out a contact email or phone + number. +

        +

        + If you require a screenshot of the PM monitor, do mention this + in the comment. +