diff --git a/hydro/handler/contest.js b/hydro/handler/contest.js index d7799a410..e499bdd82 100644 --- a/hydro/handler/contest.js +++ b/hydro/handler/contest.js @@ -69,7 +69,7 @@ class ContestDetailHandler extends ContestHandler { } async postAttend() { - if (contest.is_done(this.tdoc)) throw new ContestNotLiveError(this.tdoc._id); + if (contest.isDone(this.tdoc)) throw new ContestNotLiveError(this.tdoc._id); await contest.attend(this.tdoc._id, this.user._id); this.back(); } @@ -88,6 +88,17 @@ class ContestScoreboardHandler extends ContestDetailHandler { }; } } +class ContestScoreboardDownloadHandler extends ContestDetailHandler { + async get({ tid, ext }) { + const getContent = { + csv: async (rows) => `\uFEFF${rows.map((c) => (c.map((i) => i.value).join(','))).join('\n')}`, + html: (rows) => this.renderHTML('contest_scoreboard_download_html.html', { rows }), + }; + if (!getContent[ext]) throw new ValidationError('ext'); + const [, rows] = await this.getScoreboard(tid, true); + this.binary(await getContent[ext](rows), `${this.tdoc.title}.${ext}`); + } +} class ContestEditHandler extends ContestDetailHandler { async prepare({ tid }) { const tdoc = await contest.get(tid); @@ -150,9 +161,9 @@ class ContestProblemHandler extends ContestDetailHandler { ]); this.attended = this.tsdoc && this.tsdoc.attend === 1; this.response.template = 'problem_detail.html'; - if (contest.is_done(this.tdoc)) { + if (contest.isDone(this.tdoc)) { if (!this.attended) throw new ContestNotAttendedError(this.tdoc._id); - if (!contest.is_ongoing(this.tdoc)) throw new ContestNotLiveError(this.tdoc._id); + if (!contest.isOngoing(this.tdoc)) throw new ContestNotLiveError(this.tdoc._id); } console.log(this.tdoc.pids, this.pdoc._id); if (!this.tdoc.pids.map((s) => s.toString()).includes(this.pdoc._id.toString())) { @@ -260,6 +271,7 @@ Route('/c', ContestListHandler); Route('/c/:tid', ContestDetailHandler); Route('/c/:tid/edit', ContestEditHandler); Route('/c/:tid/scoreboard', ContestScoreboardHandler); +Route('/c/:tid/export/:ext', ContestScoreboardDownloadHandler); Route('/c/:tid/p/:pid', ContestProblemHandler); Route('/c/:tid/p/:pid/submit', ContestProblemSubmitHandler); Route('/contest/create', ContestCreateHandler); diff --git a/hydro/model/contest.js b/hydro/model/contest.js index 8f93e64b5..565ded9ab 100644 --- a/hydro/model/contest.js +++ b/hydro/model/contest.js @@ -83,9 +83,12 @@ async function updateStatus(tid, uid, rid, pid, accept, score) { }, { upsert: true }); if (!tsdoc.value.attend) throw new ContestNotAttendedError(tid, uid); } +function getStatus(tid, uid) { + return collStatus.findOne({ tid, uid }); +} async function getListStatus(uid, tids) { const r = {}; - for (const tid of tids) r[tid] = await this.getStatus(tid, uid); // eslint-disable-line no-await-in-loop + for (const tid of tids) r[tid] = await getStatus(tid, uid); // eslint-disable-line no-await-in-loop return r; } async function attend(tid, uid) { @@ -99,24 +102,24 @@ async function attend(tid, uid) { function getMultiStatus(query) { return collStatus.find(query); } -function is_new(tdoc, days = 1) { +function isNew(tdoc, days = 1) { const now = new Date().getTime(); const readyAt = tdoc.beginAt.getTime(); return (now < readyAt - days * 24 * 3600 * 1000); } -function is_upcoming(tdoc, days = 1) { +function isUpcoming(tdoc, days = 1) { const now = new Date().getTime(); const readyAt = tdoc.beginAt.getTime(); return (now > readyAt - days * 24 * 3600 * 1000 && now < tdoc.beginAt); } -function is_not_started(tdoc) { +function isNotStarted(tdoc) { return (new Date()) < tdoc.beginAt; } -function is_ongoing(tdoc) { +function isOngoing(tdoc) { const now = new Date(); return (tdoc.beginAt <= now && now < tdoc.endAt); } -function is_done(tdoc) { +function isDone(tdoc) { return tdoc.endAt <= new Date(); } @@ -169,168 +172,38 @@ module.exports = { edit, get, updateStatus, + getStatus, count: (query) => coll.find(query).count(), getMulti: (query) => coll.find(query), - getStatus: (tid, uid) => coll_status.findOne({ tid, uid }), - setStatus: (tid, uid, $set) => coll_status.findOneAndUpdate({ tid, uid }, { $set }), - is_new, - is_upcoming, - is_not_started, - is_ongoing, - is_done, - status_text: (tdoc) => ( - is_new(tdoc) + setStatus: (tid, uid, $set) => collStatus.findOneAndUpdate({ tid, uid }, { $set }), + isNew, + isUpcoming, + isNotStarted, + isOngoing, + isDone, + statusText: (tdoc) => ( + isNew(tdoc) ? 'New' - : is_upcoming(tdoc) + : isUpcoming(tdoc) ? 'Ready (☆▽☆)' - : is_ongoing(tdoc) + : isOngoing(tdoc) ? 'Live...' : 'Done'), - get_status: (tdoc) => ( - is_not_started(tdoc) + getStatusText: (tdoc) => ( + isNotStarted(tdoc) ? 'not_started' - : is_ongoing(tdoc) + : isOngoing(tdoc) ? 'ongoing' : 'finished'), }; /* -journal_key_func = lambda j: j['rid'] - -Rule = collections.namedtuple('Rule', ['show_record_func', - 'show_scoreboard_func', - 'stat_func', - 'status_sort', - 'rank_func', - 'scoreboard_func']) - -def _oi_equ_func(a, b): - return a.get('score', 0) == b.get('score', 0) - -RULES = { - constant.contest.RULE_OI: Rule(lambda tdoc, now: now > tdoc['end_at'], - lambda tdoc, now: now > tdoc['end_at'], - _oi_stat, - [('score', -1)], - functools.partial(rank.ranked, equ_func=_oi_equ_func), - _oi_scoreboard), - constant.contest.RULE_ACM: Rule(lambda tdoc, now: now >= tdoc['begin_at'], - lambda tdoc, now: now >= tdoc['begin_at'], - _acm_stat, - [('accept', -1), ('time', 1)], - functools.partial(enumerate, start=1), - _acm_scoreboard), - constant.contest.RULE_ASSIGNMENT: Rule(lambda tdoc, now: now >= tdoc['begin_at'], - lambda tdoc, now: False, # TODO: show scoreboard according to assignment preference - _assignment_stat, - [('penalty_score', -1), ('time', 1)], - functools.partial(enumerate, start=1), - _assignment_scoreboard), -} - -def get_multi(domainId: str, doc_type: int, fields=None, **kwargs): - # TODO(twd2): projection. - if doc_type not in [document.TYPE_CONTEST, document.TYPE_HOMEWORK]: - raise error.InvalidArgumentError('doc_type') - return document.get_multi(domainId=domainId, - doc_type=doc_type, - fields=fields, - **kwargs) \ - .sort([('doc_id', -1)]) - - -@argmethod.wrap -async def attend(domainId: str, doc_type: int, cid: objeccid.Objeccid, uid: int): - # TODO(iceboy): check time. - if doc_type not in [document.TYPE_CONTEST, document.TYPE_HOMEWORK]: - raise error.InvalidArgumentError('doc_type') - try: - await document.capped_inc_status(domainId, doc_type, cid, - uid, 'attend', 1, 0, 1) - except errors.DuplicateKeyError: - if doc_type == document.TYPE_CONTEST: - raise error.ContestAlreadyAttendedError(domainId, cid, uid) from None - elif doc_type == document.TYPE_HOMEWORK: - raise error.HomeworkAlreadyAttendedError(domainId, cid, uid) from None - return await document.inc(domainId, doc_type, cid, 'attend', 1) - - -@argmethod.wrap -async def get_status(domainId: str, doc_type: int, cid: objeccid.Objeccid, uid: int, fields=None): - if doc_type not in [document.TYPE_CONTEST, document.TYPE_HOMEWORK]: - raise error.InvalidArgumentError('doc_type') - return await document.get_status(domainId, doc_type, doc_id=cid, - uid=uid, fields=fields) - - -def get_multi_status(doc_type: int, *, fields=None, **kwargs): - if doc_type not in [document.TYPE_CONTEST, document.TYPE_HOMEWORK]: - raise error.InvalidArgumentError('doc_type') - return document.get_multi_status(doc_type=doc_type, - fields=fields, **kwargs) - - -async def get_dict_status(domainId, uid, doc_type, cids, *, fields=None): - if doc_type not in [document.TYPE_CONTEST, document.TYPE_HOMEWORK]: - raise error.InvalidArgumentError('doc_type') - result = dict() - async for tsdoc in get_multi_status(domainId=domainId, - uid=uid, - doc_type=doc_type, - doc_id={'$in': list(set(cids))}, - fields=fields): - result[tsdoc['doc_id']] = tsdoc - return result - - -@argmethod.wrap -async def get_and_list_status(domainId: str, doc_type: int, cid: objeccid.Objeccid, fields=None): - # TODO(iceboy): projection, pagination. - if doc_type not in [document.TYPE_CONTEST, document.TYPE_HOMEWORK]: - raise error.InvalidArgumentError('doc_type') - tdoc = await get(domainId, doc_type, cid) - tsdocs = await document.get_multi_status(domainId=domainId, - doc_type=doc_type, - doc_id=tdoc['doc_id'], - fields=fields) \ - .sort(RULES[tdoc['rule']].status_sort) \ - .to_list() - return tdoc, tsdocs - - def _get_status_journal(tsdoc): # Sort and uniquify journal of the contest status document, by rid. return [list(g)[-1] for _, g in itertools.groupby(sorted(tsdoc['journal'], key=journal_key_func), key=journal_key_func)] - -@argmethod.wrap -async def update_status(domainId: str, doc_type: int, cid: objeccid.Objeccid, uid: int, - rid: objeccid.Objeccid, pid: document.convert_doc_id, - accept: bool, score: int): - """This method returns None when the modification has been superseded by a parallel operation.""" - if doc_type not in [document.TYPE_CONTEST, document.TYPE_HOMEWORK]: - raise error.InvalidArgumentError('doc_type') - tdoc = await document.get(domainId, doc_type, cid) - tsdoc = await document.rev_push_status( - domainId, tdoc['doc_type'], tdoc['doc_id'], uid, - 'journal', {'rid': rid, 'pid': pid, 'accept': accept, 'score': score}) - if 'attend' not in tsdoc or not tsdoc['attend']: - if tdoc['doc_type'] == document.TYPE_CONTEST: - raise error.ContestNotAttendedError(domainId, cid, uid) - elif tdoc['doc_type'] == document.TYPE_HOMEWORK: - raise error.HomeworkNotAttendedError(domainId, cid, uid) - else: - raise error.InvalidArgumentError('doc_type') - - journal = _get_status_journal(tsdoc) - stats = RULES[tdoc['rule']].stat_func(tdoc, journal) - tsdoc = await document.rev_set_status(domainId, tdoc['doc_type'], cid, uid, tsdoc['rev'], - journal=journal, **stats) - return tsdoc - - @argmethod.wrap async def recalc_status(domainId: str, doc_type: int, cid: objeccid.Objeccid): if doc_type not in [document.TYPE_CONTEST, document.TYPE_HOMEWORK]: @@ -346,125 +219,4 @@ async def recalc_status(domainId: str, doc_type: int, cid: objeccid.Objeccid): stats = RULES[tdoc['rule']].stat_func(tdoc, journal) await document.rev_set_status(domainId, doc_type, cid, tsdoc['uid'], tsdoc['rev'], return_doc=False, journal=journal, **stats) - - -def _parse_pids(pids_str): - pids = misc.dedupe(map(document.convert_doc_id, pids_str.split(','))) - return pids - - -def _format_pids(pids_list): - return ','.join([str(pid) for pid in pids_list]) - - -class ContestStatusMixin(object): - @property - @functools.lru_cache() - def now(self): - # TODO(iceboy): This does not work on multi-machine environment. - return datetime.datetime.utcnow() - - def is_new(self, tdoc): - ready_at = tdoc['begin_at'] - datetime.timedelta(days=1) - return self.now < ready_at - - def is_upcoming(self, tdoc): - ready_at = tdoc['begin_at'] - datetime.timedelta(days=1) - return ready_at <= self.now < tdoc['begin_at'] - - def is_not_started(self, tdoc): - return self.now < tdoc['begin_at'] - - def is_ongoing(self, tdoc): - return tdoc['begin_at'] <= self.now < tdoc['end_at'] - - def is_done(self, tdoc): - return tdoc['end_at'] <= self.now - - def is_homework_extended(self, tdoc): - return tdoc['penalty_since'] <= self.now < tdoc['end_at'] - - def status_text(self, tdoc): - if self.is_new(tdoc): - return 'New' - elif self.is_upcoming(tdoc): - return 'Ready (☆▽☆)' - elif self.is_ongoing(tdoc): - return 'Live...' - else: - return 'Done' - - def get_status(self, tdoc): - if self.is_not_started(tdoc): - return 'not_started' - elif self.is_ongoing(tdoc): - return 'ongoing' - else: - return 'finished' - - -class ContestVisibilityMixin(object): - def can_view_hidden_scoreboard(self, tdoc): - if self.domainId != tdoc['domainId']: - return False - if tdoc['doc_type'] == document.TYPE_CONTEST: - return self.hasPerm(builtin.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD) - elif tdoc['doc_type'] == document.TYPE_HOMEWORK: - return self.hasPerm(builtin.PERM_VIEW_HOMEWORK_HIDDEN_SCOREBOARD) - else: - return False - - def can_show_record(self, tdoc, allow_perm_override=True): - if RULES[tdoc['rule']].show_record_func(tdoc, datetime.datetime.utcnow()): - return True - if allow_perm_override and self.can_view_hidden_scoreboard(tdoc): - return True - return False - - def can_show_scoreboard(self, tdoc, allow_perm_override=True): - if RULES[tdoc['rule']].show_scoreboard_func(tdoc, datetime.datetime.utcnow()): - return True - if allow_perm_override and self.can_view_hidden_scoreboard(tdoc): - return True - return False - - -class ContestCommonOperationMixin(object): - async def get_scoreboard(self, doc_type: int, cid: objeccid.Objeccid, is_export: bool=False): - if doc_type not in [document.TYPE_CONTEST, document.TYPE_HOMEWORK]: - raise error.InvalidArgumentError('doc_type') - tdoc, tsdocs = await get_and_list_status(self.domainId, doc_type, cid) - if not self.can_show_scoreboard(tdoc): - if doc_type == document.TYPE_CONTEST: - raise error.ContestScoreboardHiddenError(self.domainId, cid) - elif doc_type == document.TYPE_HOMEWORK: - raise error.HomeworkScoreboardHiddenError(self.domainId, cid) - udict, dudict, pdict = await asyncio.gather( - user.get_dict([tsdoc['uid'] for tsdoc in tsdocs]), - domain.get_dict_user_by_uid(self.domainId, [tsdoc['uid'] for tsdoc in tsdocs]), - problem.get_dict(self.domainId, tdoc['pids'])) - ranked_tsdocs = RULES[tdoc['rule']].rank_func(tsdocs) - rows = RULES[tdoc['rule']].scoreboard_func(is_export, self.translate, tdoc, - ranked_tsdocs, udict, dudict, pdict) - return tdoc, rows, udict - - async def verify_problems(self, pids): - pdocs = await problem.get_multi(domainId=self.domainId, doc_id={'$in': pids}, - fields={'doc_id': 1}) \ - .sort('doc_id', 1) \ - .to_list() - exist_pids = [pdoc['_id'] for pdoc in pdocs] - if len(pids) != len(exist_pids): - for pid in pids: - if pid not in exist_pids: - raise error.ProblemNotFoundError(self.domainId, pid) - return pids - - async def hide_problems(self, pids): - await asyncio.gather(*[problem.set_hidden(self.domainId, pid, True) for pid in pids]) - - -class ContestMixin(ContestStatusMixin, ContestVisibilityMixin, ContestCommonOperationMixin): - pass - */ diff --git a/hydro/model/system.js b/hydro/model/system.js index fe6c48459..015851615 100644 --- a/hydro/model/system.js +++ b/hydro/model/system.js @@ -7,8 +7,8 @@ async function get(_id) { if (doc) return doc.value; return null; } -async function update(_id, update, config) { - await coll.findOneAndUpdate({ _id }, update, config); +async function update(_id, operation, config) { + await coll.findOneAndUpdate({ _id }, operation, config); return get(_id); } /** diff --git a/hydro/module/contest/acm.js b/hydro/module/contest/acm.js index bb2aa3065..b6c8d55f8 100644 --- a/hydro/module/contest/acm.js +++ b/hydro/module/contest/acm.js @@ -4,36 +4,42 @@ module.exports = { showScoreboard: () => true, showRecord: (tdoc, now) => now > tdoc.endAt, stat: (tdoc, journal) => { - const naccept = {}; const effective = {}; const detail = []; let accept = 0; let time = 0; + const naccept = {}; + const effective = {}; + const detail = []; + let accept = 0; + let time = 0; for (const j in journal) { - if (tdoc.pids.includes(j.pid) && !(effective.includes(j.pid) && effective[j.pid].accept)) { effective[j.pid] = j; } - if (!j.accept) { naccept[j.pid]++; } + if (tdoc.pids.includes(j.pid) + && !(effective.includes(j.pid) && effective[j.pid].accept)) { + effective[j.pid] = j; + } + if (!j.accept) naccept[j.pid]++; } function _time(jdoc) { const real = jdoc.rid.generationTime - Math.floor(tdoc.begin_at / 1000); const penalty = 20 * 60 * naccept[jdoc.pid]; return real + penalty; } - for (const j of effective) { detail.push({ ...j, naccept: naccept[j.pid], time: _time(j) }); } + for (const j of effective) detail.push({ ...j, naccept: naccept[j.pid], time: _time(j) }); for (const d of detail) { accept += d.accept; if (d.accept) time += d.time; } return { accept, time, detail }; }, - scoreboard(is_export, _, tdoc, ranked_tsdocs, udict, dudict, pdict) { + scoreboard(isExport, _, tdoc, rankedTsdocs, udict, pdict) { const columns = [ { type: 'rank', value: _('Rank') }, { type: 'user', value: _('User') }, - { type: 'display_name', value: _('Display Name') }, { type: 'solved_problems', value: _('Solved Problems') }, ]; - if (is_export) { + if (isExport) { columns.push({ type: 'total_time', value: _('Total Time (Seconds)') }); columns.push({ type: 'total_time_str', value: _('Total Time') }); } for (const i in tdoc.pids) { - if (is_export) { + if (isExport) { columns.push({ type: 'problem_flag', value: '#{0} {1}'.format(i + 1, pdict[tdoc.pids[i]].title), @@ -55,44 +61,45 @@ module.exports = { } } const rows = [columns]; - for (const [rank, tsdoc] of ranked_tsdocs) { + for (const [rank, tsdoc] of rankedTsdocs) { const tsddict = {}; if (tdoc.detail) { for (const item of tsdoc.detail) tsddict[item.pid] = item; } const row = []; row.push( { type: 'string', value: rank }, { type: 'user', value: udict[tsdoc.uid].uname, raw: udict[tsdoc.uid] }, - { type: 'display_name', value: (dudict[tsdoc.uid] || {}).display_name || '' }, { type: 'string', value: tsdoc.accept || 0 }, ); - if (is_export) { + if (isExport) { row.push( { type: 'string', value: tsdoc.time || 0.0 }, { type: 'string', value: tsdoc.time || 0.0 }, ); } for (const pid of tdoc.pids) { - let rdoc; let col_accepted; let col_time; let - col_time_str; + let rdoc; + let colAccepted; + let colTime; + let colTimeStr; if ((tsddict[pid] || {}).accept) { rdoc = tsddict[pid].rid; - col_accepted = _('Accepted'); - col_time = tsddict[pid].time; - col_time_str = col_time; + colAccepted = _('Accepted'); + colTime = tsddict[pid].time; + colTimeStr = colTime; } else { rdoc = null; - col_accepted = '-'; - col_time = '-'; - col_time_str = '-'; + colAccepted = '-'; + colTime = '-'; + colTimeStr = '-'; } - if (is_export) { - row.push({ type: 'string', value: col_accepted }); - row.push({ type: 'string', value: col_time }); - row.push({ type: 'string', value: col_time_str }); + if (isExport) { + row.push({ type: 'string', value: colAccepted }); + row.push({ type: 'string', value: colTime }); + row.push({ type: 'string', value: colTimeStr }); } else { row.push({ type: 'record', - value: '{0}\n{1}'.format(col_accepted, col_time_str), + value: '{0}\n{1}'.format(colAccepted, colTimeStr), raw: rdoc, }); } diff --git a/hydro/module/contest/homework.js b/hydro/module/contest/homework.js index efba001ba..7e24c5a26 100644 --- a/hydro/module/contest/homework.js +++ b/hydro/module/contest/homework.js @@ -1,45 +1,40 @@ -const { ValidationError } = require('../../error'); - module.exports = { TEXT: 'Assignment', - check(data) { - if (!data.penalty_since) throw new ValidationError('penalty_since'); - if (data.penalty_since < data.begin_at) throw new ValidationError('penalty_since', 'begin_at'); - if (data.penalty_since > data.end_at) throw new ValidationError('penalty_since', 'end_at'); - }, + check() {}, stat(tdoc, journal) { const effective = {}; const detail = []; let score = 0; - let penalty_score = 0; let time = 0; - for (const j in journal) { if (tdoc.pids.includes(j.pid) && !(effective.includes(j.pid) && effective[j.pid].accept)) effective[j.pid] = j; } - const _time = (jdoc) => jdoc.rid.generationTime - Math.floor(tdoc.begin_at / 1000); - function _penalty_score(jdoc) { - const exceed_seconds = jdoc.rid.generationTime - Math.floor(tdoc.penalty_since / 1000); - if (exceed_seconds < 0) return jdoc.score; - return 0.5 * jdoc.score; + for (const j in journal) { + if (tdoc.pids.includes(j.pid) + && !(effective.includes(j.pid) && effective[j.pid].accept)) { + effective[j.pid] = j; + } } + const _time = (jdoc) => jdoc.rid.generationTime - Math.floor(tdoc.beginAt / 1000); for (const j in effective) { - detail.push({ ...effective[j], penalty_score: _penalty_score(effective[j]), time: _time(effective[j]) }); + detail.push({ + ...effective[j], + time: _time(effective[j]), + }); } for (const d of detail) { score += d.score; - penalty_score += d.penalty_score; time += d.time; } return { - score, penalty_score, time, detail, + score, time, detail, }; }, - scoreboard(is_export, _, tdoc, ranked_tsdocs, udict, dudict, pdict) { + scoreboard(isExport, _, tdoc, rankedTsdocs, udict, pdict) { const columns = [ { type: 'rank', value: _('Rank') }, { type: 'user', value: _('User') }, { type: 'display_name', value: _('Display Name') }, { type: 'total_score', value: _('Score') }, ]; - if (is_export) { + if (isExport) { columns.push( { type: 'total_original_score', value: _('Original Score') }, { type: 'total_time', value: _('Total Time (Seconds)') }, @@ -47,7 +42,7 @@ module.exports = { } columns.push({ type: 'total_time_str', value: _('Total Time') }); for (const i in tdoc.pids) { - if (is_export) { + if (isExport) { columns.push( { type: 'problem_score', value: '#{0} {1}'.format(i + 1, pdict[tdoc.pids[i]].title) }, { type: 'problem_original_score', value: '#{0} {1}'.format(i + 1, _('Original Score')) }, @@ -57,16 +52,15 @@ module.exports = { } else columns.push({ type: 'problem_detail', value: '#{0}'.format(i + 1), raw: pdict[tdoc.pids[i]] }); } const rows = [columns]; - for (const [rank, tsdoc] in ranked_tsdocs) { + for (const [rank, tsdoc] in rankedTsdocs) { const tsddict = {}; if (tsdoc.detail) { for (const item of tsdoc.detail) tsddict[item.pid] = item; } const row = [ { type: 'string', value: rank }, { type: 'user', value: udict[tsdoc.uid].uname, raw: udict[tsdoc.uid] }, - { type: 'display_name', value: (dudict[tsdoc.uid] || {}).display_name || '' }, { type: 'string', value: tsdoc.penalty_score || 0 }, ]; - if (is_export) { + if (isExport) { row.push( { type: 'string', value: tsdoc.score || 0 }, { type: 'string', value: tsdoc.time || 0.0 }, @@ -75,21 +69,21 @@ module.exports = { row.push({ type: 'string', value: tsdoc.time || 0 }); for (const pid of tdoc.pids) { const rdoc = (tsddict[pid] || {}).rid || null; - const col_score = (tsddict[pid] || {}).penalty_score || '-'; - const col_original_score = (tsddict[pid] || {}).score || '-'; - const col_time = (tsddict[pid] || {}).time || '-'; - const col_time_str = col_time != '-' ? col_time : '-'; - if (is_export) { + const colScore = (tsddict[pid] || {}).penalty_score || '-'; + const colOriginalScore = (tsddict[pid] || {}).score || '-'; + const colTime = (tsddict[pid] || {}).time || '-'; + const colTimeStr = colTime !== '-' ? colTime : '-'; + if (isExport) { row.push( - { type: 'string', value: col_score }, - { type: 'string', value: col_original_score }, - { type: 'string', value: col_time }, - { type: 'string', value: col_time_str }, + { type: 'string', value: colScore }, + { type: 'string', value: colOriginalScore }, + { type: 'string', value: colTime }, + { type: 'string', value: colTimeStr }, ); } else { row.push({ type: 'record', - value: '{0} / {1}\n{2}'.format(col_score, col_original_score, col_time_str), + value: '{0} / {1}\n{2}'.format(colScore, colOriginalScore, colTimeStr), raw: rdoc, }); } diff --git a/hydro/service/server.js b/hydro/service/server.js index 7a5ad02ce..ea6a97bad 100644 --- a/hydro/service/server.js +++ b/hydro/service/server.js @@ -115,6 +115,13 @@ class Handler { this.response.redirect = this.request.headers.referer || '/'; } + binary(data, name) { + this.response.body = data; + this.response.template = null; + this.response.type = 'application/octet-stream'; + this.response.disposition = `attachment; filename="${name}"`; + } + async ___prepare() { this.now = new Date(); this._handler.sid = this.request.cookies.get('sid'); @@ -170,6 +177,7 @@ class Handler { } async putResponse() { + if (this.response.disposition) this.ctx.set('Content-Disposition', this.response.disposition); if (this.response.redirect && !this.preferJson) { this.ctx.response.type = 'application/octet-stream'; this.ctx.response.status = 302; diff --git a/interfaces.d.ts b/interfaces.d.ts deleted file mode 100644 index 31e58351d..000000000 --- a/interfaces.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface { - -} \ No newline at end of file diff --git a/templates/components/homework.html b/templates/components/homework.html deleted file mode 100644 index 96972a3da..000000000 --- a/templates/components/homework.html +++ /dev/null @@ -1,8 +0,0 @@ -{% import "components/user.html" as user with context %} -{% macro render_time(tdoc_time) %} -{{ datetime_span(tdoc_time, false, '%Y-%m-%d %H:%M') }} -{% endmacro %} - -{% macro render_extension(tdoc) %} -{{ ((tdoc['end_at'] - tdoc['penalty_since']).total_seconds() / 3600)|round(1) }} -{% endmacro %} diff --git a/templates/contest_detail.html b/templates/contest_detail.html index 851e59747..9e0d6c483 100644 --- a/templates/contest_detail.html +++ b/templates/contest_detail.html @@ -9,19 +9,19 @@
{{ tdoc['content']|markdown|safe }} - {% if model.contest.is_ongoing(tdoc) %} + {% if model.contest.isOngoing(tdoc) %}

{{ _('We will check code similarity after this contest.') }}

{% endif %}
- {% if model.contest.is_ongoing(tdoc) or model.contest.is_done(tdoc) %} + {% if model.contest.isOngoing(tdoc) or model.contest.isDone(tdoc) %}

{{ _('Problem') }}

- {% if attended or model.contest.is_done(tdoc) %} + {% if attended or model.contest.isDone(tdoc) %}
diff --git a/templates/contest_main.html b/templates/contest_main.html index 0a20e824e..863f97c36 100644 --- a/templates/contest_main.html +++ b/templates/contest_main.html @@ -49,9 +49,9 @@
{% for tdoc in tdocs %} - {% if model.contest.is_ongoing(tdoc) %} + {% if model.contest.isOngoing(tdoc) %} {{ render_important_contest('live', 'Live...', tdoc) }} - {% elif model.contest.is_upcoming(tdoc) %} + {% elif model.contest.isUpcoming(tdoc) %} {{ render_important_contest('ready', 'Ready (☆▽☆)', tdoc) }} {% endif %} {% endfor %} diff --git a/templates/contest_scoreboard.html b/templates/contest_scoreboard.html index 6e7a8f96c..bfd022ebd 100644 --- a/templates/contest_scoreboard.html +++ b/templates/contest_scoreboard.html @@ -4,10 +4,10 @@
diff --git a/templates/contest_scoreboard_download_html.html b/templates/contest_scoreboard_download_html.html index f925afee1..b6a61565e 100644 --- a/templates/contest_scoreboard_download_html.html +++ b/templates/contest_scoreboard_download_html.html @@ -35,7 +35,7 @@
- {%- for row in rows[1:] -%} + {%- for row in rows -%}{% if loop.index0 %} {%- for column in row -%} {%- endfor -%} - {%- endfor -%} + {% endif %}{%- endfor -%}
@@ -43,6 +43,6 @@
diff --git a/templates/discussion_detail.html b/templates/discussion_detail.html index a38a72513..e40d5a209 100644 --- a/templates/discussion_detail.html +++ b/templates/discussion_detail.html @@ -19,7 +19,7 @@
  • - {{ user.render_inline(udict[ddoc['owner_uid']], dudict[ddoc['owner_uid']]) }} + {{ user.render_inline(udict[ddoc['owner']]) }} @ {{ datetime_span(ddoc['_id'].generationTime) }}
  • @@ -82,9 +82,9 @@
    - {% with udoc = udict[ddoc['owner_uid']], dudoc = dudict[ddoc['owner_uid']] %} + {% with udoc = udict[ddoc['owner']] %}
    - +
    @@ -101,19 +101,19 @@ {% if vnode['doc_type'] == vj4.model.document.TYPE_PROBLEM %} - {% with pdoc=vnode, owner_udoc=udict[vnode['owner_uid']], owner_dudoc=dudict[vnode['owner_uid']] %} + {% with pdoc=vnode, owner_udoc=udict[vnode['owner']] %} {% include "partials/problem_sidebar.html" %} {% endwith %} {% endif %} diff --git a/templates/discussion_main_or_node.html b/templates/discussion_main_or_node.html index f19ae09ae..db359b8ee 100644 --- a/templates/discussion_main_or_node.html +++ b/templates/discussion_main_or_node.html @@ -8,11 +8,11 @@
    {% if vnode['doc_type'] == vj4.model.document.TYPE_PROBLEM %} - {% with pdoc=vnode, owner_udoc=udict[vnode['owner_uid']], owner_dudoc=dudict[vnode['owner_uid']] if dudict %} + {% with pdoc=vnode, owner_udoc=udict[vnode['owner']] %} {% include "partials/problem_sidebar.html" %} {% endwith %} {% elif vnode['doc_type'] == vj4.model.document.TYPE_CONTEST %} - {% with tdoc=vnode, owner_udoc=udict[vnode['owner_uid']], owner_dudoc=dudict[vnode['owner_uid']] if dudict %} + {% with tdoc=vnode, owner_udoc=udict[vnode['owner']] %} {% include "partials/contest_sidebar.html" %} {% endwith %} {% elif vnode['doc_type'] == vj4.model.document.TYPE_DISCUSSION_NODE %} diff --git a/templates/domain_main.html b/templates/domain_main.html index 404631c87..861197424 100644 --- a/templates/domain_main.html +++ b/templates/domain_main.html @@ -57,52 +57,6 @@
    {% endif %} - {% if not htdocs %} - - {% else %} -
    -
    -

    {{ _('Homework') }}

    -
    -
      - {% for tdoc in htdocs %} -
    1. -
      -
      -
      -
      {{ datetime_span(tdoc['penalty_since'], false, '%d') }}
      -
      {{ datetime_span(tdoc['penalty_since'], false, '%Y-%m') }}
      -
      -
      -
      -

      {{ tdoc['title'] }}

      -
        -
      • - {{ _('Status') }}: {{ _(handler.status_text(tdoc)) }} -
      • -
      • - {{ _('Open Since') }}: {{ datetime_span(tdoc['begin_at']) }} -
      • -
      • - {% if handler.is_homework_extended(tdoc) or handler.is_done(tdoc) %} - {{ _('Hard Deadline') }}: {{ datetime_span(tdoc['end_at']) }} - {% else %} - {{ _('Deadline') }}: {{ datetime_span(tdoc['penalty_since']) }} - {% endif %} -
      • - {% if htsdict[tdoc['doc_id']]['attend'] == 1 %} -
      • - {{ _('Claimed') }} -
      • - {% endif %} -
      -
      -
      -
    2. - {% endfor %} -
    -
    - {% endif %} {% if not trdocs %} {% else %} diff --git a/templates/domain_manage_user.html b/templates/domain_manage_user.html index 0e26919a1..19e685d7c 100644 --- a/templates/domain_manage_user.html +++ b/templates/domain_manage_user.html @@ -68,7 +68,7 @@ {% for role in roles %} {% for rudoc in rudocs[role] %} - {% set is_disabled=(rudoc['uid'] == handler.domain['owner_uid']) %} + {% set is_disabled=(rudoc['uid'] == handler.domain['owner']) %}
    -
    {{ _('Status') }}
    {{ _(model.contest.status_text(tdoc)) }}
    +
    {{ _('Status') }}
    {{ _(model.contest.statusText(tdoc)) }}
    {{ _('Rule') }}
    {{ model.contest.RULES[tdoc.rule].TEXT }}
    {{ _('Problem') }}
    {{ tdoc.pids.length }}
    {{ _('Start at') }}
    {{ contest.render_time(tdoc.beginAt) }}
    diff --git a/templates/partials/discussion_list.html b/templates/partials/discussion_list.html index ed5a460ef..ec9d5e0a6 100644 --- a/templates/partials/discussion_list.html +++ b/templates/partials/discussion_list.html @@ -30,7 +30,7 @@
  • - {{ user.render_inline(udict[ddoc['owner_uid']], dudoc=dudoc if dudoc else dudict[ddoc['owner_uid']], badge=false) }} + {{ user.render_inline(udict[ddoc['owner']], badge=false) }} @ {{ datetime_span(ddoc['update_at'] or ddoc['_id'].generationTime) }}
  • diff --git a/templates/partials/homework_default_penalty_rules.yaml b/templates/partials/homework_default_penalty_rules.yaml deleted file mode 100644 index 689730941..000000000 --- a/templates/partials/homework_default_penalty_rules.yaml +++ /dev/null @@ -1,6 +0,0 @@ -# Format: -# hours: coefficient -1: 0.9 -3: 0.8 -12: 0.75 -9999: 0.5 diff --git a/templates/partials/homework_sidebar.html b/templates/partials/homework_sidebar.html deleted file mode 100644 index fb49819ed..000000000 --- a/templates/partials/homework_sidebar.html +++ /dev/null @@ -1,85 +0,0 @@ -{% import "components/user.html" as user with context %} -{% import "components/homework.html" as homework with context %} -
    -{% if page_name != 'homework_detail' %} - -
    -

    {{ tdoc['title'] }}

    -
    - {% if attended %} - {{ _('Claimed') }} - {% else %} - {{ _('Not Claimed') }} - {% endif %} -
    -
    -
    -{% endif %} -
    - -
    -
    -
    -
    {{ _('Status') }}
    {{ _(handler.status_text(tdoc)) }}
    -
    {{ _('Problem') }}
    {{ tdoc['pids']|length }}
    -
    {{ _('Open Since') }}
    {{ homework.render_time(tdoc['begin_at']) }}
    -
    {{ _('Deadline') }}
    {{ homework.render_time(tdoc['penalty_since']) }}
    -
    {{ _('Can be Extended For') }}
    {{ homework.render_extension(tdoc) }} {{ _('hour(s)') }}
    -
    -
    -
    diff --git a/templates/partials/nav.html b/templates/partials/nav.html index bb1ebaf4b..12f4f3688 100644 --- a/templates/partials/nav.html +++ b/templates/partials/nav.html @@ -23,9 +23,6 @@ {% if handler.hasPerm(perm.PERM_VIEW_CONTEST) %} {{ nav_item('/c', 'contest_main', 'contest') }} {% endif %} - {% if handler.hasPerm(perm.PERM_VIEW_HOMEWORK) %} - {{ nav_item('/h', 'homework_main', 'homework') }} - {% endif %} {% if handler.hasPerm(perm.PERM_EDIT_DESCRIPTION) or handler.hasPerm(perm.PERM_EDIT_PERM) %} {{ nav_item('/d', 'domain_manage', 'domain_manage') }} {% endif %} diff --git a/templates/partials/problem_sidebar.html b/templates/partials/problem_sidebar.html index 2fc489077..8cd0fec83 100644 --- a/templates/partials/problem_sidebar.html +++ b/templates/partials/problem_sidebar.html @@ -1,15 +1,7 @@ {% if tdoc %} - {% if tdoc['doc_type'] == vj4.model.document.TYPE_CONTEST %} - {% include "partials/problem_sidebar_contest.html" %} - {% set owner_udoc=udoc %} - {% set owner_dudoc=dudoc %} - {% include "partials/contest_sidebar.html" %} - {% else %} - {% include "partials/problem_sidebar_homework.html" %} - {% set owner_udoc=udoc %} - {% set owner_dudoc=dudoc %} - {% include "partials/homework_sidebar.html" %} - {% endif %} + {% include "partials/problem_sidebar_contest.html" %} + {% set owner_udoc = udoc %} + {% include "partials/contest_sidebar.html" %} {% else %} {% include "partials/problem_sidebar_normal.html" %} {% endif %} diff --git a/templates/partials/problem_sidebar_contest.html b/templates/partials/problem_sidebar_contest.html index ac365942a..df61a2704 100644 --- a/templates/partials/problem_sidebar_contest.html +++ b/templates/partials/problem_sidebar_contest.html @@ -7,7 +7,7 @@ {% endif %}
    @@ -177,12 +177,6 @@

    {{ tdoc['title'] }}

    {% endfor %} {% endif %} - {% if htdocs %} -

    {{ _('In following homework') }}:

    - {% for tdoc in htdocs %} -

    {{ tdoc['title'] }}

    - {% endfor %} - {% endif %}
    {% endif %} diff --git a/templates/problem_detail.html b/templates/problem_detail.html index 9e8b665e2..18eecfb5e 100644 --- a/templates/problem_detail.html +++ b/templates/problem_detail.html @@ -30,11 +30,11 @@ {% endif %} {% if tdoc %} {% if tdoc['doc_type'] == vj4.model.document.TYPE_CONTEST %} - {% if model.contest.is_done(tdoc) %} + {% if model.contest.isDone(tdoc) %}

    {{ _('You cannot submit for this problem because the contest is ended. You can click "Open in Problem Set" to view this problem in normal mode.') }}

    - {% endif %}{# handler.is_done(tdoc) #} + {% endif %}{# handler.isDone(tdoc) #} {% endif %}{# tdoc['doc_type'] == vj4.model.document.TYPE_CONTEST #} {% endif %}{# tdoc #} {{ pdoc['content']|markdown|safe }} @@ -44,7 +44,6 @@
    {% set owner_udoc = udoc %} - {% set owner_dudoc = dudoc %} {% include "partials/problem_sidebar.html" %}
    diff --git a/templates/problem_edit.html b/templates/problem_edit.html index 691004184..0bcbfad11 100644 --- a/templates/problem_edit.html +++ b/templates/problem_edit.html @@ -54,7 +54,7 @@
    {% if page_name == 'problem_edit' %} - {% set owner_udoc=udoc %}{% set owner_dudoc=dudoc %} + {% set owner_udoc = udoc %} {% include "partials/problem_sidebar.html" %} {% else %} {% include 'components/md_hint.html' %} diff --git a/templates/problem_settings.html b/templates/problem_settings.html index 1da721fe2..8353471a5 100644 --- a/templates/problem_settings.html +++ b/templates/problem_settings.html @@ -69,7 +69,7 @@
    - {% set owner_udoc=udoc %}{% set owner_dudoc=dudoc %} + {% set owner_udoc = udoc %} {% include "partials/problem_sidebar.html" %}
    diff --git a/templates/problem_statistics.html b/templates/problem_statistics.html index 566645801..ce67d0ddf 100644 --- a/templates/problem_statistics.html +++ b/templates/problem_statistics.html @@ -12,7 +12,7 @@
    - {% set owner_udoc=udoc %}{% set owner_dudoc=dudoc %} + {% set owner_udoc = udoc %} {% include "partials/problem_sidebar.html" %} {% endwith %}
    diff --git a/templates/problem_submit.html b/templates/problem_submit.html index 719bd268d..5a3ffa1ab 100644 --- a/templates/problem_submit.html +++ b/templates/problem_submit.html @@ -60,7 +60,7 @@
    - {% set owner_udoc=udoc %}{% set owner_dudoc=dudoc %} + {% set owner_udoc = udoc %} {% include "partials/problem_sidebar.html" %}
    diff --git a/templates/record_detail.html b/templates/record_detail.html index 9c254ade9..5be4f53d2 100644 --- a/templates/record_detail.html +++ b/templates/record_detail.html @@ -56,7 +56,7 @@
    {{ _('Submit By') }}
    -
    {{ user.render_inline(udoc, dudoc, modbadge=false) }}
    +
    {{ user.render_inline(udoc, modbadge=false) }}
    {% if pdoc %}
    {{ _('Problem') }}
    @@ -64,18 +64,11 @@
    {% endif %} {% if tdoc %} - {% if tdoc['doc_type'] == vj4.model.document.TYPE_CONTEST %}
    {{ _('Contest') }}
    {{ tdoc['title'] }}
    - {% else %}{# tdoc['doc_type'] == vj4.model.document.TYPE_CONTEST #} -
    {{ _('Homework') }}
    -
    - {{ tdoc['title'] }} -
    - {% endif %}{# tdoc['doc_type'] == vj4.model.document.TYPE_CONTEST #} - {% endif %}{# tdoc #} + {% endif %} {% if pdoc and (user._id == pdoc.owner or handler.hasPerm(perm.PERM_READ_PROBLEM_DATA)) %}
    {{ _('Problem Data') }}
    {{ _('Download') }}
    diff --git a/templates/training_detail.html b/templates/training_detail.html index 97b3012da..326b2df77 100644 --- a/templates/training_detail.html +++ b/templates/training_detail.html @@ -21,15 +21,15 @@
    {% endif %} {% for node in tdoc['dag'] %} -
    +

    {{ _('Section') }} {{ node['_id'] }}. {{ node['title'] }}

    -

    - - {% if nsdict[node['_id']]['is_done'] %} +

    + + {% if nsdict[node['_id']]['isDone'] %} {{ _('Completed') }} {% elif nsdict[node['_id']]['is_progress'] %} {{ _('In Progress') }} @@ -196,7 +196,7 @@ {% endif %}
    {{ _('Enrollees') }}
    {{ tdoc['enroll']|default(0) }}
    {{ _('Created By') }}
    -
    {{ user.render_inline(owner_udoc, owner_dudoc, badge=false) }}
    +
    {{ user.render_inline(owner_udoc, badge=false) }}

    diff --git a/ui/build/config/webpack.js b/ui/build/config/webpack.js index bba073266..10e72b3fd 100644 --- a/ui/build/config/webpack.js +++ b/ui/build/config/webpack.js @@ -37,7 +37,7 @@ export default function (env = {}) { return { loader: 'babel-loader', options: { - ...require(root('package.json')).babelForProject, // eslint-disable-line + ...require(root('package.json')).babelForProject, // eslint-disable-line cacheDirectory, }, }; @@ -165,11 +165,6 @@ export default function (env = {}) { // don't include momentjs locale files new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/), - new MonacoWebpackPlugin({ - // available options are documented at https://github.com/Microsoft/monaco-editor-webpack-plugin#options - languages: ['cpp', 'csharp', 'java', 'javascript', 'python', 'rust', 'ruby', 'php', 'pascal', 'go'], - }), - new ExtractCssPlugin({ filename: 'vj4.css?[contenthash:10]', allChunks: true, @@ -204,9 +199,6 @@ export default function (env = {}) { }, }), - env.production - ? new UglifyJsPlugin({ sourceMap: true }) - : function () { }, env.production ? new OptimizeCssAssetsPlugin() : function () { }, @@ -228,6 +220,11 @@ export default function (env = {}) { }, }), + new MonacoWebpackPlugin({ + // available options are documented at https://github.com/Microsoft/monaco-editor-webpack-plugin#options + languages: ['cpp', 'csharp', 'java', 'javascript', 'python', 'rust', 'ruby', 'php', 'pascal', 'go'], + }), + // Finally, output asset hashes new StaticManifestPlugin({ fileName: 'static-manifest.json', diff --git a/ui/common/variables.inc.styl b/ui/common/variables.inc.styl index d2623cdb1..a587ba51a 100644 --- a/ui/common/variables.inc.styl +++ b/ui/common/variables.inc.styl @@ -252,10 +252,6 @@ $training-section-status-icon = { $contest-color = { 'acm': #6BB67A, 'oi': #F5C735, + 'homework': #FFCDCD, } -// Homework Status -$homework-status-color = { - 'not_started': $text-2-color, - 'ongoing': $success-color, -} diff --git a/ui/components/homework/homework.page.styl b/ui/components/homework/homework.page.styl deleted file mode 100644 index 052463d17..000000000 --- a/ui/components/homework/homework.page.styl +++ /dev/null @@ -1,4 +0,0 @@ -.homework-status--text - for key, value in $homework-status-color - &.{key} - color: value diff --git a/ui/pages/homework_detail.page.styl b/ui/pages/homework_detail.page.styl deleted file mode 100644 index 5eea13abf..000000000 --- a/ui/pages/homework_detail.page.styl +++ /dev/null @@ -1,13 +0,0 @@ -.page--homework_detail - .col--status - width: rem(150px) - - .col--submit-at - width: rem(150px) - - .col--problem - border-left: 1px solid $table-border-color - - +mobile() - .col--submit-at - display: none diff --git a/ui/pages/homework_main.page.js b/ui/pages/homework_main.page.js deleted file mode 100644 index 09c5bd50a..000000000 --- a/ui/pages/homework_main.page.js +++ /dev/null @@ -1,37 +0,0 @@ -import { NamedPage } from 'vj/misc/PageLoader'; -import Calendar from 'vj/components/calendar'; -import i18n from 'vj/utils/i18n'; -import { parse as parseMongoId } from 'vj/utils/mongoId'; - -const page = new NamedPage('homework_main', () => { - // Homework Calendar - if (Context.docs) { - const events = Context.docs.map((doc) => ({ - beginAt: doc.begin_at * 1000, - endAt: doc.end_at * 1000, - title: doc.title, - maskFrom: doc.penalty_since ? doc.penalty_since * 1000 : null, - maskTitle: i18n('Time Extension'), - colorIndex: parseMongoId(doc.id).timestamp % 12, - link: doc.url, - })); - const calendar = new Calendar(events); - calendar.getDom().appendTo('[name="calendar_entry"]'); - $('[name="homework_display"]').change((ev) => { - switch (ev.currentTarget.value) { - case 'calendar': - $('.homework__list').hide(); - $('[name="calendar_entry"]').show(); - break; - case 'list': - $('.homework__list').show(); - $('[name="calendar_entry"]').hide(); - break; - default: - throw new Error('Unexpected display parameter'); - } - }); - } -}); - -export default page; diff --git a/ui/pages/homework_main.page.styl b/ui/pages/homework_main.page.styl deleted file mode 100644 index 43a6b6ff9..000000000 --- a/ui/pages/homework_main.page.styl +++ /dev/null @@ -1,14 +0,0 @@ -$highlight-button-color = #F6DF45 - -.page--homework_main - .homework__title - line-height: 1.2em - margin-bottom: rem(10px) - font-size: rem($font-size-title) - - .homework__info-attended - color: $success-color - - .homework__date - margin-right: rem(10px) - white-space: nowrap diff --git a/ui/pages/homework_scoreboard.page.styl b/ui/pages/homework_scoreboard.page.styl deleted file mode 100644 index dca0f2fda..000000000 --- a/ui/pages/homework_scoreboard.page.styl +++ /dev/null @@ -1,22 +0,0 @@ -.page--homework_scoreboard - .col--rank - width: rem(70px) - border-right: 1px solid $table-border-color - - .col--user - width: rem(140px) - - .col--total_score, .col--total_time_str, .col--solved_problems - width: rem(100px) - text-align: center - - .col--problem_detail - border-left: 1px solid $table-border-color - text-align: center - - +mobile() - .col--user - width: auto - - .col--problem_detail - display: none