diff --git a/packages/hydrooj/locales/en.yaml b/packages/hydrooj/locales/en.yaml index 43cca839..7d674f01 100644 --- a/packages/hydrooj/locales/en.yaml +++ b/packages/hydrooj/locales/en.yaml @@ -49,6 +49,7 @@ manage: System Manage no_translation_warn:
This part of content is under translation.
page.problem_detail.sidebar.show_category: Click to Show page.training_detail.invalid_when_not_enrolled: You cannot view problem details unless enrolled. +page.training_detail.see_other_user_detail: You are viewing user {0} training details. pager_first: « First pager_last: Last » pager_next: Next › diff --git a/packages/hydrooj/locales/zh.yaml b/packages/hydrooj/locales/zh.yaml index 3d3a976f..28f58501 100644 --- a/packages/hydrooj/locales/zh.yaml +++ b/packages/hydrooj/locales/zh.yaml @@ -482,6 +482,7 @@ My Files: 我的文件 My Profile: 我的资料 My Recent Submissions: 我的最近递交记录 My Role: 我的角色 +My status: 我的状态 name: 名称 Name: 名称 Network error: 网络错误 @@ -537,6 +538,7 @@ Output Format: 输出格式 Owner: 所有者 page.problem_detail.sidebar.show_category: 点击显示 page.training_detail.invalid_when_not_enrolled: 未参加训练计划时您不能查看题目详情。 +page.training_detail.see_other_user_detail: 您正在查看用户 {0} 的训练详情。 pager_first: « 第一页 pager_last: 末页 » pager_next: 下一页 › diff --git a/packages/hydrooj/src/handler/training.ts b/packages/hydrooj/src/handler/training.ts index ea840a2a..9ddc92f3 100644 --- a/packages/hydrooj/src/handler/training.ts +++ b/packages/hydrooj/src/handler/training.ts @@ -91,16 +91,30 @@ class TrainingMainHandler extends Handler { class TrainingDetailHandler extends Handler { @param('tid', Types.ObjectID) - async get(domainId: string, tid: ObjectID) { + @param('uid', Types.PositiveInt, true) + @param('pjax', Types.Boolean, true) + async get(domainId: string, tid: ObjectID, uid: number, pjax: boolean) { const tdoc = await training.get(domainId, tid); await bus.parallel('training/get', tdoc, this); + let targetUser = this.user._id; + let enrollUsers = []; + let shouldCompare = false; const pids = training.getPids(tdoc.dag); + if (this.user.hasPriv(PRIV.PRIV_USER_PROFILE)) { + enrollUsers = (await training.getMultiStatus(domainId, { docId: tid, uid: { $gt: 1 } }) + .project({ uid: 1 }).limit(500).toArray()).map((x) => +x.uid); + if (uid) { + targetUser = uid; + shouldCompare = targetUser !== this.user._id; + } + } const canViewHidden = this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN) || this.user._id; - const [owner, pdict] = await Promise.all([ - user.getById(domainId, tdoc.owner), + const [udict, pdict] = await Promise.all([ + user.getList(domainId, [tdoc.owner, ...enrollUsers]), problem.getList(domainId, pids, canViewHidden, true), ]); - const psdict = await problem.getListStatus(domainId, this.user._id, pids); + const psdict = await problem.getListStatus(domainId, targetUser, pids); + const selfPsdict = shouldCompare ? await problem.getListStatus(domainId, this.user._id, pids) : {}; const donePids = new Set(); const progPids = new Set(); for (const pid in psdict) { @@ -134,10 +148,13 @@ class TrainingDetailHandler extends Handler { donePids: Array.from(donePids), done: doneNids.size === tdoc.dag.length, }); - this.response.template = 'training_detail.html'; this.response.body = { - tdoc, tsdoc, pids, pdict, psdict, ndict, nsdict, owner, + tdoc, tsdoc, pids, pdict, psdict, ndict, nsdict, udict, enrollUsers, selfPsdict, }; + if (pjax) { + const html = await this.renderHTML('partials/training_detail.html', this.response.body); + this.response.body = { fragments: [{ html }] }; + } else this.response.template = 'training_detail.html'; } @param('tid', Types.ObjectID) diff --git a/packages/hydrooj/src/service/decorators.ts b/packages/hydrooj/src/service/decorators.ts index 77d45357..025486ea 100644 --- a/packages/hydrooj/src/service/decorators.ts +++ b/packages/hydrooj/src/service/decorators.ts @@ -224,7 +224,7 @@ export function requireSudo(target: any, funcName: string, obj: any) { method: this.request.method, referer: this.request.headers.referer, args: this.args, - redirect: this.request.path, + redirect: this.request.originalPath, }; this.response.redirect = this.url('user_sudo'); return 'cleanup'; diff --git a/packages/ui-default/components/message/index.page.ts b/packages/ui-default/components/message/index.page.ts index 84bf6c95..d23d0e10 100644 --- a/packages/ui-default/components/message/index.page.ts +++ b/packages/ui-default/components/message/index.page.ts @@ -39,7 +39,7 @@ const onmessage = (msg) => { const url = new URL(`${UiContext.ws_prefix}home/messages-conn`, window.location.href); // TODO handle a better way for cookie -url.searchParams.append('sid', document.cookie); +if (url.host !== window.location.host) url.searchParams.append('sid', document.cookie.split('sid=')[1].split(';')[0]); const endpoint = url.toString().replace('http', 'ws'); const initWorkerMode = () => { diff --git a/packages/ui-default/components/socket/index.js b/packages/ui-default/components/socket/index.js index 6bb9b0e3..c6dc743e 100644 --- a/packages/ui-default/components/socket/index.js +++ b/packages/ui-default/components/socket/index.js @@ -3,6 +3,7 @@ import ReconnectingWebSocket from 'reconnecting-websocket'; export default class Sock { constructor(url) { const i = new URL(url, window.location.href); + if (i.host !== window.location.host) i.searchParams.append('sid', document.cookie.split('sid=')[1].split(';')[0]); i.protocol = i.protocol.replace('http', 'ws'); this.url = i.toString(); this.sock = new ReconnectingWebSocket(this.url); diff --git a/packages/ui-default/misc/structure.styl b/packages/ui-default/misc/structure.styl index 0291a22b..2476e215 100644 --- a/packages/ui-default/misc/structure.styl +++ b/packages/ui-default/misc/structure.styl @@ -16,6 +16,10 @@ .mode--scratchpad .scratchpad--hide display: none +.mobile--hide + +mobile() + display: none + ::-webkit-scrollbar width: 8px; height: 8px; diff --git a/packages/ui-default/pages/training_detail.page.styl b/packages/ui-default/pages/training_detail.page.styl index 99dbae54..8a7d88ba 100644 --- a/packages/ui-default/pages/training_detail.page.styl +++ b/packages/ui-default/pages/training_detail.page.styl @@ -3,6 +3,9 @@ width: rem(150px) position: relative + &.col--status--sm + width: rem(84px) + .col--tried, .col--ac, .col--difficulty width: rem(70px) text-align: center @@ -33,3 +36,13 @@ &.collapsed .training__section__detail display: none + + #menu-item-training_detail ul + max-height: calc(85vh - 345px) + overflow-y: auto + overflow-x: hidden + + .enroll_user_menu + max-height: 85vh + overflow-y: auto + overflow-x: hidden diff --git a/packages/ui-default/pages/training_detail.page.ts b/packages/ui-default/pages/training_detail.page.ts index 672ec90e..5cd44f4c 100644 --- a/packages/ui-default/pages/training_detail.page.ts +++ b/packages/ui-default/pages/training_detail.page.ts @@ -1,25 +1,82 @@ import $ from 'jquery'; +import _ from 'lodash'; import { NamedPage } from 'vj/misc/Page'; +import pjax from 'vj/utils/pjax'; import { slideDown, slideUp } from 'vj/utils/slide'; -async function handleSection(ev, type: string) { - const $section = $(ev.currentTarget).closest('.training__section'); - if ($section.is(`.${type}d, .animating`)) return; +type SectionAction = 'expand' | 'collapse'; +type SectionState = 'expanded' | 'collapsed'; + +function action2state(action: SectionAction): SectionState { + return action === 'expand' ? 'expanded' : 'collapsed'; +} + +async function setSectionState($section: JQuery, state: SectionState) { + if ($section.is(`.${state}, .animating`)) return; $section.addClass('animating'); const $detail = $section.find('.training__section__detail'); - if (type === 'expand') { + if (state === 'expanded') { await slideDown($detail, 300, { opacity: 0 }, { opacity: 1 }); } else { await slideUp($detail, 300, { opacity: 1 }, { opacity: 0 }); } - $section.addClass(type === 'expand' ? 'expanded' : 'collapsed'); - $section.removeClass(type === 'expand' ? 'collapsed' : 'expanded'); + $section.addClass(state); + $section.removeClass(state === 'expanded' ? 'collapsed' : 'expanded'); $section.removeClass('animating'); } +async function handleSection(ev: JQuery.ClickEvent, type: SectionAction) { + const $section = $(ev.currentTarget).closest('.training__section'); + await setSectionState($section, action2state(type)); +} + +function searchUser() { + const val = $('input[name=uid]').val().toString().toLowerCase(); + $('.enroll_user_menu_item').each((i, e) => { + const $item = $(e); + const $username = $item.data('uname').toString().toLowerCase(); + const $uid = $item.data('uid').toString(); + $item.toggle($username.includes(val) || $uid === val); + }); +} + +function selectUser(ev) { + ev.preventDefault(); + if ($('.enroll_user_menu_item:visible').length === 1) { + $('.enroll_user_menu_item:visible').first().find('a')[0].click(); + } +} + +function handleChooseUser(ev) { + ev.preventDefault(); + $('.enroll_user_menu_item .active').removeClass('active'); + $(ev.currentTarget).addClass('active'); + pjax.request({ url: ev.currentTarget.href }); +} + +async function handleSidebarClick(ev: JQuery.ClickEvent) { + const id = $(ev.currentTarget).attr('href'); + const $section = $(id).closest('.training__section'); + await setSectionState($section, 'expanded'); +} + +async function handleHashChange() { + const id = window.location.hash; + if (id.startsWith('#node-')) { + const $section = $(id).closest('.training__section'); + await setSectionState($section, 'expanded'); + } +} + const page = new NamedPage('training_detail', () => { + $('.search__input').on('input', _.debounce(searchUser, 500)); + $('#searchForm').on('submit', selectUser); $(document).on('click', '[name="training__section__expand"]', (ev) => handleSection(ev, 'expand')); $(document).on('click', '[name="training__section__collapse"]', (ev) => handleSection(ev, 'collapse')); + $(document).on('click', '.enroll_user_menu_item > a', (ev) => handleChooseUser(ev)); + $(document).on('click', '#menu-item-training_detail > ul > li > a', (ev) => handleSidebarClick(ev)); + window.addEventListener('hashchange', handleHashChange); + $(handleHashChange); }); export default page; diff --git a/packages/ui-default/templates/partials/training_detail.html b/packages/ui-default/templates/partials/training_detail.html new file mode 100644 index 00000000..ec90cf41 --- /dev/null +++ b/packages/ui-default/templates/partials/training_detail.html @@ -0,0 +1,164 @@ +{% import "components/record.html" as record with context %} +{% import "components/problem.html" as problem with context %} +{% set expanded = 0 %} +
+ {% if handler.request.query.uid and handler.request.query.uid != handler.user._id %} +
+

{{ _('page.training_detail.see_other_user_detail').format(udict[handler.request.query.uid].uname) }}

+
+ {% endif %} + {%- for node in tdoc['dag'] -%} +
+
+
+

{{ _('Section') }} {{ node['_id'] }}. {{ node['title'].split('\n')[0] }}

+ {% if node['title'].split('\n')[1] %}

{{ node['title'].split('\n')[1] }}

{% endif %} +
+
+

+ + {% if nsdict[node['_id']]['isDone'] %} + {{ _('Completed') }} + {% elif nsdict[node['_id']]['isProgress'] %} + {{ _('In Progress') }} + {% elif nsdict[node['_id']]['isOpen'] %} + {{ _('Open') }} + {% else %} + {{ _('Invalid') }} + {% endif %} +

+
+
+ +
+ {% if nsdict[node['_id']]['isInvalid'] %} +
+
+

{{ _('This section cannot be challenged at present, so please complete the following sections first') }}:

+
    + {%- for nid in node['requireNids'] -%} +
  • {{ _('Section') }} {{ _(nid) }}. {{ ndict[nid]['title'] }} ({{ _('Completed') }} {{ nsdict[nid]['progress'] }}%)
  • + {%- endfor -%} +
+
+
+ {% endif %} + {% if node['content'] %} +
+ {{ node['content']|markdown|safe }} +
+ {% endif %} +
+ {% set should_compare = (handler.request.query.uid or handler.user._id) != handler.user._id and tsdoc["enroll"] %} + + + {% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %} + {% if should_compare %} + + + {% else %} + + {% endif %} + {% endif %} + + + + + + + + {% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %} + {% if should_compare %} + + + {% else %} + + {% endif %} + {% endif %} + + + + + + + + {%- for pid in node['pids'] -%} + {% if pid in pdict %} + {% set pdoc=pdict[pid] %} + {% set psdoc=psdict[pid] %} + {% set self_psdoc=selfPsdict[pid] %} + + {% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %} + {% if psdoc['rid'] %} + {{ record.render_status_td(psdoc, rid_key='rid', short=should_compare) }} + {% else %} + + {% endif %} + {% if should_compare %} + {% if self_psdoc['rid'] %} + {{ record.render_status_td(self_psdoc, rid_key='rid', short=true) }} + {% else %} + + {% endif %} + {% endif %} + {% endif %} + + + + + + {% else %} + {% set pdoc = {'_id': pid, 'hidden': true, 'title': '*'} %} + + + + + + + + {% endif %} + {%- endfor -%} + +
{{ _('Status') }}{{ _('My status') }}{{ _('Status') }}{{ _('Problem') }}{{ _('Tried') }}{{ _('AC') }}{{ _('Difficulty') }}
+ {% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %} +
+ + + + +
+ {% endif %} + {{ problem.render_problem_title( + pdoc, + invalid=not tsdoc['enroll'], + show_tags=false + ) }} +
{{ pdoc.nSubmit }}{{ pdoc.nAccept }}{{ pdoc['difficulty'] or lib.difficulty(pdoc.nSubmit, pdoc.nAccept) or _('(None)') }}
+ + {{ problem.render_problem_title(pdoc, invalid=true) }} + ***
+
+
+
+ {%- endfor -%} +
 
+
\ No newline at end of file diff --git a/packages/ui-default/templates/record_main.html b/packages/ui-default/templates/record_main.html index 1cead145..463a59a9 100644 --- a/packages/ui-default/templates/record_main.html +++ b/packages/ui-default/templates/record_main.html @@ -40,7 +40,7 @@ -
+
diff --git a/packages/ui-default/templates/training_detail.html b/packages/ui-default/templates/training_detail.html index 8e4fff4b..825e2ff0 100644 --- a/packages/ui-default/templates/training_detail.html +++ b/packages/ui-default/templates/training_detail.html @@ -1,9 +1,40 @@ -{% import "components/record.html" as record with context %} -{% import "components/problem.html" as problem with context %} {% extends "layout/basic.html" %} {% block content %} -
+
+ {%- if enrollUsers.length -%} + +
+ {%- else -%}
+ {%- endif -%}
{{ tdoc['content'] }} @@ -22,141 +53,7 @@
- {%- for node in tdoc['dag'] -%} -
-
-
-

{{ _('Section') }} {{ node['_id'] }}. {{ node['title'].split('\n')[0] }}

- {% if node['title'].split('\n')[1] %}

{{ node['title'].split('\n')[1] }}

{% endif %} -
-
-

- - {% if nsdict[node['_id']]['isDone'] %} - {{ _('Completed') }} - {% elif nsdict[node['_id']]['isProgress'] %} - {{ _('In Progress') }} - {% elif nsdict[node['_id']]['isOpen'] %} - {{ _('Open') }} - {% else %} - {{ _('Invalid') }} - {% endif %} -

-
-
- -
- {% if nsdict[node['_id']]['isInvalid'] %} -
-
-

{{ _('This section cannot be challenged at present, so please complete the following sections first') }}:

-
    - {%- for nid in node['requireNids'] -%} -
  • {{ _('Section') }} {{ _(nid) }}. {{ ndict[nid]['title'] }} ({{ _('Completed') }} {{ nsdict[nid]['progress'] }}%)
  • - {%- endfor -%} -
-
-
- {% endif %} - {% if node['content'] %} -
- {{ node['content']|markdown|safe }} -
- {% endif %} -
- - - {% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %} - - {% endif %} - - - - - - - - {% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %} - - {% endif %} - - - - - - - - {%- for pid in node['pids'] -%} - {% if pid in pdict %} - {% set pdoc=pdict[pid] %} - {% set psdoc=psdict[pid] %} - - {% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %} - {% if psdoc['rid'] %} - {{ record.render_status_td(psdoc, rid_key='rid') }} - {% else %} - - {% endif %} - {% endif %} - - - - - - {% else %} - {% set pdoc = {'_id': pid, 'hidden': true, 'title': '*'} %} - - - - - - - - {% endif %} - {%- endfor -%} - -
{{ _('Status') }}{{ _('Problem') }}{{ _('Tried') }}{{ _('AC') }}{{ _('Difficulty') }}
- {% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %} -
- - - - -
- {% endif %} - {{ problem.render_problem_title( - pdoc, - invalid=not tsdoc['enroll'], - show_tags=false - ) }} -
{{ pdoc.nSubmit }}{{ pdoc.nAccept }}{{ pdoc['difficulty'] or lib.difficulty(pdoc.nSubmit, pdoc.nAccept) or _('(None)') }}
- - {{ problem.render_problem_title(pdoc, invalid=true) }} - ***
-
-
-
- {%- endfor -%} -
 
+ {% include "partials/training_detail.html" %}
@@ -195,15 +92,15 @@ {% endif %}
{{ _('Enrollees') }}
{{ tdoc.attend|default(0) }}
{{ _('Created By') }}
-
{{ user.render_inline(owner, badge=false) }}
+
{{ user.render_inline(udict[tdoc.owner], badge=false) }}
-
+
-
+
{% endblock %} \ No newline at end of file