diff --git a/src/handlers/contests/proset.py b/src/handlers/contests/proset.py index 4da9a2cf..cb9f21cb 100644 --- a/src/handlers/contests/proset.py +++ b/src/handlers/contests/proset.py @@ -1,44 +1,98 @@ +from decimal import Decimal + +from msgpack import unpackb + from handlers.base import reqenv, RequestHandler from services.pro import ProConst, ProService from services.rate import RateService +from services.contests import ContestMode +PERMISSION_DENIED_ERROR = ('Eacces', 'Permission denied') class ContestProsetHandler(RequestHandler): + async def randomset_proset(self, pageoff: int): + if not self.contest.is_member(self.acct): + return self.error(PERMISSION_DENIED_ERROR) + + scoreboard_key = f'contest_{self.contest.contest_id}_randomset_scoreboard' + randomset_prolist = self.contest.get_randomset_prolist_from_acct_by_ip(self.acct) + if randomset_prolist is None: + return self.error(('Etodo', 'TODO: Assign problem set for out of range IP not implemented. Call Yushiuan9499.')) + randomset_prolist_set = set(randomset_prolist) + + prolist_order = {pro_id: idx for idx, pro_id in enumerate(randomset_prolist)} + _, prolist = await ProService.inst.list_pro(ProConst.PRO_STATUS_CONTEST_USER) + prolist = sorted(filter(lambda pro: pro.pro_id in randomset_prolist_set, prolist), + key=lambda pro: prolist_order[pro.pro_id]) + + keys = [f'{self.acct.acct_id}_{pro_order}' for pro_order in range(len(prolist))] + score_values = await self.rs.hmget(scoreboard_key, keys) + + score_map: dict[int, dict] = {} + for pro_order, pro in enumerate(prolist): + score_map[pro.pro_id] = {'score': Decimal('0'), 'state': None} + assert 0 <= pro_order < len(score_values) + + best_record = score_values[pro_order] + if best_record is not None: + best_record = unpackb(best_record) + score_map[pro.pro_id]['score'] += Decimal(best_record['score']) + score_map[pro.pro_id]['state'] = best_record['state'] + + pro_total_cnt = len(prolist) + prolist = prolist[pageoff: pageoff + 40] + + pro_idx_map = {pro_id: idx for idx, pro_id in enumerate(randomset_prolist)} + await self.render('contests/proset', contest=self.contest, show_ac_ratio=False, + prolist=prolist, pro_idx_map=pro_idx_map, pro_total_cnt=pro_total_cnt, score_map=score_map, pageoff=pageoff) + + @reqenv async def get(self): pageoff = int(self.get_argument('pageoff', default=0)) show_ac_ratio = False + + if not self.contest.is_member(self.acct): + return self.error(PERMISSION_DENIED_ERROR) + if not self.contest.is_start() and not self.contest.is_admin(self.acct): - return self.error(('Eacces', 'Permission denied')) + return self.error(PERMISSION_DENIED_ERROR) elif self.contest.is_running() and not self.contest.is_member(self.acct): - return self.error(('Eacces', 'Permission denied')) + return self.error(PERMISSION_DENIED_ERROR) - else: - _, acct_rates = await RateService.inst.map_rate_acct(self.acct, contest_id=self.contest.contest_id) - _, prolist = await ProService.inst.list_pro(ProConst.PRO_STATUS_CONTEST_USER) + elif self.contest.contest_mode == ContestMode.RANDOM_SET and not self.contest.is_admin(self.acct): + return await self.randomset_proset(pageoff) + + _, acct_rates = await RateService.inst.map_rate_acct(self.acct, contest_id=self.contest.contest_id) + _, prolist = await ProService.inst.list_pro(ProConst.PRO_STATUS_CONTEST_USER) + + prolist_order = {pro_id: idx for idx, pro_id in enumerate(self.contest.pro_list.keys())} + prolist = sorted(filter(lambda pro: self.contest.is_pro(pro.pro_id), prolist), + key=lambda pro: prolist_order[pro.pro_id]) - prolist_order = {pro_id: idx for idx, pro_id in enumerate(self.contest.pro_list.keys())} - prolist = sorted(filter(lambda pro: self.contest.is_pro(pro.pro_id), prolist), - key=lambda pro: prolist_order[pro.pro_id]) + score_map: dict[int, dict] = {} + for pro in prolist: + pro_id = pro.pro_id + score_map[pro_id] = {'score': 0, 'state': None} + if pro_id in acct_rates: + score_map[pro_id]['score'] += acct_rates[pro.pro_id]['rate'] + score_map[pro_id]['state'] = acct_rates[pro.pro_id]['state'] - score_map: dict[int, dict] = {} + if self.contest.is_public_scoreboard or self.contest.is_admin(self.acct): + show_ac_ratio = True for pro in prolist: - pro_id = pro.pro_id - score_map[pro_id] = {'score': 0, 'state': None} - if pro_id in acct_rates: - score_map[pro_id]['score'] += acct_rates[pro.pro_id]['rate'] - score_map[pro_id]['state'] = acct_rates[pro.pro_id]['state'] - - if self.contest.is_public_scoreboard or self.contest.is_admin(self.acct): - show_ac_ratio = True - for pro in prolist: - _, rate = await RateService.inst.get_pro_ac_rate(pro.pro_id, contest_id=self.contest.contest_id) - score_map[pro.pro_id]['rate_data'] = rate + _, rate = await RateService.inst.get_pro_ac_rate(pro.pro_id, contest_id=self.contest.contest_id) + score_map[pro.pro_id]['rate_data'] = rate + + if self.contest.contest_mode == ContestMode.RANDOM_SET: + pro_idx_map = {pro_id: idx for idx, pro_set in enumerate(self.contest.pro_sets) for pro_id in pro_set} + else: + pro_idx_map = {pro_id: idx for idx, pro_id in enumerate(self.contest.pro_list.keys())} pro_total_cnt = len(prolist) prolist = prolist[pageoff: pageoff + 40] await self.render('contests/proset', contest=self.contest, show_ac_ratio=show_ac_ratio, - prolist=prolist, pro_total_cnt=pro_total_cnt, score_map=score_map, pageoff=pageoff) + prolist=prolist, pro_idx_map=pro_idx_map, pro_total_cnt=pro_total_cnt, score_map=score_map, pageoff=pageoff) diff --git a/src/handlers/contests/scoreboard.py b/src/handlers/contests/scoreboard.py index 9fee3439..3ab278f6 100644 --- a/src/handlers/contests/scoreboard.py +++ b/src/handlers/contests/scoreboard.py @@ -7,7 +7,7 @@ import config from handlers.base import RequestHandler, UnifiedWebSocketHandler, reqenv -from services.contests import ContestService, ProblemScoreType, UserStatus +from services.contests import ContestService, ProblemScoreType, UserStatus, ContestMode from services.user import UserService @@ -104,6 +104,63 @@ def _encoder(self, obj): return obj + async def random_set_scoreboard(self): + contest = self.contest + acct = self.acct + if contest.is_member(acct) and not contest.is_admin(acct) and not contest.is_randomset_pro_allocated(acct): + return self.error(('Etodo', 'TODO: Assign problem set for out of range IP not implemented. Call Yushiuan9499.')) + + if self.contest.is_public_scoreboard: + acct_list = [acct_id for acct_id, v in self.contest.user_list.items() if v['status'] == UserStatus.APPROVED] + else: + acct_list = [self.acct.acct_id] + + scoreboard_key = f'contest_{self.contest.contest_id}_randomset_scoreboard' + pro_sets_len = len(self.contest.pro_sets) + keys = [f'{acct_id}_{pro_order}' for acct_id in acct_list for pro_order in range(pro_sets_len)] + score_values = await self.rs.hmget(scoreboard_key, keys) + + start_time = self.contest.contest_start + all_scores = [] + for acct_cnt, acct_id in enumerate(acct_list): + _, acct = await UserService.inst.info_acct(acct_id) + assert acct + prolist = self.contest.get_randomset_prolist_from_acct_by_ip(acct) + if prolist is None: + continue + + total_score = Decimal('0') + scores = {} + for pro_order, pro_id in enumerate(prolist): + idx = acct_cnt * pro_sets_len + pro_order + assert 0 <= idx < len(score_values) + + best_record = score_values[idx] + if best_record is None: + continue + + best_record = unpackb(best_record) + best_record['timestamp'] = datetime.datetime.fromtimestamp(best_record['timestamp']) + best_record['score'] = Decimal(best_record['score']) + scores[pro_order] = { + 'pro_id': pro_id, + 'chal_id': best_record['chal_id'], + 'timestamp': best_record['timestamp'].astimezone(config.TIMEZONE) - start_time, + 'score': best_record['score'], + 'state': best_record['state'], + 'fail_count': best_record['fail_count'] + } + total_score += best_record['score'] + + all_scores.append({ + 'acct_id': acct_id, + 'name': acct.name, + 'scores': scores, + 'total_score': total_score + }) + + self.error(('S', all_scores), encoder=_JsonDatetimeEncoder) + @reqenv async def get(self): await self.render('contests/scoreboard', contest=self.contest) @@ -115,6 +172,9 @@ async def post(self): elif not self.contest.is_public_scoreboard and not self.contest.is_member(self.acct): return self.error(('Eacces', 'Permission denied')) + if self.contest.contest_mode == ContestMode.RANDOM_SET: + return await self.random_set_scoreboard() + has_end_time = True start_time = self.contest.contest_start try: @@ -186,7 +246,7 @@ async def post(self): 'chal_id': p['chal_id'], 'timestamp': p['timestamp'].astimezone(config.TIMEZONE) - start_time, 'score': p['score'], - 'fail_cnt': p['fail_cnt'] + 'fail_count': p['fail_count'] } total_score += p['score'] diff --git a/src/handlers/pro.py b/src/handlers/pro.py index 43ec3295..a22fd65e 100644 --- a/src/handlers/pro.py +++ b/src/handlers/pro.py @@ -8,6 +8,7 @@ from services.pro import ProClassService, ProClassConst, ProConst, ProService, Problem from services.rate import RateService from services.user import UserService, UserConst +from services.contests import ContestMode PERMISSION_DENIED_ERROR = ("Eacces", "Permission denied") @@ -175,6 +176,13 @@ async def get(self, pro_id: int, path: str): if not self.contest.is_admin(self.acct) and not self.contest.is_running(): return self.error(PERMISSION_DENIED_ERROR) + if self.contest.contest_mode == ContestMode.RANDOM_SET: + contest_prolist = self.contest.get_randomset_prolist_from_acct_by_ip(self.acct) + if contest_prolist is None: + return self.error(('Etodo', 'TODO: Assign problem set for out of range IP not implemented. Call Yushiuan9499.')) + if pro_id not in contest_prolist: + return self.error(("Enoext", "Problem not in your problem set")) + allow_statuses = ProConst.PRO_STATUS_CONTEST_USER else: if self.acct.is_kernel(): @@ -239,6 +247,13 @@ async def get(self, pro_id): if not self.contest.is_admin(self.acct) and not self.contest.is_running(): return self.error(PERMISSION_DENIED_ERROR) + if self.contest.contest_mode == ContestMode.RANDOM_SET: + contest_prolist = self.contest.get_randomset_prolist_from_acct_by_ip(self.acct) + if contest_prolist is None: + return self.error(('Etodo', 'TODO: Assign problem set for out of range IP not implemented. Call Yushiuan9499.')) + if pro_id not in contest_prolist: + return self.error(PERMISSION_DENIED_ERROR) + allow_statuses = ProConst.PRO_STATUS_CONTEST_USER else: diff --git a/src/services/contests.py b/src/services/contests.py index 43e36da9..7d445684 100644 --- a/src/services/contests.py +++ b/src/services/contests.py @@ -1,15 +1,19 @@ import datetime import enum from dataclasses import dataclass, field +import random import pickle +from ipaddress import IPv4Address, AddressValueError +from decimal import Decimal import asyncpg -from ipaddress import IPv4Address -import random +import tornado.log +from msgpack import packb, unpackb -from services.chal import Compiler -from services.user import Account +from services.chal import ChalService, Compiler, ChalConst +from services.user import Account, UserService +logger = tornado.log.app_log class RegMode(enum.IntEnum): INVITED = 0 @@ -113,6 +117,36 @@ def member_is_status(self, acct: Account | int, status: UserStatus) -> bool: return self.user_list[acct_id]['status'] == status + def get_randomset_prolist_from_acct_by_ip(self, acct: Account) -> list[int] | None: + if self.contest_mode != ContestMode.RANDOM_SET: + return None + + try: + ip = IPv4Address(acct.lastip) + except AddressValueError: + return None + + try: + return self.ip_pro_list[ip] + except KeyError: + logger.warning(f'TODO: Assign problem set for out of range IP not implemented. Contest {self.contest_id} IP range {self.start_ip} - {self.end_ip}, but acct {acct.acct_id} IP is {ip}.') + return None + + def is_randomset_pro_allocated(self, acct: Account) -> bool: + if self.contest_mode != ContestMode.RANDOM_SET: + return False + + if self.is_admin(acct): + logger.warning(f'Should not call is_randomset_pro_allocated for admin acct {acct.acct_id} in contest {self.contest_id}.') + return False + + try: + ip = IPv4Address(acct.lastip) + except AddressValueError: + return False + + return ip in self.ip_pro_list + class ContestService: def __init__(self, db, rs): self.db = db @@ -181,9 +215,9 @@ async def get_contest(self, contest_id: int): if contest.contest_mode == ContestMode.RANDOM_SET: result = await con.fetch( ''' - SELECT ip, contest_ip_joints.pro_id - FROM contest_ip_joints INNER JOIN contest_problem_joints - ON contest_ip_joints.contest_id = contest_problem_joints.contest_id + SELECT ip, contest_ip_joints.pro_id + FROM contest_ip_joints INNER JOIN contest_problem_joints + ON contest_ip_joints.contest_id = contest_problem_joints.contest_id AND contest_ip_joints.pro_id = contest_problem_joints.pro_id WHERE contest_ip_joints.contest_id = $1 ORDER BY contest_ip_joints.ip, contest_problem_joints."order" ASC; ''', @@ -500,6 +534,70 @@ async def reorder_pro_set(self, contest: Contest, new_idxs: list[int]): ) return None, None + def _encoder(self, obj): + if isinstance(obj, datetime.datetime): + return obj.timestamp() + + elif isinstance(obj, Decimal): + return str(obj) + + return obj + + async def update_randomset_scoreboard(self, chal_id: int): + err, chal = await ChalService.inst.get_chal(chal_id) + if err: + return + assert chal + + err, contest = await self.get_contest(chal.contest_id) + if err: + return + assert contest + + if contest.contest_mode != ContestMode.RANDOM_SET: + return + + if contest.is_admin(acct_id=chal.acct_id): + return + + err, acct = await UserService.inst.info_acct(chal.acct_id) + if err: + return + assert acct + + prolist = contest.get_prolist_from_acct_by_ip(acct) + if prolist is None: + return + + try: + pro_order = prolist.index(chal.pro_id) + except ValueError: + logger.error(f'Contest {contest.contest_id} randomset problem list for acct {acct.acct_id} does not contain problem {chal.pro_id}.') + return + + err, total_result = await ChalService.inst.get_total_result(chal.chal_id) + if err: + return + assert total_result + + best_record = await self.rs.hget(f'contest_{contest.contest_id}_randomset_scoreboard', f'{acct.acct_id}_{pro_order}') + fail_count = 1 if total_result.state != ChalConst.STATE_AC else 0 + if best_record is not None: + best_record = unpackb(best_record) + fail_count += best_record['fail_count'] + + # NOTE: IOI2013 + if best_record is None or total_result.rate > Decimal(best_record['score']): + best_record = { + 'chal_id': chal.chal_id, + 'timestamp': chal.timestamp, + 'state': total_result.state, + 'score': total_result.rate, + 'fail_count': fail_count, + } + + await self.rs.hset(f'contest_{contest.contest_id}_randomset_scoreboard', f'{acct.acct_id}_{pro_order}', packb(best_record, default=self._encoder)) + async def get_ioi2013_scores(self, contest_id: int, pro_id: int, before_time: datetime.datetime) -> dict: _, contest = await self.get_contest(contest_id) res = await self.db.fetch( @@ -554,9 +652,9 @@ async def get_ioi2013_scores(self, contest_id: int, pro_id: int, before_time: da 'chal_id': chal_id, 'score': score, 'timestamp': timestamp, - 'fail_cnt': fail_cnt + 'fail_count': fail_count } - for acct_id, chal_id, score, timestamp, fail_cnt in res + for acct_id, chal_id, score, timestamp, fail_count in res } return scores @@ -649,9 +747,9 @@ async def get_ioi2017_scores(self, contest_id: int, pro_id: int, before_time: da 'chal_id': chal_id, 'score': score, 'timestamp': timestamp, - 'fail_cnt': fail_cnt + 'fail_count': fail_count } - for acct_id, chal_id, score, timestamp, fail_cnt in res + for acct_id, chal_id, score, timestamp, fail_count in res } return scores diff --git a/src/services/judge.py b/src/services/judge.py index 4027045e..31e7f676 100644 --- a/src/services/judge.py +++ b/src/services/judge.py @@ -186,6 +186,8 @@ async def response_handle(self, ret: str): if contest_id != 0: await self.rs.publish('contestnewchalsub', contest_id) await self.rs.hdel(f'contest_{contest_id}_scores', str(pro_id)) + from services.contests import ContestService + await ContestService.inst.update_randomset_scoreboard(chal_id) # NOTE: Recalculate problem rate await RateService.inst.refresh_pro_ac_rate(pro_id, contest_id) diff --git a/src/static/templ/contests/proset.html b/src/static/templ/contests/proset.html index 6dd4b831..28d7a1aa 100644 --- a/src/static/templ/contests/proset.html +++ b/src/static/templ/contests/proset.html @@ -73,9 +73,10 @@ - {% for idx, pro in enumerate(prolist) %} + {% for pro in prolist %} {% set pro_id = pro.pro_id %} + {% set idx = pro_idx_map[pro_id] %} {{ pro_id }} {% if score_map[pro_id]['state'] is None %} Todo diff --git a/src/static/templ/contests/scoreboard.html b/src/static/templ/contests/scoreboard.html index 62935497..b206d651 100644 --- a/src/static/templ/contests/scoreboard.html +++ b/src/static/templ/contests/scoreboard.html @@ -1,4 +1,5 @@ {% import datetime %} +{% from services.chal import ChalConst %} {% from services.contests import ContestMode %} {% from utils.htmlgen import pro_idx_to_pro_alphabet %} @@ -103,6 +104,10 @@ } /** + * @function compare_timestamp + * @description Compare two timestamp strings in "MM:SS" format + * @description p > q => return true + * @description p <= q => return false * @argument p { String } * @argument q { String } * @returns { number } @@ -115,10 +120,10 @@ q_min = parseInt(q_min); q_sec = parseInt(q_sec); q_min = isNaN(q_min) ? 0 : q_min; - q_sec = isNaN(q_min) ? 0 : q_sec; + q_sec = isNaN(q_sec) ? 0 : q_sec; - if (p_min == q_min) return p_sec > q_sec; - return p_min > q_min; + if (p_min != q_min) return p_min > q_min; + return p_sec > q_sec; } // from https://www.explainthis.io/zh-hant/swe/throttle @@ -220,30 +225,150 @@ * @type { Array } */ let scores = res.data; - scores.sort((p, q) => { - if (q.total_score == p.total_score) { - let p_max_timestamp = ''; - let q_max_timestamp = ''; + {% if contest.contest_mode is ContestMode.IOI %} + render_ioi_style_scoreboard(scores); + {% elif contest.contest_mode is ContestMode.RANDOM_SET %} + render_randomset_style_scoreboard(scores); + {% end %} + }); + } + + {% if contest.contest_mode is ContestMode.RANDOM_SET %} - for (const [_, score] of Object.entries(p.scores)) { - if (compare_timestamp(score.timestamp, p_max_timestamp)) p_max_timestamp = score.timestamp; + /** + * @argument scores_data { Array } + */ + function render_randomset_style_scoreboard(scores_data) { + /** + * A map of problem_id to first kill acct_id + * @type { Map } + */ + let first_kill_map = new Map(); + scores_data.forEach(scores => { + scores.ac_count = Object.values(scores.scores).filter(score => score.state == {{ ChalConst.STATE_AC }}).length; + scores.fail_count = Object.values(scores.scores).reduce((acc, score) => acc + score.fail_count, 0); + scores.max_timestamp = Object.values(scores.scores).reduce((max, score) => + compare_timestamp(score.timestamp, max) ? score.timestamp : max, ''); + scores.latest_score_changed_timestamp = Object.values(scores.scores) + .filter(score => score.score !== 0) + .reduce((latest, score) => + compare_timestamp(score.timestamp, latest) ? score.timestamp : latest, ''); + + Object.entries(scores.scores) + .filter(([_, score]) => score.state === {{ ChalConst.STATE_AC }}) + .forEach(([pro_id, score]) => { + const shouldUpdate = !first_kill_map.has(pro_id) || + compare_timestamp(first_kill_map.get(pro_id).timestamp, score.timestamp); + + if (shouldUpdate) { + first_kill_map.set(pro_id, { + acct_id: scores.acct_id, + timestamp: score.timestamp + }); } + }); + }); + scores_data.forEach(scores => { + scores.first_kill_count = Object.entries(scores.scores) + .filter(([_, score]) => score.state === {{ ChalConst.STATE_AC }}) + .filter(([pro_id]) => first_kill_map.get(pro_id).acct_id === scores.acct_id) + .length; + }); + + scores_data.sort((p, q) => { + const p_not_have_challenges = Object.keys(p.scores).length === 0; + const q_not_have_challenges = Object.keys(q.scores).length === 0; + if (p_not_have_challenges || q_not_have_challenges) return p_not_have_challenges - q_not_have_challenges; + + if (q.total_score != p.total_score) return q.total_score - p.total_score; + if (q.ac_count != p.ac_count) return q.ac_count - p.ac_count; + if (q.first_kill_count != p.first_kill_count) return q.first_kill_count - p.first_kill_count; + if (q.fail_count != p.fail_count) return p.fail_count - q.fail_count; + return compare_timestamp(p.latest_score_changed_timestamp, q.latest_score_changed_timestamp) ? 1 : -1; + }); + + const pro_list = {{ [*range(len(contest.pro_sets))] }}; + let each_ranks_html = ''; + let cur_rank = 0; + let cur_total_score = 0; + for (const [_, scores] of scores_data.entries()) { + if (scores.total_score != cur_total_score) { + cur_total_score = scores.total_score; + cur_rank += 1; + } - for (const [_, score] of Object.entries(q.scores)) { - if (compare_timestamp(score.timestamp, q_max_timestamp)) q_max_timestamp = score.timestamp; + let each_problems_score_html = ''; + let max_timestamp = scores.max_timestamp; + for (const pro_order of pro_list) { + if (!(pro_order in scores.scores)) { + each_problems_score_html += ` + +

-

+ + `; + } else { + let score = scores.scores[pro_order]; + let score_idx = parseInt(score.score / 10); + if (score_idx > 10) score_idx = 10; + if (score_idx < 0) score_idx = 0; + if (score.state == {{ ChalConst.STATE_AC }}) { + score_idx = 10; } + let score_color_class_html = score_color_class_map[score_idx]; - return compare_timestamp(p_max_timestamp, q_max_timestamp) ? 1 : -1; + each_problems_score_html += ` + +

${score.score}

+

${score.timestamp}

+ + `; } + } - return q.total_score - p.total_score; - }); + each_ranks_html += ` + + ${cur_rank} + ${scores.name} + ${each_problems_score_html} + +

${scores.total_score}

+

${max_timestamp}

+ + + `; + } - {% if contest.contest_mode is ContestMode.IOI %} - render_ioi_style_scoreboard(scores); - {% end %} - }); + let scoreboard_html = ` + + + + + + {% if contest.is_admin(user) or not contest.is_member(user) %} + {% for i in range(len(contest.pro_sets)) %} + + {% end %} + {% else %} + {% for idx, pro_id in enumerate(contest.get_prolist_from_acct_by_ip(user)) %} + + {% end %} + {% end %} + + + + + ${each_ranks_html} + +
RankUsername + {{ pro_idx_to_pro_alphabet(i) }} + + {{ pro_idx_to_pro_alphabet(idx) }} + Total
+ `; + + $("#scoreboard").html(scoreboard_html); } + {% end %} {% if contest.contest_mode is ContestMode.IOI %} @@ -251,6 +376,20 @@ * @argument scores_data { Array } */ function render_ioi_style_scoreboard(scores_data) { + scores_data.forEach(scores => { + scores.max_timestamp = Object.values(scores.scores).reduce((max, score) => + compare_timestamp(score.timestamp, max) ? score.timestamp : max, ''); + }); + + scores_data.sort((p, q) => { + const p_not_have_challenges = Object.keys(p.scores).length === 0; + const q_not_have_challenges = Object.keys(q.scores).length === 0; + if (p_not_have_challenges || q_not_have_challenges) return p_not_have_challenges - q_not_have_challenges; + + if (q.total_score != p.total_score) return q.total_score - p.total_score; + return compare_timestamp(p.max_timestamp, q.max_timestamp) ? 1 : -1; + }); + let pro_list = {{ [pro_id for pro_id in contest.pro_list.keys()] }}; let each_ranks_html = ''; let cur_rank = 0; @@ -262,7 +401,7 @@ } let each_problems_score_html = ''; - let max_timestamp = '00:00'; + let max_timestamp = scores.max_timestamp; for (const pro_id of pro_list) { if (!(pro_id in scores.scores)) { each_problems_score_html += ` @@ -283,10 +422,6 @@

${score.timestamp}

`; - - if (compare_timestamp(score.timestamp, max_timestamp)) { - max_timestamp = score.timestamp; - } } }