├── vj4
├── __init__.py
├── handler
│ ├── __init__.py
│ ├── error.py
│ ├── i18n.py
│ └── misc.py
├── model
│ ├── __init__.py
│ ├── adaptor
│ │ ├── __init__.py
│ │ └── defaults.py
│ └── oplog.py
├── service
│ ├── __init__.py
│ ├── staticmanifest.py
│ ├── queue.py
│ └── mailer.py
├── test
│ ├── __init__.py
│ ├── test_misc.py
│ └── test_view.py
├── util
│ ├── __init__.py
│ ├── rank.py
│ ├── pagination.py
│ ├── version.py
│ ├── tools.py
│ ├── domainjob.py
│ ├── json.py
│ ├── useragent.py
│ ├── geoip.py
│ └── locale.py
├── pipeline
│ └── __init__.py
├── upgrader
│ └── __init__.py
├── ui
│ ├── components
│ │ ├── katex
│ │ │ ├── katex.styl
│ │ │ └── katex.page.js
│ │ ├── react-tabs
│ │ │ └── rc-tabs.page.js
│ │ ├── scratchpad
│ │ │ ├── var.inc.styl
│ │ │ ├── Panel.page.styl
│ │ │ ├── reducers
│ │ │ │ ├── index.js
│ │ │ │ ├── editor.js
│ │ │ │ └── records.js
│ │ │ ├── PanelButton.page.styl
│ │ │ ├── DataInput.page.styl
│ │ │ ├── Editor.page.styl
│ │ │ ├── PanelButtonComponent.js
│ │ │ ├── PanelComponent.js
│ │ │ ├── DataInputComponent.js
│ │ │ └── ScratchpadRecordsTableContainer.js
│ │ ├── nprogress
│ │ │ └── index.js
│ │ ├── problem
│ │ │ ├── rp.page.styl
│ │ │ └── tag.page.styl
│ │ ├── header
│ │ │ ├── header-logo.png
│ │ │ ├── header-logo-mano.png
│ │ │ ├── header-logo@2x.png
│ │ │ ├── header-background.png
│ │ │ ├── header-logo-summer.png
│ │ │ ├── header-background@2x.png
│ │ │ ├── header-logo-mano@2x.png
│ │ │ ├── header-logo-summer@2x.png
│ │ │ └── header-background.png.bak.png
│ │ ├── profile
│ │ │ ├── backgrounds
│ │ │ │ ├── 1.jpg
│ │ │ │ ├── 10.jpg
│ │ │ │ ├── 11.jpg
│ │ │ │ ├── 12.jpg
│ │ │ │ ├── 13.jpg
│ │ │ │ ├── 14.jpg
│ │ │ │ ├── 15.jpg
│ │ │ │ ├── 16.jpg
│ │ │ │ ├── 17.jpg
│ │ │ │ ├── 18.jpg
│ │ │ │ ├── 19.jpg
│ │ │ │ ├── 2.jpg
│ │ │ │ ├── 20.jpg
│ │ │ │ ├── 21.jpg
│ │ │ │ ├── 3.jpg
│ │ │ │ ├── 4.jpg
│ │ │ │ ├── 5.jpg
│ │ │ │ ├── 6.jpg
│ │ │ │ ├── 7.jpg
│ │ │ │ ├── 8.jpg
│ │ │ │ ├── 9.jpg
│ │ │ │ ├── thumbnail
│ │ │ │ │ ├── 1.jpg
│ │ │ │ │ ├── 10.jpg
│ │ │ │ │ ├── 11.jpg
│ │ │ │ │ ├── 12.jpg
│ │ │ │ │ ├── 13.jpg
│ │ │ │ │ ├── 14.jpg
│ │ │ │ │ ├── 15.jpg
│ │ │ │ │ ├── 16.jpg
│ │ │ │ │ ├── 17.jpg
│ │ │ │ │ ├── 18.jpg
│ │ │ │ │ ├── 19.jpg
│ │ │ │ │ ├── 2.jpg
│ │ │ │ │ ├── 20.jpg
│ │ │ │ │ ├── 21.jpg
│ │ │ │ │ ├── 3.jpg
│ │ │ │ │ ├── 4.jpg
│ │ │ │ │ ├── 5.jpg
│ │ │ │ │ ├── 6.jpg
│ │ │ │ │ ├── 7.jpg
│ │ │ │ │ ├── 8.jpg
│ │ │ │ │ └── 9.jpg
│ │ │ │ └── gen_thumbnails.sh
│ │ │ └── profile.page.styl
│ │ ├── contest
│ │ │ ├── problem-contest-bg.png
│ │ │ ├── problem-contest-bg@2x.png
│ │ │ ├── contest_sidebar.page.styl
│ │ │ └── contest.page.styl
│ │ ├── homework
│ │ │ └── homework.page.styl
│ │ ├── navigation
│ │ │ ├── nav-logo-small_dark.png
│ │ │ ├── nav-logo-small@2x_dark.png
│ │ │ ├── nav-logo-small_light.png
│ │ │ └── nav-logo-small@2x_light.png
│ │ ├── tab
│ │ │ ├── var.inc.styl
│ │ │ └── tab.page.js
│ │ ├── emoji
│ │ │ ├── emoji.page.styl
│ │ │ └── emojify.page.js
│ │ ├── autocomplete
│ │ │ ├── userselectautocomplete.page.styl
│ │ │ └── autocomplete.page.styl
│ │ ├── react-splitpane
│ │ │ ├── SplitPaneFillOverlay.page.styl
│ │ │ ├── SplitPaneFillOverlayComponent.js
│ │ │ └── splitpane.page.styl
│ │ ├── marker
│ │ │ └── marker.page.js
│ │ ├── dropdown
│ │ │ ├── dropdown.page.js
│ │ │ └── dropdown.page.styl
│ │ ├── table
│ │ │ └── styledTable.page.js
│ │ ├── form
│ │ │ ├── select.page.js
│ │ │ ├── var.inc.styl
│ │ │ ├── form.page.js
│ │ │ └── textbox.page.js
│ │ ├── messagepad
│ │ │ ├── reducers
│ │ │ │ ├── index.js
│ │ │ │ └── activeId.js
│ │ │ ├── DialogueListItem.page.styl
│ │ │ └── MessageComponent.js
│ │ ├── star
│ │ │ ├── star.page.styl
│ │ │ └── star.page.js
│ │ ├── rotator
│ │ │ └── rotator.page.styl
│ │ ├── discussion
│ │ │ └── discussion.page.styl
│ │ ├── react
│ │ │ ├── IconComponent.js
│ │ │ └── DomComponent.js
│ │ ├── highlighter
│ │ │ ├── highlighter.page.js
│ │ │ ├── codemirror.page.styl
│ │ │ └── meta.js
│ │ ├── menu
│ │ │ ├── menu.page.js
│ │ │ └── menu-heading.page.js
│ │ ├── loader
│ │ │ └── loader.page.styl
│ │ ├── record
│ │ │ └── record.page.styl
│ │ ├── training
│ │ │ └── training.page.styl
│ │ ├── cmeditor
│ │ │ ├── cmeditor.page.js
│ │ │ └── textareaHandler.js
│ │ ├── datepicker
│ │ │ └── datepicker.page.js
│ │ ├── dialog
│ │ │ └── dialog.page.styl
│ │ ├── tooltip
│ │ │ └── tooltip.page.js
│ │ ├── pager
│ │ │ └── pager.page.styl
│ │ └── footer
│ │ │ └── footer.page.js
│ ├── breakpoints.json
│ ├── postcss.config.js
│ ├── static
│ │ ├── favicon.ico
│ │ ├── img
│ │ │ └── avatar.png
│ │ ├── favicon-16x16.png
│ │ ├── favicon-32x32.png
│ │ ├── favicon-96x96.png
│ │ ├── mstile-144x144.png
│ │ ├── android-chrome-192x192.png
│ │ ├── apple-touch-icon-180x180.png
│ │ └── manifest.json
│ ├── utils
│ │ ├── delay.js
│ │ ├── zIndexManager.js
│ │ ├── substitute.js
│ │ ├── i18n.js
│ │ ├── parseQueryString.js
│ │ ├── mediaQuery.js
│ │ ├── mongoId.js
│ │ ├── emulateAnchorClick.js
│ │ ├── tpl.js
│ │ └── loadReactRedux.js
│ ├── misc
│ │ ├── immersive-background.jpg
│ │ ├── immersive-background@2x.jpg
│ │ ├── icons
│ │ │ ├── send.svg
│ │ │ ├── check.svg
│ │ │ ├── flag.svg
│ │ │ ├── add.svg
│ │ │ ├── expand_less.svg
│ │ │ ├── expand_more.svg
│ │ │ ├── chevron_left.svg
│ │ │ ├── chevron_right.svg
│ │ │ ├── italic.svg
│ │ │ ├── download.svg
│ │ │ ├── statistics.svg
│ │ │ ├── upload.svg
│ │ │ ├── warning.svg
│ │ │ ├── formula.svg
│ │ │ ├── star.svg
│ │ │ ├── code.svg
│ │ │ ├── quote.svg
│ │ │ ├── close.svg
│ │ │ ├── delete.svg
│ │ │ ├── play.svg
│ │ │ ├── reply.svg
│ │ │ ├── logout.svg
│ │ │ ├── hourglass.svg
│ │ │ ├── edit.svg
│ │ │ ├── info--circle.svg
│ │ │ ├── platform--windows.svg
│ │ │ ├── user.svg
│ │ │ ├── check--circle.svg
│ │ │ ├── security.svg
│ │ │ ├── link--external.svg
│ │ │ ├── heart.svg
│ │ │ ├── stack.svg
│ │ │ ├── calendar.svg
│ │ │ ├── template
│ │ │ │ ├── webicon.inc.styl
│ │ │ │ └── webicon.styl
│ │ │ ├── vote--up.svg
│ │ │ ├── close--circle.svg
│ │ │ ├── star--outline.svg
│ │ │ ├── lab.svg
│ │ │ ├── vote--down.svg
│ │ │ ├── facebook.svg
│ │ │ ├── mail.svg
│ │ │ ├── insert--image.svg
│ │ │ ├── ordered_list.svg
│ │ │ ├── enlarge.svg
│ │ │ ├── platform--mobile.svg
│ │ │ ├── shrink.svg
│ │ │ ├── info.svg
│ │ │ ├── refresh.svg
│ │ │ ├── schedule--fill.svg
│ │ │ ├── tag.svg
│ │ │ ├── comment--text.svg
│ │ │ ├── crown.svg
│ │ │ ├── account--circle.svg
│ │ │ ├── comment--multiple.svg
│ │ │ ├── google_plus.svg
│ │ │ ├── help2.svg
│ │ │ ├── erase.svg
│ │ │ ├── linkedin.svg
│ │ │ ├── bold.svg
│ │ │ ├── user--multiple.svg
│ │ │ ├── global.svg
│ │ │ ├── heart--outline.svg
│ │ │ ├── search.svg
│ │ │ ├── filter.svg
│ │ │ ├── platform--unknown.svg
│ │ │ ├── platform--ios.svg
│ │ │ ├── block.svg
│ │ │ ├── schedule.svg
│ │ │ ├── insert--link.svg
│ │ │ ├── debug.svg
│ │ │ ├── homework.svg
│ │ │ ├── award.svg
│ │ │ ├── sliders.svg
│ │ │ ├── twitter.svg
│ │ │ ├── qq.svg
│ │ │ ├── platform--chromeos.svg
│ │ │ ├── file.svg
│ │ │ ├── preview.svg
│ │ │ ├── wrench.svg
│ │ │ ├── feeling-lucky.svg
│ │ │ ├── unordered_list.svg
│ │ │ ├── github.svg
│ │ │ ├── settings.svg
│ │ │ ├── wechat.svg
│ │ │ ├── help.svg
│ │ │ ├── web.svg
│ │ │ ├── platform--android.svg
│ │ │ ├── platform--mac.svg
│ │ │ ├── link.svg
│ │ │ └── platform--linux.svg
│ │ ├── textalign.styl
│ │ ├── float.styl
│ │ ├── nothing.styl
│ │ ├── immersive.styl
│ │ └── slideout.styl
│ ├── pages
│ │ ├── discussion_node_bg
│ │ │ ├── nodes_qa.png
│ │ │ ├── nodes@2x_qa.png
│ │ │ ├── nodes_share.png
│ │ │ ├── nodes_vijos.png
│ │ │ ├── nodes@2x_share.png
│ │ │ ├── nodes@2x_vijos.png
│ │ │ ├── nodes_advice.png
│ │ │ ├── nodes_solution.png
│ │ │ ├── nodes@2x_advice.png
│ │ │ └── nodes@2x_solution.png
│ │ ├── domain_manage_role.page.styl
│ │ ├── domain_manage_user.page.styl
│ │ ├── home_account.page.styl
│ │ ├── domain_join.page.styl
│ │ ├── problem_submit.page.styl
│ │ ├── contest_detail.page.styl
│ │ ├── problem_edit.page.js
│ │ ├── discussion_node.page.js
│ │ ├── problem_submit.page.js
│ │ ├── discussion_detail.page.js
│ │ ├── problem_solution.page.js
│ │ ├── home_domain.page.styl
│ │ ├── domain_manage_permission.page.styl
│ │ ├── discussion_detail.page.styl
│ │ ├── record_detail.page.styl
│ │ ├── contest_scoreboard.page.styl
│ │ ├── contest_system_test.page.js
│ │ ├── problem_settings.page.js
│ │ ├── home_security.page.styl
│ │ ├── record_detail.page.js
│ │ ├── training_detail.page.styl
│ │ ├── domain_manage_join_applications.page.js
│ │ ├── user_detail.page.js
│ │ ├── error.page.styl
│ │ ├── discussion_main.page.styl
│ │ ├── record_main.page.js
│ │ ├── training_main.page.styl
│ │ └── record_main.page.styl
│ ├── templates
│ │ ├── layout
│ │ │ ├── simple.html
│ │ │ ├── home_base.html
│ │ │ ├── wiki_base.html
│ │ │ ├── basic.html
│ │ │ └── immersive.html
│ │ ├── partials
│ │ │ ├── homework_default_penalty_rules.yaml
│ │ │ ├── problem_stat.html
│ │ │ ├── hamburger.html
│ │ │ ├── training_default.json
│ │ │ ├── problem_lucky.html
│ │ │ ├── problem_sidebar.html
│ │ │ ├── discussion_edit_form.html
│ │ │ ├── path.html
│ │ │ ├── header.html
│ │ │ ├── domain_edit_form.html
│ │ │ └── header_mobile.html
│ │ ├── judge_playground.html
│ │ ├── components
│ │ │ ├── nothing.html
│ │ │ ├── noscript_note.html
│ │ │ ├── contest.html
│ │ │ ├── homework.html
│ │ │ ├── sidemenu.html
│ │ │ ├── home.html
│ │ │ ├── record.html
│ │ │ └── md_hint.html
│ │ ├── user_register_mail.html
│ │ ├── user_register_mail_sent.html
│ │ ├── user_changemail_mail_sent.html
│ │ ├── user_lostpass_mail_sent.html
│ │ ├── user_changemail_mail.html
│ │ ├── user_lostpass_mail.html
│ │ ├── ac_mail.html
│ │ ├── user_logout.html
│ │ ├── problem_upload.html
│ │ ├── problem_statistics.html
│ │ ├── problem_submit_tr.html
│ │ ├── discussion_nodes_widget.html
│ │ ├── error.html
│ │ ├── domain_manage_edit.html
│ │ ├── home_messages.html
│ │ ├── domain_manage_discussion.html
│ │ ├── user_lostpass.html
│ │ ├── user_register.html
│ │ ├── domain_manage_dashboard.html
│ │ ├── record_detail_summary.html
│ │ ├── user_lostpass_with_code.html
│ │ ├── discussion_create.html
│ │ ├── user_register_with_code.html
│ │ ├── domain_join.html
│ │ ├── contest_scoreboard_download_html.html
│ │ └── user_login.html
│ ├── common
│ │ ├── rem.inc.styl
│ │ ├── functions.inc.styl
│ │ ├── color.inc.styl
│ │ └── common.inc.styl
│ ├── jsconfig.json
│ └── constant
│ │ ├── util
│ │ └── objectMeta.js
│ │ ├── model.js
│ │ ├── domain.js
│ │ ├── contest.js
│ │ └── setting.js
├── job
│ └── __init__.py
└── db.py
├── .dockerignore
├── pm
├── scripts
├── build
│ ├── index.js
│ ├── utils
│ │ ├── root.js
│ │ └── mapWebpackUrlPrefix.js
│ ├── main.js
│ ├── plugins
│ │ ├── webpackDummyOutputPlugin.js
│ │ ├── gulpGenerateLocales.js
│ │ └── gulpTouch.js
│ ├── runGulp.js
│ └── runWebpack.js
├── start_server_dev.sh
├── start_server_production.sh
└── fix_abroad_shrinkwrap.js
├── .github_banner.png
├── pylintrc
├── examples
├── docker-compose.env
├── haproxy-site.cfg
├── h2o-site.conf
├── vj4.service
└── nginx-site.conf
├── Jenkinsfile
├── .gitignore
├── docker-compose.yml
├── sources.list
├── setup.py
└── requirements.txt
/vj4/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/handler/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/model/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/service/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/test/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/util/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/pipeline/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/upgrader/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/model/adaptor/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | vj4/.uibuild
2 | vj4/constant
3 |
--------------------------------------------------------------------------------
/vj4/ui/components/katex/katex.styl:
--------------------------------------------------------------------------------
1 | .katex
2 | font-size: 1em
3 |
--------------------------------------------------------------------------------
/pm:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | python3 -m $@ --db-host=$DB_HOST --db-name=$DB_NAME
--------------------------------------------------------------------------------
/scripts/build/index.js:
--------------------------------------------------------------------------------
1 | require('babel-register');
2 | require('./main.js');
3 |
--------------------------------------------------------------------------------
/vj4/ui/components/react-tabs/rc-tabs.page.js:
--------------------------------------------------------------------------------
1 | import 'rc-tabs/assets/index.css';
2 |
--------------------------------------------------------------------------------
/.github_banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/.github_banner.png
--------------------------------------------------------------------------------
/vj4/ui/breakpoints.json:
--------------------------------------------------------------------------------
1 | {
2 | "mobile": 450,
3 | "desktop": 1000,
4 | "hd": 1250
5 | }
6 |
--------------------------------------------------------------------------------
/vj4/ui/components/scratchpad/var.inc.styl:
--------------------------------------------------------------------------------
1 | $panel-header-height = 34px
2 | $tab-color = #fff170
3 |
--------------------------------------------------------------------------------
/vj4/ui/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | 'autoprefixer': {},
4 | },
5 | };
6 |
--------------------------------------------------------------------------------
/vj4/ui/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/static/favicon.ico
--------------------------------------------------------------------------------
/vj4/ui/components/nprogress/index.js:
--------------------------------------------------------------------------------
1 | import NProgress from 'nprogress';
2 |
3 | export default NProgress;
4 |
--------------------------------------------------------------------------------
/vj4/ui/static/img/avatar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/static/img/avatar.png
--------------------------------------------------------------------------------
/pylintrc:
--------------------------------------------------------------------------------
1 | [FORMAT]
2 | indent-string=' '
3 | indent-after-paren=4
4 |
5 | [MESSAGES CONTROL]
6 | disable=C0111,C0301
7 |
--------------------------------------------------------------------------------
/vj4/ui/static/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/static/favicon-16x16.png
--------------------------------------------------------------------------------
/vj4/ui/static/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/static/favicon-32x32.png
--------------------------------------------------------------------------------
/vj4/ui/static/favicon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/static/favicon-96x96.png
--------------------------------------------------------------------------------
/vj4/ui/static/mstile-144x144.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/static/mstile-144x144.png
--------------------------------------------------------------------------------
/vj4/ui/components/problem/rp.page.styl:
--------------------------------------------------------------------------------
1 | .problem__rp-tag
2 | font-size: 0.8em
3 | padding: rem(0 5px)
4 | color: #AAA
5 |
--------------------------------------------------------------------------------
/vj4/ui/utils/delay.js:
--------------------------------------------------------------------------------
1 | export default function (ms) {
2 | return new Promise(resolve => setTimeout(resolve, ms));
3 | }
4 |
--------------------------------------------------------------------------------
/examples/docker-compose.env:
--------------------------------------------------------------------------------
1 | URL_PREFIX=http://joj.sstia.tech
2 | OAUTH=jaccount
3 | OAUTH_CLIENT_ID=xxx
4 | OAUTH_CLIENT_SECRET=xxx
--------------------------------------------------------------------------------
/vj4/ui/misc/immersive-background.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/misc/immersive-background.jpg
--------------------------------------------------------------------------------
/vj4/ui/components/header/header-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/header/header-logo.png
--------------------------------------------------------------------------------
/vj4/ui/misc/immersive-background@2x.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/misc/immersive-background@2x.jpg
--------------------------------------------------------------------------------
/vj4/ui/static/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/static/android-chrome-192x192.png
--------------------------------------------------------------------------------
/vj4/ui/static/apple-touch-icon-180x180.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/static/apple-touch-icon-180x180.png
--------------------------------------------------------------------------------
/vj4/ui/components/header/header-logo-mano.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/header/header-logo-mano.png
--------------------------------------------------------------------------------
/vj4/ui/components/header/header-logo@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/header/header-logo@2x.png
--------------------------------------------------------------------------------
/vj4/ui/components/profile/backgrounds/1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/profile/backgrounds/1.jpg
--------------------------------------------------------------------------------
/vj4/ui/components/profile/backgrounds/10.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/profile/backgrounds/10.jpg
--------------------------------------------------------------------------------
/vj4/ui/components/profile/backgrounds/11.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/profile/backgrounds/11.jpg
--------------------------------------------------------------------------------
/vj4/ui/components/profile/backgrounds/12.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/profile/backgrounds/12.jpg
--------------------------------------------------------------------------------
/vj4/ui/components/profile/backgrounds/13.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/profile/backgrounds/13.jpg
--------------------------------------------------------------------------------
/vj4/ui/components/profile/backgrounds/14.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/profile/backgrounds/14.jpg
--------------------------------------------------------------------------------
/vj4/ui/components/profile/backgrounds/15.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/profile/backgrounds/15.jpg
--------------------------------------------------------------------------------
/vj4/ui/components/profile/backgrounds/16.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/profile/backgrounds/16.jpg
--------------------------------------------------------------------------------
/vj4/ui/components/profile/backgrounds/17.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/profile/backgrounds/17.jpg
--------------------------------------------------------------------------------
/vj4/ui/components/profile/backgrounds/18.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/profile/backgrounds/18.jpg
--------------------------------------------------------------------------------
/vj4/ui/components/profile/backgrounds/19.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/profile/backgrounds/19.jpg
--------------------------------------------------------------------------------
/vj4/ui/components/profile/backgrounds/2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/profile/backgrounds/2.jpg
--------------------------------------------------------------------------------
/vj4/ui/components/profile/backgrounds/20.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/profile/backgrounds/20.jpg
--------------------------------------------------------------------------------
/vj4/ui/components/profile/backgrounds/21.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/profile/backgrounds/21.jpg
--------------------------------------------------------------------------------
/vj4/ui/components/profile/backgrounds/3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/profile/backgrounds/3.jpg
--------------------------------------------------------------------------------
/vj4/ui/components/profile/backgrounds/4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/profile/backgrounds/4.jpg
--------------------------------------------------------------------------------
/vj4/ui/components/profile/backgrounds/5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/profile/backgrounds/5.jpg
--------------------------------------------------------------------------------
/vj4/ui/components/profile/backgrounds/6.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/profile/backgrounds/6.jpg
--------------------------------------------------------------------------------
/vj4/ui/components/profile/backgrounds/7.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/profile/backgrounds/7.jpg
--------------------------------------------------------------------------------
/vj4/ui/components/profile/backgrounds/8.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/profile/backgrounds/8.jpg
--------------------------------------------------------------------------------
/vj4/ui/components/profile/backgrounds/9.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/profile/backgrounds/9.jpg
--------------------------------------------------------------------------------
/vj4/ui/pages/discussion_node_bg/nodes_qa.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/pages/discussion_node_bg/nodes_qa.png
--------------------------------------------------------------------------------
/vj4/ui/components/header/header-background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/header/header-background.png
--------------------------------------------------------------------------------
/vj4/ui/components/header/header-logo-summer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/header/header-logo-summer.png
--------------------------------------------------------------------------------
/vj4/ui/pages/discussion_node_bg/nodes@2x_qa.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/pages/discussion_node_bg/nodes@2x_qa.png
--------------------------------------------------------------------------------
/vj4/ui/pages/discussion_node_bg/nodes_share.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/pages/discussion_node_bg/nodes_share.png
--------------------------------------------------------------------------------
/vj4/ui/pages/discussion_node_bg/nodes_vijos.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/pages/discussion_node_bg/nodes_vijos.png
--------------------------------------------------------------------------------
/vj4/ui/templates/layout/simple.html:
--------------------------------------------------------------------------------
1 | {% set layout_name = "basic" %}
2 | {% extends "layout/html5.html" %}
3 | {% block body %}
4 | {% endblock %}
5 |
--------------------------------------------------------------------------------
/vj4/ui/templates/partials/homework_default_penalty_rules.yaml:
--------------------------------------------------------------------------------
1 | # Format:
2 | # hours: coefficient
3 | 1: 0.9
4 | 3: 0.8
5 | 12: 0.75
6 | 9999: 0.5
7 |
--------------------------------------------------------------------------------
/vj4/ui/components/contest/problem-contest-bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/contest/problem-contest-bg.png
--------------------------------------------------------------------------------
/vj4/ui/components/header/header-background@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/header/header-background@2x.png
--------------------------------------------------------------------------------
/vj4/ui/components/header/header-logo-mano@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/header/header-logo-mano@2x.png
--------------------------------------------------------------------------------
/vj4/ui/components/header/header-logo-summer@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/header/header-logo-summer@2x.png
--------------------------------------------------------------------------------
/vj4/ui/components/homework/homework.page.styl:
--------------------------------------------------------------------------------
1 | .homework-status--text
2 | for key, value in $homework-status-color
3 | &.{key}
4 | color: value
5 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/send.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/pages/discussion_node_bg/nodes@2x_share.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/pages/discussion_node_bg/nodes@2x_share.png
--------------------------------------------------------------------------------
/vj4/ui/pages/discussion_node_bg/nodes@2x_vijos.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/pages/discussion_node_bg/nodes@2x_vijos.png
--------------------------------------------------------------------------------
/vj4/ui/pages/discussion_node_bg/nodes_advice.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/pages/discussion_node_bg/nodes_advice.png
--------------------------------------------------------------------------------
/vj4/ui/pages/discussion_node_bg/nodes_solution.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/pages/discussion_node_bg/nodes_solution.png
--------------------------------------------------------------------------------
/vj4/ui/components/contest/problem-contest-bg@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/contest/problem-contest-bg@2x.png
--------------------------------------------------------------------------------
/vj4/ui/components/navigation/nav-logo-small_dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/navigation/nav-logo-small_dark.png
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/check.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/flag.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/pages/discussion_node_bg/nodes@2x_advice.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/pages/discussion_node_bg/nodes@2x_advice.png
--------------------------------------------------------------------------------
/scripts/build/utils/root.js:
--------------------------------------------------------------------------------
1 | import path from 'path';
2 |
3 | export default function root(fn = '.') {
4 | return path.resolve(__dirname, '../../../', fn);
5 | }
6 |
--------------------------------------------------------------------------------
/vj4/ui/common/rem.inc.styl:
--------------------------------------------------------------------------------
1 | rem() {
2 | ret = ();
3 | for arg in arguments {
4 | push(ret, unit(arg / $font-size, 'rem'));
5 | }
6 | return ret;
7 | }
8 |
--------------------------------------------------------------------------------
/vj4/ui/components/header/header-background.png.bak.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/header/header-background.png.bak.png
--------------------------------------------------------------------------------
/vj4/ui/components/navigation/nav-logo-small@2x_dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/navigation/nav-logo-small@2x_dark.png
--------------------------------------------------------------------------------
/vj4/ui/components/navigation/nav-logo-small_light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/navigation/nav-logo-small_light.png
--------------------------------------------------------------------------------
/vj4/ui/components/profile/backgrounds/thumbnail/1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/profile/backgrounds/thumbnail/1.jpg
--------------------------------------------------------------------------------
/vj4/ui/components/profile/backgrounds/thumbnail/10.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/profile/backgrounds/thumbnail/10.jpg
--------------------------------------------------------------------------------
/vj4/ui/components/profile/backgrounds/thumbnail/11.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/profile/backgrounds/thumbnail/11.jpg
--------------------------------------------------------------------------------
/vj4/ui/components/profile/backgrounds/thumbnail/12.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/profile/backgrounds/thumbnail/12.jpg
--------------------------------------------------------------------------------
/vj4/ui/components/profile/backgrounds/thumbnail/13.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/profile/backgrounds/thumbnail/13.jpg
--------------------------------------------------------------------------------
/vj4/ui/components/profile/backgrounds/thumbnail/14.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/profile/backgrounds/thumbnail/14.jpg
--------------------------------------------------------------------------------
/vj4/ui/components/profile/backgrounds/thumbnail/15.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/profile/backgrounds/thumbnail/15.jpg
--------------------------------------------------------------------------------
/vj4/ui/components/profile/backgrounds/thumbnail/16.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/profile/backgrounds/thumbnail/16.jpg
--------------------------------------------------------------------------------
/vj4/ui/components/profile/backgrounds/thumbnail/17.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/profile/backgrounds/thumbnail/17.jpg
--------------------------------------------------------------------------------
/vj4/ui/components/profile/backgrounds/thumbnail/18.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/profile/backgrounds/thumbnail/18.jpg
--------------------------------------------------------------------------------
/vj4/ui/components/profile/backgrounds/thumbnail/19.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/profile/backgrounds/thumbnail/19.jpg
--------------------------------------------------------------------------------
/vj4/ui/components/profile/backgrounds/thumbnail/2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/profile/backgrounds/thumbnail/2.jpg
--------------------------------------------------------------------------------
/vj4/ui/components/profile/backgrounds/thumbnail/20.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/profile/backgrounds/thumbnail/20.jpg
--------------------------------------------------------------------------------
/vj4/ui/components/profile/backgrounds/thumbnail/21.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/profile/backgrounds/thumbnail/21.jpg
--------------------------------------------------------------------------------
/vj4/ui/components/profile/backgrounds/thumbnail/3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/profile/backgrounds/thumbnail/3.jpg
--------------------------------------------------------------------------------
/vj4/ui/components/profile/backgrounds/thumbnail/4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/profile/backgrounds/thumbnail/4.jpg
--------------------------------------------------------------------------------
/vj4/ui/components/profile/backgrounds/thumbnail/5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/profile/backgrounds/thumbnail/5.jpg
--------------------------------------------------------------------------------
/vj4/ui/components/profile/backgrounds/thumbnail/6.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/profile/backgrounds/thumbnail/6.jpg
--------------------------------------------------------------------------------
/vj4/ui/components/profile/backgrounds/thumbnail/7.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/profile/backgrounds/thumbnail/7.jpg
--------------------------------------------------------------------------------
/vj4/ui/components/profile/backgrounds/thumbnail/8.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/profile/backgrounds/thumbnail/8.jpg
--------------------------------------------------------------------------------
/vj4/ui/components/profile/backgrounds/thumbnail/9.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/profile/backgrounds/thumbnail/9.jpg
--------------------------------------------------------------------------------
/vj4/ui/components/tab/var.inc.styl:
--------------------------------------------------------------------------------
1 | $tab-item-height = 35px
2 | $tab-item-shadow-size = 10px
3 | $tab-item-height-mobile = 32px // 32 * (13 / 16) is an integer
4 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/add.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/expand_less.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/expand_more.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/pages/discussion_node_bg/nodes@2x_solution.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/pages/discussion_node_bg/nodes@2x_solution.png
--------------------------------------------------------------------------------
/vj4/job/__init__.py:
--------------------------------------------------------------------------------
1 | from vj4.job import rank
2 | from vj4.job import record
3 | from vj4.job import rp
4 | from vj4.job import num
5 | from vj4.job import difficulty
6 |
--------------------------------------------------------------------------------
/vj4/ui/components/navigation/nav-logo-small@2x_light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/joint-online-judge/cb4/HEAD/vj4/ui/components/navigation/nav-logo-small@2x_light.png
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/chevron_left.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/chevron_right.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/scripts/start_server_dev.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | PROJECT_DIR=$( dirname "${BASH_SOURCE[0]}" )/..
3 | cd "$PROJECT_DIR"
4 |
5 | npm run build
6 | python3.5 -m vj4.server --debug
7 |
--------------------------------------------------------------------------------
/vj4/ui/components/emoji/emoji.page.styl:
--------------------------------------------------------------------------------
1 | .emoji
2 | width: 1em
3 | height: 1em
4 | display: inline-block
5 | background-size: contain
6 | vertical-align: baseline
7 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/italic.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES6",
4 | "baseUrl": ".",
5 | "paths": {
6 | "vj/*": ["./*"]
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/download.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/statistics.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/upload.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/warning.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/formula.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/templates/partials/problem_stat.html:
--------------------------------------------------------------------------------
1 |
2 |
{{ _('{0} problems').format(pcount) }}
3 |
4 |
--------------------------------------------------------------------------------
/Jenkinsfile:
--------------------------------------------------------------------------------
1 | pipeline {
2 | agent any
3 | stages {
4 | stage('Build') {
5 | steps {
6 | sh 'docker build -t jioj/cb4:latest .'
7 | }
8 | }
9 | }
10 | }
--------------------------------------------------------------------------------
/scripts/start_server_production.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | PROJECT_DIR=$( dirname "${BASH_SOURCE[0]}" )/..
3 | cd "$PROJECT_DIR"
4 |
5 | npm run build:production
6 | python3.5 -m vj4.unix_server
7 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/star.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/code.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/quote.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/handler/error.py:
--------------------------------------------------------------------------------
1 | from vj4 import error
2 | from vj4.handler import base
3 |
4 |
5 | class NotFoundHandler(base.Handler):
6 | async def get(self):
7 | raise error.NotFoundError(self.url)
8 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/close.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/pages/domain_manage_role.page.styl:
--------------------------------------------------------------------------------
1 | .page--domain_manage_role
2 | .col--checkbox
3 | width: 60px
4 |
5 | .col--id
6 | width: 140px
7 |
8 | .col--users
9 | width: 100px
10 |
--------------------------------------------------------------------------------
/vj4/ui/pages/domain_manage_user.page.styl:
--------------------------------------------------------------------------------
1 | .page--domain_manage_user
2 | .col--checkbox
3 | width: 60px
4 |
5 | .col--uid
6 | width: 160px
7 |
8 | .col--role
9 | width: 160px
10 |
--------------------------------------------------------------------------------
/vj4/ui/components/autocomplete/userselectautocomplete.page.styl:
--------------------------------------------------------------------------------
1 | .menu.user-select
2 | width: rem(200px)
3 |
4 | .user-select__uid
5 | font-size: rem($font-size-small)
6 | color: $userselect-uid-color
7 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/delete.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/play.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/reply.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/pages/home_account.page.styl:
--------------------------------------------------------------------------------
1 | .page--home_account
2 | [name="form_item_background_img"] .radiobox__image
3 | width: rem(100px)
4 | height: rem(62px)
5 | background-size: rem(100px 62px)
6 |
--------------------------------------------------------------------------------
/vj4/ui/templates/judge_playground.html:
--------------------------------------------------------------------------------
1 | {% extends "layout/basic.html" %}
2 | {% block content %}
3 |
6 | {% endblock %}
7 |
--------------------------------------------------------------------------------
/examples/haproxy-site.cfg:
--------------------------------------------------------------------------------
1 | frontend vj4-frontend
2 | bind *:8080
3 | default_backend vj4-backend
4 |
5 | backend vj4-backend
6 | # Need to disable chroot for haproxy.
7 | server sock1 /home/vj4/vj4.sock
8 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/logout.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/misc/textalign.styl:
--------------------------------------------------------------------------------
1 | .text-center
2 | text-align: center
3 |
4 | .text-right
5 | text-align: right
6 |
7 | .text-left
8 | text-align: left
9 |
10 | .text-justify
11 | text-align: justify
12 |
--------------------------------------------------------------------------------
/vj4/ui/components/profile/backgrounds/gen_thumbnails.sh:
--------------------------------------------------------------------------------
1 | for f in *.jpg
2 | do
3 | echo "$f..."
4 | convert "$f" -strip -interlace Plane -quality 92 -resize 200x124^ -gravity center -extent 200x124 "thumbnail/$f"
5 | done
6 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/hourglass.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/static/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Vijos",
3 | "icons": [
4 | {
5 | "src": "\/android-chrome-192x192.png",
6 | "sizes": "192x192",
7 | "type": "image\/png",
8 | "density": 4
9 | }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/edit.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/info--circle.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/platform--windows.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/user.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/check--circle.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/templates/components/nothing.html:
--------------------------------------------------------------------------------
1 | {% macro render(text, compact=false) %}
2 |
3 |
4 | {{ _(text) }}
5 |
6 | {% endmacro %}
7 |
--------------------------------------------------------------------------------
/examples/h2o-site.conf:
--------------------------------------------------------------------------------
1 | listen:
2 | port: 8080
3 | hosts:
4 | "*":
5 | paths:
6 | /:
7 | proxy.reverse.url: "http://[unix:/home/vj4/vj4.sock]"
8 | proxy.preserve-host: ON
9 | proxy.websocket: ON
10 |
11 |
--------------------------------------------------------------------------------
/vj4/ui/components/react-splitpane/SplitPaneFillOverlay.page.styl:
--------------------------------------------------------------------------------
1 | .splitpane-fill
2 | display: flex
3 | position: absolute
4 | overflow: hidden
5 | left: 0
6 | width: 100%
7 | top: 0
8 | height: 100%
9 | user-select: none
10 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/security.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/constant/util/objectMeta.js:
--------------------------------------------------------------------------------
1 | export default function attachObjectMeta(obj, key, value) {
2 | Object.defineProperty(obj, `__${key}`, {
3 | value,
4 | enumerable: false,
5 | configurable: false,
6 | writable: false,
7 | });
8 | }
9 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/link--external.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/pages/domain_join.page.styl:
--------------------------------------------------------------------------------
1 | .page--domain_join
2 | .domain_join__container
3 | +above(rupture.mobile-cutoff)
4 | width: rem(500px)
5 | margin: rem(50px) auto
6 |
7 | +above(rupture.desktop-cutoff)
8 | width: rem(700px)
9 |
--------------------------------------------------------------------------------
/vj4/ui/utils/zIndexManager.js:
--------------------------------------------------------------------------------
1 | let zIndexCurrent = 1000;
2 |
3 | const manager = {
4 | getCurrent() {
5 | return zIndexCurrent;
6 | },
7 | getNext() {
8 | return ++zIndexCurrent;
9 | },
10 | };
11 |
12 | export default manager;
13 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/heart.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/utils/substitute.js:
--------------------------------------------------------------------------------
1 | export default function substitute(str, obj) {
2 | return str.replace(/\{([^{}]+)\}/g, (match, key) => {
3 | if (obj[key] !== undefined) {
4 | return String(obj[key]);
5 | }
6 | return `{${key}}`;
7 | });
8 | }
9 |
--------------------------------------------------------------------------------
/vj4/ui/components/scratchpad/Panel.page.styl:
--------------------------------------------------------------------------------
1 | @import './var.inc.styl'
2 |
3 | .scratchpad__panel-title
4 | background: $primary-color
5 | line-height: rem($panel-header-height)
6 | padding: rem(0 5px)
7 | font-size: rem($font-size-small)
8 | color: #FFF
9 |
--------------------------------------------------------------------------------
/vj4/ui/templates/partials/hamburger.html:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/vj4/ui/misc/float.styl:
--------------------------------------------------------------------------------
1 | .float-left
2 | float: left
3 |
4 | .float-right
5 | float: right
6 |
7 | .clearfix:after
8 | visibility: hidden
9 | display: block
10 | font-size: 0
11 | content: ' '
12 | clear: both
13 | height: 0
14 | zoom: 1
15 |
--------------------------------------------------------------------------------
/vj4/ui/common/functions.inc.styl:
--------------------------------------------------------------------------------
1 | word-wrap()
2 | overflow-wrap: break-word
3 | word-wrap: break-word
4 | -ms-word-break: break-all
5 | word-break: break-word
6 | -ms-hyphens: auto
7 | -moz-hyphens: auto
8 | -webkit-hyphens: auto
9 | hyphens: auto
10 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/stack.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/utils/i18n.js:
--------------------------------------------------------------------------------
1 | import substitute from './substitute';
2 |
3 | export default function i18n(str, ...params) {
4 | if (window.LOCALES && window.LOCALES[str]) {
5 | return substitute(window.LOCALES[str], params);
6 | }
7 | return substitute(str, params);
8 | }
9 |
--------------------------------------------------------------------------------
/vj4/ui/components/tab/tab.page.js:
--------------------------------------------------------------------------------
1 | import { AutoloadPage } from 'vj/misc/PageLoader';
2 | import Tab from './Tab';
3 |
4 | const tabPage = new AutoloadPage('tabPage', () => {
5 | Tab.initAll();
6 | Tab.initEventListeners();
7 | });
8 |
9 | export default tabPage;
10 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/calendar.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/template/webicon.inc.styl:
--------------------------------------------------------------------------------
1 | $icon-font-name = '{{ options.fontName }}'
2 |
3 | {% for icon in glyphs %}
4 | $icon-{{ icon.name }} = '\{{ icon.unicode[0].charCodeAt(0).toString(16)|upper }}'
5 | $icon-{{ icon.name }}-file = '{{ icon.name }}.svg'
6 | {% endfor %}
7 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/vote--up.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/templates/components/noscript_note.html:
--------------------------------------------------------------------------------
1 | {% macro render() %}
2 |
7 | {% endmacro %}
8 |
--------------------------------------------------------------------------------
/vj4/ui/components/marker/marker.page.js:
--------------------------------------------------------------------------------
1 | import { AutoloadPage } from 'vj/misc/PageLoader';
2 | import MarkerReactive from './MarkerReactive';
3 |
4 | const markerPage = new AutoloadPage('markerPage', () => {
5 | MarkerReactive.initAll();
6 | });
7 |
8 | export default markerPage;
9 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/close--circle.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/star--outline.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/pages/problem_submit.page.styl:
--------------------------------------------------------------------------------
1 | .page--problem_submit
2 | .col--status
3 | width: rem(150px)
4 |
5 | .col--submit-at
6 | width: rem(160px)
7 | text-align: center
8 |
9 | +mobile()
10 | .col--memory,
11 | .col--time
12 | display: none
13 |
--------------------------------------------------------------------------------
/vj4/ui/components/dropdown/dropdown.page.js:
--------------------------------------------------------------------------------
1 | import { AutoloadPage } from 'vj/misc/PageLoader';
2 | import Dropdown from './Dropdown';
3 |
4 | const dropdownPage = new AutoloadPage('dropdownPage', () => {
5 | Dropdown.registerLifeCycleHooks();
6 | });
7 |
8 | export default dropdownPage;
9 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/lab.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/vote--down.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/facebook.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/mail.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/components/table/styledTable.page.js:
--------------------------------------------------------------------------------
1 | import { AutoloadPage } from 'vj/misc/PageLoader';
2 | import StyledTable from './StyledTable';
3 |
4 | const styledTablePage = new AutoloadPage('styledTablePage', () => {
5 | StyledTable.registerLifeCycleHooks();
6 | });
7 |
8 | export default styledTablePage;
9 |
--------------------------------------------------------------------------------
/vj4/ui/components/form/select.page.js:
--------------------------------------------------------------------------------
1 | import { AutoloadPage } from 'vj/misc/PageLoader';
2 | import 'select2';
3 | import 'select2/dist/css/select2.css';
4 |
5 | const selectPage = new AutoloadPage('selectPage', () => {
6 | $('.select.select2').select2();
7 | });
8 |
9 | export default selectPage;
10 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/insert--image.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/ordered_list.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/pages/contest_detail.page.styl:
--------------------------------------------------------------------------------
1 | .page--contest_detail
2 | .col--status
3 | width: rem(150px)
4 |
5 | .col--submit-at
6 | width: rem(150px)
7 |
8 | .col--problem
9 | border-left: 1px solid $table-border-color
10 |
11 | +mobile()
12 | .col--submit-at
13 | display: none
14 |
--------------------------------------------------------------------------------
/vj4/ui/components/autocomplete/autocomplete.page.styl:
--------------------------------------------------------------------------------
1 | .drop-element.autocomplete
2 | max-width: rem(250px)
3 |
4 | .empty-row
5 | color: $autocomplete-empty-row-color
6 | font-size: $(font-size-small)
7 | padding: rem(10px)
8 |
9 | .menu
10 | max-height: rem(250px)
11 | overflow: auto
12 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/enlarge.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/platform--mobile.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/shrink.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/info.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/refresh.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/schedule--fill.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/tag.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/templates/user_register_mail.html:
--------------------------------------------------------------------------------
1 | {% extends "layout/mail.html" %}
2 | {% block title %}{{ _('Sign Up') }}{% endblock %}
3 | {% block content %}
4 | {{ _('Hello! You can click following link to sign up your Vijos account:') }}
5 | {{ url_prefix }}{{ url }}
6 | {% endblock %}
7 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/comment--text.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/crown.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/templates/partials/training_default.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "_id": 1,
4 | "title": "最初的最初 - A+B Problem",
5 | "require_nids": [],
6 | "pids": [1000, "56c52ade2fefd90539000005"]
7 | },
8 | {
9 | "_id": 2,
10 | "title": "最初的进阶",
11 | "require_nids": [1],
12 | "pids": [1001, 1002]
13 | }
14 | ]
15 |
--------------------------------------------------------------------------------
/vj4/ui/templates/user_register_mail_sent.html:
--------------------------------------------------------------------------------
1 | {% extends "layout/immersive.html" %}
2 | {% block content %}
3 |
4 |
{{ _('Sign Up') }}
5 |
6 | {{ _('Sign up mail has been sent to your email.') }}
7 |
8 |
9 | {% endblock %}
10 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/account--circle.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/comment--multiple.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/misc/nothing.styl:
--------------------------------------------------------------------------------
1 | .nothing-placeholder
2 | padding: rem(80px 0)
3 | text-align: center
4 | font-size: rem($font-size-title)
5 | color: #AAA
6 |
7 | &.compact
8 | padding: rem(20px 0)
9 |
10 | .nothing-icon
11 | background: url(./puzzled_twd2.svg) no-repeat center center
12 | height: 184px
13 | margin-bottom: rem(20px)
14 |
--------------------------------------------------------------------------------
/vj4/ui/templates/components/contest.html:
--------------------------------------------------------------------------------
1 | {% import "components/user.html" as user with context %}
2 | {% macro render_time(tdoc_time) %}
3 | {{ datetime_span(tdoc_time, false, '%Y-%m-%d %H:%M') }}
4 | {% endmacro %}
5 |
6 | {% macro render_duration(tdoc) %}
7 | {{ ((tdoc['end_at'] - tdoc['begin_at']).total_seconds() / 3600)|round(1) }}
8 | {% endmacro %}
9 |
--------------------------------------------------------------------------------
/vj4/ui/templates/user_changemail_mail_sent.html:
--------------------------------------------------------------------------------
1 | {% extends "layout/immersive.html" %}
2 | {% block content %}
3 |
4 |
{{ _('New Email') }}
5 |
6 | {{ _('Confirmation mail has been sent to your new email.') }}
7 |
8 |
9 | {% endblock %}
10 |
--------------------------------------------------------------------------------
/vj4/ui/templates/user_lostpass_mail_sent.html:
--------------------------------------------------------------------------------
1 | {% extends "layout/immersive.html" %}
2 | {% block content %}
3 |
4 |
{{ _('Lost Password') }}
5 |
6 | {{ _('Password reset mail has been sent to your email.') }}
7 |
8 |
9 | {% endblock %}
10 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/google_plus.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/templates/components/homework.html:
--------------------------------------------------------------------------------
1 | {% import "components/user.html" as user with context %}
2 | {% macro render_time(tdoc_time) %}
3 | {{ datetime_span(tdoc_time, false, '%Y-%m-%d %H:%M') }}
4 | {% endmacro %}
5 |
6 | {% macro render_extension(tdoc) %}
7 | {{ ((tdoc['end_at'] - tdoc['penalty_since']).total_seconds() / 3600)|round(1) }}
8 | {% endmacro %}
9 |
--------------------------------------------------------------------------------
/vj4/ui/utils/parseQueryString.js:
--------------------------------------------------------------------------------
1 | export default function parseQueryString(str) {
2 | const obj = {};
3 | (str || document.location.search)
4 | .replace(/(^\?)/, '')
5 | .split('&')
6 | .forEach((n) => {
7 | const s = n.split('=').map(v => decodeURIComponent(v));
8 | obj[s[0]] = s[1];
9 | });
10 | return obj;
11 | }
12 |
--------------------------------------------------------------------------------
/scripts/build/main.js:
--------------------------------------------------------------------------------
1 | import { argv } from 'yargs';
2 | import root from './utils/root.js';
3 | import runGulp from './runGulp.js';
4 | import runWebpack from './runWebpack.js';
5 |
6 | const { watch, production } = argv;
7 |
8 | async function main() {
9 | await runGulp(argv);
10 | runWebpack(argv);
11 | }
12 |
13 | process.chdir(root());
14 | main();
15 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/help2.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/components/scratchpad/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import ui from './ui';
3 | import editor from './editor';
4 | import pretest from './pretest';
5 | import records from './records';
6 |
7 | const reducer = combineReducers({
8 | ui,
9 | editor,
10 | pretest,
11 | records,
12 | });
13 |
14 | export default reducer;
15 |
--------------------------------------------------------------------------------
/vj4/ui/templates/user_changemail_mail.html:
--------------------------------------------------------------------------------
1 | {% extends "layout/mail.html" %}
2 | {% block title %}{{ _('Change Email') }}{% endblock %}
3 | {% block content %}
4 | {{ _('Hello, {0}! You can click following link to active your new email of your Vijos account:').format(uname) }}
5 | {{ url_prefix }}{{ url }}
6 | {% endblock %}
7 |
--------------------------------------------------------------------------------
/vj4/ui/templates/user_lostpass_mail.html:
--------------------------------------------------------------------------------
1 | {% extends "layout/mail.html" %}
2 | {% block title %}{{ _('Lost Password') }}{% endblock %}
3 | {% block content %}
4 | {{ _('Hello, {0}! You can click following link to reset the password of your Vijos account:').format(uname) }}
5 | {{ url_prefix }}{{ url }}
6 | {% endblock %}
7 |
--------------------------------------------------------------------------------
/vj4/ui/components/form/var.inc.styl:
--------------------------------------------------------------------------------
1 | form-styles()
2 | appearance: none
3 | display: block
4 | width: 100%
5 | font-size: rem($font-size-secondary)
6 | margin: $input-margin
7 | height: rem($form-control-height)
8 | line-height: 1.2
9 | padding: rem(5px)
10 | border: $input-border
11 |
12 | &.inline
13 | display: inline-block
14 | width: auto
15 |
--------------------------------------------------------------------------------
/vj4/ui/pages/problem_edit.page.js:
--------------------------------------------------------------------------------
1 | import { NamedPage } from 'vj/misc/PageLoader';
2 |
3 | const page = new NamedPage('problem_edit', async () => {
4 | $(document).on('click', '[name="problem-sidebar__show-category"]', (ev) => {
5 | $(ev.currentTarget).hide();
6 | $('[name="problem-sidebar__categories"]').show();
7 | });
8 | });
9 |
10 | export default page;
11 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/erase.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/pages/discussion_node.page.js:
--------------------------------------------------------------------------------
1 | import { NamedPage } from 'vj/misc/PageLoader';
2 |
3 | const page = new NamedPage('discussion_node', async () => {
4 | $(document).on('click', '[name="problem-sidebar__show-category"]', (ev) => {
5 | $(ev.currentTarget).hide();
6 | $('[name="problem-sidebar__categories"]').show();
7 | });
8 | });
9 |
10 | export default page;
11 |
--------------------------------------------------------------------------------
/vj4/ui/pages/problem_submit.page.js:
--------------------------------------------------------------------------------
1 | import { NamedPage } from 'vj/misc/PageLoader';
2 |
3 | const page = new NamedPage('problem_submit', async () => {
4 | $(document).on('click', '[name="problem-sidebar__show-category"]', (ev) => {
5 | $(ev.currentTarget).hide();
6 | $('[name="problem-sidebar__categories"]').show();
7 | });
8 | });
9 |
10 | export default page;
11 |
--------------------------------------------------------------------------------
/vj4/handler/i18n.py:
--------------------------------------------------------------------------------
1 | from vj4 import app
2 | from vj4.handler import base
3 |
4 |
5 | @app.route('/lang/{lang}', 'language_set', global_route=True)
6 | class LanguageHandler(base.Handler):
7 | @base.route_argument
8 | @base.sanitize
9 | async def get(self, *, lang: str):
10 | await self.set_settings(view_lang=lang)
11 | self.json_or_redirect(self.referer_or_main)
12 |
--------------------------------------------------------------------------------
/vj4/ui/components/scratchpad/PanelButton.page.styl:
--------------------------------------------------------------------------------
1 | @import './var.inc.styl'
2 |
3 | .scratchpad__panel-button
4 | background: none
5 | border: 0
6 | line-height: rem($panel-header-height)
7 | margin: 0
8 | color: #FFF
9 | padding: rem(0 5px)
10 | cursor: pointer
11 | font-size: rem($font-size-small)
12 | outline: 0
13 |
14 | &:hover
15 | color: $tab-color
16 |
--------------------------------------------------------------------------------
/vj4/ui/pages/discussion_detail.page.js:
--------------------------------------------------------------------------------
1 | import { NamedPage } from 'vj/misc/PageLoader';
2 |
3 | const page = new NamedPage('discussion_detail', async () => {
4 | $(document).on('click', '[name="problem-sidebar__show-category"]', (ev) => {
5 | $(ev.currentTarget).hide();
6 | $('[name="problem-sidebar__categories"]').show();
7 | });
8 | });
9 |
10 | export default page;
11 |
--------------------------------------------------------------------------------
/vj4/ui/pages/problem_solution.page.js:
--------------------------------------------------------------------------------
1 | import { NamedPage } from 'vj/misc/PageLoader';
2 |
3 | const page = new NamedPage('problem_solution', async () => {
4 | $(document).on('click', '[name="problem-sidebar__show-category"]', (ev) => {
5 | $(ev.currentTarget).hide();
6 | $('[name="problem-sidebar__categories"]').show();
7 | });
8 | });
9 |
10 | export default page;
11 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/linkedin.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/bold.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/user--multiple.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/pages/home_domain.page.styl:
--------------------------------------------------------------------------------
1 | .page--home_domain
2 | .col--icon
3 | width: 55px
4 | line-height: 1
5 |
6 | img
7 | border-radius: 50%
8 |
9 | .col--role
10 | width: rem(160px)
11 | border-left: 1px solid $table-border-color
12 |
13 | .col--action
14 | width: rem(135px)
15 |
16 | +mobile()
17 | .col--icon, .col--role
18 | display: none
19 |
--------------------------------------------------------------------------------
/vj4/ui/components/messagepad/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import activeId from './activeId';
3 | import dialogues from './dialogues';
4 | import inputs from './inputs';
5 | import isPosting from './isPosting';
6 |
7 | const reducer = combineReducers({
8 | activeId,
9 | dialogues,
10 | inputs,
11 | isPosting,
12 | });
13 |
14 | export default reducer;
15 |
--------------------------------------------------------------------------------
/vj4/ui/components/star/star.page.styl:
--------------------------------------------------------------------------------
1 | .star
2 | border: 0
3 | background: none
4 | outline: 0
5 | padding: 0
6 | margin: 0
7 | color: gray
8 |
9 | .starred--show
10 | display: none
11 | .starred--hide
12 | display: block
13 |
14 | &.activated
15 | color: orange
16 | .starred--show
17 | display: block
18 | .starred--hide
19 | display: none
20 |
21 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/global.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/scripts/build/plugins/webpackDummyOutputPlugin.js:
--------------------------------------------------------------------------------
1 | export default class DummyOutputPlugin {
2 | constructor(file) {
3 | this.file = file;
4 | }
5 | apply(compiler) {
6 | compiler.plugin('emit', (compilation, callback) => {
7 | compilation.assets[this.file] = {
8 | source: () => '',
9 | size: () => 0,
10 | };
11 | callback();
12 | });
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/heart--outline.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/search.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/common/color.inc.styl:
--------------------------------------------------------------------------------
1 | aqua = #7FDBFF
2 | blue = #0074D9
3 | navy = #001F3F
4 | teal = #39CCCC
5 | green = #2ECC40
6 | olive = #3D9970
7 | lime = #01FF70
8 | yellow = #FFDC00
9 | orange = #FF851B
10 | red = #FF4136
11 | fuchsia = #F012BE
12 | purple = #B10DC9
13 | maroon = #85144B
14 | white = #ffffff
15 | silver = #dddddd
16 | gray = #aaaaaa
17 | black = #111111
18 |
--------------------------------------------------------------------------------
/vj4/util/rank.py:
--------------------------------------------------------------------------------
1 | def ranked(diter, equ_func=lambda a, b: a == b):
2 | last_doc = None
3 | r = 0
4 | count = 0
5 | for doc in diter:
6 | count += 1
7 | if count == 1 or not equ_func(last_doc, doc):
8 | r = count
9 | last_doc = doc
10 | yield (r, doc)
11 |
12 |
13 | if __name__ == '__main__':
14 | for r, v in ranked(sorted([1, 2, 2, 2, 2, 3, 3, 3, 4, 5, 6])):
15 | print(r, v)
16 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/filter.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/platform--unknown.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/utils/mediaQuery.js:
--------------------------------------------------------------------------------
1 | export function isAbove(width) {
2 | if (window.matchMedia) {
3 | return window.matchMedia(`(min-width: ${width}px)`).matches;
4 | }
5 | return window.innerWidth >= width;
6 | }
7 |
8 | export function isBelow(width) {
9 | if (window.matchMedia) {
10 | return window.matchMedia(`(max-width: ${width}px)`).matches;
11 | }
12 | return window.innerWidth <= width;
13 | }
14 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/platform--ios.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/block.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/schedule.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/insert--link.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/debug.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/homework.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/pages/domain_manage_permission.page.styl:
--------------------------------------------------------------------------------
1 | .page--domain_manage_permission
2 | .col--p
3 | width: 80px
4 | text-align: center
5 | border-left: 1px solid $table-border-color
6 |
7 | td.col--description
8 | padding-left: rem(30px) !important
9 |
10 | .col--family
11 | line-height: 2
12 | color: #FFF
13 | background: lighten(saturate($primary-color, 70%), 30%)
14 | border-top: 2px solid $primary-color
15 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/award.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/sliders.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/twitter.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/qq.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/components/rotator/rotator.page.styl:
--------------------------------------------------------------------------------
1 | .rotator
2 | position: relative
3 | height: 1em
4 |
5 | .rotator__item
6 | position: absolute
7 | left: 0
8 | top: 0
9 | width: 100%
10 | height: 100%
11 | transition: transform .2s, opacity .2s
12 | transition-timing-function: ease-out-cubic
13 |
14 | &.pos--above
15 | transform: translateY(-100%)
16 | opacity: 0
17 |
18 | &.pos--below
19 | transform: translateY(100%)
20 | opacity: 0
21 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/platform--chromeos.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/file.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/templates/layout/home_base.html:
--------------------------------------------------------------------------------
1 | {% import "components/home.html" as home with context %}
2 | {% extends "layout/basic.html" %}
3 | {% block content %}
4 |
5 |
6 | {% block home_content %}{% endblock %}
7 |
8 |
9 | {{ home.render_sidebar() }}
10 | {% block home_sidebar %}{% endblock %}
11 |
12 |
13 | {% endblock %}
14 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/preview.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/wrench.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/templates/partials/problem_lucky.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ _('Lucky') }}
4 |
5 |
6 |
{{ _('Pick a problem randomly based on current filter') }}
7 |
8 |
9 |
--------------------------------------------------------------------------------
/vj4/ui/components/discussion/discussion.page.styl:
--------------------------------------------------------------------------------
1 | .discussion-node-tag
2 | font-size: rem($font-size-small)
3 | display: inline-block
4 | line-height: rem(22px)
5 | padding: rem(0 10px)
6 | vertical-align: middle
7 | border-radius: rem(12px)
8 | border: 1px solid $supplementary-border-color
9 |
10 | &:hover
11 | background: $primary-color
12 | color: #FFF !important
13 | text-decoration: none
14 | border-color: $primary-color
15 |
16 | .icon
17 | margin-right: rem(5px)
18 |
--------------------------------------------------------------------------------
/examples/vj4.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=Vj4
3 | After=syslog.target network.target mongodb.service rabbitmq.service
4 |
5 | [Service]
6 | Type=simple
7 | User=vj4
8 | Group=vj4
9 | WorkingDirectory=/home/vj4/vj4
10 | ExecStart=/home/vj4/python-venv/bin/python3.5 -m vj4.server \
11 | --listen=unix:/home/vj4/vj4.sock \
12 | --prefork=8 \
13 | --ip-header="X-Real-IP" \
14 |
15 | Restart=always
16 | StandardOutput=syslog
17 | StandardError=syslog
18 |
19 | [Install]
20 | WantedBy=multi-user.target
21 |
--------------------------------------------------------------------------------
/vj4/ui/components/react/IconComponent.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import classNames from 'classnames';
3 |
4 | export default function IconComponent(props) {
5 | const {
6 | name,
7 | className,
8 | ...rest
9 | } = props;
10 | const cn = classNames(className, `icon icon-${name}`);
11 | return (
12 |
13 | );
14 | }
15 |
16 | IconComponent.propTypes = {
17 | name: React.PropTypes.string.isRequired,
18 | className: React.PropTypes.string,
19 | };
20 |
--------------------------------------------------------------------------------
/vj4/ui/common/common.inc.styl:
--------------------------------------------------------------------------------
1 | // This file is imported in all stylus source code
2 | breakpoints = json('../breakpoints.json', { hash: true })
3 | rupture.mobile-cutoff = unit(breakpoints.mobile, 'px')
4 | rupture.desktop-cutoff = unit(breakpoints.desktop, 'px')
5 | rupture.hd-cutoff = unit(breakpoints.hd, 'px')
6 |
7 | @import '~vj/misc/.iconfont/webicon.inc.styl'
8 | @import 'color.inc.styl'
9 | @import 'variables.inc.styl'
10 | @import 'easing.inc.styl'
11 | @import 'rem.inc.styl'
12 | @import 'functions.inc.styl'
13 |
--------------------------------------------------------------------------------
/vj4/ui/components/scratchpad/DataInput.page.styl:
--------------------------------------------------------------------------------
1 | .scratchpad__data-input
2 | border: 0
3 | border-left: 3px solid #E0E0E0
4 | background: #F8F8F8
5 | padding: rem(8px 4px)
6 | font-family: $code-font-family
7 | font-size: rem($font-size-secondary)
8 | outline: 0
9 | resize: none
10 | color: #666
11 | height: 100%
12 | width: 100%
13 | transition: background-color .2s linear, border-color .2s linear
14 |
15 | &:focus
16 | background: #FFF
17 | border-color: lighten($primary-color, 70%)
18 |
--------------------------------------------------------------------------------
/vj4/ui/utils/mongoId.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 |
3 | // https://github.com/andrasq/node-mongoid-js/blob/master/mongoid.js
4 |
5 | export function parse(idstring) {
6 | if (typeof idstring !== 'string') {
7 | idstring = String(idstring);
8 | }
9 | return {
10 | timestamp: parseInt(idstring.slice( 0, 0+8), 16),
11 | machineid: parseInt(idstring.slice( 8, 8+6), 16),
12 | pid: parseInt(idstring.slice(14, 14+4), 16),
13 | sequence: parseInt(idstring.slice(18, 18+6), 16),
14 | };
15 | }
16 |
--------------------------------------------------------------------------------
/vj4/ui/components/scratchpad/reducers/editor.js:
--------------------------------------------------------------------------------
1 | export default function reducer(state = {
2 | lang: Context.code_lang,
3 | code: Context.code_template,
4 | }, action) {
5 | switch (action.type) {
6 | case 'SCRATCHPAD_EDITOR_UPDATE_CODE': {
7 | return {
8 | ...state,
9 | code: action.payload,
10 | };
11 | }
12 | case 'SCRATCHPAD_EDITOR_SET_LANG': {
13 | return {
14 | ...state,
15 | lang: action.payload,
16 | };
17 | }
18 | default:
19 | return state;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/vj4/ui/pages/discussion_detail.page.styl:
--------------------------------------------------------------------------------
1 | .page--discussion_detail
2 |
3 | .topic__content
4 | margin: rem(20px 0)
5 |
6 | .profile__bg
7 | background-size: cover
8 | background-position: center center
9 | height: rem(60px)
10 | margin-bottom: rem(-32px)
11 |
12 | .sidebar-user-profile
13 | .media__body
14 | padding-left: rem(10px)
15 | padding-top: rem(20px)
16 |
17 | .user-profile-name
18 | font-size: 1.2rem
19 | color: #444
20 | vertical-align: middle
21 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/feeling-lucky.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/unordered_list.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/components/highlighter/highlighter.page.js:
--------------------------------------------------------------------------------
1 | import { AutoloadPage } from 'vj/misc/PageLoader';
2 |
3 | const highlighterPage = new AutoloadPage('highlighterPage', () => {
4 | System.import('./prismjs').then((module) => {
5 | const prismjs = module.default;
6 | function runHighlight($container) {
7 | prismjs.highlightBlocks($container);
8 | }
9 | runHighlight($('body'));
10 | $(document).on('vjContentNew', e => runHighlight($(e.target)));
11 | });
12 | });
13 |
14 | export default highlighterPage;
15 |
--------------------------------------------------------------------------------
/vj4/ui/templates/ac_mail.html:
--------------------------------------------------------------------------------
1 | {% extends "layout/mail.html" %}
2 | {% block title %}{{ _('P{0} - {1} Accepted!').format(pdoc['doc_id'], pdoc['title']) }}{% endblock %}
3 | {% block content %}
4 | {{ _('Congratulations! Your submission is accepted.') }}
5 | {{ _('Problem') }}: P{{ pdoc['doc_id'] }} {{ pdoc['title'] }}
6 | {{ _('Code') }}:
7 |
8 | {{ rdoc['code'] }}
9 |
10 | {% endblock %}
11 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/github.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/test/test_misc.py:
--------------------------------------------------------------------------------
1 | import unittest
2 |
3 | from vj4.util import misc
4 |
5 |
6 | class Test(unittest.TestCase):
7 | def test_dedupe(self):
8 | self.assertListEqual(misc.dedupe([2,1,1,3,2,3]),[2,1,3])
9 | self.assertListEqual(misc.dedupe([]),[])
10 | self.assertListEqual(misc.dedupe(map(int,['2','1','1','3','2','3'])),[2,1,3])
11 | self.assertListEqual(misc.dedupe(['b','a','b','c','b']),['b','a','c'])
12 | self.assertListEqual(misc.dedupe([0]),[0])
13 |
14 |
15 | if __name__ == '__main__':
16 | unittest.main()
17 |
--------------------------------------------------------------------------------
/vj4/ui/templates/partials/problem_sidebar.html:
--------------------------------------------------------------------------------
1 | {% if tdoc %}
2 | {% if tdoc['doc_type'] == vj4.model.document.TYPE_CONTEST %}
3 | {% include "partials/problem_sidebar_contest.html" %}
4 | {% with owner_udoc=udoc %}
5 | {% include "partials/contest_sidebar.html" %}
6 | {% endwith %}
7 | {% else %}
8 | {% include "partials/problem_sidebar_homework.html" %}
9 | {% include "partials/homework_sidebar.html" %}
10 | {% endif %}
11 | {% else %}
12 | {% include "partials/problem_sidebar_normal.html" %}
13 | {% endif %}
14 |
--------------------------------------------------------------------------------
/vj4/ui/components/form/form.page.js:
--------------------------------------------------------------------------------
1 | import { AutoloadPage } from 'vj/misc/PageLoader';
2 |
3 | const formPage = new AutoloadPage('formPage', () => {
4 | $(document).on('vjFormDisableUpdate', 'input, select, textarea', (ev) => {
5 | const $input = $(ev.currentTarget);
6 | const $formItem = $input.closest('.form__item');
7 | if ($input.prop('disabled')) {
8 | $formItem.addClass('is--disabled');
9 | } else {
10 | $formItem.removeClass('is--disabled');
11 | }
12 | });
13 | });
14 |
15 | export default formPage;
16 |
--------------------------------------------------------------------------------
/vj4/ui/components/menu/menu.page.js:
--------------------------------------------------------------------------------
1 | import { AutoloadPage } from 'vj/misc/PageLoader';
2 | import { slideDown } from 'vj/utils/slide';
3 | import delay from 'vj/utils/delay';
4 |
5 | function expandMenu($menu) {
6 | slideDown($menu, 500, { opacity: 0 }, { opacity: 1 });
7 | }
8 |
9 | async function expandAllMenus() {
10 | await delay(200);
11 | $('.menu.collapsed').get().forEach(menu => expandMenu($(menu)));
12 | }
13 |
14 | const menuPage = new AutoloadPage('menuPage', () => {
15 | expandAllMenus();
16 | });
17 |
18 | export default menuPage;
19 |
--------------------------------------------------------------------------------
/vj4/ui/templates/layout/wiki_base.html:
--------------------------------------------------------------------------------
1 | {% extends "layout/basic.html" %}
2 | {% block content %}
3 |
4 |
5 | {% block wiki_content %}{% endblock %}
6 |
7 |
15 |
16 | {% endblock %}
17 |
--------------------------------------------------------------------------------
/vj4/ui/templates/user_logout.html:
--------------------------------------------------------------------------------
1 | {% extends "layout/immersive.html" %}
2 | {% block content %}
3 |
4 |
5 |
{{ _('Logout') }}
6 |
12 |
13 |
14 | {% endblock %}
15 |
--------------------------------------------------------------------------------
/vj4/ui/components/scratchpad/Editor.page.styl:
--------------------------------------------------------------------------------
1 | .ReactCodeMirror
2 | flex: 1
3 | position: relative
4 | overflow: hidden
5 |
6 | .cm-s-vjcm
7 | position: absolute
8 | left: 0
9 | top: 0
10 | width: 100%
11 | height: 100%
12 | font-family: $code-font-family
13 | font-size: rem($font-size-secondary)
14 | line-height: 1.2
15 |
16 | &.CodeMirror-focused .CodeMirror-selected
17 | background: #cee4f2
18 |
19 | .CodeMirror-gutters
20 | border-right: 0
21 | background: none
22 |
23 | .CodeMirror-linenumber
24 | color: #DDD
25 |
--------------------------------------------------------------------------------
/vj4/ui/components/scratchpad/PanelButtonComponent.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import classNames from 'classnames';
3 |
4 | export default function PanelButtonComponent(props) {
5 | const {
6 | className,
7 | children,
8 | ...rest
9 | } = props;
10 | const cn = classNames(className, 'scratchpad__panel-button');
11 | return (
12 |
13 | );
14 | }
15 |
16 | PanelButtonComponent.propTypes = {
17 | className: React.PropTypes.string,
18 | children: React.PropTypes.node,
19 | };
20 |
--------------------------------------------------------------------------------
/vj4/ui/templates/layout/basic.html:
--------------------------------------------------------------------------------
1 | {% set layout_name = "basic" %}
2 | {% extends "layout/html5.html" %}
3 | {% block body %}
4 |
5 | {% include "partials/nav.html" %}
6 |
7 |
8 |
9 | {% include "partials/header_mobile.html" %}
10 | {% include "partials/header.html" %}
11 |
12 | {% block content %}{% endblock %}
13 |
14 | {% include "partials/footer.html" %}
15 |
16 |
17 | {% include "partials/login_dialog.html" %}
18 |
19 | {% endblock %}
20 |
--------------------------------------------------------------------------------
/vj4/ui/templates/problem_upload.html:
--------------------------------------------------------------------------------
1 | {% extends 'layout/simple.html' %}
2 | {% block body %}
3 |
13 | {% endblock %}
14 |
--------------------------------------------------------------------------------
/vj4/ui/pages/record_detail.page.styl:
--------------------------------------------------------------------------------
1 | .page--record_detail
2 |
3 | .compiler-text
4 | padding: rem(20px 0)
5 | font-size: rem($font-size-small)
6 | &:empty
7 | display: none
8 |
9 | .col--case
10 | width: rem(70px)
11 |
12 | .col--memory, .col--time
13 | width: rem(120px)
14 |
15 | .col--case, .col--status
16 | border-right: 1px solid $table-border-color
17 |
18 | +mobile()
19 | .col--case
20 | display: none
21 |
22 | .col--time
23 | width: rem(70px)
24 |
25 | .col--memory
26 | width: rem(100px)
27 |
--------------------------------------------------------------------------------
/vj4/util/pagination.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | from vj4 import error
3 |
4 |
5 | async def paginate(cursor, page: int, page_size: int):
6 | if page <= 0:
7 | raise error.ValidationError('page')
8 | count, page_docs = await asyncio.gather(cursor.count(),
9 | cursor.skip((page - 1) * page_size) \
10 | .limit(page_size) \
11 | .to_list())
12 | num_pages = (count + page_size - 1) // page_size
13 | return page_docs, num_pages, count
14 |
--------------------------------------------------------------------------------
/scripts/build/utils/mapWebpackUrlPrefix.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 |
3 | export default function mapWebpackUrlPrefix(mapList) {
4 | const rules = mapList.map(mappingRule => {
5 | const regex = mappingRule.prefix.split('/').map(s => _.escapeRegExp(s)).join('[\\/\\\\]');
6 | return {
7 | from: new RegExp(`^${regex}`, mappingRule.flag),
8 | to: mappingRule.replace,
9 | };
10 | });
11 | return function mapUrl(url) {
12 | rules.forEach(rule => {
13 | url = url.replace(rule.from, rule.to);
14 | });
15 | return url;
16 | };
17 | }
18 |
--------------------------------------------------------------------------------
/vj4/ui/components/react-splitpane/SplitPaneFillOverlayComponent.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import classNames from 'classnames';
3 |
4 | export default function SplitPaneFillOverlayComponent(props) {
5 | const {
6 | className,
7 | children,
8 | ...rest
9 | } = props;
10 | const cn = classNames(className, 'splitpane-fill');
11 | return (
12 | {children}
13 | );
14 | }
15 |
16 | SplitPaneFillOverlayComponent.propTypes = {
17 | className: React.PropTypes.string,
18 | children: React.PropTypes.node,
19 | };
20 |
--------------------------------------------------------------------------------
/vj4/ui/utils/emulateAnchorClick.js:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * @param {Event} ev
4 | * @param {String} targetUrl
5 | * @param {Boolean} alwaysOpenInNewWindow
6 | */
7 | export default function emulateAnchorClick(ev, targetUrl, alwaysOpenInNewWindow = false) {
8 | let openInNewWindow;
9 | if (alwaysOpenInNewWindow) {
10 | openInNewWindow = true;
11 | } else {
12 | openInNewWindow = (ev.ctrlKey || ev.shiftKey || ev.metaKey);
13 | }
14 | if (openInNewWindow) {
15 | window.open(targetUrl);
16 | } else {
17 | window.location.href = targetUrl;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/vj4/ui/templates/components/sidemenu.html:
--------------------------------------------------------------------------------
1 | {% macro render_item(icon_name, page_name, label=none) %}
2 |
15 | {% endmacro %}
16 |
--------------------------------------------------------------------------------
/vj4/ui/components/dropdown/dropdown.page.styl:
--------------------------------------------------------------------------------
1 | .dropdown-target
2 | display: none
3 |
4 | .drop .dropdown-target
5 | display: block
6 |
7 | .dropdown
8 | .menu
9 | min-width: rem(150px)
10 | box-shadow: $menu-drop-shadow
11 | background: $menu-drop-bg-color
12 | font-size: rem($font-size-small)
13 |
14 | .menu__link
15 | padding: rem(8px 10px)
16 |
17 | .dropdown
18 | .drop-content
19 | transition: transform .1s ease-in-out
20 | transform: scale(0.9) translateZ(0)
21 |
22 | &.drop-after-open
23 | .drop-content
24 | transform: scale(1) translateZ(0)
25 |
--------------------------------------------------------------------------------
/vj4/ui/components/katex/katex.page.js:
--------------------------------------------------------------------------------
1 | import { AutoloadPage } from 'vj/misc/PageLoader';
2 |
3 | import 'katex/dist/katex.min.css';
4 | import './katex.styl';
5 |
6 | const katexPage = new AutoloadPage('katexPage', () => {
7 | System.import('katex/dist/contrib/auto-render.min.js').then((renderKatex) => {
8 | function runKatex($containers) {
9 | $containers.get().forEach(container => renderKatex(container));
10 | }
11 | runKatex($('.typo'));
12 | $(document).on('vjContentNew', e => runKatex($(e.target).find('.typo')));
13 | });
14 | });
15 |
16 | export default katexPage;
17 |
--------------------------------------------------------------------------------
/vj4/ui/components/react/DomComponent.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export default class DomComponent extends React.PureComponent {
4 | componentDidMount() {
5 | this.refs.dom.appendChild(this.props.childDom);
6 | }
7 | componentWillUnmount() {
8 | $(this.refs.dom).empty();
9 | }
10 | render() {
11 | const {
12 | childDom,
13 | ...rest
14 | } = this.props;
15 | return (
16 |
17 | );
18 | }
19 | }
20 |
21 | DomComponent.propTypes = {
22 | childDom: React.PropTypes.instanceOf(HTMLElement).isRequired,
23 | };
24 |
--------------------------------------------------------------------------------
/vj4/ui/templates/partials/discussion_edit_form.html:
--------------------------------------------------------------------------------
1 | {% import "components/form.html" as form with context %}
2 | {% if ddoc['highlight'] or handler.has_perm(vj4.model.builtin.PERM_HIGHLIGHT_DISCUSSION) %}
3 | {{ form.form_checkbox(label='Highlight', name='highlight', value=ddoc['highlight']|default(false)) }}
4 | {% endif %}
5 | {{ form.form_text(label='Title', name='title', value=ddoc['title']|default(''), autofocus=true, required=true) }}
6 | {{ form.form_textarea(columns=12, label='Content', name='content', value=ddoc['content']|default(''), hotkeys='ctrl+enter:submit', markdown=true, required=true) }}
7 |
--------------------------------------------------------------------------------
/vj4/ui/components/contest/contest_sidebar.page.styl:
--------------------------------------------------------------------------------
1 | .contest-sidebar__bg
2 | display: block
3 | background: #67AABB url(problem-contest-bg.png) right top no-repeat
4 | color: #FFF !important
5 | font-size: rem($font-size)
6 | padding: rem(20px 0)
7 |
8 | +retina()
9 | background-image: url(problem-contest-bg@2x.png)
10 | background-size: 131px 150px
11 |
12 | h1
13 | font-size: rem($font-size-title)
14 | color: #FFF
15 | text-shadow: 0 1px 3px rgba(#000, 0.5)
16 |
17 | &:hover
18 | text-decoration: none
19 |
20 | .contest-sidebar__status
21 | margin-top: rem(20px)
22 |
--------------------------------------------------------------------------------
/vj4/ui/misc/immersive.styl:
--------------------------------------------------------------------------------
1 | .layout--immersive
2 | #panel
3 | background: #80b2d5 url('immersive-background.jpg') no-repeat
4 | background-size: 1920px 1080px
5 | background-size: cover
6 | +retina()
7 | background-image: url('immersive-background@2x.jpg')
8 |
9 | .immersive--content
10 | color: $immersive-text-color
11 |
12 | h1, h2, h3, h4, h5, h6
13 | color: $immersive-header-color
14 |
15 | +above(rupture.mobile-cutoff)
16 | .immersive--center
17 | width: 200px
18 | margin: 0 auto
19 |
20 | .immersive--center
21 | h1
22 | text-align: center
23 | margin: 2em 0
24 |
--------------------------------------------------------------------------------
/vj4/ui/components/highlighter/codemirror.page.styl:
--------------------------------------------------------------------------------
1 | @css {
2 | span.cm-comment { color: #008000; }
3 | span.cm-keyword, span.cm-variable-3 { line-height: 1em; color: #00f; }
4 | span.cm-string { color: #a31515; }
5 | span.cm-builtin { line-height: 1em; font-weight: bold; color: #077; }
6 | span.cm-special { line-height: 1em; font-weight: bold; color: #0aa; }
7 | span.cm-variable { color: black; }
8 | span.cm-meta { color: #2b91af; }
9 | span.cm-link { color: #3a3; }
10 | .CodeMirror-activeline-background { background: #e8f2ff; }
11 | .CodeMirror-matchingbracket { outline:1px solid grey; color:black !important; }
12 | }
13 |
--------------------------------------------------------------------------------
/vj4/ui/pages/contest_scoreboard.page.styl:
--------------------------------------------------------------------------------
1 | .page--contest_scoreboard
2 | .col--rank
3 | width: rem(70px)
4 | border-right: 1px solid $table-border-color
5 |
6 | .col--user
7 | width: rem(140px)
8 |
9 | .col--uid
10 | width: rem(140px)
11 |
12 | .col--total_score, .col--total_time_str, .col--solved_problems
13 | width: rem(100px)
14 | text-align: center
15 |
16 | .col--problem_detail
17 | border-left: 1px solid $table-border-color
18 | text-align: center
19 |
20 | +mobile()
21 | .col--user
22 | width: auto
23 |
24 | .col--problem_detail
25 | display: none
26 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/settings.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/utils/tpl.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 |
3 | export default function tpl(pieces, ...substitutions) {
4 | let result = pieces[0];
5 | for (let i = 0; i < substitutions.length; ++i) {
6 | const subst = substitutions[i];
7 | let substHtml;
8 | if (typeof subst === 'object' && subst.templateRaw) {
9 | substHtml = subst.html;
10 | } else {
11 | substHtml = _.escape(String(subst));
12 | }
13 | result += substHtml + pieces[i + 1];
14 | }
15 | return result;
16 | }
17 |
18 | export function rawHtml(html) {
19 | return {
20 | templateRaw: true,
21 | html,
22 | };
23 | }
24 |
--------------------------------------------------------------------------------
/vj4/util/version.py:
--------------------------------------------------------------------------------
1 | import functools
2 | import git
3 | import logging
4 | from os import path
5 |
6 | import vj4
7 | from vj4.util import argmethod
8 |
9 | _logger = logging.getLogger(__name__)
10 |
11 |
12 | @functools.lru_cache()
13 | @argmethod.wrap
14 | def get():
15 | try:
16 | return git.Repo(path.dirname(path.dirname(vj4.__file__))).git.describe(always=True, tags=True)
17 | except (git.InvalidGitRepositoryError, git.GitCommandError) as e:
18 | _logger.error('Failed to get repository: %s', repr(e))
19 | return 'unknown'
20 |
21 |
22 | if __name__ == '__main__':
23 | argmethod.invoke_by_args()
24 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/wechat.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/pages/contest_system_test.page.js:
--------------------------------------------------------------------------------
1 | import { NamedPage } from 'vj/misc/PageLoader';
2 | import { LANG_MOSS_WILDCARDS } from 'vj/constant/language';
3 |
4 | const page = new NamedPage('contest_system_test', async () => {
5 | const $language = $('[name="language"]');
6 | const $wildcards = $('[name="wildcards"]');
7 |
8 | const changeWildcards = () => {
9 | const lang = $language.val();
10 | const wildcards = LANG_MOSS_WILDCARDS[lang] || [];
11 | $wildcards.val(wildcards.join(', '));
12 | };
13 |
14 | $language.on('change', changeWildcards);
15 | changeWildcards();
16 | });
17 |
18 | export default page;
19 |
--------------------------------------------------------------------------------
/vj4/ui/templates/problem_statistics.html:
--------------------------------------------------------------------------------
1 | {% extends "layout/basic.html" %}
2 | {% block content %}
3 |
4 |
5 |
6 |
9 |
10 | {{ _('Swx Template') }}
11 |
12 |
13 |
14 |
15 | {% with owner_udoc=udoc %}
16 | {% include "partials/problem_sidebar.html" %}
17 | {% endwith %}
18 |
19 |
20 | {% endblock %}
21 |
--------------------------------------------------------------------------------
/vj4/service/staticmanifest.py:
--------------------------------------------------------------------------------
1 | from os import path
2 | from vj4.util import json
3 |
4 |
5 | MANIFEST_FILE = 'static-manifest.json'
6 | _manifest_dir = None
7 | _manifest_path = None
8 | _manifest = {}
9 |
10 |
11 | def init(static_dir):
12 | global _manifest_dir, _manifest_path, _manifest
13 | _manifest_dir = static_dir
14 | _manifest_path = path.join(_manifest_dir, MANIFEST_FILE)
15 | try:
16 | with open(_manifest_path, 'r') as manifest_file:
17 | data = json.decode(manifest_file.read())
18 | _manifest = data
19 | except Exception:
20 | pass
21 |
22 |
23 | def get(name):
24 | return _manifest.get(name, name)
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Editor temporaries.
2 | *~
3 | *.swp
4 | *.sublime-project
5 | *.sublime-workspace
6 | .idea/
7 | .vscode/
8 |
9 | # Python cache.
10 | __pycache__/
11 | *.pyc
12 | *.egg-info/
13 |
14 | # UI stuffs.
15 | node_modules/
16 | npm-debug.log
17 | .uibuild/
18 | .cache/
19 |
20 | # Vagrant dirts.
21 | Vagrantfile
22 | .vagrant/
23 |
24 | # MaxMind DB.
25 | *.mmdb
26 |
27 | # Generated fonts
28 | vj4/ui/misc/.iconfont
29 |
30 | # Generated constants
31 | vj4/constant
32 |
33 | # Generated locales
34 | vj4/ui/static/locale
35 |
36 | # Webpack Stats
37 | .webpackStats.json
38 |
39 | !.gitkeep
40 | config.yaml
41 |
42 | venv
43 | .env
44 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/help.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/templates/problem_submit_tr.html:
--------------------------------------------------------------------------------
1 | {% import "components/record.html" as record with context %}
2 |
3 | {{ record.render_status_td(rdoc) }}
4 | |
5 | {% if (rdoc['judge_category']) %}
6 | {{ ', '.join(rdoc['judge_category']) }}
7 | {% else %}
8 | pretest
9 | {% endif %}
10 | |
11 |
12 | {{ rdoc['memory_kb'] |format_size(1024) }}
13 | |
14 |
15 | {{ rdoc['time_ms'] }}ms
16 | |
17 | {{ datetime_span(rdoc['submit_time'] or rdoc['_id'].generation_time) }} |
18 |
19 |
--------------------------------------------------------------------------------
/examples/nginx-site.conf:
--------------------------------------------------------------------------------
1 | upstream app_server {
2 | server unix:/home/vj4/vj4.sock fail_timeout=0;
3 | }
4 |
5 | map $http_upgrade $connection_upgrade {
6 | default upgrade;
7 | '' close;
8 | }
9 |
10 | server {
11 | listen 8080;
12 | server_name localhost;
13 | root /home/vj4/vj4/vj4/.uibuild;
14 |
15 | location / {
16 | try_files $uri @proxy_to_app;
17 | }
18 |
19 | location @proxy_to_app {
20 | proxy_pass http://app_server;
21 | proxy_http_version 1.1;
22 | proxy_set_header Upgrade $http_upgrade;
23 | proxy_set_header Connection $connection_upgrade;
24 | proxy_set_header X-Real-IP $remote_addr;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/vj4/ui/components/loader/loader.page.styl:
--------------------------------------------------------------------------------
1 | .loader-container
2 | position: absolute
3 | left: 0
4 | width: 100%
5 | top: 0
6 | height: 100%
7 |
8 | .loader
9 | text-indent: -9999em
10 | border: rem(5px) solid rgba($primary-color, .2)
11 | border-left: rem(5px) solid $primary-color
12 | animation: load8 1.1s infinite linear
13 | border-radius: 50%
14 | width: rem(50px)
15 | height: rem(50px)
16 | margin-left: rem(-25px)
17 | margin-top: rem(-25px)
18 | left: 50%
19 | top: 50%
20 | position: absolute
21 |
22 | @keyframes load8
23 | 0%
24 | transform: rotate(0deg)
25 |
26 | 100%
27 | transform: rotate(360deg)
28 |
29 |
30 |
--------------------------------------------------------------------------------
/vj4/ui/components/profile/profile.page.styl:
--------------------------------------------------------------------------------
1 | .user-profile-avatar
2 | border-radius: 50%
3 |
4 | for n in (1..21)
5 | .user-profile-bg--{n}
6 | background-image: url('backgrounds/' + n + '.jpg')
7 |
8 | .user-profile-bg--thumbnail-{n}
9 | background-image: url('backgrounds/thumbnail/' + n + '.jpg')
10 |
11 | .user-profile-badge
12 | display: inline-block
13 | font-size: rem(12px)
14 | padding: rem(3px 4px)
15 | line-height: 1
16 |
17 | &:hover
18 | text-decoration: none
19 |
20 | for key, _ in $badge-bg-color
21 | .badge--{key}
22 | background-color: $badge-bg-color[key] !important
23 | color: $badge-text-color[key] !important
24 |
--------------------------------------------------------------------------------
/vj4/ui/templates/layout/immersive.html:
--------------------------------------------------------------------------------
1 | {% set layout_name = "immersive" %}
2 | {% set no_path_section = true %}
3 | {% extends "layout/html5.html" %}
4 | {% block body %}
5 |
6 | {% include "partials/nav.html" %}
7 |
8 |
9 |
10 | {% include "partials/header_mobile.html" %}
11 |
12 |
13 | {% block content %}{% endblock %}
14 |
15 |
16 | {% with show_topics = false %}
17 | {% include "partials/footer.html" %}
18 | {% endwith %}
19 |
20 |
21 |
22 | {% include "partials/login_dialog.html" %}
23 |
24 | {% endblock %}
25 |
--------------------------------------------------------------------------------
/vj4/ui/components/highlighter/meta.js:
--------------------------------------------------------------------------------
1 | // The following content is extracted from:
2 | // https://github.com/codemirror/CodeMirror/blob/master/mode/meta.js
3 |
4 | export default [
5 | { name: 'C', ext: ['c', 'h'] },
6 | { name: 'C++', ext: ['cpp', 'c++', 'cc', 'cxx', 'hpp', 'h++', 'hh', 'hxx'] },
7 | { name: 'Go', ext: ['go'] },
8 | { name: 'Haskell', ext: ['hs'] },
9 | { name: 'Java', ext: ['java'] },
10 | { name: 'JavaScript', ext: ['js'] },
11 | { name: 'Pascal', ext: ['p', 'pas'] },
12 | { name: 'PHP', ext: ['php', 'php3', 'php4', 'php5', 'php7', 'phtml'] },
13 | { name: 'Python', ext: ['BUILD', 'bzl', 'py', 'pyw'] },
14 | { name: 'Rust', ext: ['rs'] },
15 | ];
16 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/web.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/components/emoji/emojify.page.js:
--------------------------------------------------------------------------------
1 | import emojify from 'emojify.js';
2 |
3 | import { AutoloadPage } from 'vj/misc/PageLoader';
4 |
5 | function runEmojify($container) {
6 | if ($container.is('[data-emoji-enabled]')) {
7 | emojify.run($container[0]);
8 | return;
9 | }
10 | $container.find('[data-emoji-enabled]').get().forEach(element => emojify.run(element));
11 | }
12 |
13 | const emojifyPage = new AutoloadPage('emojifyPage', () => {
14 | emojify.setConfig({
15 | img_dir: `${UiContext.cdn_prefix}img/emoji`,
16 | });
17 | runEmojify($('body'));
18 | $(document).on('vjContentNew', e => runEmojify($(e.target)));
19 | });
20 |
21 | export default emojifyPage;
22 |
--------------------------------------------------------------------------------
/vj4/util/tools.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import pkgutil
3 | from os import path
4 |
5 | from vj4.util import argmethod
6 |
7 | _logger = logging.getLogger(__name__)
8 |
9 |
10 | @argmethod.wrap
11 | async def ensure_all_indexes():
12 | model_path = path.join(path.dirname(path.dirname(__file__)), 'model')
13 | for module_finder, name, ispkg in pkgutil.iter_modules([model_path]):
14 | if not ispkg:
15 | module = module_finder.find_module(name).load_module()
16 | if 'ensure_indexes' in dir(module):
17 | _logger.info('Ensuring indexes for "%s".' % name)
18 | await module.ensure_indexes()
19 |
20 |
21 | if __name__ == '__main__':
22 | argmethod.invoke_by_args()
23 |
--------------------------------------------------------------------------------
/vj4/ui/components/form/textbox.page.js:
--------------------------------------------------------------------------------
1 | import { AutoloadPage } from 'vj/misc/PageLoader';
2 |
3 | const textboxPage = new AutoloadPage('textboxPage', () => {
4 | $(document).on('focusin', '.textbox.material input', (ev) => {
5 | $(ev.currentTarget).parent().addClass('focus');
6 | });
7 |
8 | $(document).on('focusout', '.textbox.material input', (ev) => {
9 | $(ev.currentTarget).parent().removeClass('focus');
10 | });
11 |
12 | const $focusElement = $(document.activeElement);
13 | if ($focusElement.prop('tagName') === 'INPUT' &&
14 | $focusElement.parent().is('.textbox.material')
15 | ) {
16 | $focusElement.focusin();
17 | }
18 | });
19 |
20 | export default textboxPage;
21 |
--------------------------------------------------------------------------------
/vj4/ui/constant/model.js:
--------------------------------------------------------------------------------
1 | import attachObjectMeta from './util/objectMeta';
2 |
3 | export const USER_GENDER_MALE = 0;
4 | export const USER_GENDER_FEMALE = 1;
5 | export const USER_GENDER_OTHER = 2;
6 | export const USER_GENDERS = [USER_GENDER_MALE, USER_GENDER_FEMALE, USER_GENDER_OTHER];
7 | export const USER_GENDER_RANGE = {
8 | [USER_GENDER_MALE]: 'Boy ♂',
9 | [USER_GENDER_FEMALE]: 'Girl ♀',
10 | [USER_GENDER_OTHER]: 'Other',
11 | };
12 | attachObjectMeta(USER_GENDER_RANGE, 'intKey', true);
13 | export const USER_GENDER_ICONS = {
14 | [USER_GENDER_MALE]: '♂',
15 | [USER_GENDER_FEMALE]: '♀',
16 | [USER_GENDER_OTHER]: '?',
17 | };
18 | attachObjectMeta(USER_GENDER_ICONS, 'intKey', true);
19 |
--------------------------------------------------------------------------------
/vj4/ui/pages/problem_settings.page.js:
--------------------------------------------------------------------------------
1 | import { NamedPage } from 'vj/misc/PageLoader';
2 |
3 | async function handleCategoryClick(ev) {
4 | const $target = $(ev.currentTarget);
5 | const $txt = $('[name="category"]');
6 | console.log($target);
7 | console.log($txt);
8 | $txt.val(`${$txt.val()}, ${$target.data('category')}`);
9 | }
10 |
11 | const page = new NamedPage('problem_settings', async () => {
12 | $(document).on('click', '.category-a', handleCategoryClick);
13 | $(document).on('click', '[name="problem-sidebar__show-category"]', (ev) => {
14 | $(ev.currentTarget).hide();
15 | $('[name="problem-sidebar__categories"]').show();
16 | });
17 | });
18 |
19 | export default page;
20 |
--------------------------------------------------------------------------------
/vj4/ui/templates/partials/path.html:
--------------------------------------------------------------------------------
1 |
2 | {% if not no_path_section %}
3 |
18 | {% endif %}
19 |
20 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/platform--android.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/templates/discussion_nodes_widget.html:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 | {% for category, nodes in discussion_nodes.items() %}
7 | -
8 |
{{ category }}
9 |
10 | {% for node in nodes %}
11 | - {{ node['name'] }}
12 | {% endfor %}
13 |
14 |
15 | {% endfor %}
16 |
17 |
18 |
--------------------------------------------------------------------------------
/vj4/ui/templates/error.html:
--------------------------------------------------------------------------------
1 | {% set no_path_section = true %}
2 | {% extends "layout/basic.html" %}
3 | {% block content %}
4 |
5 |
8 |
9 |
{{ _('Oops!') }}
10 |
{{ _(error.message).format(*error.args) }}
11 |
{{ _('Technical Information') }}:
12 |
{{ _('Type') }}: {{ error.__class__.__name__ }}
13 |
{{ _('Arguments') }}:
14 |
15 | {% for arg in error.args %}
16 | - {{ arg }}
17 | {% endfor %}
18 |
19 |
20 |
21 |
22 | {% endblock %}
23 |
--------------------------------------------------------------------------------
/vj4/ui/components/record/record.page.styl:
--------------------------------------------------------------------------------
1 | .record-status--text
2 | for key, value in $record-status-color
3 | &.{key}
4 | color: value !important
5 |
6 | .record-status--icon
7 | display: inline-block
8 | width: 1.15em
9 | for key, value in $record-status-icon
10 | &.{key}:before
11 | content: value
12 | color: $record-status-color[key]
13 |
14 | .record-status--border
15 | border-left: rem(3px) solid transparent
16 | for key, value in $record-status-color
17 | &.{key}
18 | border-left: rem(3px) solid lighten(value, 10%)
19 |
20 | .record-status--background
21 | color: #FFF
22 |
23 | for key, value in $record-status-color
24 | &.{key}
25 | background: value
26 |
27 |
--------------------------------------------------------------------------------
/vj4/ui/pages/home_security.page.styl:
--------------------------------------------------------------------------------
1 | .page--home_security
2 |
3 | .sessionlist
4 | font-size: rem($font-size-secondary)
5 |
6 | .sessionlist__icon
7 | font-size: rem(30px)
8 | color: #888
9 | margin: rem(0 20px)
10 |
11 | .sessionlist__item
12 | margin: rem(10px 0)
13 | padding: rem(10px 0)
14 | border-bottom: 1px solid #DDD
15 |
16 | input
17 | margin: 0
18 |
19 | .sessionlist__current-session
20 | line-height: rem($form-control-height)
21 | color: green
22 |
23 | +mobile()
24 | .sessionlist__item
25 | .media__left
26 | display: none
27 |
28 | .media__body, .media__right
29 | display: block
30 | padding: 0
31 | margin: 0
32 |
--------------------------------------------------------------------------------
/vj4/handler/misc.py:
--------------------------------------------------------------------------------
1 | from vj4 import app
2 | from vj4.handler import base
3 | from vj4.util import misc
4 |
5 |
6 | @app.route('/about', 'about', global_route=True)
7 | class AboutHandler(base.Handler):
8 | async def get(self):
9 | self.render('about.html')
10 |
11 |
12 | @app.route('/wiki/help', 'wiki_help', global_route=True)
13 | class AboutHandler(base.Handler):
14 | async def get(self):
15 | self.render('wiki_help.html')
16 |
17 |
18 | @app.route('/preview', 'preview', global_route=True)
19 | class PreviewHandler(base.Handler):
20 | @base.post_argument
21 | @base.sanitize
22 | async def post(self, *, text: str):
23 | self.response.content_type = 'text/html'
24 | self.response.text = misc.markdown(text)
25 |
--------------------------------------------------------------------------------
/vj4/ui/components/training/training.page.styl:
--------------------------------------------------------------------------------
1 | .training-status--icon
2 | display: inline-block
3 | width: 1.15em
4 | for key, value in $training-status-icon
5 | &.{key}:before
6 | content: value
7 | color: $training-status-color[key]
8 |
9 | .training-status--text
10 | for key, value in $training-status-color
11 | &.{key}
12 | color: value
13 |
14 | .training-section-status--icon
15 | display: inline-block
16 | width: 1.15em
17 | for key, value in $training-section-status-icon
18 | &.{key}:before
19 | content: value
20 | color: $training-status-color[key]
21 |
22 | .training-section-status--text
23 | for key, value in $training-section-status-color
24 | &.{key}
25 | color: value
26 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/platform--mac.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/components/cmeditor/cmeditor.page.js:
--------------------------------------------------------------------------------
1 | import { AutoloadPage } from 'vj/misc/PageLoader';
2 | import delay from 'vj/utils/delay';
3 | import CmEditor from '.';
4 |
5 | import 'vj-simplemde/src/css/simplemde.css';
6 | import './cmeditor.styl';
7 |
8 | function runSubstitute($container) {
9 | const selector = ['textarea[data-markdown]'];
10 | $container.find(selector.join(', ')).get().forEach((element) => {
11 | CmEditor.getOrConstruct($(element));
12 | });
13 | }
14 |
15 | const cmEditorPage = new AutoloadPage('cmEditorPage', () => {
16 | runSubstitute($('body'));
17 | $(document).on('vjContentNew', async (e) => {
18 | await delay(0);
19 | runSubstitute($(e.target));
20 | });
21 | });
22 |
23 | export default cmEditorPage;
24 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3"
2 | services:
3 | cb4:
4 | restart: always
5 | image: jioj/cb4
6 | container_name: cb4
7 | depends_on:
8 | - mongo
9 | - rabbitmq
10 | ports:
11 | - "34765:34765"
12 | networks:
13 | - default
14 | environment:
15 | DB_HOST: mongo
16 | MQ_HOST: rabbitmq
17 | HOST: cb4
18 | env_file: .env
19 | mongo:
20 | restart: always
21 | image: mvertes/alpine-mongo
22 | container_name: mongo
23 | volumes:
24 | - ~/mongo_data:/data/db
25 | networks:
26 | - default
27 | rabbitmq:
28 | restart: always
29 | image: rabbitmq:alpine
30 | container_name: rabbitmq
31 | networks:
32 | - default
33 | networks:
34 | default:
35 |
--------------------------------------------------------------------------------
/vj4/service/queue.py:
--------------------------------------------------------------------------------
1 | import bson
2 |
3 | from vj4 import mq
4 | from vj4.util import options
5 |
6 | options.define('queue_prefetch', default=1, help='Queue prefetch count.')
7 |
8 |
9 | async def publish(key, **kwargs):
10 | channel = await mq.channel('queue')
11 | await channel.queue_declare(key)
12 | await channel.basic_publish(bson.BSON.encode(kwargs), '', key)
13 |
14 |
15 | async def consume(key, on_message):
16 | channel = await mq.channel()
17 | await channel.queue_declare(key)
18 | await channel.basic_qos(prefetch_count=options.queue_prefetch)
19 | await channel.basic_consume((lambda channel, body, envelope, properties:
20 | on_message(envelope.delivery_tag, **bson.BSON.decode(body))), key)
21 | return channel
22 |
--------------------------------------------------------------------------------
/vj4/ui/templates/partials/header.html:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/vj4/ui/templates/domain_manage_edit.html:
--------------------------------------------------------------------------------
1 | {% extends "domain_base.html" %}
2 | {% block domain_content %}
3 |
21 | {% endblock %}
22 |
--------------------------------------------------------------------------------
/vj4/ui/templates/home_messages.html:
--------------------------------------------------------------------------------
1 | {% extends "layout/home_base.html" %}
2 | {% block home_content %}
3 |
4 |
5 |
6 |
{{ _('Select User') }}
7 |
8 |
9 |
10 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | {{ noscript_note.render() }}
21 |
22 |
23 |
24 | {% endblock %}
25 |
--------------------------------------------------------------------------------
/vj4/ui/templates/domain_manage_discussion.html:
--------------------------------------------------------------------------------
1 | {% extends "domain_base.html" %}
2 | {% block domain_content %}
3 |
21 | {% include 'discussion_nodes_widget.html' %}
22 | {% endblock %}
23 |
--------------------------------------------------------------------------------
/vj4/ui/templates/components/home.html:
--------------------------------------------------------------------------------
1 | {% import "components/sidemenu.html" as sidemenu with context %}
2 | {% macro render_sidebar() %}
3 |
4 |
15 |
16 | {% endmacro %}
17 |
--------------------------------------------------------------------------------
/vj4/ui/pages/record_detail.page.js:
--------------------------------------------------------------------------------
1 | import { NamedPage } from 'vj/misc/PageLoader';
2 |
3 | const page = new NamedPage('record_detail', async () => {
4 | const SockJs = await System.import('sockjs-client');
5 | const DiffDOM = await System.import('diff-dom');
6 |
7 | const sock = new SockJs(Context.socketUrl);
8 | const dd = new DiffDOM();
9 |
10 | sock.onmessage = (message) => {
11 | const msg = JSON.parse(message.data);
12 | const newStatus = $(msg.status_html);
13 | const oldStatus = $('#status');
14 | dd.apply(oldStatus[0], dd.diff(oldStatus[0], newStatus[0]));
15 | const newSummary = $(msg.summary_html);
16 | const oldSummary = $('#summary');
17 | dd.apply(oldSummary[0], dd.diff(oldSummary[0], newSummary[0]));
18 | };
19 | });
20 |
21 | export default page;
22 |
--------------------------------------------------------------------------------
/vj4/ui/templates/partials/domain_edit_form.html:
--------------------------------------------------------------------------------
1 | {% import "components/form.html" as form with context %}
2 | {% if page_name == 'domain_manage_edit' %}
3 | {{ form.form_text(label='ID', name='id', value=ddoc['_id'], disabled=true) }}
4 | {% elif page_name == 'home_domain_create' %}
5 | {{ form.form_text(label='ID', name='id', autofocus=true, required=true) }}
6 | {% endif %}
7 | {{ form.form_text(label='Name', name='name', value=ddoc['name']|default(''), autofocus=(page_name == 'domain_manage_edit'), required=true) }}
8 | {{ form.form_text(label='Gravatar Email', help_text='Will be used as the domain icon.', name='gravatar', value=ddoc['gravatar']|default('')) }}
9 | {{ form.form_textarea(columns=12, label='Bulletin', name='bulletin', value=ddoc['bulletin']|default(''), markdown=true, required=false) }}
10 |
--------------------------------------------------------------------------------
/vj4/ui/templates/user_lostpass.html:
--------------------------------------------------------------------------------
1 | {% extends "layout/immersive.html" %}
2 | {% block content %}
3 |
4 |
5 |
{{ _('Lost Password') }}
6 |
19 |
20 |
21 | {% endblock %}
22 |
--------------------------------------------------------------------------------
/vj4/ui/templates/user_register.html:
--------------------------------------------------------------------------------
1 | {% extends "layout/immersive.html" %}
2 | {% block content %}
3 |
4 |
5 |
{{ _('Sign Up') }}
6 |
19 |
20 |
21 | {% endblock %}
22 |
--------------------------------------------------------------------------------
/vj4/util/domainjob.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from vj4.model import builtin
4 | from vj4.model import domain
5 | from vj4.util import argmethod
6 |
7 |
8 | _logger = logging.getLogger(__name__)
9 |
10 |
11 | def wrap(method):
12 | async def run():
13 | _logger.info('Built in domains')
14 | for ddoc in builtin.DOMAINS:
15 | _logger.info('Domain: {0}'.format(ddoc['_id']))
16 | await method(ddoc['_id'])
17 | _logger.info('User domains')
18 | ddocs = domain.get_multi(fields={'_id': 1})
19 | async for ddoc in ddocs:
20 | _logger.info('Domain: {0}'.format(ddoc['_id']))
21 | await method(ddoc['_id'])
22 |
23 | if method.__module__ == '__main__':
24 | argmethod._methods[method.__name__] = method
25 | argmethod._methods[method.__name__ + '_all'] = run
26 | return method
27 |
--------------------------------------------------------------------------------
/vj4/ui/components/messagepad/DialogueListItem.page.styl:
--------------------------------------------------------------------------------
1 | $stripe-width = 2px
2 |
3 | .messagepad__list-item
4 | display: block
5 | padding: rem(20px $section-gap-h 20px ($section-gap-h - $stripe-width))
6 | border-left: $stripe-width solid transparent
7 | cursor: pointer
8 | transition: border-color .1s linear, background-color .1s linear
9 | overflow: hidden
10 | text-overflow: ellipsis
11 |
12 | &:hover
13 | border-color: #DDD
14 | background: #F4F4F4
15 | text-decoration: none
16 |
17 | &.active
18 | border-color: $secondary-color
19 | background: rgba($secondary-color, 0.07)
20 |
21 | .messagepad__desc
22 | font-size: rem($font-size-small)
23 | color: #888
24 | max-height: 2em
25 | margin-top: rem(5px)
26 | overflow: hidden
27 | text-overflow: ellipsis
28 | word-wrap()
29 |
--------------------------------------------------------------------------------
/vj4/ui/components/react-splitpane/splitpane.page.styl:
--------------------------------------------------------------------------------
1 | $resizer-extend = 4px
2 | $resizer-extend-color = rgba($primary-color, 0.3)
3 |
4 | .Resizer
5 | background: #000
6 | opacity: .2
7 | z-index: 1
8 | box-sizing: border-box
9 | background-clip: padding-box
10 |
11 | .Resizer.horizontal
12 | height: ($resizer-extend * 2 + 1px)
13 | margin: -($resizer-extend) 0
14 | border-top: $resizer-extend solid transparent
15 | border-bottom: $resizer-extend solid transparent
16 | cursor: row-resize
17 | width: 100%
18 |
19 | .Resizer.vertical
20 | width: ($resizer-extend * 2 + 1px)
21 | margin: 0 -($resizer-extend)
22 | border-left: $resizer-extend solid transparent
23 | border-right: $resizer-extend solid transparent
24 | cursor: col-resize
25 |
26 | .Resizer:hover
27 | border-color: $resizer-extend-color
28 |
--------------------------------------------------------------------------------
/vj4/ui/components/scratchpad/PanelComponent.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import classNames from 'classnames';
3 | import SplitPaneFillOverlay from 'vj/components/react-splitpane/SplitPaneFillOverlayComponent';
4 |
5 | export default function PanelComponent(props) {
6 | const {
7 | title,
8 | className,
9 | children,
10 | ...rest
11 | } = props;
12 | const cn = classNames(className, 'flex-col');
13 | return (
14 |
15 | {title}
16 | {children}
17 |
18 | );
19 | }
20 |
21 | PanelComponent.propTypes = {
22 | title: React.PropTypes.node,
23 | className: React.PropTypes.string,
24 | children: React.PropTypes.node,
25 | };
26 |
--------------------------------------------------------------------------------
/vj4/ui/templates/domain_manage_dashboard.html:
--------------------------------------------------------------------------------
1 | {% import "components/user.html" as user %}
2 | {% extends "domain_base.html" %}
3 | {% block domain_content %}
4 | {% if handler.domain['bulletin'] %}
5 |
6 |
7 | {{ handler.domain['bulletin']|markdown }}
8 |
9 |
10 | {% endif %}
11 |
12 |
15 |
16 |
17 | - {{ _('Gravatar') }}

18 | - {{ _('Name') }}
- {{ handler.domain['name'] }}
19 |
20 |
21 |
22 | {% endblock %}
23 |
--------------------------------------------------------------------------------
/vj4/ui/templates/record_detail_summary.html:
--------------------------------------------------------------------------------
1 |
2 | {% if rdoc['score'] is defined %}
3 | - {{ _('Score') }}
4 | - {{ rdoc['score'] }}
5 | {% endif %}
6 | - {{ _('Total Time') }}
7 | - {% if rdoc['status'] == vj4.constant.record.STATUS_TIME_LIMIT_EXCEEDED or rdoc['status'] == vj4.constant.record.STATUS_MEMORY_LIMIT_EXCEEDED or rdoc['status'] == vj4.constant.record.STATUS_OUTPUT_LIMIT_EXCEEDED %}≥{% endif %}{{ rdoc['time_ms'] }}ms
8 | - {{ _('Peak Memory') }}
9 | - {% if rdoc['status'] == vj4.constant.record.STATUS_TIME_LIMIT_EXCEEDED or rdoc['status'] == vj4.constant.record.STATUS_MEMORY_LIMIT_EXCEEDED or rdoc['status'] == vj4.constant.record.STATUS_OUTPUT_LIMIT_EXCEEDED %}≥{% endif %}{{ rdoc['memory_kb']|format_size(1024) }}
10 |
11 |
--------------------------------------------------------------------------------
/vj4/ui/components/contest/contest.page.styl:
--------------------------------------------------------------------------------
1 | .contest-type-tag
2 | .contest-tag
3 | font-size: rem($font-size-small)
4 | display: inline-block
5 | line-height: rem(22px)
6 | padding: rem(0 10px)
7 | vertical-align: middle
8 | border-radius: rem(12px)
9 |
10 | &:hover
11 | text-decoration: none
12 | color: #FFF !important
13 |
14 | .contest-type-tag
15 | &, &:visited
16 | &, .supplementary &
17 | color: #FFF
18 |
19 | .icon
20 | margin-right: rem(5px)
21 |
22 | for key, value in $contest-color
23 | .contest-type--{key} .contest-type-tag
24 | background: value
25 |
26 | &:hover
27 | background: lighten(value, 20%)
28 |
29 | .contest-tag
30 | border: 1px solid $supplementary-border-color
31 |
32 | &:hover
33 | background: $primary-color
34 | border-color: $primary-color
35 |
--------------------------------------------------------------------------------
/scripts/fix_abroad_shrinkwrap.js:
--------------------------------------------------------------------------------
1 | var fs = require('fs');
2 | var tree = JSON.parse(fs.readFileSync('./package-lock.json').toString());
3 |
4 | function fixTree(subtree) {
5 | if (subtree.dependencies) {
6 | for (var key in subtree.dependencies) {
7 | var resolved = subtree.dependencies[key].resolved;
8 | if (typeof resolved === 'string') {
9 | resolved = resolved.replace(
10 | /^https?:\/\/registry.npm.taobao.org\//,
11 | 'https://registry.npmjs.org/'
12 | );
13 | subtree.dependencies[key].resolved = resolved;
14 | }
15 | if (subtree.dependencies[key].dependencies !== undefined) {
16 | fixTree(subtree.dependencies[key]);
17 | }
18 | }
19 | }
20 | }
21 |
22 | fixTree(tree);
23 |
24 | fs.writeFileSync('./package-lock.json', JSON.stringify(tree, null, 2));
25 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/template/webicon.styl:
--------------------------------------------------------------------------------
1 | @font-face
2 | font-family: $icon-font-name
3 | src: url('./{{ options.fontName }}.eot')
4 | src: url('./{{ options.fontName }}.eot#iefix') format('embedded-opentype'),
5 | url('./{{ options.fontName }}.woff2') format('woff2'),
6 | url('./{{ options.fontName }}.woff') format('woff'),
7 | url('./{{ options.fontName }}.ttf') format('truetype')
8 | font-weight: normal
9 | font-style: normal
10 |
11 | .icon
12 | font-family: $icon-font-name !important
13 | speak: none
14 | font-style: normal
15 | font-weight: normal
16 | font-variant: normal
17 | text-transform: none
18 | line-height: 1
19 | -webkit-font-smoothing: antialiased
20 | -moz-osx-font-smoothing: grayscale
21 |
22 | {% for icon in glyphs %}
23 | .icon-{{ icon.name }}:before
24 | content: $icon-{{ icon.name }}
25 | {% endfor %}
26 |
--------------------------------------------------------------------------------
/vj4/ui/components/datepicker/datepicker.page.js:
--------------------------------------------------------------------------------
1 | import { AutoloadPage } from 'vj/misc/PageLoader';
2 |
3 | import 'pickadate/lib/themes/classic.css';
4 | import 'pickadate/lib/themes/classic.date.css';
5 | import 'pickadate/lib/themes/classic.time.css';
6 | import './datepicker.styl';
7 |
8 | const datepickerPage = new AutoloadPage('datepickerPage', async () => {
9 | if ($('[data-pick-date]').length > 0) {
10 | await System.import('pickadate/lib/picker.date');
11 | $('[data-pick-date]').pickadate({
12 | format: 'yyyy-m-d',
13 | clear: false,
14 | });
15 | }
16 | if ($('[data-pick-time]').length > 0) {
17 | await System.import('pickadate/lib/picker.time');
18 | $('[data-pick-time]').pickatime({
19 | format: 'H:i',
20 | interval: 15,
21 | clear: false,
22 | });
23 | }
24 | });
25 |
26 | export default datepickerPage;
27 |
--------------------------------------------------------------------------------
/vj4/ui/components/cmeditor/textareaHandler.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import DOMAttachedObject from 'vj/components/DOMAttachedObject';
3 | import CmEditor from '.';
4 |
5 | export default class TextareaHandler extends DOMAttachedObject {
6 | static DOMAttachKey = 'vjTextareaHandlerInstance';
7 |
8 | getCmEditor() {
9 | return CmEditor.get(this.$dom);
10 | }
11 |
12 | isCmEditor() {
13 | const editor = this.getCmEditor();
14 | return (editor !== undefined && editor.isValid());
15 | }
16 |
17 | val(...argv) {
18 | if (this.isCmEditor()) {
19 | return this.getCmEditor().value(...argv);
20 | }
21 | return this.$dom.val(...argv);
22 | }
23 |
24 | focus() {
25 | if (this.isCmEditor()) {
26 | this.getCmEditor().focus();
27 | }
28 | this.$dom.focus();
29 | }
30 | }
31 |
32 | _.assign(TextareaHandler, DOMAttachedObject);
33 |
--------------------------------------------------------------------------------
/vj4/ui/templates/components/record.html:
--------------------------------------------------------------------------------
1 | {% macro render_status_td(rdoc, rid_key='_id') %}
2 |
3 |
9 | {% if rdoc['status'] == vj4.constant.record.STATUS_JUDGING %}
10 |
13 | {% endif %}
14 | |
15 | {% endmacro %}
16 |
--------------------------------------------------------------------------------
/vj4/ui/pages/training_detail.page.styl:
--------------------------------------------------------------------------------
1 | .page--training_detail
2 | .col--status
3 | width: rem(150px)
4 | position: relative
5 |
6 | .col--submit_n, .col--ac_rate, .col--difficulty
7 | width: rem(70px)
8 | text-align: center
9 |
10 | .col--status
11 | border-right: 1px solid $table-border-color
12 |
13 | .col--submit_n, .col--difficulty
14 | border-left: 1px solid $table-border-color
15 |
16 | +mobile()
17 | .col--status,
18 | .col--submit_n,
19 | .col--difficulty
20 | display: none
21 |
22 | .col--ac_rate
23 | border-left: 1px solid $table-border-color
24 |
25 | .training__section
26 | &.invalid
27 | .training__problems
28 | opacity: 0.6
29 |
30 | &.collapsed .collapsed--hidden,
31 | &.expanded .expanded--hidden
32 | display: none
33 |
34 | &.collapsed .training__section__detail
35 | display: none
36 |
--------------------------------------------------------------------------------
/sources.list:
--------------------------------------------------------------------------------
1 | deb http://mirrors.163.com/ubuntu/ bionic main restricted universe multiverse
2 | deb http://mirrors.163.com/ubuntu/ bionic-security main restricted universe multiverse
3 | deb http://mirrors.163.com/ubuntu/ bionic-updates main restricted universe multiverse
4 | deb http://mirrors.163.com/ubuntu/ bionic-proposed main restricted universe multiverse
5 | deb http://mirrors.163.com/ubuntu/ bionic-backports main restricted universe multiverse
6 | deb-src http://mirrors.163.com/ubuntu/ bionic main restricted universe multiverse
7 | deb-src http://mirrors.163.com/ubuntu/ bionic-security main restricted universe multiverse
8 | deb-src http://mirrors.163.com/ubuntu/ bionic-updates main restricted universe multiverse
9 | deb-src http://mirrors.163.com/ubuntu/ bionic-proposed main restricted universe multiverse
10 | deb-src http://mirrors.163.com/ubuntu/ bionic-backports main restricted universe multiverse
11 |
12 |
--------------------------------------------------------------------------------
/vj4/ui/templates/partials/header_mobile.html:
--------------------------------------------------------------------------------
1 | {% if no_path_section %}
2 |
17 | {% endif %}
18 |
--------------------------------------------------------------------------------
/vj4/ui/pages/domain_manage_join_applications.page.js:
--------------------------------------------------------------------------------
1 | import { NamedPage } from 'vj/misc/PageLoader';
2 |
3 | import * as domainEnum from 'vj/constant/domain';
4 |
5 | const page = new NamedPage('domain_manage_join_applications', () => {
6 | const $role = $('[name="role"]');
7 | const $expire = $('[name="expire"]');
8 | const $code = $('[name="invitation_code"]');
9 | function updateFormState() {
10 | const method = parseInt($('[name="method"]:checked').val(), 10);
11 | $role.prop('disabled', method === domainEnum.JOIN_METHOD_NONE).trigger('vjFormDisableUpdate');
12 | $expire.prop('disabled', method === domainEnum.JOIN_METHOD_NONE).trigger('vjFormDisableUpdate');
13 | $code.prop('disabled', method !== domainEnum.JOIN_METHOD_CODE).trigger('vjFormDisableUpdate');
14 | }
15 | updateFormState();
16 | $('[name="method"]').change(() => updateFormState());
17 | });
18 |
19 | export default page;
20 |
--------------------------------------------------------------------------------
/vj4/ui/components/scratchpad/DataInputComponent.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import classNames from 'classnames';
3 |
4 | export default function DataInputComponent(props) {
5 | const {
6 | title,
7 | value,
8 | onChange,
9 | className,
10 | ...rest
11 | } = props;
12 | const cn = classNames(className, 'flex-col flex-fill');
13 | return (
14 |
15 |
26 | );
27 | }
28 |
29 | DataInputComponent.propTypes = {
30 | title: React.PropTypes.node,
31 | value: React.PropTypes.string,
32 | onChange: React.PropTypes.func,
33 | className: React.PropTypes.string,
34 | };
35 |
--------------------------------------------------------------------------------
/vj4/ui/pages/user_detail.page.js:
--------------------------------------------------------------------------------
1 | import base64 from 'base-64';
2 | import Clipboard from 'clipboard';
3 |
4 | import { NamedPage } from 'vj/misc/PageLoader';
5 | import substitute from 'vj/utils/substitute';
6 | import Notification from 'vj/components/notification';
7 | import i18n from 'vj/utils/i18n';
8 |
9 | const page = new NamedPage('user_detail', async () => {
10 | $('[name="profile_contact_copy"]').get().forEach((el) => {
11 | const data = $(el).attr('data-content');
12 | const decoded = base64.decode(data);
13 | const clip = new Clipboard(el, { text: () => decoded });
14 | clip.on('success', () => {
15 | Notification.success(substitute(i18n('"{data}" copied to clipboard!'), { data: decoded }), 2000);
16 | });
17 | clip.on('error', () => {
18 | Notification.error(substitute(i18n('Copy "{data}" failed :('), { data: decoded }));
19 | });
20 | });
21 | });
22 |
23 | export default page;
24 |
--------------------------------------------------------------------------------
/scripts/build/plugins/gulpGenerateLocales.js:
--------------------------------------------------------------------------------
1 | import yaml from 'js-yaml';
2 |
3 | import { PluginError } from 'gulp-util';
4 | import through from 'through2';
5 | import path from 'path';
6 |
7 | export default function generateLocales() {
8 |
9 | function bufferContents(file, encoding, callback) {
10 | if (file.isNull()) {
11 | callback();
12 | return;
13 | }
14 | if (file.isStream()) {
15 | this.emit('error', new PluginError('Stream not supported'));
16 | callback();
17 | return;
18 | }
19 |
20 | const doc = yaml.safeLoad(file.contents);
21 | file.contents = new Buffer(`window.LOCALES = ${JSON.stringify(doc, null, 2)}`);
22 | file.path = path.join(
23 | path.dirname(file.path),
24 | path.basename(file.path, path.extname(file.path)) + '.js'
25 | );
26 |
27 | this.push(file);
28 | callback();
29 | }
30 |
31 | return through.obj(bufferContents);
32 |
33 | };
34 |
--------------------------------------------------------------------------------
/scripts/build/plugins/gulpTouch.js:
--------------------------------------------------------------------------------
1 | import { PluginError } from 'gulp-util';
2 | import through from 'through2';
3 | import fs from 'fs-extra';
4 |
5 | export default function touch(mtime) {
6 |
7 | async function touchFile(file) {
8 | const fd = await fs.open(file.path, 'a');
9 | try {
10 | await fs.futimes(fd, mtime, mtime);
11 | } finally {
12 | await fs.close(fd);
13 | }
14 | }
15 |
16 | function processStream(file, encoding, callback) {
17 | if (file.isNull()) {
18 | callback();
19 | return;
20 | }
21 | if (file.isStream()) {
22 | this.emit('error', new PluginError('Stream not supported'));
23 | callback();
24 | return;
25 | }
26 | touchFile(file)
27 | .catch(err => this.emit('error', err))
28 | .then(() => {
29 | this.push(file);
30 | callback();
31 | });
32 | }
33 |
34 | return through.obj(processStream);
35 |
36 | };
37 |
--------------------------------------------------------------------------------
/vj4/ui/components/scratchpad/reducers/records.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 |
3 | export default function reducer(state = {
4 | rows: [],
5 | items: {},
6 | }, action) {
7 | switch (action.type) {
8 | case 'SCRATCHPAD_RECORDS_LOAD_SUBMISSIONS_FULFILLED': {
9 | const { rdocs } = action.payload;
10 | return {
11 | ...state,
12 | rows: _.map(rdocs, '_id'),
13 | items: _.keyBy(rdocs, '_id'),
14 | };
15 | }
16 | case 'SCRATCHPAD_RECORDS_PUSH': {
17 | const { rdoc } = action.payload;
18 | if (rdoc.uid !== UserContext.uid
19 | || rdoc.domain_id !== UserContext.domain
20 | || rdoc.pid !== Context.problemId
21 | ) {
22 | return state;
23 | }
24 | return {
25 | ...state,
26 | rows: [rdoc._id, ...state.rows],
27 | items: {
28 | ...state.items,
29 | [rdoc._id]: rdoc,
30 | },
31 | };
32 | }
33 | default:
34 | return state;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/vj4/ui/components/problem/tag.page.styl:
--------------------------------------------------------------------------------
1 | .problem__tags
2 | margin: 0 !important
3 | padding: 0 !important
4 | list-style: none !important
5 | font-size: 0
6 |
7 | .problem__tag
8 | display: inline-block
9 | vertical-align: top
10 | height: rem($table-lh)
11 | margin: rem(0 2px 2px 0) !important
12 | padding: 0 !important
13 |
14 | .problem__tag-link
15 | display: inline-block
16 | padding: rem(0 7px)
17 | overflow: hidden
18 | text-overflow: ellipsis
19 | white-space: nowrap
20 | max-width: rem(130px)
21 | line-height: rem($table-lh)
22 | height: rem($table-lh)
23 | background: #F0F0F0
24 | color: $text-1-color !important
25 | font-size: rem($font-size-small)
26 |
27 | &:hover
28 | background: $primary-color
29 | color: #FFF !important
30 | text-decoration: none
31 |
32 | .col--problem-name
33 | .problem__tags
34 | float: right
35 |
36 | .problem__tag
37 | margin: rem(0 2px 0 0) !important
38 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import setuptools
2 |
3 | setuptools.setup(name='vj4',
4 | version='4.0',
5 | author='Vijos',
6 | author_email='me@iceboy.org',
7 | description='Vijos Web Server',
8 | license='AGPL-3.0',
9 | keywords='vijos online judge web',
10 | url='https://vijos.org/',
11 | packages=[
12 | 'vj4',
13 | 'vj4.handler',
14 | 'vj4.model',
15 | 'vj4.model.adaptor',
16 | 'vj4.pipeline',
17 | 'vj4.service',
18 | 'vj4.util',
19 | ],
20 | package_data={
21 | 'vj4': ['locale/*.csv', 'ui/templates/*', '.uibuild/*'],
22 | },
23 | install_requires=open('requirements.txt').readlines(),
24 | test_suite='vj4.test')
25 |
--------------------------------------------------------------------------------
/vj4/ui/misc/slideout.styl:
--------------------------------------------------------------------------------
1 | html
2 | height: 100%
3 |
4 | body
5 | width: 100%
6 |
7 | #panel
8 | display: flex
9 | flex-direction: column
10 |
11 | body, #panel
12 | min-height: 100%
13 | min-height: 100vh
14 |
15 | .slideout-panel
16 | position: relative
17 | z-index: 1
18 |
19 | .slideout-overlay
20 | display: none
21 | position: absolute
22 | left: 0
23 | top: 0
24 | width: 100%
25 | height: 100%
26 | z-index: 99999
27 |
28 | +mobile()
29 | .nav.slideout-menu
30 | position: fixed
31 | top: 0
32 | bottom: 0
33 | right: 0
34 | left: auto
35 | z-index: 0
36 | width: 200px
37 | overflow-y: auto
38 | -webkit-overflow-scrolling: touch
39 | display: none
40 | height: auto
41 | min-height: 100vh
42 |
43 | .slideout-open,
44 | .slideout-open body,
45 | .slideout-open .slideout-panel
46 | overflow: hidden
47 |
48 | .slideout-open .slideout-menu
49 | display: block !important
50 |
--------------------------------------------------------------------------------
/vj4/ui/components/dialog/dialog.page.styl:
--------------------------------------------------------------------------------
1 | .dialog
2 | position: fixed
3 | left: 0
4 | top: 0
5 | width: 100%
6 | height: 100%
7 | opacity: 0
8 | z-index: 1000 // managed
9 | // display flex will be added after animation completes
10 | flex-direction: column
11 | align-items: center
12 | justify-content: center
13 | display: none
14 | background: $dialog-layer-bg-color
15 |
16 | .dialog__content
17 | position: relative
18 |
19 | .dialog.withBg .dialog__content
20 | background: $dialog-bg-color
21 | border: 1px solid $dialog-border-color
22 | box-shadow: $dialog-shadow
23 | min-width: rem(500px)
24 | max-width: rem(900px)
25 | padding: rem(40px) rem(30px)
26 |
27 | .dialog__body
28 | margin-bottom: rem(20px)
29 | min-height: rem(50px)
30 |
31 | h1
32 | font-size: 1.6rem
33 | color: $dialog-title-color
34 | margin-bottom: 1rem
35 |
36 | .dialog__action button
37 | margin-left: rem(10px)
38 | margin-bottom: 0
39 |
--------------------------------------------------------------------------------
/vj4/model/oplog.py:
--------------------------------------------------------------------------------
1 | from bson import objectid
2 |
3 | from vj4 import db
4 | from vj4.util import argmethod
5 |
6 | TYPE_DELETE_DOCUMENT = 1
7 | TYPE_DELETE_SUB_DOCUMENT = 2
8 | TYPE_REJUDGE = 3 # TODO(twd2)
9 |
10 |
11 | @argmethod.wrap
12 | async def add(uid: int, type: int, **kwargs):
13 | """Add an operation log. Returns the document id."""
14 | obj_id = objectid.ObjectId()
15 | coll = db.coll('oplog')
16 | doc = {'_id': obj_id,
17 | 'uid': uid,
18 | 'type': type,
19 | **kwargs}
20 | await coll.insert_one(doc)
21 | return obj_id
22 |
23 |
24 | @argmethod.wrap
25 | async def ensure_indexes():
26 | coll = db.coll('oplog')
27 | await coll.create_index('uid')
28 | # type delete document
29 | await coll.create_index([('doc.domain_id', 1),
30 | ('doc.doc_type', 1),
31 | ('doc.doc_id', 1)], sparse=True)
32 |
33 |
34 | if __name__ == '__main__':
35 | argmethod.invoke_by_args()
36 |
--------------------------------------------------------------------------------
/vj4/ui/components/tooltip/tooltip.page.js:
--------------------------------------------------------------------------------
1 | import { AutoloadPage } from 'vj/misc/PageLoader';
2 | import Tooltip from './Tooltip';
3 |
4 | function getClosestTooltipElement(element) {
5 | const MAX_DEPTH = 3;
6 | for (let i = 0, e = element; i < MAX_DEPTH && e !== null && e !== document; e = e.parentNode, i++) {
7 | if (e.getAttribute('data-has-timeago')) {
8 | return e;
9 | }
10 | }
11 | return null;
12 | }
13 |
14 | const tooltipPage = new AutoloadPage('tooltipPage', () => {
15 | $(document).on('mouseover', (e) => {
16 | const element = getClosestTooltipElement(e.target);
17 | if (!element) {
18 | return;
19 | }
20 | const $element = $(element);
21 | const options = {};
22 | if ($element.attr('data-tooltip-pos')) {
23 | options.position = $element.attr('data-tooltip-pos');
24 | }
25 | const tooltip = Tooltip.getOrConstruct($element, options);
26 | tooltip.open();
27 | });
28 | });
29 |
30 | export default tooltipPage;
31 |
--------------------------------------------------------------------------------
/vj4/ui/constant/domain.js:
--------------------------------------------------------------------------------
1 | import attachObjectMeta from './util/objectMeta';
2 |
3 | export const JOIN_METHOD_NONE = 0;
4 | export const JOIN_METHOD_ALL = 1;
5 | export const JOIN_METHOD_CODE = 2;
6 | export const JOIN_METHOD_RANGE = {
7 | [JOIN_METHOD_NONE]: 'No user is allowed to join this domain',
8 | [JOIN_METHOD_ALL]: 'Any user is allowed to join this domain',
9 | [JOIN_METHOD_CODE]: 'Any user is allowed to join this domain with an invitation code',
10 | };
11 | attachObjectMeta(JOIN_METHOD_RANGE, 'intKey', true);
12 |
13 | export const JOIN_EXPIRATION_KEEP_CURRENT = 0;
14 | export const JOIN_EXPIRATION_UNLIMITED = -1;
15 |
16 | export const JOIN_EXPIRATION_RANGE = {
17 | [JOIN_EXPIRATION_KEEP_CURRENT]: 'Keep current expiration',
18 | 3: 'In 3 hours',
19 | 24: 'In 1 day',
20 | [24 * 3]: 'In 3 days',
21 | [24 * 7]: 'In 1 week',
22 | [24 * 30]: 'In 1 month',
23 | [JOIN_EXPIRATION_UNLIMITED]: 'Never expire',
24 | };
25 | attachObjectMeta(JOIN_EXPIRATION_RANGE, 'intKey', true);
26 |
--------------------------------------------------------------------------------
/vj4/ui/components/messagepad/MessageComponent.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import classNames from 'classnames';
3 |
4 | export default function MessageComponent(props) {
5 | const {
6 | faceUrl,
7 | isSelf,
8 | className,
9 | children,
10 | ...rest
11 | } = props;
12 | const cn = classNames(className, 'messagepad__message', {
13 | 'side--self': isSelf,
14 | 'side--other': !isSelf,
15 | });
16 | return (
17 |
18 |
19 |

20 |
21 |
22 | {children}
23 |
24 |
25 | );
26 | }
27 |
28 | MessageComponent.propTypes = {
29 | isSelf: React.PropTypes.bool,
30 | faceUrl: React.PropTypes.string.isRequired,
31 | className: React.PropTypes.string,
32 | children: React.PropTypes.node,
33 | };
34 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/link.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/templates/user_lostpass_with_code.html:
--------------------------------------------------------------------------------
1 | {% extends "layout/immersive.html" %}
2 | {% block content %}
3 |
4 |
{{ _('Lost Password') }}
5 |
28 |
29 | {% endblock %}
30 |
--------------------------------------------------------------------------------
/scripts/build/runGulp.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import gulp from 'gulp';
3 | import gutil from 'gulp-util';
4 | import chalk from 'chalk';
5 | import gulpConfig from './config/gulp.js';
6 |
7 | export default async function ({ watch, production }) {
8 | function handleError(err) {
9 | gutil.log(chalk.red('Error: %s'), chalk.reset(err.toString()));
10 | if (err && !watch) {
11 | process.exit(1);
12 | return;
13 | }
14 | }
15 | gulpConfig({ watch, production, errorHandler: handleError });
16 | return new Promise(resolve => {
17 | gulp.on('task_start', ({ task }) => {
18 | gutil.log(chalk.blue(`Starting task: %s`), chalk.reset(task));
19 | });
20 | gulp.on('task_stop', ({ task }) => {
21 | gutil.log(chalk.green(`Finished: %s`), chalk.reset(task));
22 | if (task === 'default') {
23 | if (watch) {
24 | gulp.start('watch');
25 | }
26 | resolve();
27 | }
28 | });
29 | gulp.start('default');
30 | });
31 | };
32 |
--------------------------------------------------------------------------------
/vj4/model/adaptor/defaults.py:
--------------------------------------------------------------------------------
1 | DEFAULT_CODE_TEMPLATES = {
2 | 'c': r"""
3 | #include
4 |
5 | int main()
6 | {
7 | printf("hello, world\n");
8 | }
9 | """,
10 | 'cc': r"""
11 | #include
12 |
13 | using namespace std;
14 |
15 | int main()
16 | {
17 | cout << "hello, world" << endl;
18 | }
19 | """,
20 | 'pas': r"""
21 | begin
22 | writeln('hello, world');
23 | end.
24 | """,
25 | 'java': r"""
26 | import java.io.*;
27 | import java.util.Scanner;
28 |
29 | public class Main {
30 | public static void main(String[] args) throws IOException {
31 | Scanner sc = new Scanner(System.in);
32 | System.out.println("hello, world");
33 | }
34 | }
35 | """,
36 | 'py': r"""
37 | print 'hello, world'
38 | """,
39 | 'py3': r"""
40 | print('hello, world')
41 | """,
42 | 'php': r"""
43 | hello, world
44 | """,
45 | 'rs': r"""
46 | fn main() {
47 | println!("hello, world!");
48 | }
49 | """,
50 | 'hs': r"""
51 | main = putStrLn "hello, world"
52 | """,
53 | }
54 |
--------------------------------------------------------------------------------
/vj4/ui/components/pager/pager.page.styl:
--------------------------------------------------------------------------------
1 | .pager
2 | margin-bottom: rem(10px)
3 | border-top: 2px solid $pager-border-color
4 | text-align: center
5 | font-size: rem($font-size-small)
6 |
7 | &:empty
8 | display: none
9 |
10 | li
11 | margin: 0
12 | display: inline-block
13 | margin-top: -2px
14 |
15 | .pager__item
16 | display: inline-block
17 | padding: rem(10px)
18 | text-decoration: none
19 | border: 0
20 | border-top: 2px solid $pager-border-color
21 | transition: border-top .2s linear, color .2s linear
22 | color: #a7a7a7
23 |
24 | &.link
25 | &, .typo &
26 | color: #a7a7a7
27 |
28 | &:hover
29 | border-color: $primary-color
30 | text-decoration: none
31 | &, .typo &
32 | color: #666
33 |
34 | &.current
35 | color: $secondary-color
36 | border-color: $secondary-color
37 |
38 | +mobile()
39 | .pager__item
40 | &.ellipsis,
41 | &.first,
42 | &.previous,
43 | &.next,
44 | &.last
45 | display: none
46 |
47 |
--------------------------------------------------------------------------------
/vj4/util/json.py:
--------------------------------------------------------------------------------
1 | import calendar
2 | import datetime
3 | import json
4 |
5 | from bson import objectid
6 |
7 |
8 | class Encoder(json.JSONEncoder):
9 | item_separator = ','
10 | key_separator = ':'
11 |
12 | def __init__(self, **kwargs):
13 | super(Encoder, self).__init__(ensure_ascii=False, **kwargs)
14 |
15 | def default(self, o):
16 | if type(o) is objectid.ObjectId:
17 | return str(o)
18 | if type(o) is datetime.datetime:
19 | return calendar.timegm(o.utctimetuple()) * 1000
20 | return super(Encoder, self).default(o)
21 |
22 |
23 | class EncoderPretty(Encoder):
24 | item_separator = ', '
25 | key_separator = ': '
26 |
27 | def __init__(self, **kwargs):
28 | super(EncoderPretty, self).__init__(indent=2, **kwargs)
29 |
30 | def default(self, o):
31 | return super(EncoderPretty, self).default(o)
32 |
33 |
34 | class Decoder(json.JSONDecoder):
35 | pass
36 |
37 |
38 | encode = Encoder().encode
39 | encode_pretty = EncoderPretty().encode
40 | decode = Decoder().decode
41 |
--------------------------------------------------------------------------------
/vj4/ui/constant/contest.js:
--------------------------------------------------------------------------------
1 | import attachObjectMeta from './util/objectMeta';
2 |
3 | export const CONTEST_TYPE_CONTEST = 1;
4 | export const CONTEST_TYPE_HOMEWORK = 2;
5 |
6 | export const RULE_OI = 2;
7 | export const RULE_ACM = 3;
8 | export const RULE_ASSIGNMENT = 11;
9 |
10 | export const DOCTYPE_TO_CTYPE = {
11 | 30: 'contest',
12 | 60: 'homework',
13 | };
14 | attachObjectMeta(DOCTYPE_TO_CTYPE, 'intKey', true);
15 |
16 | export const CTYPE_TO_DOCTYPE = {
17 | contest: 30,
18 | homework: 60,
19 | };
20 |
21 | export const CONTEST_RULES = [
22 | RULE_OI,
23 | RULE_ACM,
24 | ];
25 |
26 | export const HOMEWORK_RULES = [
27 | RULE_ASSIGNMENT,
28 | ];
29 |
30 | export const RULE_ID = {
31 | [RULE_OI]: 'oi',
32 | [RULE_ACM]: 'acm',
33 | [RULE_ASSIGNMENT]: 'assignment',
34 | };
35 | attachObjectMeta(RULE_ID, 'intKey', true);
36 |
37 | export const RULE_TEXTS = {
38 | [RULE_OI]: 'OI',
39 | [RULE_ACM]: 'ACM/ICPC',
40 | [RULE_ASSIGNMENT]: 'Assignment',
41 | };
42 | attachObjectMeta(RULE_TEXTS, 'intKey', true);
43 |
--------------------------------------------------------------------------------
/vj4/ui/constant/setting.js:
--------------------------------------------------------------------------------
1 | import attachObjectMeta from './util/objectMeta';
2 |
3 | export const PRIVACY_PUBLIC = 0;
4 | export const PRIVACY_REGISTERED_ONLY = 1;
5 | export const PRIVACY_SECRET = 2;
6 | export const PRIVACY_RANGE = {
7 | [PRIVACY_PUBLIC]: 'Public',
8 | [PRIVACY_REGISTERED_ONLY]: 'Visible to registered users',
9 | [PRIVACY_SECRET]: 'Secret',
10 | };
11 | attachObjectMeta(PRIVACY_RANGE, 'intKey', true);
12 |
13 | export const FUNCTION_RANGE = {
14 | 0: 'Disabled',
15 | 1: 'Enabled',
16 | };
17 | attachObjectMeta(FUNCTION_RANGE, 'intKey', true);
18 |
19 | export const BACKGROUND_RANGE = {
20 | 1: 'Bg1',
21 | 2: 'Bg2',
22 | 3: 'Bg3',
23 | 4: 'Bg4',
24 | 5: 'Bg5',
25 | 6: 'Bg6',
26 | 7: 'Bg7',
27 | 8: 'Bg8',
28 | 9: 'Bg9',
29 | 10: 'Bg10',
30 | 11: 'Bg11',
31 | 12: 'Bg12',
32 | 13: 'Bg13',
33 | 14: 'Bg14',
34 | 15: 'Bg15',
35 | 16: 'Bg16',
36 | 17: 'Bg17',
37 | 18: 'Bg18',
38 | 19: 'Bg19',
39 | 20: 'Bg20',
40 | 21: 'Bg21',
41 | };
42 | attachObjectMeta(BACKGROUND_RANGE, 'intKey', true);
43 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | git+https://github.com/LukeXuan/aiosmtplib # custom aiosmtlib with ssl support
2 | git+https://github.com/iceb0y/aiomongo@d229a1f9a0901753c5d66e9a9c45cb756aea2199
3 | accept==0.1.0
4 | aioamqp==0.14.0
5 | aiohttp==2.2.0
6 | asn1crypto==0.24.0
7 | async-timeout==3.0.1
8 | beautifulsoup4==4.9.3
9 | certifi==2020.12.5
10 | chardet==4.0.0
11 | coloredlogs==15.0
12 | cryptography==2.1.4
13 | geoip2==4.2.0
14 | gitdb==4.0.7
15 | GitPython==3.1.17
16 | hoedown==0.3.0
17 | httpagentparser==1.9.1
18 | humanfriendly==9.1
19 | idna==2.6
20 | Jinja2==3.0.1
21 | keyring==10.6.0
22 | keyrings.alt==3.0
23 | lxml==4.6.5
24 | MarkupSafe==2.0.1
25 | maxminddb==2.0.3
26 | mosspy==1.0.9
27 | multidict==5.1.0
28 | oauthlib==3.1.0
29 | pamqp==2.3.0
30 | pycrypto==2.6.1
31 | pygobject==3.26.1
32 | pymongo==3.3.1
33 | pytz==2021.1
34 | pyxdg==0.25
35 | PyYAML==5.4.1
36 | rarfile==4.0
37 | requests==2.25.1
38 | SecretStorage==2.3.1
39 | six==1.11.0
40 | smmap==4.0.0
41 | sockjs==0.6.0
42 | soupsieve==2.2.1
43 | typing-extensions==3.10.0.0
44 | urllib3==1.26.5
45 | yarl==0.10.3
46 |
--------------------------------------------------------------------------------
/vj4/ui/pages/error.page.styl:
--------------------------------------------------------------------------------
1 | $error_width = 650px
2 | $twd2_width = 270px
3 |
4 | .page--error
5 | .error__icon-container,
6 | .error__text-container
7 | text-align: center
8 | margin: rem(20px 0)
9 |
10 | .error__text-container
11 | color: #777
12 | line-height: 1.5
13 |
14 | h1
15 | font-size: 4rem
16 | color: #999
17 | margin-bottom: rem(20px)
18 |
19 | li
20 | list-style: decimal inside
21 |
22 | .error__twd2
23 | background: url('error/twd2.svg') no-repeat
24 | width: $twd2_width
25 | height: $twd2_width
26 | display: inline-block
27 |
28 | +above(rupture.mobile-cutoff)
29 | .error__container
30 | width: rem(700px)
31 | margin: rem(50px) auto
32 | position: relative
33 |
34 | .error__icon-container
35 | float: left
36 | width: $twd2_width
37 |
38 | .error__text-container
39 | left: rem($twd2_width + 50px)
40 | position: absolute
41 | top: 50%
42 | transform: translateY(-50%)
43 | text-align: left
44 | margin: 0
45 |
--------------------------------------------------------------------------------
/vj4/service/mailer.py:
--------------------------------------------------------------------------------
1 | import aiosmtplib as asmtp
2 | from email.mime import text
3 |
4 | from vj4.util import argmethod
5 | from vj4.util import options
6 |
7 | options.define('smtp_host', default='', help='SMTP server')
8 | options.define('smtp_port', default=465, help='SMTP server')
9 | options.define('smtp_user', default='', help='SMTP username')
10 | options.define('smtp_password', default='', help='SMTP password')
11 | options.define('mail_from', default='', help='Mail from')
12 |
13 |
14 | @argmethod.wrap
15 | async def send_mail(to: str, subject: str, content: str):
16 | msg = text.MIMEText(content, _subtype='html', _charset='UTF-8')
17 | msg['Subject'] = subject
18 | msg['From'] = options.mail_from
19 | msg['To'] = to
20 |
21 | async with asmtp.SMTP_SSL(hostname=options.smtp_host, port=options.smtp_port) as server:
22 | await server.ehlo()
23 | await server.login(options.smtp_user, options.smtp_password)
24 | await server.sendmail(options.mail_from, to, msg.as_string())
25 |
26 | if __name__ == '__main__':
27 | argmethod.invoke_by_args()
28 |
--------------------------------------------------------------------------------
/vj4/ui/components/star/star.page.js:
--------------------------------------------------------------------------------
1 | import { AutoloadPage } from 'vj/misc/PageLoader';
2 | import request from 'vj/utils/request';
3 |
4 | function setStarButtonState($starButton, star) {
5 | if (star) {
6 | $starButton.addClass('activated');
7 | } else {
8 | $starButton.removeClass('activated');
9 | }
10 | }
11 |
12 | const starPage = new AutoloadPage('starPage', () => {
13 | $(document).on('click', '.star', (ev) => {
14 | const $button = $(ev.currentTarget);
15 | const currentState = $button.hasClass('activated');
16 | const $form = $button.closest('form');
17 | $form.find('[name="operation"]').val(currentState ? 'unstar' : 'star');
18 | setStarButtonState($button, !currentState);
19 | request
20 | .post($form.attr('action'), $form)
21 | .then((data) => {
22 | setStarButtonState($button, data.star);
23 | })
24 | .catch(() => {
25 | // TODO: notify failure
26 | setStarButtonState($button, currentState);
27 | });
28 | return false;
29 | });
30 | });
31 |
32 | export default starPage;
33 |
--------------------------------------------------------------------------------
/vj4/util/useragent.py:
--------------------------------------------------------------------------------
1 | import httpagentparser
2 |
3 | _PLATFORM_ICON_MAP = {
4 | 'windows': 'windows',
5 | 'linux': 'linux',
6 | 'mac os': 'mac',
7 | 'macintosh': 'mac',
8 | 'ios': 'ios',
9 | 'chromeos': 'chromeos',
10 | 'android': 'android',
11 | 'playstation': 'unknown',
12 | 'blackberry': 'mobile',
13 | 'symbian': 'mobile',
14 | 'nokia s40': 'mobile',
15 | }
16 |
17 |
18 | def parse(ua: str):
19 | dresult = httpagentparser.detect(ua)
20 | platform = 'unknown'
21 | if 'platform' in dresult and dresult['platform']['name'] != None:
22 | platform = dresult['platform']['name']
23 | elif 'os' in dresult and dresult['os']['name'] != None:
24 | platform = dresult['os']['name']
25 | icon = _PLATFORM_ICON_MAP.get(platform.strip().lower(), 'unknown')
26 | os, browser = httpagentparser.simple_detect(ua)
27 | if (ua.lower().find('amd64') >= 0 or ua.lower().find('x86_64') >= 0 or
28 | ua.lower().find('x64') >= 0):
29 | os += ' x64'
30 | return {
31 | 'str': ua,
32 | 'icon': icon,
33 | 'os': os,
34 | 'browser': browser,
35 | }
36 |
--------------------------------------------------------------------------------
/vj4/db.py:
--------------------------------------------------------------------------------
1 | import aiomongo
2 | import functools
3 | import logging
4 | import time
5 |
6 | from vj4.util import options
7 |
8 | options.define('db_host', default='localhost', help='Database hostname or IP address.')
9 | options.define('db_name', default='test', help='Database name.')
10 |
11 | _logger = logging.getLogger(__name__)
12 |
13 | async def init():
14 | global _client, _db
15 | error_count = 0
16 | while True:
17 | try:
18 | _client = await aiomongo.create_client('mongodb://' + options.db_host)
19 | except OSError as e:
20 | if error_count < 10:
21 | error_count += 1
22 | _logger.error(e.args)
23 | _logger.error('Unable to connect mongodb, try again 5 seconds later (%d/10)' % error_count)
24 | time.sleep(5)
25 | else:
26 | raise
27 | else:
28 | break
29 | _db = _client.get_database(options.db_name)
30 |
31 |
32 | @functools.lru_cache()
33 | def coll(name):
34 | return aiomongo.Collection(_db, name)
35 |
36 |
37 | @functools.lru_cache()
38 | def fs(name):
39 | return aiomongo.GridFS(_db, name)
40 |
--------------------------------------------------------------------------------
/vj4/ui/misc/icons/platform--linux.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vj4/ui/pages/discussion_main.page.styl:
--------------------------------------------------------------------------------
1 | .page--discussion_main,
2 | .page--discussion_node,
3 | .page--user_detail
4 | .discussion__item
5 | border-left: rem(5px) solid transparent
6 | padding-left: rem(5px)
7 |
8 | &.highlight
9 | border-left: rem(5px) solid $highlight-color
10 |
11 | .discussion__title
12 | font-size: rem($font-size-title)
13 | line-height: 1.2em
14 | margin-bottom: rem(10px)
15 |
16 | a:visited
17 | color: lighten($text-1-color, 60%)
18 |
19 | .discussion__replies
20 | width: rem(70px)
21 |
22 | .discussion-node__bg
23 | height: 0
24 | box-sizing: content-box
25 | padding-bottom: 40%
26 | background-size: cover
27 | background-position: left center
28 | background-repeat: no-repeat
29 |
30 | for id in 'qa' 'share' 'vijos' 'solution' 'advice'
31 | &.{id}
32 | background-image: url('./discussion_node_bg/nodes_' + id + '.png')
33 | +retina()
34 | background-image: url('./discussion_node_bg/nodes@2x_' + id + '.png')
35 |
36 | +below(rupture.desktop-cutoff)
37 | display: none
38 |
--------------------------------------------------------------------------------
/vj4/ui/components/menu/menu-heading.page.js:
--------------------------------------------------------------------------------
1 | import { AutoloadPage } from 'vj/misc/PageLoader';
2 |
3 | import tpl from 'vj/utils/tpl';
4 |
5 | const menuHeadingPage = new AutoloadPage('menuHeadingPage', null, () => {
6 | $('[data-heading-extract-to]').get().forEach((container) => {
7 | const $container = $(container);
8 | const $target = $('body').find($container.attr('data-heading-extract-to'));
9 | if ($target.length === 0) {
10 | return;
11 | }
12 | let $menu = $target.children('.menu');
13 | if ($menu.length === 0) {
14 | $menu = $(tpl``).appendTo($target);
15 | $target.children('.menu__link').addClass('expandable');
16 | }
17 | $container.find('[data-heading]').get().forEach((heading) => {
18 | const $heading = $(heading);
19 | $(tpl`
20 |
25 | `).appendTo($menu);
26 | });
27 | });
28 | });
29 |
30 | export default menuHeadingPage;
31 |
--------------------------------------------------------------------------------
/vj4/ui/utils/loadReactRedux.js:
--------------------------------------------------------------------------------
1 | export default async function loadReactRedux(storeReducer) {
2 | const React = await System.import('react');
3 | const { render, unmountComponentAtNode } = await System.import('react-dom');
4 | const { Provider } = await System.import('react-redux');
5 | const { createStore, applyMiddleware } = await System.import('redux');
6 | const { default: reduxThunk } = await System.import('redux-thunk');
7 | const { default: reduxPromise } = await System.import('redux-promise-middleware');
8 |
9 | const reduxMiddlewares = [];
10 | reduxMiddlewares.push(reduxThunk);
11 | reduxMiddlewares.push(reduxPromise());
12 |
13 | if (process.env.NODE_ENV !== 'production') {
14 | const { createLogger } = await System.import('redux-logger');
15 | reduxMiddlewares.push(createLogger({
16 | collapsed: true,
17 | duration: true,
18 | }));
19 | }
20 |
21 | const store = createStore(storeReducer, applyMiddleware(...reduxMiddlewares));
22 |
23 | return {
24 | React,
25 | render,
26 | unmountComponentAtNode,
27 | Provider,
28 | store,
29 | };
30 | }
31 |
--------------------------------------------------------------------------------
/vj4/ui/pages/record_main.page.js:
--------------------------------------------------------------------------------
1 | import { NamedPage } from 'vj/misc/PageLoader';
2 | import UserSelectAutoComplete from 'vj/components/autocomplete/UserSelectAutoComplete';
3 |
4 | const page = new NamedPage('record_main', async () => {
5 | const SockJs = await System.import('sockjs-client');
6 | const DiffDOM = await System.import('diff-dom');
7 |
8 | const sock = new SockJs(Context.socketUrl);
9 | const dd = new DiffDOM();
10 |
11 | sock.onmessage = (message) => {
12 | const msg = JSON.parse(message.data);
13 | const $newTr = $(msg.html);
14 | const $oldTr = $(`.record_main__table tr[data-rid="${$newTr.attr('data-rid')}"]`);
15 | if ($oldTr.length) {
16 | $oldTr.trigger('vjContentRemove');
17 | dd.apply($oldTr[0], dd.diff($oldTr[0], $newTr[0]));
18 | $oldTr.trigger('vjContentNew');
19 | } else {
20 | $('.record_main__table tbody').prepend($newTr);
21 | $newTr.trigger('vjContentNew');
22 | }
23 | };
24 | UserSelectAutoComplete.getOrConstruct($('.filter-user [name="uid_or_name"]'), {
25 | clearDefaultValue: false,
26 | });
27 | });
28 |
29 | export default page;
30 |
--------------------------------------------------------------------------------
/vj4/util/geoip.py:
--------------------------------------------------------------------------------
1 | import geoip2.database
2 | import os.path
3 |
4 | _GEOIP2_DB_FILE = './GeoLite2-City.mmdb'
5 |
6 | if os.path.isfile(_GEOIP2_DB_FILE):
7 | _reader = geoip2.database.Reader(_GEOIP2_DB_FILE)
8 | else:
9 | _reader = None
10 |
11 | _LOCALE_MAP = {
12 | 'zh_CN': 'zh-CN'
13 | }
14 |
15 |
16 | def ip2geo(ip: str, locale: str='en'):
17 | if _reader == None:
18 | return 'Not available'
19 |
20 | # Map VJ locale string to MaxMind locale string
21 | locale = _LOCALE_MAP.get(locale, locale)
22 |
23 | try:
24 | geodoc = _reader.city(ip)
25 | geo = []
26 |
27 | if locale in geodoc.city.names:
28 | geo.append(geodoc.city.names[locale])
29 | elif 'en' in geodoc.city.names:
30 | geo.append(geodoc.city.names['en'])
31 |
32 | if locale in geodoc.country.names:
33 | geo.append(geodoc.country.names[locale])
34 | elif 'en' in geodoc.country.names:
35 | geo.append(geodoc.country.names['en'])
36 |
37 | if len(geo) == 0:
38 | return 'Unknown'
39 | else:
40 | return ', '.join(geo)
41 | except geoip2.errors.AddressNotFoundError:
42 | return 'Unknown'
43 |
--------------------------------------------------------------------------------
/vj4/util/locale.py:
--------------------------------------------------------------------------------
1 | """A locale module which mimics tornado's interface."""
2 | import collections
3 | import functools
4 | import os
5 | import os.path
6 | import yaml
7 |
8 | from vj4.util import options
9 |
10 | options.define('default_locale', default='en', help='Default locale.')
11 |
12 | _locales = {}
13 |
14 | # View langs.
15 | VIEW_LANGS = {}
16 |
17 |
18 | def load_translations(translation_path):
19 | langs = []
20 | for filename in os.listdir(translation_path):
21 | if not filename.endswith(".yaml"):
22 | continue
23 | with open(os.path.join(translation_path, filename), encoding='utf-8') as yaml_file:
24 | code = filename[:-5]
25 | name = yaml_file.readline()[1:].strip()
26 | _locales[code] = yaml.load(yaml_file)
27 | langs.append((code, name))
28 | global VIEW_LANGS
29 | VIEW_LANGS = collections.OrderedDict(langs)
30 |
31 |
32 | @functools.lru_cache()
33 | def get_translate(locale_code):
34 | if locale_code not in _locales:
35 | locale_code = options.default_locale
36 | locale = _locales[locale_code]
37 | return lambda text: locale[text] if text in locale else text
38 |
--------------------------------------------------------------------------------
/scripts/build/runWebpack.js:
--------------------------------------------------------------------------------
1 | import fs from 'fs';
2 | import webpack from 'webpack';
3 | import root from './utils/root.js';
4 | import webpackConfig from './config/webpack.js';
5 |
6 | export default function ({ watch, production }) {
7 |
8 | const compiler = webpack(webpackConfig({ watch, production }));
9 | compiler.apply(new webpack.ProgressPlugin());
10 |
11 | const outputOptions = {
12 | colors: true,
13 | errorDetails: true,
14 | optimizationBailout: production,
15 | };
16 |
17 | function compilerCallback(err, stats) {
18 | if (err) {
19 | // config errors
20 | console.error(err.stack || err);
21 | if (err.details) console.error(err.details);
22 | process.exit(1);
23 | return;
24 | }
25 | console.log(stats.toString(outputOptions));
26 | fs.writeFileSync(root('./.webpackStats.json'), JSON.stringify(stats.toJson(), null, 2));
27 | if (!watch && stats.hasErrors()) {
28 | process.exitCode = 1;
29 | }
30 | }
31 |
32 | if (watch) {
33 | compiler.watch({}, compilerCallback);
34 | } else {
35 | compiler.run(compilerCallback);
36 | }
37 |
38 | };
39 |
--------------------------------------------------------------------------------
/vj4/test/test_view.py:
--------------------------------------------------------------------------------
1 | import unittest
2 |
3 | from vj4.handler import base
4 |
5 | PERM_DUMMY = 1
6 | PRIV_DUMMY = 2
7 |
8 |
9 | class DummyHandler(object):
10 | def __init__(self):
11 | self.perm_checked = None
12 | self.priv_checked = None
13 |
14 | def check_perm(self, perm):
15 | self.perm_checked = perm
16 |
17 | def check_priv(self, priv):
18 | self.priv_checked = priv
19 |
20 |
21 | class DecoratorTest(unittest.TestCase, DummyHandler):
22 | def setUp(self):
23 | DummyHandler.__init__(self)
24 |
25 | @base.require_perm(PERM_DUMMY)
26 | def assert_perm_checked(self, perm):
27 | self.assertEqual(self.perm_checked, perm)
28 |
29 | @base.require_priv(PRIV_DUMMY)
30 | def assert_priv_checked(self, priv):
31 | self.assertEqual(self.priv_checked, priv)
32 |
33 | def test_require_perm_func(self):
34 | self.assertIsNone(self.perm_checked)
35 | self.assert_perm_checked(PERM_DUMMY)
36 |
37 | def test_require_priv_func(self):
38 | self.assertIsNone(self.priv_checked)
39 | self.assert_priv_checked(PRIV_DUMMY)
40 |
41 |
42 | if __name__ == '__main__':
43 | unittest.main()
44 |
--------------------------------------------------------------------------------
/vj4/ui/components/scratchpad/ScratchpadRecordsTableContainer.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import classNames from 'classnames';
3 | import { connect } from 'react-redux';
4 | import ScratchpadRecordsRow from './ScratchpadRecordsRowContainer';
5 |
6 | const mapStateToProps = state => ({
7 | rows: state.records.rows,
8 | isLoading: state.ui.records.isLoading,
9 | });
10 |
11 | @connect(mapStateToProps)
12 | export default class ScratchpadRecordsTableContainer extends React.PureComponent {
13 | render() {
14 | const cn = classNames('data-table is--full-row scratchpad__records__table', {
15 | loading: this.props.isLoading,
16 | });
17 | return (
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | {this.props.rows.map(rowId => (
27 |
28 | ))}
29 |
30 |
31 | );
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/vj4/ui/templates/discussion_create.html:
--------------------------------------------------------------------------------
1 | {% extends "layout/basic.html" %}
2 | {% block content %}
3 |
4 |
25 |
26 | {% include 'components/md_hint.html' %}
27 |
28 |
29 | {% endblock %}
30 |
--------------------------------------------------------------------------------
/vj4/ui/components/footer/footer.page.js:
--------------------------------------------------------------------------------
1 | import { AutoloadPage } from 'vj/misc/PageLoader';
2 | import { isBelow } from 'vj/utils/mediaQuery';
3 | import { slideUp, slideDown } from 'vj/utils/slide';
4 | import responsiveCutoff from 'vj/breakpoints.json';
5 |
6 | const footerPage = new AutoloadPage('footerPage', () => {
7 | if ($('.footer').length === 0) {
8 | return;
9 | }
10 | $('.footer__category.expandable > h1').click(async (ev) => {
11 | if (!isBelow(responsiveCutoff.mobile)) {
12 | return;
13 | }
14 | const $category = $(ev.currentTarget).closest('.footer__category');
15 | const $list = $category.find('.footer__category__expander');
16 | if ($category.hasClass('animating')) {
17 | return;
18 | }
19 | $category.addClass('animating');
20 | if ($category.hasClass('expanded')) {
21 | $category.removeClass('expanded');
22 | await slideUp($list, 300, { opacity: 1 }, { opacity: 0 });
23 | } else {
24 | $category.addClass('expanded');
25 | await slideDown($list, 300, { opacity: 0 }, { opacity: 1 });
26 | }
27 | $category.removeClass('animating');
28 | });
29 | });
30 |
31 | export default footerPage;
32 |
--------------------------------------------------------------------------------
/vj4/ui/templates/user_register_with_code.html:
--------------------------------------------------------------------------------
1 | {% extends "layout/immersive.html" %}
2 | {% block content %}
3 |
4 |
{{ _('Sign Up') }}
5 |
34 |
35 | {% endblock %}
36 |
--------------------------------------------------------------------------------
/vj4/ui/pages/training_main.page.styl:
--------------------------------------------------------------------------------
1 | .page--training_main
2 | .training__title
3 | line-height: 1.2
4 | margin-bottom: 0.4em
5 | font-size: rem($font-size-title)
6 |
7 | .training__intro
8 | color: lighten($text-1-color, 20%)
9 | font-size: rem($font-size-secondary)
10 | margin: 1em 0
11 |
12 | .training__item
13 | padding-left: rem(5px)
14 |
15 | .training__participants
16 | width: rem(80px)
17 |
18 | .training__progress
19 | line-height: rem(17px)
20 | vertical-align: top
21 |
22 | .training__progress-bar
23 | display: inline-block
24 | width: rem(100px)
25 | height: rem(5px)
26 | position: relative
27 | background: #EEE
28 | margin: rem(6px 0)
29 | line-height: 1
30 | vertical-align: top
31 |
32 | .training__progress-track
33 | position: absolute
34 | left: 0
35 | top: 0
36 | height: 100%
37 | width: 0%
38 | background-color: green
39 |
40 | .secondary.training__list
41 | .training__item
42 | margin-bottom: rem(15px)
43 | .training__title
44 | font-size: rem($font-size)
45 |
46 | &.my
47 | .training-status--icon
48 | font-size: rem($font-size-icon * 1.2)
49 |
--------------------------------------------------------------------------------
/vj4/ui/pages/record_main.page.styl:
--------------------------------------------------------------------------------
1 | .page--record_main
2 | .col--status
3 | width: rem(170px)
4 | position: relative
5 |
6 | .col--submit-at
7 | width: rem(160px)
8 | text-align: center
9 |
10 | &.nojs .col--submit-at
11 | width: rem(160px)
12 |
13 | .col--submit-by
14 | width: rem(160px)
15 | text-align: right
16 |
17 | .col--time, .col--memory, .col--lang
18 | width: rem(100px)
19 | text-align: center
20 |
21 | .col--status, .col--submit-by, .col--lang
22 | border-right: 1px solid $table-border-color
23 |
24 | .col--status__text
25 | position: relative
26 | z-index: 2
27 |
28 | .col--status__progress-container
29 | position: absolute
30 | left: 0
31 | top: 0
32 | width: 100%
33 | height: 100%
34 | z-index: 1
35 |
36 | .col--status__progress
37 | position: absolute
38 | left: 0
39 | top: 0
40 | height: 100%
41 | background: #fff6d1
42 |
43 | +tablet()
44 | .col--time,
45 | .col--memory,
46 | .col--lang
47 | display: none
48 |
49 | +mobile()
50 | .col--submit-by,
51 | .col--submit-at,
52 | .col--time,
53 | .col--memory,
54 | .col--lang
55 | display: none
56 |
--------------------------------------------------------------------------------
/vj4/ui/templates/domain_join.html:
--------------------------------------------------------------------------------
1 | {% extends "layout/basic.html" %}
2 | {% block content %}
3 |
4 |
5 |
6 |
11 |
12 |
{{ _('By clicking the button, you will become a member of the domain {0}.').format(handler.domain['name']) }}
13 |
14 |
15 |
22 |
23 |
24 |
25 |
26 | {% endblock %}
27 |
--------------------------------------------------------------------------------
/vj4/ui/templates/contest_scoreboard_download_html.html:
--------------------------------------------------------------------------------
1 |
2 |
27 |
28 |
29 |
30 | {%- for column in rows[0] -%}
31 | |
32 | {{ column['value'] }}
33 | |
34 | {%- endfor -%}
35 |
36 |
37 |
38 | {%- for row in rows[1:] -%}
39 |
40 | {%- for column in row -%}
41 | |
42 | {{ column['value'] }}
43 | |
44 | {%- endfor -%}
45 |
46 | {%- endfor -%}
47 |
48 |
49 |
--------------------------------------------------------------------------------
/vj4/ui/components/messagepad/reducers/activeId.js:
--------------------------------------------------------------------------------
1 | import 'jquery.easing';
2 |
3 | function scrollToViewport() {
4 | const BOUND_TOP = 60;
5 | const BOUND_BOTTOM = 20;
6 | const node = $('.messagepad')[0];
7 | if (node.offsetHeight + BOUND_TOP + BOUND_BOTTOM < window.innerHeight) {
8 | const rect = node.getBoundingClientRect();
9 | const rectBody = document.body.getBoundingClientRect();
10 | let targetScrollTop = null;
11 | if (rect.top < BOUND_TOP) {
12 | targetScrollTop = rect.top - rectBody.top - BOUND_TOP;
13 | } else if (rect.top + node.offsetHeight > window.innerHeight) {
14 | targetScrollTop = rect.top - rectBody.top + node.offsetHeight + BOUND_BOTTOM - window.innerHeight;
15 | }
16 | if (targetScrollTop !== null) {
17 | $('html, body').stop().animate({ scrollTop: targetScrollTop }, 200, 'easeOutCubic');
18 | }
19 | }
20 | }
21 |
22 | export default function reducer(state = null, action) {
23 | switch (action.type) {
24 | case 'DIALOGUES_SWITCH_TO': {
25 | scrollToViewport();
26 | return action.payload;
27 | }
28 | case 'DIALOGUES_POST_SEND_FULFILLED': {
29 | return action.payload.mdoc._id;
30 | }
31 | default:
32 | return state;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/vj4/ui/templates/user_login.html:
--------------------------------------------------------------------------------
1 | {% extends "layout/immersive.html" %}
2 | {% block content %}
3 |
4 |
5 |
{{ _('Login') }}
6 |
30 |
31 |
32 | {% endblock %}
33 |
--------------------------------------------------------------------------------
/vj4/ui/templates/components/md_hint.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | - {{ _('Inline codes') }}
5 | `std::cout()`
6 | - {{ _('Code blocks') }}
7 | -
8 |
```c++
9 | int main() {
10 | return 0;
11 | }
12 | ```
13 |
14 | - {{ _('Inline formulas') }}
15 | $ E=mc^2 $
16 | - {{ _('Formula blocks') }}
17 | $$ E=mc^2 $$
18 | - {{ _('Links') }}
19 | [{{ _('Text') }}](http://...)
20 | - {{ _('Images') }}
21 | 
22 |
23 |
{{ _('Learn More') }}
24 |
28 |
29 |
30 |
--------------------------------------------------------------------------------