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 @@
{{ cellData.checked }}
-
+
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