Skip to content

Commit 73dcf74

Browse files
authored
Add ability to delete Teams in admin interface (#1416)
1 parent c207b6f commit 73dcf74

9 files changed

Lines changed: 138 additions & 21 deletions

File tree

AUTHORS.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ Alexander Kernozhitsky <sh200105@mail.ru>
4747
Benjamin Swart <Benjaminswart@email.cz>
4848
Andrey Vihrov <andrey.vihrov@gmail.com>
4949
Grace Hawkins <amoomajid99@gmail.com>
50+
Pasit Sangprachathanarak <ouipingpasit@gmail.com>
5051

5152
And many other people that didn't write code, but provided useful
5253
comments, suggestions and feedback. :-)

cms/db/user.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -229,15 +229,15 @@ class Participation(Base):
229229
user: User = relationship(
230230
User,
231231
back_populates="participations")
232-
__table_args__ = (UniqueConstraint('contest_id', 'user_id'),)
232+
__table_args__ = (UniqueConstraint("contest_id", "user_id"),)
233233

234234
# Team (id and object) that the user is representing with this
235235
# participation.
236236
team_id: int | None = Column(
237237
Integer,
238-
ForeignKey(Team.id,
239-
onupdate="CASCADE", ondelete="RESTRICT"),
240-
nullable=True)
238+
ForeignKey(Team.id, onupdate="CASCADE", ondelete="SET NULL"),
239+
nullable=True,
240+
)
241241
team: Team | None = relationship(
242242
Team,
243243
back_populates="participations")

cms/server/admin/handlers/__init__.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,9 @@
9898
AddParticipationHandler, \
9999
EditParticipationHandler, \
100100
AddTeamHandler, \
101-
TeamHandler
101+
TeamHandler, \
102+
TeamListHandler, \
103+
RemoveTeamHandler
102104
from .usertest import \
103105
UserTestHandler, \
104106
UserTestFileHandler
@@ -145,8 +147,7 @@
145147

146148
# Contest's announcements
147149

148-
(r"/contest/([0-9]+)/announcements",
149-
SimpleContestHandler("announcements.html")),
150+
(r"/contest/([0-9]+)/announcements", SimpleContestHandler("announcements.html")),
150151
(r"/contest/([0-9]+)/announcements/add", AddAnnouncementHandler),
151152
(r"/contest/([0-9]+)/announcement/([0-9]+)", AnnouncementHandler),
152153

@@ -193,7 +194,8 @@
193194

194195
(r"/users", UserListHandler),
195196
(r"/users/([0-9]+)/remove", RemoveUserHandler),
196-
(r"/teams", SimpleHandler("teams.html")),
197+
(r"/teams", TeamListHandler),
198+
(r"/teams/([0-9]+)/remove", RemoveTeamHandler),
197199
(r"/users/add", AddUserHandler),
198200
(r"/teams/add", AddTeamHandler),
199201
(r"/user/([0-9]+)", UserHandler),
@@ -211,15 +213,14 @@
211213

212214
(r"/submission/([0-9]+)(?:/([0-9]+))?", SubmissionHandler),
213215
(r"/submission/([0-9]+)(?:/([0-9]+))?/comment", SubmissionCommentHandler),
214-
(r"/submission/([0-9]+)(?:/([0-9]+))?/official",
215-
SubmissionOfficialStatusHandler),
216+
(r"/submission/([0-9]+)(?:/([0-9]+))?/official", SubmissionOfficialStatusHandler),
216217
(r"/submission_file/([0-9]+)", SubmissionFileHandler),
217218

218219
# User tests
219220

220221
(r"/user_test/([0-9]+)(?:/([0-9]+))?", UserTestHandler),
221222
(r"/user_test_file/([0-9]+)", UserTestFileHandler),
222-
223+
223224
# The following prefixes are handled by WSGI middlewares:
224225
# * /rpc, defined in cms/io/web_service.py
225226
# * /static, defined in cms/io/web_service.py

cms/server/admin/handlers/contestuser.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -239,10 +239,17 @@ def post(self, contest_id, user_id):
239239

240240
# Update the team
241241
self.get_string(attrs, "team")
242-
team: Team | None = (
243-
self.sql_session.query(Team).filter(Team.code == attrs["team"]).first()
244-
)
245-
participation.team = team
242+
team_code = attrs["team"]
243+
244+
if team_code: # If a team code is provided
245+
team: Team | None = (
246+
self.sql_session.query(Team).filter(Team.code == team_code).first()
247+
)
248+
if team is None:
249+
raise ValueError(f"Team with code '{team_code}' does not exist")
250+
participation.team = team
251+
else: # If no team code is provided, set to None
252+
participation.team = None
246253

247254
except Exception as error:
248255
self.service.add_notification(

cms/server/admin/handlers/user.py

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,14 +105,36 @@ def post(self):
105105

106106
if operation == self.REMOVE:
107107
asking_page = self.url("users", user_id, "remove")
108-
# Open asking for remove page
109108
self.redirect(asking_page)
110109
else:
111110
self.service.add_notification(
112111
make_datetime(), "Invalid operation %s" % operation, "")
113112
self.redirect(self.url("contests"))
114113

115114

115+
class TeamListHandler(SimpleHandler("teams.html")):
116+
"""Get returns the list of all teams, post perform operations on
117+
a specific team (removing them from CMS).
118+
119+
"""
120+
121+
REMOVE = "Remove"
122+
123+
@require_permission(BaseHandler.AUTHENTICATED)
124+
def post(self):
125+
team_id: str = self.get_argument("team_id")
126+
operation: str = self.get_argument("operation")
127+
128+
if operation == self.REMOVE:
129+
asking_page = self.url("teams", team_id, "remove")
130+
self.redirect(asking_page)
131+
else:
132+
self.service.add_notification(
133+
make_datetime(), "Invalid operation %s" % operation, ""
134+
)
135+
self.redirect(self.url("contests"))
136+
137+
116138
class RemoveUserHandler(BaseHandler):
117139
"""Get returns a page asking for confirmation, delete actually removes
118140
the user from CMS.
@@ -145,6 +167,47 @@ def delete(self, user_id):
145167
self.write("../../users")
146168

147169

170+
class RemoveTeamHandler(BaseHandler):
171+
"""Get returns a page asking for confirmation, delete actually removes
172+
the team from CMS.
173+
174+
"""
175+
176+
@require_permission(BaseHandler.PERMISSION_ALL)
177+
def get(self, team_id):
178+
team = self.safe_get_item(Team, team_id)
179+
participation_query = self.sql_session.query(Participation).filter(
180+
Participation.team == team
181+
)
182+
183+
self.r_params = self.render_params()
184+
self.r_params["team"] = team
185+
self.r_params["participation_count"] = participation_query.count()
186+
self.render("team_remove.html", **self.r_params)
187+
188+
@require_permission(BaseHandler.PERMISSION_ALL)
189+
def delete(self, team_id):
190+
team = self.safe_get_item(Team, team_id)
191+
try:
192+
193+
# Remove associations
194+
self.sql_session.query(Participation).filter(
195+
Participation.team_id == team_id
196+
).update({Participation.team_id: None})
197+
198+
# delete the team
199+
self.sql_session.delete(team)
200+
if self.try_commit():
201+
self.service.proxy_service.reinitialize()
202+
except Exception as fallback_error:
203+
self.service.add_notification(
204+
make_datetime(), "Error removing team", repr(fallback_error)
205+
)
206+
207+
# Maybe they'll want to do this again (for another team)
208+
self.write("../../teams")
209+
210+
148211
class TeamHandler(BaseHandler):
149212
"""Manage a single team.
150213

cms/server/admin/templates/team.html

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,27 @@ <h1>{{ team.name }} ({{ team.code }})</h1>
77
<h2 id="title_general_info" class="toggling_on">General Information</h2>
88
<div id="general_info">
99
<!-- We use "multipart/form-data" to have Tornado distinguish between missing and empty values. -->
10-
<form enctype="multipart/form-data" action="{{ url("team", team.id) }}" method="POST">
10+
<form enctype="multipart/form-data" action="{{ url("team", team.id) }}" method="POST" style="display:inline;">
1111
{{ xsrf_form_html|safe }}
1212
<table>
1313
<tr>
1414
<td>Code</td>
15-
<td><input type="text" name="code" value="{{ team.code }}"/></td>
15+
<td><input type="text" name="code" value="{{ team.code }}" /></td>
1616
</tr>
1717
<tr>
1818
<td>Name</td>
19-
<td><input type="text" name="name" value="{{ team.name }}"/></td>
19+
<td><input type="text" name="name" value="{{ team.name }}" /></td>
2020
</tr>
2121
</table>
2222
<input type="submit" value="Update" />
2323
<input type="reset" value="Reset" />
2424
</form>
25+
<form action="{{ url("teams") }}" method="POST" style="display:inline; float: right;">
26+
{{ xsrf_form_html|safe }}
27+
<input type="hidden" name="team_id" value="{{ team.id }}" />
28+
<input type="submit" name="operation" value="Remove" {% if not admin.permission_all %}
29+
disabled {% endif %} />
30+
</form>
2531
<div class="hr"></div>
2632
</div>
2733

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{% extends "base.html" %}
2+
3+
{% block core %}
4+
<div class="core_title">
5+
<h1>Remove Team</h1>
6+
</div>
7+
8+
{% if admin.permission_all %}
9+
<p>
10+
Are you sure you want to remove team {{ team.code }} ({{ team.name }})?
11+
<br>
12+
{% if participation_count > 0 %}
13+
This will remove the team association from {{ participation_count }} participation(s). The participations will remain but will no longer be associated with this team.
14+
{% else %}
15+
This team has no associated participations.
16+
{% endif %}
17+
<br>
18+
This operation cannot be undone.
19+
</p>
20+
<br>
21+
<a onclick="CMS.AWSUtils.ajax_delete('{{ url("teams", team.id, "remove") }}');" class="button-link">Yes, remove</a>
22+
<a href="{{ url("teams") }}" class="button-link">No</a>
23+
24+
{% else %}
25+
You do not have permission to remove a team.
26+
{% endif %}
27+
28+
{% endblock core %}

cms/server/admin/templates/teams.html

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,30 @@
66
<h1>Teams list</h1>
77
</div>
88

9-
<div id="details">
9+
<form action="{{ url("teams") }}" method="POST">
10+
{{ xsrf_form_html|safe }}
11+
Edit selected team:
12+
<input type="submit" name="operation" value="Remove" {% if not admin.permission_all %} disabled {% endif %} />
1013
<table class="bordered">
1114
<thead>
1215
<tr>
16+
<th></th>
1317
<th>Code</th>
1418
<th>Name</th>
1519
</tr>
1620
</thead>
1721
<tbody>
1822
{% for t in team_list|sort(attribute="code") %}
1923
<tr>
24+
<td>
25+
<input type="radio" name="team_id" value="{{ t.id }}" />
26+
</td>
2027
<td><a href="{{ url("team", t.id) }}">{{ t.code }}</a></td>
2128
<td>{{ t.name }}</td>
2229
</tr>
2330
{% endfor %}
2431
</tbody>
2532
</table>
26-
</div>
33+
</form>
2734

2835
{% endblock core %}

cmscontrib/updaters/update_from_1.5.sql

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ ALTER TABLE public.contests ALTER COLUMN allow_unofficial_submission_before_anal
1414
-- https://github.com/cms-dev/cms/pull/1393
1515
ALTER TABLE public.submission_results ADD COLUMN scored_at timestamp without time zone;
1616

17+
-- https://github.com/cms-dev/cms/pull/1416
18+
ALTER TABLE ONLY public.participations DROP CONSTRAINT participations_team_id_fkey;
19+
ALTER TABLE ONLY public.participations ADD CONSTRAINT participations_team_id_fkey FOREIGN KEY (team_id) REFERENCES public.teams(id) ON UPDATE CASCADE ON DELETE SET NULL;
20+
1721
-- https://github.com/cms-dev/cms/pull/1419
1822
ALTER TABLE submissions ADD COLUMN opaque_id BIGINT;
1923
UPDATE submissions SET opaque_id = id WHERE opaque_id IS NULL;

0 commit comments

Comments
 (0)