HydroOJ 从入门到入土(9)源码简易修改记录——卍解!

发布时间 2023-12-21 16:35:50作者: Bowen404

随着 OJ 的使用越来越深入, 本强迫症总会觉得一些细节有时候不那么符合自己的习惯, 但是想改又无处下手, 最终还是走上了修改源码的邪路.

0. 重要

  1. 做好备份, 并先用测试机做好测试.
  2. 慎用各种脚本, 除非知道都做了些什么, 并能全部恢复.
  3. 能写插件修改的尽量用插件, 方便很多, 风险也小.
  4. 修改源码相当于打开了基因锁, 或者卍解. 带来强大的自定义功能的同时, 也可能会导致一些不可预测的风险, 所以一定要保存好每一次的修改记录. 建议改一处就重启一次看效果, 方便失败恢复.
  5. 尽量使用现代化的IDE, 方法提示很有用.
  6. 每次更新之后, 所有修改都需要重新再来一遍, 修改方法可能会因版本更新而失效.
  7. 本人不懂 JS, 甚至 HTML/css 都是现学的, 所以只能照着改改逻辑代码, 这是我个人的操作记录, 并非指南, 仅供参考. 错误在所难免, 欢迎交流指正. 如果出现意外, 本人概不负责.

1. 超级管理员查看自测代码

前置小技巧: 对于任意查询记录的界面, 都可以通过在 url 后边加上 &allDomain=1 来查看所有的提交记录.

第一次打开这个开关之后, 我就看到一些后边标注了 (自测) 的代码, 但是一点击, 发现管理员无法查看.

能查看自测的话, 给学生指导代码会更方便, 所以就想改一下这个功能.

这个问题的核心是权限, 这里出于不污染其他权限的考虑, 选了只有超级管理员才有的权限, 即拥有所有权限(PRIV_ALL).

需要用其他权限的话, 可以参考 HydroOJ 从入门到入土(3)权限管理

先观察 Hydro源码 , 发现有个自测特判, 于是加上个管理权限特判(!this.user.hasPriv(PRIV.PRIV_ALL) && )就行.

# 可能行数不一致, 上下瞅瞅
vi +161 /usr/local/share/.config/yarn/global/node_modules/hydrooj/src/handler/record.ts
//            if (rdoc.uid !== this.user._id) throw new PermissionError(PERM.PERM_READ_RECORD_CODE);
改成超级管理员特权:
            if (!this.user.hasPriv(PRIV.PRIV_ALL) && rdoc.uid !== this.user._id) throw new PermissionError(PERM.PERM_READ_RECORD_CODE);
pm2 restart hydrooj

2. 超级管理员隐身查看比赛 / 作业题目

在使用过程中, 我发现在一个作业或者比赛中, 如果在 成绩表所有递交中, 点击题目, 会显示没参加所以没有权限访问.

不改源码的解决方法有上中下三策:

  1. 上策: 写插件, 但是这个属于后端插件, 需要对项目结构有一定理解, 不然会浪费大量时间.
  2. 中策: 仔细观察, 是带上了?tid=xxx这样一个参数, 让没参赛的人无法访问, 于是删之即可正常访问.
  3. 下策: 参加/认领后再查看, 但是成绩榜单上会留下自己的名字.

最后我果然还是选了中策, 但每次都要删除那一串东西, 实在很麻烦, 而且不符合超级管理员的身份, 所以只能想办法改之.

观察Hydro源码, 发现跟上边的修改类似, 在 !this.tsdoc?.attend 这种判定之前, 加上一个权限特判即可.

# 改这里可以隐身查看作业题目
# 可能行数不一致, 上下瞅瞅
vi +331 /usr/local/share/.config/yarn/global/node_modules/hydrooj/src/handler/problem.ts
//            if (!contest.isDone(this.tdoc, this.tsdoc) && (!this.tsdoc?.attend || !this.tsdoc.startAt)) throw new ContestNotAttendedError(tid);

            if (!this.user.hasPriv(PRIV.PRIV_ALL) && !contest.isDone(this.tdoc, this.tsdoc) && (!this.tsdoc?.attend || !this.tsdoc.startAt)) throw new ContestNotAttendedError(tid);
pm2 restart hydrooj

3. 超级管理员隐身查看比赛题目列表

同上, 在比赛中, 点击题目列表, 会显示没参加所以没有权限访问.

观察Hydro源码, 发现跟上边的修改类似, 在 !this.tsdoc?.attend 这种判定之前, 加上一个权限特判即可.

# 改这里可以隐身查看比赛题目列表
# 可能行数不一致, 上下瞅瞅
vi +260 /usr/local/share/.config/yarn/global/node_modules/hydrooj/src/handler/contest.ts
//        if (!this.tsdoc?.attend && !contest.isDone(this.tdoc)) throw new ContestNotAttendedError(domainId, tid);

        if (!this.user.hasPriv(PRIV.PRIV_ALL) && !this.tsdoc?.attend && !contest.isDone(this.tdoc)) throw new ContestNotAttendedError(domainId, tid);
pm2 restart hydrooj

4. 关掉客观题的多选题部分分

多选题我倾向于要么全对, 要么全错的判题方式, 加大难度.

Hydro 目前默认是有部分分, 所以改掉.

观察Hydro源码, 注释掉'Partially Correct'判断即可.

# 可能行数不一致, 上下瞅瞅
vi +71 /usr/local/share/.config/yarn/global/node_modules/@hydrooj/hydrojudge/src/judge/objective.ts
//                 else if (ans.size && Set.isSuperset(stdSet, ans)) report(STATUS.STATUS_WRONG_ANSWER, Math.floor(fullScore / 2), 'Partially Correct');
pm2 restart hydrooj

5. 修改题目页面title,加上pid

目前的页面 title 长这样:题目详情 - A+B Problem - HydroOJ, 但是没有题号信息, 而且标签多了之后, 后边的信息就会被挤掉, 所有的标签页都会变成题目详情..., 难以找到相应的题目, 我希望能改为洛谷这种 title, 比如 P1001 A+B Problem - 洛谷 | 计算机科学教育新生态, 这样标签多了也能一眼找到自己要的题.

Hydro 目前默认是有部分分, 所以改掉.

观察Hydro源码, 发现其实只要检测到pid就提前返回一个含pidtitle即可.

# 可能行数不一致, 上下瞅瞅
vi +151 /usr/local/share/.config/yarn/global/node_modules/hydrooj/src/service/server.ts
// 在原来的 151 行前边插入这一行
if (this.pdoc?.pid && this.UiContext.extraTitleContent) return `${this.pdoc.pid} ${this.UiContext.extraTitleContent} - ${name}`;

// 改完之后:
if (this.pdoc?.pid && this.UiContext.extraTitleContent) return `${this.pdoc.pid} ${this.UiContext.extraTitleContent} - ${name}`;

if (this.UiContext.extraTitleContent) return `${this.translate(str)} - ${this.UiContext.extraTitleContent} - ${name}`;

pm2 restart hydrooj

5. 修改作业 / 训练 / 比赛页面title,加上名字

当前title: 作业/训练/比赛详情 - Hydro,

改完title, 以作业为例: xxx的作业 - 作业详情 - Hydro

注: 写的比较麻烦, 因为只有比赛给了单独的 this.tdoc, 作业和训练没给.

// 如果修改了题目页面 title, 就把这个修改放到 this.pdoc?.pid 判断后边

// 在原来的 151 行前边插入这一行
if (this.response?.body?.tdoc?.title ) return `${this.response.body.tdoc.title} - ${this.translate(str)} - ${name}`;

// 改完之后

if (this.response?.body?.tdoc?.title ) return `${this.response.body.tdoc.title} - ${this.translate(str)} - ${name}`;

if (this.UiContext.extraTitleContent) return `${this.translate(str)} - ${this.UiContext.extraTitleContent} - ${name}`;
pm2 restart hydrooj

5.1 作业详情页增加作业标题块(插件)

此外, 如果需要在作业内部和比赛一样, 显示作业名称, 起止日期等信息, 可以复制一个模版homework_detail.html, 然后在第 7<div class="medium-9 columns">下边增加一个section, 之后塞到插件里即可.

section内容如下, 效果参考比赛详情页的比赛标题块:

    <div class="section">
      <div class="section__header" style="align: center">
        <h1 class="section__title">{{ tdoc.title }}</h1>
      </div>
      <div class="section__body">
        <span class="bp5-tag bp5-large bp5-minimal problem__tag-item">{{ _(model.contest.statusText(tdoc, tsdoc)) }}</span>
        <span class="bp5-tag bp5-large bp5-minimal problem__tag-item icon icon-award">{{ _(model.contest.RULES[tdoc.rule].TEXT) }}</span>
        <span class="bp5-tag bp5-large bp5-minimal problem__tag-item">{{ _('Start at') }}: {{ datetimeSpan(tdoc['beginAt'])|safe }}</span>
        <span class="bp5-tag bp5-large bp5-minimal problem__tag-item icon icon-schedule">{% if model.contest.isExtended(tdoc) or model.contest.isDone(tdoc) %}
                    {{ _('Hard Deadline') }}: {{ datetimeSpan(tdoc['endAt'])|safe }}
                  {% else %}
                    {{ _('Deadline') }}: {{ datetimeSpan(tdoc['penaltySince'])|safe }}
                  {% endif %}</span>
        <span class="bp5-tag bp5-large bp5-minimal problem__tag-item">{{ _('Host') }}: {{ user.render_inline(udict[tdoc.owner], badge=false) }}</span>
        <span class="bp5-tag bp5-large bp5-minimal problem__tag-item icon icon-user--multiple">{{ tdoc['attend']|default(0) }}</span>
        {% if tsdoc.attend %}
          <span class="bp5-tag bp5-large bp5-minimal problem__tag-item icon icon-check">{{ _('Attended') }}</span>
        {% endif %}
      </div>
    </div>

5.2 训练详情页增加训练标题块(插件)

训练详情页看不到题单标题, 有点迷惑.

可以复制一个模版training_detail.html, 然后在第 44<div class="section">下边增加一个h1的header, 之后塞到插件里即可.

section内容如下, 效果就是在训练详情页加入一个标题:

      <div class="section__header" style="align: center">
        <h1 class="section__title">{{ tdoc.title }}</h1>
      </div>

如果觉得不够显眼, 也参考前边可以单独做一个section, 但是注意, 状态等信息已经在右侧了, 不需要再重复添加.