Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions tabbycat/actionlog/migrations/0014_alter_actionlogentry_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 5.2.7 on 2026-03-20 23:08

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('actionlog', '0013_actionlogentry_agent_alter_actionlogentry_type'),
]

operations = [
migrations.AlterField(
model_name='actionlogentry',
name='type',
field=models.CharField(choices=[('br.aj.set', 'Changed adjudicator breaking status'), ('aj.crea', 'Created adjudicator'), ('aj.edit', 'Edited adjudicator'), ('aj.note', 'Set adjudicator note'), ('aa.auto', 'Auto-allocated adjudicators'), ('aa.save', 'Saved adjudicator allocation'), ('av.aj.save', 'Edited adjudicators availability'), ('av.save', 'Edited availability'), ('av.tm.save', 'Edited teams availability'), ('av.ve.save', 'Edited room availability'), ('ba.ckin', 'Checked in ballot set'), ('ba.conf', 'Confirmed ballot set'), ('ba.crea', 'Created ballot set'), ('ba.disc', 'Discarded ballot set'), ('ba.edit', 'Edited ballot set'), ('ba.subm', 'Submitted ballot set from the public form'), ('br.ca.edit', 'Edited break categories'), ('br.del', 'Deleted team break for category'), ('br.rm.edit', 'Edited breaking team remarks'), ('br.el.edit', 'Edited break eligibility'), ('br.gene', 'Generated the team break for all categories'), ('br.gen1', 'Generated the team break for one category'), ('br.upda', 'Edited breaking team remarks and updated all team breaks'), ('br.upd1', 'Edited breaking team remarks and updated this team break'), ('ch.aj.gene', 'Generated check in identifiers for adjudicators'), ('ch.sp.gene', 'Generated check in identifiers for speakers'), ('ch.ve.gene', 'Generated check in identifiers for rooms'), ('ac.aa.edit', 'Edited adjudicator-adjudicator conflicts'), ('ac.ai.edit', 'Edited adjudicator-institution conflicts'), ('ac.at.edit', 'Edited adjudicator-team conflicts'), ('ac.ti.edit', 'Edited team-institution conflicts'), ('db.crea', 'Created debate'), ('db.edit', 'Edited debate'), ('db.im.auto', 'Auto-prioritized debate importance'), ('db.im.edit', 'Edited debate importance'), ('dv.save', 'Saved divisions'), ('dr.conf', 'Confirmed draw'), ('dr.crea', 'Created draw'), ('dr.rege', 'Regenerated draw'), ('dr.rele', 'Released draw'), ('dr.unre', 'Unreleased draw'), ('fq.crea', 'Created feedback question'), ('fq.edit', 'Edited feedback question'), ('fb.save', 'Saved feedback'), ('fb.subm', 'Submitted feedback from the public form'), ('in.crea', 'Created institution'), ('in.edit', 'Edited institution'), ('mu.save', 'Saved a matchup manual edit'), ('mo.edit', 'Added/edited motion'), ('mo.rele', 'Released motions'), ('mo.unre', 'Unreleased motions'), ('op.edit', 'Edited tournament options'), ('pp.aj.auto', 'Auto-allocated adjudicators to preformed panels'), ('pp.aj.edit', 'Edited preformed panel adjudicator'), ('pp.crea', 'Created preformed panels'), ('pp.db.auto', 'Auto-allocated preformed panels to debates'), ('pp.del', 'Deleted preformed panels'), ('pp.im.auto', 'Auto-prioritized preformed panels'), ('pp.im.edit', 'Edited preformed panel importance'), ('rd.adva', 'Advanced the current round to'), ('rd.comp', 'Marked round as completed'), ('rd.crea', 'Created round'), ('rd.edit', 'Edited round'), ('rd.st.set', 'Set start time'), ('ms.save', 'Saved the sides status of a matchup'), ('si.adju', 'Imported adjudicators using the simple importer'), ('si.inst', 'Imported institutions using the simple importer'), ('si.team', 'Imported teams using the simple importer'), ('si.venu', 'Imported rooms using the simple importer'), ('se.ca.edit', 'Edited speaker categories'), ('sp.crea', 'Created speaker'), ('sp.del', 'Deleted speaker'), ('sp.edit', 'Edited speaker'), ('se.edit', 'Edited speaker category eligibility'), ('te.crea', 'Created team'), ('te.edit', 'Edited team'), ('ts.edit', 'Edited adjudicator base score'), ('to.crea', 'Created tournament'), ('to.edit', 'Edited tournament'), ('aj.sc.upda', 'Updated adjudicator scores in bulk'), ('ur.inv', 'Invited user to the instance'), ('ve.ca.edit', 'Edited room categories'), ('ve.ca.crea', 'Created room category'), ('ve.co.edit', 'Edited room constraints'), ('ve.crea', 'Created room'), ('ve.edit', 'Edited room'), ('ve.auto', 'Auto-allocated rooms'), ('ve.save', 'Saved a room manual edit'), ('qu.crea', 'Created question'), ('qu.edit', 'Edited question'), ('inst.reg', 'Registered institution'), ('te.reg', 'Registered team'), ('aj.reg', 'Registered adjudicator'), ('sp.reg', 'Registered speaker'), ('re.conf', 'Confirmed registration'), ('sc.crea', 'Created schedule event'), ('sc.edit', 'Edited schedule event')], max_length=10, verbose_name='type'),
),
]
1 change: 1 addition & 0 deletions tabbycat/actionlog/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ class ActionType(models.TextChoices):
SIMPLE_IMPORT_VENUES = 'si.venu', _("Imported rooms using the simple importer")
SPEAKER_CATEGORIES_EDIT = 'se.ca.edit', _("Edited speaker categories")
SPEAKER_CREATE = 'sp.crea', _("Created speaker")
SPEAKER_DELETE = 'sp.del', _("Deleted speaker")
SPEAKER_EDIT = 'sp.edit', _("Edited speaker")
SPEAKER_ELIGIBILITY_EDIT = 'se.edit', _("Edited speaker category eligibility")
TEAM_CREATE = 'te.crea', _("Created team")
Expand Down
16 changes: 16 additions & 0 deletions tabbycat/participants/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from django import forms
from django.utils.translation import gettext as _


class ConfirmSpeakerDeletionForm(forms.Form):
speaker_name = forms.CharField(label=_("Speaker's full name"), required=True)

def __init__(self, *args, speaker=None, **kwargs):
self.speaker = speaker
super().__init__(*args, **kwargs)

def clean_speaker_name(self):
if self.cleaned_data['speaker_name'] != self.speaker.name:
raise forms.ValidationError(
_("You must type '%(name)s' exactly to confirm deletion.") % {'name': self.speaker.name},
)
33 changes: 33 additions & 0 deletions tabbycat/participants/templates/speaker_confirm_delete.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{% extends "base.html" %}
{% load debate_tags i18n %}

{% block head-title %}<span class="emoji">🗑</span> {% trans "Confirm Speaker Deletion" %}{% endblock %}
{% block page-title %}{% trans "Confirm Speaker Deletion" %}{% endblock %}

{% block page-subnav-sections %}
<a class="btn btn-outline-primary" href="{% tournamenturl 'participants-team-record' pk=team.pk %}">
<i data-feather="chevron-left"></i>{% trans "Back to Team Record" %}
</a>
{% endblock %}

{% block page-alerts %}

<div class="alert alert-danger">
{% blocktrans trimmed with name=speaker.name team=team.short_name %}
Do you really want to delete <strong>{{ name }}</strong> from {{ team }}?
This will permanently remove the speaker and <strong>cannot be undone</strong>.
Any ballot scores attributed to this speaker will also be deleted.
{% endblocktrans %}
</div>

<form method="POST" action="">
{% csrf_token %}
{% include "components/form-main.html" %}

{% blocktrans trimmed with name=speaker.name asvar title %}
Yes, delete {{ name }}
{% endblocktrans %}
{% include "components/form-submit.html" with type='danger' %}
</form>

{% endblock %}
25 changes: 25 additions & 0 deletions tabbycat/participants/templates/speaker_create.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{% extends "base.html" %}
{% load i18n %}

{% block head-title %}<span class="emoji">👤</span> {% trans "Add Speaker" %}{% endblock %}
{% block page-title %}{% trans "Add Speaker" %}{% endblock %}
{% block page-subtitle %}{% blocktrans trimmed with name=team.short_name %}for {{ name }}{% endblocktrans %}{% endblock %}

{% block page-subnav-sections %}
{% include "participants_subnav.html" %}
{% endblock %}

{% block content %}

<form action="" method="POST">
{% csrf_token %}
<div class="card">
<div class="list-group list-group-flush">
{% include "components/form-main.html" %}
{% trans "Add Speaker" as save_text %}
{% include "components/form-submit.html" with title=save_text suburl=team_record_url subtitle=cancel_text %}
</div>
</div>
</form>

{% endblock content %}
37 changes: 35 additions & 2 deletions tabbycat/participants/templates/team_registration_card.html
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just an idea, but maybe add an "Edit" button to the top of the registration card to send the user to the team's "Edit DB" page?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea, I'll add that.

Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@
<div class="card mt-md-3">
<div class="list-group list-group-flush">

<div class="list-group-item">
<div class="list-group-item d-flex justify-content-between align-items-center">
<h4 class="card-title mb-0">
{{ card_title|safe }}
</h4>
{% if admin_page %}
<a href="{% url 'admin:participants_team_change' team.pk %}" class="btn btn-sm btn-outline-secondary">
{% trans "Edit Database" %}
</a>
{% endif %}
</div>

{% if team.registration_status == 'U' %}
Expand Down Expand Up @@ -109,6 +114,8 @@ <h4 class="card-title mb-0">
{% endif %}

{% haspermission "view.answers" as answers_perm %}
{% haspermission "add.team" as add_team_perm %}
{% haspermission "delete.speaker" as delete_speaker_perm %}
{% if admin_page and answers_perm %}
<div class="list-group-item list-group-item-secondary">
<div>
Expand All @@ -126,7 +133,6 @@ <h4 class="card-title mb-0">
{% for speaker in team.speakers %}
<details>
<summary>{{ speaker.name }}</summary>

<ul>
{% for answer in speaker.answers.all %}
<li><strong>{{ answer.question.name }}</strong>: {{ answer.answer }}</li>
Expand All @@ -136,6 +142,33 @@ <h4 class="card-title mb-0">
{% endfor %}
</div>
</div>
{% endif %}

{% if admin_page and add_team_perm %}
<div class="list-group-item list-group-item-secondary">
<div>
<strong>{% trans "Speakers:" %}</strong>
</div>
{% for speaker in team.speakers %}
<div class="d-flex align-items-center mb-1">
<span class="flex-grow-1">{{ speaker.name }}</span>
{% if delete_speaker_perm %}
<a href="{% tournamenturl 'participants-team-speaker-delete' pk=team.pk speaker_pk=speaker.pk %}"
class="btn btn-sm btn-outline-danger ml-2 flex-shrink-0">
{% trans "Delete" %}
</a>
{% endif %}
</div>
{% endfor %}
<div class="mt-2">
<a href="{% tournamenturl 'participants-team-speaker-add' pk=team.pk %}" class="btn btn-sm btn-success">
{% trans "Add Speaker" %}
</a>
</div>
</div>
{% endif %}

{% if admin_page and answers_perm %}

<div class="list-group-item list-group-item-secondary">

Expand Down
6 changes: 6 additions & 0 deletions tabbycat/participants/urls_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@
path('team/<int:pk>/',
views.TeamRecordView.as_view(),
name='participants-team-record'),
path('team/<int:pk>/speaker/add/',
views.AdminCreateSpeakerView.as_view(),
name='participants-team-speaker-add'),
path('team/<int:pk>/speaker/<int:speaker_pk>/delete/',
views.AdminDeleteSpeakerView.as_view(),
name='participants-team-speaker-delete'),
path('adjudicator/<int:pk>/',
views.AdjudicatorRecordView.as_view(),
name='participants-adjudicator-record'),
Expand Down
83 changes: 83 additions & 0 deletions tabbycat/participants/views.py
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small things, but we should use form_class for both new form views, and then add the object in get_form_kwargs() rather than overriding get_form()

Also, for creation, I'm not sure the registration form would be the best, as a lot of fields may be missing, and admins may not want to answer the custom fields.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do, good catch.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, I'll swap it out for a plain ModelForm on Speaker instead.

Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
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

from actionlog.mixins import LogActionMixin
from actionlog.models import ActionLogEntry
Expand All @@ -23,6 +24,7 @@
from notifications.models import BulkNotification
from notifications.views import TournamentTemplateEmailCreateView
from options.utils import use_team_code_names
from registration.forms import AdminSpeakerForm
from tournaments.mixins import (PublicTournamentPageMixin,
SingleObjectFromTournamentMixin, TournamentMixin)
from tournaments.models import Round
Expand All @@ -32,6 +34,7 @@
from utils.tables import TabbycatTableBuilder
from utils.views import ModelFormSetView, VueTableTemplateView

from .forms import ConfirmSpeakerDeletionForm
from .models import Adjudicator, Institution, Person, Speaker, SpeakerCategory, Team
from .serializers import SpeakerSerializer
from .tables import AdjudicatorDebateTable, TeamDebateTable
Expand Down Expand Up @@ -364,6 +367,86 @@ def get_queryset(self):
)


class AdminCreateSpeakerView(LogActionMixin, AdministratorMixin, TournamentMixin, FormView):
template_name = 'speaker_create.html'
action_log_type = ActionLogEntry.ActionType.SPEAKER_CREATE
edit_permission = Permission.ADD_TEAMS
form_class = AdminSpeakerForm

def get_team(self):
if not hasattr(self, '_team'):
self._team = Team.objects.get(tournament=self.tournament, pk=self.kwargs['pk'])
return self._team

def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['team'] = self.get_team()
return kwargs

def get_action_log_content_object(self):
return self.object

def get_page_title(self):
return _("Add Speaker to %(team)s") % {'team': self.get_team().short_name}

def get_context_data(self, **kwargs):
kwargs['team'] = self.get_team()
kwargs['team_record_url'] = reverse_tournament('participants-team-record', self.tournament, kwargs={'pk': self.get_team().pk})
kwargs['cancel_text'] = _("Cancel and return to team record")
return super().get_context_data(**kwargs)

def form_valid(self, form):
self.object = form.save()
messages.success(self.request, _("%(name)s was added as a speaker.") % {'name': self.object.name})
return super().form_valid(form)

def get_success_url(self):
return reverse_tournament('participants-team-record', self.tournament, kwargs={'pk': self.get_team().pk})


class AdminDeleteSpeakerView(LogActionMixin, AdministratorMixin, TournamentMixin, FormView):
template_name = 'speaker_confirm_delete.html'
action_log_type = ActionLogEntry.ActionType.SPEAKER_DELETE
edit_permission = Permission.DELETE_SPEAKER
form_class = ConfirmSpeakerDeletionForm

def get_speaker(self):
if not hasattr(self, '_speaker'):
self._speaker = Speaker.objects.select_related('team__tournament').get(
pk=self.kwargs['speaker_pk'], team__tournament=self.tournament,
)
return self._speaker

def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['speaker'] = self.get_speaker()
return kwargs

def get_action_log_content_object(self):
return self.object

def get_page_title(self):
return _("Delete %(name)s") % {'name': self.get_speaker().name}

def get_context_data(self, **kwargs):
kwargs['speaker'] = self.get_speaker()
kwargs['team'] = self.get_speaker().team
return super().get_context_data(**kwargs)

def form_valid(self, form):
self.object = self.get_speaker()
team_pk = self.object.team_id
speaker_name = self.object.name
self.log_action()
self.object.delete()
messages.success(self.request, _("%(name)s was deleted.") % {'name': speaker_name})
return redirect_tournament('participants-team-record', self.tournament, pk=team_pk)

def get_success_url(self):
return reverse_tournament('participants-team-record', self.tournament,
kwargs={'pk': self.get_speaker().team_id})


class AdjudicatorRecordView(AdministratorMixin, BaseAdjudicatorRecordView):
admin = True
view_permission = Permission.VIEW_ADJUDICATORS
Expand Down
17 changes: 17 additions & 0 deletions tabbycat/registration/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,23 @@ def save(self, commit=True):
return obj


class AdminSpeakerForm(forms.ModelForm):

def __init__(self, *args, team=None, **kwargs):
self.team = team
super().__init__(*args, **kwargs)

class Meta:
model = Speaker
fields = ('name', 'email')

def save(self, commit=True):
self.instance.team = self.team
obj = super().save(commit=commit)
populate_url_keys([obj])
return obj


class AdjudicatorForm(CustomQuestionsFormMixin, forms.ModelForm):

key = forms.CharField(widget=forms.HiddenInput(), required=False)
Expand Down
1 change: 1 addition & 0 deletions tabbycat/users/groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ class Registration(BaseGroup):
name = _("Registration")
permissions = [
Permission.ADD_TEAMS,
Permission.DELETE_SPEAKER,
Permission.VIEW_DECODED_TEAMS,
Permission.VIEW_ANONYMOUS,
Permission.ADD_ADJUDICATORS,
Expand Down
1 change: 1 addition & 0 deletions tabbycat/users/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class Permission(TextChoices):

VIEW_TEAMS = 'view.team', _("view teams")
ADD_TEAMS = 'add.team', _("add teams")
DELETE_SPEAKER = 'delete.speaker', _("delete speakers")
VIEW_DECODED_TEAMS = 'view.teamname', _("view decoded team names")
VIEW_ANONYMOUS = 'view.anonymous', _("View names of anonymized participants")
VIEW_ADJUDICATORS = 'view.adj', _("view adjudicators")
Expand Down
Loading