Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
86be5e4
feat: add randset problem's manage ui
yushiuan9499 Dec 31, 2025
41d3b2e
feat(contest/rand-pro): add js logic
yushiuan9499 Dec 31, 2025
2a5b9f5
feat(contests): add get_contest & class fields for random-set
yushiuan9499 Dec 31, 2025
08cca79
feat(contest): add_pro_set & remove_pro_set
yushiuan9499 Dec 31, 2025
b2b8929
fix(contest): add pro_sets
yushiuan9499 Dec 31, 2025
b286938
feat(contest): allow set ip range
yushiuan9499 Dec 31, 2025
4788f79
feat(contest): add reorder_pro_set
yushiuan9499 Dec 31, 2025
36cd718
fix: use 0.0.0.0 as dummy ip
yushiuan9499 Dec 31, 2025
fd96fa8
feat(contest): add update ip range UI
yushiuan9499 Dec 31, 2025
3012613
feat(contest/acct): add ip handler
yushiuan9499 Jan 2, 2026
50b7d1a
fix(rand-pro): rename req_type of reorder set
yushiuan9499 Jan 2, 2026
168fe4b
feat(contest/pro): add random-set pro handler
yushiuan9499 Jan 2, 2026
5159822
fix(contest): remove checking param in service
yushiuan9499 Jan 2, 2026
96414dc
fix(contests): only update contest object when necessary
yushiuan9499 Jan 2, 2026
ab3e0d5
fix(contest): add dummy dict to pro_list when add pro_set
yushiuan9499 Jan 2, 2026
4e04a91
chore(migration): add column ip to contest & table contest_ip_joint
yushiuan9499 Jan 2, 2026
f8534be
fix(contest): split ip_range to start_ip & end_ip
yushiuan9499 Jan 2, 2026
5d5161b
fix(get_contest): put order in double quotes
yushiuan9499 Jan 2, 2026
9d8eaa2
fix: add random set option in contest/general/mange
yushiuan9499 Jan 2, 2026
32be32c
fix(templ): fix js syntax error
yushiuan9499 Jan 2, 2026
eba1d9a
revert(contests): only update contest object when necessary
yushiuan9499 Jan 9, 2026
b4e13e1
fix(contest): set contest cache when updated
yushiuan9499 Jan 9, 2026
79bf87a
fix(contest): remove order field in pro_list
yushiuan9499 Jan 9, 2026
968ac65
test: add integrated test for randomset/manage
yushiuan9499 Jan 9, 2026
8ea538d
fix(contest): move contest mode check to handler
yushiuan9499 Jan 9, 2026
728fcf2
fix(handler/contest): return self.error instead of tuple
yushiuan9499 Jan 9, 2026
8430d0f
fix(update_ip): wrong callee & no await for rs
yushiuan9499 Jan 9, 2026
2884ac1
fix(contest): split add random problem to other func
yushiuan9499 Jan 9, 2026
de66e86
fix(contest): use sql to get pro_set count
yushiuan9499 Jan 9, 2026
d702365
fix(templ): syntax error
yushiuan9499 Jan 9, 2026
16661a7
fix(migration): add forkey constraint
yushiuan9499 Jan 9, 2026
2038515
fix(contest): check if the problem exist
yushiuan9499 Jan 9, 2026
e49a4a6
fix(contest): await redis hset
yushiuan9499 Jan 10, 2026
e98b5a9
fix(contest): delete problems in 1 execute
yushiuan9499 Jan 10, 2026
71c873d
fix(test): wrong get_contest id
yushiuan9499 Jan 10, 2026
be42a6a
test: add more assert
yushiuan9499 Jan 10, 2026
10b8afd
fix(contest): avoid invalid actions to random-set
yushiuan9499 Jan 12, 2026
5a6c88d
chore: fix import order
yushiuan9499 Jan 12, 2026
8a8296e
fix(contest): wrong error message
yushiuan9499 Jan 13, 2026
f8f8e3f
test: enhance test coverage
yushiuan9499 Jan 13, 2026
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
29 changes: 29 additions & 0 deletions migration/20260102144900_add_random_set.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import json

async def dochange(db, rs):
await db.execute("ALTER TABLE contest ADD COLUMN start_ip character varying(64) DEFAULT '0.0.0.0';")
await db.execute("ALTER TABLE contest ADD COLUMN end_ip character varying(64) DEFAULT '0.0.0.0';")

await db.execute(
'''
CREATE TABLE contest_ip_joints (
contest_id integer NOT NULL,
ip character varying(64) NOT NULL,
pro_id integer NOT NULL
);
'''
)

await db.execute(
'''
ALTER TABLE ONLY contest_ip_joints ADD CONSTRAINT contest_problem_joints_forkey_contest_id
FOREIGN KEY (contest_id) REFERENCES contest(contest_id) ON DELETE CASCADE;
'''
)

await db.execute(
'''
ALTER TABLE ONLY contest_ip_joints ADD CONSTRAINT contest_problem_joints_forkey_pro_id
FOREIGN KEY (pro_id) REFERENCES problem(pro_id) ON DELETE CASCADE;
'''
)
28 changes: 28 additions & 0 deletions src/handlers/contests/manage/acct.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from services.user import UserService
from utils.numeric import parse_str_to_list

from ipaddress import IPv4Address, AddressValueError

contest_manage_acct_dispatcher = ActionDispatcher()


Expand All @@ -26,6 +28,8 @@ async def get(self):
contest_id=self.contest.contest_id,
acct_list=acct_list,
admin_list=admin_list,
start_ip=str(self.contest.start_ip),
end_ip=str(self.contest.end_ip)
)

@contest_manage_acct_dispatcher.action("add")
Expand Down Expand Up @@ -145,6 +149,30 @@ async def multi_remove_action(self):
("S", f"Accounts(#{acct_list} successfully removed from user list.")
)

@contest_manage_acct_dispatcher.action("update_ip")
async def update_ip_action(self):
start_ip = self.get_argument("start_ip")
end_ip = self.get_argument("end_ip")

try:
start_ip = IPv4Address(start_ip)
end_ip = IPv4Address(end_ip)
except AddressValueError:
return self.error(("Eparam", "Invalid IP address format."))

if start_ip > end_ip:
return self.error(('Eparam', 'Invalid IP range'))

self.contest.start_ip = start_ip
self.contest.end_ip = end_ip
await ContestService.inst.update_ip(
self.contest
)

return self.error(
("S", f"Contest IP range successfully updated.")
)

@reqenv
@contest_require_permission("admin")
async def post(self):
Expand Down
4 changes: 4 additions & 0 deletions src/handlers/contests/manage/general.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ async def update_action(self):
if err:
return self.error(err)

if contest_mode != self.contest.contest_mode and ContestMode.RANDOM_SET in (contest_mode, self.contest.contest_mode):
if len(self.contest.pro_list) != 0:
return self.error(('Echmod', 'Cannot change contest mode when problem list is not empty'))

self.contest.name = name

self.contest.contest_mode = contest_mode
Expand Down
102 changes: 94 additions & 8 deletions src/handlers/contests/manage/pro.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from handlers.base import reqenv, RequestHandler, ActionDispatcher
from handlers.contests.base import contest_require_permission
from services.chal import ChalConst, ChalService
from services.contests import ContestService, ProblemScoreType
from services.contests import ContestService, ProblemScoreType, ContestMode
from services.judge import JudgeServerClusterService
from services.pro import ProService, ProConst
from utils.numeric import parse_str_to_list
Expand All @@ -24,16 +24,28 @@ async def get(self):
continue
pro_list.append(pro)

await self.render(
"contests/manage/pro",
page="pro",
contest_id=self.contest.contest_id,
contest=self.contest,
pro_list=pro_list,
)
if self.contest.contest_mode == ContestMode.RANDOM_SET:
await self.render(
"contests/manage/rand-pro",
page="pro",
contest_id=self.contest.contest_id,
contest=self.contest,
pro_sets=self.contest.pro_sets
)
else:
await self.render(
"contests/manage/pro",
page="pro",
contest_id=self.contest.contest_id,
contest=self.contest,
pro_list=pro_list,
)

@contest_manage_pro_dispatcher.action("add")
async def add_action(self):
if self.contest.contest_mode == ContestMode.RANDOM_SET:
return self.error(('Emod', 'Cannot add problems to random set contests'))

pro_id = int(self.get_argument("pro_id"))

if self.contest.is_pro(pro_id):
Expand All @@ -51,6 +63,9 @@ async def add_action(self):

@contest_manage_pro_dispatcher.action("remove")
async def remove_action(self):
if self.contest.contest_mode == ContestMode.RANDOM_SET:
return self.error(('Emod', 'Cannot remove problems from random set contests'))

pro_id = int(self.get_argument("pro_id"))

if not self.contest.is_pro(pro_id):
Expand All @@ -68,6 +83,9 @@ async def remove_action(self):

@contest_manage_pro_dispatcher.action("multi_add")
async def multi_add_action(self):
if self.contest.contest_mode == ContestMode.RANDOM_SET:
return self.error(('Emod', 'Cannot add problems to random set contests'))

pro_id = self.get_argument("pro_id")
pro_id = parse_str_to_list(pro_id)
for p_id in pro_id:
Expand All @@ -83,6 +101,9 @@ async def multi_add_action(self):

@contest_manage_pro_dispatcher.action("multi_remove")
async def multi_remove_action(self):
if self.contest.contest_mode == ContestMode.RANDOM_SET:
return self.error(('Emod', 'Cannot remove problems from random set contests'))

pro_id = self.get_argument("pro_id")
pro_list = parse_str_to_list(pro_id)

Expand Down Expand Up @@ -172,6 +193,71 @@ async def public_action(self):

return self.error(("S", ""))

@contest_manage_pro_dispatcher.action("add_set")
async def add_set_action(self):
'''
Add a problem set in random set mode
pro_id should be a list of problem ids separated by comma
'''
if self.contest.contest_mode != ContestMode.RANDOM_SET:
return self.error(('Emod', 'Cannot add problem set to non-random set contests'))

pro_ids = self.get_argument("pro_id")
pro_ids = parse_str_to_list(pro_ids)

if len(pro_ids) < 1:
return self.error(('Eparam', 'Problem set must contain at least one problem'))

pro_set = [(pro_id, ProblemScoreType.IOI2017) for pro_id in pro_ids]

err, _ = await ContestService.inst.add_pro_set(self.contest, pro_set)
if err:
return self.error(err)

return self.error(("S", ""))

@contest_manage_pro_dispatcher.action("remove_set")
async def remove_set_action(self):
'''
Remove a problem set in random set mode
pro_id is the problem set index
'''
if self.contest.contest_mode != ContestMode.RANDOM_SET:
return self.error(('Emod', 'Cannot remove problem set from non-random set contests'))

pro_set_idx = int(self.get_argument("pro_id"))
if pro_set_idx < 0 or pro_set_idx > len(self.contest.pro_sets) - 1:
return self.error(('Eparam', 'Problem set index out of range'))

err, _ = await ContestService.inst.remove_pro_set(self.contest, pro_set_idx)
if err:
return self.error(err)

return self.error(("S", ""))

@contest_manage_pro_dispatcher.action("update_order")
async def update_order_action(self):
'''
Update problem order in random set mode
pro_id is a comma separated list of new problem set indices
'''
if self.contest.contest_mode != ContestMode.RANDOM_SET:
return self.error(('Emod', 'Cannot update problem order in non-random set contests'))

new_idxs = self.get_argument("pro_id")
new_idxs = parse_str_to_list(new_idxs)

pro_set_len = len(self.contest.pro_sets)

if len(new_idxs) != pro_set_len or sorted(new_idxs) != list(range(pro_set_len)):
return self.error(('Eparam', 'Invalid new indexes for problem sets'))

err, _ = await ContestService.inst.reorder_pro_set(self.contest, new_idxs)
if err:
return self.error(err)

return self.error(("S", ""))

@reqenv
@contest_require_permission("admin")
async def post(self):
Expand Down
Loading