Skip to content

Commit

Permalink
Add "Olympic scoring" method.
Browse files Browse the repository at this point in the history
If there are more than two votes, then one copy each of the highest and lowest
scores is removed before averaging.
  • Loading branch information
xocolatl committed Oct 10, 2024
1 parent e0e5f7c commit a2d79c1
Show file tree
Hide file tree
Showing 7 changed files with 80 additions and 9 deletions.
13 changes: 13 additions & 0 deletions docs/confreg/callforpapers.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,19 @@ Click the status to bring up a dialog allowing the change of status.
Sessions can be sorted by session name (default), speakers or average
score by clicking the appropriate headlines.

#### 2.1 Scoring method

There are two methods for calculating the average score of a
proposal:

- Average — This is the standard average of all the scores.
- Olympic average — This method removes one instance each of the
maximum and minimum of the scores before averaging. This helps
prevent both favoritism and also sabotage.

The method used by the conference is set by the administrator and
shown at the top of the voting page.

### 3. Deciding and notifying speakers

Once the voting is done, the decisions can be made and the speakers be
Expand Down
8 changes: 8 additions & 0 deletions docs/confreg/configuring.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@ You can also decide if you want talkvoters to be able to see how others
voted and the overall average vote. Usually this would be off until
everyone has finished voting.

There are two methods for calculating the average score of a
proposal:

- Average — This is the standard average of all the scores.
- Olympic average — This method removes one instance each of the
maximum and minimum of the scores before averaging. This helps
prevent both favoritism and also sabotage.

### Roles

There are four types of roles that can be configured at the level of
Expand Down
4 changes: 2 additions & 2 deletions postgresqleu/confreg/backendforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ class Meta:
'schedulewidth', 'pixelsperminute', 'notifyregs', 'notifysessionstatus', 'notifyvolunteerstatus',
'testers', 'talkvoters', 'staff', 'volunteers', 'checkinprocessors',
'asktshirt', 'askfood', 'asknick', 'asktwitter', 'askbadgescan', 'askshareemail', 'askphotoconsent',
'callforpapersmaxsubmissions', 'skill_levels', 'showvotes', 'callforpaperstags', 'callforpapersrecording', 'sendwelcomemail',
'callforpapersmaxsubmissions', 'skill_levels', 'showvotes', 'scoring_method', 'callforpaperstags', 'callforpapersrecording', 'sendwelcomemail',
'tickets', 'confirmpolicy', 'queuepartitioning', 'invoice_autocancel_hours', 'attendees_before_waitlist',
'transfer_cost', 'initial_common_countries', 'jinjaenabled', 'dynafields', 'scannerfields',
'videoproviders', ]
Expand All @@ -106,7 +106,7 @@ def fix_fields(self):
{'id': 'twitter', 'legend': 'Twitter settings', 'fields': ['twitter_timewindow_start', 'twitter_timewindow_end', 'twitter_postpolicy', ]},
{'id': 'fields', 'legend': 'Registration fields', 'fields': ['asktshirt', 'askfood', 'asknick', 'asktwitter', 'askbadgescan', 'askshareemail', 'askphotoconsent', 'dynafields', 'scannerfields', ]},
{'id': 'steps', 'legend': 'Steps', 'fields': ['registrationopen', 'registrationtimerange', 'allowedit', 'callforpapersopen', 'callforpaperstimerange', 'callforsponsorsopen', 'callforsponsorstimerange', 'scheduleactive', 'tbdinschedule', 'sessionsactive', 'cardsactive', 'checkinactive', 'conferencefeedbackopen', 'feedbackopen']},
{'id': 'callforpapers', 'legend': 'Call for papers', 'fields': ['callforpapersmaxsubmissions', 'skill_levels', 'callforpaperstags', 'callforpapersrecording', 'showvotes']},
{'id': 'callforpapers', 'legend': 'Call for papers', 'fields': ['callforpapersmaxsubmissions', 'skill_levels', 'callforpaperstags', 'callforpapersrecording', 'showvotes', 'scoring_method']},
{'id': 'roles', 'legend': 'Roles', 'fields': ['testers', 'talkvoters', 'staff', 'volunteers', 'checkinprocessors', ]},
{'id': 'display', 'legend': 'Display', 'fields': ['jinjaenabled', 'videoproviders', ]},
{'id': 'legacy', 'legend': 'Legacy', 'fields': ['schedulewidth', 'pixelsperminute']},
Expand Down
18 changes: 18 additions & 0 deletions postgresqleu/confreg/migrations/0116_conference_scoring_method.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.2.16 on 2024-09-11 14:08

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('confreg', '0115_speaker_photo_hashvals'),
]

operations = [
migrations.AddField(
model_name='conference',
name='scoring_method',
field=models.IntegerField(choices=[(0, 'Average'), (1, 'Olympic Average')], default=0, verbose_name='Scoring method'),
),
]
6 changes: 6 additions & 0 deletions postgresqleu/confreg/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@
(4, "Volunteers and admins can post without approval"),
)

SCORING_METHOD_CHOICES = (
(0, "Average"),
(1, "Olympic Average"),
)

# NOTE! The contents of these arrays must also be matched with the
# database table confreg_status_strings. This one is managed by
# manually creating a separate migration in case the contents change.
Expand Down Expand Up @@ -209,6 +214,7 @@ class Conference(models.Model):
callforpaperstags = models.BooleanField(blank=False, null=False, default=False, verbose_name='Use tags')
callforpapersrecording = models.BooleanField(blank=False, null=False, default=False, verbose_name='Ask for recording consent')
showvotes = models.BooleanField(blank=False, null=False, default=False, verbose_name="Show votes", help_text="Show other people's votes on the talkvote page")
scoring_method = models.IntegerField(blank=False, null=False, default=0, choices=SCORING_METHOD_CHOICES, verbose_name="Scoring method")

sendwelcomemail = models.BooleanField(blank=False, null=False, default=False, verbose_name="Send welcome email", help_text="Send an email to attendees once their registration is completed.")
tickets = models.BooleanField(blank=False, null=False, default=False, verbose_name="Use tickets", help_text="Generate and send tickets to all attendees once their registration is completed.")
Expand Down
37 changes: 30 additions & 7 deletions postgresqleu/confreg/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
from .models import PendingAdditionalOrder
from .models import RegistrationWaitlistEntry, RegistrationWaitlistHistory
from .models import RegistrationTransferPending
from .models import STATUS_CHOICES
from .models import STATUS_CHOICES, SCORING_METHOD_CHOICES
from .models import ConferenceNews, ConferenceTweetQueue
from .models import SavedReportDefinition
from .models import ConferenceMessaging
Expand Down Expand Up @@ -2877,12 +2877,34 @@ def talkvote(request, confname):
WHERE cs.conferencesession_id=s.id
) speakers ON true
LEFT JOIN LATERAL (
SELECT avg(vote) FILTER (WHERE vote > 0)::numeric(3,2) AS avg,
jsonb_object_agg(username, vote) AS votes,
jsonb_object_agg(username, comment) FILTER (WHERE comment IS NOT NULL AND comment != '') AS comments
FROM confreg_conferencesessionvote
INNER JOIN auth_user ON auth_user.id=voter_id
WHERE session_id=s.id
WITH
aggs (votes, comments, avg, sum, count, min, max) AS (
SELECT
jsonb_object_agg(username, vote),
jsonb_object_agg(username, comment) FILTER (WHERE comment > ''),
AVG(vote) FILTER (WHERE vote > 0),
SUM(vote) FILTER (WHERE vote > 0),
COUNT(*) FILTER (WHERE vote > 0),
MIN(vote) FILTER (WHERE vote > 0),
MAX(vote) FILTER (WHERE vote > 0)
FROM confreg_conferencesessionvote
INNER JOIN auth_user ON auth_user.id=voter_id
WHERE session_id=s.id
)
SELECT
CASE (SELECT scoring_method FROM confreg_conference WHERE id = %(confid)s)
WHEN 0 /* Average */
THEN avg
WHEN 1 /* Olympic average */
THEN CASE WHEN count > 2
THEN (sum - max - min) / (count - 2)
ELSE avg
END
END::numeric(3,2) AS avg,
votes,
comments
FROM aggs
) votes ON true
WHERE s.conference_id=%(confid)s AND
(COALESCE(s.track_id,0)=ANY(%(tracks)s)) AND
Expand Down Expand Up @@ -2925,6 +2947,7 @@ def talkvote(request, confname):
'urlfilter': urltrackfilter + urlstatusfilter,
'helplink': 'callforpapers',
'options': options,
'scoring_method': SCORING_METHOD_CHOICES[conference.scoring_method][1],
})


Expand Down
3 changes: 3 additions & 0 deletions template/confreg/sessionvotes.html
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,9 @@ <h1>Vote for sessions - {{conference}}</h1>
{%if not hasvoters%}
<div class="alert alert-warning">There are no talkvoters configured on this conference! List of talks will be empty!</div>
{%endif%}

<p><b>Scoring method:</b> {{ scoring_method }}</p>

<table id="votetable" class="table table-bordered table-condensed" style="display:none">
<tr>
<th style="width: 1%">Seq</th>
Expand Down

0 comments on commit a2d79c1

Please sign in to comment.