diff --git a/docs/confreg/callforpapers.md b/docs/confreg/callforpapers.md index b2194a12..e7bdb472 100644 --- a/docs/confreg/callforpapers.md +++ b/docs/confreg/callforpapers.md @@ -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 diff --git a/docs/confreg/configuring.md b/docs/confreg/configuring.md index 3a266848..dae15d1a 100644 --- a/docs/confreg/configuring.md +++ b/docs/confreg/configuring.md @@ -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 diff --git a/postgresqleu/confreg/backendforms.py b/postgresqleu/confreg/backendforms.py index f3b4f6a6..70cfdc14 100644 --- a/postgresqleu/confreg/backendforms.py +++ b/postgresqleu/confreg/backendforms.py @@ -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', ] @@ -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']}, diff --git a/postgresqleu/confreg/migrations/0116_conference_scoring_method.py b/postgresqleu/confreg/migrations/0116_conference_scoring_method.py new file mode 100644 index 00000000..d2959ab1 --- /dev/null +++ b/postgresqleu/confreg/migrations/0116_conference_scoring_method.py @@ -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'), + ), + ] diff --git a/postgresqleu/confreg/models.py b/postgresqleu/confreg/models.py index 860449af..b24f6d6b 100644 --- a/postgresqleu/confreg/models.py +++ b/postgresqleu/confreg/models.py @@ -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. @@ -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.") diff --git a/postgresqleu/confreg/views.py b/postgresqleu/confreg/views.py index 3ba7eb80..41729554 100644 --- a/postgresqleu/confreg/views.py +++ b/postgresqleu/confreg/views.py @@ -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 @@ -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 @@ -2925,6 +2947,7 @@ def talkvote(request, confname): 'urlfilter': urltrackfilter + urlstatusfilter, 'helplink': 'callforpapers', 'options': options, + 'scoring_method': SCORING_METHOD_CHOICES[conference.scoring_method][1], }) diff --git a/template/confreg/sessionvotes.html b/template/confreg/sessionvotes.html index 2a2ddccd..e7211acd 100644 --- a/template/confreg/sessionvotes.html +++ b/template/confreg/sessionvotes.html @@ -245,6 +245,9 @@
Scoring method: {{ scoring_method }}
+