diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 01baae40..50d9196b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -9,7 +9,7 @@ permissions: jobs: e2etest: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v5 @@ -26,9 +26,10 @@ jobs: echo deb [arch=amd64 signed-by=/usr/share/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main | sudo tee /etc/apt/sources.list.d/postgresql.list sudo apt update -y sudo apt install -f -y postgresql-16 postgresql-client-16 redis dos2unix - sudo sed -i 's/peer/trust/' /etc/postgresql/16/main/pg_hba.conf - sudo service postgresql start - sudo service redis-server start + sudo sed -i 's/peer/trust/g' /etc/postgresql/16/main/pg_hba.conf + sudo sed -i 's/scram-sha-256/trust/g' /etc/postgresql/16/main/pg_hba.conf + sudo service postgresql restart + sudo service redis-server restart - name: Install Coverage run: | diff --git a/migration/20241012000000_special_score.py b/migration/20241012000000_special_score.py new file mode 100644 index 00000000..23d4910a --- /dev/null +++ b/migration/20241012000000_special_score.py @@ -0,0 +1,94 @@ +class NewStatus: + STATE_AC = 1 + STATE_PC = 2 + STATE_WA = 3 + STATE_RE = 4 + STATE_RESIG = 5 + STATE_TLE = 6 + STATE_MLE = 7 + STATE_OLE = 8 + STATE_CE = 9 + STATE_CLE = 10 + STATE_ERR = 11 + STATE_SJE = 12 + STATE_JUDGE = 100 + STATE_NOTSTARTED = 101 + +OldStatusMapper = { + 1: NewStatus.STATE_AC, + 2: NewStatus.STATE_WA, + 3: NewStatus.STATE_RE, + 9: NewStatus.STATE_RESIG, + 4: NewStatus.STATE_TLE, + 5: NewStatus.STATE_MLE, + 6: NewStatus.STATE_CE, + 10: NewStatus.STATE_CLE, + 7: NewStatus.STATE_ERR, + 8: NewStatus.STATE_OLE, + 100: NewStatus.STATE_JUDGE, + 101: NewStatus.STATE_NOTSTARTED, +} + +async def dochange(db, rs): + + res = await db.fetch('SELECT chal_id, test_idx, state FROM test;') + for test in res: + chal_id, test_idx, old_state = test['chal_id'], test['test_idx'], test['state'] + await db.execute('UPDATE test SET state = $1 WHERE chal_id = $2 AND test_idx = $3', + OldStatusMapper[old_state], chal_id, test_idx) + await db.execute(f'SELECT update_challenge_state({chal_id});') + + await db.execute('ALTER TABLE test ADD COLUMN rate NUMERIC(10, 3);') + await db.execute('ALTER TABLE problem ADD COLUMN rate_precision INTEGER DEFAULT 0;') + await db.execute('ALTER TABLE challenge_state ALTER COLUMN rate TYPE NUMERIC(10, 3);') + + await rs.delete('rate') + + await db.execute( + ''' + CREATE OR REPLACE FUNCTION update_challenge_state(p_chal_id INTEGER) + RETURNS VOID AS $$ + BEGIN + WITH challenge_summary AS ( + SELECT + t.chal_id, + MAX(t.state) AS max_state, + SUM(t.runtime) AS total_runtime, + SUM(t.memory) AS total_memory, + SUM( + CASE + WHEN (t.state = 1 OR t.state = 2) AND t.rate IS NOT NULL THEN t.rate -- special score + WHEN t.state = 1 AND t.rate IS NULL THEN tvr.rate -- default score + ELSE 0 + END + ) AS total_rate + FROM test t + LEFT JOIN test_valid_rate tvr ON t.pro_id = tvr.pro_id AND t.test_idx = tvr.test_idx + WHERE t.chal_id = p_chal_id + GROUP BY t.chal_id + ) + INSERT INTO challenge_state (chal_id, state, runtime, memory, rate) + SELECT + chal_id, + max_state, + total_runtime, + total_memory, + total_rate + FROM challenge_summary + ON CONFLICT (chal_id) DO UPDATE + SET + state = EXCLUDED.state, + runtime = EXCLUDED.runtime, + memory = EXCLUDED.memory, + rate = EXCLUDED.rate + WHERE + challenge_state.state != EXCLUDED.state OR + challenge_state.runtime != EXCLUDED.runtime OR + challenge_state.memory != EXCLUDED.memory OR + challenge_state.rate != EXCLUDED.rate; + + RETURN; + END; + $$ LANGUAGE plpgsql; + ''' + ) diff --git a/src/handlers/chal.py b/src/handlers/chal.py index 667e0576..7705e5cd 100644 --- a/src/handlers/chal.py +++ b/src/handlers/chal.py @@ -1,4 +1,5 @@ import asyncio +import decimal import json import tornado.web @@ -132,6 +133,11 @@ async def get(self, chal_id): await self.render('chal', pro=pro, chal=chal, rechal=rechal) return +class _Encoder(json.JSONEncoder): + def default(self, o): + if isinstance(o, decimal.Decimal): + return str(o) + return super().default(o) class ChalListNewChalHandler(WebSocketSubHandler): async def listen_challistnewchal(self): @@ -156,7 +162,7 @@ async def listen_challiststate(self): chal_id = int(msg['data']) if self.first_chal_id <= chal_id <= self.last_chal_id: _, new_state = await ChalService.inst.get_single_chal_state_in_list(chal_id, self.acct) - await self.write_message(json.dumps(new_state)) + await self.write_message(json.dumps(new_state, cls=_Encoder)) async def open(self): self.first_chal_id = -1 @@ -189,7 +195,7 @@ async def listen_chalstate(self): if int(msg['data']) == self.chal_id: _, chal_states = await ChalService.inst.get_chal_state(self.chal_id) - await self.write_message(json.dumps(chal_states)) + await self.write_message(json.dumps(chal_states, cls=_Encoder)) async def open(self): self.chal_id = -1 diff --git a/src/handlers/contests/manage/pro.py b/src/handlers/contests/manage/pro.py index fe6f2f8e..4137a863 100644 --- a/src/handlers/contests/manage/pro.py +++ b/src/handlers/contests/manage/pro.py @@ -90,7 +90,7 @@ async def post(self): self.error('Ejudge') return - err, pro = await ProService.inst.get_pro(pro_id, self.acct) + err, pro = await ProService.inst.get_pro(pro_id, self.acct, is_contest=True) if err: self.error(err) return diff --git a/src/handlers/contests/scoreboard.py b/src/handlers/contests/scoreboard.py index 717c2c59..4ae0471d 100644 --- a/src/handlers/contests/scoreboard.py +++ b/src/handlers/contests/scoreboard.py @@ -25,7 +25,7 @@ def default(self, o): return f"{minutes}:{seconds:02}" elif isinstance(o, Decimal): - return int(o) + return float(o) else: return json.JSONEncoder.default(self, o) @@ -36,6 +36,9 @@ def _encoder(self, obj): if isinstance(obj, datetime.datetime): return obj.timestamp() + elif isinstance(obj, Decimal): + return str(obj) + return obj @reqenv @@ -97,6 +100,7 @@ async def post(self): for pro_score in s[pro_id].values(): pro_score['timestamp'] = datetime.datetime.fromtimestamp(pro_score['timestamp']).replace( tzinfo=UTC8) + pro_score['score'] = Decimal(pro_score['score']) if is_ended: await self.rs.expire(cache_name, time=60 * 60) diff --git a/src/handlers/manage/pro.py b/src/handlers/manage/pro.py index 5b1b1540..f07999ac 100644 --- a/src/handlers/manage/pro.py +++ b/src/handlers/manage/pro.py @@ -775,6 +775,11 @@ async def post(self, page=None): tags = self.get_argument('tags') allow_submit = self.get_argument('allow_submit') == "true" # NOTE: test config + rate_precision = int(self.get_argument('rate_precision')) + if rate_precision > ProConst.RATE_PRECISION_MAX or rate_precision < ProConst.RATE_PRECISION_MIN: + self.error('Eparam') + return + is_makefile = self.get_argument('is_makefile') == "true" check_type = int(self.get_argument('check_type')) @@ -811,6 +816,7 @@ async def post(self, page=None): if check_type == ProConst.CHECKER_IOREDIR: chalmeta = json.dumps(chalmeta) + pro['testm_conf']['rate_precision'] = rate_precision await ProService.inst.update_test_config(pro_id, pro['testm_conf']) await LogService.inst.add_log( f"{self.acct.name} has sent a request to update the problem #{pro_id}", 'manage.pro.update.pro', @@ -822,6 +828,7 @@ async def post(self, page=None): 'is_makefile': is_makefile, 'chalmeta': chalmeta, 'check_type': check_type, + 'rate_precision': rate_precision, } ) if err: diff --git a/src/handlers/rank.py b/src/handlers/rank.py index 33ecd259..33734a01 100644 --- a/src/handlers/rank.py +++ b/src/handlers/rank.py @@ -4,6 +4,7 @@ from handlers.base import RequestHandler, reqenv from services.user import UserConst, UserService, Account +from services.chal import ChalConst class ProRankHandler(RequestHandler): @@ -26,26 +27,33 @@ async def get(self, pro_id): async with self.db.acquire() as con: result = await con.fetch( - ''' + f''' SELECT * FROM ( SELECT DISTINCT ON ("challenge"."acct_id") - "challenge"."chal_id", - "challenge"."acct_id", - "challenge"."timestamp", - "account"."name" AS "acct_name", - "challenge_state"."runtime", - "challenge_state"."memory" - FROM "challenge" - INNER JOIN "account" - ON "challenge"."acct_id"="account"."acct_id" - INNER JOIN "challenge_state" - ON "challenge"."chal_id"="challenge_state"."chal_id" - WHERE "challenge"."pro_id"= $1 - AND "challenge_state"."state"=1 - ORDER BY "challenge"."acct_id" ASC, - "challenge_state"."runtime" ASC, "challenge_state"."memory" ASC, - "challenge"."timestamp" ASC, "challenge"."acct_id" ASC + "challenge"."chal_id", + "challenge"."acct_id", + "challenge"."timestamp", + "account"."name" AS "acct_name", + "challenge_state"."runtime", + "challenge_state"."memory", + ROUND("challenge_state"."rate", "problem"."rate_precision") + + FROM "challenge" + INNER JOIN "account" + ON "challenge"."acct_id"="account"."acct_id" + + INNER JOIN "challenge_state" + ON "challenge"."chal_id"="challenge_state"."chal_id" + + INNER JOIN "problem" + ON "challenge"."pro_id" = $1 + + WHERE "challenge_state"."state"={ChalConst.STATE_AC} + + ORDER BY "challenge"."acct_id" ASC, "challenge_state"."rate" ASC, + "challenge_state"."runtime" ASC, "challenge_state"."memory" ASC, + "challenge"."timestamp" ASC ) temp ORDER BY "runtime" ASC, "memory" ASC, "timestamp" ASC, "acct_id" ASC OFFSET $2 LIMIT $3; @@ -73,7 +81,7 @@ async def get(self, pro_id): total_cnt = total_cnt[0]['count'] chal_list = [] - for rank, (chal_id, acct_id, timestamp, acct_name, runtime, memory) in enumerate(result): + for rank, (chal_id, acct_id, timestamp, acct_name, runtime, memory, rate) in enumerate(result): chal_list.append( { 'rank': rank + pageoff + 1, @@ -82,6 +90,7 @@ async def get(self, pro_id): 'acct_name': acct_name, 'runtime': int(runtime), 'memory': int(memory), + 'rate': rate, 'timestamp': timestamp.astimezone(tz), } ) diff --git a/src/handlers/submit.py b/src/handlers/submit.py index 398a60f7..e8a3cef5 100644 --- a/src/handlers/submit.py +++ b/src/handlers/submit.py @@ -133,7 +133,7 @@ async def post(self): pro_id = chal['pro_id'] comp_type = chal['comp_type'] - err, pro = await ProService.inst.get_pro(pro_id, self.acct) + err, pro = await ProService.inst.get_pro(pro_id, self.acct, is_contest=self.contest is not None) if err: self.finish(err) return diff --git a/src/services/chal.py b/src/services/chal.py index b2e85dda..bd38e5f7 100644 --- a/src/services/chal.py +++ b/src/services/chal.py @@ -11,20 +11,23 @@ class ChalConst: STATE_AC = 1 - STATE_WA = 2 - STATE_RE = 3 - STATE_RESIG = 9 - STATE_TLE = 4 - STATE_MLE = 5 - STATE_CE = 6 - STATE_CLE = 10 - STATE_ERR = 7 + STATE_PC = 2 + STATE_WA = 3 + STATE_RE = 4 + STATE_RESIG = 5 + STATE_TLE = 6 + STATE_MLE = 7 STATE_OLE = 8 + STATE_CE = 9 + STATE_CLE = 10 + STATE_ERR = 11 + STATE_SJE = 12 STATE_JUDGE = 100 STATE_NOTSTARTED = 101 STATE_STR = { STATE_AC: 'AC', + STATE_PC: 'PC', STATE_WA: 'WA', STATE_RE: 'RE', STATE_RESIG: 'RE(SIG)', @@ -33,12 +36,14 @@ class ChalConst: STATE_CE: 'CE', STATE_CLE: 'CLE', STATE_OLE: 'OLE', + STATE_SJE: 'SJE', STATE_ERR: 'IE', STATE_JUDGE: 'JDG', } STATE_LONG_STR = { STATE_AC: 'Accepted', + STATE_PC: 'Partial Correct', STATE_WA: 'Wrong Answer', STATE_RE: 'Runtime Error', STATE_RESIG: 'Runtime Error (Killed by signal)', @@ -48,6 +53,7 @@ class ChalConst: STATE_CE: 'Compile Error', STATE_CLE: 'Compilation Limit Exceed', STATE_ERR: 'Internal Error', + STATE_SJE: 'Special Judge Error', STATE_JUDGE: 'Challenging', STATE_NOTSTARTED: 'Not Started', } @@ -114,7 +120,7 @@ def get_sql_query_str(self): if self.contest != 0: query += f' AND "challenge"."contest_id"={self.contest} ' else: - query += f' AND "challenge"."contest_id"=0 ' + query += ' AND "challenge"."contest_id"=0 ' return query @@ -196,15 +202,24 @@ async def get_chal_state(self, chal_id): async with self.db.acquire() as con: result = await con.fetch( ''' - SELECT "test_idx", "state", "runtime", "memory", "response" + SELECT "test"."test_idx", "state", "runtime", "memory", "response", + ROUND(COALESCE(test.rate, tvr.rate), problem.rate_precision) FROM "test" + INNER JOIN test_valid_rate AS tvr + ON test.pro_id = tvr.pro_id AND test.test_idx = tvr.test_idx + INNER JOIN problem + ON test.pro_id = problem.pro_id WHERE "chal_id" = $1 ORDER BY "test_idx" ASC; ''', chal_id, ) tests = [] - for test_idx, state, runtime, memory, response in result: + for test_idx, state, runtime, memory, response, rate in result: + r = 0 + if state in [ChalConst.STATE_AC, ChalConst.STATE_PC]: + r = rate + tests.append( { 'test_idx': test_idx, @@ -212,6 +227,7 @@ async def get_chal_state(self, chal_id): 'runtime': int(runtime), 'memory': int(memory), 'response': response, + 'rate': r, } ) @@ -249,24 +265,34 @@ async def get_chal(self, chal_id): async with self.db.acquire() as con: result = await con.fetch( ''' - SELECT "test_idx", "state", "runtime", "memory", "response" + SELECT "test"."test_idx", "state", "runtime", "memory", "response", + ROUND(COALESCE(test.rate, tvr.rate), problem.rate_precision) FROM "test" + INNER JOIN test_valid_rate AS tvr + ON test.pro_id = tvr.pro_id AND test.test_idx = tvr.test_idx + INNER JOIN problem + ON test.pro_id = problem.pro_id WHERE "chal_id" = $1 ORDER BY "test_idx" ASC; ''', chal_id, ) testl = [] - for test_idx, state, runtime, memory, response in result: + for test_idx, state, runtime, memory, response, rate in result: if final_response == "": final_response = response + r = 0 + if state in [ChalConst.STATE_AC, ChalConst.STATE_PC]: + r = rate + testl.append( { 'test_idx': test_idx, 'state': state, 'runtime': int(runtime), 'memory': int(memory), + 'rate': r, } ) @@ -336,7 +362,7 @@ async def emit_chal(self, chal_id, pro_id, testm_conf, comp_type, pri: int): if not os.path.isfile(f"code/{chal_id}/main.{file_ext}"): for test in testl: - await self.update_test(chal_id, test['test_idx'], ChalConst.STATE_ERR, 0, 0, '', refresh_db=False) + await self.update_test(chal_id, test['test_idx'], ChalConst.STATE_ERR, 0, 0, None, '', refresh_db=False) await self.update_challenge_state(chal_id) return None, None @@ -375,7 +401,8 @@ async def list_chal(self, off, num, acct: Account, flt: ChalSearchingParam): f''' SELECT "challenge"."chal_id", "challenge"."pro_id", "challenge"."acct_id", "challenge"."contest_id", "challenge"."compiler_type", "challenge"."timestamp", "account"."name" AS "acct_name", - "challenge_state"."state", "challenge_state"."runtime", "challenge_state"."memory" + "challenge_state"."state", "challenge_state"."runtime", "challenge_state"."memory", + ROUND("challenge_state"."rate", problem.rate_precision) FROM "challenge" INNER JOIN "account" ON "challenge"."acct_id" = "account"."acct_id" @@ -392,7 +419,7 @@ async def list_chal(self, off, num, acct: Account, flt: ChalSearchingParam): ) challist = [] - for chal_id, pro_id, acct_id, contest_id, comp_type, timestamp, acct_name, state, runtime, memory in result: + for chal_id, pro_id, acct_id, contest_id, comp_type, timestamp, acct_name, state, runtime, memory, rate in result: if state is None: state = ChalConst.STATE_NOTSTARTED @@ -420,6 +447,7 @@ async def list_chal(self, off, num, acct: Account, flt: ChalSearchingParam): 'state': state, 'runtime': runtime, 'memory': memory, + 'rate': rate, } ) @@ -436,7 +464,9 @@ async def get_single_chal_state_in_list( async with self.db.acquire() as con: result = await con.fetch( ''' - SELECT "challenge"."chal_id", "challenge_state"."state", "challenge_state"."runtime", "challenge_state"."memory" + SELECT "challenge"."chal_id", + "challenge_state"."state", "challenge_state"."runtime", "challenge_state"."memory", + ROUND("challenge_state"."rate", problem.rate_precision) AS "rate" FROM "challenge" INNER JOIN "account" ON "challenge"."acct_id" = "account"."acct_id" INNER JOIN "problem" ON "challenge"."pro_id" = "problem"."pro_id" @@ -456,6 +486,7 @@ async def get_single_chal_state_in_list( 'state': result['state'], 'runtime': int(result['runtime']), 'memory': int(result['memory']), + 'rate': result['rate'], } async def get_stat(self, acct: Account, flt: ChalSearchingParam): @@ -479,23 +510,38 @@ async def get_stat(self, acct: Account, flt: ChalSearchingParam): total_chal = result[0]['count'] return None, {'total_chal': total_chal} - async def update_test(self, chal_id, test_idx, state, runtime, memory, response, refresh_db=True): + async def update_test(self, chal_id, test_idx, state, runtime, memory, rate, response, rate_is_cms_type=False, refresh_db=True): chal_id = int(chal_id) async with self.db.acquire() as con: await con.execute( ''' UPDATE "test" - SET "state" = $1, "runtime" = $2, "memory" = $3, "response" = $4 - WHERE "chal_id" = $5 AND "test_idx" = $6; + SET "state" = $1, "runtime" = $2, "memory" = $3, "response" = $4, "rate" = $5 + WHERE "chal_id" = $6 AND "test_idx" = $7; ''', state, runtime, memory, response, + rate, chal_id, test_idx, ) + if rate_is_cms_type: + await con.execute( + ''' + UPDATE "test" + SET "rate" = $1::decimal * "test_config"."weight"::decimal + FROM "test_config" + WHERE "test"."chal_id" = $2 AND + "test_config"."pro_id" = "test"."pro_id" AND + "test_config"."test_idx" = $3 AND + "test"."test_idx" = $3; + ''', + rate, chal_id, test_idx + ) + if refresh_db: await self.update_challenge_state(chal_id) diff --git a/src/services/contests.py b/src/services/contests.py index 443f725e..381d1fd8 100644 --- a/src/services/contests.py +++ b/src/services/contests.py @@ -247,7 +247,7 @@ async def get_ioi2013_scores(self, contest_id: int, pro_id: int, before_time: da "challenge"."pro_id", "challenge"."acct_id", "challenge"."timestamp", - "challenge_state"."rate", + ROUND("challenge_state"."rate", problem.rate_precision), ROW_NUMBER() OVER ( PARTITION BY "challenge"."pro_id", "challenge"."acct_id" @@ -263,7 +263,8 @@ async def get_ioi2013_scores(self, contest_id: int, pro_id: int, before_time: da INNER JOIN "challenge_state" ON "challenge"."contest_id" = $1 AND "challenge"."acct_id" in ({user}) AND "challenge"."pro_id" = $2 AND "challenge"."timestamp" < $3 AND "challenge"."chal_id" = "challenge_state"."chal_id" - + INNER JOIN "problem" + ON "problem"."pro_id" = $2 ) SELECT acct_id, @@ -312,7 +313,11 @@ async def get_ioi2017_scores(self, contest_id: int, pro_id: int, before_time: da pt.pro_id, pt.test_idx, pt.weight, - CASE WHEN t.state = 1 THEN pt.weight ELSE 0 END AS rate, + CASE + WHEN t.state = 1 AND t.rate IS NULL THEN pt.weight + WHEN t.state = 1 AND t.rate IS NOT NULL THEN t.rate + ELSE 0 + END AS rate, t.timestamp FROM problem_tests pt JOIN test t ON pt.pro_id = t.pro_id AND pt.test_idx = t.test_idx @@ -363,12 +368,13 @@ async def get_ioi2017_scores(self, contest_id: int, pro_id: int, before_time: da SELECT ar.acct_id, ar.last_chal_id AS chal_id, - ar.total_rate AS score, + ROUND(ar.total_rate, problem.rate_precision) AS score, ar.best_timestamp, cc.challenges_before FROM aggregated_results ar JOIN challenge_counts cc ON ar.acct_id = cc.acct_id AND ar.pro_id = cc.pro_id JOIN account a ON ar.acct_id = a.acct_id + INNER JOIN problem ON problem.pro_id = $2 ORDER BY ar.acct_id, ar.pro_id; ''', contest_id, pro_id, before_time) diff --git a/src/services/judge.py b/src/services/judge.py index 21e15701..e04c4cf6 100644 --- a/src/services/judge.py +++ b/src/services/judge.py @@ -1,9 +1,9 @@ -import asyncio import json +import decimal +import asyncio import smtplib from email.header import Header from email.mime.text import MIMEText -from queue import PriorityQueue from typing import Dict, List, Literal, Union from tornado.websocket import websocket_connect @@ -51,13 +51,23 @@ async def connect_server(self): await self.response_handle(ret) async def response_handle(self, ret): - from services.chal import ChalService + from services.chal import ChalService, ChalConst res = json.loads(ret) if res['results'] is not None: for test_idx, result in enumerate(res['results']): - # INFO: CE會回傳 result['verdict'] + + score = None + is_cms_type = False + if 'score_type' in result and result['score_type'] in ["CMS", "CF"]: + is_cms_type = result['score_type'] == "CMS" + if 'score' in result: + try: + score = decimal.Decimal(result['score']) + except decimal.InvalidOperation: + score = None + result['status'] = ChalConst.STATE_SJE _, ret = await ChalService.inst.update_test( res['chal_id'], @@ -65,7 +75,9 @@ async def response_handle(self, ret): result['status'], int(result['time'] / 10 ** 6), # ns to ms result['memory'], + score, result['verdict'], + rate_is_cms_type=is_cms_type, refresh_db=False, ) @@ -145,7 +157,7 @@ async def offline_notice(self): class JudgeServerClusterService: def __init__(self, rs, server_urls: List[Dict]) -> None: JudgeServerClusterService.inst = self - self.queue = PriorityQueue() + self.queue = asyncio.PriorityQueue() self.rs = rs self.servers: List[JudgeServerService] = [] self.idx = 0 @@ -173,7 +185,7 @@ def __init__(self, rs, server_urls: List[Dict]) -> None: async def start(self) -> None: for idx, judge_server in enumerate(self.servers): - self.queue.put([0, idx]) + await self.queue.put([0, idx]) await judge_server.start() async def connect_server(self, idx) -> Literal['Eparam', 'Ejudge', 'S']: @@ -186,7 +198,7 @@ async def connect_server(self, idx) -> Literal['Eparam', 'Ejudge', 'S']: if not self.servers[idx].status: return 'Ejudge' - self.queue.put([0, idx]) + await self.queue.put([0, idx]) return 'S' async def disconnect_server(self, idx) -> Literal['Eparam', 'Ejudge', 'S']: @@ -201,7 +213,7 @@ async def disconnect_server(self, idx) -> Literal['Eparam', 'Ejudge', 'S']: async def disconnect_all_server(self) -> None: for server in self.servers: - self.queue.get() + await self.queue.get() await server.disconnect_server() def get_server_status(self, idx): @@ -233,8 +245,8 @@ async def send(self, data, pro_id, contest_id) -> None: if not self.is_server_online(): return - while not self.queue.empty(): - running_cnt, idx = self.queue.get() + while True: + running_cnt, idx = await self.queue.get() _, status = self.get_server_status(idx) if not status['status']: continue @@ -242,13 +254,13 @@ async def send(self, data, pro_id, contest_id) -> None: judge_id = status['judge_id'] if data['chal_id'] in self.servers[judge_id].chal_map: - self.queue.put([running_cnt, idx]) + await self.queue.put([running_cnt, idx]) break await self.servers[judge_id].send(data) _, status = self.get_server_status(idx) - self.queue.put([status['running_chal_cnt'], judge_id]) + await self.queue.put([status['running_chal_cnt'], judge_id]) self.servers[idx].chal_map[data['chal_id']] = {"pro_id": pro_id, "contest_id": contest_id} break diff --git a/src/services/pro.py b/src/services/pro.py index d9d84a1e..e9dc06fe 100644 --- a/src/services/pro.py +++ b/src/services/pro.py @@ -13,6 +13,10 @@ class ProConst: NAME_MIN = 1 NAME_MAX = 64 CODE_MAX = 16384 + + RATE_PRECISION_MIN = 0 + RATE_PRECISION_MAX = 3 + STATUS_ONLINE = 0 STATUS_CONTEST = 1 STATUS_HIDDEN = 2 @@ -73,7 +77,7 @@ async def get_pro(self, pro_id, acct: Account | None = None, is_contest: bool = result = await con.fetch( """ SELECT "name", "status", "tags", "allow_submit", - "check_type", "is_makefile", "chalmeta", "limit" + "check_type", "is_makefile", "chalmeta", "limit", "rate_precision" FROM "problem" WHERE "pro_id" = $1 AND "status" <= $2; """, pro_id, @@ -83,13 +87,14 @@ async def get_pro(self, pro_id, acct: Account | None = None, is_contest: bool = return "Enoext", None result = result[0] - name, status, tags, allow_submit, check_type, is_makefile, limit, chalmeta = ( + name, status, tags, allow_submit, check_type, is_makefile, rate_precision, limit, chalmeta = ( result["name"], result["status"], result["tags"], result["allow_submit"], result["check_type"], result["is_makefile"], + result["rate_precision"], json.loads(result["limit"]), json.loads(result["chalmeta"]), ) @@ -115,6 +120,7 @@ async def get_pro(self, pro_id, acct: Account | None = None, is_contest: bool = "check_type": check_type, "is_makefile": is_makefile, "test_group": test_groups, + "rate_precision": rate_precision, } return ( @@ -259,6 +265,7 @@ async def update_test_config(self, pro_id, testm_conf: dict): check_type = testm_conf['check_type'] chalmeta = testm_conf['chalmeta'] limit = testm_conf['limit'] + rate_precision = testm_conf['rate_precision'] for test_group_idx, test_group_conf in testm_conf['test_group'].items(): weight = test_group_conf['weight'] @@ -268,8 +275,8 @@ async def update_test_config(self, pro_id, testm_conf: dict): async with self.db.acquire() as con: await con.execute('DELETE FROM "test_config" WHERE "pro_id" = $1;', int(pro_id)) await con.execute( - 'UPDATE "problem" SET is_makefile = $1, check_type = $2, chalmeta = $3, "limit" = $4 WHERE pro_id = $5', - is_makefile, check_type, json.dumps(chalmeta), json.dumps(limit), pro_id + 'UPDATE "problem" SET is_makefile = $1, check_type = $2, chalmeta = $3, "limit" = $4, "rate_precision" = $5 WHERE pro_id = $6', + is_makefile, check_type, json.dumps(chalmeta), json.dumps(limit), rate_precision, pro_id ) if insert_sql: @@ -303,37 +310,7 @@ def get_acct_limit(self, acct: Account | None = None, contest=False): async def unpack_pro(self, pro_id, pack_type, pack_token): from services.chal import ChalConst - def _clean_cont(prefix): - try: - os.remove(f"{prefix}cont.html") - - except OSError: - pass - - try: - os.remove(f"{prefix}cont.pdf") - - except OSError: - pass - - if ( - pack_type != ProService.PACKTYPE_FULL - and pack_type != ProService.PACKTYPE_CONTHTML - and pack_type != ProService.PACKTYPE_CONTPDF - ): - return "Eparam", None - - if pack_type == ProService.PACKTYPE_CONTHTML: - prefix = f"problem/{pro_id}/http/" - _clean_cont(prefix) - await PackService.inst.direct_copy(pack_token, f"{prefix}cont.html") - - elif pack_type == ProService.PACKTYPE_CONTPDF: - prefix = f"problem/{pro_id}/http/" - _clean_cont(prefix) - await PackService.inst.direct_copy(pack_token, f"{prefix}cont.pdf") - - elif pack_type == ProService.PACKTYPE_FULL: + if pack_type == ProService.PACKTYPE_FULL: err, _ = await PackService.inst.unpack(pack_token, f"problem/{pro_id}", True) if err: return err, None @@ -354,9 +331,13 @@ def _clean_cont(prefix): except json.decoder.JSONDecodeError: return "Econf", None - is_makefile = conf["compile"] == 'makefile' + is_makefile = False + if 'compile' in conf: + is_makefile = conf["compile"] == 'makefile' + elif 'is_makefile' in conf: + is_makefile = conf["is_makefile"] + check_type = self._get_check_type(conf["check"]) - chalmeta = conf["metadata"] # INFO: ioredir data ALLOW_COMPILERS = list(ChalConst.ALLOW_COMPILERS) + ['default'] if is_makefile: @@ -364,13 +345,19 @@ def _clean_cont(prefix): if "limit" in conf: limit = {lang: lim for lang, lim in conf["limit"].items() if lang in ALLOW_COMPILERS} - else: + elif 'timelimit' in conf and 'memlimit' in conf: limit = { 'default': { 'timelimit': conf["timelimit"], 'memlimit': conf["memlimit"] * 1024 } } + else: + return "Econf", None + + chalmeta = {} + if 'metadata' in conf: + chalmeta = conf["metadata"] # INFO: ioredir data async with self.db.acquire() as con: await con.execute('DELETE FROM "test_config" WHERE "pro_id" = $1;', int(pro_id)) diff --git a/src/services/rate.py b/src/services/rate.py index e1bd5232..c356e9dc 100644 --- a/src/services/rate.py +++ b/src/services/rate.py @@ -1,3 +1,4 @@ +import decimal import datetime from collections import defaultdict @@ -19,52 +20,48 @@ async def get_acct_rate_and_chal_cnt(self, acct: Account): if (rate_data := await self.rs.hget(key, acct_id)) is None: async with self.db.acquire() as con: - all_chal_cnt = await con.fetchrow('SELECT COUNT(*) FROM "challenge" WHERE "acct_id" = $1', acct_id) - all_chal_cnt = all_chal_cnt['count'] - - ac_chal_cnt = await con.fetchrow( - ''' - SELECT COUNT(*) FROM "challenge" - INNER JOIN "challenge_state" - ON "challenge"."chal_id" = "challenge_state"."chal_id" - AND "challenge_state"."state" = $1 - WHERE "acct_id" = $2 + result = await con.fetch( + f''' + SELECT + COUNT(*) AS all_chal_cnt, + COUNT(CASE WHEN challenge_state.state = {ChalConst.STATE_AC} THEN 1 END) AS ac_chal_cnt + FROM challenge + INNER JOIN challenge_state + ON challenge_state.chal_id = challenge.chal_id AND challenge.acct_id = $1 ''', - ChalConst.STATE_AC, acct_id, ) - ac_chal_cnt = ac_chal_cnt['count'] + if len(result) != 1: + return 'Eunk', None + result = result[0] + + ac_chal_cnt, all_chal_cnt = ( + result['ac_chal_cnt'], + result['all_chal_cnt'], + ) result = await con.fetch( ''' - SELECT - SUM("test_valid_rate"."rate") AS "rate" FROM "test_valid_rate" - INNER JOIN ( - SELECT "test"."pro_id","test"."test_idx", - MIN("test"."timestamp") AS "timestamp" - FROM "test" - INNER JOIN "account" - ON "test"."acct_id" = "account"."acct_id" - INNER JOIN "problem" - ON "test"."pro_id" = "problem"."pro_id" - WHERE "account"."acct_id" = $1 - AND "test"."state" = $2 - GROUP BY "test"."pro_id","test"."test_idx" - ) AS "valid_test" - ON "test_valid_rate"."pro_id" = "valid_test"."pro_id" - AND "test_valid_rate"."test_idx" = "valid_test"."test_idx"; + SELECT SUM(max_rate) AS total_rate + FROM ( + SELECT MAX(cs.rate) AS max_rate + FROM public.account a + JOIN public.challenge c ON a.acct_id = c.acct_id + JOIN public.challenge_state cs ON c.chal_id = cs.chal_id + WHERE a.acct_id = $1 + GROUP BY c.pro_id + ) AS subquery; ''', - acct_id, - int(ChalConst.STATE_AC), + acct_id ) if len(result) != 1: return 'Eunk', None - - if (rate := result[0]['rate']) is None: + rate = result[0]['total_rate'] + if rate is None: rate = 0 rate_data = { - 'rate': rate, + 'rate': str(rate), 'ac_cnt': ac_chal_cnt, 'all_cnt': all_chal_cnt, } @@ -72,6 +69,8 @@ async def get_acct_rate_and_chal_cnt(self, acct: Account): else: rate_data = unpackb(rate_data) + rate_data['rate'] = decimal.Decimal(rate_data['rate']) + return None, rate_data async def get_pro_ac_rate(self, pro_id): @@ -155,8 +154,10 @@ async def map_rate_acct( async with self.db.acquire() as con: result = await con.fetch( f''' - SELECT "challenge"."pro_id", MAX("challenge_state"."rate") AS "score", - COUNT("challenge_state") AS "count", MIN("challenge_state"."state") as "state" + SELECT "challenge"."pro_id", + ROUND(MAX("challenge_state"."rate"), (SELECT rate_precision FROM problem WHERE pro_id = challenge.pro_id)) AS "score", + COUNT("challenge_state") AS "count", + MIN("challenge_state"."state") as "state" FROM "challenge" INNER JOIN "challenge_state" ON "challenge"."chal_id" = "challenge_state"."chal_id" AND "challenge"."acct_id" = $1 @@ -191,7 +192,8 @@ async def map_rate(self, starttime='1970-01-01 00:00:00.000', endtime='2100-01-0 async with self.db.acquire() as con: result = await con.fetch( ''' - SELECT "challenge"."acct_id", "challenge"."pro_id", MAX("challenge_state"."rate") AS "score", + SELECT "challenge"."acct_id", "challenge"."pro_id", + ROUND(MAX("challenge_state"."rate"), (SELECT rate_precision FROM problem WHERE pro_id = challenge.pro_id)) AS "rate", COUNT("challenge_state") AS "count" FROM "challenge" INNER JOIN "challenge_state" diff --git a/src/static/index.css b/src/static/index.css index f4899601..d28a2bd3 100644 --- a/src/static/index.css +++ b/src/static/index.css @@ -6,42 +6,50 @@ a:hover a:visited a:link a:active { text-decoration: none !important; } -.state-1 { +.state-1 { /* AC */ color: #0f0 !important; } -.state-2 { +.state-2 { /* PC */ + color: #0f0 !important; +} + +.state-3 { /* WA */ color: #ed784a !important; } -.state-3 { +.state-4 { /* RE */ color: #27fff6 !important; } -.state-4 { +.state-5 { /* RESIG */ + color: #237ac7 !important; +} + +.state-6 { /* TLE */ color: #70649a !important; } -.state-5 { +.state-7 { /* MLE */ color: #e83015 !important; } -.state-6 { +.state-8 { /* OLE */ color: #ffeb00 !important; } -.state-8 { +.state-9 { /* CE */ color: #ffeb00 !important; } -.state-9 { - color: #237ac7 !important; -} - -.state-10 { +.state-10 { /* CLE */ color: #ffeb00 !important; } +/* .state-11 IE */ +/* .state-100 JUDGE */ +/* .state-101 NOTSTARTED */ + .panel { margin-bottom: 20px; border: 1px solid transparent; diff --git a/src/static/static.code-workspace b/src/static/static.code-workspace new file mode 100644 index 00000000..e56cfc87 --- /dev/null +++ b/src/static/static.code-workspace @@ -0,0 +1,22 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": { + "files.associations": { + "*.templ": "handlebars" + }, + + "css.styleSheets": [ + "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.2/css/bootstrap.min.css" + ], + + "editor.quickSuggestions": { + "other": "on", + "comments": "off", + "strings": "on" + } + } +} \ No newline at end of file diff --git a/src/static/templ/chal.html b/src/static/templ/chal.html index 819fe672..d0c34534 100644 --- a/src/static/templ/chal.html +++ b/src/static/templ/chal.html @@ -4,18 +4,9 @@