Skip to content

Commit 3e318f8

Browse files
committed
Merge branch 'hotfix/2.7.1'
2 parents 0e693f2 + e6d20ac commit 3e318f8

File tree

24 files changed

+140
-95
lines changed

24 files changed

+140
-95
lines changed

.github/CHANGELOG.rst

+13
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,19 @@
22
Change Log
33
==========
44

5+
2.7.1
6+
-----
7+
*Release date: TBD*
8+
9+
- Escaped values in tables to avoid malicious data
10+
- Fixed crash on loading email dialog for team draws
11+
- Fixed team standing emails not being sent
12+
- Fixed sorting by venue name or priority in the allocator
13+
- Fixed adjudicator private URLs not loading
14+
- Adjudicator feedback tables now properly sortable by number of feedback
15+
- Checkboxes no longer overlap with table headers
16+
17+
518
2.7.0 (Pixie-bob)
619
---------
720
*Release date: 1 October 2022*

docs/conf.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
# The short X.Y version.
6161
version = '2.7'
6262
# The full version, including alpha/beta/rc tags.
63-
release = '2.7.0-dev'
63+
release = '2.7.1'
6464

6565
rst_epilog = """
6666
.. |vrelease| replace:: v{release}

tabbycat/adjallocation/utils.py

+9-8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import math
22
from itertools import combinations, product
33

4+
from django.utils.html import escape
45
from django.utils.translation import gettext as _
56

67
from participants.models import Adjudicator, Team
@@ -27,16 +28,16 @@ def adjudicator_conflicts_display(debates):
2728
conflict_messages[debate].append(("danger", _(
2829
"Conflict: <strong>%(adjudicator)s</strong> & <strong>%(team)s</strong> "
2930
"(personal)",
30-
) % {'adjudicator': adj.name, 'team': team.short_name}))
31+
) % {'adjudicator': escape(adj.name), 'team': escape(team.short_name)}))
3132

3233
for institution in conflicts.conflicting_institutions_adj_team(adj, team):
3334
conflict_messages[debate].append(("danger", _(
3435
"Conflict: <strong>%(adjudicator)s</strong> & <strong>%(team)s</strong> "
3536
"via institution <strong>%(institution)s</strong>",
3637
) % {
37-
'adjudicator': adj.name,
38-
'team': team.short_name,
39-
'institution': institution.code,
38+
'adjudicator': escape(adj.name),
39+
'team': escape(team.short_name),
40+
'institution': escape(institution.code),
4041
}))
4142

4243
for adj1, adj2 in combinations(debate.adjudicators.all(), 2):
@@ -45,16 +46,16 @@ def adjudicator_conflicts_display(debates):
4546
conflict_messages[debate].append(("danger", _(
4647
"Conflict: <strong>%(adjudicator1)s</strong> & <strong>%(adjudicator2)s</strong> "
4748
"(personal)",
48-
) % {'adjudicator1': adj1.name, 'adjudicator2': adj2.name}))
49+
) % {'adjudicator1': escape(adj1.name), 'adjudicator2': escape(adj2.name)}))
4950

5051
for institution in conflicts.conflicting_institutions_adj_adj(adj1, adj2):
5152
conflict_messages[debate].append(("warning", _(
5253
"Conflict: <strong>%(adjudicator1)s</strong> & <strong>%(adjudicator2)s</strong> "
5354
"via institution <strong>%(institution)s</strong>",
5455
) % {
55-
'adjudicator1': adj1.name,
56-
'adjudicator2': adj2.name,
57-
'institution': institution.code,
56+
'adjudicator1': escape(adj1.name),
57+
'adjudicator2': escape(adj2.name),
58+
'institution': escape(institution.code),
5859
}))
5960

6061
return conflict_messages

tabbycat/adjfeedback/tables.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,13 @@ def add_base_score_columns(self, adjudicators, editable=False):
6363
'modal': adj.id,
6464
'class': 'edit-base-score',
6565
'tooltip': _("Click to edit base score"),
66+
'sort': adj.base_score,
6667
} for adj in adjudicators]
6768
else:
6869
test_data = [{
6970
'text': self.get_formatted_adj_score(adj.base_score),
7071
'tooltip': _("Assigned base score"),
72+
'sort': adj.base_score,
7173
} for adj in adjudicators]
7274

7375
self.add_column(test_header, test_data)
@@ -127,7 +129,7 @@ def add_feedback_link_columns(self, adjudicators):
127129
len(adj.feedback_data) - 1,
128130
) % {'count': len(adj.feedback_data) - 1}, # -1 to account for base score
129131
'class': 'view-feedback',
130-
'sort': adj.debates,
132+
'sort': len(adj.feedback_data) - 1,
131133
'link': reverse_tournament('adjfeedback-view-on-adjudicator', self.tournament, kwargs={'pk': adj.pk}),
132134
} for adj in adjudicators]
133135
self.add_column(link_head, link_cell)

tabbycat/adjfeedback/templates/feedback_card.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
</div>
4949
<div class="list-group-item h5">
5050
{% if feedback.source_adjudicator %}
51-
{% person_display_name feedback.source_adjudicator tournament as source %}
51+
{% person_display_name feedback.source_adjudicator.adjudicator as source %}
5252
{% blocktrans trimmed with source=source relationship=feedback.source_adjudicator.get_type_display %}
5353
From {{ source }} <span class="text-secondary small">(their {{ relationship }})</span>
5454
{% endblocktrans %}

tabbycat/adjfeedback/views.py

+8-4
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from django.db.models import Count, F, Q
88
from django.http import HttpResponse, JsonResponse
99
from django.utils import timezone
10+
from django.utils.html import conditional_escape, escape
1011
from django.utils.translation import gettext as _, gettext_lazy, ngettext, ngettext_lazy
1112
from django.views.generic.base import TemplateView, View
1213
from django.views.generic.edit import FormView
@@ -151,6 +152,7 @@ def get_table(self):
151152
count = adj.feedback_count
152153
feedback_data.append({
153154
'text': ngettext("%(count)d feedback", "%(count)d feedbacks", count) % {'count': count},
155+
'sort': count,
154156
'link': reverse_tournament('adjfeedback-view-on-adjudicator', self.tournament, kwargs={'pk': adj.id}),
155157
})
156158
table.add_column({'key': 'feedbacks', 'title': _("Feedbacks")}, feedback_data)
@@ -175,6 +177,7 @@ def get_tables(self):
175177
count = team.feedback_count
176178
team_feedback_data.append({
177179
'text': ngettext("%(count)d feedback", "%(count)d feedbacks", count) % {'count': count},
180+
'sort': count,
178181
'link': reverse_tournament('adjfeedback-view-from-team',
179182
tournament,
180183
kwargs={'pk': team.id}),
@@ -190,6 +193,7 @@ def get_tables(self):
190193
count = adj.feedback_count
191194
adj_feedback_data.append({
192195
'text': ngettext("%(count)d feedback", "%(count)d feedbacks", count) % {'count': count},
196+
'sort': count,
193197
'link': reverse_tournament('adjfeedback-view-from-adjudicator',
194198
tournament,
195199
kwargs={'pk': adj.id}),
@@ -361,7 +365,7 @@ def get_tables(self):
361365
use_code_names = use_team_code_names_data_entry(self.tournament, self.tabroom)
362366
teams_table = TabbycatTableBuilder(view=self, sort_key="team", title=_("A Team"))
363367
add_link_data = [{
364-
'text': team_name_for_data_entry(team, use_code_names),
368+
'text': conditional_escape(team_name_for_data_entry(team, use_code_names)),
365369
'link': self.get_from_team_link(team),
366370
} for team in tournament.team_set.all()]
367371
header = {'key': 'team', 'title': _("Team")}
@@ -372,13 +376,13 @@ def get_tables(self):
372376
'key': 'institution',
373377
'icon': 'home',
374378
'tooltip': _("Institution"),
375-
}, [team.institution.code if team.institution else TabbycatTableBuilder.BLANK_TEXT for team in tournament.team_set.all()])
379+
}, [escape(team.institution.code) if team.institution else TabbycatTableBuilder.BLANK_TEXT for team in tournament.team_set.all()])
376380

377381
adjs_table = TabbycatTableBuilder(view=self, sort_key="adjudicator", title=_("An Adjudicator"))
378382
adjudicators = tournament.adjudicator_set.all()
379383

380384
add_link_data = [{
381-
'text': adj.get_public_name(tournament),
385+
'text': escape(adj.get_public_name(tournament)),
382386
'link': self.get_from_adj_link(adj),
383387
} for adj in adjudicators]
384388
header = {'key': 'adjudicator', 'title': _("Adjudicator")}
@@ -389,7 +393,7 @@ def get_tables(self):
389393
'key': 'institution',
390394
'icon': 'home',
391395
'tooltip': _("Institution"),
392-
}, [adj.institution.code if adj.institution else TabbycatTableBuilder.BLANK_TEXT for adj in adjudicators])
396+
}, [escape(adj.institution.code) if adj.institution else TabbycatTableBuilder.BLANK_TEXT for adj in adjudicators])
393397

394398
return [teams_table, adjs_table]
395399

tabbycat/availability/views.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from django.db.models import Min
99
from django.db.models.functions import Coalesce
1010
from django.http import JsonResponse
11+
from django.utils.html import escape
1112
from django.utils.translation import gettext as _
1213
from django.utils.translation import gettext_lazy, ngettext
1314
from django.views.generic.base import TemplateView, View
@@ -188,7 +189,7 @@ def get_table(self):
188189
} for inst in queryset])
189190

190191
if self.round.prev:
191-
title = _("Active in %(prev_round)s") % {'prev_round': self.round.prev.abbreviation}
192+
title = _("Active in %(prev_round)s") % {'prev_round': escape(self.round.prev.abbreviation)}
192193
table.add_column({'key': 'active-prev', 'title': title}, [{
193194
'sort': inst.prev_available,
194195
'icon': 'check' if inst.prev_available else '',

tabbycat/breakqual/views.py

+5-4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from django.db.models import Count, Q
77
from django.forms import HiddenInput
88
from django.forms.models import BaseModelFormSet
9+
from django.utils.html import escape
910
from django.utils.translation import gettext as _, ngettext
1011
from django.views.generic import FormView, TemplateView
1112

@@ -61,7 +62,7 @@ def get_standings(self):
6162

6263
def get_table(self):
6364
self.standings = self.get_standings()
64-
table = TabbycatTableBuilder(view=self, title=self.object.name, sort_key='Rk')
65+
table = TabbycatTableBuilder(view=self, title=escape(self.object.name), sort_key='Rk')
6566
table.add_ranking_columns(self.standings)
6667
table.add_column({'title': _("Break"), 'key': 'break'},
6768
[tsi.break_rank for tsi in self.standings])
@@ -291,7 +292,7 @@ def get_table(self):
291292

292293
break_categories = t.breakcategory_set.order_by('seq')
293294
for bc in break_categories:
294-
table.add_column({'title': bc.name, 'key': bc.slug}, [{
295+
table.add_column({'title': escape(bc.name), 'key': escape(bc.slug)}, [{
295296
'component': 'check-cell',
296297
'checked': True if bc in team.break_categories.all() else False,
297298
'sort': True if bc in team.break_categories.all() else False,
@@ -301,13 +302,13 @@ def get_table(self):
301302

302303
# Provide list of members within speaker categories for convenient entry
303304
for sc in speaker_categories:
304-
table.add_column({'title': _('%s Speakers') % sc.name, 'key': sc.name + "_speakers"}, [{
305+
table.add_column({'title': _('%s Speakers') % escape(sc.name), 'key': escape(sc.name) + "_speakers"}, [{
305306
'text': getattr(team, 'nspeakers_%s' % sc.slug, 'N/A'),
306307
'tooltip': ngettext(
307308
'Team has %(nspeakers)s speaker with the %(category)s speaker category assigned',
308309
'Team has %(nspeakers)s speakers with the %(category)s speaker category assigned',
309310
getattr(team, 'nspeakers_%s' % sc.slug, 0),
310-
) % {'nspeakers': getattr(team, 'nspeakers_%s' % sc.slug, 'N/A'), 'category': sc.name},
311+
) % {'nspeakers': getattr(team, 'nspeakers_%s' % sc.slug, 'N/A'), 'category': escape(sc.name)},
311312
} for team in teams])
312313

313314
return table

tabbycat/draw/views.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from django.db.models import OuterRef, Subquery
88
from django.http import HttpResponseBadRequest, HttpResponseRedirect
99
from django.utils.functional import cached_property
10-
from django.utils.html import format_html
10+
from django.utils.html import escape, format_html
1111
from django.utils.safestring import mark_safe
1212
from django.utils.timezone import get_current_timezone_name
1313
from django.utils.translation import gettext as _
@@ -430,7 +430,7 @@ class EmailTeamAssignmentsView(RoundTemplateEmailCreateView):
430430
round_redirect_pattern_name = 'draw-display'
431431

432432
def get_queryset(self):
433-
return Speaker.objects.filter(team__debateteam__round=self.round).select_related('team')
433+
return Speaker.objects.filter(team__debateteam__debate__round=self.round).select_related('team')
434434

435435

436436
# ==============================================================================
@@ -806,7 +806,7 @@ def get_table(self):
806806
table = TabbycatTableBuilder(view=self)
807807
table.add_team_columns(teams)
808808

809-
headers = [round.abbreviation for round in rounds]
809+
headers = [escape(round.abbreviation) for round in rounds]
810810
data = [[tsas.get((team.id, round.seq), "—") for round in rounds] for team in teams]
811811
table.add_columns(headers, data)
812812

tabbycat/notifications/utils.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ def standings_email_generator(to, url, round):
161161

162162
for team in teams:
163163
context_team = context.copy()
164-
context_team['POINTS'] = str(team._points)
164+
context_team['POINTS'] = str(team.points_count)
165165
context_team['TEAM'] = team.short_name
166166

167167
for speaker in team.speaker_set.all():

tabbycat/notifications/views.py

+5-4
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from django.http import HttpResponse
1212
from django.urls import reverse_lazy
1313
from django.utils import formats, timezone
14+
from django.utils.html import escape
1415
from django.utils.translation import gettext as _, gettext_lazy, ngettext
1516
from django.views.generic.base import View
1617
from django.views.generic.edit import FormView
@@ -140,8 +141,8 @@ def get_tables(self):
140141
emails_time = []
141142

142143
for sentmessage in notification.sentmessage_set.all():
143-
emails_recipient.append(sentmessage.recipient.name if sentmessage.recipient else self.UNKNOWN_RECIPIENT_CELL)
144-
emails_addresses.append(sentmessage.email or self.UNKNOWN_RECIPIENT_CELL)
144+
emails_recipient.append(escape(sentmessage.recipient.name) if sentmessage.recipient else self.UNKNOWN_RECIPIENT_CELL)
145+
emails_addresses.append(escape(sentmessage.email) or self.UNKNOWN_RECIPIENT_CELL)
145146

146147
if len(sentmessage.statuses) > 0:
147148
latest_status = sentmessage.statuses[0] # already ordered
@@ -258,12 +259,12 @@ def get_table(self, mixed_participants=False):
258259
} for p in queryset])
259260

260261
table.add_column({'key': 'name', 'tooltip': _("Participant"), 'icon': 'user'}, [{
261-
'text': p.name,
262+
'text': escape(p.name),
262263
'class': 'no-wrap' if len(p.name) < 20 else '',
263264
} for p in queryset])
264265

265266
table.add_column({'key': 'email', 'tooltip': _("Email address"), 'icon': 'mail'}, [{
266-
'text': p.email if p.email else _("Not Provided"),
267+
'text': escape(p.email) if p.email else _("Not Provided"),
267268
'class': 'small' if p.email else 'small text-warning',
268269
} for p in queryset])
269270

tabbycat/participants/models.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -280,11 +280,9 @@ def points_count(self):
280280
try:
281281
return self._points
282282
except AttributeError:
283-
from results.models import TeamScore
284283
from standings.teams import PointsMetricAnnotator
285-
self._points = TeamScore.objects.filter(
286-
ballot_submission__confirmed=True,
287-
debate_team__team=self,
284+
self._points = self.__class__.objects.filter(
285+
id=self.id,
288286
).aggregate(p=Coalesce(PointsMetricAnnotator().get_annotation(), Value(0)))['p']
289287
return self._points
290288

tabbycat/participants/tables.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from django.db.models import Prefetch
2+
from django.utils.html import escape
23
from django.utils.translation import gettext as _
34

45
from adjallocation.models import DebateAdjudicator
@@ -34,7 +35,7 @@ def add_cumulative_team_points_column(self, teamscores):
3435
def add_speaker_scores_column(self, teamscores):
3536
data = [{
3637
'text': ", ".join([metricformat(ss.score) for ss in ts.debate_team.speaker_scores]) or "—",
37-
'tooltip': "<br>".join(["%s for %s" % (metricformat(ss.score), ss.speaker) for ss in ts.debate_team.speaker_scores]),
38+
'tooltip': "<br>".join(["%s for %s" % (metricformat(ss.score), escape(ss.speaker)) for ss in ts.debate_team.speaker_scores]),
3839
} for ts in teamscores]
3940
header = {'key': 'speaks', 'tooltip': _("Speaker scores<br>(in speaking order)"), 'text': _("Speaks")}
4041
self.add_column(header, data)

tabbycat/participants/templates/current_round/round_adj.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
{# Position, teams and room #}
44
<div class="list-group-item lead {% if draw_released %}active{% else %}list-group-item-dark{% endif %}">
55

6-
{% person_display_name adjudicator as adjudicator_name %}
6+
{% person_display_name debateadjudicator.adjudicator as adjudicator_name %}
77

88
{# (Two-team formats) #}
99
{% if pref.teams_in_debate == 'two' %}

tabbycat/participants/views.py

+6-5
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from django.db.models import Count, Prefetch, Q
88
from django.forms import HiddenInput
99
from django.http import JsonResponse
10+
from django.utils.html import escape
1011
from django.utils.translation import gettext as _, gettext_lazy, ngettext
1112
from django.views.generic.base import View
1213

@@ -99,11 +100,11 @@ def get_table(self):
99100
).distinct()
100101

101102
table = TabbycatTableBuilder(view=self, sort_key='code')
102-
table.add_column({'key': 'code', 'title': _("Code")}, [i.code for i in institutions])
103-
table.add_column({'key': 'name', 'title': _("Full name")}, [i.name for i in institutions])
103+
table.add_column({'key': 'code', 'title': _("Code")}, [escape(i.code) for i in institutions])
104+
table.add_column({'key': 'name', 'title': _("Full name")}, [escape(i.name) for i in institutions])
104105
if any(i.region is not None for i in institutions):
105106
table.add_column({'key': 'region', 'title': _("Region")},
106-
[i.region.name if i.region else "—" for i in institutions])
107+
[escape(i.region.name) if i.region else "—" for i in institutions])
107108
table.add_column({'key': 'nteams', 'title': _("Teams"), 'tooltip': _("Number of teams")},
108109
[i.nteams for i in institutions])
109110
table.add_column({'key': 'nadjs', 'title': _("Adjs"),
@@ -141,7 +142,7 @@ def get_table(self):
141142
table = TabbycatTableBuilder(view=self, sort_key='code_name')
142143
table.add_column(
143144
{'key': 'code_name', 'title': _("Code name")},
144-
[{'text': t.code_name or "—"} for t in teams],
145+
[{'text': escape(t.code_name) or "—"} for t in teams],
145146
)
146147
table.add_team_columns(teams)
147148
return table
@@ -380,7 +381,7 @@ def get_table(self):
380381
speaker_categories = self.tournament.speakercategory_set.all()
381382

382383
for sc in speaker_categories:
383-
table.add_column({'key': sc.name, 'title': sc.name}, [{
384+
table.add_column({'key': escape(sc.name), 'title': escape(sc.name)}, [{
384385
'component': 'check-cell',
385386
'checked': True if sc in speaker.categories.all() else False,
386387
'id': speaker.id,

tabbycat/results/tables.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from django.utils.html import escape
12
from django.utils.translation import gettext as _
23

34
from utils.misc import reverse_tournament
@@ -63,7 +64,7 @@ def get_ballot_cells(self, debate, tournament, view_role, user):
6364
return {
6465
'component': 'ballots-cell',
6566
'ballots': [b.serialize(tournament) for b in ballotsubmissions],
66-
'current_user': user.username,
67+
'current_user': escape(user.username),
6768
'acting_role': view_role,
6869
'new_ballot': reverse_tournament(new_link, self.tournament,
6970
kwargs={'debate_id': debate.id}),

0 commit comments

Comments
 (0)