diff --git a/.github/CHANGELOG.rst b/.github/CHANGELOG.rst index ad8c7314285..92003392b61 100644 --- a/.github/CHANGELOG.rst +++ b/.github/CHANGELOG.rst @@ -2,6 +2,19 @@ Change Log ========== +2.7.1 +----- +*Release date: TBD* + +- Escaped values in tables to avoid malicious data +- Fixed crash on loading email dialog for team draws +- Fixed team standing emails not being sent +- Fixed sorting by venue name or priority in the allocator +- Fixed adjudicator private URLs not loading +- Adjudicator feedback tables now properly sortable by number of feedback +- Checkboxes no longer overlap with table headers + + 2.7.0 (Pixie-bob) --------- *Release date: 1 October 2022* diff --git a/docs/conf.py b/docs/conf.py index bd2224d0b86..282267c3181 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -60,7 +60,7 @@ # The short X.Y version. version = '2.7' # The full version, including alpha/beta/rc tags. -release = '2.7.0-dev' +release = '2.7.1' rst_epilog = """ .. |vrelease| replace:: v{release} diff --git a/tabbycat/adjallocation/utils.py b/tabbycat/adjallocation/utils.py index 3995d4dda43..7d06bba1774 100644 --- a/tabbycat/adjallocation/utils.py +++ b/tabbycat/adjallocation/utils.py @@ -1,6 +1,7 @@ import math from itertools import combinations, product +from django.utils.html import escape from django.utils.translation import gettext as _ from participants.models import Adjudicator, Team @@ -27,16 +28,16 @@ def adjudicator_conflicts_display(debates): conflict_messages[debate].append(("danger", _( "Conflict: %(adjudicator)s & %(team)s " "(personal)", - ) % {'adjudicator': adj.name, 'team': team.short_name})) + ) % {'adjudicator': escape(adj.name), 'team': escape(team.short_name)})) for institution in conflicts.conflicting_institutions_adj_team(adj, team): conflict_messages[debate].append(("danger", _( "Conflict: %(adjudicator)s & %(team)s " "via institution %(institution)s", ) % { - 'adjudicator': adj.name, - 'team': team.short_name, - 'institution': institution.code, + 'adjudicator': escape(adj.name), + 'team': escape(team.short_name), + 'institution': escape(institution.code), })) for adj1, adj2 in combinations(debate.adjudicators.all(), 2): @@ -45,16 +46,16 @@ def adjudicator_conflicts_display(debates): conflict_messages[debate].append(("danger", _( "Conflict: %(adjudicator1)s & %(adjudicator2)s " "(personal)", - ) % {'adjudicator1': adj1.name, 'adjudicator2': adj2.name})) + ) % {'adjudicator1': escape(adj1.name), 'adjudicator2': escape(adj2.name)})) for institution in conflicts.conflicting_institutions_adj_adj(adj1, adj2): conflict_messages[debate].append(("warning", _( "Conflict: %(adjudicator1)s & %(adjudicator2)s " "via institution %(institution)s", ) % { - 'adjudicator1': adj1.name, - 'adjudicator2': adj2.name, - 'institution': institution.code, + 'adjudicator1': escape(adj1.name), + 'adjudicator2': escape(adj2.name), + 'institution': escape(institution.code), })) return conflict_messages diff --git a/tabbycat/adjfeedback/tables.py b/tabbycat/adjfeedback/tables.py index 2e393041d23..38b9818f3b1 100644 --- a/tabbycat/adjfeedback/tables.py +++ b/tabbycat/adjfeedback/tables.py @@ -63,11 +63,13 @@ def add_base_score_columns(self, adjudicators, editable=False): 'modal': adj.id, 'class': 'edit-base-score', 'tooltip': _("Click to edit base score"), + 'sort': adj.base_score, } for adj in adjudicators] else: test_data = [{ 'text': self.get_formatted_adj_score(adj.base_score), 'tooltip': _("Assigned base score"), + 'sort': adj.base_score, } for adj in adjudicators] self.add_column(test_header, test_data) @@ -127,7 +129,7 @@ def add_feedback_link_columns(self, adjudicators): len(adj.feedback_data) - 1, ) % {'count': len(adj.feedback_data) - 1}, # -1 to account for base score 'class': 'view-feedback', - 'sort': adj.debates, + 'sort': len(adj.feedback_data) - 1, 'link': reverse_tournament('adjfeedback-view-on-adjudicator', self.tournament, kwargs={'pk': adj.pk}), } for adj in adjudicators] self.add_column(link_head, link_cell) diff --git a/tabbycat/adjfeedback/templates/feedback_card.html b/tabbycat/adjfeedback/templates/feedback_card.html index d2a9c0ebf55..6b04629b139 100644 --- a/tabbycat/adjfeedback/templates/feedback_card.html +++ b/tabbycat/adjfeedback/templates/feedback_card.html @@ -48,7 +48,7 @@
{% if feedback.source_adjudicator %} - {% person_display_name feedback.source_adjudicator tournament as source %} + {% person_display_name feedback.source_adjudicator.adjudicator as source %} {% blocktrans trimmed with source=source relationship=feedback.source_adjudicator.get_type_display %} From {{ source }} (their {{ relationship }}) {% endblocktrans %} diff --git a/tabbycat/adjfeedback/views.py b/tabbycat/adjfeedback/views.py index 665745bc955..d29c121a283 100644 --- a/tabbycat/adjfeedback/views.py +++ b/tabbycat/adjfeedback/views.py @@ -7,6 +7,7 @@ from django.db.models import Count, F, Q from django.http import HttpResponse, JsonResponse from django.utils import timezone +from django.utils.html import conditional_escape, escape from django.utils.translation import gettext as _, gettext_lazy, ngettext, ngettext_lazy from django.views.generic.base import TemplateView, View from django.views.generic.edit import FormView @@ -151,6 +152,7 @@ def get_table(self): count = adj.feedback_count feedback_data.append({ 'text': ngettext("%(count)d feedback", "%(count)d feedbacks", count) % {'count': count}, + 'sort': count, 'link': reverse_tournament('adjfeedback-view-on-adjudicator', self.tournament, kwargs={'pk': adj.id}), }) table.add_column({'key': 'feedbacks', 'title': _("Feedbacks")}, feedback_data) @@ -175,6 +177,7 @@ def get_tables(self): count = team.feedback_count team_feedback_data.append({ 'text': ngettext("%(count)d feedback", "%(count)d feedbacks", count) % {'count': count}, + 'sort': count, 'link': reverse_tournament('adjfeedback-view-from-team', tournament, kwargs={'pk': team.id}), @@ -190,6 +193,7 @@ def get_tables(self): count = adj.feedback_count adj_feedback_data.append({ 'text': ngettext("%(count)d feedback", "%(count)d feedbacks", count) % {'count': count}, + 'sort': count, 'link': reverse_tournament('adjfeedback-view-from-adjudicator', tournament, kwargs={'pk': adj.id}), @@ -361,7 +365,7 @@ def get_tables(self): use_code_names = use_team_code_names_data_entry(self.tournament, self.tabroom) teams_table = TabbycatTableBuilder(view=self, sort_key="team", title=_("A Team")) add_link_data = [{ - 'text': team_name_for_data_entry(team, use_code_names), + 'text': conditional_escape(team_name_for_data_entry(team, use_code_names)), 'link': self.get_from_team_link(team), } for team in tournament.team_set.all()] header = {'key': 'team', 'title': _("Team")} @@ -372,13 +376,13 @@ def get_tables(self): 'key': 'institution', 'icon': 'home', 'tooltip': _("Institution"), - }, [team.institution.code if team.institution else TabbycatTableBuilder.BLANK_TEXT for team in tournament.team_set.all()]) + }, [escape(team.institution.code) if team.institution else TabbycatTableBuilder.BLANK_TEXT for team in tournament.team_set.all()]) adjs_table = TabbycatTableBuilder(view=self, sort_key="adjudicator", title=_("An Adjudicator")) adjudicators = tournament.adjudicator_set.all() add_link_data = [{ - 'text': adj.get_public_name(tournament), + 'text': escape(adj.get_public_name(tournament)), 'link': self.get_from_adj_link(adj), } for adj in adjudicators] header = {'key': 'adjudicator', 'title': _("Adjudicator")} @@ -389,7 +393,7 @@ def get_tables(self): 'key': 'institution', 'icon': 'home', 'tooltip': _("Institution"), - }, [adj.institution.code if adj.institution else TabbycatTableBuilder.BLANK_TEXT for adj in adjudicators]) + }, [escape(adj.institution.code) if adj.institution else TabbycatTableBuilder.BLANK_TEXT for adj in adjudicators]) return [teams_table, adjs_table] diff --git a/tabbycat/availability/views.py b/tabbycat/availability/views.py index 6e09b000969..09a4a3e382e 100644 --- a/tabbycat/availability/views.py +++ b/tabbycat/availability/views.py @@ -8,6 +8,7 @@ from django.db.models import Min from django.db.models.functions import Coalesce from django.http import JsonResponse +from django.utils.html import escape from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy, ngettext from django.views.generic.base import TemplateView, View @@ -188,7 +189,7 @@ def get_table(self): } for inst in queryset]) if self.round.prev: - title = _("Active in %(prev_round)s") % {'prev_round': self.round.prev.abbreviation} + title = _("Active in %(prev_round)s") % {'prev_round': escape(self.round.prev.abbreviation)} table.add_column({'key': 'active-prev', 'title': title}, [{ 'sort': inst.prev_available, 'icon': 'check' if inst.prev_available else '', diff --git a/tabbycat/breakqual/views.py b/tabbycat/breakqual/views.py index 5059ba243d2..2c46434128d 100644 --- a/tabbycat/breakqual/views.py +++ b/tabbycat/breakqual/views.py @@ -6,6 +6,7 @@ from django.db.models import Count, Q from django.forms import HiddenInput from django.forms.models import BaseModelFormSet +from django.utils.html import escape from django.utils.translation import gettext as _, ngettext from django.views.generic import FormView, TemplateView @@ -61,7 +62,7 @@ def get_standings(self): def get_table(self): self.standings = self.get_standings() - table = TabbycatTableBuilder(view=self, title=self.object.name, sort_key='Rk') + table = TabbycatTableBuilder(view=self, title=escape(self.object.name), sort_key='Rk') table.add_ranking_columns(self.standings) table.add_column({'title': _("Break"), 'key': 'break'}, [tsi.break_rank for tsi in self.standings]) @@ -291,7 +292,7 @@ def get_table(self): break_categories = t.breakcategory_set.order_by('seq') for bc in break_categories: - table.add_column({'title': bc.name, 'key': bc.slug}, [{ + table.add_column({'title': escape(bc.name), 'key': escape(bc.slug)}, [{ 'component': 'check-cell', 'checked': True if bc in team.break_categories.all() else False, 'sort': True if bc in team.break_categories.all() else False, @@ -301,13 +302,13 @@ def get_table(self): # Provide list of members within speaker categories for convenient entry for sc in speaker_categories: - table.add_column({'title': _('%s Speakers') % sc.name, 'key': sc.name + "_speakers"}, [{ + table.add_column({'title': _('%s Speakers') % escape(sc.name), 'key': escape(sc.name) + "_speakers"}, [{ 'text': getattr(team, 'nspeakers_%s' % sc.slug, 'N/A'), 'tooltip': ngettext( 'Team has %(nspeakers)s speaker with the %(category)s speaker category assigned', 'Team has %(nspeakers)s speakers with the %(category)s speaker category assigned', getattr(team, 'nspeakers_%s' % sc.slug, 0), - ) % {'nspeakers': getattr(team, 'nspeakers_%s' % sc.slug, 'N/A'), 'category': sc.name}, + ) % {'nspeakers': getattr(team, 'nspeakers_%s' % sc.slug, 'N/A'), 'category': escape(sc.name)}, } for team in teams]) return table diff --git a/tabbycat/draw/views.py b/tabbycat/draw/views.py index f0caff66ad9..366bfffba8a 100644 --- a/tabbycat/draw/views.py +++ b/tabbycat/draw/views.py @@ -7,7 +7,7 @@ from django.db.models import OuterRef, Subquery from django.http import HttpResponseBadRequest, HttpResponseRedirect from django.utils.functional import cached_property -from django.utils.html import format_html +from django.utils.html import escape, format_html from django.utils.safestring import mark_safe from django.utils.timezone import get_current_timezone_name from django.utils.translation import gettext as _ @@ -430,7 +430,7 @@ class EmailTeamAssignmentsView(RoundTemplateEmailCreateView): round_redirect_pattern_name = 'draw-display' def get_queryset(self): - return Speaker.objects.filter(team__debateteam__round=self.round).select_related('team') + return Speaker.objects.filter(team__debateteam__debate__round=self.round).select_related('team') # ============================================================================== @@ -806,7 +806,7 @@ def get_table(self): table = TabbycatTableBuilder(view=self) table.add_team_columns(teams) - headers = [round.abbreviation for round in rounds] + headers = [escape(round.abbreviation) for round in rounds] data = [[tsas.get((team.id, round.seq), "—") for round in rounds] for team in teams] table.add_columns(headers, data) diff --git a/tabbycat/notifications/utils.py b/tabbycat/notifications/utils.py index ecebe42ce9e..99c1404ea27 100644 --- a/tabbycat/notifications/utils.py +++ b/tabbycat/notifications/utils.py @@ -161,7 +161,7 @@ def standings_email_generator(to, url, round): for team in teams: context_team = context.copy() - context_team['POINTS'] = str(team._points) + context_team['POINTS'] = str(team.points_count) context_team['TEAM'] = team.short_name for speaker in team.speaker_set.all(): diff --git a/tabbycat/notifications/views.py b/tabbycat/notifications/views.py index 142446658c2..dd286bccb58 100644 --- a/tabbycat/notifications/views.py +++ b/tabbycat/notifications/views.py @@ -11,6 +11,7 @@ from django.http import HttpResponse from django.urls import reverse_lazy from django.utils import formats, timezone +from django.utils.html import escape from django.utils.translation import gettext as _, gettext_lazy, ngettext from django.views.generic.base import View from django.views.generic.edit import FormView @@ -140,8 +141,8 @@ def get_tables(self): emails_time = [] for sentmessage in notification.sentmessage_set.all(): - emails_recipient.append(sentmessage.recipient.name if sentmessage.recipient else self.UNKNOWN_RECIPIENT_CELL) - emails_addresses.append(sentmessage.email or self.UNKNOWN_RECIPIENT_CELL) + emails_recipient.append(escape(sentmessage.recipient.name) if sentmessage.recipient else self.UNKNOWN_RECIPIENT_CELL) + emails_addresses.append(escape(sentmessage.email) or self.UNKNOWN_RECIPIENT_CELL) if len(sentmessage.statuses) > 0: latest_status = sentmessage.statuses[0] # already ordered @@ -258,12 +259,12 @@ def get_table(self, mixed_participants=False): } for p in queryset]) table.add_column({'key': 'name', 'tooltip': _("Participant"), 'icon': 'user'}, [{ - 'text': p.name, + 'text': escape(p.name), 'class': 'no-wrap' if len(p.name) < 20 else '', } for p in queryset]) table.add_column({'key': 'email', 'tooltip': _("Email address"), 'icon': 'mail'}, [{ - 'text': p.email if p.email else _("Not Provided"), + 'text': escape(p.email) if p.email else _("Not Provided"), 'class': 'small' if p.email else 'small text-warning', } for p in queryset]) diff --git a/tabbycat/participants/models.py b/tabbycat/participants/models.py index 701d99741a2..ef7bdcf4346 100644 --- a/tabbycat/participants/models.py +++ b/tabbycat/participants/models.py @@ -280,11 +280,9 @@ def points_count(self): try: return self._points except AttributeError: - from results.models import TeamScore from standings.teams import PointsMetricAnnotator - self._points = TeamScore.objects.filter( - ballot_submission__confirmed=True, - debate_team__team=self, + self._points = self.__class__.objects.filter( + id=self.id, ).aggregate(p=Coalesce(PointsMetricAnnotator().get_annotation(), Value(0)))['p'] return self._points diff --git a/tabbycat/participants/tables.py b/tabbycat/participants/tables.py index b90427b387f..cba6b8bb04d 100644 --- a/tabbycat/participants/tables.py +++ b/tabbycat/participants/tables.py @@ -1,4 +1,5 @@ from django.db.models import Prefetch +from django.utils.html import escape from django.utils.translation import gettext as _ from adjallocation.models import DebateAdjudicator @@ -34,7 +35,7 @@ def add_cumulative_team_points_column(self, teamscores): def add_speaker_scores_column(self, teamscores): data = [{ 'text': ", ".join([metricformat(ss.score) for ss in ts.debate_team.speaker_scores]) or "—", - 'tooltip': "
".join(["%s for %s" % (metricformat(ss.score), ss.speaker) for ss in ts.debate_team.speaker_scores]), + 'tooltip': "
".join(["%s for %s" % (metricformat(ss.score), escape(ss.speaker)) for ss in ts.debate_team.speaker_scores]), } for ts in teamscores] header = {'key': 'speaks', 'tooltip': _("Speaker scores
(in speaking order)"), 'text': _("Speaks")} self.add_column(header, data) diff --git a/tabbycat/participants/templates/current_round/round_adj.html b/tabbycat/participants/templates/current_round/round_adj.html index 607fa71056a..74586c1c606 100644 --- a/tabbycat/participants/templates/current_round/round_adj.html +++ b/tabbycat/participants/templates/current_round/round_adj.html @@ -3,7 +3,7 @@ {# Position, teams and room #}
- {% person_display_name adjudicator as adjudicator_name %} + {% person_display_name debateadjudicator.adjudicator as adjudicator_name %} {# (Two-team formats) #} {% if pref.teams_in_debate == 'two' %} diff --git a/tabbycat/participants/views.py b/tabbycat/participants/views.py index a03800230d4..80d5ba6e618 100644 --- a/tabbycat/participants/views.py +++ b/tabbycat/participants/views.py @@ -7,6 +7,7 @@ from django.db.models import Count, Prefetch, Q from django.forms import HiddenInput from django.http import JsonResponse +from django.utils.html import escape from django.utils.translation import gettext as _, gettext_lazy, ngettext from django.views.generic.base import View @@ -99,11 +100,11 @@ def get_table(self): ).distinct() table = TabbycatTableBuilder(view=self, sort_key='code') - table.add_column({'key': 'code', 'title': _("Code")}, [i.code for i in institutions]) - table.add_column({'key': 'name', 'title': _("Full name")}, [i.name for i in institutions]) + table.add_column({'key': 'code', 'title': _("Code")}, [escape(i.code) for i in institutions]) + table.add_column({'key': 'name', 'title': _("Full name")}, [escape(i.name) for i in institutions]) if any(i.region is not None for i in institutions): table.add_column({'key': 'region', 'title': _("Region")}, - [i.region.name if i.region else "—" for i in institutions]) + [escape(i.region.name) if i.region else "—" for i in institutions]) table.add_column({'key': 'nteams', 'title': _("Teams"), 'tooltip': _("Number of teams")}, [i.nteams for i in institutions]) table.add_column({'key': 'nadjs', 'title': _("Adjs"), @@ -141,7 +142,7 @@ def get_table(self): table = TabbycatTableBuilder(view=self, sort_key='code_name') table.add_column( {'key': 'code_name', 'title': _("Code name")}, - [{'text': t.code_name or "—"} for t in teams], + [{'text': escape(t.code_name) or "—"} for t in teams], ) table.add_team_columns(teams) return table @@ -380,7 +381,7 @@ def get_table(self): speaker_categories = self.tournament.speakercategory_set.all() for sc in speaker_categories: - table.add_column({'key': sc.name, 'title': sc.name}, [{ + table.add_column({'key': escape(sc.name), 'title': escape(sc.name)}, [{ 'component': 'check-cell', 'checked': True if sc in speaker.categories.all() else False, 'id': speaker.id, diff --git a/tabbycat/results/tables.py b/tabbycat/results/tables.py index 4ea895014a2..058b21d149b 100644 --- a/tabbycat/results/tables.py +++ b/tabbycat/results/tables.py @@ -1,3 +1,4 @@ +from django.utils.html import escape from django.utils.translation import gettext as _ from utils.misc import reverse_tournament @@ -63,7 +64,7 @@ def get_ballot_cells(self, debate, tournament, view_role, user): return { 'component': 'ballots-cell', 'ballots': [b.serialize(tournament) for b in ballotsubmissions], - 'current_user': user.username, + 'current_user': escape(user.username), 'acting_role': view_role, 'new_ballot': reverse_tournament(new_link, self.tournament, kwargs={'debate_id': debate.id}), diff --git a/tabbycat/results/views.py b/tabbycat/results/views.py index 2fd8f66bea5..03dc2c04904 100644 --- a/tabbycat/results/views.py +++ b/tabbycat/results/views.py @@ -10,6 +10,7 @@ from django.http import HttpResponseRedirect from django.shortcuts import render from django.utils import timezone +from django.utils.html import escape from django.utils.translation import gettext as _ from django.utils.translation import gettext_lazy from django.views.generic import FormView, TemplateView @@ -803,7 +804,7 @@ def get_table(self): table = TabbycatTableBuilder(view=self, sort_key='adj') data = [{ - 'text': _("Add result from %(adjudicator)s") % {'adjudicator': da.adjudicator.get_public_name(self.tournament)}, + 'text': _("Add result from %(adjudicator)s") % {'adjudicator': escape(da.adjudicator.get_public_name(self.tournament))}, 'link': reverse_round('old-results-public-ballotset-new-pk', self.round, kwargs={'adjudicator_pk': da.adjudicator_id}), } for da in debateadjs] diff --git a/tabbycat/settings/core.py b/tabbycat/settings/core.py index b74c3ebf686..eaebab5b99c 100644 --- a/tabbycat/settings/core.py +++ b/tabbycat/settings/core.py @@ -22,9 +22,9 @@ # Version # ============================================================================== -TABBYCAT_VERSION = '2.7.0-dev' +TABBYCAT_VERSION = '2.7.1' TABBYCAT_CODENAME = 'Pixie-bob' -READTHEDOCS_VERSION = 'v2.7.0' +READTHEDOCS_VERSION = 'v2.7.1' # ============================================================================== # Internationalization and Localization diff --git a/tabbycat/standings/views.py b/tabbycat/standings/views.py index 5f47eadeb2e..97d6908574a 100644 --- a/tabbycat/standings/views.py +++ b/tabbycat/standings/views.py @@ -4,7 +4,7 @@ from django.conf import settings from django.contrib import messages from django.db.models import Avg, Count, Prefetch -from django.utils.html import mark_safe +from django.utils.html import escape, mark_safe from django.utils.translation import gettext as _, gettext_lazy from django.views.generic.base import TemplateView @@ -248,7 +248,7 @@ def get_table(self): table.add_speaker_columns([info.speaker for info in standings]) table.add_team_columns([info.speaker.team for info in standings]) - scores_headers = [{'key': round.abbreviation, 'title': round.abbreviation} for round in rounds] + scores_headers = [{'key': escape(round.abbreviation), 'title': escape(round.abbreviation)} for round in rounds] scores_data = [[metricformat(x) if x is not None else '—' for x in standing.scores] for standing in standings] table.add_columns(scores_headers, scores_data) table.add_metric_columns(standings, integer_score_columns=self.integer_score_columns(rounds)) diff --git a/tabbycat/templates/allocations/DragAndDropUnallocatedItems.vue b/tabbycat/templates/allocations/DragAndDropUnallocatedItems.vue index baf351c3226..a605dbb66cb 100644 --- a/tabbycat/templates/allocations/DragAndDropUnallocatedItems.vue +++ b/tabbycat/templates/allocations/DragAndDropUnallocatedItems.vue @@ -87,7 +87,10 @@ export default { }, }, computed: { - filtedUnallocatedItems: function () { + isVenue: function () { + return this.unallocatedItems[0] && 'priority' in this.unallocatedItems[0] + }, + filteredUnallocatedItems: function () { return this.showUnavailable ? this.filteredAll : this.filteredAvailable }, filteredAll: function () { @@ -101,20 +104,30 @@ export default { return this[activeKey[0].property] }, sortedUnallocatedItemsByOrder: function () { - return this.filtedUnallocatedItems.slice(0).sort((itemA, itemB) => { - return itemA.vue_last_modified - itemB.vue_last_modified - }).reverse() + return this.filteredUnallocatedItems.slice(0).sort((itemA, itemB) => { + return itemB.vue_last_modified - itemA.vue_last_modified + }) }, sortedUnallocatedItemsByName: function () { // Note slice makes a copy so we are not mutating - return this.filtedUnallocatedItems.slice(0).sort((itemA, itemB) => { + if (this.isVenue) { + return this.filteredUnallocatedItems.slice(0).sort((itemA, itemB) => { + return itemA.display_name.localeCompare(itemB.display_name) + }) + } + return this.filteredUnallocatedItems.slice(0).sort((itemA, itemB) => { return itemA.name.localeCompare(itemB.name) }) }, sortedUnallocatedItemsByScore: function () { - return this.filtedUnallocatedItems.slice(0).sort((itemA, itemB) => { - return itemA.score - itemB.score - }).reverse() + if (this.isVenue) { + return this.filteredUnallocatedItems.slice(0).sort((itemA, itemB) => { + return itemB.priority - itemA.priority + }) + } + return this.filteredUnallocatedItems.slice(0).sort((itemA, itemB) => { + return itemB.score - itemA.score + }) }, }, methods: { diff --git a/tabbycat/templates/scss/modules/tables.scss b/tabbycat/templates/scss/modules/tables.scss index fcfb3aacf42..094b1ba9a7b 100644 --- a/tabbycat/templates/scss/modules/tables.scss +++ b/tabbycat/templates/scss/modules/tables.scss @@ -213,6 +213,10 @@ thead, } } +.table-check { + padding-left: 1.25rem; +} + //------------------------------------------------------------------------------ // MOBILE TWEAKS //------------------------------------------------------------------------------ diff --git a/tabbycat/templates/tables/CheckCell.vue b/tabbycat/templates/tables/CheckCell.vue index a5e6db8ad07..22e99ca17ba 100644 --- a/tabbycat/templates/tables/CheckCell.vue +++ b/tabbycat/templates/tables/CheckCell.vue @@ -5,7 +5,7 @@ -
+
diff --git a/tabbycat/utils/tables.py b/tabbycat/utils/tables.py index 8ad7606ceed..a2f412ff832 100644 --- a/tabbycat/utils/tables.py +++ b/tabbycat/utils/tables.py @@ -5,6 +5,7 @@ from django.db.models import Exists, OuterRef, Prefetch from django.template.loader import render_to_string from django.utils.encoding import force_str +from django.utils.html import escape from django.utils.translation import gettext as _ from django.utils.translation import ngettext @@ -216,28 +217,28 @@ def _use_team_code_names(self): def _team_short_name(self, team): """Returns the appropriate short name for the team, accounting for team code name preference.""" if self._use_team_code_names: - return team.code_name + return escape(team.code_name) else: - return team.short_name + return escape(team.short_name) def _team_long_name(self, team): """Returns the appropriate long name for the team, accounting for team code name preference.""" if self._use_team_code_names: - return team.code_name + return escape(team.code_name) else: - return team.long_name + return escape(team.long_name) def _adjudicator_record_link(self, adj, suffix=""): adj_short_name = adj.get_public_name(self.tournament).split(" ")[0] if self.admin: return { - 'text': _("View %(a)s's %(d)s Record") % {'a': adj_short_name, 'd': suffix}, + 'text': _("View %(a)s's %(d)s Record") % {'a': escape(adj_short_name), 'd': suffix}, 'link': reverse_tournament('participants-adjudicator-record', self.tournament, kwargs={'pk': adj.pk}), } elif self.tournament.pref('public_record'): return { - 'text': _("View %(a)s's %(d)s Record") % {'a': adj_short_name, 'd': suffix}, + 'text': _("View %(a)s's %(d)s Record") % {'a': escape(adj_short_name), 'd': suffix}, 'link': reverse_tournament('participants-public-adjudicator-record', self.tournament, kwargs={'pk': adj.pk}), } @@ -261,7 +262,7 @@ def _team_record_link(self, team): def _team_cell(self, team, show_emoji=False, subtext=None, highlight=False): cell = { 'text': self._team_short_name(team), - 'emoji': team.emoji if show_emoji and self.tournament.pref('show_emoji') else None, + 'emoji': escape(team.emoji) if show_emoji and self.tournament.pref('show_emoji') else None, 'sort': self._team_short_name(team), 'class': 'team-name no-wrap' if len(self._team_short_name(team)) < 18 else 'team-name', 'popover': {'title': self._team_long_name(team), 'content': []}, @@ -274,15 +275,15 @@ def _team_cell(self, team, show_emoji=False, subtext=None, highlight=False): if (self.tournament.pref('team_code_names') == 'all-tooltips' or # code names in tooltips (self.tournament.pref('team_code_names') == 'admin-tooltips-code' and self.admin)): - cell['popover']['content'].append({'text': _("Code name: %(name)s") % {'name': team.code_name}}) + cell['popover']['content'].append({'text': _("Code name: %(name)s") % {'name': escape(team.code_name)}}) if self.tournament.pref('team_code_names') == 'admin-tooltips-real' and self.admin: - cell['popover']['content'].append({'text': _("Real name: %(name)s") % {'name': team.short_name}}) + cell['popover']['content'].append({'text': _("Real name: %(name)s") % {'name': escape(team.short_name)}}) if self._show_speakers_in_draw: if self.admin: - speakers = ["%s" % s.name if s.anonymous else s.name for s in team.speakers] + speakers = ["%s" % escape(s.name) if s.anonymous else escape(s.name) for s in team.speakers] else: - speakers = [self.REDACTED_CELL['text'] if s.anonymous else s.get_public_name(self.tournament) for s in team.speakers] + speakers = [self.REDACTED_CELL['text'] if s.anonymous else escape(s.get_public_name(self.tournament)) for s in team.speakers] cell['popover']['content'].append({'text': ", ".join(speakers)}) if self._show_record_links: @@ -366,7 +367,7 @@ def _result_cell_two(self, ts, compress=False, show_score=False, show_ballots=Fa return {'text': self.BLANK_TEXT} opp = ts.debate_team.opponent.team - opp_vshort = '' + opp.emoji + '' if opp.emoji else "…" + opp_vshort = '' + escape(opp.emoji) + '' if opp.emoji else "…" cell = { 'text': _(" vs %(opposition)s") % {'opposition': opp_vshort if compress else self._team_short_name(opp)}, @@ -390,7 +391,7 @@ def _result_cell_two(self, ts, compress=False, show_score=False, show_ballots=Fa if self._show_speakers_in_draw: cell['popover']['content'].append({ - 'text': ", ".join([s.get_public_name(self.tournament) for s in opp.speakers]), + 'text': ", ".join([escape(s.get_public_name(self.tournament)) for s in opp.speakers]), }) if self._show_record_links: @@ -466,7 +467,7 @@ def add_tournament_column(self, tournaments): 'key': "tournament", 'icon': 'tag', 'tooltip': _("Tournament"), } data = [{ - 'sort': t.seq, 'text': t.short_name, 'tooltip': t.short_name, + 'sort': t.seq, 'text': escape(t.short_name), 'tooltip': escape(t.short_name), } for t in tournaments] self.add_column(header, data) @@ -475,7 +476,7 @@ def add_round_column(self, rounds): 'key': "round", 'icon': 'clock', 'tooltip': _("Round"), } data = [{ - 'sort': round.seq, 'text': round.abbreviation, 'tooltip': round.name, + 'sort': round.seq, 'text': escape(round.abbreviation), 'tooltip': escape(round.name), } for round in rounds] self.add_column(header, data) @@ -487,13 +488,13 @@ def add_adjudicator_columns(self, adjudicators, show_institutions=True, if adj.anonymous and not self.admin: adj_data.append(self.REDACTED_CELL) else: - cell = {'text': adj.get_public_name(self.tournament)} + cell = {'text': escape(adj.get_public_name(self.tournament))} if adj.anonymous: cell['class'] = 'admin-redacted' if self._show_record_links: - cell['popover'] = {'title': adj.get_public_name(self.tournament), 'content': [self._adjudicator_record_link(adj)]} + cell['popover'] = {'title': escape(adj.get_public_name(self.tournament)), 'content': [self._adjudicator_record_link(adj)]} if subtext == 'institution' and adj.institution is not None: - cell['subtext'] = adj.institution.code + cell['subtext'] = escape(adj.institution.code) adj_data.append(cell) self.add_column({'key': 'name', 'tooltip': _("Name"), 'icon': 'user'}, adj_data) @@ -502,7 +503,7 @@ def add_adjudicator_columns(self, adjudicators, show_institutions=True, 'key': "institution", 'icon': 'home', 'tooltip': _("Institution"), - }, [adj.institution.code if adj.institution else self.BLANK_TEXT for adj in adjudicators]) + }, [escape(adj.institution.code) if adj.institution else self.BLANK_TEXT for adj in adjudicators]) if show_metadata: adjcore_header = { @@ -534,7 +535,7 @@ def add_debate_adjudicators_column(self, debates, title="Adjudicators", def construct_text(adjs_data): adjs_list = [] for a in adjs_data: - adj_str = '' + a['adj'].get_public_name(self.tournament) + adj_str = '' + escape(a['adj'].get_public_name(self.tournament)) symbol = self.ADJ_SYMBOLS.get(a['position']) if symbol: adj_str += "%s" % symbol @@ -555,10 +556,10 @@ def construct_popover(adjs_data): descriptors.append(self.ADJ_POSITION_NAMES[a['position']]) if (for_admin or self.tournament.pref('show_adjudicator_institutions')) and \ a['adj'].institution is not None: - descriptors.append(a['adj'].institution.code) + descriptors.append(escape(a['adj'].institution.code)) if a.get('split', False): descriptors.append("" + _("in minority") + "") - text = a['adj'].get_public_name(self.tournament) + text = escape(a['adj'].get_public_name(self.tournament)) descriptors = " (%s)" % (", ".join(descriptors)) if descriptors else "" @@ -613,8 +614,8 @@ def add_debate_motion_column(self, debates): def add_motion_column(self, motions): motion_data = [{ - 'text': motion.reference if motion.reference else _('??'), - 'popover': {'content' : [{'text': motion.text}]}, + 'text': escape(motion.reference) if motion.reference else _('??'), + 'popover': {'content' : [{'text': escape(motion.text)}]}, } if motion else self.BLANK_TEXT for motion in motions] self.add_column({'key': "motion", 'title': _("Motion")}, motion_data) @@ -637,7 +638,7 @@ def add_team_columns(self, teams, show_break_categories=False, show_emoji=True, if show_break_categories and self.tournament.breakcategory_set.filter(is_general=False).exists(): self.add_column( {'key': 'categories', 'icon': 'user-check', 'tooltip': _("Categories")}, - [", ".join(bc.name for bc in getattr(team, 'break_categories_nongeneral', [])) + [", ".join(escape(bc.name) for bc in getattr(team, 'break_categories_nongeneral', [])) for team in teams], ) @@ -647,7 +648,7 @@ def add_team_columns(self, teams, show_break_categories=False, show_emoji=True, 'icon': 'home', 'tooltip': _("Institution"), }, [ - team.institution.code + escape(team.institution.code) if not getattr(team, 'anonymise', False) and team.institution is not None else self.BLANK_TEXT for team in teams ]) @@ -660,7 +661,7 @@ def add_speaker_columns(self, speakers, categories=True): speaker_data.append(self.REDACTED_CELL) else: cell = { - 'text': speaker.get_public_name(self.tournament), + 'text': escape(speaker.get_public_name(self.tournament)), 'class': 'no-wrap' if len(speaker.get_public_name(self.tournament)) < 20 else '', } if anonymous: @@ -680,9 +681,9 @@ def add_speaker_columns(self, speakers, categories=True): category_strs = [] for cat in speaker.categories.all(): if cat.public: - category_strs.append(cat.name) + category_strs.append(escape(cat.name)) elif self.admin: - category_strs.append("" + cat.name + "") + category_strs.append("" + escape(cat.name) + "") categories_data.append(", ".join(category_strs)) self.add_column({ @@ -698,7 +699,7 @@ def construct_venue_cell(venue): if not venue: return {'text': ''} - cell = {'text': venue.display_name, 'class': 'venue-name', 'link': venue.url} + cell = {'text': escape(venue.display_name), 'class': 'venue-name', 'link': escape(venue.url)} categories = venue.venuecategory_set.all() if not for_admin: @@ -708,7 +709,7 @@ def construct_venue_cell(venue): if len(categories) == 0: return cell - descriptions = [category.description.strip() for category in categories] + descriptions = [escape(category.description.strip()) for category in categories] descriptions = ["" + description + "" for description in descriptions if len(description) > 0] @@ -721,7 +722,7 @@ def construct_venue_cell(venue): 'last_predicate': descriptions[-1]} cell['popover'] = { - 'title': venue.display_name, + 'title': escape(venue.display_name), 'content': [{'text': categories_sentence}]} return cell @@ -757,8 +758,8 @@ def add_draw_conflicts_columns(self, debates, venue_conflicts, adjudicator_confl if len(set(institutions)) != len(institutions): conflicts.append(("warning", _("Teams are from the same institution"))) - conflicts.extend(adjudicator_conflicts[debate]) - conflicts.extend(venue_conflicts[debate]) + conflicts.extend(adjudicator_conflicts[debate]) # Escaped in adjallocation.utils + conflicts.extend(venue_conflicts[debate]) # Escaped in venues.utils conflicts_by_debate.append(conflicts) conflicts_header = {'title': _("Conflicts/Flags"), 'key': 'conflags'} @@ -890,11 +891,12 @@ def add_debate_side_by_team_column(self, teamscores, tournament=None): self.add_column(header, sides_data) def add_team_results_columns(self, teams, rounds): - """ Takes an iterable of Teams, assumes their round_results match rounds""" + """Takes an iterable of Teams, assumes their round_results match rounds""" for round_seq, round in enumerate(rounds): results = [self._result_cell( t.round_results[round_seq]) for t in teams] - header = {'key': 'r%d' % round_seq, 'title': round.abbreviation} + # Should the key be the round abbreviation (like for standings_results_columns)? + header = {'key': 'r%d' % round_seq, 'title': escape(round.abbreviation)} self.add_column(header, results) def add_debate_results_columns(self, debates, iron=False): @@ -961,7 +963,7 @@ def add_debate_postponement_column(self, debates): def add_standings_results_columns(self, standings, rounds, show_ballots): for round_seq, round in enumerate(rounds): - header = {'title': round.abbreviation, 'key': round.abbreviation} + header = {'title': escape(round.abbreviation), 'key': escape(round.abbreviation)} results = [self._result_cell( s.round_results[round_seq], compress=True, diff --git a/tabbycat/venues/utils.py b/tabbycat/venues/utils.py index 241a0d8ee20..ecf742bb286 100644 --- a/tabbycat/venues/utils.py +++ b/tabbycat/venues/utils.py @@ -1,4 +1,5 @@ from django.contrib.contenttypes.models import ContentType +from django.utils.html import escape from django.utils.translation import gettext as _ from venues.models import VenueConstraint @@ -21,7 +22,7 @@ def _add_constraint_message(debate, instance, venue, success_message, failure_me return for constraint in constraints[key]: if constraint.category in venue.venuecategory_set.all(): - message_args['category'] = constraint.category.name + message_args['category'] = escape(constraint.category.name) conflict_messages[debate].append(("success", success_message % message_args)) return else: @@ -37,18 +38,18 @@ def _add_constraint_message(debate, instance, venue, success_message, failure_me _add_constraint_message(debate, team, venue, _("Room constraint of %(name)s met (%(category)s)"), _("Room does not meet any constraint of %(name)s"), - {'name': team.short_name}) + {'name': escape(team.short_name)}) if team.institution is not None: _add_constraint_message(debate, team.institution, venue, _("Room constraint of %(team)s met (%(category)s, via institution %(institution)s)"), _("Room does not meet any constraint of institution %(institution)s (%(team)s)"), - {'institution': team.institution.code, 'team': team.short_name}) + {'institution': escape(team.institution.code), 'team': escape(team.short_name)}) for adjudicator in debate.adjudicators.all(): _add_constraint_message(debate, adjudicator, venue, _("Room constraint of %(name)s met (%(category)s)"), _("Room does not meet any constraint of %(name)s"), - {'name': adjudicator.name}) + {'name': escape(adjudicator.name)}) return conflict_messages