core&ui: fix contest with more than 26 problems have wrong alphabetic id (#1008)

Co-authored-by: undefined <i@undefined.moe>
This commit is contained in:
Bhscerandundefined authored and GitHub committed 2025-07-07 15:31:46 +01:00
1 parent 627ce77d30
commit dc6345c0b3
11 files changed
+30 -18

No files matched your search

+3 -3
View File
@@ -5,7 +5,7 @@ import { escapeRegExp, pick } from 'lodash';
import moment from 'moment-timezone'; import moment from 'moment-timezone';
import { ObjectId } from 'mongodb'; import { ObjectId } from 'mongodb';
import { import {
Counter, diffArray, randomstring, sortFiles, Time, yaml, Counter, diffArray, getAlphabeticId, randomstring, sortFiles, Time, yaml,
} from '@hydrooj/utils/lib/utils'; } from '@hydrooj/utils/lib/utils';
import { Context, Service } from '../context'; import { Context, Service } from '../context';
import { import {
@@ -132,7 +132,7 @@ export class ContestDetailBaseHandler extends Handler {
}, },
{ {
name: 'problem_detail', name: 'problem_detail',
displayName: `${String.fromCharCode(65 + this.tdoc.pids.indexOf(pdoc.docId))}. ${pdoc.title}`, displayName: `${getAlphabeticId(this.tdoc.pids.indexOf(pdoc.docId))}. ${pdoc.title}`,
args: { query: { tid }, pid: pdoc.docId, prefix: 'contest_detail_problem' }, args: { query: { tid }, pid: pdoc.docId, prefix: 'contest_detail_problem' },
checker: () => 'pdoc' in this, checker: () => 'pdoc' in this,
}, },
@@ -791,7 +791,7 @@ export async function apply(ctx: Context) {
const teamIds: Record<number, number> = {}; const teamIds: Record<number, number> = {};
for (let i = 1; i <= teams.length; i++) teamIds[teams[i - 1].uid] = i; for (let i = 1; i <= teams.length; i++) teamIds[teams[i - 1].uid] = i;
const time = (t: ObjectId) => Math.floor((t.getTimestamp().getTime() - tdoc.beginAt.getTime()) / Time.second); const time = (t: ObjectId) => Math.floor((t.getTimestamp().getTime() - tdoc.beginAt.getTime()) / Time.second);
const pid = (i: number) => String.fromCharCode(65 + i); const pid = (i: number) => getAlphabeticId(i);
const escape = (i: string) => i.replace(/[",]/g, ''); const escape = (i: string) => i.replace(/[",]/g, '');
const unknownSchool = this.translate('Unknown School'); const unknownSchool = this.translate('Unknown School');
const statusMap = { const statusMap = {
+6 -4
View File
@@ -1,6 +1,8 @@
import { sumBy } from 'lodash'; import { sumBy } from 'lodash';
import { Filter, ObjectId } from 'mongodb'; import { Filter, ObjectId } from 'mongodb';
import { Counter, formatSeconds, Time } from '@hydrooj/utils/lib/utils'; import {
Counter, formatSeconds, getAlphabeticId, Time,
} from '@hydrooj/utils/lib/utils';
import { import {
ContestAlreadyAttendedError, ContestNotFoundError, ContestAlreadyAttendedError, ContestNotFoundError,
ContestScoreboardHiddenError, ValidationError, ContestScoreboardHiddenError, ValidationError,
@@ -158,7 +160,7 @@ const acm = buildContestRule({
} else { } else {
columns.push({ columns.push({
type: 'problem', type: 'problem',
value: String.fromCharCode(65 + i - 1), value: getAlphabeticId(i - 1),
raw: pid, raw: pid,
}); });
} }
@@ -324,7 +326,7 @@ const oi = buildContestRule({
} else { } else {
columns.push({ columns.push({
type: 'problem', type: 'problem',
value: String.fromCharCode(65 + i - 1), value: getAlphabeticId(i - 1),
raw: tdoc.pids[i - 1], raw: tdoc.pids[i - 1],
}); });
} }
@@ -699,7 +701,7 @@ const homework = buildContestRule({
} else { } else {
columns.push({ columns.push({
type: 'problem', type: 'problem',
value: String.fromCharCode(65 + i - 1), value: getAlphabeticId(i - 1),
raw: pid, raw: pid,
}); });
} }
+2 -2
View File
@@ -1,6 +1,6 @@
import path from 'path'; import path from 'path';
import * as status from '@hydrooj/common/status'; import * as status from '@hydrooj/common/status';
import { findFileSync } from '@hydrooj/utils/lib/utils'; import { findFileSync, getAlphabeticId } from '@hydrooj/utils/lib/utils';
import { import {
avatar, Context, difficultyAlgorithm, fs, PERM, PRIV, Service, STATUS, yaml, avatar, Context, difficultyAlgorithm, fs, PERM, PRIV, Service, STATUS, yaml,
} from 'hydrooj'; } from 'hydrooj';
@@ -130,7 +130,7 @@ class Nunjucks extends nunjucks.Environment {
this.addGlobal('instanceof', (a, b) => a instanceof b); this.addGlobal('instanceof', (a, b) => a instanceof b);
this.addGlobal('paginate', misc.paginate); this.addGlobal('paginate', misc.paginate);
this.addGlobal('size', misc.size); this.addGlobal('size', misc.size);
this.addGlobal('utils', { status }); this.addGlobal('utils', { status, getAlphabeticId });
this.addGlobal('avatarUrl', avatar); this.addGlobal('avatarUrl', avatar);
this.addGlobal('formatSeconds', misc.formatSeconds); this.addGlobal('formatSeconds', misc.formatSeconds);
this.addGlobal('model', global.Hydro.model); this.addGlobal('model', global.Hydro.model);
@@ -1,4 +1,5 @@
/* eslint-disable react-refresh/only-export-components */ /* eslint-disable react-refresh/only-export-components */
import { getAlphabeticId } from '@hydrooj/utils/lib/common';
import yaml from 'js-yaml'; import yaml from 'js-yaml';
import React from 'react'; import React from 'react';
import { HexColorInput, HexColorPicker } from 'react-colorful'; import { HexColorInput, HexColorPicker } from 'react-colorful';
@@ -31,8 +32,8 @@ function Balloon({ tdoc, val }) {
<tr key={pid}> <tr key={pid}>
<td> <td>
{now === pid {now === pid
? (<b>{String.fromCharCode(65 + tdoc.pids.indexOf(+pid))}</b>) ? (<b>{getAlphabeticId(tdoc.pids.indexOf(+pid))}</b>)
: (<span>{String.fromCharCode(65 + tdoc.pids.indexOf(+pid))}</span>)} : (<span>{getAlphabeticId(tdoc.pids.indexOf(+pid))}</span>)}
</td> </td>
<td> <td>
<HexColorInput <HexColorInput
@@ -8,5 +8,5 @@
{% endmacro %} {% endmacro %}
{% macro render_clarification_subject(tdoc, pdict, subject) %} {% macro render_clarification_subject(tdoc, pdict, subject) %}
{% if subject == 0 %}{{ _('General Issue') }}{% elif subject == -1 %}{{ _('Technical Issue') }}{% else %}{{ String.fromCharCode(65 + tdoc.pids.indexOf(subject)) + 1 }}. {{ pdict[subject].title }}{% endif %} {% if subject == 0 %}{{ _('General Issue') }}{% elif subject == -1 %}{{ _('Technical Issue') }}{% else %}{{ utils.getAlphabeticId(tdoc.pids.indexOf(subject)) }}. {{ pdict[subject].title }}{% endif %}
{% endmacro %} {% endmacro %}
@@ -5,7 +5,7 @@
<a href="{{ url('problem_detail', _linkArgs) }}"{% if small %} data-tooltip="{{ pdoc.title }}"{% endif %}> <a href="{{ url('problem_detail', _linkArgs) }}"{% if small %} data-tooltip="{{ pdoc.title }}"{% endif %}>
{%- endif -%} {%- endif -%}
{% if show_pid %} {% if show_pid %}
<b>{%- if tdoc and alphabetic -%}{{ String.fromCharCode(65+tdoc.pids.indexOf(pdoc.docId)) }} <b>{%- if tdoc and alphabetic -%}{{ utils.getAlphabeticId(tdoc.pids.indexOf(pdoc.docId)) }}
{%- elif pdoc.pid and pdoc.pid.includes('-') -%}{{ pdoc.pid.split('-').join('#') }} {%- elif pdoc.pid and pdoc.pid.includes('-') -%}{{ pdoc.pid.split('-').join('#') }}
{%- else -%}{{ pdoc.pid|default(pdoc.docId) }}{%- endif -%}</b> {%- else -%}{{ pdoc.pid|default(pdoc.docId) }}{%- endif -%}</b>
{% endif %} {% endif %}
@@ -127,7 +127,7 @@
<option value="0">{{ _('General Issue') }}</option> <option value="0">{{ _('General Issue') }}</option>
<option value="-1">{{ _('Technical Issue') }}</option> <option value="-1">{{ _('Technical Issue') }}</option>
{% for pid in tdoc.pids %} {% for pid in tdoc.pids %}
<option value="{{ pid }}">{{ String.fromCharCode(65+loop.index0) }}. {{ pdict[pid].title }}</option> <option value="{{ pid }}">{{ utils.getAlphabeticId(loop.index0) }}. {{ pdict[pid].title }}</option>
{% endfor %} {% endfor %}
</select> </select>
</label> </label>
@@ -185,7 +185,7 @@
<option value="0">{{ _('General Issue') }}</option> <option value="0">{{ _('General Issue') }}</option>
<option value="-1">{{ _('Technical Issue') }}</option> <option value="-1">{{ _('Technical Issue') }}</option>
{% for pid in tdoc.pids %} {% for pid in tdoc.pids %}
<option value="{{ pid }}">{{ String.fromCharCode(65+loop.index0) }}. {{ pdict[pid].title }}</option> <option value="{{ pid }}">{{ utils.getAlphabeticId(loop.index0) }}. {{ pdict[pid].title }}</option>
{% endfor %} {% endfor %}
</select> </select>
</label> </label>
@@ -25,7 +25,7 @@
</button> </button>
</form> </form>
{% endif %} {% endif %}
<b>{{ String.fromCharCode(65+tdoc.pids.indexOf(bdoc.pid)) }}</b>&nbsp;&nbsp;({{ tdoc.balloon[bdoc.pid].name }}) <b>{{ utils.getAlphabeticId(tdoc.pids.indexOf(bdoc.pid)) }}</b>&nbsp;&nbsp;({{ tdoc.balloon[bdoc.pid].name }})
</td> </td>
<td class="col--submit-by" data-tooltip="{{ bdoc._id.getTimestamp().format() }}">{{ user.render_inline(udict[bdoc.uid], badge=false) }}</td> <td class="col--submit-by" data-tooltip="{{ bdoc._id.getTimestamp().format() }}">{{ user.render_inline(udict[bdoc.uid], badge=false) }}</td>
<td class="col--send-by"{% if bdoc.sent %} data-tooltip="{{ bdoc.sentAt.format() }}"{% endif %}> <td class="col--send-by"{% if bdoc.sent %} data-tooltip="{{ bdoc.sentAt.format() }}"{% endif %}>
@@ -64,7 +64,7 @@
</form> </form>
{% endif %} {% endif %}
{%- if tdoc -%} {%- if tdoc -%}
{{ String.fromCharCode(65+tdoc.pids.indexOf(pdoc.docId)) }} {{ utils.getAlphabeticId(tdoc.pids.indexOf(pdoc.docId)) }}
{%- elif pdoc.pid and pdoc.pid.includes('-') -%} {%- elif pdoc.pid and pdoc.pid.includes('-') -%}
{{ pdoc.pid.split('-').join('#') }} {{ pdoc.pid.split('-').join('#') }}
{%- else -%} {%- else -%}
@@ -80,7 +80,7 @@
<a href="{{ url('problem_detail', pid=pid, query={tid:tdoc.docId}) }}" <a href="{{ url('problem_detail', pid=pid, query={tid:tdoc.docId}) }}"
class="{% if pass %}pass{% elif fail %}fail{% endif %} class="{% if pass %}pass{% elif fail %}fail{% endif %}
{% if pid==pdoc.docId %} active{% endif %}"> {% if pid==pdoc.docId %} active{% endif %}">
<span class="id">{{ String.fromCharCode(65+loop.index0) }}</span> <span class="id">{{ utils.getAlphabeticId(loop.index0) }}</span>
{% if status %}<span class="icon icon-{% if pass %}check{% else %}close{% endif %}"></span>{% endif %} {% if status %}<span class="icon icon-{% if pass %}check{% else %}close{% endif %}"></span>{% endif %}
</a> </a>
{%- endfor %} {%- endfor %}
+9
View File
@@ -236,3 +236,12 @@ export function sortFiles(files: Record<string, any>[] | string[], key = '_id')
}); });
return result.map((x) => (isString ? x.name : (delete x._weights && x))); return result.map((x) => (isString ? x.name : (delete x._weights && x)));
} }
export const getAlphabeticId = (() => {
// A...Z, AA...AZ, BA...BZ, ...
const f = (a: number) => (a < 0 ? '' : f(a / 26 - 1) + String.fromCharCode((a % 26) + 65)) as string;
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
const cache = alphabet.split('');
for (const ch of alphabet) cache.push(...alphabet.split('').map((c) => ch + c));
return (i: number) => cache[i] || (i < 0 ? '?' : f(i));
})();