From 774028497b6111d976dbdcc5a2db645cc1cea4da Mon Sep 17 00:00:00 2001 From: Sander Roosendaal Date: Wed, 28 Nov 2018 15:37:41 +0100 Subject: [PATCH] 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. +

    +
  • +
  • + + + + + + + + + + + + + + + + + + + + +
    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):