Compare commits

..

1011 commits
VKU ... master

Author SHA1 Message Date
Phuoc Anh Kha Le
c5666e8ff1
Merge pull request #128 from anhkha2003/master
Improve UI for mobile
2024-10-03 01:54:33 -05:00
Phuoc Anh Kha Le
6d85b5e00d
Merge branch 'LQDJudge:master' into master 2024-10-03 01:52:11 -05:00
anhkha2003
dbe3caadb3 Improve design for mobile 2024-10-03 01:50:21 -05:00
Phuoc Anh Kha Le
f771aac922
Merge pull request #127 from anhkha2003/master
Fix contest rescore bug
2024-10-02 20:43:13 -05:00
anhkha2003
18d23c771a Fix contest rescore bug 2024-10-02 20:27:49 -05:00
Phuoc Anh Kha Le
3d67fb274e
Add contest to course (#126) 2024-10-02 15:06:33 -05:00
cuom1999
72eada0a4e Fix pagedown bugs 2024-10-01 23:35:33 -05:00
cuom1999
9c01ec8a22 Fix contest summary 2024-10-01 11:07:55 -05:00
cuom1999
1dd4ccd324 Fix user admin bug and comment index bug 2024-09-19 16:14:45 -05:00
cuom1999
c54bcd4a16 Add validation to user admin 2024-09-17 21:11:40 -05:00
cuom1999
05ab90e1d4 Fix some bugs 2024-09-08 00:31:24 -05:00
cuom1999
88845aebd8 Delete contest in form before adding 2024-09-03 10:48:01 -05:00
cuom1999
a230441862 Add trans and migration 2024-09-03 09:51:38 -05:00
Phuoc Anh Kha Le
c833dc06d9
Add order and score for course problems (#124)
* Add order and grade for course problems

* Fix delete problems bug
2024-09-03 09:26:20 -05:00
cuom1999
67888bcd27 Simplify common.js 2024-08-30 22:45:18 -05:00
cuom1999
7250c71e93 Hacky fix for course edit 2024-08-21 22:59:16 -05:00
cuom1999
a42bae51f7 Fix minor bugs and improve some model fields 2024-08-21 22:42:42 -05:00
cuom1999
f98549e92d Fix problem feed 2024-08-15 17:46:09 +07:00
cuom1999
ff2c4e91d2 Keep query params between problem feed and list 2024-08-15 15:08:36 +07:00
cuom1999
091c662b3b Add limit for contest time limit 2024-08-14 19:34:46 +07:00
cuom1999
37cdd2dd04 Add end time limit to a contest 2024-08-14 16:15:19 +07:00
cuom1999
1f91299d41 Allow author deletes chat 2024-08-13 22:10:25 +07:00
cuom1999
34e8ac6b8e Fix manifest 2024-08-13 21:36:01 +07:00
cuom1999
8cd7327d20 Fix race condition in chat 2024-08-13 17:57:05 +07:00
cuom1999
7406d081aa Improve notif and organization add member 2024-08-13 17:42:51 +07:00
cuom1999
cdbed121cd Remove cache for org member check 2024-08-08 10:45:03 +07:00
cuom1999
48d0a58dae Dont show official contests in home 2024-07-24 17:19:25 +07:00
Phuoc Dinh Le
7aecf6b046
Update dmoj-user.po 2024-07-23 13:25:35 +07:00
Phuoc Anh Kha Le
44554d7de6
Design pending blogs in organizations (#123) 2024-07-23 13:24:43 +07:00
cuom1999
66f6212947 Merge branch 'master' of https://github.com/LQDJudge/online-judge 2024-07-18 10:23:17 +07:00
cuom1999
6421e4f5be Fix value error for new org image 2024-07-18 10:23:10 +07:00
cuom1999
6224649dd0 Fix value error for new org image 2024-07-18 09:20:31 +07:00
Phuoc Anh Kha Le
9dd779f4fa
Add image uploading feature for organization (#122) 2024-07-18 09:05:42 +07:00
cuom1999
c00db58cb1 Temp fix for cache bug 2024-07-09 00:08:25 -05:00
cuom1999
08ede3f797 Revert "Refactor darkmode"
This reverts commit d08010a2ab.
2024-07-08 23:59:34 -05:00
cuom1999
309f0dd993 Revert "Change css order"
This reverts commit 981e0f1b4e.
2024-07-08 23:59:03 -05:00
cuom1999
981e0f1b4e Change css order 2024-07-08 23:15:39 -05:00
cuom1999
d08010a2ab Refactor darkmode 2024-07-08 22:29:57 -05:00
cuom1999
5537ef5522 Fix nav list 2024-07-01 15:03:09 -05:00
cuom1999
cc666c8361 Refactor contest ranking 2024-06-25 00:23:40 -05:00
cuom1999
73541ef8dd Fix navicon 2024-06-24 22:40:24 -05:00
cuom1999
0470bfec68 Render problem list stats with ajax 2024-06-24 21:15:03 -05:00
cuom1999
caf599b5b3 Optimize friend submission page 2024-06-24 20:48:31 -05:00
cuom1999
2c3e982b7b Import font first 2024-06-24 20:13:15 -05:00
cuom1999
3c99a2c477 Fix some organization bugs 2024-06-24 20:01:00 -05:00
cuom1999
a711fb9768 Refactor 3-col-content 2024-06-24 19:42:50 -05:00
Phuoc Anh Kha Le
326b3d5dd3
Design organization list page and add organization search (#119) 2024-06-18 22:11:36 -05:00
cuom1999
02ba30a29e Merge branch 'master' of https://github.com/LQDJudge/online-judge 2024-06-12 13:50:42 -05:00
cuom1999
67839fbbd0 Don't compress darkmode css 2024-06-12 13:50:37 -05:00
Phuoc Anh Kha Le
45c1f400a1
Design right sidebar of problem and organization home page (#117)
* Design right sidebar of problem and organization home page

* Change button small and message button
2024-06-10 15:13:53 -05:00
cuom1999
d7cc620a0a Add markdown utils
- External links open in a new tab
- Clicking on image open an image modal
2024-06-06 21:17:11 -05:00
cuom1999
a75a080b9c Update darkmode 2024-06-04 22:05:37 -05:00
Phuoc Anh Kha Le
44682900e1
Replace fontawesome with latest version 6.5.2 (#116) 2024-06-04 22:00:23 -05:00
Phuoc Anh Kha Le
46c950dc37
Edit displaying rating and points rank for unlisted (#115)
* Edit displaying rating and points rank for unlisted

* Edit rating/points rank of unlisted users and cache get_rank functions
2024-06-04 19:40:20 -05:00
cuom1999
570c3071ee Fix admin bugs 2024-06-01 01:37:29 -05:00
cuom1999
bb891e5b49 Fix missing arg 2024-05-31 01:38:25 -05:00
cuom1999
0406dea2a2 Fix wrong ranking order in previous commit 2024-05-31 01:23:22 -05:00
cuom1999
308006f7bd Add virtual participation to ranking list when virtual joining 2024-05-31 00:35:20 -05:00
cuom1999
dc86e3a095 Merge branch 'master' of https://github.com/LQDJudge/online-judge 2024-05-30 23:22:50 -05:00
cuom1999
f3a393b767 Filter out official contests in contest list 2024-05-30 23:22:38 -05:00
Phuoc Anh Kha Le
938af7c720
Fix changing tab with page param bug (#114) 2024-05-30 18:22:20 -05:00
cuom1999
10e50795d9 Add official contest 2024-05-30 02:59:22 -05:00
Phuoc Anh Kha Le
796a670cd7
Fix organization contest bug and add Bookmarks on dropdown list (#113) 2024-05-29 14:22:58 -05:00
Phuoc Anh Kha Le
c6acfa5e05
Redesign Bookmark (#112) 2024-05-29 00:14:42 -05:00
cuom1999
829e6a802d Fix active contest list 2024-05-26 22:26:38 -05:00
Phuoc Anh Kha Le
9f4ae9f78f
Organize contest list into timeline tabs (#111) 2024-05-25 13:27:20 -05:00
Phuoc Dinh Le
6c8926ec56
Fix demo.json 2024-05-21 23:34:23 -05:00
Phuoc Anh Kha Le
5335bc248f
Ad profile table (#110) 2024-05-21 23:09:22 -05:00
cuom1999
ee17bc0778 Add validation for random problem 2024-05-14 13:55:18 -05:00
cuom1999
5d79374b92 Fix type error 2024-05-14 13:47:22 -05:00
cuom1999
adb7c182d8 Add validation code 2024-05-14 13:42:26 -05:00
cuom1999
0ea822f7a0 Add type check for cache 2024-05-08 10:15:55 -05:00
cuom1999
f1ba0e79c1 Organization contest list: Only show create button for admin 2024-05-05 23:59:11 -05:00
cuom1999
896eec6c22 Fix organization contest list 2024-05-05 23:41:10 -05:00
cuom1999
eb1f6942f0 Add dark mode 2024-05-05 23:01:08 -05:00
cuom1999
4549d57ee1 Change layout of contest list 2024-05-05 22:47:57 -05:00
cuom1999
fae8422508 Dirty cache when rating 2024-05-05 22:43:41 -05:00
cuom1999
913ec45acf Update darkmode 2024-05-03 18:16:27 -05:00
cuom1999
e5de51f8fa Make custom background better 2024-05-03 18:06:07 -05:00
cuom1999
4d56d18a24 Import models when django ready
Since we store model's instances in cache, this will help django figure out the correct model when unserializing cache data.
2024-05-03 15:25:04 -05:00
cuom1999
04f9fe8252 Bridge: only update necessary problems when judge sends update signal 2024-05-03 13:37:19 -05:00
cuom1999
4ee2e1b940 Fix inconsistency of contest is_organization_private 2024-05-02 23:33:18 -05:00
cuom1999
9270a017d3 Fix last row border 2024-05-02 19:18:47 -05:00
cuom1999
8ff0f369a6 Table css 2024-05-02 19:13:19 -05:00
cuom1999
cad679ad90 Fix chat emoji toggle 2024-05-02 13:49:01 -05:00
cuom1999
a63afd6f3c Add contest submissions page 2024-04-29 21:08:48 -05:00
cuom1999
66f2184b39 Fix import 2024-04-27 02:24:11 -05:00
cuom1999
c8f21aa9a5 Refactor submission detail page 2024-04-27 00:37:34 -05:00
cuom1999
8d0045ec82 Add profile info 2024-04-26 22:51:16 -05:00
cuom1999
55a85689e9 Rewrite notif page 2024-04-26 20:55:24 -05:00
cuom1999
1439dd12c8 Change comment feed size back 2024-04-26 20:40:49 -05:00
cuom1999
bf5514032b Clean up more sql queries 2024-04-26 20:37:35 -05:00
cuom1999
571596dcbf Contest caching 2024-04-25 01:58:47 -05:00
cuom1999
86d1ff4eaa Joining contest also leaves current contest 2024-04-24 22:33:57 -05:00
cuom1999
345684300f Only allow clone contest after 1 day 2024-04-24 14:13:11 -05:00
cuom1999
e24c208e1d Fix None bug 2024-04-23 15:53:58 -05:00
cuom1999
ba96d83db8 Contest and Org css 2024-04-23 15:36:51 -05:00
cuom1999
d6832a0550 Fix div by 0 2024-04-21 20:08:25 -05:00
cuom1999
86815fb460 Add lazy loading img and fix friend ranking 2024-04-14 00:23:14 -05:00
cuom1999
4a2bc46206 Fix typo 2024-04-13 20:02:59 -05:00
cuom1999
c6c5ea0c7a Make image in user link bigger 2024-04-13 17:19:39 -05:00
cuom1999
e6a1c04509 Fix css 2024-04-13 17:08:49 -05:00
cuom1999
8f1c8d6c96 Caching and refactors 2024-04-13 17:02:54 -05:00
cuom1999
67b06d7856 Fix datetime widget 2024-04-13 00:51:14 -05:00
cuom1999
7d83efed7f Change date time picker widget 2024-04-12 22:26:17 -05:00
cuom1999
08eef6408f Add text limit to about fields 2024-04-12 12:09:40 -05:00
cuom1999
208a4e4ef7 Standardize user image + minor bugs 2024-04-12 01:51:57 -05:00
cuom1999
5147980d43 Move submission to user tab 2024-04-12 00:08:25 -05:00
cuom1999
2f64c87d56 Fix logged-out user page 2024-04-12 00:04:07 -05:00
cuom1999
361d3fc33a Rewrite user follow + fix some css 2024-04-11 23:56:58 -05:00
cuom1999
04877b47c1 Update judge problems by chunk 2024-04-04 00:27:08 -05:00
cuom1999
7223dccf75 Fix user about css 2024-03-27 00:14:10 -05:00
cuom1999
28156a9952 Fix type 2024-03-26 20:23:47 -05:00
cuom1999
45469ff103 Update collab filter to use dict result 2024-03-26 12:23:13 -05:00
cuom1999
a4c2fad04f Memorize contest filter and fix some css 2024-03-25 13:48:21 -05:00
cuom1999
64a3d1bbb2 Add trans 2024-03-23 00:33:43 -05:00
cuom1999
fd77975390 Add contest rate limit submission 2024-03-23 00:26:53 -05:00
cuom1999
664dc3ca71 Add trans 2024-03-22 22:58:42 -05:00
cuom1999
44c4795282 Add custom file upload 2024-03-22 22:52:21 -05:00
cuom1999
680de724ba Fix activation email 2024-03-22 19:58:00 -05:00
cuom1999
d0d6b1e4f9 Update logo svg 2024-03-20 22:15:10 -05:00
cuom1999
d9d821c6db Fix email - Use png 2024-03-20 17:58:42 -05:00
cuom1999
0c4a49d446 Fix email 2024-03-20 17:51:50 -05:00
cuom1999
d93767abdd Fix email protocol - Change to use svg logo 2024-03-20 17:16:50 -05:00
cuom1999
6dcd3ed0c9 Don't allow normal contests to run pretests only 2024-03-20 11:17:57 -05:00
cuom1999
6bc0eb4b1c Wrap space around emoji regex 2024-03-20 10:48:17 -05:00
cuom1999
d835ee741a Focus editor after pasting image 2024-03-20 00:05:12 -05:00
cuom1999
e923d1b2fe Implement markdown emoji, youtube, clipboard 2024-03-19 23:51:12 -05:00
cuom1999
5e72b472e6 Rename problem data fields 2024-03-18 23:53:35 -05:00
cuom1999
acdf94a8c9 Fix problem admin error and give pdf problem more time to render 2024-03-17 02:39:20 -05:00
cuom1999
6dbe3932de Fix cache + problem pdf 2024-02-28 14:13:59 -06:00
cuom1999
85bee3e77c Fix signals 2024-02-26 15:47:17 -06:00
cuom1999
2189de9433 Fix url.py 2024-02-26 14:51:19 -06:00
cuom1999
1e7957a2cd Remove math engines 2024-02-26 14:49:52 -06:00
cuom1999
3f53c62d4d Fix random frontend (problem info, lazy load img, comment pagedown) 2024-02-26 14:31:33 -06:00
cuom1999
7bba448ef5 Merge branch 'master' of https://github.com/LQDJudge/online-judge 2024-02-23 17:07:49 -06:00
cuom1999
2831a24b90 Cache prefetch 2024-02-23 17:07:34 -06:00
Bao Le
df34e547ad
Copyright and fix for test formatter (#107) 2024-02-21 22:05:06 -06:00
cuom1999
c2f6dba462 Fix some js 2024-02-19 18:30:39 -06:00
cuom1999
6d763f2db5 Fix div by 0 2024-02-19 17:35:36 -06:00
cuom1999
fcaf76e89f Add trans 2024-02-19 17:20:57 -06:00
cuom1999
a0feaa8fc9 Darkmodecss 2024-02-19 17:10:13 -06:00
cuom1999
83579891b9 Add course 2024-02-19 17:00:44 -06:00
cuom1999
d409f0e9b4 Fix toggle 2024-02-14 20:35:13 -06:00
cuom1999
c5aff93fb8 Fix user problem css 2024-02-12 13:43:07 -06:00
cuom1999
4259b909a0 Fix submission css 2024-02-08 12:57:06 -06:00
cuom1999
e996e9e47f Don't allow clone contest with access code 2024-02-07 00:10:31 -06:00
cuom1999
d9b477c441 Submission css 2024-02-05 21:21:44 -06:00
cuom1999
1073ad45ff Fix user table 2024-02-05 20:11:54 -06:00
cuom1999
031e4f5f6b Fix problem pdf 2024-02-05 19:21:17 -06:00
cuom1999
c87566673a Improve katex markdown 2024-02-05 18:32:08 -06:00
cuom1999
08d2437d49 Move from mathjax to katex 2024-02-05 17:02:49 -06:00
cuom1999
b2c9be7bda Modify the offset 2024-02-05 15:40:06 -06:00
cuom1999
2a4882f598 Store scroll offset in cache back button 2024-02-05 15:33:58 -06:00
cuom1999
ea2c7d2f36 Make websocket retry longer 2024-02-05 15:17:37 -06:00
cuom1999
ce04b268c3 Cache back button 2024-02-05 15:17:02 -06:00
cuom1999
76afe927b6 Make mathjax async 2024-02-05 15:16:35 -06:00
cuom1999
695fa85b19 Modify some select2 box 2024-02-05 15:15:32 -06:00
cuom1999
847e8b6660 Tune submission css 2024-02-04 22:33:21 -06:00
cuom1999
9376750a1b Fix css 2024-02-03 01:13:47 -06:00
cuom1999
c8b7848f5a Fix user search 2024-02-03 01:02:55 -06:00
cuom1999
2a4d4e3bc1 Submission css 2024-02-02 22:23:05 -06:00
cuom1999
24a9969738 More md css 2024-02-02 18:54:33 -06:00
cuom1999
9b7cdf811a Change md font 2024-02-02 18:35:50 -06:00
cuom1999
faedcc5c70 Update admin font 2024-01-31 21:05:05 -06:00
cuom1999
96ad972600 Update css 2024-01-31 20:46:25 -06:00
cuom1999
0de11d26a6 Add trans 2024-01-30 22:20:55 -06:00
cuom1999
2ff1ed0f54 No require password if register via social auth 2024-01-30 19:47:06 -06:00
cuom1999
9fd93a3b53 More CSS 2024-01-29 21:18:22 -06:00
cuom1999
4b6ba43c42 CSS fix 2024-01-29 20:43:28 -06:00
cuom1999
e22061fc84 Add login required to test formatter 2024-01-29 13:22:43 -06:00
cuom1999
a079827aa6 Merge branch 'master' of https://github.com/LQDJudge/online-judge 2024-01-29 13:17:46 -06:00
cuom1999
f4eb9c54a4 CSS pagination 2024-01-29 13:17:40 -06:00
Phuoc Anh Kha Le
458598b1cb
Fix bug comparing latest message in room chat (#106) 2024-01-28 22:22:57 -06:00
Phuoc Anh Kha Le
f7fa1c01cb
Add character limit and check validation of messages in Chat (#105) 2024-01-28 15:39:27 -06:00
cuom1999
350492c6e4 Change monospace font 2024-01-25 13:05:40 -06:00
cuom1999
545c655e73 Add submission index 2024-01-22 18:35:19 -06:00
cuom1999
07c3a8859b Update darkmode 2024-01-18 20:45:03 -06:00
cuom1999
aef795b40c Change pre code css in markdown 2024-01-18 19:46:41 -06:00
cuom1999
d75a498d18 Small optimizations 2024-01-18 12:33:36 -06:00
cuom1999
995ff88c87 Force using old python memcached 2024-01-13 19:40:33 -06:00
cuom1999
80b91435cf Remove user script 2024-01-13 19:05:36 -06:00
cuom1999
e09008bcb7 Optimize user log 2024-01-13 18:52:11 -06:00
cuom1999
ee4a947385 Clean up rss 2024-01-13 18:46:44 -06:00
cuom1999
3457eff339 Clean up more lock DB 2024-01-13 18:40:10 -06:00
cuom1999
ad73e4cdf3 Fix related_problems 2024-01-13 18:35:48 -06:00
cuom1999
2cf386e8b5 Add rate limit and don't use lock for vote 2024-01-13 18:23:37 -06:00
cuom1999
104cee9e81 Merge branch 'master' of https://github.com/LQDJudge/online-judge 2024-01-09 13:13:13 -06:00
cuom1999
999e0dcb15 Fix manage problem & update impersonate 2024-01-09 13:13:08 -06:00
Phuoc Anh Kha Le
3126b6ecad
Add max-width for right panel of chat (#104) 2024-01-08 18:47:55 -06:00
cuom1999
2b84d62260 Small changes to test_formatter 2024-01-08 13:06:18 -06:00
cuom1999
d7080a4d1b Fix migration 2024-01-08 12:36:25 -06:00
cuom1999
81a31eafa2 Remove bad migration file 2024-01-08 12:30:14 -06:00
Bao Le
04c6af1dff
Test Formatter (#100) 2024-01-08 12:27:20 -06:00
cuom1999
14ecef649e Fix clone problem 2024-01-04 11:25:39 -06:00
Phuoc Anh Kha Le
65e7d4961d
Change width of left panel in chat (#103) 2024-01-03 18:09:40 -06:00
Phuoc Anh Kha Le
88b07644ee
Fix chat bugs (#102) 2024-01-03 02:07:17 -06:00
cuom1999
eb07dd8fa7 Try fixing problem admin glitch 2023-12-31 23:23:40 -06:00
Phuoc Anh Kha Le
bfe939564b
Create user setting button in chat (#101) 2023-12-30 17:57:37 -06:00
Phuoc Anh Kha Le
c9f1d69b47
Reset textarea to default size after submitting message (#99) 2023-12-24 00:39:48 -06:00
cuom1999
015cbcc758 Set recaptcha host to google 2023-12-24 00:38:36 -06:00
cuom1999
dd32982687 Remove transaction in comment 2023-12-22 23:01:45 -06:00
cuom1999
f970d11d67 Modify problem submit button 2023-12-22 01:22:05 -06:00
cuom1999
5d54b6b3c4 Add trans 2023-12-22 00:17:36 -06:00
cuom1999
c188051aee Import jquery on top 2023-12-21 23:09:04 -06:00
cuom1999
f75c2a391f Fix chat last message body 2023-12-05 20:06:56 -06:00
cuom1999
ff6988f29c Add comment revision count field 2023-12-05 20:01:23 -06:00
cuom1999
b02a30819f Fix frozen handle 2023-12-03 19:54:01 -06:00
cuom1999
1689f7ec7b Add profile index 2023-11-28 20:32:31 -06:00
cuom1999
e1054077fa Add contest field to manage submission page 2023-11-28 20:04:02 -06:00
cuom1999
038aa8674a Update darkmode 2023-11-27 22:02:18 -06:00
cuom1999
de8adf983e Change dropdown css 2023-11-27 21:47:12 -06:00
cuom1999
39b42a29a4 Add email to authentication 2023-11-27 19:49:38 -06:00
cuom1999
26f26a1722 Limit user-table column width 2023-11-27 19:23:48 -06:00
cuom1999
09e01d620e Fix contest time bug 2023-11-26 18:05:19 -06:00
cuom1999
126ed83ee5 Don't update submissions for hidden sub contests 2023-11-24 03:47:08 -06:00
cuom1999
c36884846d Hide check status in problem detail for hidden subtask 2023-11-24 03:21:48 -06:00
cuom1999
b2a91af011 Use DB field instead of cache for contest summary 2023-11-24 00:35:38 -06:00
cuom1999
159b2b4cc0 Paginate contest summary 2023-11-23 23:16:01 -06:00
Van Duc Le
77b441eb5e
Update Submit Button's Font Size & Problem Info Padding's (#97) 2023-11-23 22:11:25 -06:00
cuom1999
de4ee1a655 Update darkmode 2023-11-23 21:56:28 -06:00
cuom1999
9bc4ed00e9 Improve code 2023-11-23 21:52:19 -06:00
Van Duc Le
d21e24dd6c
Problem UI (#96) 2023-11-23 21:46:45 -06:00
Van Duc Le
729a28bce5
New Problem UI (Problem Information & Submit Button) (#95) 2023-11-23 21:23:14 -06:00
cuom1999
32a1ea8919 Fix contest filter 2023-11-17 01:11:54 -06:00
cuom1999
2ac300ff02 Fix contest search 2023-11-16 20:35:15 -06:00
cuom1999
fdb5293edb Modify json resolver 2023-11-16 20:16:22 -06:00
cuom1999
d143218206 Add darkmode 2023-11-16 20:15:45 -06:00
Van Duc Le
5cbf3489b3
Update Problem UI (Problem Information) (#94) 2023-11-16 20:02:36 -06:00
cuom1999
20f55047b8 Fix css 2023-11-16 14:45:35 -06:00
cuom1999
34756c399d Handle vanished submission 2023-11-15 08:17:48 -06:00
cuom1999
0cb981db9f Set is_rated=False for cloned contest 2023-11-10 00:37:21 -06:00
cuom1999
0b4eeb8751 Refactor problem feed code 2023-11-09 02:43:11 -06:00
cuom1999
b6c9ce4763 Fix bug 2023-11-01 21:50:36 -05:00
cuom1999
45587d0884 Add url for internal problem votes 2023-11-01 21:26:27 -05:00
cuom1999
87d7484a89 Make internal problem faster 2023-11-01 21:17:54 -05:00
cuom1999
e5b2481345 Make chat faster 2023-11-01 20:54:09 -05:00
cuom1999
58f3807b8d Try moving js to body end 2023-11-01 19:14:21 -05:00
cuom1999
df49a0e353 Update darkmode 2023-11-01 02:11:02 -05:00
cuom1999
fcbf74ca97 Some css 2023-11-01 02:08:36 -05:00
cuom1999
7a05ad1c3b Fix problem code change bug 2023-10-27 18:02:02 -05:00
cuom1999
b053c43b19 Merge branch 'master' of https://github.com/LQDJudge/online-judge 2023-10-27 16:49:37 -05:00
cuom1999
27586b25b8 Fix group contest m2m fields (authors, curators, etc) 2023-10-27 16:49:28 -05:00
Phuoc Anh Kha Le
3a7d4d8f0a
Fix Comment bugs (#93) 2023-10-25 02:19:05 -05:00
cuom1999
93d032fc72 Fix ranking filters 2023-10-18 14:21:38 -05:00
cuom1999
97d2029b5a Fix bridge 2023-10-17 11:45:52 -05:00
cuom1999
35756d2f15 Allow clone ended contests 2023-10-16 17:37:52 -05:00
cuom1999
1291d750de Add trans 2023-10-16 16:48:44 -05:00
cuom1999
6312f143f5 Don't send error on judge disconnect 2023-10-16 16:38:23 -05:00
cuom1999
ae3f1090bf Reformat 2023-10-16 16:36:56 -05:00
cuom1999
af17ca9665 Admin css 2023-10-15 18:31:30 -05:00
cuom1999
eb6fd48e21 Change admin css 2023-10-15 18:19:56 -05:00
cuom1999
edb0bf30d8 Sort contest by relevance 2023-10-14 15:04:11 -05:00
cuom1999
36e505952c Add fulltext search 2023-10-14 14:56:22 -05:00
cuom1999
aa1b627e6f Fix import bug 2023-10-13 20:16:21 -05:00
cuom1999
11bc57f2b1 Move dirty logic to admin 2023-10-12 08:56:53 -05:00
cuom1999
e1a38d42c3 Use better field 2023-10-11 20:45:32 -05:00
cuom1999
c3cecb3f58 Add more caching 2023-10-11 20:33:48 -05:00
cuom1999
130c96a2fe Fix register 2023-10-11 00:21:21 -05:00
cuom1999
d4e0c5ca86 Fix register 2023-10-11 00:08:26 -05:00
cuom1999
67a3c7274e Try fixing memcache error 2023-10-10 21:01:06 -05:00
cuom1999
8da03aebb0 Fix new notif 2023-10-10 20:49:23 -05:00
cuom1999
a377f45e0b Another fix 2023-10-10 20:31:25 -05:00
cuom1999
94395ae71a Fix registration 2023-10-10 20:28:16 -05:00
cuom1999
5741866c07 Remove try except 2023-10-10 20:23:52 -05:00
cuom1999
801738fea9 Add another fix 2023-10-10 20:09:05 -05:00
cuom1999
9940d9cc4c Add a temp fix 2023-10-10 19:54:55 -05:00
cuom1999
56c2b6d9b9 Fix contest summary 2023-10-10 19:46:48 -05:00
cuom1999
ed287b6ff3 Add caching for user basic info 2023-10-10 19:37:36 -05:00
cuom1999
7f854c40dd Change notification backend 2023-10-10 17:38:48 -05:00
cuom1999
5f97491f0d Add contest summary url 2023-10-06 13:04:12 -05:00
Phuoc Anh Kha Le
b4c1620497
GP Ranking (#90) 2023-10-06 03:54:37 -05:00
cuom1999
9decd11218 Change textarea css 2023-10-05 14:00:20 -05:00
cuom1999
44aca3c2e5 Add darkmode 2023-10-05 13:11:58 -05:00
Bao Le
49d1bc2e1b
Markdown editor improvements (#87) 2023-10-05 13:08:24 -05:00
Võ Trung Hoàng Hưng
458b9e425e
fix color in pagination of contest page (#86) 2023-10-05 13:06:21 -05:00
cuom1999
c64baad181 Fix xss 2023-10-05 01:09:09 -05:00
Bao Le
067214b587
Markdown Editor (#85) 2023-10-02 13:30:46 -05:00
cuom1999
21905fd1db Fix local var error 2023-09-29 00:37:28 -05:00
cuom1999
0e1a3992eb Comment page speed 2023-09-28 18:23:39 -05:00
cuom1999
cbcbbc5277 Add chunk upload dir to settings 2023-09-27 18:07:24 -05:00
cuom1999
c535ae4415 Fix bug for <a> without href 2023-09-26 13:05:00 -05:00
cuom1999
7d517e1a7d Render mathjax when loading more comments 2023-09-26 01:11:39 -05:00
cuom1999
66bf42cb61 Some hacky chat css 2023-09-25 03:50:30 -05:00
cuom1999
555191009c Handle special case for loading bar 2023-09-25 02:16:51 -05:00
cuom1999
1cbac6fb1c Add darkmode 2023-09-25 02:13:49 -05:00
Phuoc Anh Kha Le
b417c08bfe
Update chat UI(#84) 2023-09-25 02:12:06 -05:00
cuom1999
db37cb4c40 Align center logo 2023-09-21 12:25:02 -05:00
Van Duc Le
caf9fc15fd
Update Logo (#83) 2023-09-21 12:11:25 -05:00
cuom1999
8a73a8ff78 Refactor navbar 2023-09-20 01:39:14 -05:00
Van Duc Le
a2243ca668
New NavBar + Theme (#82) 2023-09-19 23:01:22 -05:00
cuom1999
ad278f58a9 Make public scoreboard better 2023-09-17 00:44:07 -05:00
cuom1999
3f72466e3d Fix css 2023-09-17 00:27:32 -05:00
cuom1999
d6410d5acf Add trans 2023-09-17 00:23:59 -05:00
cuom1999
34da746408 Make left sidebar item become <a> 2023-09-17 00:18:30 -05:00
cuom1999
8bb3812f97 Add public scoreboard option 2023-09-16 23:55:24 -05:00
cuom1999
f3bcc25eb0 Update darkmode 2023-09-14 15:28:29 -05:00
cuom1999
9a1381c2dc Fix problem accessibility 2023-09-12 15:14:06 -05:00
cuom1999
7e4784ea0e Add check for username in import user 2023-09-10 13:44:04 -05:00
cuom1999
32fbdb4530 Standardize css 2023-09-08 13:14:09 -05:00
cuom1999
a74056f101 Update darkmode 2023-09-08 11:24:59 -05:00
Võ Trung Hoàng Hưng
3542d6ba64
new contest ui (#80) 2023-09-08 11:22:57 -05:00
cuom1999
6c64e42322 Darkmode + some small color change 2023-09-05 20:00:09 -05:00
cuom1999
0b5afc96e1 Fix copy button css 2023-09-05 19:47:37 -05:00
cuom1999
c6a268114a Copy button css 2023-09-05 19:39:30 -05:00
Van Duc Le
a5bad300b8
Edit theme 2023-09-05 19:30:05 -05:00
cuom1999
5f80859022 Add custom css background 2023-09-01 19:42:58 -05:00
cuom1999
036509c47f More css 2023-09-01 18:52:25 -05:00
cuom1999
ef47461ee9 Make loading bar better 2023-09-01 18:23:23 -05:00
cuom1999
4401fa7376 Simplify nav user span 2023-09-01 18:20:10 -05:00
cuom1999
41ba0894ac Add loading bar 2023-09-01 18:09:30 -05:00
cuom1999
9a89c5a15a Make chat ava square 2023-09-01 17:51:40 -05:00
Tran Trong Nghia
345e9985e3
Beautify README.md 2023-09-01 21:34:11 +07:00
Tran Trong Nghia
fa21cde2c9
Update README.md
Fix typo
2023-09-01 21:32:41 +07:00
Tran Trong Nghia
b7c6d45b80
Update README.md
Profile Images configuration
2023-09-01 21:31:59 +07:00
Tran Trong Nghia
120cc3c06d
Update README.md
Profile images nginx guide update.
2023-09-01 21:31:24 +07:00
cuom1999
1f03106766 Add ultimate format 2023-08-31 19:35:56 -05:00
cuom1999
b03836715f Fix setting 2023-08-30 18:48:04 -05:00
cuom1999
abbe5f15e1 Add meta address key to setting 2023-08-30 18:46:47 -05:00
cuom1999
1473118c5a More css fix for chat 2023-08-30 13:19:51 -05:00
cuom1999
1749e64802 Fix chat css 2023-08-30 13:07:57 -05:00
Nguyễn Văn Thắng
3cd95e9349
Fixing Hidden-content feature for Upcoming contests (#77) 2023-08-30 00:13:34 -05:00
cuom1999
944d3a733e More chat ui 2023-08-29 21:50:33 -05:00
cuom1999
accf586413 Add search to internal problem 2023-08-29 18:52:24 -05:00
cuom1999
20a3a61206 Merge branch 'master' of https://github.com/LQDJudge/online-judge 2023-08-29 18:36:07 -05:00
cuom1999
00113848c8 Modify Chat UI 2023-08-29 18:36:01 -05:00
Thịnh Nguyễn
f22412b827
add border radius first submission row (#78) 2023-08-29 12:44:26 -05:00
cuom1999
9f0213865d Move chat template out of compress 2023-08-28 14:35:44 -05:00
cuom1999
2854ac97e9 Make chat faster 2023-08-28 14:20:35 -05:00
cuom1999
f11d9b4b53 Fix ticket ui on mobile 2023-08-26 12:38:50 -05:00
cuom1999
3ff608e4ff Add password to email change form 2023-08-25 23:50:03 -05:00
cuom1999
9a825225dd Small UI improvement 2023-08-25 18:38:19 -05:00
cuom1999
8f046c59c1 Drop output_prefix_override and use show_testcases 2023-08-25 18:12:53 -05:00
cuom1999
97d0239963 Add duplication check for contest edit in group 2023-08-25 17:34:33 -05:00
cuom1999
0da2098bbe Fix datetimepicker 2023-08-25 16:51:32 -05:00
cuom1999
d34fe19754 Check for email exists during verification 2023-08-25 16:04:07 -05:00
cuom1999
20a8f29cd6 Send html for email change 2023-08-25 15:58:59 -05:00
cuom1999
0d3ebaba47 Add trans 2023-08-25 15:47:20 -05:00
cuom1999
b8bee7e63d Add trans 2023-08-25 15:44:27 -05:00
cuom1999
af5bee5147 Update emails 2023-08-25 15:36:38 -05:00
cuom1999
164a712902 Small UI improvements 2023-08-24 11:10:39 -05:00
cuom1999
37e5e6a3b3 Fix chat user search 2023-08-24 10:37:04 -05:00
cuom1999
47d3811aa5 Fix another bug for user search 2023-08-24 00:02:02 -05:00
cuom1999
c083ba5a3c Make edit profile better on mobile 2023-08-23 23:30:17 -05:00
cuom1999
532137e54c Fix image url in user search 2023-08-23 23:14:53 -05:00
cuom1999
fdbfa01f6b Change loading gif to transparent 2023-08-23 22:21:05 -05:00
cuom1999
57136d9652 Add profile image 2023-08-23 22:14:09 -05:00
cuom1999
a22afe0c57 Make change user permission stricter 2023-08-21 23:21:25 -05:00
cuom1999
105b7e3c75 Remove debug line 2023-08-21 18:03:39 -05:00
cuom1999
c459226604 Add AMQP websocket 2023-08-21 17:27:21 -05:00
Bao Le
752d21b500
Modify left column (#75) 2023-08-17 09:22:40 -05:00
cuom1999
5de5e278ee Fix emoji bar position 2023-08-14 16:21:38 -05:00
Võ Trung Hoàng Hưng
807ba554ca
Fix emoji bar in chat (#76) 2023-08-14 12:39:41 -05:00
cuom1999
68e705404e Only allow group contest when cloning 2023-08-14 09:10:28 -05:00
yucyle
8f42885482
Remove group adding in edit profile 2023-08-08 17:30:47 +07:00
cuom1999
f9bdc75176 Fix bug 2023-08-07 20:36:28 +07:00
cuom1999
7ac43188a4 Fix bug 2023-08-07 20:33:49 +07:00
cuom1999
dfae9607fe Allow contest editor to view submissions in contest 2023-08-07 20:31:37 +07:00
cuom1999
9a889d158c Fix trans 2023-08-07 12:42:12 +07:00
cuom1999
a75cd01fe2 Fix interactive judge bug 2023-08-07 00:21:35 +07:00
Phuoc Dinh Le
d1e66228fc
Update README.md 2023-08-06 19:45:05 +07:00
Phuoc Dinh Le
5bd904bac1
Update README.md 2023-08-06 19:43:58 +07:00
Bao Le
d45bdbc408
Remove "From <group>" in user about 2023-08-06 19:06:47 +07:00
Zang
d5c492d96b Fix name 'datetime' is not defined 2023-08-06 00:23:38 +07:00
cuom1999
abe5b5eb92 Simplify submission admin 2023-08-04 20:19:42 +07:00
cuom1999
4ceae6d066 Fix pagevote and bookmark bug for new objects 2023-08-03 23:19:45 +07:00
cuom1999
36e27321f7 Migrate pagevote and bookmark to use content_type 2023-08-03 16:04:39 +07:00
cuom1999
64495be799 Add IOI signature to UI 2023-08-01 12:26:15 +07:00
cuom1999
220a7e7237 Add submission to queue if judge disconnect 2023-07-31 22:27:26 +07:00
cuom1999
1e88e73082 Add option for bridge to create new judge 2023-07-28 11:21:34 +07:00
cuom1999
daee631ef6 Fix trans + update DB query 2023-07-26 23:52:34 +07:00
cuom1999
9019bcb990 Move function out of compressor 2023-07-26 00:19:01 +07:00
cuom1999
8c7bbd4b39 Move function out of compressor 2023-07-26 00:12:54 +07:00
cuom1999
ec7f5a2047 Move function out of compressor 2023-07-25 22:49:50 +07:00
cuom1999
2116dda86b Make join button block 2023-07-13 22:07:25 +07:00
cuom1999
0b27c9da23 Fix some css 2023-07-07 00:54:52 +07:00
cuom1999
1ca0d51f67 Format 2023-07-06 22:39:16 +07:00
cuom1999
1595063463 Fix organization submissions 2023-07-06 22:37:43 +07:00
cuom1999
a02814621e Fix chat input 2023-06-02 12:45:30 +07:00
cuom1999
8cfc58ad91 Fix show types bug for problem feed 2023-05-31 16:04:47 +07:00
cuom1999
9070036978 Fix actionbar style 2023-05-27 11:06:40 +07:00
cuom1999
2291d6bbb8 Revert "Try optimizing batch rejudge"
This reverts commit fbd1d865fa.
2023-05-27 09:00:42 +07:00
cuom1999
fbd1d865fa Try optimizing batch rejudge 2023-05-27 08:33:19 +07:00
cuom1999
f65238ba42 Remove sleep 2023-05-24 18:36:15 +07:00
cuom1999
1cbd4dee49 Fix bug 2023-05-22 23:27:04 +07:00
cuom1999
57ded6ff5e Fix some bugs for new comment 2023-05-22 23:11:40 +07:00
cuom1999
b5816bbcd6 Change make_style.sh to 755 2023-05-22 20:52:55 +07:00
Dung T.Bui
1056a470b0
Change comment style (#70) 2023-05-22 20:52:18 +07:00
cuom1999
d80ec962a5 Try slowing down rejudge enqueue 2023-05-20 10:06:00 +09:00
cuom1999
966e8c9db5 Fix subdomain download files 2023-05-15 00:01:53 -05:00
cuom1999
ad974530d5 Fix contest submission bug when modifying from admin page 2023-05-07 21:37:22 -05:00
cuom1999
167fa1ad66 Remove unused cache 2023-05-06 23:12:09 -05:00
cuom1999
30fb38f52e Show error in output instead of exception 2023-04-28 20:53:54 -05:00
cuom1999
7565d6ff01 Fix edit button in editorial page 2023-04-28 11:42:03 -05:00
cuom1999
d4db6bc0be Add pagination to org users 2023-04-26 04:02:26 -05:00
cuom1999
998182f65e Fix bug 2023-04-26 03:50:47 -05:00
cuom1999
bfedef666e Add trans 2023-04-26 03:41:45 -05:00
cuom1999
a7c555c853 Fix feed scroll on mobile 2023-04-26 03:38:34 -05:00
cuom1999
00f2ea2648 Make actionbar comment smoother 2023-04-26 03:14:43 -05:00
cuom1999
da07a9a9a4 Fix stat out-of-list 2023-04-24 11:56:10 -05:00
cuom1999
9cdcfa54d6 Filter hidden comments 2023-04-10 20:40:41 -05:00
cuom1999
7ef0c47427 Change comment css 2023-04-10 20:25:49 -05:00
cuom1999
f17519fbc4 Merge branch 'master' of https://github.com/LQDJudge/online-judge 2023-04-10 19:44:42 -05:00
cuom1999
9f225157d2 Make comment revision work again 2023-04-10 19:44:33 -05:00
Phuoc Dinh Le
2bd0e41653
Merge pull request #62 from thangitcbg/Bold-Contest-Name
Fix the past contest's title
2023-04-09 13:02:17 -05:00
thangitcbg
c454a3ce83 Fix the past contest's title 2023-04-09 23:50:19 +07:00
cuom1999
d0511f46c0 Fix bug 2023-04-05 17:07:02 -05:00
cuom1999
57a6233779 Add cache wrapper 2023-04-05 12:49:23 -05:00
cuom1999
9d645841ae Fix task bug 2023-04-05 12:24:29 -05:00
cuom1999
ff91a1e5fa Use file url for SCAT 2023-04-04 22:41:04 -05:00
cuom1999
9ce925fd6a Fix trans 2023-04-03 11:55:13 -05:00
cuom1999
eb36ed4f79 Fix loaddata 2023-04-03 11:17:55 -05:00
cuom1999
d4cb680199 Fix bleach markdown 2023-04-03 02:27:35 -05:00
cuom1999
d36a15d02d Fix problem search pagination 2023-04-02 17:57:32 -05:00
cuom1999
90ea6a7844 Fix scat bug 2023-03-26 03:00:05 -05:00
cuom1999
ef813f5dbe Fix css bug 2023-03-26 01:43:02 -05:00
cuom1999
8952683505 Support output-only SCAT 2023-03-25 22:51:43 -05:00
cuom1999
a07b147ea6 Add problem action filter for contest 2023-03-25 22:11:56 -05:00
cuom1999
c8d4a57270 Fix css bug 2023-03-25 02:24:51 -05:00
cuom1999
2b0058ca4c Fix bug when there's no data 2023-03-17 22:24:23 -05:00
cuom1999
4792134990 Add a bit delay to avoid race 2023-03-17 15:15:39 -05:00
cuom1999
56c5b3dd3c Disable ace editor for output-only problem 2023-03-17 13:41:03 -05:00
cuom1999
1af44ac9fa Fix resolver when there's no subtask 2023-03-17 11:53:19 -05:00
cuom1999
5200a7c2ad Revert prev commits 2023-03-16 14:56:27 -05:00
cuom1999
d58ef657e0 Add one more decimal digit to status cell 2023-03-16 14:47:15 -05:00
cuom1999
79d6397fd7 Add one more decimal digit to status cell 2023-03-16 14:41:44 -05:00
cuom1999
954e7a15ea Add one more decimal digit to status cell 2023-03-16 14:35:38 -05:00
cuom1999
b44b6e58bd Add one more decimal to status table 2023-03-16 14:33:05 -05:00
cuom1999
99c9475ff7 Don't remove file after judging on output-only 2023-03-16 11:09:03 -05:00
cuom1999
a62df01ecf Fix mute/delete chat for prev pages 2023-03-15 23:44:30 -05:00
cuom1999
03cd608b9d Add chat mute 2023-03-15 23:08:13 -05:00
cuom1999
533b5aa7c8 Fix url bug 2023-03-13 01:23:06 -05:00
cuom1999
580a4019c7 Another fix pdf_description save 2023-03-12 03:25:23 -05:00
cuom1999
a4b06a354c Fix pdf_description save 2023-03-12 01:57:50 -06:00
cuom1999
195450ebc3 Add output-only on UI 2023-03-09 22:31:55 -06:00
cuom1999
bdae79eeda Revert prev commit 2023-03-08 21:31:41 -06:00
cuom1999
28923f755b Fix prev commit 2023-03-08 21:30:43 -06:00
cuom1999
f373ecbc84 Optimize pdf description 2023-03-08 21:25:54 -06:00
cuom1999
8b814640ea Fix chat index 2023-03-08 01:25:54 -06:00
cuom1999
9b5f0c0969 Optimize actionbar comment 2023-03-07 23:43:26 -06:00
cuom1999
d2f261acfe Add submission indexes to optimize user_problems 2023-03-07 19:59:12 -06:00
cuom1999
ec2c2ccf13 Fix bug 2023-03-07 18:30:47 -06:00
cuom1999
2822f7acaf Fix previous commit 2023-03-07 01:31:55 -06:00
cuom1999
92e2b45ada Add internal speed pages 2023-03-07 01:19:45 -06:00
cuom1999
d5b21935ae Optimize contest ranked submission (DMOJ) 2023-03-06 22:56:31 -06:00
cuom1999
8a6288f8e6 Some css improvements 2023-03-06 15:57:16 -06:00
cuom1999
0475ce21aa Fix small css 2023-03-02 18:50:33 -06:00
cuom1999
5d15cb9bad Some UI improvements 2023-03-02 18:43:15 -06:00
cuom1999
0708eb7bb0 Change icon for friend submissions 2023-03-01 21:50:33 -06:00
cuom1999
e2d3d11591 Fix bug when org is deleted 2023-03-01 21:37:16 -06:00
cuom1999
cf31735e80 Add some shadow 2023-02-28 18:42:40 -06:00
cuom1999
d01f536935 Silent comment revision error 2023-02-23 02:03:13 -06:00
cuom1999
f911345984 Fix prev commit 2023-02-22 17:03:14 -06:00
cuom1999
82a6f910e3 Change pdf description path when code changes 2023-02-22 16:49:30 -06:00
cuom1999
6b2bd7c550 Fix feed paging 2023-02-21 19:00:05 -06:00
cuom1999
de25c2045c Fix problem feed completed problem 2023-02-21 18:20:06 -06:00
cuom1999
603b251511 Fix 404 error for deleted org 2023-02-21 14:20:14 -06:00
cuom1999
9d42119a09 Fix hash 2023-02-21 14:13:37 -06:00
cuom1999
227946db8c Increase comment feed pagesize 2023-02-20 17:56:05 -06:00
cuom1999
4c4cee1a05 Fix migration name 2023-02-20 17:53:25 -06:00
cuom1999
f9207b811b Drop comment page 2023-02-20 17:52:28 -06:00
cuom1999
799ff5f8f8 Infinite scrolling and comment migration 2023-02-20 17:15:13 -06:00
cuom1999
4b558bd656 Fix submission_layout 2023-02-18 20:11:41 -06:00
cuom1999
3f6841932b Some more optimizations 2023-02-18 16:38:47 -06:00
cuom1999
a9dc97a46d Use infinite pagination 2023-02-18 15:12:33 -06:00
cuom1999
212029e755 Fix submission page 2023-02-15 23:19:55 -06:00
cuom1999
56651e0e0c Make submission data consistent 2023-02-15 17:22:37 -06:00
cuom1999
a5df36c476 Optimize submission page 2023-02-15 16:36:33 -06:00
cuom1999
0f3e5edc8c More css 2023-02-14 18:13:25 -06:00
cuom1999
9569b096cc Remove console.log 2023-02-14 18:08:09 -06:00
cuom1999
f656ca69d9 Fix mobile css 2023-02-14 18:05:58 -06:00
cuom1999
426145db5e Update requirement 2023-02-14 17:42:31 -06:00
cuom1999
3de0d7f745 Add freeze option to org contest form 2023-02-14 17:36:45 -06:00
cuom1999
0a5251f533 Update contest submission points when modifying contest 2023-02-14 17:25:20 -06:00
cuom1999
7e8906ae7e Clean up unused url 2023-02-13 21:01:37 -06:00
cuom1999
7b2c4126f9 Use full url instead of path in middleware 2023-02-13 18:00:24 -06:00
cuom1999
5e4b289833 Optimize db query 2023-02-13 17:57:27 -06:00
cuom1999
b95acb6202 Add logging for slow requests 2023-02-13 16:44:04 -06:00
cuom1999
fc852d1bc7 Add friend submissions 2023-02-12 21:35:48 -06:00
cuom1999
3eda48f3ea Fix markdown bug 2023-02-12 18:37:34 -06:00
cuom1999
2f7274b3f0 Fix chat bug 2023-02-12 18:32:31 -06:00
cuom1999
0ac620b9bd Fix css 2023-02-08 22:00:25 -06:00
Phuoc Dinh Le
6bc8b54d94
Merge pull request #59 from zhaospei/master
update contest list filter
2023-02-08 21:14:57 -06:00
zhaospei
e2067d4d18 add hide organization contests list 2023-02-09 02:12:23 +07:00
Bùi Tuấn Dũng
5a4bac2911
Merge branch 'LQDJudge:master' into master 2023-02-09 00:20:53 +07:00
Phuoc Dinh Le
f489287707
Merge pull request #58 from LQDJudge/courseLuzi
Add initial implementation for course
2023-02-08 10:22:37 -06:00
Luzivanlt
b629459b46 add course.py, list.html and create class CourseList 2023-02-08 22:42:09 +07:00
zhaospei
8e6bcd90af fix superuser contest search 2023-02-08 17:22:43 +07:00
cuom1999
2ee279098f Update layout UI 2023-02-07 23:14:48 -06:00
cuom1999
993309d56b Fix final ranking 2023-02-07 22:12:01 -06:00
cuom1999
5f12afdda9 Clean up actionbar 2023-02-07 17:22:37 -06:00
Bùi Tuấn Dũng
ec893149d1
Merge pull request #57 from zhaospei/master
allow blog-box pre-expand-blog click
2023-02-07 18:50:45 +07:00
zhaospei
e0116f9c54 allow blog-box pre-expand-blog click 2023-02-07 18:50:02 +07:00
Phuoc Dinh Le
1e79a53575
Merge pull request #56 from zhaospei/master
fix pre-expand-blog
2023-02-06 11:44:15 -06:00
Bùi Tuấn Dũng
4e9cb329d6
Merge branch 'LQDJudge:master' into master 2023-02-07 00:26:33 +07:00
zhaospei
262514acee fix pre-expand-blog check always true 2023-02-07 00:26:04 +07:00
Bùi Tuấn Dũng
4380117e62
Merge pull request #55 from zhaospei/master
fix show-more display
2023-02-06 23:26:31 +07:00
zhaospei
4e7b8daada fix show-more display 2023-02-06 23:25:50 +07:00
Bùi Tuấn Dũng
88943f40fb
update blog-box border-radius 2023-02-06 00:49:22 +07:00
Phuoc Dinh Le
024e89f2aa
Merge pull request #54 from zhaospei/master
change show-more pre-expand-blog background
2023-02-05 11:42:44 -06:00
zhaospei
66a1f62986 change show-more pre-expand-blog background 2023-02-06 00:40:04 +07:00
Phuoc Dinh Le
b04370f7ce
Merge pull request #53 from zhaospei/master
Change pre-expand-blog style
2023-02-05 10:21:41 -06:00
zhaospei
ef1096c9f3 add show-more comment and add trans 2023-02-05 20:41:18 +07:00
zhaospei
d736381beb update darkmode 2023-02-05 20:22:17 +07:00
zhaospei
38d5c2cab1 show actionbar pre-expand-blog 2023-02-05 20:13:12 +07:00
Bùi Tuấn Dũng
d2405de7dd
fix index shadow pre-expand-blog 2023-02-05 00:25:38 +07:00
cuom1999
131adf3cbb Update darkmode 2023-02-04 10:53:14 -06:00
cuom1999
542595a1cd don't include www in subdomain 2023-02-04 10:34:41 -06:00
Phuoc Dinh Le
d519c41802
Merge pull request #52 from zhaospei/master
Fix pre-expand-blog css
2023-02-04 07:37:46 -06:00
zhaospei
8c135a5396 fix actionbar pre-expand-blog 2023-02-04 20:14:50 +07:00
zhaospei
d59828df1c fix shadow pre-expand-blog 2023-02-04 19:41:31 +07:00
cuom1999
a40e577dd1 Notify users when problem orgs are changed 2023-02-01 20:04:04 -06:00
cuom1999
c269e34873 Fix bug that superuser can't leave group 2023-02-01 19:52:43 -06:00
Phuoc Dinh Le
a03624b73e
Merge pull request #51 from zhaospei/master
add course models
2023-01-31 10:17:55 -06:00
zhaospei
92d64c3a87 fix typo again 2023-01-31 22:56:16 +07:00
zhaospei
037feef8f8 fix typo 2023-01-31 22:54:03 +07:00
zhaospei
51006bc773 add course models 2023-01-31 22:50:52 +07:00
cuom1999
b049f6eace Create option to use subdomain 2023-01-30 23:19:30 -06:00
cuom1999
41837db827 Fix bug 2023-01-30 14:09:03 -06:00
cuom1999
69d23fded4 Add copy clipboard to base 2023-01-28 23:56:52 -06:00
cuom1999
e46775301c Add cache for related problems 2023-01-28 10:46:17 -06:00
cuom1999
c3f2930d4a Fix bug 2023-01-28 02:20:53 -06:00
cuom1999
e0ce058989 Add condition for related problems 2023-01-27 19:16:48 -06:00
cuom1999
ca13ee4e8d Add related problems 2023-01-27 19:15:37 -06:00
cuom1999
03455fca2c Optimize a query 2023-01-27 17:58:44 -06:00
cuom1999
bd05ba6b78 Simplify logic 2023-01-27 17:51:38 -06:00
cuom1999
64a5c73c21 Add trans 2023-01-27 17:38:05 -06:00
cuom1999
f33dd38269 Add better responses for subdomains 2023-01-27 17:25:41 -06:00
cuom1999
dfb0921ff7 Update black hook 2023-01-27 17:18:43 -06:00
cuom1999
d3c6c1b4f6 Add trans 2023-01-27 17:17:21 -06:00
cuom1999
c5c97f0e58 Add black hook 2023-01-27 17:13:02 -06:00
cuom1999
52f1e77fe1 Reformat html files 2023-01-27 17:11:10 -06:00
cuom1999
9a208ca108 Fix submission page bug in subdomain 2023-01-27 16:52:35 -06:00
cuom1999
65eb49a840 Fix ticket page 2023-01-27 16:26:28 -06:00
cuom1999
840209b2cb Add comment count to actionbar 2023-01-26 11:53:24 -06:00
cuom1999
08fae0d0dc Add slug validator for org 2023-01-25 13:13:42 -06:00
cuom1999
3791d2e90f Fix subdomain login 2023-01-23 21:08:11 -06:00
cuom1999
15913e51f3 Back to old query 2023-01-23 21:00:11 -06:00
cuom1999
2270730407 Optimize a query 2023-01-23 20:55:07 -06:00
cuom1999
dc243dc136 Fix a tag 2023-01-23 20:46:17 -06:00
cuom1999
1628e63084 Initial subdomain implementation 2023-01-23 20:36:44 -06:00
cuom1999
dea24f7f71 Fix editor bug 2023-01-22 19:18:14 -06:00
cuom1999
2259596ef7 Fix blog edit 2023-01-21 12:51:45 -06:00
Tran Trong Nghia
b9d0c9b0f9
Update README.md 2023-01-18 11:14:25 +07:00
Tran Trong Nghia
d40b599a9c
Update views.py
Better name for the chatbox
2023-01-18 11:04:52 +07:00
Tran Trong Nghia
7228146c95 Reverted the color
Màu nhìn có vấn đề nên cần revert lại change, CSS sẽ được chỉnh sửa trong thời gian kế tiếp
2023-01-17 22:34:32 +07:00
Tran Trong Nghia
2803ed5907 Color changes
Nên đặt thêm background color cho WA và CE
2023-01-17 22:28:07 +07:00
Tran Trong Nghia
b9ae053927
Update init.yml
Only black will be used
2023-01-17 11:37:04 +07:00
Phuoc Dinh Le
2444ca8ecb
Merge pull request #50 from emladevops/master
Ace theme font changes
2023-01-16 09:30:42 -06:00
Tran Trong Nghia
5fa8249030
Lint runner config 2023-01-16 22:23:57 +07:00
Tran Trong Nghia
563b42031f
Update widget.css 2023-01-16 11:58:51 +07:00
Tran Trong Nghia
ed774098db
Add fonts 2023-01-16 11:49:03 +07:00
Phuoc Dinh Le
24dbcd8824
Merge pull request #49 from emladevops/master
Update fonts
2023-01-14 10:02:58 -06:00
Tran Trong Nghia
7733ce04e5
Update content-description.scss
Fonts for content-description.scss
2023-01-14 13:46:46 +07:00
Tran Trong Nghia
d085b54f82
Update chatbox.scss
Fonts for chatbox
2023-01-14 13:46:10 +07:00
Tran Trong Nghia
cbbe0c4a0a
Update base.scss
Fonts for base.html
2023-01-14 13:45:38 +07:00
Tran Trong Nghia
91e3080240
Add Google Font to html 2023-01-14 13:44:24 +07:00
cuom1999
f8d08c477c Update font family order 2023-01-13 18:14:17 -06:00
cuom1999
8bf07d7722 Fix admin css 2023-01-12 23:21:29 -06:00
cuom1999
ea9758772d Merge branch 'master' of https://github.com/LQDJudge/online-judge 2023-01-11 22:07:41 -06:00
cuom1999
d344282425 Fix css on darkmode 2023-01-11 22:07:35 -06:00
Phuoc Dinh Le
bcddc2fb13
Merge pull request #48 from emladevops/patch-5
Update README.md
2023-01-07 22:32:08 -06:00
cuom1999
e09c28031b Fix css 2023-01-07 22:16:28 -06:00
Tran Trong Nghia
5f5e5b654c
Update README.md 2023-01-08 07:44:11 +07:00
cuom1999
ceb48b5bbe Fix organization problem bug while in contest 2023-01-07 03:18:16 -06:00
cuom1999
619388a36a Fix select bug 2023-01-07 02:46:21 -06:00
cuom1999
bb78b743dd Fix bug 2023-01-04 15:57:10 -06:00
cuom1999
48adf7e6b8 Refactor contest views 2023-01-04 15:21:03 -06:00
cuom1999
d42cf64abd Rescore after changing time 2023-01-04 15:12:41 -06:00
cuom1999
a512694976 Refactor submission views 2023-01-04 15:11:00 -06:00
cuom1999
69692504aa Display submission list for hidden-subtasks contest 2023-01-02 22:38:25 -06:00
cuom1999
13f1f03462 Fix navigation bug 2023-01-02 18:23:43 -06:00
cuom1999
6c1d0c7672 Fix bug 2023-01-02 18:18:03 -06:00
cuom1999
07d5ad2216 Make ioi16 related files cleaner 2023-01-02 17:22:45 -06:00
cuom1999
e10a8aca5c Fix ioi16 2022-12-29 00:20:50 -06:00
cuom1999
8f15fc8f65 Fix ioi16 bug 2022-12-29 00:00:27 -06:00
cuom1999
cdb7834de5 Fix ioi16 2022-12-28 23:16:17 -06:00
cuom1999
45ada7995a Hide submission time for frozen sub 2022-12-28 23:05:44 -06:00
cuom1999
5f7199669c Fix ioi16 2022-12-28 22:50:26 -06:00
cuom1999
02e2539606 Update resolver 2022-12-28 21:27:00 -06:00
cuom1999
39586c5d64 Fix resolver display 2022-12-28 19:16:38 -06:00
cuom1999
46260b6592 Fix resolver status 2022-12-28 18:59:12 -06:00
cuom1999
7ef27a1412 Fix resolver 2022-12-28 18:37:40 -06:00
cuom1999
0650223d10 Fix contest display on home 2022-12-28 14:59:15 -06:00
cuom1999
309b6298e2 Add final ranking for superuser (ioi16 only) 2022-12-28 14:38:32 -06:00
cuom1999
6635c3ee99 Allow admin to view submissions in ioi16 2022-12-28 12:32:58 -06:00
cuom1999
52cff64981 Merge branch 'master' of https://github.com/LQDJudge/online-judge 2022-12-28 12:29:08 -06:00
cuom1999
a9cf695786 Limit submissions view in ioi16 2022-12-28 12:29:02 -06:00
Phuoc Dinh Le
fac462d99d
Merge pull request #46 from emladevops/patch-3
Update README.md
2022-12-28 11:43:55 -06:00
cuom1999
6746d5769a Change icon 2022-12-28 11:35:19 -06:00
cuom1999
62cb2e96d2 Change logo 2022-12-28 11:15:08 -06:00
Tran Trong Nghia
6faa39affd
Images hosted on GitHub 2022-12-28 21:01:20 +07:00
Tran Trong Nghia
e8d5f59267
Fancier README.md
Better images
2022-12-28 20:56:58 +07:00
Tran Trong Nghia
17637e7543
Update README.md
#45 Fixed
2022-12-28 18:05:14 +07:00
cuom1999
7d5efa60c7 Fix bridge (https://github.com/DMOJ/online-judge/pull/2080) 2022-12-27 13:19:02 -06:00
cuom1999
304fa0e0f3 Remove test_site 2022-12-27 12:39:58 -06:00
Phuoc Dinh Le
58e4b16a90
Merge pull request #43 from emladevops/master
Remove newsletter and README.md update
2022-12-27 12:27:21 -06:00
cuom1999
b9fc7bcb06 Merge branch 'master' of https://github.com/LQDJudge/online-judge 2022-12-27 12:23:02 -06:00
cuom1999
4bf1c79515 Fix error in make_style 2022-12-27 12:22:53 -06:00
Tran Trong Nghia
ca8c095e96
Merge branch 'LQDJudge:master' into master 2022-12-26 18:12:38 +07:00
Tran Trong Nghia
ad85942c33 README.md changes and newsletter removal 2022-12-26 18:11:02 +07:00
Phuoc Dinh Le
faaf4dac88
Merge pull request #42 from emladevops/patch-1
Update registration_form.html
2022-12-26 02:39:05 -06:00
Tran Trong Nghia
5665bb1f34
Update registration_form.html
Mình đã bỏ phần Notify newsletter, vì trong pull request trước, phần newsletter khi đăng kí đã được lược bỏ
2022-12-25 18:03:14 +07:00
Tran Trong Nghia
62a6f9f64c
Merge pull request #1 from emladevops/emladevops-patch-1
Update registration_form.html
2022-12-25 18:02:03 +07:00
Tran Trong Nghia
2295ff6290
Update registration_form.html
Trong commit trước, mình đã bỏ tính năng newsletter, nên chỉnh thêm cả HTML vì nút không có chức năng gì nữa.
2022-12-25 18:01:43 +07:00
Phuoc Dinh Le
581c760243
Merge pull request #41 from emladevops/master
Fix lỗi newsletter
2022-12-25 02:06:32 -06:00
Tran Trong Nghia
23ee47bb26
Fix lỗi newsletter
Mình đã fix lỗi SQL khi đăng ký
2022-12-25 13:44:07 +07:00
cuom1999
51ecaba048 Make mobile fontsize bigger 2022-12-24 22:22:05 -06:00
cuom1999
9f384387b9 Add darkmode svg css to html 2022-12-23 03:16:33 -06:00
cuom1999
1d45f31690 Add darkmode for editor icons 2022-12-23 03:13:12 -06:00
cuom1999
5b78ea65e4 Reformat 2022-12-23 02:21:14 -06:00
cuom1999
a5f6d32977 Fix comment edit 2022-12-23 02:20:53 -06:00
cuom1999
51707df439 Merge branch 'master' of https://github.com/LQDJudge/online-judge 2022-12-23 02:06:55 -06:00
cuom1999
bb58054b77 Fix comment reply 2022-12-23 02:06:44 -06:00
Phuoc Dinh Le
2520b3ba54
Merge pull request #38 from emladevops/master
Update README.md
2022-12-21 00:35:05 -06:00
Tran Trong Nghia
0c8319c138
Update README.md
The version for Python is Python3, as stated in the beginning of the README.md [![OS](https://img.shields.io/badge/Ubuntu-16.04%20%7C%2018.04%20%7C%2020.04-brightgreen)]

Some users may not have python-is-python3 set up on their server, so python3 is a better approach here.
2022-12-21 11:53:32 +07:00
Tran Trong Nghia
78ab7b752a
Update README.md
Small typo, the source file is ./make_style.sh, not ./make_style
2022-12-21 11:48:00 +07:00
Phuoc Dinh Le
52dea1a3bd
Merge pull request #37 from emladevops/master
Fix problem pdf view
2022-12-20 15:27:30 -06:00
cuom1999
56a9b5d6a0 Hide submissions for ioi16 2022-12-20 15:24:34 -06:00
cuom1999
9522e17acd Fix css bug 2022-12-20 12:21:23 -06:00
Tran Trong Nghia
465dd08043
Update problem.py
Fixed, if you follow the docs of DMOJ to setup nginx and supervisor, Pdf view won't work
2022-12-20 21:14:19 +07:00
cuom1999
36007e86ed Add backend for resolver 2022-12-20 05:27:04 -06:00
cuom1999
793ccb52fd Add trans 2022-12-20 02:26:15 -06:00
cuom1999
6bd5bb290f Add frozen subtasks 2022-12-20 02:24:24 -06:00
cuom1999
bdd30f94b7 Fix problem data bug 2022-12-20 00:02:28 -06:00
Phuoc Dinh Le
e4f46146cc
Merge pull request #36 from zhaospei/master
Add resolver page
2022-12-19 21:58:07 -06:00
Bui Tuan Dung
551c9c3503 modify resolver link 2022-12-19 08:01:02 +07:00
Bui Tuan Dung
cf431b8fab add resolver link 2022-12-19 07:22:08 +07:00
Bui Tuan Dung
fe9b899232 add resolver page 2022-12-19 06:53:28 +07:00
cuom1999
97a56145b2 Use css for darkmode 2022-12-18 03:31:31 -06:00
cuom1999
de875bd384 Fix problem data bug 2022-12-16 13:02:40 -06:00
cuom1999
be83844d28 More reloadable links 2022-12-16 05:11:24 -06:00
cuom1999
87ff346d7f Add reload for some more links 2022-12-16 04:53:37 -06:00
cuom1999
813e7d92ce Add cache for problem feed 2022-12-16 04:32:00 -06:00
cuom1999
613c002d5a Clean up todo 2022-12-16 03:49:42 -06:00
cuom1999
de15d61ebb Fix iphone bug 2022-12-08 20:09:09 -06:00
cuom1999
6bd8b9c3f1 More css for prev commit 2022-12-08 19:40:57 -06:00
cuom1999
cff62ae98f Make src code better for mobile 2022-12-08 19:30:15 -06:00
cuom1999
c3e0136a64 Update trans 2022-12-08 19:22:12 -06:00
cuom1999
073138986c Not display footer in chat 2022-12-07 13:43:49 -06:00
Phuoc Dinh Le
c368756032
Merge pull request #34 from zhaospei/master
fix cant change language on chat page
2022-12-07 13:41:46 -06:00
Bui Tuan Dung
38ada59938 fix cant change language on chat page 2022-12-07 14:15:26 +07:00
cuom1999
30af55054f Change z-index of featherlight 2022-11-27 23:08:15 -06:00
cuom1999
4c95aba764 Add contest title 2022-11-27 18:13:40 -06:00
cuom1999
6b476ce2f7 Fix popper bug 2022-11-27 15:51:57 -06:00
cuom1999
9c6cd01ec2 Move join button 2022-11-27 01:23:04 -06:00
cuom1999
756023a097 Rewrite contest UI 2022-11-27 01:03:38 -06:00
cuom1999
6c9551e089 Fix blog box bug 2022-11-26 19:15:01 -06:00
cuom1999
602c245645 Fix admin page bug 2022-11-26 11:01:39 -06:00
cuom1999
3df9b6ab79 Change mathjax config file name 2022-11-25 12:52:49 -06:00
cuom1999
51a8400fa5 Add temporary config for mathjax 2022-11-25 12:45:44 -06:00
cuom1999
756e4749c9 Fix some css bugs: 2022-11-25 01:50:32 -06:00
cuom1999
7a74f940fa Fix bug 2022-11-25 00:41:13 -06:00
cuom1999
e35f91ca2d Add ajax reload to org home 2022-11-25 00:27:39 -06:00
cuom1999
392a39c43e Add preloader for 3 col 2022-11-25 00:10:57 -06:00
cuom1999
a79f18a748 Make three col pages only reload middle-right content 2022-11-24 23:28:24 -06:00
cuom1999
c2716348f3 Make contest banner a bit better 2022-11-22 19:57:43 -06:00
cuom1999
f21de0f1e1 Remove css 2022-11-22 19:25:55 -06:00
cuom1999
ccb78128b0 Remove unnecessary css 2022-11-22 19:11:16 -06:00
cuom1999
dbb189444d Add new IOI format 2022-11-21 22:05:35 -06:00
cuom1999
af8ab310ce Fix bookmark page 2022-11-21 00:34:39 -06:00
cuom1999
181127d632 Fix school bug ranking 2022-11-20 20:27:18 -06:00
cuom1999
152913f916 Fix runtime page display 2022-11-20 12:34:47 -06:00
cuom1999
26baaa113b Fix bug 2022-11-20 12:28:31 -06:00
cuom1999
8b0d5e8f47 Add trans 2022-11-19 21:43:34 -06:00
cuom1999
01c0365208 Change how batch scoring works 2022-11-19 21:41:43 -06:00
cuom1999
65dea68be3 Update settings 2022-11-19 20:00:31 -06:00
cuom1999
76c22c074b Fix css bug 2022-11-19 19:58:23 -06:00
cuom1999
5b3238ca55 Fix bug 2022-11-19 17:40:58 -06:00
cuom1999
b6a09c9ebb Update actionbar share and comment 2022-11-19 17:30:07 -06:00
Phuoc Dinh Le
a5c045986b
Merge pull request #32 from LQDJudge/new_actionBar
add function share , comment button and write comments
2022-11-19 15:07:49 -06:00
DELL
e3040d99fa add function share , comment button and write comments 2022-11-19 23:02:45 +07:00
cuom1999
e229b53226 Add school to ranking 2022-11-18 18:14:12 -06:00
cuom1999
a35ea0f6d5 Add contest freeze 2022-11-18 16:59:58 -06:00
cuom1999
2c39774ff7 Format and add trans 2022-11-17 16:11:47 -06:00
Phuoc Dinh Le
bba7a761ac
Merge pull request #31 from LQDJudge/pagevote
add bookmark pages
2022-11-17 15:50:25 -06:00
Zhao-Linux
a447cdfcf1 fix typo 2022-11-18 04:45:27 +07:00
Zhao-Linux
6e72c08ef4 add bookmarks page 2022-11-18 04:21:32 +07:00
Zhao-Linux
8108967959 Merge branch 'master' of https://github.com/LQDJudge/online-judge into pagevote 2022-11-18 02:17:56 +07:00
Zhao-Linux
d0e4d9512c add bookmark model 2022-11-18 02:17:45 +07:00
cuom1999
56982806e4 Add actionbar for group homepage 2022-11-17 13:10:19 -06:00
Phuoc Dinh Le
36511d1a13
Merge pull request #30 from LQDJudge/pagevote
fix bugs
2022-11-17 13:08:01 -06:00
Zhao-Linux
03db2db899 fix pagevote score not true 2022-11-17 16:52:46 +07:00
Zhao-Linux
7174fc1066 fix bugs 2022-11-17 16:09:28 +07:00
cuom1999
b75a52fe74 Add UI for action bar 2022-11-16 18:48:32 -06:00
Phuoc Dinh Le
f9e9df6056
Merge pull request #29 from LQDJudge/pagevote
add pages vote
2022-11-16 12:04:49 -06:00
Zhao-Linux
d86f3d8f3e add pages vote 2022-11-16 22:43:03 +07:00
cuom1999
3ee2f2afb0 Lazy load outside logos 2022-11-13 22:00:54 -06:00
cuom1999
710fae5fe3 Upgrade to MathJax 3 2022-11-13 21:42:27 -06:00
cuom1999
6bd15ded9c Try moving darkmode up 2022-11-13 20:43:51 -06:00
cuom1999
c3d86ae28a Test darkmode 2022-11-13 20:39:18 -06:00
cuom1999
187206ba2b Update setting 2022-11-13 19:01:34 -06:00
cuom1999
fb3ceb51ac Fix js bug 2022-11-12 21:32:42 -06:00
cuom1999
b91fdb5f4c Fix infinite loop 2022-11-12 21:28:36 -06:00
cuom1999
affcca39f1 Fix login to participate button 2022-11-12 02:11:28 -06:00
cuom1999
0998f8c782 Update mathjax config 2022-11-10 16:24:32 -06:00
cuom1999
ff45665bbc Make markdown latex better 2022-11-10 15:22:17 -06:00
cuom1999
d02d195c94 Fix css 2022-11-09 21:55:02 -06:00
cuom1999
ed5893a097 Make english dropdown a bit wider 2022-11-09 21:22:51 -06:00
cuom1999
98f8f52bde Refactor navbar 2022-11-09 20:57:50 -06:00
cuom1999
7150718a51 Lazy load iframe 2022-11-09 14:53:55 -06:00
cuom1999
f6b16a30ac Make contest header bigger 2022-11-09 14:45:46 -06:00
cuom1999
30417e1cde Change style of user table 2022-11-09 14:41:08 -06:00
cuom1999
924f209311 Fix markdown bug 2022-11-08 15:11:45 -06:00
cuom1999
139cb93811 Make org logo lazy load 2022-11-07 16:19:26 -06:00
cuom1999
fddde73583 Make some markdown lazy load image 2022-11-07 16:11:35 -06:00
cuom1999
b2950cfcdc Fix comment UI on mobile 2022-11-07 16:00:38 -06:00
cuom1999
149e1b5507 Make VNese default 2022-11-07 15:48:05 -06:00
cuom1999
4b0de87f1e Fix problem and contest clone 2022-11-07 15:39:10 -06:00
cuom1999
ac2fd3dfe0 Update participation when editing contest from UI 2022-11-04 16:56:26 -05:00
cuom1999
c86e34fba1 Fix print 2022-11-03 22:38:39 -05:00
cuom1999
78de6e2b34 Increase reading font size 2022-11-03 22:16:25 -05:00
cuom1999
e2ef2bdb21 Fix bug: 2022-11-03 01:47:20 -05:00
cuom1999
c0ffbb3ada Remove distinct 2022-11-02 17:36:40 -05:00
cuom1999
796f135530 Fix bug 2022-11-01 13:52:14 -05:00
cuom1999
58486846c1 Fix bug 2022-11-01 13:45:58 -05:00
cuom1999
16f23b6e01 Fix bug 2022-11-01 13:40:18 -05:00
cuom1999
6c170cccdd Add markdown for sample 2022-10-31 22:26:26 -05:00
cuom1999
409d2e3115 Upgrade to Django 3.2 2022-10-31 20:43:06 -05:00
cuom1999
c1f710c9ac Set u+x on manage.py 2022-10-30 18:48:23 -05:00
cuom1999
2a7a33fe1a Fix bug 2022-10-29 21:07:55 -05:00
cuom1999
7d009c36de Change sidebar style 2022-10-29 21:06:11 -05:00
cuom1999
091826afdb Fix bug 2022-10-29 03:24:14 -05:00
cuom1999
9ac06bbd51 Add i18n name for new and volunteer 2022-10-29 03:11:15 -05:00
cuom1999
0146d9a33e Fix bug 2022-10-29 01:52:02 -05:00
cuom1999
b43772c3e5 Add image lazy load for markdown 2022-10-29 01:25:22 -05:00
cuom1999
42eb5115d1 Update markdown 2022-10-28 23:28:31 -05:00
cuom1999
bfadc499b3 Fix typo 2022-10-28 23:17:24 -05:00
cuom1999
d0547ec106 Limit submission page to 50000 2022-10-28 22:34:12 -05:00
cuom1999
45b844d6c9 Optimize problem feed DB query 2022-10-28 21:29:48 -05:00
cuom1999
cffb76e220 Change user svg 2022-10-27 23:59:05 -05:00
cuom1999
5fc18d31d6 Change editor toolbar 2022-10-27 23:56:13 -05:00
cuom1999
ec3821ec2e Change style 2022-10-27 20:42:13 -05:00
cuom1999
edfee1f643 Add indent to editor 2022-10-27 20:32:31 -05:00
cuom1999
6b83a1ff0d Add unique together for model 2022-10-27 15:02:54 -05:00
cuom1999
f3bed03d8f Fix markdown bug 2022-10-27 14:55:37 -05:00
cuom1999
0eaffafc73 Fix bug 2022-10-26 01:11:37 -05:00
cuom1999
f768a20b97 Update markdown 2022-10-25 20:26:49 -05:00
cuom1999
75b04140f3 Update markdown 2022-10-25 20:23:11 -05:00
cuom1999
1ec1127f21 Update markdown 2022-10-25 18:22:49 -05:00
cuom1999
2d5c04fde8 Update markdown 2022-10-25 18:12:28 -05:00
cuom1999
8f95b070cd Update markdown 2022-10-25 18:10:33 -05:00
cuom1999
2a0276d0fd Update markdown 2022-10-25 18:07:45 -05:00
cuom1999
122c387420 Change link position 2022-10-25 00:46:08 -05:00
cuom1999
38368e9641 Change css position 2022-10-25 00:30:44 -05:00
cuom1999
9af5c40075 Fix bug 2022-10-25 00:09:12 -05:00
cuom1999
152b1bb187 Add requirement 2022-10-25 00:02:29 -05:00
cuom1999
77aaae6735 Migrate mistune to markdown 2022-10-24 23:59:04 -05:00
cuom1999
412945626b Fix bug 2022-10-24 13:29:06 -05:00
cuom1999
1af5d38a0c Add trans 2022-10-20 01:36:28 -05:00
cuom1999
b82bb88771 Add trans 2022-10-20 01:31:50 -05:00
Phuoc Dinh Le
b929964ae6
Merge pull request #26 from LQDJudge/edit_link
add edit link to authors
2022-10-20 01:06:20 -05:00
Phuoc Dinh Le
572026aecd
Merge branch 'master' into edit_link 2022-10-20 01:04:13 -05:00
cuom1999
b4e922226f Change style 2022-10-17 21:04:35 -05:00
cuom1999
f203c25201 Remove workflows 2022-10-17 19:49:31 -05:00
cuom1999
898daf8d4a Change css 2022-10-17 19:46:38 -05:00
cuom1999
8f15e327b8 Fix logged out bug for most recent organizations 2022-10-17 19:32:13 -05:00
cuom1999
30f1c105cb Format and fix logged out bug for most recent orgs 2022-10-17 19:30:00 -05:00
DELL
21ca51650c fix po again 2022-10-18 07:25:14 +07:00
cuom1999
103a4bb69d Change words and styles 2022-10-17 19:03:07 -05:00
Phuoc Dinh Le
f34529c3d8
Merge pull request #27 from LQDJudge/recent_organization
Add recent view organization
2022-10-17 18:54:49 -05:00
Zhao-Linux
87eb082a18 Fix recently visited organization 2022-10-18 05:08:12 +07:00
Zhao-Linux
512bc92116 Add recent view organization 2022-10-18 03:48:07 +07:00
DELL
50bb2c4361 add django.po 2022-10-18 01:06:50 +07:00
DELL
70e74fd619 delete unnecessary import 2022-10-17 23:08:37 +07:00
DELL
f4163fd017 fix edit link 2022-10-17 23:04:39 +07:00
DELL
4345e289e8 add edit link to authors 2022-10-17 12:12:20 +07:00
cuom1999
7a9dad71b4 Merge branch 'master' of https://github.com/LQDJudge/online-judge 2022-10-16 22:57:15 -05:00
cuom1999
56a9811170 Remove print 2022-10-16 22:57:09 -05:00
Phuoc Dinh Le
36e0cd797c
Merge pull request #25 from LQDJudge/features/allow-change-fullname
Allow user change school
2022-10-15 12:30:38 -05:00
Zhao-Linux
cf58f8dacb Allow user change school 2022-10-16 00:28:33 +07:00
cuom1999
d49a41d56b Remove adding name by GG form 2022-10-15 12:11:20 -05:00
Phuoc Dinh Le
af74edecbb
Merge pull request #24 from LQDJudge/features/allow-change-fullname
Allow user change fullname
2022-10-15 12:05:48 -05:00
Zhao-Linux
81a490ca93 Allow user change fullname 2022-10-15 23:23:50 +07:00
Zhao-Linux
7fafe394c5 Fix user profile nav page css 2022-10-15 22:26:00 +07:00
cuom1999
0ee7de1b46 Move problem clarification to contest clarification 2022-10-12 21:19:22 -05:00
Zhao-Linux
f001ae4e0a Add vi language to csv file 2022-10-10 17:59:05 +07:00
Zhao-Linux
cd9647463b Fix typo 2022-10-10 14:07:50 +07:00
Zhao-Linux
ad8c1c5e43 Fix admin contests view homepage 2022-10-10 04:36:02 +07:00
cuom1999
339a1b8817 Fix typo 2022-10-08 19:16:22 -05:00
Phuoc Dinh Le
8ebcd68865
Merge pull request #22 from LQDJudge/ranking_csv_file
Fix ranking csv file
2022-10-08 18:40:05 -05:00
Zhao
f3fef31e8d Fix ranking csv file 2022-10-09 06:34:19 +07:00
Zhao
d060259396 Merge branch 'master' into ranking_csv_file 2022-10-09 05:46:56 +07:00
Phuoc Dinh Le
855d2b2738
Merge pull request #21 from LQDJudge/ranking_csv_file
Add download ranking csv file
2022-10-08 15:12:24 -05:00
Phuoc Dinh Le
b87cfbb774
Merge pull request #19 from Canuc80k/master
Center the Join Button in Contest List
2022-10-08 15:10:55 -05:00
Phuoc Dinh Le
a3b5ebbc95
Merge pull request #20 from LQDJudge/delete_blog_button
add delete_blog button
2022-10-08 15:10:10 -05:00
DELL
c9d495d53c override post EditOrganizationContest 2022-10-08 22:12:08 +07:00
Zhao-Linux
fbc57f8b33 Add download ranking csv file 2022-10-08 17:55:34 +07:00
Zhao
e584c75991 Add download ranking table as csv file 2022-10-08 17:43:55 +07:00
DELL
d03150a396 review and fix delete_blog_button 2022-10-08 12:15:04 +07:00
DELL
9d42082b52 add delete_blog button 2022-10-08 09:35:21 +07:00
Zhao-Linux
43ffca160b Fix error when save edit organization contest 2022-10-08 00:54:12 +07:00
cuom1999
046c82e77d Revert to 9438fa3e14 2022-10-07 12:42:21 -05:00
cuom1999
995f5bc06b Merge branch 'master' of https://github.com/LQDJudge/online-judge 2022-10-07 12:40:36 -05:00
cuom1999
5f40da64c9 Revert to 9438fa3e14 2022-10-07 12:40:25 -05:00
cuom1999
7cac4157c6 Revert 2022-10-07 12:32:39 -05:00
cuom1999
36abf602e6 Merge branch 'master' of https://github.com/LQDJudge/online-judge 2022-10-07 12:17:26 -05:00
cuom1999
dfc12f81f2 Revert last 3 commits 2022-10-07 12:17:07 -05:00
Zhao-Linux
2646d12fe2 Fix error when save edit organization contest 2022-10-08 00:13:21 +07:00
DELL
11e4bc5a10 edit BlogPost.get_absolute_url 2022-10-07 14:07:53 +07:00
DELL
c44fb6c47a pull requirement.txt 2022-10-07 13:25:05 +07:00
DELL
87e8f3d966 add delete blog 2022-10-07 10:11:46 +07:00
Zhao-Linux
9438fa3e14 Change about.html file 2022-10-04 10:33:16 +07:00
Zhao-Linux
d0a0f86d46 Change about file 2022-10-04 07:18:35 +07:00
cuom1999
539a2639ee Allow volunteer search 2022-09-29 12:23:16 -05:00
cuom1999
f36a5497e0 Fix error when access deleted org 2022-09-24 23:50:26 -05:00
cuom1999
3da6a25867 Add stat page 2022-09-22 19:23:41 -05:00
cuom1999
1258d6dc25 Merge branch 'master' of https://github.com/LQDJudge/online-judge 2022-09-22 17:33:31 -05:00
cuom1999
fe5e7198ee Change lang stat page 2022-09-22 17:33:14 -05:00
cuom1999
8459d3c6e6 Fix problem code bug 2022-09-21 22:21:05 -05:00
cuom1999
fa8c683439 Import Markup from markupsafe 2022-09-16 18:02:53 -05:00
cuom1999
9e336a7bc9 Change add contest form 2022-09-16 00:07:27 -05:00
cuom1999
aeab9e8d0e Fix overflow 2022-09-15 13:52:11 -05:00
cuom1999
2633db27d8 Reformat 2022-09-15 13:26:07 -05:00
cuom1999
13b1d54ac3 Fix bug: 2022-09-15 13:00:49 -05:00
cuom1999
1c0686e30a Fix pygments latest version 2022-09-15 12:01:29 -05:00
cuom1999
979b9862fb Update admin org 2022-09-15 11:21:04 -05:00
cuom1999
071da74b3d Fix bug: 2022-09-15 02:13:14 -05:00
cuom1999
67ef6b9111 Allow to create group and its contest on UI 2022-09-15 02:05:02 -05:00
cuom1999
196e2a9bb0 Use raw sql to gen data 2022-09-14 16:11:15 -05:00
cuom1999
88c1f6324d Fix admin bug 2022-09-06 15:36:02 -05:00
cuom1999
e6a4519291 Add curators to notif list 2022-09-05 21:01:41 -05:00
cuom1999
9d9d171209 Black 2022-09-04 12:32:10 -05:00
cuom1999
ba75b5c086 Update admin page 2022-09-04 12:31:49 -05:00
cuom1999
737cd8a068 Update requirements 2022-09-02 18:44:45 -05:00
cuom1999
b6e450260b Modify some admin 2022-09-02 18:03:54 -05:00
cuom1999
1f86b3fc07 Fix best submission page bug 2022-09-02 00:39:45 -05:00
cuom1999
e832391621 Fix button style 2022-09-02 00:32:28 -05:00
cuom1999
cc6c692cc5 Change lang icon to blue 2022-09-01 22:04:14 -05:00
cuom1999
96ef0bfd2b Fix button style 2022-09-01 19:09:16 -05:00
cuom1999
019976d90a Fix js 2022-09-01 18:42:13 -05:00
cuom1999
fe405dbca4 Move lang to top 2022-09-01 18:39:19 -05:00
cuom1999
bddb00050a Move unread chat count to one request 2022-08-31 23:18:38 -05:00
cuom1999
a7f7aab444 Support view pdf for pdf statements 2022-08-31 10:58:47 -05:00
cuom1999
1c47d87b61 Fix nginx pdf 2022-08-31 10:44:33 -05:00
cuom1999
66b9240fd4 Fix nginx for pdf 2022-08-31 10:28:54 -05:00
cuom1999
7f3e22e3bf Add memory unit to problem admin 2022-08-31 00:23:23 -05:00
cuom1999
dddaf69fb7 Add trans 2022-08-30 23:07:32 -05:00
cuom1999
69f08e84b5 Add pdf option for problem 2022-08-30 22:50:08 -05:00
cuom1999
98b8cbe518 Change styles navbar icons 2022-08-27 23:00:17 -05:00
cuom1999
2f94879278 Small fix 2022-08-27 22:05:49 -05:00
cuom1999
6ee4eaf0f0 Fix typo 2022-08-27 22:03:39 -05:00
cuom1999
f208930a3d Make judges in problem page togglable 2022-08-27 22:02:16 -05:00
cuom1999
cef059ea9d Change blog post style 2022-08-27 21:45:28 -05:00
Canuc80k
57268d65b8 Center the Join Button in Contest List 2022-08-25 22:34:21 +07:00
cuom1999
793bcee491 Another chat potential bug 2022-08-25 09:01:43 -05:00
cuom1999
7311a050b0 Fix chat bugs 2022-08-25 08:51:54 -05:00
cuom1999
e3881c7409 Fix chat buttons 2022-08-22 23:49:20 -05:00
cuom1999
69e3303f61 Fix status page update 2022-08-22 23:34:22 -05:00
cuom1999
83192b540e Add icons to status page 2022-08-22 23:30:41 -05:00
cuom1999
f25c389b7f Change color of button of feed 2022-08-21 10:52:49 -05:00
cuom1999
88c9d687d7 Change button for problem page 2022-08-20 11:25:56 -05:00
cuom1999
f5234dde56 Change button style 2022-08-20 11:18:28 -05:00
cuom1999
af670f7437 Add trans 2022-08-18 23:28:51 -05:00
cuom1999
b43850d776 Add icons to submission page 2022-08-18 23:26:30 -05:00
cuom1999
f5c124a2e9 Add source to submission status 2022-08-18 22:47:41 -05:00
cuom1999
04b9849b40 Fix mathjax 2022-08-08 18:05:44 +07:00
cuom1999
644d640682 Relax password validator 2022-08-03 22:04:43 +07:00
cuom1999
db096c83f8 Relax more markdown 2022-08-01 15:01:19 +07:00
cuom1999
7f2e3f2594 Relax markdwn style 2022-08-01 13:57:56 +07:00
cuom1999
427e391416 Temporary change to wait for cached frontend 2022-07-30 21:01:37 +07:00
cuom1999
627cf37996 Improve spoiler 2022-07-30 17:21:08 +07:00
cuom1999
3c6108298c Add hard wrap to markdown 2022-07-30 12:03:56 +07:00
cuom1999
c9f8fbe098 Add old volunteer link to UI 2022-07-29 15:53:35 +07:00
cuom1999
89b74e8ef8 Remove submodule 2022-07-29 15:00:44 +07:00
cuom1999
f969dbb290 Fix toolbar UI 2022-07-29 14:55:12 +07:00
cuom1999
724aadbe20 Fix internal page url 2022-07-28 04:22:01 +07:00
cuom1999
3ed4fc7a0e Change ~ to $ for latex 2022-07-27 20:18:31 +07:00
cuom1999
dc8cbc6976 Auto add editing user to curators 2022-07-20 15:06:15 +07:00
cuom1999
bd703af53e Add view statment src 2022-07-18 13:17:00 +07:00
cuom1999
c0dc6f4474 Add trans 2022-07-18 13:00:56 +07:00
cuom1999
aa8abdec20 Add filter problems by solved 2022-07-18 12:59:45 +07:00
cuom1999
d1e5aaa3e1 Always show problem types for volunteer 2022-07-18 11:35:51 +07:00
cuom1999
1ef68e0fdb Upgrade pagedown 2022-07-15 13:00:34 +07:00
cuom1999
ab64ab6134 Fix submission status on mobile 2022-07-05 13:03:15 +07:00
cuom1999
8a4ec11d82 Fix mobile width: 2022-07-05 12:54:04 +07:00
cuom1999
12dc691f20 Change mobile x width 2022-07-05 10:23:43 +07:00
cuom1999
91ab000d80 Fix mobile x scroll 2022-07-05 10:19:32 +07:00
cuom1999
ee6aaaf1af Change mobile width of blog 2022-07-05 10:07:47 +07:00
cuom1999
4cf77f95d8 Fix notification 2022-06-28 19:23:28 +07:00
cuom1999
a19a795316 Add notification for pending blogs 2022-06-26 14:24:38 +07:00
cuom1999
0e324a3cce Only allow open org to show members 2022-06-26 12:07:34 +07:00
cuom1999
50afd6cd59 Fix html 2022-06-24 20:26:04 +07:00
cuom1999
ed90cdab57 Fix vnese first solve 2022-06-23 15:52:15 +07:00
cuom1999
7c171d59c1 Add testlib 2022-06-22 14:28:34 +07:00
cuom1999
2eaf71c535 Add trans 2022-06-20 22:28:01 +07:00
cuom1999
b0cbd4aecc Update problem admin 2022-06-20 22:16:58 +07:00
cuom1999
0bc355132e Fix translation 2022-06-20 22:05:11 +07:00
Phuoc Dinh Le
d5fdd110b8
Merge pull request #18 from letangphuquy/patch-1
Update django.po
2022-06-20 21:57:27 +07:00
Le Tang Phu Quy
c2cc59c452
Update django.po
dịch các helper text trong trang https://lqdoj.edu.vn/admin/judge/problem/add/
2022-06-20 14:56:04 +07:00
cuom1999
6270becd4a Fix table style 2022-06-19 16:25:06 +07:00
cuom1999
c69127748a Redesign org and fullname 2022-06-19 16:15:58 +07:00
cuom1999
bce34a0c40 Fix chat_box schema 2022-06-18 14:58:59 +07:00
cuom1999
289e9ab7db Redesign org list 2022-06-18 14:32:37 +07:00
cuom1999
cd5672abf0 Update checker sample 2022-06-17 21:37:13 +07:00
cuom1999
df1b721968 Allow partial score interactive 2022-06-17 20:54:40 +07:00
cuom1999
2baa17f9dd Fix permission 2022-06-16 14:56:02 +07:00
cuom1999
eb44b6510a Add view public submissions permission 2022-06-16 14:46:29 +07:00
cuom1999
fdb71ba3c4 Add hover style to navbar icons 2022-06-15 13:44:01 +07:00
cuom1999
6beba95332 Add revision register 2022-06-14 01:30:40 +07:00
cuom1999
a1fd7caee4 Remove manually managed from admin 2022-06-13 11:35:24 +07:00
cuom1999
e07fdc318f Fix highlight first solve 2022-06-13 11:35:02 +07:00
cuom1999
fd4b78178f Fix problem io position 2022-06-12 15:00:14 +07:00
cuom1999
3dfd3a5dab Add language template 2022-06-12 14:57:46 +07:00
cuom1999
0b6031eef7 Update admin page 2022-06-12 13:52:02 +07:00
cuom1999
56a147191d Add features for problem submit 2022-06-11 23:31:56 +07:00
cuom1999
da551060da Add submission title 2022-06-09 12:43:15 +07:00
cuom1999
b523dcd2f6 Fix group submission wsevent 2022-06-08 23:00:34 +07:00
cuom1999
ad2f4f5952 Add trans 2022-06-08 05:13:04 +07:00
cuom1999
1efbf4cc91 Add group submissions 2022-06-08 05:11:30 +07:00
cuom1999
247e0e4740 New UI for users and submissions 2022-06-06 11:36:35 -05:00
cuom1999
d0ac92914d Add back category 2022-06-05 12:05:25 -05:00
cuom1999
a0c6454933 Not display category in problem table 2022-06-05 12:00:21 -05:00
cuom1999
7128845dcf Fix problem table css 2022-06-05 11:53:44 -05:00
cuom1999
f7d1db4dd5 Fix mobile css 2022-06-05 11:29:59 -05:00
cuom1999
819e436af1 Clean up 2022-06-04 21:31:52 -05:00
cuom1999
0f0dd502c2 Add submission ajax to other contest formats 2022-06-04 21:29:51 -05:00
cuom1999
81d6a57b26 Fix bug 2022-06-04 21:25:23 -05:00
cuom1999
6e6b247bf9 Move js to css 2022-06-03 14:07:27 -05:00
cuom1999
1802458408 Fix bug 2022-06-03 14:01:49 -05:00
cuom1999
2ee1885880 Use bridged checker 2022-06-03 01:27:51 -05:00
cuom1999
253693d1ea Fix submission page bug 2022-06-02 22:14:01 -05:00
cuom1999
d26597c9b6 Remove icon on mobile 2022-06-02 17:35:25 -05:00
cuom1999
3d2b7b44d5 Add blog hover 2022-06-02 17:29:00 -05:00
cuom1999
ba69ec2ffc Change UI 2022-06-02 17:09:24 -05:00
cuom1999
5e963c6494 Fix io bug for problem raw 2022-06-02 11:23:16 -05:00
cuom1999
b89c127707 Fix io display bug 2022-06-02 11:21:55 -05:00
cuom1999
8ba39a1f97 Add help text and trans 2022-06-02 00:20:45 -05:00
cuom1999
a1bcc2cb46 Add file io 2022-06-01 23:59:46 -05:00
cuom1999
c9091f2e75 Remove caniuse to always use jax 2022-06-01 23:43:31 -05:00
cuom1999
76b631366b Fix mobile UI 2022-06-01 15:18:52 -05:00
cuom1999
78b818901e Relax contest submission view 2022-06-01 14:31:20 -05:00
cuom1999
1e35ba864f Add back access check 2022-06-01 11:59:58 -05:00
cuom1999
957f04dcf1 Relax access check 2022-06-01 11:53:17 -05:00
cuom1999
752ba6c605 Add trans 2022-06-01 00:53:15 -05:00
cuom1999
3fb78e714d Fix small bugs 2022-06-01 00:47:22 -05:00
cuom1999
9b1724cdad Change UI ranking 2022-06-01 00:28:56 -05:00
cuom1999
d38342ad43 Redesign UI 2022-05-31 00:41:57 -05:00
cuom1999
eaa7be6ec6 fix blog permissions 2022-05-30 23:35:30 -05:00
cuom1999
9f43e712d0 Add more statuses to submission 2022-05-30 18:23:31 -05:00
cuom1999
4ef9e09c6e Add navbar trans 2022-05-30 17:19:46 -05:00
cuom1999
d1d0c6e1f4 Fix slug bug 2022-05-30 05:54:21 -05:00
cuom1999
e269a4d63f Fix org kick 2022-05-30 03:14:02 -05:00
cuom1999
5c9acd0bf1 Fix css bug 2022-05-30 03:07:18 -05:00
cuom1999
22470c2f58 Edit blog home UI 2022-05-30 02:37:48 -05:00
cuom1999
8c846bf414 Add trans 2022-05-30 02:11:58 -05:00
cuom1999
a05b0f7a73 Add trans 2022-05-30 02:09:53 -05:00
cuom1999
a2fa61bfd8 Add trans 2022-05-30 02:07:09 -05:00
cuom1999
5fff6b1510 Add organization blogs 2022-05-30 01:59:53 -05:00
cuom1999
99fc3d1015 fix error 2022-05-28 14:20:41 -05:00
cuom1999
a78f1db5e6 Make admin a little smarter 2022-05-28 03:54:12 -05:00
cuom1999
1b3b27f1d9 Fix org user 2022-05-28 02:52:41 -05:00
cuom1999
982c975a20 Add line break to top users 2022-05-28 02:33:37 -05:00
cuom1999
b8876ba023 Make admin members 2022-05-28 02:29:25 -05:00
cuom1999
b1a52cc872 Make wmd bar scrollbar hidden 2022-05-27 23:41:14 -05:00
cuom1999
cae65de1d5 Add permission to org pages 2022-05-27 23:33:00 -05:00
cuom1999
82ec9e098d Rewrite org page 2022-05-27 23:28:22 -05:00
cuom1999
05ff359f89 Add translation 2022-05-23 20:56:45 -05:00
cuom1999
cd36b54f56 Add translation 2022-05-22 00:42:54 -05:00
cuom1999
f79f5a8455 Add last unsolved 2022-05-21 20:30:44 -05:00
cuom1999
0b2c410fe5 Add debug to submission list 2022-05-18 14:10:23 -05:00
cuom1999
300572d6da Remove onwsclose in templates 2022-05-18 14:02:23 -05:00
cuom1999
ac0b7afc1a Try fixing ws 2022-05-18 13:50:18 -05:00
cuom1999
088d78d762 Try fixing ws 2022-05-18 13:39:20 -05:00
cuom1999
cce42995b9 Fix previous bug 2022-05-18 12:51:29 -05:00
cuom1999
c0e27205ac Add console print when ws close 2022-05-18 12:34:21 -05:00
cuom1999
6047d292dc Add notif for comment page author 2022-05-17 22:34:08 -05:00
cuom1999
1533607551 Fix trans 2022-05-14 13:22:35 -05:00
cuom1999
a87fb49918 Reformat using black 2022-05-14 12:57:27 -05:00
cuom1999
efee4ad081 Add char limit to chat 2022-05-12 00:01:47 -05:00
cuom1999
b1f91d432c Make upload zip save form 2022-05-07 20:10:10 -05:00
cuom1999
a19bfefa8c Fix zip saving bug 2022-05-07 20:06:39 -05:00
cuom1999
e5560cd6e9 Add translations 2022-05-02 21:55:55 -05:00
cuom1999
e70618ed19 Add problem volunteer 2022-05-02 21:44:14 -05:00
cuom1999
e51129d36f Fix race in chat ignore 2022-04-29 15:27:25 -05:00
cuom1999
1dfc6d312d Allow contest host to view submissions 2022-04-29 14:36:26 -05:00
cuom1999
f095729cb6 Fix view editorial in contest mode 2022-04-28 00:15:58 -05:00
cuom1999
01c19b172a Add compare reversion for contest and blog 2022-04-26 13:24:36 -05:00
cuom1999
308234166b Add reversion compare 2022-04-25 22:00:15 -05:00
cuom1999
27d9ea8326 Add help text to contest time limit 2022-04-25 00:10:02 -05:00
cuom1999
81644a7415 Make recommendation random 2022-04-24 23:25:50 -05:00
cuom1999
ebe09cf0ad Update custom checker sample 2022-04-20 14:04:06 -05:00
cuom1999
15370d8862 Allow interactive feedback 2022-04-20 14:00:32 -05:00
cuom1999
1526b8a0ff Move style to file 2022-04-20 14:00:13 -05:00
cuom1999
8028b9ab55 Fix scroll right sidebar problem list 2022-04-18 17:18:21 -05:00
cuom1999
0b32af57d6 Improve problem search 2022-04-16 17:11:29 -05:00
cuom1999
4d9d1f206a Add logger for feed 2022-04-16 16:05:55 -05:00
cuom1999
6797a8523b Decrease problem feed page to 20 2022-04-15 14:34:09 -05:00
cuom1999
7ce3bdda49 Fix page list bug 2022-04-14 15:40:48 -05:00
cuom1999
ff8f12c134 Add interactive option 2022-04-14 14:14:58 -05:00
cuom1999
122cd0fa73 Make font-w weight bolder in blog feed 2022-04-13 13:37:30 -05:00
cuom1999
2831b73b83 Fix mobile style 2022-04-13 12:53:34 -05:00
cuom1999
b46d046b95 Change UI 2022-04-13 12:40:34 -05:00
cuom1999
432b1ce946 Fix mobile left-sidebar style 2022-04-13 11:55:40 -05:00
cuom1999
f539a90635 Change problem page 2022-04-13 00:52:03 -05:00
cuom1999
5c6391fb76 Fix sort key error 2022-04-12 11:53:39 -05:00
cuom1999
e9725d27aa Add * point 2022-04-11 23:13:36 -05:00
cuom1999
2fe571379c Add ML to problem feed 2022-04-11 21:18:01 -05:00
cuom1999
34523ab53f Add median to problem admin 2022-04-04 17:43:56 -05:00
cuom1999
891e2b0d92 Add vote count to problem admin 2022-04-04 17:13:23 -05:00
cuom1999
30a856dcb9 add vote mean and std to problem admin 2022-04-04 17:04:40 -05:00
cuom1999
ce77242008 Fix bugs 2022-03-31 20:33:37 -05:00
cuom1999
329a5293dd Fix field problem vote admin 2022-03-31 01:31:10 -05:00
cuom1999
ca2c74af7c Add fields to problem vote admin 2022-03-31 01:28:40 -05:00
cuom1999
68a53b8749 Remove unlisted users from home page 2022-03-31 01:09:38 -05:00
cuom1999
847fc11bc5 Localize rating chart (DMOJ) 2022-03-28 19:29:11 -05:00
cuom1999
0dfad5b0cd Add manifest 2022-03-28 18:42:50 -05:00
cuom1999
d0e2c7daa9 Fix duplicate view comment 2022-03-24 17:34:03 -05:00
cuom1999
3bf31ff073 Merge branch 'master' of https://github.com/LQDJudge/online-judge 2022-03-21 16:43:35 -05:00
cuom1999
3d3ab23d27 New home UI 2022-03-21 16:40:08 -05:00
cuom1999
d10173df5d New home UI 2022-03-21 16:09:16 -05:00
2868 changed files with 117945 additions and 23196 deletions

View file

@ -1,17 +0,0 @@
name: build
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Set up Python 3.7
uses: actions/setup-python@v1
with:
python-version: 3.7
- name: Install flake8
run: pip install flake8 flake8-import-order flake8-future-import flake8-commas flake8-logging-format
- name: Lint with flake8
run: |
flake8 --version
flake8

View file

@ -1,40 +0,0 @@
name: compilemessages
on:
push:
paths:
- 'locale/**'
pull_request:
paths:
- 'locale/**'
jobs:
compilemessages:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Set up Python 3.7
uses: actions/setup-python@v1
with:
python-version: 3.7
- name: Checkout submodules
run: |
git submodule init
git submodule update
- name: Install requirements
run: |
sudo apt-get install gettext
pip install -r requirements.txt
pip install pymysql
- name: Check .po file validity
run: |
fail=0
while read -r file; do
if ! msgfmt --check-format "$file"; then
fail=$((fail + 1))
fi
done < <(find locale -name '*.po')
exit "$fail"
shell: bash
- name: Compile messages
run: |
echo "STATIC_ROOT = '/tmp'" > dmoj/local_settings.py
python manage.py compilemessages

33
.github/workflows/init.yml vendored Normal file
View file

@ -0,0 +1,33 @@
name: Lint
on:
# Trigger the workflow on push or pull request,
# but only for the main branch
push:
branches:
- master
pull_request:
branches:
- master
jobs:
run-linters:
name: Run linters
runs-on: ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v1
with:
python-version: 3.8
- name: Install Python dependencies
run: pip install black
- name: Run linters
uses: wearerequired/lint-action@v2
with:
black: true

View file

@ -1,53 +0,0 @@
name: makemessages
on:
push:
branches:
- master
jobs:
makemessages:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Set up Python 3.7
uses: actions/setup-python@v1
with:
python-version: 3.7
- name: Checkout submodules
run: |
git submodule init
git submodule update
- name: Install requirements
run: |
sudo apt-get install gettext
curl -O https://artifacts.crowdin.com/repo/deb/crowdin.deb
sudo dpkg -i crowdin.deb
pip install -r requirements.txt
pip install pymysql
- name: Collect localizable strings
run: |
echo "STATIC_ROOT = '/tmp'" > dmoj/local_settings.py
python manage.py makemessages -l en -e py,html,txt
python manage.py makemessages -l en -d djangojs
- name: Upload strings to Crowdin
env:
CROWDIN_API_TOKEN: ${{ secrets.CROWDIN_API_TOKEN }}
run: |
cat > crowdin.yaml <<EOF
project_identifier: dmoj
files:
- source: /locale/en/LC_MESSAGES/django.po
translation: /locale/%two_letters_code%/LC_MESSAGES/django.po
languages_mapping:
two_letters_code:
zh-CN: zh_Hans
sr-CS: sr_Latn
- source: /locale/en/LC_MESSAGES/djangojs.po
translation: /locale/%two_letters_code%/LC_MESSAGES/djangojs.po
languages_mapping:
two_letters_code:
zh-CN: zh_Hans
sr-CS: sr_Latn
EOF
echo "api_key: ${CROWDIN_API_TOKEN}" >> crowdin.yaml
crowdin upload sources

View file

@ -1,67 +0,0 @@
name: updatemessages
on:
schedule:
- cron: '0 * * * *'
jobs:
updatemessages:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Set up Python 3.7
uses: actions/setup-python@v1
with:
python-version: 3.7
- name: Checkout submodules
run: |
git submodule init
git submodule update
- name: Install requirements
run: |
sudo apt-get install gettext
curl -O https://artifacts.crowdin.com/repo/deb/crowdin.deb
sudo dpkg -i crowdin.deb
pip install -r requirements.txt
pip install pymysql
- name: Download strings from Crowdin
env:
CROWDIN_API_TOKEN: ${{ secrets.CROWDIN_API_TOKEN }}
run: |
cat > crowdin.yaml <<EOF
project_identifier: dmoj
files:
- source: /locale/en/LC_MESSAGES/django.po
translation: /locale/%two_letters_code%/LC_MESSAGES/django.po
languages_mapping:
two_letters_code:
zh-CN: zh_Hans
zh-TW: zh_Hant
sr-CS: sr_Latn
- source: /locale/en/LC_MESSAGES/djangojs.po
translation: /locale/%two_letters_code%/LC_MESSAGES/djangojs.po
languages_mapping:
two_letters_code:
zh-CN: zh_Hans
zh-TW: zh_Hant
sr-CS: sr_Latn
EOF
echo "api_key: ${CROWDIN_API_TOKEN}" >> crowdin.yaml
crowdin download
rm crowdin.yaml
- name: Cleanup
run: |
rm -rf src/
git add locale
git checkout .
git clean -fd
- name: Create pull request
uses: peter-evans/create-pull-request@v1.4.1-multi
env:
GITHUB_TOKEN: ${{ secrets.REPO_SCOPED_TOKEN }}
COMMIT_MESSAGE: 'i18n: update translations from Crowdin'
PULL_REQUEST_TITLE: 'Update translations from Crowdin'
PULL_REQUEST_BODY: This PR has been auto-generated to pull in latest translations from [Crowdin](https://translate.dmoj.ca).
PULL_REQUEST_LABELS: i18n, enhancement
PULL_REQUEST_REVIEWERS: Xyene, quantum5
PULL_REQUEST_BRANCH: update-i18n
BRANCH_SUFFIX: none

8
.gitmodules vendored
View file

@ -1,8 +0,0 @@
[submodule "resources/pagedown"]
path = resources/pagedown
url = https://github.com/DMOJ/dmoj-pagedown.git
branch = master
[submodule "resources/libs"]
path = resources/libs
url = https://github.com/DMOJ/site-assets.git
branch = master

17
.pre-commit-config.yaml Normal file
View file

@ -0,0 +1,17 @@
repos:
- repo: https://github.com/rtts/djhtml
rev: 'v1.5.2' # replace with the latest tag on GitHub
hooks:
- id: djhtml
entry: djhtml -i -t 2
files: templates/.
- id: djcss
types: [scss]
- repo: https://github.com/psf/black
rev: 22.12.0
hooks:
- id: black
- repo: https://github.com/hadialqattan/pycln
rev: 'v2.3.0'
hooks:
- id: pycln

View file

@ -49,7 +49,7 @@
<br> <br>
<div class="popup"> <div class="popup">
<div> <div>
<img class="logo" src="logo.png" alt="LQDOJ"> <img class="logo" src="logo.svg" alt="LQDOJ">
</div> </div>
<h1 style="width: 100%;">Oops, LQDOJ is down now.</h1> <h1 style="width: 100%;">Oops, LQDOJ is down now.</h1>
</div> </div>

232
README.md
View file

@ -17,11 +17,12 @@ Supported languages:
- Assembly (x64) - Assembly (x64)
- AWK - AWK
- C - C
- C++03 / C++11 / C++14 / C++17 - C++03 / C++11 / C++14 / C++17 / C++20
- Java 11 - Java 11
- Pascal - Pascal
- Perl - Perl
- Python 2 / Python 3 - Python 2 / Python 3
- PyPy 2 / PyPy 3
Support plagiarism detection via [Stanford MOSS](https://theory.stanford.edu/~aiken/moss/). Support plagiarism detection via [Stanford MOSS](https://theory.stanford.edu/~aiken/moss/).
@ -30,10 +31,196 @@ Support plagiarism detection via [Stanford MOSS](https://theory.stanford.edu/~ai
Most of the setup are the same as DMOJ installations. You can view the installation guide of DMOJ here: https://docs.dmoj.ca/#/site/installation. Most of the setup are the same as DMOJ installations. You can view the installation guide of DMOJ here: https://docs.dmoj.ca/#/site/installation.
There is one minor change: Instead of `git clone https://github.com/DMOJ/site.git`, you clone this repo `git clone https://github.com/LQDJudge/online-judge.git`. There is one minor change: Instead of `git clone https://github.com/DMOJ/site.git`, you clone this repo `git clone https://github.com/LQDJudge/online-judge.git`.
### Additional Steps in Production:
1. To use newsletter (email sending), go to admin and create a newsletter. - Bước 1: cài các thư viện cần thiết
2. Change the domain name and website name in Admin page: Navigation Bars/Sites - $ ở đây nghĩa là sudo. Ví dụ dòng đầu nghĩa là chạy lệnh `sudo apt update`
```jsx
$ apt update
$ apt install git gcc g++ make python3-dev python3-pip libxml2-dev libxslt1-dev zlib1g-dev gettext curl redis-server
$ curl -sL https://deb.nodesource.com/setup_18.x | sudo -E bash -
$ apt install nodejs
$ npm install -g sass postcss-cli postcss autoprefixer
```
- Bước 2: tạo DB
- Server đang dùng MariaDB ≥ 10.5, các bạn cũng có thể dùng Mysql nếu bị conflict
- Nếu các bạn chạy lệnh dưới này xong mà version mariadb bị cũ (< 10.5) thì thể tra google cách cài MariaDB mới nhất (10.5 hoặc 10.6).
- Các bạn có thể thấy version MariaDB bằng cách gõ lệnh `sudo mysql` (Ctrl + C để quit)
```jsx
$ apt update
$ apt install mariadb-server libmysqlclient-dev
```
- Bước 3: tạo table trong DB
- Các bạn có thể thay tên table và password
```jsx
$ sudo mysql
mariadb> CREATE DATABASE dmoj DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci;
mariadb> GRANT ALL PRIVILEGES ON dmoj.* TO 'dmoj'@'localhost' IDENTIFIED BY '<password>';
mariadb> exit
```
- Bước 4: Cài đặt môi trường ảo (virtual env) và pull code
- Nếu `pip3 install mysqlclient` bị lỗi thì thử chạy `pip3 install mysqlclient==2.1.1`
```jsx
$ python3 -m venv dmojsite
$ . dmojsite/bin/activate
(dmojsite) $ git clone https://github.com/LQDJudge/online-judge.git
(dmojsite) $ cd online-judge
(dmojsite) $ git submodule init
(dmojsite) $ git submodule update
(dmojsite) $ pip3 install -r requirements.txt
(dmojsite) $ pip3 install mysqlclient
(dmojsite) $ pre-commit install
```
- Bước 5: tạo local_settings.py. Đây là file để custom setting cho Django. Các bạn tạo file vào `online-judge/dmoj/local_settings.py`
- File mẫu: https://github.com/DMOJ/docs/blob/master/sample_files/local_settings.py
- Nếu bạn đổi tên hoặc mật khẩu table databases thì thay đổi thông tin tương ứng trong `Databases`
- Sau khi xong, chạy lệnh `(dmojsite) $ python3 manage.py check` để kiểm tra
- Bước 6: Compile CSS và translation
- Giải thích:
- Lệnh 1 và 2 gọi sau mỗi lần thay đổi 1 file css hoặc file js (file html thì không cần)
- Lệnh 3 và 4 gọi sau mỗi lần thay đổi file dịch
- Note: Sau khi chạy lệnh này, folder tương ứng với STATIC_ROOT trong local_settings phải được tạo. Nếu chưa được tạo thì mình cần tạo folder đó trước khi chạy 2 lệnh đầu.
```jsx
(dmojsite) $ ./make_style.sh
(dmojsite) $ python3 manage.py collectstatic
(dmojsite) $ python3 manage.py compilemessages
(dmojsite) $ python3 manage.py compilejsi18n
```
- Bước 7: Thêm dữ liệu vào DB
```jsx
(dmojsite) $ python3 manage.py migrate
(dmojsite) $ python3 manage.py loaddata navbar
(dmojsite) $ python3 manage.py loaddata language_small
(dmojsite) $ python3 manage.py loaddata demo
```
- Bước 8: Chạy site. Đến đây thì cơ bản đã hoàn thành (chưa có judge, websocket, celery). Các bạn có thể truy cập tại `localhost:8000`
```jsx
python3 manage.py runserver 0.0.0.0:8000
```
**Một số lưu ý:**
1. (WSL) có thể tải ứng dụng Terminal trong Windows Store
2. (WSL) mỗi lần mở ubuntu, các bạn cần chạy lệnh sau để mariadb khởi động: `sudo service mysql restart` (tương tự cho một số service khác như memcached, celery)
3. Sau khi cài đặt, các bạn chỉ cần activate virtual env và chạy lệnh runserver là ok
```jsx
. dmojsite/bin/activate
python3 manage.py runserver
```
5. Đối với nginx, sau khi config xong theo guide của DMOJ, bạn cần thêm location như sau để sử dụng được tính năng profile image, thay thế `path/to/oj` thành đường dẫn nơi bạn đã clone source code.
```
location /profile_images/ {
root /path/to/oj;
}
```
6. Quy trình dev:
1. Sau khi thay đổi code thì django tự build lại, các bạn chỉ cần F5
2. Một số style nằm trong các file .scss. Các bạn cần recompile css thì mới thấy được thay đổi.
**Optional:**
************Alias:************ Các bạn có thể lưu các alias này để sau này dùng cho nhanh
- mtrans: để generate translation khi các bạn add một string trong code
- trans: compile translation (sau khi bạn đã dịch tiếng Việt)
- cr: chuyển tới folder OJ
- pr: chạy server
- sm: restart service (chủ yếu dùng cho WSL)
- sd: activate virtual env
- css: compile các file css
```jsx
alias mtrans='python3 manage.py makemessages -l vi && python3 manage.py makedmojmessages -l vi'
alias pr='python3 manage.py runserver'
alias sd='source ~/LQDOJ/dmojsite/bin/activate'
alias sm='sudo service mysql restart && sudo service redis-server start && sudo service memcached start'
alias trans='python3 manage.py compilemessages -l vi && python3 manage.py compilejsi18n -l vi'
alias cr='cd ~/LQDOJ/online-judge'
alias css='./make_style.sh && python3 manage.py collectstatic --noinput'
```
**Memcached:** dùng cho in-memory cache
```jsx
$ sudo apt install memcached
```
**Websocket:** dùng để live update (như chat)
- Tạo file online-judge/websocket/config.js
```jsx
module.exports = {
get_host: '127.0.0.1',
get_port: 15100,
post_host: '127.0.0.1',
post_port: 15101,
http_host: '127.0.0.1',
http_port: 15102,
long_poll_timeout: 29000,
};
```
- Cài các thư viện
```jsx
(dmojsite) $ npm install qu ws simplesets
(dmojsite) $ pip3 install websocket-client
```
- Khởi động (trong 1 tab riêng)
```jsx
(dmojsite) $ node websocket/daemon.js
```
**************Celery:************** (dùng cho một số task như batch rejudge_
```jsx
celery -A dmoj_celery worker
```
**************Judge:**************
- Cài đặt ở 1 folder riêng bên ngoài site:
```jsx
$ apt install python3-dev python3-pip build-essential libseccomp-dev
$ git clone https://github.com/LQDJudge/judge-server.git
$ cd judge-server
$ sudo pip3 install -e .
```
- Tạo một file judge.yml ở bên ngoài folder judge-server (file mẫu https://github.com/DMOJ/docs/blob/master/sample_files/judge_conf.yml)
- Thêm judge vào site bằng UI: Admin → Judge → Thêm Judge → nhập id và key (chỉ cần thêm 1 lần) hoặc dùng lệnh `(dmojsite) $ python3 managed.py addjudge <id> <key>`.
- Chạy Bridge (cầu nối giữa judge và site) trong 1 tab riêng trong folder online-judge:
```jsx
(dmojsite) $ python3 managed.py runbridged
```
- Khởi động Judge (trong 1 tab riêng):
```jsx
$ dmoj -c judge.yml localhost
```
- Lưu ý: mỗi lần sau này muốn chạy judge thì mở 1 tab cho bridge và n tab cho judge. Mỗi judge cần 1 file yml khác nhau (chứa authentication khác nhau)
### Some frequent difficulties when installation: ### Some frequent difficulties when installation:
@ -41,6 +228,21 @@ There is one minor change: Instead of `git clone https://github.com/DMOJ/site.gi
2. Missing the problem folder in `local_settings.py`. You need to create a folder to contain all problem packages and configure in `local_settings.py`. 2. Missing the problem folder in `local_settings.py`. You need to create a folder to contain all problem packages and configure in `local_settings.py`.
3. Missing static folder in `local_settings.py`. Similar to problem folder, make sure to configure `STATIC_FILES` inside `local_settings.py`. 3. Missing static folder in `local_settings.py`. Similar to problem folder, make sure to configure `STATIC_FILES` inside `local_settings.py`.
4. Missing configure file for judges. Each judge must have a seperate configure file. To create this file, you can run `python dmojauto-conf`. Checkout all sample files here https://github.com/DMOJ/docs/blob/master/sample_files. 4. Missing configure file for judges. Each judge must have a seperate configure file. To create this file, you can run `python dmojauto-conf`. Checkout all sample files here https://github.com/DMOJ/docs/blob/master/sample_files.
5. Missing timezone data for SQL. If you're using Ubuntu and you're following DMOJ's installation guide for the server, and you are getting the error mentioned in https://github.com/LQDJudge/online-judge/issues/45, then you can follow this method to fix:
```
mysql
-- You may have to do this if you haven't set root password for MySQL, replace mypass with your password
-- SET PASSWORD FOR 'root'@'localhost' = PASSWORD('mypass');
-- FLUSH PRIVILEGES;
exit
mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -D mysql -u root -p
mysql -u root -p -e "flush tables;" mysql
```
6. Missing the chat secret key, you must generate a Fernet key, and assign a variable in `local_settings.py` like this
```python
CHAT_SECRET_KEY = "81HqDtbqAywKSOumSxxxxxxxxxxxxxxxxx="
```
## Usage ## Usage
@ -53,12 +255,12 @@ source dmojsite/bin/activate
2. Run server: 2. Run server:
```bash ```bash
python manage.py runserver 0.0.0.0:8000 python3 manage.py runserver 0.0.0.0:8000
``` ```
3. Create a bridge (this is opened in a different terminal with the second step if you are using the same machine) 3. Create a bridge (this is opened in a different terminal with the second step if you are using the same machine)
```bash ```bash
python manage.py runbridged python3 manage.py runbridged
``` ```
4. Create a judge (another terminal) 4. Create a judge (another terminal)
@ -79,20 +281,22 @@ celery -A dmoj_celery worker
node websocket/daemon.js node websocket/daemon.js
``` ```
7. To use subdomain for each organization, go to admin page -> navigation bar -> sites, add domain name (e.g, "localhost:8000"). Then go to add `USE_SUBDOMAIN = True` to local_settings.py.
## Deploy ## Deploy
Most of the steps are similar to Django tutorials. Here are two usual steps: Most of the steps are similar to Django tutorials. Here are two usual steps:
1. Update vietnamese translation: 1. Update vietnamese translation:
- If you add any new phrases in the code, ```python manage.py makemessages``` - If you add any new phrases in the code, ```python3 manage.py makemessages```
- go to `locale/vi` - go to `locale/vi`
- modify `.po` file - modify `.po` file
- ```python manage.py compilemessages``` - ```python3 manage.py compilemessages```
- ```python manage.py compilejsi18n``` - ```python3 manage.py compilejsi18n```
2. Update styles (using SASS) 2. Update styles (using SASS)
- Change .css/.scss files in `resources` folder - Change .css/.scss files in `resources` folder
- ```./make_style && python manage.py collectstatic``` - ```./make_style.sh && python3 manage.py collectstatic```
- Sometimes you need to `Ctrl + F5` to see the new user interface in browser. - Sometimes you need to press `Ctrl + F5` to see the new user interface in browser.
## Screenshots ## Screenshots
@ -100,7 +304,8 @@ Most of the steps are similar to Django tutorials. Here are two usual steps:
Leaderboard with information about contest rating, performance points and real name of all users. Leaderboard with information about contest rating, performance points and real name of all users.
![](https://i.imgur.com/ampxHXM.png) ![](https://raw.githubusercontent.com/emladevops/LQDOJ-image/main/brave_SK67WA26FA.png#gh-light-mode-only)
![](https://raw.githubusercontent.com/emladevops/LQDOJ-image/main/brave_cmqqCnwaFc.png#gh-dark-mode-only)
### Admin dashboard ### Admin dashboard
@ -118,4 +323,5 @@ You can write the problems' statement in Markdown with LaTeX figures and formula
Users can communicate with each other and can see who's online. Users can communicate with each other and can see who's online.
![](https://i.imgur.com/y9SGCgl.png) ![](https://raw.githubusercontent.com/emladevops/LQDOJ-image/main/brave_kPsC5bJluc.png#gh-light-mode-only)
![](https://raw.githubusercontent.com/emladevops/LQDOJ-image/main/brave_AtrEzXzEAx.png#gh-dark-mode-only)

View file

@ -2,4 +2,7 @@ from django.apps import AppConfig
class ChatBoxConfig(AppConfig): class ChatBoxConfig(AppConfig):
name = 'chat_box' name = "chat_box"
def ready(self):
from . import models

View file

@ -9,22 +9,43 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [
('judge', '0100_auto_20200127_0059'), ("judge", "0100_auto_20200127_0059"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Message', name="Message",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('time', models.DateTimeField(auto_now_add=True, verbose_name='posted time')), "id",
('body', models.TextField(max_length=8192, verbose_name='body of comment')), models.AutoField(
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='judge.Profile', verbose_name='user')), auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"time",
models.DateTimeField(auto_now_add=True, verbose_name="posted time"),
),
(
"body",
models.TextField(max_length=8192, verbose_name="body of comment"),
),
(
"author",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="judge.Profile",
verbose_name="user",
),
),
], ],
options={ options={
'verbose_name': 'message', "verbose_name": "message",
'verbose_name_plural': 'messages', "verbose_name_plural": "messages",
'ordering': ('-time',), "ordering": ("-time",),
}, },
), ),
] ]

View file

@ -6,13 +6,13 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('chat_box', '0001_initial'), ("chat_box", "0001_initial"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='message', model_name="message",
name='hidden', name="hidden",
field=models.BooleanField(default=False, verbose_name='is hidden'), field=models.BooleanField(default=False, verbose_name="is hidden"),
), ),
] ]

View file

@ -6,13 +6,13 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('chat_box', '0002_message_hidden'), ("chat_box", "0002_message_hidden"),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='message', model_name="message",
name='hidden', name="hidden",
field=models.BooleanField(default=True, verbose_name='is hidden'), field=models.BooleanField(default=True, verbose_name="is hidden"),
), ),
] ]

View file

@ -6,13 +6,13 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('chat_box', '0003_auto_20200505_2306'), ("chat_box", "0003_auto_20200505_2306"),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='message', model_name="message",
name='hidden', name="hidden",
field=models.BooleanField(default=False, verbose_name='is hidden'), field=models.BooleanField(default=False, verbose_name="is hidden"),
), ),
] ]

View file

@ -7,22 +7,52 @@ import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('judge', '0116_auto_20211011_0645'), ("judge", "0116_auto_20211011_0645"),
('chat_box', '0004_auto_20200505_2336'), ("chat_box", "0004_auto_20200505_2336"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Room', name="Room",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('user_one', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_one', to='judge.Profile', verbose_name='user 1')), "id",
('user_two', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_two', to='judge.Profile', verbose_name='user 2')), models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"user_one",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="user_one",
to="judge.Profile",
verbose_name="user 1",
),
),
(
"user_two",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="user_two",
to="judge.Profile",
verbose_name="user 2",
),
),
], ],
), ),
migrations.AddField( migrations.AddField(
model_name='message', model_name="message",
name='room', name="room",
field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='chat_box.Room', verbose_name='room id'), field=models.ForeignKey(
default=None,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="chat_box.Room",
verbose_name="room id",
),
), ),
] ]

View file

@ -7,18 +7,42 @@ import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('judge', '0116_auto_20211011_0645'), ("judge", "0116_auto_20211011_0645"),
('chat_box', '0005_auto_20211011_0714'), ("chat_box", "0005_auto_20211011_0714"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='UserRoom', name="UserRoom",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('last_seen', models.DateTimeField(verbose_name='last seen')), "id",
('room', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='chat_box.Room', verbose_name='room id')), models.AutoField(
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='judge.Profile', verbose_name='user')), auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("last_seen", models.DateTimeField(verbose_name="last seen")),
(
"room",
models.ForeignKey(
default=None,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="chat_box.Room",
verbose_name="room id",
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="judge.Profile",
verbose_name="user",
),
),
], ],
), ),
] ]

View file

@ -6,13 +6,13 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('chat_box', '0006_userroom'), ("chat_box", "0006_userroom"),
] ]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='userroom', model_name="userroom",
name='last_seen', name="last_seen",
field=models.DateTimeField(auto_now_add=True, verbose_name='last seen'), field=models.DateTimeField(auto_now_add=True, verbose_name="last seen"),
), ),
] ]

View file

@ -7,17 +7,33 @@ import django.db.models.deletion
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('judge', '0116_auto_20211011_0645'), ("judge", "0116_auto_20211011_0645"),
('chat_box', '0007_auto_20211112_1255'), ("chat_box", "0007_auto_20211112_1255"),
] ]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
name='Ignore', name="Ignore",
fields=[ fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), (
('ignored_users', models.ManyToManyField(to='judge.Profile')), "id",
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ignored_chat_users', to='judge.Profile', verbose_name='user')), models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("ignored_users", models.ManyToManyField(to="judge.Profile")),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="ignored_chat_users",
to="judge.Profile",
verbose_name="user",
),
),
], ],
), ),
] ]

View file

@ -0,0 +1,24 @@
# Generated by Django 2.2.25 on 2022-06-18 07:52
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("chat_box", "0008_ignore"),
]
operations = [
migrations.AlterField(
model_name="ignore",
name="user",
field=models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name="ignored_chat_users",
to="judge.Profile",
verbose_name="user",
),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 2.2.25 on 2022-10-27 20:00
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("judge", "0135_auto_20221028_0300"),
("chat_box", "0009_auto_20220618_1452"),
]
operations = [
migrations.AlterUniqueTogether(
name="userroom",
unique_together={("user", "room")},
),
]

View file

@ -0,0 +1,20 @@
# Generated by Django 3.2.18 on 2023-02-18 21:10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("chat_box", "0010_auto_20221028_0300"),
]
operations = [
migrations.AlterField(
model_name="message",
name="hidden",
field=models.BooleanField(
db_index=True, default=False, verbose_name="is hidden"
),
),
]

View file

@ -0,0 +1,34 @@
# Generated by Django 3.2.18 on 2023-03-08 07:17
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("judge", "0154_add_submission_indexes"),
("chat_box", "0011_alter_message_hidden"),
]
operations = [
migrations.AlterModelOptions(
name="message",
options={
"ordering": ("-id",),
"verbose_name": "message",
"verbose_name_plural": "messages",
},
),
migrations.AlterField(
model_name="message",
name="hidden",
field=models.BooleanField(default=False, verbose_name="is hidden"),
),
migrations.AddIndex(
model_name="message",
index=models.Index(
fields=["hidden", "room", "-id"], name="chat_box_me_hidden_b2307a_idx"
),
),
]

View file

@ -0,0 +1,20 @@
# Generated by Django 3.2.18 on 2023-08-28 01:24
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("chat_box", "0012_auto_20230308_1417"),
]
operations = [
migrations.AlterField(
model_name="message",
name="time",
field=models.DateTimeField(
auto_now_add=True, db_index=True, verbose_name="posted time"
),
),
]

View file

@ -0,0 +1,38 @@
# Generated by Django 3.2.18 on 2023-08-28 06:02
from django.db import migrations, models
def migrate(apps, schema_editor):
UserRoom = apps.get_model("chat_box", "UserRoom")
Message = apps.get_model("chat_box", "Message")
for ur in UserRoom.objects.all():
if not ur.room:
continue
messages = ur.room.message_set
last_msg = messages.first()
try:
if last_msg and last_msg.author != ur.user:
ur.unread_count = messages.filter(time__gte=ur.last_seen).count()
else:
ur.unread_count = 0
ur.save()
except:
continue
class Migration(migrations.Migration):
dependencies = [
("chat_box", "0013_alter_message_time"),
]
operations = [
migrations.AddField(
model_name="userroom",
name="unread_count",
field=models.IntegerField(db_index=True, default=0),
),
migrations.RunPython(migrate, migrations.RunPython.noop, atomic=True),
]

View file

@ -0,0 +1,33 @@
# Generated by Django 3.2.18 on 2023-11-02 01:41
from django.db import migrations, models
def migrate(apps, schema_editor):
Room = apps.get_model("chat_box", "Room")
Message = apps.get_model("chat_box", "Message")
for room in Room.objects.all():
messages = room.message_set
last_msg = messages.first()
if last_msg:
room.last_msg_time = last_msg.time
room.save()
class Migration(migrations.Migration):
dependencies = [
("chat_box", "0014_userroom_unread_count"),
]
operations = [
migrations.AddField(
model_name="room",
name="last_msg_time",
field=models.DateTimeField(
db_index=True, null=True, verbose_name="last seen"
),
),
migrations.RunPython(migrate, migrations.RunPython.noop, atomic=True),
]

View file

@ -0,0 +1,32 @@
# Generated by Django 3.2.18 on 2024-08-22 03:12
from django.db import migrations, models
def remove_duplicates(apps, schema_editor):
Room = apps.get_model("chat_box", "Room")
seen = set()
for room in Room.objects.all():
pair = (room.user_one_id, room.user_two_id)
reverse_pair = (room.user_two_id, room.user_one_id)
if pair in seen or reverse_pair in seen:
room.delete()
else:
seen.add(pair)
class Migration(migrations.Migration):
dependencies = [
("chat_box", "0015_room_last_msg_time"),
]
operations = [
migrations.RunPython(remove_duplicates),
migrations.AlterUniqueTogether(
name="room",
unique_together={("user_one", "user_two")},
),
]

View file

@ -1,57 +1,113 @@
from django.db import models from django.db import models
from django.db.models import CASCADE from django.db.models import CASCADE, Q
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.utils.functional import cached_property
from judge.models.profile import Profile from judge.models.profile import Profile
from judge.caching import cache_wrapper
__all__ = ['Message'] __all__ = ["Message", "Room", "UserRoom", "Ignore"]
class Room(models.Model): class Room(models.Model):
user_one = models.ForeignKey(Profile, related_name="user_one", verbose_name='user 1', on_delete=CASCADE) user_one = models.ForeignKey(
user_two = models.ForeignKey(Profile, related_name="user_two", verbose_name='user 2', on_delete=CASCADE) Profile, related_name="user_one", verbose_name="user 1", on_delete=CASCADE
)
user_two = models.ForeignKey(
Profile, related_name="user_two", verbose_name="user 2", on_delete=CASCADE
)
last_msg_time = models.DateTimeField(
verbose_name=_("last seen"), null=True, db_index=True
)
class Meta:
app_label = "chat_box"
unique_together = ("user_one", "user_two")
@cached_property
def _cached_info(self):
return get_room_info(self.id)
def contain(self, profile): def contain(self, profile):
return self.user_one == profile or self.user_two == profile return profile.id in [self.user_one_id, self.user_two_id]
def other_user(self, profile): def other_user(self, profile):
return self.user_one if profile == self.user_two else self.user_two return self.user_one if profile == self.user_two else self.user_two
def other_user_id(self, profile):
user_ids = [self.user_one_id, self.user_two_id]
return sum(user_ids) - profile.id
def users(self): def users(self):
return [self.user_one, self.user_two] return [self.user_one, self.user_two]
def last_message_body(self):
return self._cached_info["last_message"]
@classmethod
def prefetch_room_cache(self, room_ids):
get_room_info.prefetch_multi([(i,) for i in room_ids])
class Message(models.Model): class Message(models.Model):
author = models.ForeignKey(Profile, verbose_name=_('user'), on_delete=CASCADE) author = models.ForeignKey(Profile, verbose_name=_("user"), on_delete=CASCADE)
time = models.DateTimeField(verbose_name=_('posted time'), auto_now_add=True) time = models.DateTimeField(
body = models.TextField(verbose_name=_('body of comment'), max_length=8192) verbose_name=_("posted time"), auto_now_add=True, db_index=True
hidden = models.BooleanField(verbose_name='is hidden', default=False) )
room = models.ForeignKey(Room, verbose_name='room id', on_delete=CASCADE, default=None, null=True) body = models.TextField(verbose_name=_("body of comment"), max_length=8192)
hidden = models.BooleanField(verbose_name="is hidden", default=False)
room = models.ForeignKey(
Room, verbose_name="room id", on_delete=CASCADE, default=None, null=True
)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
new_message = self.id
self.body = self.body.strip() self.body = self.body.strip()
super(Message, self).save(*args, **kwargs) super(Message, self).save(*args, **kwargs)
class Meta: class Meta:
app_label = 'chat_box' verbose_name = "message"
verbose_name = 'message' verbose_name_plural = "messages"
verbose_name_plural = 'messages' ordering = ("-id",)
ordering = ('-time',) indexes = [
models.Index(fields=["hidden", "room", "-id"]),
]
app_label = "chat_box"
class UserRoom(models.Model): class UserRoom(models.Model):
user = models.ForeignKey(Profile, verbose_name=_('user'), on_delete=CASCADE) user = models.ForeignKey(Profile, verbose_name=_("user"), on_delete=CASCADE)
room = models.ForeignKey(Room, verbose_name='room id', on_delete=CASCADE, default=None, null=True) room = models.ForeignKey(
last_seen = models.DateTimeField(verbose_name=_('last seen'), auto_now_add=True) Room, verbose_name="room id", on_delete=CASCADE, default=None, null=True
)
last_seen = models.DateTimeField(verbose_name=_("last seen"), auto_now_add=True)
unread_count = models.IntegerField(default=0, db_index=True)
class Meta:
unique_together = ("user", "room")
app_label = "chat_box"
class Ignore(models.Model): class Ignore(models.Model):
user = models.ForeignKey(Profile, related_name="ignored_chat_users", verbose_name=_('user'), on_delete=CASCADE) user = models.OneToOneField(
Profile,
related_name="ignored_chat_users",
verbose_name=_("user"),
on_delete=CASCADE,
db_index=True,
)
ignored_users = models.ManyToManyField(Profile) ignored_users = models.ManyToManyField(Profile)
class Meta:
app_label = "chat_box"
@classmethod @classmethod
def is_ignored(self, current_user, new_friend): def is_ignored(self, current_user, new_friend):
try: try:
return current_user.ignored_chat_users.get().ignored_users \ return current_user.ignored_chat_users.ignored_users.filter(
.filter(id=new_friend.id).exists() id=new_friend.id
).exists()
except: except:
return False return False
@ -59,26 +115,40 @@ class Ignore(models.Model):
def get_ignored_users(self, user): def get_ignored_users(self, user):
try: try:
return self.objects.get(user=user).ignored_users.all() return self.objects.get(user=user).ignored_users.all()
except Ignore.DoesNotExist: except:
return Profile.objects.none() return Profile.objects.none()
@classmethod @classmethod
def add_ignore(self, current_user, friend): def get_ignored_rooms(self, user):
ignore, created = self.objects.get_or_create( try:
user = current_user ignored_users = self.objects.get(user=user).ignored_users.all()
return Room.objects.filter(Q(user_one=user) | Q(user_two=user)).filter(
Q(user_one__in=ignored_users) | Q(user_two__in=ignored_users)
) )
except:
return Room.objects.none()
@classmethod
def add_ignore(self, current_user, friend):
ignore, created = self.objects.get_or_create(user=current_user)
ignore.ignored_users.add(friend) ignore.ignored_users.add(friend)
@classmethod @classmethod
def remove_ignore(self, current_user, friend): def remove_ignore(self, current_user, friend):
ignore, created = self.objects.get_or_create( ignore, created = self.objects.get_or_create(user=current_user)
user = current_user
)
ignore.ignored_users.remove(friend) ignore.ignored_users.remove(friend)
@classmethod @classmethod
def toggle_ignore(self, current_user, friend): def toggle_ignore(self, current_user, friend):
if (self.is_ignored(current_user, friend)): if self.is_ignored(current_user, friend):
self.remove_ignore(current_user, friend) self.remove_ignore(current_user, friend)
else: else:
self.add_ignore(current_user, friend) self.add_ignore(current_user, friend)
@cache_wrapper(prefix="Rinfo")
def get_room_info(room_id):
last_msg = Message.objects.filter(room_id=room_id).first()
return {
"last_message": last_msg.body if last_msg else None,
}

View file

@ -1,18 +1,51 @@
from cryptography.fernet import Fernet from cryptography.fernet import Fernet
import hmac
import hashlib
from django.conf import settings from django.conf import settings
from django.db.models import OuterRef, Count, Subquery, IntegerField, Q
from django.db.models.functions import Coalesce
from chat_box.models import Ignore, Message, UserRoom, Room
from judge.caching import cache_wrapper
secret_key = settings.CHAT_SECRET_KEY secret_key = settings.CHAT_SECRET_KEY
fernet = Fernet(secret_key) fernet = Fernet(secret_key)
def encrypt_url(creator_id, other_id): def encrypt_url(creator_id, other_id):
message = str(creator_id) + '_' + str(other_id) message = str(creator_id) + "_" + str(other_id)
return fernet.encrypt(message.encode()).decode() return fernet.encrypt(message.encode()).decode()
def decrypt_url(message_encrypted): def decrypt_url(message_encrypted):
try: try:
dec_message = fernet.decrypt(message_encrypted.encode()).decode() dec_message = fernet.decrypt(message_encrypted.encode()).decode()
creator_id, other_id = dec_message.split('_') creator_id, other_id = dec_message.split("_")
return int(creator_id), int(other_id) return int(creator_id), int(other_id)
except Exception as e: except Exception as e:
return None, None return None, None
def encrypt_channel(channel):
return (
hmac.new(
settings.CHAT_SECRET_KEY.encode(),
channel.encode(),
hashlib.sha512,
).hexdigest()[:16]
+ "%s" % channel
)
@cache_wrapper(prefix="gub")
def get_unread_boxes(profile):
ignored_rooms = Ignore.get_ignored_rooms(profile)
unread_boxes = (
UserRoom.objects.filter(user=profile, unread_count__gt=0)
.exclude(room__in=ignored_rooms)
.count()
)
return unread_boxes

View file

@ -1,47 +1,77 @@
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from django.views.generic import ListView from django.views.generic import ListView
from django.http import HttpResponse, JsonResponse, HttpResponseBadRequest, HttpResponsePermanentRedirect, HttpResponseRedirect from django.http import (
HttpResponse,
JsonResponse,
HttpResponseBadRequest,
HttpResponsePermanentRedirect,
HttpResponseRedirect,
)
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.shortcuts import render from django.shortcuts import render
from django.forms.models import model_to_dict from django.forms.models import model_to_dict
from django.db.models import Case, BooleanField, When, Q, Subquery, OuterRef, Exists, Count, IntegerField from django.db.models import (
Case,
BooleanField,
When,
Q,
Subquery,
OuterRef,
Exists,
Count,
IntegerField,
F,
Max,
)
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce
from django.utils import timezone from django.utils import timezone
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.urls import reverse from django.urls import reverse
import datetime
from judge import event_poster as event from judge import event_poster as event
from judge.jinja2.gravatar import gravatar from judge.jinja2.gravatar import gravatar
from judge.models import Friend from judge.models import Friend
from chat_box.models import Message, Profile, Room, UserRoom, Ignore from chat_box.models import Message, Profile, Room, UserRoom, Ignore, get_room_info
from chat_box.utils import encrypt_url, decrypt_url from chat_box.utils import encrypt_url, decrypt_url, encrypt_channel, get_unread_boxes
import json from reversion import revisions
class ChatView(ListView): class ChatView(ListView):
context_object_name = 'message' context_object_name = "message"
template_name = 'chat/chat.html' template_name = "chat/chat.html"
title = _('Chat Box') title = _("LQDOJ Chat")
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.room_id = None self.room_id = None
self.room = None self.room = None
self.paginate_by = 50
self.messages = None self.messages = None
self.paginator = None self.first_page_size = 20 # only for first request
self.follow_up_page_size = 50
def get_queryset(self): def get_queryset(self):
return self.messages return self.messages
def has_next(self):
try:
msg = Message.objects.filter(room=self.room_id).earliest("id")
except Exception as e:
return False
return msg not in self.messages
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
request_room = kwargs['room_id'] request_room = kwargs["room_id"]
page = request.GET.get('page') page_size = self.follow_up_page_size
try:
last_id = int(request.GET.get("last_id"))
except Exception:
last_id = 1e15
page_size = self.first_page_size
only_messages = request.GET.get("only_messages")
if request_room: if request_room:
try: try:
@ -53,180 +83,261 @@ class ChatView(ListView):
else: else:
request_room = None request_room = None
if request_room != self.room_id or not self.messages:
self.room_id = request_room self.room_id = request_room
self.messages = Message.objects.filter(hidden=False, room=self.room_id) self.messages = (
self.paginator = Paginator(self.messages, self.paginate_by) Message.objects.filter(hidden=False, room=self.room_id, id__lt=last_id)
.select_related("author")
if page == None: .only("body", "time", "author__rating", "author__display_rank")[:page_size]
update_last_seen(request, **kwargs) )
if not only_messages:
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
cur_page = self.paginator.get_page(page) return render(
request,
return render(request, 'chat/message_list.html', { "chat/message_list.html",
'object_list': cur_page.object_list, {
'num_pages': self.paginator.num_pages "object_list": self.messages,
}) "has_next": self.has_next(),
},
)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['title'] = self.title context["title"] = self.title
context['last_msg'] = event.last() context["last_msg"] = event.last()
context['status_sections'] = get_status_context(self.request) context["status_sections"] = get_status_context(self.request.profile)
context['room'] = self.room_id context["room"] = self.room_id
context['unread_count_lobby'] = get_unread_count(None, self.request.profile) context["has_next"] = self.has_next()
context["unread_count_lobby"] = get_unread_count(None, self.request.profile)
context["chat_channel"] = encrypt_channel(
"chat_" + str(self.request.profile.id)
)
context["chat_lobby_channel"] = encrypt_channel("chat_lobby")
if self.room: if self.room:
users_room = [self.room.user_one, self.room.user_two] users_room = [self.room.user_one, self.room.user_two]
users_room.remove(self.request.profile) users_room.remove(self.request.profile)
context['other_user'] = users_room[0] context["other_user"] = users_room[0]
context['other_online'] = get_user_online_status(context['other_user']) context["other_online"] = get_user_online_status(context["other_user"])
context['is_ignored'] = Ignore.is_ignored(self.request.profile, context['other_user']) context["is_ignored"] = Ignore.is_ignored(
self.request.profile, context["other_user"]
)
else: else:
context['online_count'] = get_online_count() context["online_count"] = get_online_count()
context['message_template'] = { context["message_template"] = {
'author': self.request.profile, "author": self.request.profile,
'id': '$id', "id": "$id",
'time': timezone.now(), "time": timezone.now(),
'body': '$body' "body": "$body",
} }
return context return context
def delete_message(request): def delete_message(request):
ret = {'delete': 'done'} ret = {"delete": "done"}
if request.method == 'GET': if request.method == "GET":
return JsonResponse(ret) return HttpResponseBadRequest()
if request.user.is_staff:
try: try:
messid = int(request.POST.get('message')) messid = int(request.POST.get("message"))
mess = Message.objects.get(id=messid) mess = Message.objects.get(id=messid)
except: except:
return HttpResponseBadRequest() return HttpResponseBadRequest()
if not request.user.is_staff and request.profile != mess.author:
return HttpResponseBadRequest()
mess.hidden = True mess.hidden = True
mess.save() mess.save()
return JsonResponse(ret) return JsonResponse(ret)
def mute_message(request):
ret = {"mute": "done"}
if request.method == "GET":
return HttpResponseBadRequest()
if not request.user.is_staff:
return HttpResponseBadRequest()
try:
messid = int(request.POST.get("message"))
mess = Message.objects.get(id=messid)
except:
return HttpResponseBadRequest()
with revisions.create_revision():
revisions.set_comment(_("Mute chat") + ": " + mess.body)
revisions.set_user(request.user)
mess.author.mute = True
mess.author.save()
Message.objects.filter(room=None, author=mess.author).update(hidden=True)
return JsonResponse(ret) return JsonResponse(ret)
def check_valid_message(request, room):
if not room and len(request.POST["body"]) > 200:
return False
if not can_access_room(request, room) or request.profile.mute:
return False
last_msg = Message.objects.filter(room=room).first()
if (
last_msg
and last_msg.author == request.profile
and last_msg.body == request.POST["body"].strip()
):
return False
if not room:
four_last_msg = Message.objects.filter(room=room).order_by("-id")[:4]
if len(four_last_msg) >= 4:
same_author = all(msg.author == request.profile for msg in four_last_msg)
time_diff = timezone.now() - four_last_msg[3].time
if same_author and time_diff.total_seconds() < 300:
return False
return True
@login_required @login_required
def post_message(request): def post_message(request):
ret = {'msg': 'posted'} ret = {"msg": "posted"}
if request.method != 'POST':
if request.method != "POST":
return HttpResponseBadRequest()
if len(request.POST["body"]) > 5000 or len(request.POST["body"].strip()) == 0:
return HttpResponseBadRequest() return HttpResponseBadRequest()
room = None room = None
if request.POST['room']: if request.POST["room"]:
room = Room.objects.get(id=request.POST['room']) room = Room.objects.get(id=request.POST["room"])
if not can_access_room(request, room) or request.profile.mute: if not check_valid_message(request, room):
return HttpResponseBadRequest() return HttpResponseBadRequest()
new_message = Message(author=request.profile, new_message = Message(author=request.profile, body=request.POST["body"], room=room)
body=request.POST['body'],
room=room)
new_message.save() new_message.save()
if not room: if not room:
event.post('chat_lobby', { event.post(
'type': 'lobby', encrypt_channel("chat_lobby"),
'author_id': request.profile.id, {
'message': new_message.id, "type": "lobby",
'room': 'None', "author_id": request.profile.id,
'tmp_id': request.POST.get('tmp_id') "message": new_message.id,
}) "room": "None",
"tmp_id": request.POST.get("tmp_id"),
},
)
else: else:
get_room_info.dirty(room.id)
room.last_msg_time = new_message.time
room.save()
for user in room.users(): for user in room.users():
event.post('chat_' + str(user.id), { event.post(
'type': 'private', encrypt_channel("chat_" + str(user.id)),
'author_id': request.profile.id, {
'message': new_message.id, "type": "private",
'room': room.id, "author_id": request.profile.id,
'tmp_id': request.POST.get('tmp_id') "message": new_message.id,
}) "room": room.id,
"tmp_id": request.POST.get("tmp_id"),
},
)
if user != request.profile:
UserRoom.objects.filter(user=user, room=room).update(
unread_count=F("unread_count") + 1
)
get_unread_boxes.dirty(user)
return JsonResponse(ret) return JsonResponse(ret)
def can_access_room(request, room): def can_access_room(request, room):
return not room or room.user_one == request.profile or room.user_two == request.profile return not room or room.contain(request.profile)
@login_required @login_required
def chat_message_ajax(request): def chat_message_ajax(request):
if request.method != 'GET': if request.method != "GET":
return HttpResponseBadRequest() return HttpResponseBadRequest()
try: try:
message_id = request.GET['message'] message_id = request.GET["message"]
except KeyError: except KeyError:
return HttpResponseBadRequest() return HttpResponseBadRequest()
try: try:
message = Message.objects.filter(hidden=False).get(id=message_id) message = Message.objects.filter(hidden=False).get(id=message_id)
room = message.room room = message.room
if room and not room.contain(request.profile): if not can_access_room(request, room):
return HttpResponse('Unauthorized', status=401) return HttpResponse("Unauthorized", status=401)
except Message.DoesNotExist: except Message.DoesNotExist:
return HttpResponseBadRequest() return HttpResponseBadRequest()
return render(request, 'chat/message.html', { return render(
'message': message, request,
}) "chat/message.html",
{
"message": message,
},
)
@login_required @login_required
def update_last_seen(request, **kwargs): def update_last_seen(request, **kwargs):
if 'room_id' in kwargs: if "room_id" in kwargs:
room_id = kwargs['room_id'] room_id = kwargs["room_id"]
elif request.method == 'GET': elif request.method == "GET":
room_id = request.GET.get('room') room_id = request.GET.get("room")
elif request.method == 'POST': elif request.method == "POST":
room_id = request.POST.get('room') room_id = request.POST.get("room")
else: else:
return HttpResponseBadRequest() return HttpResponseBadRequest()
try: try:
profile = request.profile profile = request.profile
room = None room = None
if room_id: if room_id:
room = Room.objects.get(id=int(room_id)) room = Room.objects.filter(id=int(room_id)).first()
except Room.DoesNotExist: except Room.DoesNotExist:
return HttpResponseBadRequest() return HttpResponseBadRequest()
except Exception as e:
return HttpResponseBadRequest()
if room and not room.contain(profile): if not can_access_room(request, room):
return HttpResponseBadRequest() return HttpResponseBadRequest()
user_room, _ = UserRoom.objects.get_or_create(user=profile, room=room) user_room, _ = UserRoom.objects.get_or_create(user=profile, room=room)
user_room.last_seen = timezone.now() user_room.last_seen = timezone.now()
user_room.unread_count = 0
user_room.save() user_room.save()
return JsonResponse({'msg': 'updated'}) get_unread_boxes.dirty(profile)
return JsonResponse({"msg": "updated"})
def get_online_count(): def get_online_count():
last_two_minutes = timezone.now()-timezone.timedelta(minutes=2) last_5_minutes = timezone.now() - timezone.timedelta(minutes=5)
return Profile.objects.filter(last_access__gte=last_two_minutes).count() return Profile.objects.filter(last_access__gte=last_5_minutes).count()
def get_user_online_status(user): def get_user_online_status(user):
time_diff = timezone.now() - user.last_access time_diff = timezone.now() - user.last_access
is_online = time_diff <= timezone.timedelta(minutes=2) is_online = time_diff <= timezone.timedelta(minutes=5)
return is_online return is_online
def user_online_status_ajax(request): def user_online_status_ajax(request):
if request.method != 'GET': if request.method != "GET":
return HttpResponseBadRequest() return HttpResponseBadRequest()
user_id = request.GET.get('user') user_id = request.GET.get("user")
if user_id: if user_id:
try: try:
@ -236,109 +347,126 @@ def user_online_status_ajax(request):
return HttpResponseBadRequest() return HttpResponseBadRequest()
is_online = get_user_online_status(user) is_online = get_user_online_status(user)
return render(request, 'chat/user_online_status.html', { return render(
'other_user': user, request,
'other_online': is_online, "chat/user_online_status.html",
'is_ignored': Ignore.is_ignored(request.profile, user) {
}) "other_user": user,
"other_online": is_online,
"is_ignored": Ignore.is_ignored(request.profile, user),
},
)
else: else:
return render(request, 'chat/user_online_status.html', { return render(
'online_count': get_online_count(), request,
}) "chat/user_online_status.html",
{
"online_count": get_online_count(),
},
)
def get_online_status(request_user, queryset, rooms=None): def get_online_status(profile, other_profile_ids, rooms=None):
if not queryset: if not other_profile_ids:
return None return None
last_two_minutes = timezone.now()-timezone.timedelta(minutes=2) Profile.prefetch_profile_cache(other_profile_ids)
joined_ids = ",".join([str(id) for id in other_profile_ids])
other_profiles = Profile.objects.raw(
f"SELECT * from judge_profile where id in ({joined_ids}) order by field(id,{joined_ids})"
)
last_5_minutes = timezone.now() - timezone.timedelta(minutes=5)
ret = [] ret = []
if rooms: if rooms:
unread_count = get_unread_count(rooms, request_user) unread_count = get_unread_count(rooms, profile)
count = {} count = {}
last_msg = {}
room_of_user = {}
for i in unread_count: for i in unread_count:
count[i['other_user']] = i['unread_count'] room = Room.objects.get(id=i["room"])
other_profile = room.other_user(profile)
count[other_profile.id] = i["unread_count"]
rooms = Room.objects.filter(id__in=rooms)
for room in rooms:
other_profile_id = room.other_user_id(profile)
last_msg[other_profile_id] = room.last_message_body()
room_of_user[other_profile_id] = room.id
for user in queryset: for other_profile in other_profiles:
is_online = False is_online = False
if (user.last_access >= last_two_minutes): if other_profile.last_access >= last_5_minutes:
is_online = True is_online = True
user_dict = {'user': user, 'is_online': is_online} user_dict = {"user": other_profile, "is_online": is_online}
if rooms and user.id in count: if rooms:
user_dict['unread_count'] = count[user.id] user_dict.update(
user_dict['url'] = encrypt_url(request_user.id, user.id) {
"unread_count": count.get(other_profile.id),
"last_msg": last_msg.get(other_profile.id),
"room": room_of_user.get(other_profile.id),
}
)
user_dict["url"] = encrypt_url(profile.id, other_profile.id)
ret.append(user_dict) ret.append(user_dict)
return ret return ret
def get_status_context(request, include_ignored=False): def get_status_context(profile, include_ignored=False):
if include_ignored: if include_ignored:
ignored_users = Profile.objects.none() ignored_users = []
queryset = Profile.objects queryset = Profile.objects
else: else:
ignored_users = Ignore.get_ignored_users(request.profile) ignored_users = list(
Ignore.get_ignored_users(profile).values_list("id", flat=True)
)
queryset = Profile.objects.exclude(id__in=ignored_users) queryset = Profile.objects.exclude(id__in=ignored_users)
last_two_minutes = timezone.now()-timezone.timedelta(minutes=2) last_5_minutes = timezone.now() - timezone.timedelta(minutes=5)
recent_profile = Room.objects.filter( recent_profile = (
Q(user_one=request.profile) | Q(user_two=request.profile) Room.objects.filter(Q(user_one=profile) | Q(user_two=profile))
).annotate( .annotate(
last_msg_time=Subquery(
Message.objects.filter(room=OuterRef('pk')).values('time')[:1]
),
other_user=Case( other_user=Case(
When(user_one=request.profile, then='user_two'), When(user_one=profile, then="user_two"),
default='user_one', default="user_one",
),
)
.filter(last_msg_time__isnull=False)
.exclude(other_user__in=ignored_users)
.order_by("-last_msg_time")
.values("other_user", "id")[:20]
) )
).filter(last_msg_time__isnull=False)\
.exclude(other_user__in=ignored_users)\
.order_by('-last_msg_time').values('other_user', 'id')[:20]
recent_profile_id = [str(i['other_user']) for i in recent_profile] recent_profile_ids = [str(i["other_user"]) for i in recent_profile]
joined_id = ','.join(recent_profile_id) recent_rooms = [int(i["id"]) for i in recent_profile]
recent_rooms = [int(i['id']) for i in recent_profile] Room.prefetch_room_cache(recent_rooms)
recent_list = None
if joined_id: admin_list = (
recent_list = Profile.objects.raw( queryset.filter(display_rank="admin")
f'SELECT * from judge_profile where id in ({joined_id}) order by field(id,{joined_id})') .exclude(id__in=recent_profile_ids)
friend_list = Friend.get_friend_profiles(request.profile).exclude(id__in=recent_profile_id)\ .values_list("id", flat=True)
.exclude(id__in=ignored_users)\ )
.order_by('-last_access')
admin_list = queryset.filter(display_rank='admin')\
.exclude(id__in=friend_list).exclude(id__in=recent_profile_id)
all_user_status = queryset\
.filter(display_rank='user',
last_access__gte = last_two_minutes)\
.annotate(is_online=Case(default=True,output_field=BooleanField()))\
.order_by('-rating').exclude(id__in=friend_list).exclude(id__in=admin_list)\
.exclude(id__in=recent_profile_id)[:30]
return [ return [
{ {
'title': 'Recent', "title": _("Recent"),
'user_list': get_online_status(request.profile, recent_list, recent_rooms), "user_list": get_online_status(profile, recent_profile_ids, recent_rooms),
}, },
{ {
'title': 'Following', "title": _("Admin"),
'user_list': get_online_status(request.profile, friend_list), "user_list": get_online_status(profile, admin_list),
},
{
'title': 'Admin',
'user_list': get_online_status(request.profile, admin_list),
},
{
'title': 'Other',
'user_list': get_online_status(request.profile, all_user_status),
}, },
] ]
@login_required @login_required
def online_status_ajax(request): def online_status_ajax(request):
return render(request, 'chat/online_status.html', { return render(
'status_sections': get_status_context(request), request,
'unread_count_lobby': get_unread_count(None, request.profile), "chat/online_status.html",
}) {
"status_sections": get_status_context(request.profile),
"unread_count_lobby": get_unread_count(None, request.profile),
},
)
@login_required @login_required
@ -351,15 +479,14 @@ def get_room(user_one, user_two):
@login_required @login_required
def get_or_create_room(request): def get_or_create_room(request):
if request.method == 'GET': if request.method == "GET":
decrypted_other_id = request.GET.get('other') decrypted_other_id = request.GET.get("other")
elif request.method == 'POST': elif request.method == "POST":
decrypted_other_id = request.POST.get('other') decrypted_other_id = request.POST.get("other")
else: else:
return HttpResponseBadRequest() return HttpResponseBadRequest()
request_id, other_id = decrypt_url(decrypted_other_id) request_id, other_id = decrypt_url(decrypted_other_id)
if not other_id or not request_id or request_id != request.profile.id: if not other_id or not request_id or request_id != request.profile.id:
return HttpResponseBadRequest() return HttpResponseBadRequest()
@ -380,47 +507,41 @@ def get_or_create_room(request):
user_room.last_seen = timezone.now() user_room.last_seen = timezone.now()
user_room.save() user_room.save()
if request.method == 'GET': room_url = reverse("chat", kwargs={"room_id": room.id})
return JsonResponse({'room': room.id, 'other_user_id': other_user.id}) if request.method == "GET":
return HttpResponseRedirect(reverse('chat', kwargs={'room_id': room.id})) return JsonResponse(
{
"room": room.id,
"other_user_id": other_user.id,
"url": room_url,
}
)
return HttpResponseRedirect(room_url)
def get_unread_count(rooms, user): def get_unread_count(rooms, user):
if rooms: if rooms:
mess = Message.objects.filter(room=OuterRef('room'), return UserRoom.objects.filter(
time__gte=OuterRef('last_seen'))\ user=user, room__in=rooms, unread_count__gt=0
.exclude(author=user)\ ).values("unread_count", "room")
.order_by().values('room')\
.annotate(unread_count=Count('pk')).values('unread_count')
return UserRoom.objects\
.filter(user=user, room__in=rooms)\
.annotate(
unread_count=Coalesce(Subquery(mess, output_field=IntegerField()), 0),
other_user=Case(
When(room__user_one=user, then='room__user_two'),
default='room__user_one',
)
).filter(unread_count__gte=1).values('other_user', 'unread_count')
else: # lobby else: # lobby
mess = Message.objects.filter(room__isnull=True, user_room = UserRoom.objects.filter(user=user, room__isnull=True).first()
time__gte=OuterRef('last_seen'))\ if not user_room:
.exclude(author=user)\ return 0
.order_by().values('room')\ last_seen = user_room.last_seen
.annotate(unread_count=Count('pk')).values('unread_count') res = (
Message.objects.filter(room__isnull=True, time__gte=last_seen)
.exclude(author=user)
.exclude(hidden=True)
.count()
)
res = UserRoom.objects\ return res
.filter(user=user, room__isnull=True)\
.annotate(
unread_count=Coalesce(Subquery(mess, output_field=IntegerField()), 0),
).values_list('unread_count', flat=True)
return res[0] if len(res) else 0
@login_required @login_required
def toggle_ignore(request, **kwargs): def toggle_ignore(request, **kwargs):
user_id = kwargs['user_id'] user_id = kwargs["user_id"]
if not user_id: if not user_id:
return HttpResponseBadRequest() return HttpResponseBadRequest()
try: try:
@ -429,28 +550,5 @@ def toggle_ignore(request, **kwargs):
return HttpResponseBadRequest() return HttpResponseBadRequest()
Ignore.toggle_ignore(request.profile, other_user) Ignore.toggle_ignore(request.profile, other_user)
next_url = request.GET.get('next', '/') next_url = request.GET.get("next", "/")
return HttpResponseRedirect(next_url) return HttpResponseRedirect(next_url)
@login_required
def get_unread_boxes(request):
if (request.method != 'GET'):
return HttpResponseBadRequest()
ignored_users = Ignore.get_ignored_users(request.profile)
mess = Message.objects.filter(room=OuterRef('room'),
time__gte=OuterRef('last_seen'))\
.exclude(author=request.profile)\
.exclude(author__in=ignored_users)\
.order_by().values('room')\
.annotate(unread_count=Count('pk')).values('unread_count')
unread_boxes = UserRoom.objects\
.filter(user=request.profile, room__isnull=False)\
.annotate(
unread_count=Coalesce(Subquery(mess, output_field=IntegerField()), 0),
).filter(unread_count__gte=1).count()
return JsonResponse({'unread_boxes': unread_boxes})

View file

@ -12,6 +12,6 @@ if (2, 2) <= django.VERSION < (3,):
# attribute where the exact query sent to the database is saved. # attribute where the exact query sent to the database is saved.
# See MySQLdb/cursors.py in the source distribution. # See MySQLdb/cursors.py in the source distribution.
# MySQLdb returns string, PyMySQL bytes. # MySQLdb returns string, PyMySQL bytes.
return force_text(getattr(cursor, '_executed', None), errors='replace') return force_text(getattr(cursor, "_executed", None), errors="replace")
DatabaseOperations.last_executed_query = last_executed_query DatabaseOperations.last_executed_query = last_executed_query

View file

@ -11,6 +11,12 @@
bottom: 0; bottom: 0;
} }
.ace_editor {
overflow: hidden;
font: 12px/normal 'Fira Code', 'Monaco', 'Menlo', monospace;
direction: 1tr;
}
.django-ace-widget.loading { .django-ace-widget.loading {
display: none; display: none;
} }

View file

@ -77,15 +77,17 @@
mode = widget.getAttribute('data-mode'), mode = widget.getAttribute('data-mode'),
theme = widget.getAttribute('data-theme'), theme = widget.getAttribute('data-theme'),
wordwrap = widget.getAttribute('data-wordwrap'), wordwrap = widget.getAttribute('data-wordwrap'),
toolbar = prev(widget), toolbar = prev(widget);
main_block = toolbar.parentNode; var main_block = div.parentNode.parentNode;
if (toolbar != null) {
// Toolbar maximize/minimize button // Toolbar maximize/minimize button
var min_max = toolbar.getElementsByClassName('django-ace-max_min'); var min_max = toolbar.getElementsByClassName('django-ace-max_min');
min_max[0].onclick = function () { min_max[0].onclick = function () {
minimizeMaximize(widget, main_block, editor); minimizeMaximize(widget, main_block, editor);
return false; return false;
}; };
}
editor.getSession().setValue(textarea.value); editor.getSession().setValue(textarea.value);
@ -160,7 +162,7 @@
]); ]);
window[widget.id] = editor; window[widget.id] = editor;
$(widget).trigger('ace_load', [editor]); setTimeout(() => $(widget).trigger('ace_load', [editor]), 100);
} }
function init() { function init() {

View file

@ -11,22 +11,33 @@ from django.utils.safestring import mark_safe
class AceWidget(forms.Textarea): class AceWidget(forms.Textarea):
def __init__(self, mode=None, theme=None, wordwrap=False, width='100%', height='300px', def __init__(
no_ace_media=False, *args, **kwargs): self,
mode=None,
theme=None,
wordwrap=False,
width="100%",
height="300px",
no_ace_media=False,
toolbar=True,
*args,
**kwargs
):
self.mode = mode self.mode = mode
self.theme = theme self.theme = theme
self.wordwrap = wordwrap self.wordwrap = wordwrap
self.width = width self.width = width
self.height = height self.height = height
self.ace_media = not no_ace_media self.ace_media = not no_ace_media
self.toolbar = toolbar
super(AceWidget, self).__init__(*args, **kwargs) super(AceWidget, self).__init__(*args, **kwargs)
@property @property
def media(self): def media(self):
js = [urljoin(settings.ACE_URL, 'ace.js')] if self.ace_media else [] js = [urljoin(settings.ACE_URL, "ace.js")] if self.ace_media else []
js.append('django_ace/widget.js') js.append("django_ace/widget.js")
css = { css = {
'screen': ['django_ace/widget.css'], "screen": ["django_ace/widget.css"],
} }
return forms.Media(js=js, css=css) return forms.Media(js=js, css=css)
@ -34,24 +45,32 @@ class AceWidget(forms.Textarea):
attrs = attrs or {} attrs = attrs or {}
ace_attrs = { ace_attrs = {
'class': 'django-ace-widget loading', "class": "django-ace-widget loading",
'style': 'width:%s; height:%s' % (self.width, self.height), "style": "width:%s; height:%s" % (self.width, self.height),
'id': 'ace_%s' % name, "id": "ace_%s" % name,
} }
if self.mode: if self.mode:
ace_attrs['data-mode'] = self.mode ace_attrs["data-mode"] = self.mode
if self.theme: if self.theme:
ace_attrs['data-theme'] = self.theme ace_attrs["data-theme"] = self.theme
if self.wordwrap: if self.wordwrap:
ace_attrs['data-wordwrap'] = 'true' ace_attrs["data-wordwrap"] = "true"
attrs.update(style='width: 100%; min-width: 100%; max-width: 100%; resize: none') attrs.update(
style="width: 100%; min-width: 100%; max-width: 100%; resize: none"
)
textarea = super(AceWidget, self).render(name, value, attrs) textarea = super(AceWidget, self).render(name, value, attrs)
html = '<div%s><div></div></div>%s' % (flatatt(ace_attrs), textarea) html = "<div%s><div></div></div>%s" % (flatatt(ace_attrs), textarea)
# add toolbar if self.toolbar:
html = ('<div class="django-ace-editor"><div style="width: 100%%" class="django-ace-toolbar">' toolbar = (
'<a href="./" class="django-ace-max_min"></a></div>%s</div>') % html '<div style="width: {}" class="django-ace-toolbar">'
'<a href="#" class="django-ace-max_min"></a>'
"</div>"
).format(self.width)
html = toolbar + html
html = '<div class="django-ace-editor">{}</div>'.format(html)
return mark_safe(html) return mark_safe(html)

View file

@ -4,24 +4,30 @@ import socket
from celery import Celery from celery import Celery
from celery.signals import task_failure from celery.signals import task_failure
app = Celery('dmoj') app = Celery("dmoj")
from django.conf import settings # noqa: E402, I202, django must be imported here from django.conf import settings # noqa: E402, I202, django must be imported here
app.config_from_object(settings, namespace='CELERY')
if hasattr(settings, 'CELERY_BROKER_URL_SECRET'): app.config_from_object(settings, namespace="CELERY")
if hasattr(settings, "CELERY_BROKER_URL_SECRET"):
app.conf.broker_url = settings.CELERY_BROKER_URL_SECRET app.conf.broker_url = settings.CELERY_BROKER_URL_SECRET
if hasattr(settings, 'CELERY_RESULT_BACKEND_SECRET'): if hasattr(settings, "CELERY_RESULT_BACKEND_SECRET"):
app.conf.result_backend = settings.CELERY_RESULT_BACKEND_SECRET app.conf.result_backend = settings.CELERY_RESULT_BACKEND_SECRET
# Load task modules from all registered Django app configs. # Load task modules from all registered Django app configs.
app.autodiscover_tasks() app.autodiscover_tasks()
# Logger to enable errors be reported. # Logger to enable errors be reported.
logger = logging.getLogger('judge.celery') logger = logging.getLogger("judge.celery")
@task_failure.connect() @task_failure.connect()
def celery_failure_log(sender, task_id, exception, traceback, *args, **kwargs): def celery_failure_log(sender, task_id, exception, traceback, *args, **kwargs):
logger.error('Celery Task %s: %s on %s', sender.name, task_id, socket.gethostname(), # noqa: G201 logger.error(
exc_info=(type(exception), exception, traceback)) "Celery Task %s: %s on %s",
sender.name,
task_id,
socket.gethostname(), # noqa: G201
exc_info=(type(exception), exception, traceback),
)

View file

@ -22,7 +22,7 @@ BASE_DIR = os.path.dirname(os.path.dirname(__file__))
# See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '5*9f5q57mqmlz2#f$x1h76&jxy#yortjl1v+l*6hd18$d*yx#0' SECRET_KEY = "5*9f5q57mqmlz2#f$x1h76&jxy#yortjl1v+l*6hd18$d*yx#0"
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True DEBUG = True
@ -30,9 +30,10 @@ DEBUG = True
ALLOWED_HOSTS = [] ALLOWED_HOSTS = []
SITE_ID = 1 SITE_ID = 1
SITE_NAME = 'LQDOJ' SITE_NAME = "LQDOJ"
SITE_LONG_NAME = 'LQDOJ: Le Quy Don Online Judge' SITE_LONG_NAME = "LQDOJ: Le Quy Don Online Judge"
SITE_ADMIN_EMAIL = False SITE_ADMIN_EMAIL = False
SITE_DOMAIN = "lqdoj.edu.vn"
DMOJ_REQUIRE_STAFF_2FA = True DMOJ_REQUIRE_STAFF_2FA = True
@ -46,11 +47,9 @@ DMOJ_PP_STEP = 0.95
DMOJ_PP_ENTRIES = 100 DMOJ_PP_ENTRIES = 100
DMOJ_PP_BONUS_FUNCTION = lambda n: 300 * (1 - 0.997**n) # noqa: E731 DMOJ_PP_BONUS_FUNCTION = lambda n: 300 * (1 - 0.997**n) # noqa: E731
NODEJS = '/usr/bin/node' NODEJS = "/usr/bin/node"
EXIFTOOL = '/usr/bin/exiftool' EXIFTOOL = "/usr/bin/exiftool"
ACE_URL = '//cdnjs.cloudflare.com/ajax/libs/ace/1.1.3' ACE_URL = "//cdnjs.cloudflare.com/ajax/libs/ace/1.1.3"
SELECT2_JS_URL = '//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/js/select2.min.js'
DEFAULT_SELECT2_CSS = '//cdnjs.cloudflare.com/ajax/libs/select2/4.0.3/css/select2.min.css'
DMOJ_CAMO_URL = None DMOJ_CAMO_URL = None
DMOJ_CAMO_KEY = None DMOJ_CAMO_KEY = None
@ -62,6 +61,7 @@ DMOJ_PROBLEM_MAX_TIME_LIMIT = 60 # seconds
DMOJ_PROBLEM_MIN_MEMORY_LIMIT = 0 # kilobytes DMOJ_PROBLEM_MIN_MEMORY_LIMIT = 0 # kilobytes
DMOJ_PROBLEM_MAX_MEMORY_LIMIT = 1048576 # kilobytes DMOJ_PROBLEM_MAX_MEMORY_LIMIT = 1048576 # kilobytes
DMOJ_PROBLEM_MIN_PROBLEM_POINTS = 0 DMOJ_PROBLEM_MIN_PROBLEM_POINTS = 0
DMOJ_SUBMISSION_ROOT = "/tmp"
DMOJ_RATING_COLORS = True DMOJ_RATING_COLORS = True
DMOJ_EMAIL_THROTTLING = (10, 60) DMOJ_EMAIL_THROTTLING = (10, 60)
DMOJ_STATS_LANGUAGE_THRESHOLD = 10 DMOJ_STATS_LANGUAGE_THRESHOLD = 10
@ -73,16 +73,20 @@ DMOJ_BLOG_NEW_CONTEST_COUNT = 7
DMOJ_BLOG_RECENTLY_ATTEMPTED_PROBLEMS_COUNT = 7 DMOJ_BLOG_RECENTLY_ATTEMPTED_PROBLEMS_COUNT = 7
DMOJ_TOTP_TOLERANCE_HALF_MINUTES = 1 DMOJ_TOTP_TOLERANCE_HALF_MINUTES = 1
DMOJ_USER_MAX_ORGANIZATION_COUNT = 10 DMOJ_USER_MAX_ORGANIZATION_COUNT = 10
DMOJ_USER_MAX_ORGANIZATION_ADD = 5
DMOJ_COMMENT_VOTE_HIDE_THRESHOLD = -5 DMOJ_COMMENT_VOTE_HIDE_THRESHOLD = -5
DMOJ_PDF_PROBLEM_CACHE = '' DMOJ_PDF_PROBLEM_CACHE = ""
DMOJ_PDF_PROBLEM_TEMP_DIR = tempfile.gettempdir() DMOJ_PDF_PROBLEM_TEMP_DIR = tempfile.gettempdir()
DMOJ_STATS_SUBMISSION_RESULT_COLORS = { DMOJ_STATS_SUBMISSION_RESULT_COLORS = {
'TLE': '#a3bcbd', "TLE": "#a3bcbd",
'AC': '#00a92a', "AC": "#00a92a",
'WA': '#ed4420', "WA": "#ed4420",
'CE': '#42586d', "CE": "#42586d",
'ERR': '#ffa71c', "ERR": "#ffa71c",
} }
DMOJ_PROFILE_IMAGE_ROOT = "profile_images"
DMOJ_ORGANIZATION_IMAGE_ROOT = "organization_images"
DMOJ_TEST_FORMATTER_ROOT = "test_formatter"
MARKDOWN_STYLES = {} MARKDOWN_STYLES = {}
MARKDOWN_DEFAULT_STYLE = {} MARKDOWN_DEFAULT_STYLE = {}
@ -90,16 +94,15 @@ MARKDOWN_DEFAULT_STYLE = {}
MATHOID_URL = False MATHOID_URL = False
MATHOID_GZIP = False MATHOID_GZIP = False
MATHOID_MML_CACHE = None MATHOID_MML_CACHE = None
MATHOID_CSS_CACHE = 'default' MATHOID_CSS_CACHE = "default"
MATHOID_DEFAULT_TYPE = 'auto' MATHOID_DEFAULT_TYPE = "auto"
MATHOID_MML_CACHE_TTL = 86400 MATHOID_MML_CACHE_TTL = 86400
MATHOID_CACHE_ROOT = tempfile.gettempdir() + '/mathoidCache' MATHOID_CACHE_ROOT = tempfile.gettempdir() + "/mathoidCache"
MATHOID_CACHE_URL = False MATHOID_CACHE_URL = False
TEXOID_GZIP = False TEXOID_GZIP = False
TEXOID_META_CACHE = 'default' TEXOID_META_CACHE = "default"
TEXOID_META_CACHE_TTL = 86400 TEXOID_META_CACHE_TTL = 86400
DMOJ_NEWSLETTER_ID_ON_REGISTER = 1
BAD_MAIL_PROVIDERS = () BAD_MAIL_PROVIDERS = ()
BAD_MAIL_PROVIDER_REGEX = () BAD_MAIL_PROVIDER_REGEX = ()
@ -110,31 +113,30 @@ TIMEZONE_MAP = None
TIMEZONE_DETECT_BACKEND = None TIMEZONE_DETECT_BACKEND = None
TERMS_OF_SERVICE_URL = None TERMS_OF_SERVICE_URL = None
DEFAULT_USER_LANGUAGE = 'PY3' DEFAULT_USER_LANGUAGE = "PY3"
PHANTOMJS = '' PHANTOMJS = ""
PHANTOMJS_PDF_ZOOM = 0.75 PHANTOMJS_PDF_ZOOM = 0.75
PHANTOMJS_PDF_TIMEOUT = 5.0 PHANTOMJS_PDF_TIMEOUT = 5.0
PHANTOMJS_PAPER_SIZE = 'Letter' PHANTOMJS_PAPER_SIZE = "Letter"
SLIMERJS = '' SLIMERJS = ""
SLIMERJS_PDF_ZOOM = 0.75 SLIMERJS_PDF_ZOOM = 0.75
SLIMERJS_FIREFOX_PATH = '' SLIMERJS_FIREFOX_PATH = ""
SLIMERJS_PAPER_SIZE = 'Letter' SLIMERJS_PAPER_SIZE = "Letter"
PUPPETEER_MODULE = '/usr/lib/node_modules/puppeteer' PUPPETEER_MODULE = "/usr/lib/node_modules/puppeteer"
PUPPETEER_PAPER_SIZE = 'Letter' PUPPETEER_PAPER_SIZE = "Letter"
USE_SELENIUM = False USE_SELENIUM = False
SELENIUM_CUSTOM_CHROME_PATH = None SELENIUM_CUSTOM_CHROME_PATH = None
SELENIUM_CHROMEDRIVER_PATH = 'chromedriver' SELENIUM_CHROMEDRIVER_PATH = "chromedriver"
PYGMENT_THEME = 'pygment-github.css'
INLINE_JQUERY = True INLINE_JQUERY = True
INLINE_FONTAWESOME = True INLINE_FONTAWESOME = True
JQUERY_JS = '//ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js' JQUERY_JS = "//ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"
FONTAWESOME_CSS = '//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css' FONTAWESOME_CSS = "//cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css"
DMOJ_CANONICAL = '' DMOJ_CANONICAL = ""
# Application definition # Application definition
@ -146,357 +148,315 @@ except ImportError:
pass pass
else: else:
del wpadmin del wpadmin
INSTALLED_APPS += ('wpadmin',) INSTALLED_APPS += ("wpadmin",)
WPADMIN = { WPADMIN = {
'admin': { "admin": {
'title': 'LQDOJ Admin', "title": "LQDOJ Admin",
'menu': { "menu": {
'top': 'wpadmin.menu.menus.BasicTopMenu', "top": "wpadmin.menu.menus.BasicTopMenu",
'left': 'wpadmin.menu.custom.CustomModelLeftMenuWithDashboard', "left": "wpadmin.menu.custom.CustomModelLeftMenuWithDashboard",
}, },
'custom_menu': [ "custom_menu": [
{ {
'model': 'judge.Problem', "model": "judge.Problem",
'icon': 'fa-question-circle', "icon": "fa-question-circle",
'children': [ "children": [
'judge.ProblemGroup', "judge.ProblemGroup",
'judge.ProblemType', "judge.ProblemType",
'judge.ProblemPointsVote', "judge.ProblemPointsVote",
], ],
}, },
{ {
'model': 'judge.Submission', "model": "judge.Submission",
'icon': 'fa-check-square-o', "icon": "fa-check-square",
'children': [ "children": [
'judge.Language', "judge.Language",
'judge.Judge', "judge.Judge",
], ],
}, },
{ {
'model': 'judge.Contest', "model": "judge.Contest",
'icon': 'fa-bar-chart', "icon": "fa-bar-chart",
'children': [ "children": [
'judge.ContestParticipation', "judge.ContestParticipation",
'judge.ContestTag', "judge.ContestTag",
], ],
}, },
{ {
'model': 'auth.User', "model": "auth.User",
'icon': 'fa-user', "icon": "fa-user",
'children': [ "children": [
'auth.Group', "auth.Group",
'registration.RegistrationProfile', "registration.RegistrationProfile",
], ],
}, },
{ {
'model': 'judge.Profile', "model": "judge.Profile",
'icon': 'fa-user-plus', "icon": "fa-user-plus",
'children': [ "children": [
'judge.Organization', "judge.Organization",
'judge.OrganizationRequest', "judge.OrganizationRequest",
], ],
}, },
{ {
'model': 'judge.NavigationBar', "model": "judge.NavigationBar",
'icon': 'fa-bars', "icon": "fa-bars",
'children': [ "children": [
'judge.MiscConfig', "judge.MiscConfig",
'judge.License', "judge.License",
'sites.Site', "sites.Site",
'redirects.Redirect', "redirects.Redirect",
], ],
}, },
('judge.BlogPost', 'fa-rss-square'), ("judge.BlogPost", "fa-rss-square"),
('judge.Comment', 'fa-comment-o'), ("judge.Ticket", "fa-exclamation-circle"),
('judge.Ticket', 'fa-exclamation-circle'), ("admin.LogEntry", "fa-empire"),
('flatpages.FlatPage', 'fa-file-text-o'),
('judge.Solution', 'fa-pencil'),
], ],
'dashboard': { "dashboard": {
'breadcrumbs': True, "breadcrumbs": True,
}, },
}, },
} }
INSTALLED_APPS += ( INSTALLED_APPS += (
'django.contrib.admin', "django.contrib.admin",
'judge', "judge",
'django.contrib.auth', "django.contrib.auth",
'django.contrib.contenttypes', "django.contrib.contenttypes",
'django.contrib.flatpages', "django.contrib.flatpages",
'django.contrib.sessions', "django.contrib.sessions",
'django.contrib.messages', "django.contrib.messages",
'django.contrib.redirects', "django.contrib.redirects",
'django.contrib.staticfiles', "django.contrib.staticfiles",
'django.contrib.sites', "django.contrib.sites",
'django.contrib.sitemaps', "django.contrib.sitemaps",
'registration', "registration",
'mptt', "mptt",
'reversion', "reversion",
'django_social_share', "reversion_compare",
'social_django', "django_social_share",
'compressor', "social_django",
'django_ace', "compressor",
'pagedown', "django_ace",
'sortedm2m', "pagedown",
'statici18n', "sortedm2m",
'impersonate', "statici18n",
'django_jinja', "impersonate",
'chat_box', "django_jinja",
'newsletter', "chat_box",
'django.forms', "django.forms",
) )
MIDDLEWARE = ( MIDDLEWARE = (
'judge.middleware.ShortCircuitMiddleware', "judge.middleware.SlowRequestMiddleware",
'django.middleware.common.CommonMiddleware', "judge.middleware.ShortCircuitMiddleware",
'django.contrib.sessions.middleware.SessionMiddleware', "django.contrib.sessions.middleware.SessionMiddleware",
'django.middleware.locale.LocaleMiddleware', "django.middleware.locale.LocaleMiddleware",
'django.middleware.csrf.CsrfViewMiddleware', "django.middleware.common.CommonMiddleware",
'django.contrib.auth.middleware.AuthenticationMiddleware', "django.middleware.csrf.CsrfViewMiddleware",
'judge.middleware.DMOJLoginMiddleware', "django.contrib.auth.middleware.AuthenticationMiddleware",
'django.contrib.messages.middleware.MessageMiddleware', "judge.middleware.DMOJLoginMiddleware",
'django.middleware.clickjacking.XFrameOptionsMiddleware', "django.contrib.messages.middleware.MessageMiddleware",
'judge.user_log.LogUserAccessMiddleware', "django.middleware.clickjacking.XFrameOptionsMiddleware",
'judge.timezone.TimezoneMiddleware', "judge.user_log.LogUserAccessMiddleware",
'impersonate.middleware.ImpersonateMiddleware', "judge.timezone.TimezoneMiddleware",
'judge.middleware.DMOJImpersonationMiddleware', "impersonate.middleware.ImpersonateMiddleware",
'judge.middleware.ContestMiddleware', "judge.middleware.DMOJImpersonationMiddleware",
'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware', "judge.middleware.ContestMiddleware",
'judge.social_auth.SocialAuthExceptionMiddleware', "judge.middleware.DarkModeMiddleware",
'django.contrib.redirects.middleware.RedirectFallbackMiddleware', "judge.middleware.SubdomainMiddleware",
"django.contrib.flatpages.middleware.FlatpageFallbackMiddleware",
"judge.social_auth.SocialAuthExceptionMiddleware",
"django.contrib.redirects.middleware.RedirectFallbackMiddleware",
) )
FORM_RENDERER = 'django.forms.renderers.TemplatesSetting' X_FRAME_OPTIONS = "SAMEORIGIN"
IMPERSONATE_REQUIRE_SUPERUSER = True LANGUAGE_COOKIE_AGE = 8640000
IMPERSONATE_DISABLE_LOGGING = True
FORM_RENDERER = "django.forms.renderers.TemplatesSetting"
IMPERSONATE = {
"REQUIRE_SUPERUSER": True,
"DISABLE_LOGGING": True,
"ADMIN_DELETE_PERMISSION": True,
}
ACCOUNT_ACTIVATION_DAYS = 7 ACCOUNT_ACTIVATION_DAYS = 7
AUTH_PASSWORD_VALIDATORS = [ AUTH_PASSWORD_VALIDATORS = [
{ {
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
}, },
{ {
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', "NAME": "judge.utils.pwned.PwnedPasswordsValidator",
}, },
{ {
'NAME': 'judge.utils.pwned.PwnedPasswordsValidator', "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
}, },
] ]
SILENCED_SYSTEM_CHECKS = ['urls.W002', 'fields.W342'] SILENCED_SYSTEM_CHECKS = ["urls.W002", "fields.W342"]
ROOT_URLCONF = 'dmoj.urls' ROOT_URLCONF = "dmoj.urls"
LOGIN_REDIRECT_URL = '/user' LOGIN_REDIRECT_URL = "/user"
WSGI_APPLICATION = 'dmoj.wsgi.application' WSGI_APPLICATION = "dmoj.wsgi.application"
TEMPLATES = [ TEMPLATES = [
{ {
'BACKEND': 'django_jinja.backend.Jinja2', "BACKEND": "django_jinja.backend.Jinja2",
'DIRS': [ "DIRS": [
os.path.join(BASE_DIR, 'templates'), os.path.join(BASE_DIR, "templates"),
], ],
'APP_DIRS': False, "APP_DIRS": False,
'OPTIONS': { "OPTIONS": {
'match_extension': ('.html', '.txt'), "match_extension": (".html", ".txt"),
'match_regex': '^(?!admin/)', "match_regex": "^(?!admin/)",
'context_processors': [ "context_processors": [
'django.template.context_processors.media', "django.template.context_processors.media",
'django.template.context_processors.tz', "django.template.context_processors.tz",
'django.template.context_processors.i18n', "django.template.context_processors.i18n",
'django.template.context_processors.request', "django.template.context_processors.request",
'django.contrib.messages.context_processors.messages', "django.contrib.messages.context_processors.messages",
'judge.template_context.comet_location', "judge.template_context.comet_location",
'judge.template_context.get_resource', "judge.template_context.get_resource",
'judge.template_context.general_info', "judge.template_context.general_info",
'judge.template_context.site', "judge.template_context.site",
'judge.template_context.site_name', "judge.template_context.site_name",
'judge.template_context.misc_config', "judge.template_context.misc_config",
'judge.template_context.math_setting', "social_django.context_processors.backends",
'social_django.context_processors.backends', "social_django.context_processors.login_redirect",
'social_django.context_processors.login_redirect',
], ],
'autoescape': select_autoescape(['html', 'xml']), "autoescape": select_autoescape(["html", "xml"]),
'trim_blocks': True, "trim_blocks": True,
'lstrip_blocks': True, "lstrip_blocks": True,
'extensions': DEFAULT_EXTENSIONS + [ "extensions": DEFAULT_EXTENSIONS
'compressor.contrib.jinja2ext.CompressorExtension', + [
'judge.jinja2.DMOJExtension', "compressor.contrib.jinja2ext.CompressorExtension",
'judge.jinja2.spaceless.SpacelessExtension', "judge.jinja2.DMOJExtension",
"judge.jinja2.spaceless.SpacelessExtension",
], ],
}, },
}, },
{ {
'BACKEND': 'django.template.backends.django.DjangoTemplates', "BACKEND": "django.template.backends.django.DjangoTemplates",
'APP_DIRS': True, "APP_DIRS": True,
'DIRS': [ "DIRS": [
os.path.join(BASE_DIR, 'templates'), os.path.join(BASE_DIR, "templates"),
], ],
'OPTIONS': { "OPTIONS": {
'context_processors': [ "context_processors": [
'django.contrib.auth.context_processors.auth', "django.contrib.auth.context_processors.auth",
'django.template.context_processors.media', "django.template.context_processors.media",
'django.template.context_processors.tz', "django.template.context_processors.tz",
'django.template.context_processors.i18n', "django.template.context_processors.i18n",
'django.template.context_processors.request', "django.template.context_processors.request",
'django.contrib.messages.context_processors.messages', "django.contrib.messages.context_processors.messages",
], ],
}, },
}, },
] ]
LOCALE_PATHS = [ LOCALE_PATHS = [
os.path.join(BASE_DIR, 'locale'), os.path.join(BASE_DIR, "locale"),
] ]
LANGUAGES = [ LANGUAGES = [
('de', _('German')), ("vi", _("Vietnamese")),
('en', _('English')), ("en", _("English")),
('es', _('Spanish')),
('fr', _('French')),
('hr', _('Croatian')),
('hu', _('Hungarian')),
('ja', _('Japanese')),
('ko', _('Korean')),
('pt', _('Brazilian Portuguese')),
('ro', _('Romanian')),
('ru', _('Russian')),
('sr-latn', _('Serbian (Latin)')),
('tr', _('Turkish')),
('vi', _('Vietnamese')),
('zh-hans', _('Simplified Chinese')),
('zh-hant', _('Traditional Chinese')),
] ]
MARKDOWN_ADMIN_EDITABLE_STYLE = {
'safe_mode': False,
'use_camo': True,
'texoid': True,
'math': True,
}
MARKDOWN_DEFAULT_STYLE = {
'safe_mode': True,
'nofollow': True,
'use_camo': True,
'math': True,
}
MARKDOWN_USER_LARGE_STYLE = {
'safe_mode': True,
'nofollow': True,
'use_camo': True,
'math': True,
}
MARKDOWN_STYLES = {
'comment': MARKDOWN_DEFAULT_STYLE,
'self-description': MARKDOWN_USER_LARGE_STYLE,
'problem': MARKDOWN_ADMIN_EDITABLE_STYLE,
'contest': MARKDOWN_ADMIN_EDITABLE_STYLE,
'language': MARKDOWN_ADMIN_EDITABLE_STYLE,
'license': MARKDOWN_ADMIN_EDITABLE_STYLE,
'judge': MARKDOWN_ADMIN_EDITABLE_STYLE,
'blog': MARKDOWN_ADMIN_EDITABLE_STYLE,
'solution': MARKDOWN_ADMIN_EDITABLE_STYLE,
'contest_tag': MARKDOWN_ADMIN_EDITABLE_STYLE,
'organization-about': MARKDOWN_USER_LARGE_STYLE,
'ticket': MARKDOWN_USER_LARGE_STYLE,
}
# Database # Database
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases # https://docs.djangoproject.com/en/1.11/ref/settings/#databases
DATABASES = { DATABASES = {
'default': { "default": {
'ENGINE': 'django.db.backends.sqlite3', "ENGINE": "django.db.backends.sqlite3",
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), "NAME": os.path.join(BASE_DIR, "db.sqlite3"),
}, },
} }
ENABLE_FTS = False ENABLE_FTS = False
# Bridged configuration # Bridged configuration
BRIDGED_JUDGE_ADDRESS = [('localhost', 9999)] BRIDGED_JUDGE_ADDRESS = [("localhost", 9999)]
BRIDGED_JUDGE_PROXIES = None BRIDGED_JUDGE_PROXIES = None
BRIDGED_DJANGO_ADDRESS = [('localhost', 9998)] BRIDGED_DJANGO_ADDRESS = [("localhost", 9998)]
BRIDGED_DJANGO_CONNECT = None BRIDGED_DJANGO_CONNECT = None
BRIDGED_AUTO_CREATE_JUDGE = False
# Event Server configuration # Event Server configuration
EVENT_DAEMON_USE = False EVENT_DAEMON_USE = False
EVENT_DAEMON_POST = 'ws://localhost:9997/' EVENT_DAEMON_POST = "ws://localhost:9997/"
EVENT_DAEMON_GET = 'ws://localhost:9996/' EVENT_DAEMON_GET = "ws://localhost:9996/"
EVENT_DAEMON_POLL = '/channels/' EVENT_DAEMON_POLL = "/channels/"
EVENT_DAEMON_KEY = None EVENT_DAEMON_KEY = None
EVENT_DAEMON_AMQP_EXCHANGE = 'dmoj-events' EVENT_DAEMON_AMQP_EXCHANGE = "dmoj-events"
EVENT_DAEMON_SUBMISSION_KEY = '6Sdmkx^%pk@GsifDfXcwX*Y7LRF%RGT8vmFpSxFBT$fwS7trc8raWfN#CSfQuKApx&$B#Gh2L7p%W!Ww' EVENT_DAEMON_SUBMISSION_KEY = (
"6Sdmkx^%pk@GsifDfXcwX*Y7LRF%RGT8vmFpSxFBT$fwS7trc8raWfN#CSfQuKApx&$B#Gh2L7p%W!Ww"
)
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/1.11/topics/i18n/ # https://docs.djangoproject.com/en/1.11/topics/i18n/
# Whatever you do, this better be one of the entries in `LANGUAGES`. # Whatever you do, this better be one of the entries in `LANGUAGES`.
LANGUAGE_CODE = 'vi' LANGUAGE_CODE = "vi"
TIME_ZONE = 'Asia/Ho_Chi_Minh' TIME_ZONE = "Asia/Ho_Chi_Minh"
DEFAULT_USER_TIME_ZONE = 'Asia/Ho_Chi_Minh' DEFAULT_USER_TIME_ZONE = "Asia/Ho_Chi_Minh"
USE_I18N = True USE_I18N = True
USE_L10N = True USE_L10N = True
USE_TZ = True USE_TZ = True
# Cookies # Cookies
SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db' SESSION_ENGINE = "django.contrib.sessions.backends.cached_db"
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.11/howto/static-files/ # https://docs.djangoproject.com/en/1.11/howto/static-files/
DMOJ_RESOURCES = os.path.join(BASE_DIR, 'resources') DMOJ_RESOURCES = os.path.join(BASE_DIR, "resources")
STATICFILES_FINDERS = ( STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder', "django.contrib.staticfiles.finders.FileSystemFinder",
'django.contrib.staticfiles.finders.AppDirectoriesFinder', "django.contrib.staticfiles.finders.AppDirectoriesFinder",
) )
STATICFILES_DIRS = [ STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'resources'), os.path.join(BASE_DIR, "resources"),
] ]
STATIC_URL = '/static/' STATIC_URL = "/static/"
# Define a cache # Define a cache
CACHES = {} CACHES = {}
# Authentication # Authentication
AUTHENTICATION_BACKENDS = ( AUTHENTICATION_BACKENDS = (
'social_core.backends.google.GoogleOAuth2', "social_core.backends.google.GoogleOAuth2",
'social_core.backends.facebook.FacebookOAuth2', "social_core.backends.facebook.FacebookOAuth2",
'judge.social_auth.GitHubSecureEmailOAuth2', "judge.social_auth.GitHubSecureEmailOAuth2",
'django.contrib.auth.backends.ModelBackend', "judge.authentication.CustomModelBackend",
) )
SOCIAL_AUTH_PIPELINE = ( SOCIAL_AUTH_PIPELINE = (
'social_core.pipeline.social_auth.social_details', "social_core.pipeline.social_auth.social_details",
'social_core.pipeline.social_auth.social_uid', "social_core.pipeline.social_auth.social_uid",
'social_core.pipeline.social_auth.auth_allowed', "social_core.pipeline.social_auth.auth_allowed",
'judge.social_auth.verify_email', "judge.social_auth.verify_email",
'social_core.pipeline.social_auth.social_user', "social_core.pipeline.social_auth.social_user",
'social_core.pipeline.user.get_username', "social_core.pipeline.user.get_username",
'social_core.pipeline.social_auth.associate_by_email', "social_core.pipeline.social_auth.associate_by_email",
'judge.social_auth.choose_username', "judge.social_auth.choose_username",
'social_core.pipeline.user.create_user', "social_core.pipeline.user.create_user",
'judge.social_auth.make_profile', "judge.social_auth.make_profile",
'social_core.pipeline.social_auth.associate_user', "social_core.pipeline.social_auth.associate_user",
'social_core.pipeline.social_auth.load_extra_data', "social_core.pipeline.social_auth.load_extra_data",
'social_core.pipeline.user.user_details', "social_core.pipeline.user.user_details",
) )
SOCIAL_AUTH_PROTECTED_USER_FIELDS = ['first_name', 'last_name'] SOCIAL_AUTH_PROTECTED_USER_FIELDS = ["first_name", "last_name"]
SOCIAL_AUTH_GOOGLE_OAUTH2_USER_FIELDS = ['email', 'username'] SOCIAL_AUTH_GOOGLE_OAUTH2_USER_FIELDS = ["email", "username"]
SOCIAL_AUTH_GITHUB_SECURE_SCOPE = ['user:email'] SOCIAL_AUTH_GITHUB_SECURE_SCOPE = ["user:email"]
SOCIAL_AUTH_FACEBOOK_SCOPE = ['email'] SOCIAL_AUTH_FACEBOOK_SCOPE = ["email"]
SOCIAL_AUTH_SLUGIFY_USERNAMES = True SOCIAL_AUTH_SLUGIFY_USERNAMES = True
SOCIAL_AUTH_SLUGIFY_FUNCTION = 'judge.social_auth.slugify_username' SOCIAL_AUTH_SLUGIFY_FUNCTION = "judge.social_auth.slugify_username"
JUDGE_AMQP_PATH = None JUDGE_AMQP_PATH = None
@ -513,22 +473,29 @@ FILE_UPLOAD_PERMISSIONS = 0o644
MESSAGES_TO_LOAD = 15 MESSAGES_TO_LOAD = 15
NEWSLETTER_CONFIRM_EMAIL = False ML_OUTPUT_PATH = None
# Amount of seconds to wait between each email. Here 100ms is used. # Use subdomain for organizations
NEWSLETTER_EMAIL_DELAY = 0.1 USE_SUBDOMAIN = False
# Amount of seconds to wait between each batch. Here one minute is used. # Chat
NEWSLETTER_BATCH_DELAY = 60 CHAT_SECRET_KEY = "QUdVFsxk6f5-Hd8g9BXv81xMqvIZFRqMl-KbRzztW-U="
# Number of emails in one batch # Nginx
NEWSLETTER_BATCH_SIZE = 100 META_REMOTE_ADDRESS_KEY = "REMOTE_ADDR"
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
# Chunk upload
CHUNK_UPLOAD_DIR = "/tmp/chunk_upload_tmp"
# Rate limit
RL_VOTE = "200/h"
RL_COMMENT = "30/h"
# Google form to request name
REGISTER_NAME_URL = None
try: try:
with open(os.path.join(os.path.dirname(__file__), 'local_settings.py')) as f: with open(os.path.join(os.path.dirname(__file__), "local_settings.py")) as f:
exec(f.read(), globals()) exec(f.read(), globals())
except IOError: except IOError:
pass pass

View file

@ -8,8 +8,8 @@ DEFAULT_THROTTLE = (10, 60)
def new_email(): def new_email():
cache.add('error_email_throttle', 0, settings.DMOJ_EMAIL_THROTTLING[1]) cache.add("error_email_throttle", 0, settings.DMOJ_EMAIL_THROTTLING[1])
return cache.incr('error_email_throttle') return cache.incr("error_email_throttle")
class ThrottledEmailHandler(AdminEmailHandler): class ThrottledEmailHandler(AdminEmailHandler):

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,6 @@
import os import os
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dmoj.settings')
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dmoj.settings")
try: try:
import MySQLdb # noqa: F401, imported for side effect import MySQLdb # noqa: F401, imported for side effect
@ -8,5 +9,8 @@ except ImportError:
pymysql.install_as_MySQLdb() pymysql.install_as_MySQLdb()
from django.core.wsgi import get_wsgi_application # noqa: E402, django must be imported here from django.core.wsgi import (
get_wsgi_application,
) # noqa: E402, django must be imported here
application = get_wsgi_application() application = get_wsgi_application()

View file

@ -2,13 +2,17 @@ import os
import gevent.monkey # noqa: I100, gevent must be imported here import gevent.monkey # noqa: I100, gevent must be imported here
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dmoj.settings') os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dmoj.settings")
gevent.monkey.patch_all() gevent.monkey.patch_all()
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
import dmoj_install_pymysql # noqa: F401, I100, I202, imported for side effect import dmoj_install_pymysql # noqa: F401, I100, I202, imported for side effect
from django.core.wsgi import get_wsgi_application # noqa: E402, I100, I202, django must be imported here from django.core.wsgi import (
get_wsgi_application,
) # noqa: E402, I100, I202, django must be imported here
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
import django_2_2_pymysql_patch # noqa: I100, F401, I202, imported for side effect import django_2_2_pymysql_patch # noqa: I100, F401, I202, imported for side effect
application = get_wsgi_application() application = get_wsgi_application()

View file

@ -2,19 +2,22 @@ import os
import gevent.monkey # noqa: I100, gevent must be imported here import gevent.monkey # noqa: I100, gevent must be imported here
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dmoj.settings') os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dmoj.settings")
gevent.monkey.patch_all() gevent.monkey.patch_all()
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
import dmoj_install_pymysql # noqa: E402, F401, I100, I202, imported for side effect import dmoj_install_pymysql # noqa: E402, F401, I100, I202, imported for side effect
import django # noqa: E402, F401, I100, I202, django must be imported here import django # noqa: E402, F401, I100, I202, django must be imported here
django.setup() django.setup()
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
import django_2_2_pymysql_patch # noqa: E402, I100, F401, I202, imported for side effect import django_2_2_pymysql_patch # noqa: E402, I100, F401, I202, imported for side effect
from judge.bridge.daemon import judge_daemon # noqa: E402, I100, I202, django code must be imported here from judge.bridge.daemon import (
judge_daemon,
) # noqa: E402, I100, I202, django code must be imported here
if __name__ == '__main__': if __name__ == "__main__":
judge_daemon() judge_daemon()

View file

@ -6,7 +6,7 @@ except ImportError:
import dmoj_install_pymysql # noqa: F401, imported for side effect import dmoj_install_pymysql # noqa: F401, imported for side effect
# set the default Django settings module for the 'celery' program. # set the default Django settings module for the 'celery' program.
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dmoj.settings') os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dmoj.settings")
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
import django_2_2_pymysql_patch # noqa: I100, F401, I202, imported for side effect import django_2_2_pymysql_patch # noqa: I100, F401, I202, imported for side effect

View file

@ -1,4 +1,4 @@
import pymysql import pymysql
pymysql.install_as_MySQLdb() pymysql.install_as_MySQLdb()
pymysql.version_info = (1, 3, 13, "final", 0) pymysql.version_info = (1, 4, 0, "final", 0)

View file

@ -1 +1 @@
default_app_config = 'judge.apps.JudgeAppConfig' default_app_config = "judge.apps.JudgeAppConfig"

View file

@ -1,19 +1,62 @@
from django.contrib import admin from django.contrib import admin
from django.contrib.admin.models import LogEntry from django.contrib.admin.models import LogEntry
from django.contrib.auth.models import User
from judge.admin.comments import CommentAdmin from judge.admin.comments import CommentAdmin
from judge.admin.contest import ContestAdmin, ContestParticipationAdmin, ContestTagAdmin from judge.admin.contest import (
from judge.admin.interface import BlogPostAdmin, LicenseAdmin, LogEntryAdmin, NavigationBarAdmin ContestAdmin,
ContestParticipationAdmin,
ContestTagAdmin,
ContestsSummaryAdmin,
)
from judge.admin.interface import (
BlogPostAdmin,
LicenseAdmin,
LogEntryAdmin,
NavigationBarAdmin,
)
from judge.admin.organization import OrganizationAdmin, OrganizationRequestAdmin from judge.admin.organization import OrganizationAdmin, OrganizationRequestAdmin
from judge.admin.problem import ProblemAdmin, ProblemPointsVoteAdmin from judge.admin.problem import ProblemAdmin, ProblemPointsVoteAdmin
from judge.admin.profile import ProfileAdmin from judge.admin.profile import ProfileAdmin, UserAdmin
from judge.admin.runtime import JudgeAdmin, LanguageAdmin from judge.admin.runtime import JudgeAdmin, LanguageAdmin
from judge.admin.submission import SubmissionAdmin from judge.admin.submission import SubmissionAdmin
from judge.admin.taxon import ProblemGroupAdmin, ProblemTypeAdmin from judge.admin.taxon import (
ProblemGroupAdmin,
ProblemTypeAdmin,
OfficialContestCategoryAdmin,
OfficialContestLocationAdmin,
)
from judge.admin.ticket import TicketAdmin from judge.admin.ticket import TicketAdmin
from judge.models import BlogPost, Comment, CommentLock, Contest, ContestParticipation, \ from judge.admin.volunteer import VolunteerProblemVoteAdmin
ContestTag, Judge, Language, License, MiscConfig, NavigationBar, Organization, \ from judge.admin.course import CourseAdmin
OrganizationRequest, Problem, ProblemGroup, ProblemPointsVote, ProblemType, Profile, Submission, Ticket from judge.models import (
BlogPost,
Comment,
CommentLock,
Contest,
ContestParticipation,
ContestTag,
Judge,
Language,
License,
MiscConfig,
NavigationBar,
Organization,
OrganizationRequest,
Problem,
ProblemGroup,
ProblemPointsVote,
ProblemType,
Profile,
Submission,
Ticket,
VolunteerProblemVote,
Course,
ContestsSummary,
OfficialContestCategory,
OfficialContestLocation,
)
admin.site.register(BlogPost, BlogPostAdmin) admin.site.register(BlogPost, BlogPostAdmin)
admin.site.register(Comment, CommentAdmin) admin.site.register(Comment, CommentAdmin)
@ -36,3 +79,10 @@ admin.site.register(ProblemType, ProblemTypeAdmin)
admin.site.register(Profile, ProfileAdmin) admin.site.register(Profile, ProfileAdmin)
admin.site.register(Submission, SubmissionAdmin) admin.site.register(Submission, SubmissionAdmin)
admin.site.register(Ticket, TicketAdmin) admin.site.register(Ticket, TicketAdmin)
admin.site.register(VolunteerProblemVote, VolunteerProblemVoteAdmin)
admin.site.register(Course, CourseAdmin)
admin.site.unregister(User)
admin.site.register(User, UserAdmin)
admin.site.register(ContestsSummary, ContestsSummaryAdmin)
admin.site.register(OfficialContestCategory, OfficialContestCategoryAdmin)
admin.site.register(OfficialContestLocation, OfficialContestLocationAdmin)

View file

@ -11,52 +11,71 @@ from judge.widgets import AdminHeavySelect2Widget, HeavyPreviewAdminPageDownWidg
class CommentForm(ModelForm): class CommentForm(ModelForm):
class Meta: class Meta:
widgets = { widgets = {
'author': AdminHeavySelect2Widget(data_view='profile_select2'), "author": AdminHeavySelect2Widget(data_view="profile_select2"),
'parent': AdminHeavySelect2Widget(data_view='comment_select2'),
} }
if HeavyPreviewAdminPageDownWidget is not None: if HeavyPreviewAdminPageDownWidget is not None:
widgets['body'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('comment_preview')) widgets["body"] = HeavyPreviewAdminPageDownWidget(
preview=reverse_lazy("comment_preview")
)
class CommentAdmin(VersionAdmin): class CommentAdmin(VersionAdmin):
fieldsets = ( fieldsets = (
(None, {'fields': ('author', 'page', 'parent', 'score', 'hidden')}), (
('Content', {'fields': ('body',)}), None,
{
"fields": (
"author",
"parent",
"score",
"hidden",
"content_type",
"object_id",
) )
list_display = ['author', 'linked_page', 'time'] },
search_fields = ['author__user__username', 'page', 'body'] ),
actions = ['hide_comment', 'unhide_comment'] ("Content", {"fields": ("body",)}),
list_filter = ['hidden'] )
list_display = ["author", "linked_object", "time"]
search_fields = ["author__user__username", "body"]
readonly_fields = ["score", "parent"]
actions = ["hide_comment", "unhide_comment"]
list_filter = ["hidden"]
actions_on_top = True actions_on_top = True
actions_on_bottom = True actions_on_bottom = True
form = CommentForm form = CommentForm
date_hierarchy = 'time' date_hierarchy = "time"
def get_queryset(self, request): def get_queryset(self, request):
return Comment.objects.order_by('-time') return Comment.objects.order_by("-time")
def hide_comment(self, request, queryset): def hide_comment(self, request, queryset):
count = queryset.update(hidden=True) count = queryset.update(hidden=True)
self.message_user(request, ungettext('%d comment successfully hidden.', self.message_user(
'%d comments successfully hidden.', request,
count) % count) ungettext(
hide_comment.short_description = _('Hide comments') "%d comment successfully hidden.",
"%d comments successfully hidden.",
count,
)
% count,
)
hide_comment.short_description = _("Hide comments")
def unhide_comment(self, request, queryset): def unhide_comment(self, request, queryset):
count = queryset.update(hidden=False) count = queryset.update(hidden=False)
self.message_user(request, ungettext('%d comment successfully unhidden.', self.message_user(
'%d comments successfully unhidden.', request,
count) % count) ungettext(
unhide_comment.short_description = _('Unhide comments') "%d comment successfully unhidden.",
"%d comments successfully unhidden.",
count,
)
% count,
)
def linked_page(self, obj): unhide_comment.short_description = _("Unhide comments")
link = obj.link
if link is not None:
return format_html('<a href="{0}">{1}</a>', link, obj.page)
else:
return format_html('{0}', obj.page)
linked_page.short_description = _('Associated page')
linked_page.admin_order_field = 'page'
def save_model(self, request, obj, form, change): def save_model(self, request, obj, form, change):
super(CommentAdmin, self).save_model(request, obj, form, change) super(CommentAdmin, self).save_model(request, obj, form, change)

View file

@ -3,7 +3,7 @@ from django.contrib import admin
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.db import connection, transaction from django.db import connection, transaction
from django.db.models import Q, TextField from django.db.models import Q, TextField
from django.forms import ModelForm, ModelMultipleChoiceField from django.forms import ModelForm, ModelMultipleChoiceField, TextInput
from django.http import Http404, HttpResponseRedirect from django.http import Http404, HttpResponseRedirect
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
@ -11,12 +11,28 @@ from django.utils import timezone
from django.utils.html import format_html from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _, ungettext from django.utils.translation import gettext_lazy as _, ungettext
from reversion.admin import VersionAdmin from reversion.admin import VersionAdmin
from reversion_compare.admin import CompareVersionAdmin
from django_ace import AceWidget from django_ace import AceWidget
from judge.models import Contest, ContestProblem, ContestSubmission, Profile, Rating from judge.models import (
Contest,
ContestProblem,
ContestSubmission,
Profile,
Rating,
OfficialContest,
)
from judge.ratings import rate_contest from judge.ratings import rate_contest
from judge.widgets import AdminHeavySelect2MultipleWidget, AdminHeavySelect2Widget, AdminPagedownWidget, \ from judge.widgets import (
AdminSelect2MultipleWidget, AdminSelect2Widget, HeavyPreviewAdminPageDownWidget AdminHeavySelect2MultipleWidget,
AdminHeavySelect2Widget,
AdminPagedownWidget,
AdminSelect2MultipleWidget,
AdminSelect2Widget,
HeavyPreviewAdminPageDownWidget,
)
from judge.views.contests import recalculate_contest_summary_result
from judge.utils.contest import maybe_trigger_contest_rescore
class AdminHeavySelect2Widget(AdminHeavySelect2Widget): class AdminHeavySelect2Widget(AdminHeavySelect2Widget):
@ -27,247 +43,394 @@ class AdminHeavySelect2Widget(AdminHeavySelect2Widget):
class ContestTagForm(ModelForm): class ContestTagForm(ModelForm):
contests = ModelMultipleChoiceField( contests = ModelMultipleChoiceField(
label=_('Included contests'), label=_("Included contests"),
queryset=Contest.objects.all(), queryset=Contest.objects.all(),
required=False, required=False,
widget=AdminHeavySelect2MultipleWidget(data_view='contest_select2')) widget=AdminHeavySelect2MultipleWidget(data_view="contest_select2"),
)
class ContestTagAdmin(admin.ModelAdmin): class ContestTagAdmin(admin.ModelAdmin):
fields = ('name', 'color', 'description', 'contests') fields = ("name", "color", "description", "contests")
list_display = ('name', 'color') list_display = ("name", "color")
actions_on_top = True actions_on_top = True
actions_on_bottom = True actions_on_bottom = True
form = ContestTagForm form = ContestTagForm
if AdminPagedownWidget is not None: if AdminPagedownWidget is not None:
formfield_overrides = { formfield_overrides = {
TextField: {'widget': AdminPagedownWidget}, TextField: {"widget": AdminPagedownWidget},
} }
def save_model(self, request, obj, form, change): def save_model(self, request, obj, form, change):
super(ContestTagAdmin, self).save_model(request, obj, form, change) super(ContestTagAdmin, self).save_model(request, obj, form, change)
obj.contests.set(form.cleaned_data['contests']) obj.contests.set(form.cleaned_data["contests"])
def get_form(self, request, obj=None, **kwargs): def get_form(self, request, obj=None, **kwargs):
form = super(ContestTagAdmin, self).get_form(request, obj, **kwargs) form = super(ContestTagAdmin, self).get_form(request, obj, **kwargs)
if obj is not None: if obj is not None:
form.base_fields['contests'].initial = obj.contests.all() form.base_fields["contests"].initial = obj.contests.all()
return form return form
class ContestProblemInlineForm(ModelForm): class ContestProblemInlineForm(ModelForm):
class Meta: class Meta:
widgets = {'problem': AdminHeavySelect2Widget(data_view='problem_select2')} widgets = {
"problem": AdminHeavySelect2Widget(data_view="problem_select2"),
"hidden_subtasks": TextInput(attrs={"size": "3"}),
"points": TextInput(attrs={"size": "1"}),
"order": TextInput(attrs={"size": "1"}),
}
class ContestProblemInline(admin.TabularInline): class ContestProblemInline(admin.TabularInline):
model = ContestProblem model = ContestProblem
verbose_name = _('Problem') verbose_name = _("Problem")
verbose_name_plural = 'Problems' verbose_name_plural = "Problems"
fields = ('problem', 'points', 'partial', 'is_pretested', 'max_submissions', 'output_prefix_override', 'order', fields = (
'rejudge_column') "problem",
readonly_fields = ('rejudge_column',) "points",
"partial",
"is_pretested",
"max_submissions",
"hidden_subtasks",
"show_testcases",
"order",
"rejudge_column",
)
readonly_fields = ("rejudge_column",)
form = ContestProblemInlineForm form = ContestProblemInlineForm
def rejudge_column(self, obj): def rejudge_column(self, obj):
if obj.id is None: if obj.id is None:
return '' return ""
return format_html('<a class="button rejudge-link" href="{}">Rejudge</a>', return format_html(
reverse('admin:judge_contest_rejudge', args=(obj.contest.id, obj.id))) '<a class="button rejudge-link" href="{}">Rejudge</a>',
rejudge_column.short_description = '' reverse("admin:judge_contest_rejudge", args=(obj.contest.id, obj.id)),
)
rejudge_column.short_description = ""
class ContestForm(ModelForm): class ContestForm(ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(ContestForm, self).__init__(*args, **kwargs) super(ContestForm, self).__init__(*args, **kwargs)
if 'rate_exclude' in self.fields: if "rate_exclude" in self.fields:
if self.instance and self.instance.id: if self.instance and self.instance.id:
self.fields['rate_exclude'].queryset = \ self.fields["rate_exclude"].queryset = Profile.objects.filter(
Profile.objects.filter(contest_history__contest=self.instance).distinct() contest_history__contest=self.instance
).distinct()
else: else:
self.fields['rate_exclude'].queryset = Profile.objects.none() self.fields["rate_exclude"].queryset = Profile.objects.none()
self.fields['banned_users'].widget.can_add_related = False self.fields["banned_users"].widget.can_add_related = False
self.fields['view_contest_scoreboard'].widget.can_add_related = False self.fields["view_contest_scoreboard"].widget.can_add_related = False
def clean(self): def clean(self):
cleaned_data = super(ContestForm, self).clean() cleaned_data = super(ContestForm, self).clean()
cleaned_data['banned_users'].filter(current_contest__contest=self.instance).update(current_contest=None) cleaned_data["banned_users"].filter(
current_contest__contest=self.instance
).update(current_contest=None)
class Meta: class Meta:
widgets = { widgets = {
'authors': AdminHeavySelect2MultipleWidget(data_view='profile_select2'), "authors": AdminHeavySelect2MultipleWidget(data_view="profile_select2"),
'curators': AdminHeavySelect2MultipleWidget(data_view='profile_select2'), "curators": AdminHeavySelect2MultipleWidget(data_view="profile_select2"),
'testers': AdminHeavySelect2MultipleWidget(data_view='profile_select2'), "testers": AdminHeavySelect2MultipleWidget(data_view="profile_select2"),
'private_contestants': AdminHeavySelect2MultipleWidget(data_view='profile_select2', "private_contestants": AdminHeavySelect2MultipleWidget(
attrs={'style': 'width: 100%'}), data_view="profile_select2", attrs={"style": "width: 100%"}
'organizations': AdminHeavySelect2MultipleWidget(data_view='organization_select2'), ),
'tags': AdminSelect2MultipleWidget, "organizations": AdminHeavySelect2MultipleWidget(
'banned_users': AdminHeavySelect2MultipleWidget(data_view='profile_select2', data_view="organization_select2"
attrs={'style': 'width: 100%'}), ),
'view_contest_scoreboard': AdminHeavySelect2MultipleWidget(data_view='profile_select2', "tags": AdminSelect2MultipleWidget,
attrs={'style': 'width: 100%'}), "banned_users": AdminHeavySelect2MultipleWidget(
data_view="profile_select2", attrs={"style": "width: 100%"}
),
"view_contest_scoreboard": AdminHeavySelect2MultipleWidget(
data_view="profile_select2", attrs={"style": "width: 100%"}
),
} }
if HeavyPreviewAdminPageDownWidget is not None: if HeavyPreviewAdminPageDownWidget is not None:
widgets['description'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('contest_preview')) widgets["description"] = HeavyPreviewAdminPageDownWidget(
preview=reverse_lazy("contest_preview")
class ContestAdmin(VersionAdmin):
fieldsets = (
(None, {'fields': ('key', 'name', 'authors', 'curators', 'testers')}),
(_('Settings'), {'fields': ('is_visible', 'use_clarifications', 'hide_problem_tags', 'scoreboard_visibility',
'run_pretests_only', 'points_precision')}),
(_('Scheduling'), {'fields': ('start_time', 'end_time', 'time_limit')}),
(_('Details'), {'fields': ('description', 'og_image', 'logo_override_image', 'tags', 'summary')}),
(_('Format'), {'fields': ('format_name', 'format_config', 'problem_label_script')}),
(_('Rating'), {'fields': ('is_rated', 'rate_all', 'rating_floor', 'rating_ceiling', 'rate_exclude')}),
(_('Access'), {'fields': ('access_code', 'is_private', 'private_contestants', 'is_organization_private',
'organizations', 'view_contest_scoreboard')}),
(_('Justice'), {'fields': ('banned_users',)}),
) )
list_display = ('key', 'name', 'is_visible', 'is_rated', 'start_time', 'end_time', 'time_limit', 'user_count')
search_fields = ('key', 'name')
inlines = [ContestProblemInline] class OfficialContestInlineForm(ModelForm):
class Meta:
widgets = {
"category": AdminSelect2Widget,
"location": AdminSelect2Widget,
}
class OfficialContestInline(admin.StackedInline):
fields = (
"category",
"year",
"location",
)
model = OfficialContest
can_delete = True
form = OfficialContestInlineForm
extra = 0
class ContestAdmin(CompareVersionAdmin):
fieldsets = (
(None, {"fields": ("key", "name", "authors", "curators", "testers")}),
(
_("Settings"),
{
"fields": (
"is_visible",
"use_clarifications",
"hide_problem_tags",
"public_scoreboard",
"scoreboard_visibility",
"run_pretests_only",
"points_precision",
"rate_limit",
)
},
),
(
_("Scheduling"),
{"fields": ("start_time", "end_time", "time_limit", "freeze_after")},
),
(
_("Details"),
{
"fields": (
"description",
"og_image",
"logo_override_image",
"tags",
"summary",
)
},
),
(
_("Format"),
{"fields": ("format_name", "format_config", "problem_label_script")},
),
(
_("Rating"),
{
"fields": (
"is_rated",
"rate_all",
"rating_floor",
"rating_ceiling",
"rate_exclude",
)
},
),
(
_("Access"),
{
"fields": (
"access_code",
"private_contestants",
"organizations",
"view_contest_scoreboard",
)
},
),
(_("Justice"), {"fields": ("banned_users",)}),
)
list_display = (
"key",
"name",
"is_visible",
"is_rated",
"start_time",
"end_time",
"time_limit",
"user_count",
)
search_fields = ("key", "name")
inlines = [ContestProblemInline, OfficialContestInline]
actions_on_top = True actions_on_top = True
actions_on_bottom = True actions_on_bottom = True
form = ContestForm form = ContestForm
change_list_template = 'admin/judge/contest/change_list.html' change_list_template = "admin/judge/contest/change_list.html"
filter_horizontal = ['rate_exclude'] filter_horizontal = ["rate_exclude"]
date_hierarchy = 'start_time' date_hierarchy = "start_time"
def get_actions(self, request): def get_actions(self, request):
actions = super(ContestAdmin, self).get_actions(request) actions = super(ContestAdmin, self).get_actions(request)
if request.user.has_perm('judge.change_contest_visibility') or \ if request.user.has_perm(
request.user.has_perm('judge.create_private_contest'): "judge.change_contest_visibility"
for action in ('make_visible', 'make_hidden'): ) or request.user.has_perm("judge.create_private_contest"):
for action in ("make_visible", "make_hidden"):
actions[action] = self.get_action(action) actions[action] = self.get_action(action)
return actions return actions
def get_queryset(self, request): def get_queryset(self, request):
queryset = Contest.objects.all() queryset = Contest.objects.all()
if request.user.has_perm('judge.edit_all_contest'): if request.user.has_perm("judge.edit_all_contest"):
return queryset return queryset
else: else:
return queryset.filter(Q(authors=request.profile) | Q(curators=request.profile)).distinct() return queryset.filter(
Q(authors=request.profile) | Q(curators=request.profile)
).distinct()
def get_readonly_fields(self, request, obj=None): def get_readonly_fields(self, request, obj=None):
readonly = [] readonly = []
if not request.user.has_perm('judge.contest_rating'): if not request.user.has_perm("judge.contest_rating"):
readonly += ['is_rated', 'rate_all', 'rate_exclude'] readonly += ["is_rated", "rate_all", "rate_exclude"]
if not request.user.has_perm('judge.contest_access_code'): if not request.user.has_perm("judge.contest_access_code"):
readonly += ['access_code'] readonly += ["access_code"]
if not request.user.has_perm('judge.create_private_contest'): if not request.user.has_perm("judge.create_private_contest"):
readonly += ['is_private', 'private_contestants', 'is_organization_private', 'organizations'] readonly += [
if not request.user.has_perm('judge.change_contest_visibility'): "private_contestants",
readonly += ['is_visible'] "organizations",
if not request.user.has_perm('judge.contest_problem_label'): ]
readonly += ['problem_label_script'] if not request.user.has_perm("judge.change_contest_visibility"):
readonly += ["is_visible"]
if not request.user.has_perm("judge.contest_problem_label"):
readonly += ["problem_label_script"]
return readonly return readonly
def save_model(self, request, obj, form, change): def save_model(self, request, obj, form, change):
# `is_visible` will not appear in `cleaned_data` if user cannot edit it # `is_visible` will not appear in `cleaned_data` if user cannot edit it
if form.cleaned_data.get('is_visible') and not request.user.has_perm('judge.change_contest_visibility'): if form.cleaned_data.get("is_visible") and not request.user.has_perm(
if not form.cleaned_data['is_private'] and not form.cleaned_data['is_organization_private']: "judge.change_contest_visibility"
):
if (
not len(form.cleaned_data["organizations"]) > 0
and not len(form.cleaned_data["private_contestants"]) > 0
):
raise PermissionDenied raise PermissionDenied
if not request.user.has_perm('judge.create_private_contest'): if not request.user.has_perm("judge.create_private_contest"):
raise PermissionDenied raise PermissionDenied
super().save_model(request, obj, form, change) super().save_model(request, obj, form, change)
# We need this flag because `save_related` deals with the inlines, but does not know if we have already rescored
self._rescored = False
if form.changed_data and any(f in form.changed_data for f in ('format_config', 'format_name')):
self._rescore(obj.key)
self._rescored = True
def save_related(self, request, form, formsets, change): def save_related(self, request, form, formsets, change):
super().save_related(request, form, formsets, change) super().save_related(request, form, formsets, change)
# Only rescored if we did not already do so in `save_model` # Only rescored if we did not already do so in `save_model`
if not self._rescored and any(formset.has_changed() for formset in formsets): formset_changed = False
self._rescore(form.cleaned_data['key']) if any(formset.has_changed() for formset in formsets):
formset_changed = True
maybe_trigger_contest_rescore(form, form.instance, formset_changed)
def has_change_permission(self, request, obj=None): def has_change_permission(self, request, obj=None):
if not request.user.has_perm('judge.edit_own_contest'): if not request.user.has_perm("judge.edit_own_contest"):
return False return False
if obj is None: if obj is None:
return True return True
return obj.is_editable_by(request.user) return obj.is_editable_by(request.user)
def _rescore(self, contest_key):
from judge.tasks import rescore_contest
transaction.on_commit(rescore_contest.s(contest_key).delay)
def make_visible(self, request, queryset): def make_visible(self, request, queryset):
if not request.user.has_perm('judge.change_contest_visibility'): if not request.user.has_perm("judge.change_contest_visibility"):
queryset = queryset.filter(Q(is_private=True) | Q(is_organization_private=True)) queryset = queryset.filter(
Q(is_private=True) | Q(is_organization_private=True)
)
count = queryset.update(is_visible=True) count = queryset.update(is_visible=True)
self.message_user(request, ungettext('%d contest successfully marked as visible.', self.message_user(
'%d contests successfully marked as visible.', request,
count) % count) ungettext(
make_visible.short_description = _('Mark contests as visible') "%d contest successfully marked as visible.",
"%d contests successfully marked as visible.",
count,
)
% count,
)
make_visible.short_description = _("Mark contests as visible")
def make_hidden(self, request, queryset): def make_hidden(self, request, queryset):
if not request.user.has_perm('judge.change_contest_visibility'): if not request.user.has_perm("judge.change_contest_visibility"):
queryset = queryset.filter(Q(is_private=True) | Q(is_organization_private=True)) queryset = queryset.filter(
Q(is_private=True) | Q(is_organization_private=True)
)
count = queryset.update(is_visible=True) count = queryset.update(is_visible=True)
self.message_user(request, ungettext('%d contest successfully marked as hidden.', self.message_user(
'%d contests successfully marked as hidden.', request,
count) % count) ungettext(
make_hidden.short_description = _('Mark contests as hidden') "%d contest successfully marked as hidden.",
"%d contests successfully marked as hidden.",
count,
)
% count,
)
make_hidden.short_description = _("Mark contests as hidden")
def get_urls(self): def get_urls(self):
return [ return [
url(r'^rate/all/$', self.rate_all_view, name='judge_contest_rate_all'), url(r"^rate/all/$", self.rate_all_view, name="judge_contest_rate_all"),
url(r'^(\d+)/rate/$', self.rate_view, name='judge_contest_rate'), url(r"^(\d+)/rate/$", self.rate_view, name="judge_contest_rate"),
url(r'^(\d+)/judge/(\d+)/$', self.rejudge_view, name='judge_contest_rejudge'), url(
r"^(\d+)/judge/(\d+)/$", self.rejudge_view, name="judge_contest_rejudge"
),
] + super(ContestAdmin, self).get_urls() ] + super(ContestAdmin, self).get_urls()
def rejudge_view(self, request, contest_id, problem_id): def rejudge_view(self, request, contest_id, problem_id):
queryset = ContestSubmission.objects.filter(problem_id=problem_id).select_related('submission') queryset = ContestSubmission.objects.filter(
problem_id=problem_id
).select_related("submission")
for model in queryset: for model in queryset:
model.submission.judge(rejudge=True) model.submission.judge(rejudge=True)
self.message_user(request, ungettext('%d submission was successfully scheduled for rejudging.', self.message_user(
'%d submissions were successfully scheduled for rejudging.', request,
len(queryset)) % len(queryset)) ungettext(
return HttpResponseRedirect(reverse('admin:judge_contest_change', args=(contest_id,))) "%d submission was successfully scheduled for rejudging.",
"%d submissions were successfully scheduled for rejudging.",
len(queryset),
)
% len(queryset),
)
return HttpResponseRedirect(
reverse("admin:judge_contest_change", args=(contest_id,))
)
def rate_all_view(self, request): def rate_all_view(self, request):
if not request.user.has_perm('judge.contest_rating'): if not request.user.has_perm("judge.contest_rating"):
raise PermissionDenied() raise PermissionDenied()
with transaction.atomic(): with transaction.atomic():
with connection.cursor() as cursor: with connection.cursor() as cursor:
cursor.execute('TRUNCATE TABLE `%s`' % Rating._meta.db_table) cursor.execute("TRUNCATE TABLE `%s`" % Rating._meta.db_table)
Profile.objects.update(rating=None) Profile.objects.update(rating=None)
for contest in Contest.objects.filter(is_rated=True, end_time__lte=timezone.now()).order_by('end_time'): for contest in Contest.objects.filter(
is_rated=True, end_time__lte=timezone.now()
).order_by("end_time"):
rate_contest(contest) rate_contest(contest)
return HttpResponseRedirect(reverse('admin:judge_contest_changelist')) return HttpResponseRedirect(reverse("admin:judge_contest_changelist"))
def rate_view(self, request, id): def rate_view(self, request, id):
if not request.user.has_perm('judge.contest_rating'): if not request.user.has_perm("judge.contest_rating"):
raise PermissionDenied() raise PermissionDenied()
contest = get_object_or_404(Contest, id=id) contest = get_object_or_404(Contest, id=id)
if not contest.is_rated or not contest.ended: if not contest.is_rated or not contest.ended:
raise Http404() raise Http404()
with transaction.atomic(): with transaction.atomic():
contest.rate() contest.rate()
return HttpResponseRedirect(request.META.get('HTTP_REFERER', reverse('admin:judge_contest_changelist'))) return HttpResponseRedirect(
request.META.get("HTTP_REFERER", reverse("admin:judge_contest_changelist"))
)
def get_form(self, request, obj=None, **kwargs): def get_form(self, request, obj=None, **kwargs):
form = super(ContestAdmin, self).get_form(request, obj, **kwargs) form = super(ContestAdmin, self).get_form(request, obj, **kwargs)
if 'problem_label_script' in form.base_fields: if "problem_label_script" in form.base_fields:
# form.base_fields['problem_label_script'] does not exist when the user has only view permission # form.base_fields['problem_label_script'] does not exist when the user has only view permission
# on the model. # on the model.
form.base_fields['problem_label_script'].widget = AceWidget('lua', request.profile.ace_theme) form.base_fields["problem_label_script"].widget = AceWidget(
"lua", request.profile.ace_theme
)
perms = ('edit_own_contest', 'edit_all_contest') perms = ("edit_own_contest", "edit_all_contest")
form.base_fields['curators'].queryset = Profile.objects.filter( form.base_fields["curators"].queryset = Profile.objects.filter(
Q(user__is_superuser=True) | Q(user__is_superuser=True)
Q(user__groups__permissions__codename__in=perms) | | Q(user__groups__permissions__codename__in=perms)
Q(user__user_permissions__codename__in=perms), | Q(user__user_permissions__codename__in=perms),
).distinct() ).distinct()
return form return form
@ -275,29 +438,48 @@ class ContestAdmin(VersionAdmin):
class ContestParticipationForm(ModelForm): class ContestParticipationForm(ModelForm):
class Meta: class Meta:
widgets = { widgets = {
'contest': AdminSelect2Widget(), "contest": AdminSelect2Widget(),
'user': AdminHeavySelect2Widget(data_view='profile_select2'), "user": AdminHeavySelect2Widget(data_view="profile_select2"),
} }
class ContestParticipationAdmin(admin.ModelAdmin): class ContestParticipationAdmin(admin.ModelAdmin):
fields = ('contest', 'user', 'real_start', 'virtual', 'is_disqualified') fields = ("contest", "user", "real_start", "virtual", "is_disqualified")
list_display = ('contest', 'username', 'show_virtual', 'real_start', 'score', 'cumtime', 'tiebreaker') list_display = (
actions = ['recalculate_results'] "contest",
"username",
"show_virtual",
"real_start",
"score",
"cumtime",
"tiebreaker",
)
actions = ["recalculate_results"]
actions_on_bottom = actions_on_top = True actions_on_bottom = actions_on_top = True
search_fields = ('contest__key', 'contest__name', 'user__user__username') search_fields = ("contest__key", "contest__name", "user__user__username")
form = ContestParticipationForm form = ContestParticipationForm
date_hierarchy = 'real_start' date_hierarchy = "real_start"
def get_queryset(self, request): def get_queryset(self, request):
return super(ContestParticipationAdmin, self).get_queryset(request).only( return (
'contest__name', 'contest__format_name', 'contest__format_config', super(ContestParticipationAdmin, self)
'user__user__username', 'real_start', 'score', 'cumtime', 'tiebreaker', 'virtual', .get_queryset(request)
.only(
"contest__name",
"contest__format_name",
"contest__format_config",
"user__user__username",
"real_start",
"score",
"cumtime",
"tiebreaker",
"virtual",
)
) )
def save_model(self, request, obj, form, change): def save_model(self, request, obj, form, change):
super().save_model(request, obj, form, change) super().save_model(request, obj, form, change)
if form.changed_data and 'is_disqualified' in form.changed_data: if form.changed_data and "is_disqualified" in form.changed_data:
obj.set_disqualified(obj.is_disqualified) obj.set_disqualified(obj.is_disqualified)
def recalculate_results(self, request, queryset): def recalculate_results(self, request, queryset):
@ -305,17 +487,48 @@ class ContestParticipationAdmin(admin.ModelAdmin):
for participation in queryset: for participation in queryset:
participation.recompute_results() participation.recompute_results()
count += 1 count += 1
self.message_user(request, ungettext('%d participation recalculated.', self.message_user(
'%d participations recalculated.', request,
count) % count) ungettext(
recalculate_results.short_description = _('Recalculate results') "%d participation recalculated.",
"%d participations recalculated.",
count,
)
% count,
)
recalculate_results.short_description = _("Recalculate results")
def username(self, obj): def username(self, obj):
return obj.user.username return obj.user.username
username.short_description = _('username')
username.admin_order_field = 'user__user__username' username.short_description = _("username")
username.admin_order_field = "user__user__username"
def show_virtual(self, obj): def show_virtual(self, obj):
return obj.virtual or '-' return obj.virtual or "-"
show_virtual.short_description = _('virtual')
show_virtual.admin_order_field = 'virtual' show_virtual.short_description = _("virtual")
show_virtual.admin_order_field = "virtual"
class ContestsSummaryForm(ModelForm):
class Meta:
widgets = {
"contests": AdminHeavySelect2MultipleWidget(
data_view="contest_select2", attrs={"style": "width: 100%"}
),
}
class ContestsSummaryAdmin(admin.ModelAdmin):
fields = ("key", "contests", "scores")
list_display = ("key",)
search_fields = ("key", "contests__key")
form = ContestsSummaryForm
def save_model(self, request, obj, form, change):
super(ContestsSummaryAdmin, self).save_model(request, obj, form, change)
obj.refresh_from_db()
obj.results = recalculate_contest_summary_result(request, obj)
obj.save()

52
judge/admin/course.py Normal file
View file

@ -0,0 +1,52 @@
from django.contrib import admin
from django.utils.html import format_html
from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext, gettext_lazy as _, ungettext
from django.forms import ModelForm
from judge.models import Course, CourseRole
from judge.widgets import AdminSelect2MultipleWidget
from judge.widgets import (
AdminHeavySelect2MultipleWidget,
AdminHeavySelect2Widget,
HeavyPreviewAdminPageDownWidget,
AdminSelect2Widget,
)
class CourseRoleInlineForm(ModelForm):
class Meta:
widgets = {
"user": AdminHeavySelect2Widget(
data_view="profile_select2", attrs={"style": "width: 100%"}
),
"role": AdminSelect2Widget,
}
class CourseRoleInline(admin.TabularInline):
model = CourseRole
extra = 1
form = CourseRoleInlineForm
class CourseForm(ModelForm):
class Meta:
widgets = {
"organizations": AdminHeavySelect2MultipleWidget(
data_view="organization_select2"
),
"about": HeavyPreviewAdminPageDownWidget(
preview=reverse_lazy("blog_preview")
),
}
class CourseAdmin(admin.ModelAdmin):
prepopulated_fields = {"slug": ("name",)}
inlines = [
CourseRoleInline,
]
list_display = ("name", "is_public", "is_open")
search_fields = ("name",)
form = CourseForm

View file

@ -6,26 +6,33 @@ from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from mptt.admin import DraggableMPTTAdmin from mptt.admin import DraggableMPTTAdmin
from reversion.admin import VersionAdmin from reversion.admin import VersionAdmin
from reversion_compare.admin import CompareVersionAdmin
from judge.dblock import LockModel from judge.dblock import LockModel
from judge.models import NavigationBar from judge.models import NavigationBar
from judge.widgets import AdminHeavySelect2MultipleWidget, AdminHeavySelect2Widget, HeavyPreviewAdminPageDownWidget from judge.widgets import (
AdminHeavySelect2MultipleWidget,
AdminHeavySelect2Widget,
HeavyPreviewAdminPageDownWidget,
)
class NavigationBarAdmin(DraggableMPTTAdmin): class NavigationBarAdmin(DraggableMPTTAdmin):
list_display = DraggableMPTTAdmin.list_display + ('key', 'linked_path') list_display = DraggableMPTTAdmin.list_display + ("key", "linked_path")
fields = ('key', 'label', 'path', 'order', 'regex', 'parent') fields = ("key", "label", "path", "order", "regex", "parent")
list_editable = () # Bug in SortableModelAdmin: 500 without list_editable being set list_editable = () # Bug in SortableModelAdmin: 500 without list_editable being set
mptt_level_indent = 20 mptt_level_indent = 20
sortable = 'order' sortable = "order"
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(NavigationBarAdmin, self).__init__(*args, **kwargs) super(NavigationBarAdmin, self).__init__(*args, **kwargs)
self.__save_model_calls = 0 self.__save_model_calls = 0
def linked_path(self, obj): def linked_path(self, obj):
return format_html(u'<a href="{0}" target="_blank">{0}</a>', obj.path) return format_html('<a href="{0}" target="_blank">{0}</a>', obj.path)
linked_path.short_description = _('link path')
linked_path.short_description = _("link path")
def save_model(self, request, obj, form, change): def save_model(self, request, obj, form, change):
self.__save_model_calls += 1 self.__save_model_calls += 1
@ -34,7 +41,9 @@ class NavigationBarAdmin(DraggableMPTTAdmin):
def changelist_view(self, request, extra_context=None): def changelist_view(self, request, extra_context=None):
self.__save_model_calls = 0 self.__save_model_calls = 0
with NavigationBar.objects.disable_mptt_updates(): with NavigationBar.objects.disable_mptt_updates():
result = super(NavigationBarAdmin, self).changelist_view(request, extra_context) result = super(NavigationBarAdmin, self).changelist_view(
request, extra_context
)
if self.__save_model_calls: if self.__save_model_calls:
with LockModel(write=(NavigationBar,)): with LockModel(write=(NavigationBar,)):
NavigationBar.objects.rebuild() NavigationBar.objects.rebuild()
@ -44,74 +53,106 @@ class NavigationBarAdmin(DraggableMPTTAdmin):
class BlogPostForm(ModelForm): class BlogPostForm(ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(BlogPostForm, self).__init__(*args, **kwargs) super(BlogPostForm, self).__init__(*args, **kwargs)
self.fields['authors'].widget.can_add_related = False if "authors" in self.fields:
self.fields["authors"].widget.can_add_related = False
class Meta: class Meta:
widgets = { widgets = {
'authors': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}), "authors": AdminHeavySelect2MultipleWidget(
'organizations': AdminHeavySelect2MultipleWidget(data_view='organization_select2', data_view="profile_select2", attrs={"style": "width: 100%"}
attrs={'style': 'width: 100%'}), ),
"organizations": AdminHeavySelect2MultipleWidget(
data_view="organization_select2", attrs={"style": "width: 100%"}
),
} }
if HeavyPreviewAdminPageDownWidget is not None: if HeavyPreviewAdminPageDownWidget is not None:
widgets['content'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('blog_preview')) widgets["content"] = HeavyPreviewAdminPageDownWidget(
widgets['summary'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('blog_preview')) preview=reverse_lazy("blog_preview")
class BlogPostAdmin(VersionAdmin):
fieldsets = (
(None, {'fields': ('title', 'slug', 'authors', 'visible', 'sticky', 'publish_on',
'is_organization_private', 'organizations')}),
(_('Content'), {'fields': ('content', 'og_image')}),
(_('Summary'), {'classes': ('collapse',), 'fields': ('summary',)}),
) )
prepopulated_fields = {'slug': ('title',)} widgets["summary"] = HeavyPreviewAdminPageDownWidget(
list_display = ('id', 'title', 'visible', 'sticky', 'publish_on') preview=reverse_lazy("blog_preview")
list_display_links = ('id', 'title') )
ordering = ('-publish_on',)
class BlogPostAdmin(CompareVersionAdmin):
fieldsets = (
(
None,
{
"fields": (
"title",
"slug",
"authors",
"visible",
"sticky",
"publish_on",
"is_organization_private",
"organizations",
)
},
),
(_("Content"), {"fields": ("content", "og_image")}),
(_("Summary"), {"classes": ("collapse",), "fields": ("summary",)}),
)
prepopulated_fields = {"slug": ("title",)}
list_display = ("id", "title", "visible", "sticky", "publish_on")
list_display_links = ("id", "title")
ordering = ("-publish_on",)
form = BlogPostForm form = BlogPostForm
date_hierarchy = 'publish_on' date_hierarchy = "publish_on"
def has_change_permission(self, request, obj=None): def has_change_permission(self, request, obj=None):
return (request.user.has_perm('judge.edit_all_post') or return (
request.user.has_perm('judge.change_blogpost') and ( request.user.has_perm("judge.edit_all_post")
obj is None or or request.user.has_perm("judge.change_blogpost")
obj.authors.filter(id=request.profile.id).exists())) and (obj is None or obj.authors.filter(id=request.profile.id).exists())
)
class SolutionForm(ModelForm): class SolutionForm(ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(SolutionForm, self).__init__(*args, **kwargs) super(SolutionForm, self).__init__(*args, **kwargs)
self.fields['authors'].widget.can_add_related = False self.fields["authors"].widget.can_add_related = False
class Meta: class Meta:
widgets = { widgets = {
'authors': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}), "authors": AdminHeavySelect2MultipleWidget(
'problem': AdminHeavySelect2Widget(data_view='problem_select2', attrs={'style': 'width: 250px'}), data_view="profile_select2", attrs={"style": "width: 100%"}
),
"problem": AdminHeavySelect2Widget(
data_view="problem_select2", attrs={"style": "width: 250px"}
),
} }
if HeavyPreviewAdminPageDownWidget is not None: if HeavyPreviewAdminPageDownWidget is not None:
widgets['content'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('solution_preview')) widgets["content"] = HeavyPreviewAdminPageDownWidget(
preview=reverse_lazy("solution_preview")
)
class LicenseForm(ModelForm): class LicenseForm(ModelForm):
class Meta: class Meta:
if HeavyPreviewAdminPageDownWidget is not None: if HeavyPreviewAdminPageDownWidget is not None:
widgets = {'text': HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('license_preview'))} widgets = {
"text": HeavyPreviewAdminPageDownWidget(
preview=reverse_lazy("license_preview")
)
}
class LicenseAdmin(admin.ModelAdmin): class LicenseAdmin(admin.ModelAdmin):
fields = ('key', 'link', 'name', 'display', 'icon', 'text') fields = ("key", "link", "name", "display", "icon", "text")
list_display = ('name', 'key') list_display = ("name", "key")
form = LicenseForm form = LicenseForm
class UserListFilter(admin.SimpleListFilter): class UserListFilter(admin.SimpleListFilter):
title = _('user') title = _("user")
parameter_name = 'user' parameter_name = "user"
def lookups(self, request, model_admin): def lookups(self, request, model_admin):
return User.objects.filter(is_staff=True).values_list('id', 'username') return User.objects.filter(is_staff=True).values_list("id", "username")
def queryset(self, request, queryset): def queryset(self, request, queryset):
if self.value(): if self.value():
@ -120,10 +161,29 @@ class UserListFilter(admin.SimpleListFilter):
class LogEntryAdmin(admin.ModelAdmin): class LogEntryAdmin(admin.ModelAdmin):
readonly_fields = ('user', 'content_type', 'object_id', 'object_repr', 'action_flag', 'change_message') readonly_fields = (
list_display = ('__str__', 'action_time', 'user', 'content_type', 'object_link') "user",
search_fields = ('object_repr', 'change_message') "content_type",
list_filter = (UserListFilter, 'content_type') "object_id",
"object_repr",
"action_flag",
"change_message",
)
list_display = (
"__str__",
"action_time",
"user",
"content_type",
"object_link",
"diff_link",
)
search_fields = (
"object_repr",
"change_message",
"user__username",
"content_type__model",
)
list_filter = (UserListFilter, "content_type")
list_display_links = None list_display_links = None
actions = None actions = None
@ -142,13 +202,35 @@ class LogEntryAdmin(admin.ModelAdmin):
else: else:
ct = obj.content_type ct = obj.content_type
try: try:
link = format_html('<a href="{1}">{0}</a>', obj.object_repr, link = format_html(
reverse('admin:%s_%s_change' % (ct.app_label, ct.model), args=(obj.object_id,))) '<a href="{1}">{0}</a>',
obj.object_repr,
reverse(
"admin:%s_%s_change" % (ct.app_label, ct.model),
args=(obj.object_id,),
),
)
except NoReverseMatch: except NoReverseMatch:
link = obj.object_repr link = obj.object_repr
return link return link
object_link.admin_order_field = 'object_repr'
object_link.short_description = _('object') object_link.admin_order_field = "object_repr"
object_link.short_description = _("object")
def diff_link(self, obj):
if obj.is_deletion():
return None
ct = obj.content_type
try:
url = reverse(
"admin:%s_%s_history" % (ct.app_label, ct.model), args=(obj.object_id,)
)
link = format_html('<a href="{1}">{0}</a>', _("Diff"), url)
except NoReverseMatch:
link = None
return link
diff_link.short_description = _("diff")
def queryset(self, request): def queryset(self, request):
return super().queryset(request).prefetch_related('content_type') return super().queryset(request).prefetch_related("content_type")

View file

@ -6,61 +6,94 @@ from django.utils.translation import gettext, gettext_lazy as _
from reversion.admin import VersionAdmin from reversion.admin import VersionAdmin
from judge.models import Organization from judge.models import Organization
from judge.widgets import AdminHeavySelect2MultipleWidget, AdminHeavySelect2Widget, HeavyPreviewAdminPageDownWidget from judge.widgets import (
AdminHeavySelect2MultipleWidget,
AdminHeavySelect2Widget,
HeavyPreviewAdminPageDownWidget,
)
class OrganizationForm(ModelForm): class OrganizationForm(ModelForm):
class Meta: class Meta:
widgets = { widgets = {
'admins': AdminHeavySelect2MultipleWidget(data_view='profile_select2'), "admins": AdminHeavySelect2MultipleWidget(data_view="profile_select2"),
'registrant': AdminHeavySelect2Widget(data_view='profile_select2'), "registrant": AdminHeavySelect2Widget(data_view="profile_select2"),
} }
if HeavyPreviewAdminPageDownWidget is not None: if HeavyPreviewAdminPageDownWidget is not None:
widgets['about'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('organization_preview')) widgets["about"] = HeavyPreviewAdminPageDownWidget(
preview=reverse_lazy("organization_preview")
)
class OrganizationAdmin(VersionAdmin): class OrganizationAdmin(VersionAdmin):
readonly_fields = ('creation_date',) readonly_fields = ("creation_date",)
fields = ('name', 'slug', 'short_name', 'is_open', 'about', 'logo_override_image', 'slots', 'registrant', fields = (
'creation_date', 'admins') "name",
list_display = ('name', 'short_name', 'is_open', 'slots', 'registrant', 'show_public') "slug",
prepopulated_fields = {'slug': ('name',)} "short_name",
"is_open",
"about",
"slots",
"registrant",
"creation_date",
"admins",
)
list_display = (
"name",
"short_name",
"is_open",
"creation_date",
"registrant",
"show_public",
)
search_fields = ("name", "short_name", "registrant__user__username")
prepopulated_fields = {"slug": ("name",)}
actions_on_top = True actions_on_top = True
actions_on_bottom = True actions_on_bottom = True
form = OrganizationForm form = OrganizationForm
ordering = ["-creation_date"]
def show_public(self, obj): def show_public(self, obj):
return format_html('<a href="{0}" style="white-space:nowrap;">{1}</a>', return format_html(
obj.get_absolute_url(), gettext('View on site')) '<a href="{0}" style="white-space:nowrap;">{1}</a>',
obj.get_absolute_url(),
gettext("View on site"),
)
show_public.short_description = '' show_public.short_description = ""
def get_readonly_fields(self, request, obj=None): def get_readonly_fields(self, request, obj=None):
fields = self.readonly_fields fields = self.readonly_fields
if not request.user.has_perm('judge.organization_admin'): if not request.user.has_perm("judge.organization_admin"):
return fields + ('registrant', 'admins', 'is_open', 'slots') return fields + ("registrant", "admins", "is_open", "slots")
return fields return fields
def get_queryset(self, request): def get_queryset(self, request):
queryset = Organization.objects.all() queryset = Organization.objects.all()
if request.user.has_perm('judge.edit_all_organization'): if request.user.has_perm("judge.edit_all_organization"):
return queryset return queryset
else: else:
return queryset.filter(admins=request.profile.id) return queryset.filter(admins=request.profile.id)
def has_change_permission(self, request, obj=None): def has_change_permission(self, request, obj=None):
if not request.user.has_perm('judge.change_organization'): if not request.user.has_perm("judge.change_organization"):
return False return False
if request.user.has_perm('judge.edit_all_organization') or obj is None: if request.user.has_perm("judge.edit_all_organization") or obj is None:
return True return True
return obj.admins.filter(id=request.profile.id).exists() return obj.admins.filter(id=request.profile.id).exists()
def save_related(self, request, form, formsets, change):
super().save_related(request, form, formsets, change)
obj = form.instance
obj.members.add(*obj.admins.all())
class OrganizationRequestAdmin(admin.ModelAdmin): class OrganizationRequestAdmin(admin.ModelAdmin):
list_display = ('username', 'organization', 'state', 'time') list_display = ("username", "organization", "state", "time")
readonly_fields = ('user', 'organization') readonly_fields = ("user", "organization")
def username(self, obj): def username(self, obj):
return obj.user.user.username return obj.user.user.username
username.short_description = _('username')
username.admin_order_field = 'user__user__username' username.short_description = _("username")
username.admin_order_field = "user__user__username"

View file

@ -1,54 +1,115 @@
from operator import attrgetter from operator import attrgetter
from django import forms from django import forms
from django.contrib import admin from django.contrib import admin, messages
from django.db import transaction from django.db import transaction, IntegrityError
from django.db.models import Q from django.db.models import Q, Avg, Count
from django.forms import ModelForm from django.db.models.aggregates import StdDev
from django.forms import ModelForm, TextInput
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.html import format_html from django.utils.html import format_html
from django.utils.translation import gettext, gettext_lazy as _, ungettext from django.utils.translation import gettext, gettext_lazy as _, ungettext
from reversion.admin import VersionAdmin from django_ace import AceWidget
from django.utils import timezone
from django.core.exceptions import ValidationError
from judge.models import LanguageLimit, Problem, ProblemClarification, ProblemTranslation, Profile, Solution from reversion.admin import VersionAdmin
from judge.widgets import AdminHeavySelect2MultipleWidget, AdminSelect2MultipleWidget, AdminSelect2Widget, \ from reversion_compare.admin import CompareVersionAdmin
CheckboxSelectMultipleWithSelectAll, HeavyPreviewAdminPageDownWidget, HeavyPreviewPageDownWidget
from judge.models import (
LanguageLimit,
LanguageTemplate,
Problem,
ProblemTranslation,
Profile,
Solution,
Notification,
)
from judge.models.notification import make_notification
from judge.widgets import (
AdminHeavySelect2MultipleWidget,
AdminSelect2MultipleWidget,
AdminSelect2Widget,
CheckboxSelectMultipleWithSelectAll,
HeavyPreviewAdminPageDownWidget,
)
from judge.utils.problems import user_editable_ids, user_tester_ids
MEMORY_UNITS = (("KB", "KB"), ("MB", "MB"))
class ProblemForm(ModelForm): class ProblemForm(ModelForm):
change_message = forms.CharField(max_length=256, label='Edit reason', required=False) change_message = forms.CharField(
max_length=256, label="Edit reason", required=False
)
memory_unit = forms.ChoiceField(choices=MEMORY_UNITS)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(ProblemForm, self).__init__(*args, **kwargs) super(ProblemForm, self).__init__(*args, **kwargs)
self.fields['authors'].widget.can_add_related = False self.fields["authors"].widget.can_add_related = False
self.fields['curators'].widget.can_add_related = False self.fields["curators"].widget.can_add_related = False
self.fields['testers'].widget.can_add_related = False self.fields["testers"].widget.can_add_related = False
self.fields['banned_users'].widget.can_add_related = False self.fields["banned_users"].widget.can_add_related = False
self.fields['change_message'].widget.attrs.update({ self.fields["change_message"].widget.attrs.update(
'placeholder': gettext('Describe the changes you made (optional)'), {
}) "placeholder": gettext("Describe the changes you made (optional)"),
}
)
def clean_code(self):
code = self.cleaned_data.get("code")
if self.instance.pk:
return code
if Problem.objects.filter(code=code).exists():
raise ValidationError(_("A problem with this code already exists."))
return code
def clean(self):
memory_unit = self.cleaned_data.get("memory_unit", "KB")
if memory_unit == "MB":
self.cleaned_data["memory_limit"] *= 1024
date = self.cleaned_data.get("date")
if not date or date > timezone.now():
self.cleaned_data["date"] = timezone.now()
return self.cleaned_data
class Meta: class Meta:
widgets = { widgets = {
'authors': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}), "authors": AdminHeavySelect2MultipleWidget(
'curators': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}), data_view="profile_select2", attrs={"style": "width: 100%"}
'testers': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}), ),
'banned_users': AdminHeavySelect2MultipleWidget(data_view='profile_select2', "curators": AdminHeavySelect2MultipleWidget(
attrs={'style': 'width: 100%'}), data_view="profile_select2", attrs={"style": "width: 100%"}
'organizations': AdminHeavySelect2MultipleWidget(data_view='organization_select2', ),
attrs={'style': 'width: 100%'}), "testers": AdminHeavySelect2MultipleWidget(
'types': AdminSelect2MultipleWidget, data_view="profile_select2", attrs={"style": "width: 100%"}
'group': AdminSelect2Widget, ),
"banned_users": AdminHeavySelect2MultipleWidget(
data_view="profile_select2", attrs={"style": "width: 100%"}
),
"organizations": AdminHeavySelect2MultipleWidget(
data_view="organization_select2", attrs={"style": "width: 100%"}
),
"types": AdminSelect2MultipleWidget,
"group": AdminSelect2Widget,
"memory_limit": TextInput(attrs={"size": "20"}),
} }
if HeavyPreviewAdminPageDownWidget is not None: if HeavyPreviewAdminPageDownWidget is not None:
widgets['description'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('problem_preview')) widgets["description"] = HeavyPreviewAdminPageDownWidget(
preview=reverse_lazy("problem_preview")
)
class ProblemCreatorListFilter(admin.SimpleListFilter): class ProblemCreatorListFilter(admin.SimpleListFilter):
title = parameter_name = 'creator' title = parameter_name = "creator"
def lookups(self, request, model_admin): def lookups(self, request, model_admin):
queryset = Profile.objects.exclude(authored_problems=None).values_list('user__username', flat=True) queryset = Profile.objects.exclude(authored_problems=None).values_list(
"user__username", flat=True
)
return [(name, name) for name in queryset] return [(name, name) for name in queryset]
def queryset(self, request, queryset): def queryset(self, request, queryset):
@ -58,46 +119,68 @@ class ProblemCreatorListFilter(admin.SimpleListFilter):
class LanguageLimitInlineForm(ModelForm): class LanguageLimitInlineForm(ModelForm):
memory_unit = forms.ChoiceField(choices=MEMORY_UNITS, label=_("Memory unit"))
class Meta: class Meta:
widgets = {'language': AdminSelect2Widget} widgets = {
"language": AdminSelect2Widget,
"memory_limit": TextInput(attrs={"size": "10"}),
}
def clean(self):
if not self.cleaned_data.get("language"):
self.cleaned_data["DELETE"] = True
if (
self.cleaned_data.get("memory_limit")
and self.cleaned_data.get("memory_unit") == "MB"
):
self.cleaned_data["memory_limit"] *= 1024
return self.cleaned_data
class LanguageLimitInline(admin.TabularInline): class LanguageLimitInline(admin.TabularInline):
model = LanguageLimit model = LanguageLimit
fields = ('language', 'time_limit', 'memory_limit') fields = ("language", "time_limit", "memory_limit", "memory_unit")
form = LanguageLimitInlineForm form = LanguageLimitInlineForm
extra = 0
class ProblemClarificationForm(ModelForm): class LanguageTemplateInlineForm(ModelForm):
class Meta: class Meta:
if HeavyPreviewPageDownWidget is not None: widgets = {
widgets = {'description': HeavyPreviewPageDownWidget(preview=reverse_lazy('comment_preview'))} "language": AdminSelect2Widget,
"source": AceWidget(width="600px", height="200px", toolbar=False),
}
class ProblemClarificationInline(admin.StackedInline): class LanguageTemplateInline(admin.TabularInline):
model = ProblemClarification model = LanguageTemplate
fields = ('description',) fields = ("language", "source")
form = ProblemClarificationForm form = LanguageTemplateInlineForm
extra = 0 extra = 0
class ProblemSolutionForm(ModelForm): class ProblemSolutionForm(ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(ProblemSolutionForm, self).__init__(*args, **kwargs) super(ProblemSolutionForm, self).__init__(*args, **kwargs)
self.fields['authors'].widget.can_add_related = False self.fields["authors"].widget.can_add_related = False
class Meta: class Meta:
widgets = { widgets = {
'authors': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}), "authors": AdminHeavySelect2MultipleWidget(
data_view="profile_select2", attrs={"style": "width: 100%"}
),
} }
if HeavyPreviewAdminPageDownWidget is not None: if HeavyPreviewAdminPageDownWidget is not None:
widgets['content'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('solution_preview')) widgets["content"] = HeavyPreviewAdminPageDownWidget(
preview=reverse_lazy("solution_preview")
)
class ProblemSolutionInline(admin.StackedInline): class ProblemSolutionInline(admin.StackedInline):
model = Solution model = Solution
fields = ('is_public', 'publish_on', 'authors', 'content') fields = ("is_public", "publish_on", "authors", "content")
form = ProblemSolutionForm form = ProblemSolutionForm
extra = 0 extra = 0
@ -105,164 +188,300 @@ class ProblemSolutionInline(admin.StackedInline):
class ProblemTranslationForm(ModelForm): class ProblemTranslationForm(ModelForm):
class Meta: class Meta:
if HeavyPreviewAdminPageDownWidget is not None: if HeavyPreviewAdminPageDownWidget is not None:
widgets = {'description': HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('problem_preview'))} widgets = {
"description": HeavyPreviewAdminPageDownWidget(
preview=reverse_lazy("problem_preview")
)
}
class ProblemTranslationInline(admin.StackedInline): class ProblemTranslationInline(admin.StackedInline):
model = ProblemTranslation model = ProblemTranslation
fields = ('language', 'name', 'description') fields = ("language", "name", "description")
form = ProblemTranslationForm form = ProblemTranslationForm
extra = 0 extra = 0
class ProblemAdmin(VersionAdmin): class ProblemAdmin(CompareVersionAdmin):
fieldsets = ( fieldsets = (
(None, { (
'fields': ( None,
'code', 'name', 'is_public', 'is_manually_managed', 'date', 'authors', 'curators', 'testers', {
'is_organization_private', 'organizations', 'description', 'license', "fields": (
"code",
"name",
"is_public",
"organizations",
"date",
"authors",
"curators",
"testers",
"description",
"pdf_description",
"license",
), ),
}), },
(_('Social Media'), {'classes': ('collapse',), 'fields': ('og_image', 'summary')}), ),
(_('Taxonomy'), {'fields': ('types', 'group')}), (
(_('Points'), {'fields': (('points', 'partial'), 'short_circuit')}), _("Social Media"),
(_('Limits'), {'fields': ('time_limit', 'memory_limit')}), {"classes": ("collapse",), "fields": ("og_image", "summary")},
(_('Language'), {'fields': ('allowed_languages',)}), ),
(_('Justice'), {'fields': ('banned_users',)}), (_("Taxonomy"), {"fields": ("types", "group")}),
(_('History'), {'fields': ('change_message',)}), (_("Points"), {"fields": (("points", "partial"), "short_circuit")}),
(_("Limits"), {"fields": ("time_limit", ("memory_limit", "memory_unit"))}),
(_("Language"), {"fields": ("allowed_languages",)}),
(_("Justice"), {"fields": ("banned_users",)}),
(_("History"), {"fields": ("change_message",)}),
) )
list_display = ['code', 'name', 'show_authors', 'points', 'is_public', 'show_public'] list_display = [
ordering = ['code'] "code",
search_fields = ('code', 'name', 'authors__user__username', 'curators__user__username') "name",
inlines = [LanguageLimitInline, ProblemClarificationInline, ProblemSolutionInline, ProblemTranslationInline] "show_authors",
"date",
"points",
"is_public",
"show_public",
]
ordering = ["-date"]
search_fields = (
"code",
"name",
"authors__user__username",
"curators__user__username",
)
inlines = [
LanguageLimitInline,
LanguageTemplateInline,
ProblemSolutionInline,
ProblemTranslationInline,
]
list_max_show_all = 1000 list_max_show_all = 1000
actions_on_top = True actions_on_top = True
actions_on_bottom = True actions_on_bottom = True
list_filter = ('is_public', ProblemCreatorListFilter) list_filter = ("is_public", ProblemCreatorListFilter)
form = ProblemForm form = ProblemForm
date_hierarchy = 'date' date_hierarchy = "date"
def get_actions(self, request): def get_actions(self, request):
actions = super(ProblemAdmin, self).get_actions(request) actions = super(ProblemAdmin, self).get_actions(request)
if request.user.has_perm('judge.change_public_visibility'): if request.user.has_perm("judge.change_public_visibility"):
func, name, desc = self.get_action('make_public') func, name, desc = self.get_action("make_public")
actions[name] = (func, name, desc) actions[name] = (func, name, desc)
func, name, desc = self.get_action('make_private') func, name, desc = self.get_action("make_private")
actions[name] = (func, name, desc) actions[name] = (func, name, desc)
return actions return actions
def get_readonly_fields(self, request, obj=None): def get_readonly_fields(self, request, obj=None):
fields = self.readonly_fields fields = self.readonly_fields
if not request.user.has_perm('judge.change_public_visibility'): if not request.user.has_perm("judge.change_public_visibility"):
fields += ('is_public',) fields += ("is_public",)
if not request.user.has_perm('judge.change_manually_managed'):
fields += ('is_manually_managed',)
return fields return fields
def show_authors(self, obj): def show_authors(self, obj):
return ', '.join(map(attrgetter('user.username'), obj.authors.all())) return ", ".join(map(attrgetter("user.username"), obj.authors.all()))
show_authors.short_description = _('Authors') show_authors.short_description = _("Authors")
def show_public(self, obj): def show_public(self, obj):
return format_html('<a href="{1}">{0}</a>', gettext('View on site'), obj.get_absolute_url()) return format_html(
'<a href="{1}">{0}</a>', gettext("View on site"), obj.get_absolute_url()
)
show_public.short_description = '' show_public.short_description = ""
def _rescore(self, request, problem_id): def _rescore(self, request, problem_id):
from judge.tasks import rescore_problem from judge.tasks import rescore_problem
transaction.on_commit(rescore_problem.s(problem_id).delay) transaction.on_commit(rescore_problem.s(problem_id).delay)
def make_public(self, request, queryset): def make_public(self, request, queryset):
count = queryset.update(is_public=True) count = queryset.update(is_public=True)
for problem_id in queryset.values_list('id', flat=True): for problem_id in queryset.values_list("id", flat=True):
self._rescore(request, problem_id) self._rescore(request, problem_id)
self.message_user(request, ungettext('%d problem successfully marked as public.', self.message_user(
'%d problems successfully marked as public.', request,
count) % count) ungettext(
"%d problem successfully marked as public.",
"%d problems successfully marked as public.",
count,
)
% count,
)
make_public.short_description = _('Mark problems as public') make_public.short_description = _("Mark problems as public")
def make_private(self, request, queryset): def make_private(self, request, queryset):
count = queryset.update(is_public=False) count = queryset.update(is_public=False)
for problem_id in queryset.values_list('id', flat=True): for problem_id in queryset.values_list("id", flat=True):
self._rescore(request, problem_id) self._rescore(request, problem_id)
self.message_user(request, ungettext('%d problem successfully marked as private.', self.message_user(
'%d problems successfully marked as private.', request,
count) % count) ungettext(
"%d problem successfully marked as private.",
"%d problems successfully marked as private.",
count,
)
% count,
)
make_private.short_description = _('Mark problems as private') make_private.short_description = _("Mark problems as private")
def get_queryset(self, request): def get_queryset(self, request):
queryset = Problem.objects.prefetch_related('authors__user') queryset = Problem.objects.prefetch_related("authors__user")
if request.user.has_perm('judge.edit_all_problem'): if request.user.has_perm("judge.edit_all_problem"):
return queryset return queryset
access = Q() access = Q()
if request.user.has_perm('judge.edit_public_problem'): if request.user.has_perm("judge.edit_public_problem"):
access |= Q(is_public=True) access |= Q(is_public=True)
if request.user.has_perm('judge.edit_own_problem'): if request.user.has_perm("judge.edit_own_problem"):
access |= Q(authors__id=request.profile.id) | Q(curators__id=request.profile.id) access |= Q(authors__id=request.profile.id) | Q(
curators__id=request.profile.id
)
return queryset.filter(access).distinct() if access else queryset.none() return queryset.filter(access).distinct() if access else queryset.none()
def has_change_permission(self, request, obj=None): def has_change_permission(self, request, obj=None):
if request.user.has_perm('judge.edit_all_problem') or obj is None: if request.user.has_perm("judge.edit_all_problem") or obj is None:
return True return True
if request.user.has_perm('judge.edit_public_problem') and obj.is_public: if request.user.has_perm("judge.edit_public_problem") and obj.is_public:
return True return True
if not request.user.has_perm('judge.edit_own_problem'): if not request.user.has_perm("judge.edit_own_problem"):
return False return False
return obj.is_editor(request.profile) return obj.is_editor(request.profile)
def formfield_for_manytomany(self, db_field, request=None, **kwargs): def formfield_for_manytomany(self, db_field, request=None, **kwargs):
if db_field.name == 'allowed_languages': if db_field.name == "allowed_languages":
kwargs['widget'] = CheckboxSelectMultipleWithSelectAll() kwargs["widget"] = CheckboxSelectMultipleWithSelectAll()
return super(ProblemAdmin, self).formfield_for_manytomany(db_field, request, **kwargs) return super(ProblemAdmin, self).formfield_for_manytomany(
db_field, request, **kwargs
)
def get_form(self, *args, **kwargs): def get_form(self, *args, **kwargs):
form = super(ProblemAdmin, self).get_form(*args, **kwargs) form = super(ProblemAdmin, self).get_form(*args, **kwargs)
form.base_fields['authors'].queryset = Profile.objects.all() form.base_fields["authors"].queryset = Profile.objects.all()
return form return form
def save_model(self, request, obj, form, change): def save_model(self, request, obj, form, change):
form.changed_data.remove("memory_unit")
super().save_model(request, obj, form, change) super().save_model(request, obj, form, change)
if form.changed_data and any(f in form.changed_data for f in ('is_public', 'points', 'partial')): if form.changed_data and any(
f in form.changed_data for f in ("is_public", "points", "partial")
):
self._rescore(request, obj.id) self._rescore(request, obj.id)
def save_related(self, request, form, formsets, change):
editors = set()
testers = set()
if "curators" in form.changed_data or "authors" in form.changed_data:
editors = set(form.instance.editor_ids)
if "testers" in form.changed_data:
testers = set(form.instance.tester_ids)
super().save_related(request, form, formsets, change)
obj = form.instance
obj.curators.add(request.profile)
if "curators" in form.changed_data or "authors" in form.changed_data:
del obj.editor_ids
editors = editors.union(set(obj.editor_ids))
if "testers" in form.changed_data:
del obj.tester_ids
testers = testers.union(set(obj.tester_ids))
for editor in editors:
user_editable_ids.dirty(editor)
for tester in testers:
user_tester_ids.dirty(tester)
# Create notification
if "is_public" in form.changed_data or "organizations" in form.changed_data:
users = set(obj.authors.all())
users = users.union(users, set(obj.curators.all()))
orgs = []
if obj.organizations.count() > 0:
for org in obj.organizations.all():
users = users.union(users, set(org.admins.all()))
orgs.append(org.name)
else:
admins = Profile.objects.filter(user__is_superuser=True).all()
users = users.union(users, admins)
link = reverse_lazy("admin:judge_problem_change", args=(obj.id,))
html = f'<a href="{link}">{obj.name}</a>'
category = "Problem public: " + str(obj.is_public)
if orgs:
category += " (" + ", ".join(orgs) + ")"
make_notification(users, category, html, request.profile)
def construct_change_message(self, request, form, *args, **kwargs): def construct_change_message(self, request, form, *args, **kwargs):
if form.cleaned_data.get('change_message'): if form.cleaned_data.get("change_message"):
return form.cleaned_data['change_message'] return form.cleaned_data["change_message"]
return super(ProblemAdmin, self).construct_change_message(request, form, *args, **kwargs) return super(ProblemAdmin, self).construct_change_message(
request, form, *args, **kwargs
)
class ProblemPointsVoteAdmin(admin.ModelAdmin): class ProblemPointsVoteAdmin(admin.ModelAdmin):
list_display = ('vote_points', 'voter', 'problem_name', 'problem_code', 'problem_points', 'vote_time') list_display = (
search_fields = ('voter__user__username', 'problem__code', 'problem__name') "vote_points",
readonly_fields = ('voter', 'problem', 'problem_code', 'problem_points', 'vote_time') "voter",
"voter_rating",
"voter_point",
"problem_name",
"problem_code",
"problem_points",
)
search_fields = ("voter__user__username", "problem__code", "problem__name")
readonly_fields = (
"voter",
"problem",
"problem_code",
"problem_points",
"voter_rating",
"voter_point",
)
def has_change_permission(self, request, obj=None): def has_change_permission(self, request, obj=None):
if obj is None: if obj is None:
return request.user.has_perm('judge.edit_own_problem') return request.user.has_perm("judge.edit_own_problem")
return obj.problem.is_editable_by(request.user) return obj.problem.is_editable_by(request.user)
def lookup_allowed(self, key, value):
return True
def problem_code(self, obj): def problem_code(self, obj):
return obj.problem.code return obj.problem.code
problem_code.short_description = _('Problem code')
problem_code.admin_order_field = 'problem__code' problem_code.short_description = _("Problem code")
problem_code.admin_order_field = "problem__code"
def problem_points(self, obj): def problem_points(self, obj):
return obj.problem.points return obj.problem.points
problem_points.short_description = _('Points')
problem_points.admin_order_field = 'problem__points' problem_points.short_description = _("Points")
problem_points.admin_order_field = "problem__points"
def problem_name(self, obj): def problem_name(self, obj):
return obj.problem.name return obj.problem.name
problem_name.short_description = _('Problem name')
problem_name.admin_order_field = 'problem__name' problem_name.short_description = _("Problem name")
problem_name.admin_order_field = "problem__name"
def voter_rating(self, obj):
return obj.voter.rating
voter_rating.short_description = _("Voter rating")
voter_rating.admin_order_field = "voter__rating"
def voter_point(self, obj):
return round(obj.voter.performance_points)
voter_point.short_description = _("Voter point")
voter_point.admin_order_field = "voter__performance_points"
def vote_points(self, obj): def vote_points(self, obj):
return obj.points return obj.points
vote_points.short_description = _('Vote')
vote_points.short_description = _("Vote")

View file

@ -1,41 +1,58 @@
from django.contrib import admin from django.contrib import admin
from django.forms import ModelForm from django.forms import ModelForm, CharField, TextInput
from django.utils.html import format_html from django.utils.html import format_html
from django.utils.translation import gettext, gettext_lazy as _, ungettext from django.utils.translation import gettext, gettext_lazy as _, ungettext
from reversion.admin import VersionAdmin from django.contrib.auth.admin import UserAdmin as OldUserAdmin
from django.core.exceptions import ValidationError
from django.contrib.auth.forms import UserChangeForm
from django_ace import AceWidget from django_ace import AceWidget
from judge.models import Profile
from judge.models import Profile, ProfileInfo
from judge.widgets import AdminPagedownWidget, AdminSelect2Widget from judge.widgets import AdminPagedownWidget, AdminSelect2Widget
from reversion.admin import VersionAdmin
import re
class ProfileForm(ModelForm): class ProfileForm(ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(ProfileForm, self).__init__(*args, **kwargs) super(ProfileForm, self).__init__(*args, **kwargs)
if 'current_contest' in self.base_fields: if "current_contest" in self.base_fields:
# form.fields['current_contest'] does not exist when the user has only view permission on the model. # form.fields['current_contest'] does not exist when the user has only view permission on the model.
self.fields['current_contest'].queryset = self.instance.contest_history.select_related('contest') \ self.fields[
.only('contest__name', 'user_id', 'virtual') "current_contest"
self.fields['current_contest'].label_from_instance = \ ].queryset = self.instance.contest_history.select_related("contest").only(
lambda obj: '%s v%d' % (obj.contest.name, obj.virtual) if obj.virtual else obj.contest.name "contest__name", "user_id", "virtual"
)
self.fields["current_contest"].label_from_instance = (
lambda obj: "%s v%d" % (obj.contest.name, obj.virtual)
if obj.virtual
else obj.contest.name
)
class Meta: class Meta:
widgets = { widgets = {
'timezone': AdminSelect2Widget, "timezone": AdminSelect2Widget,
'language': AdminSelect2Widget, "language": AdminSelect2Widget,
'ace_theme': AdminSelect2Widget, "ace_theme": AdminSelect2Widget,
'current_contest': AdminSelect2Widget, "current_contest": AdminSelect2Widget,
} }
if AdminPagedownWidget is not None: if AdminPagedownWidget is not None:
widgets['about'] = AdminPagedownWidget widgets["about"] = AdminPagedownWidget
class TimezoneFilter(admin.SimpleListFilter): class TimezoneFilter(admin.SimpleListFilter):
title = _('timezone') title = _("timezone")
parameter_name = 'timezone' parameter_name = "timezone"
def lookups(self, request, model_admin): def lookups(self, request, model_admin):
return Profile.objects.values_list('timezone', 'timezone').distinct().order_by('timezone') return (
Profile.objects.values_list("timezone", "timezone")
.distinct()
.order_by("timezone")
)
def queryset(self, request, queryset): def queryset(self, request, queryset):
if self.value() is None: if self.value() is None:
@ -43,76 +60,168 @@ class TimezoneFilter(admin.SimpleListFilter):
return queryset.filter(timezone=self.value()) return queryset.filter(timezone=self.value())
class ProfileInfoInline(admin.StackedInline):
model = ProfileInfo
can_delete = False
verbose_name_plural = "profile info"
fk_name = "profile"
class ProfileAdmin(VersionAdmin): class ProfileAdmin(VersionAdmin):
fields = ('user', 'display_rank', 'about', 'organizations', 'timezone', 'language', 'ace_theme', fields = (
'math_engine', 'last_access', 'ip', 'mute', 'is_unlisted', 'is_banned_problem_voting', 'notes', 'is_totp_enabled', 'user_script', "user",
'current_contest') "display_rank",
readonly_fields = ('user',) "about",
list_display = ('admin_user_admin', 'email', 'is_totp_enabled', 'timezone_full', "organizations",
'date_joined', 'last_access', 'ip', 'show_public') "timezone",
ordering = ('user__username',) "language",
search_fields = ('user__username', 'ip', 'user__email') "ace_theme",
list_filter = ('language', TimezoneFilter) "last_access",
actions = ('recalculate_points',) "ip",
"mute",
"is_unlisted",
"notes",
"is_totp_enabled",
"current_contest",
)
readonly_fields = ("user",)
list_display = (
"admin_user_admin",
"email",
"is_totp_enabled",
"timezone_full",
"date_joined",
"last_access",
"ip",
"show_public",
)
ordering = ("user__username",)
search_fields = ("user__username", "ip", "user__email")
list_filter = ("language", TimezoneFilter)
actions = ("recalculate_points",)
actions_on_top = True actions_on_top = True
actions_on_bottom = True actions_on_bottom = True
form = ProfileForm form = ProfileForm
inlines = (ProfileInfoInline,)
def get_queryset(self, request): def get_queryset(self, request):
return super(ProfileAdmin, self).get_queryset(request).select_related('user') return super(ProfileAdmin, self).get_queryset(request).select_related("user")
def get_fields(self, request, obj=None): def get_fields(self, request, obj=None):
if request.user.has_perm('judge.totp'): if request.user.has_perm("judge.totp"):
fields = list(self.fields) fields = list(self.fields)
fields.insert(fields.index('is_totp_enabled') + 1, 'totp_key') fields.insert(fields.index("is_totp_enabled") + 1, "totp_key")
return tuple(fields) return tuple(fields)
else: else:
return self.fields return self.fields
def get_readonly_fields(self, request, obj=None): def get_readonly_fields(self, request, obj=None):
fields = self.readonly_fields fields = self.readonly_fields
if not request.user.has_perm('judge.totp'): if not request.user.has_perm("judge.totp"):
fields += ('is_totp_enabled',) fields += ("is_totp_enabled",)
return fields return fields
def show_public(self, obj): def show_public(self, obj):
return format_html('<a href="{0}" style="white-space:nowrap;">{1}</a>', return format_html(
obj.get_absolute_url(), gettext('View on site')) '<a href="{0}" style="white-space:nowrap;">{1}</a>',
show_public.short_description = '' obj.get_absolute_url(),
gettext("View on site"),
)
show_public.short_description = ""
def admin_user_admin(self, obj): def admin_user_admin(self, obj):
return obj.username return obj.username
admin_user_admin.admin_order_field = 'user__username'
admin_user_admin.short_description = _('User') admin_user_admin.admin_order_field = "user__username"
admin_user_admin.short_description = _("User")
def email(self, obj): def email(self, obj):
return obj.user.email return obj.email
email.admin_order_field = 'user__email'
email.short_description = _('Email') email.admin_order_field = "user__email"
email.short_description = _("Email")
def timezone_full(self, obj): def timezone_full(self, obj):
return obj.timezone return obj.timezone
timezone_full.admin_order_field = 'timezone'
timezone_full.short_description = _('Timezone') timezone_full.admin_order_field = "timezone"
timezone_full.short_description = _("Timezone")
def date_joined(self, obj): def date_joined(self, obj):
return obj.user.date_joined return obj.user.date_joined
date_joined.admin_order_field = 'user__date_joined'
date_joined.short_description = _('date joined') date_joined.admin_order_field = "user__date_joined"
date_joined.short_description = _("date joined")
def recalculate_points(self, request, queryset): def recalculate_points(self, request, queryset):
count = 0 count = 0
for profile in queryset: for profile in queryset:
profile.calculate_points() profile.calculate_points()
count += 1 count += 1
self.message_user(request, ungettext('%d user have scores recalculated.', self.message_user(
'%d users have scores recalculated.', request,
count) % count) ungettext(
recalculate_points.short_description = _('Recalculate scores') "%d user have scores recalculated.",
"%d users have scores recalculated.",
count,
)
% count,
)
def get_form(self, request, obj=None, **kwargs): recalculate_points.short_description = _("Recalculate scores")
form = super(ProfileAdmin, self).get_form(request, obj, **kwargs)
if 'user_script' in form.base_fields:
# form.base_fields['user_script'] does not exist when the user has only view permission on the model. class UserForm(UserChangeForm):
form.base_fields['user_script'].widget = AceWidget('javascript', request.profile.ace_theme) def __init__(self, *args, **kwargs):
return form super().__init__(*args, **kwargs)
self.fields["username"].help_text = _(
"Username can only contain letters, digits, and underscores."
)
def clean_username(self):
username = self.cleaned_data.get("username")
if not re.match(r"^\w+$", username):
raise ValidationError(
_("Username can only contain letters, digits, and underscores.")
)
return username
class UserAdmin(OldUserAdmin):
# Customize the fieldsets for adding and editing users
form = UserForm
fieldsets = (
(None, {"fields": ("username", "password")}),
("Personal Info", {"fields": ("first_name", "last_name", "email")}),
(
"Permissions",
{
"fields": (
"is_active",
"is_staff",
"is_superuser",
"groups",
"user_permissions",
)
},
),
("Important dates", {"fields": ("last_login", "date_joined")}),
)
readonly_fields = ("last_login", "date_joined")
def get_readonly_fields(self, request, obj=None):
fields = self.readonly_fields
if not request.user.is_superuser:
fields += (
"is_staff",
"is_active",
"is_superuser",
"groups",
"user_permissions",
)
return fields
def has_add_permission(self, request):
return False

View file

@ -16,41 +16,63 @@ from judge.widgets import AdminHeavySelect2MultipleWidget, AdminPagedownWidget
class LanguageForm(ModelForm): class LanguageForm(ModelForm):
problems = ModelMultipleChoiceField( problems = ModelMultipleChoiceField(
label=_('Disallowed problems'), label=_("Disallowed problems"),
queryset=Problem.objects.all(), queryset=Problem.objects.all(),
required=False, required=False,
help_text=_('These problems are NOT allowed to be submitted in this language'), help_text=_("These problems are NOT allowed to be submitted in this language"),
widget=AdminHeavySelect2MultipleWidget(data_view='problem_select2')) widget=AdminHeavySelect2MultipleWidget(data_view="problem_select2"),
)
class Meta: class Meta:
if AdminPagedownWidget is not None: if AdminPagedownWidget is not None:
widgets = {'description': AdminPagedownWidget} widgets = {"description": AdminPagedownWidget}
class LanguageAdmin(VersionAdmin): class LanguageAdmin(VersionAdmin):
fields = ('key', 'name', 'short_name', 'common_name', 'ace', 'pygments', 'info', 'description', fields = (
'template', 'problems') "key",
list_display = ('key', 'name', 'common_name', 'info') "name",
"short_name",
"common_name",
"ace",
"pygments",
"info",
"description",
"template",
"problems",
)
list_display = ("key", "name", "common_name", "info")
form = LanguageForm form = LanguageForm
def save_model(self, request, obj, form, change): def save_model(self, request, obj, form, change):
super(LanguageAdmin, self).save_model(request, obj, form, change) super(LanguageAdmin, self).save_model(request, obj, form, change)
obj.problem_set.set(Problem.objects.exclude(id__in=form.cleaned_data['problems'].values('id'))) obj.problem_set.set(
Problem.objects.exclude(id__in=form.cleaned_data["problems"].values("id"))
)
def get_form(self, request, obj=None, **kwargs): def get_form(self, request, obj=None, **kwargs):
self.form.base_fields['problems'].initial = \ self.form.base_fields["problems"].initial = (
Problem.objects.exclude(id__in=obj.problem_set.values('id')).values_list('pk', flat=True) if obj else [] Problem.objects.exclude(id__in=obj.problem_set.values("id")).values_list(
"pk", flat=True
)
if obj
else []
)
form = super(LanguageAdmin, self).get_form(request, obj, **kwargs) form = super(LanguageAdmin, self).get_form(request, obj, **kwargs)
if obj is not None: if obj is not None:
form.base_fields['template'].widget = AceWidget(obj.ace, request.profile.ace_theme) form.base_fields["template"].widget = AceWidget(
obj.ace, request.profile.ace_theme
)
return form return form
class GenerateKeyTextInput(TextInput): class GenerateKeyTextInput(TextInput):
def render(self, name, value, attrs=None, renderer=None): def render(self, name, value, attrs=None, renderer=None):
text = super(TextInput, self).render(name, value, attrs) text = super(TextInput, self).render(name, value, attrs)
return mark_safe(text + format_html( return mark_safe(
'''\ text
+ format_html(
"""\
<a href="#" onclick="return false;" class="button" id="id_{0}_regen">Regenerate</a> <a href="#" onclick="return false;" class="button" id="id_{0}_regen">Regenerate</a>
<script type="text/javascript"> <script type="text/javascript">
django.jQuery(document).ready(function ($) {{ django.jQuery(document).ready(function ($) {{
@ -65,37 +87,59 @@ django.jQuery(document).ready(function ($) {{
}}); }});
}}); }});
</script> </script>
''', name)) """,
name,
)
)
class JudgeAdminForm(ModelForm): class JudgeAdminForm(ModelForm):
class Meta: class Meta:
widgets = {'auth_key': GenerateKeyTextInput} widgets = {"auth_key": GenerateKeyTextInput}
if AdminPagedownWidget is not None: if AdminPagedownWidget is not None:
widgets['description'] = AdminPagedownWidget widgets["description"] = AdminPagedownWidget
class JudgeAdmin(VersionAdmin): class JudgeAdmin(VersionAdmin):
form = JudgeAdminForm form = JudgeAdminForm
readonly_fields = ('created', 'online', 'start_time', 'ping', 'load', 'last_ip', 'runtimes', 'problems') readonly_fields = (
fieldsets = ( "created",
(None, {'fields': ('name', 'auth_key', 'is_blocked')}), "online",
(_('Description'), {'fields': ('description',)}), "start_time",
(_('Information'), {'fields': ('created', 'online', 'last_ip', 'start_time', 'ping', 'load')}), "ping",
(_('Capabilities'), {'fields': ('runtimes', 'problems')}), "load",
"last_ip",
"runtimes",
"problems",
) )
list_display = ('name', 'online', 'start_time', 'ping', 'load', 'last_ip') fieldsets = (
ordering = ['-online', 'name'] (None, {"fields": ("name", "auth_key", "is_blocked")}),
(_("Description"), {"fields": ("description",)}),
(
_("Information"),
{"fields": ("created", "online", "last_ip", "start_time", "ping", "load")},
),
(_("Capabilities"), {"fields": ("runtimes", "problems")}),
)
list_display = ("name", "online", "start_time", "ping", "load", "last_ip")
ordering = ["-online", "name"]
def get_urls(self): def get_urls(self):
return ([url(r'^(\d+)/disconnect/$', self.disconnect_view, name='judge_judge_disconnect'), return [
url(r'^(\d+)/terminate/$', self.terminate_view, name='judge_judge_terminate')] + url(
super(JudgeAdmin, self).get_urls()) r"^(\d+)/disconnect/$",
self.disconnect_view,
name="judge_judge_disconnect",
),
url(
r"^(\d+)/terminate/$", self.terminate_view, name="judge_judge_terminate"
),
] + super(JudgeAdmin, self).get_urls()
def disconnect_judge(self, id, force=False): def disconnect_judge(self, id, force=False):
judge = get_object_or_404(Judge, id=id) judge = get_object_or_404(Judge, id=id)
judge.disconnect(force=force) judge.disconnect(force=force)
return HttpResponseRedirect(reverse('admin:judge_judge_changelist')) return HttpResponseRedirect(reverse("admin:judge_judge_changelist"))
def disconnect_view(self, request, id): def disconnect_view(self, request, id):
return self.disconnect_judge(id) return self.disconnect_judge(id)
@ -105,7 +149,7 @@ class JudgeAdmin(VersionAdmin):
def get_readonly_fields(self, request, obj=None): def get_readonly_fields(self, request, obj=None):
if obj is not None and obj.online: if obj is not None and obj.online:
return self.readonly_fields + ('name',) return self.readonly_fields + ("name",)
return self.readonly_fields return self.readonly_fields
def has_delete_permission(self, request, obj=None): def has_delete_permission(self, request, obj=None):
@ -116,5 +160,5 @@ class JudgeAdmin(VersionAdmin):
if AdminPagedownWidget is not None: if AdminPagedownWidget is not None:
formfield_overrides = { formfield_overrides = {
TextField: {'widget': AdminPagedownWidget}, TextField: {"widget": AdminPagedownWidget},
} }

View file

@ -13,239 +13,365 @@ from django.utils.html import format_html
from django.utils.translation import gettext, gettext_lazy as _, pgettext, ungettext from django.utils.translation import gettext, gettext_lazy as _, pgettext, ungettext
from django_ace import AceWidget from django_ace import AceWidget
from judge.models import ContestParticipation, ContestProblem, ContestSubmission, Profile, Submission, \ from judge.models import (
SubmissionSource, SubmissionTestCase ContestParticipation,
ContestProblem,
ContestSubmission,
Profile,
Submission,
SubmissionSource,
SubmissionTestCase,
)
from judge.utils.raw_sql import use_straight_join from judge.utils.raw_sql import use_straight_join
class SubmissionStatusFilter(admin.SimpleListFilter): class SubmissionStatusFilter(admin.SimpleListFilter):
parameter_name = title = 'status' parameter_name = title = "status"
__lookups = (('None', _('None')), ('NotDone', _('Not done')), ('EX', _('Exceptional'))) + Submission.STATUS __lookups = (
("None", _("None")),
("NotDone", _("Not done")),
("EX", _("Exceptional")),
) + Submission.STATUS
__handles = set(map(itemgetter(0), Submission.STATUS)) __handles = set(map(itemgetter(0), Submission.STATUS))
def lookups(self, request, model_admin): def lookups(self, request, model_admin):
return self.__lookups return self.__lookups
def queryset(self, request, queryset): def queryset(self, request, queryset):
if self.value() == 'None': if self.value() == "None":
return queryset.filter(status=None) return queryset.filter(status=None)
elif self.value() == 'NotDone': elif self.value() == "NotDone":
return queryset.exclude(status__in=['D', 'IE', 'CE', 'AB']) return queryset.exclude(status__in=["D", "IE", "CE", "AB"])
elif self.value() == 'EX': elif self.value() == "EX":
return queryset.exclude(status__in=['D', 'CE', 'G', 'AB']) return queryset.exclude(status__in=["D", "CE", "G", "AB"])
elif self.value() in self.__handles: elif self.value() in self.__handles:
return queryset.filter(status=self.value()) return queryset.filter(status=self.value())
class SubmissionResultFilter(admin.SimpleListFilter): class SubmissionResultFilter(admin.SimpleListFilter):
parameter_name = title = 'result' parameter_name = title = "result"
__lookups = (('None', _('None')), ('BAD', _('Unaccepted'))) + Submission.RESULT __lookups = (("None", _("None")), ("BAD", _("Unaccepted"))) + Submission.RESULT
__handles = set(map(itemgetter(0), Submission.RESULT)) __handles = set(map(itemgetter(0), Submission.RESULT))
def lookups(self, request, model_admin): def lookups(self, request, model_admin):
return self.__lookups return self.__lookups
def queryset(self, request, queryset): def queryset(self, request, queryset):
if self.value() == 'None': if self.value() == "None":
return queryset.filter(result=None) return queryset.filter(result=None)
elif self.value() == 'BAD': elif self.value() == "BAD":
return queryset.exclude(result='AC') return queryset.exclude(result="AC")
elif self.value() in self.__handles: elif self.value() in self.__handles:
return queryset.filter(result=self.value()) return queryset.filter(result=self.value())
class SubmissionTestCaseInline(admin.TabularInline): class SubmissionTestCaseInline(admin.TabularInline):
fields = ('case', 'batch', 'status', 'time', 'memory', 'points', 'total') fields = ("case", "batch", "status", "time", "memory", "points", "total")
readonly_fields = ('case', 'batch', 'total') readonly_fields = ("case", "batch", "total")
model = SubmissionTestCase model = SubmissionTestCase
can_delete = False can_delete = False
max_num = 0 max_num = 0
class ContestSubmissionInline(admin.StackedInline): class ContestSubmissionInline(admin.StackedInline):
fields = ('problem', 'participation', 'points') fields = ("problem", "participation", "points")
model = ContestSubmission model = ContestSubmission
def get_formset(self, request, obj=None, **kwargs): def get_formset(self, request, obj=None, **kwargs):
kwargs['formfield_callback'] = partial(self.formfield_for_dbfield, request=request, obj=obj) kwargs["formfield_callback"] = partial(
self.formfield_for_dbfield, request=request, obj=obj
)
return super(ContestSubmissionInline, self).get_formset(request, obj, **kwargs) return super(ContestSubmissionInline, self).get_formset(request, obj, **kwargs)
def formfield_for_dbfield(self, db_field, **kwargs): def formfield_for_dbfield(self, db_field, **kwargs):
submission = kwargs.pop('obj', None) submission = kwargs.pop("obj", None)
label = None label = None
if submission: if submission:
if db_field.name == 'participation': if db_field.name == "participation":
kwargs['queryset'] = ContestParticipation.objects.filter(user=submission.user, kwargs["queryset"] = ContestParticipation.objects.filter(
contest__problems=submission.problem) \ user=submission.user, contest__problems=submission.problem
.only('id', 'contest__name') ).only("id", "contest__name")
def label(obj): def label(obj):
return obj.contest.name return obj.contest.name
elif db_field.name == 'problem':
kwargs['queryset'] = ContestProblem.objects.filter(problem=submission.problem) \ elif db_field.name == "problem":
.only('id', 'problem__name', 'contest__name') kwargs["queryset"] = ContestProblem.objects.filter(
problem=submission.problem
).only("id", "problem__name", "contest__name")
def label(obj): def label(obj):
return pgettext('contest problem', '%(problem)s in %(contest)s') % { return pgettext("contest problem", "%(problem)s in %(contest)s") % {
'problem': obj.problem.name, 'contest': obj.contest.name, "problem": obj.problem.name,
"contest": obj.contest.name,
} }
field = super(ContestSubmissionInline, self).formfield_for_dbfield(db_field, **kwargs)
field = super(ContestSubmissionInline, self).formfield_for_dbfield(
db_field, **kwargs
)
if label is not None: if label is not None:
field.label_from_instance = label field.label_from_instance = label
return field return field
class SubmissionSourceInline(admin.StackedInline): class SubmissionSourceInline(admin.StackedInline):
fields = ('source',) fields = ("source",)
model = SubmissionSource model = SubmissionSource
can_delete = False can_delete = False
extra = 0 extra = 0
def get_formset(self, request, obj=None, **kwargs): def get_formset(self, request, obj=None, **kwargs):
kwargs.setdefault('widgets', {})['source'] = AceWidget(mode=obj and obj.language.ace, kwargs.setdefault("widgets", {})["source"] = AceWidget(
theme=request.profile.ace_theme) mode=obj and obj.language.ace, theme=request.profile.ace_theme
)
return super().get_formset(request, obj, **kwargs) return super().get_formset(request, obj, **kwargs)
class SubmissionAdmin(admin.ModelAdmin): class SubmissionAdmin(admin.ModelAdmin):
readonly_fields = ('user', 'problem', 'date', 'judged_date') readonly_fields = ("user", "problem", "date", "judged_date")
fields = ('user', 'problem', 'date', 'judged_date', 'time', 'memory', 'points', 'language', 'status', 'result', fields = (
'case_points', 'case_total', 'judged_on', 'error') "user",
actions = ('judge', 'recalculate_score') "problem",
list_display = ('id', 'problem_code', 'problem_name', 'user_column', 'execution_time', 'pretty_memory', "date",
'points', 'language_column', 'status', 'result', 'judge_column') "judged_date",
list_filter = ('language', SubmissionStatusFilter, SubmissionResultFilter) "time",
search_fields = ('problem__code', 'problem__name', 'user__user__username') "memory",
"points",
"language",
"status",
"result",
"case_points",
"case_total",
"judged_on",
"error",
)
actions = ("judge", "recalculate_score")
list_display = (
"id",
"problem_code",
"problem_name",
"user_column",
"execution_time",
"pretty_memory",
"points",
"language_column",
"status",
"result",
"judge_column",
)
list_filter = ("language", SubmissionStatusFilter, SubmissionResultFilter)
search_fields = ("problem__code", "problem__name", "user__user__username")
actions_on_top = True actions_on_top = True
actions_on_bottom = True actions_on_bottom = True
inlines = [SubmissionSourceInline, SubmissionTestCaseInline, ContestSubmissionInline] inlines = [
SubmissionSourceInline,
SubmissionTestCaseInline,
ContestSubmissionInline,
]
def get_queryset(self, request): def get_queryset(self, request):
queryset = Submission.objects.select_related('problem', 'user__user', 'language').only( queryset = Submission.objects.select_related(
'problem__code', 'problem__name', 'user__user__username', 'language__name', "problem", "user__user", "language"
'time', 'memory', 'points', 'status', 'result', ).only(
"problem__code",
"problem__name",
"user__user__username",
"language__name",
"time",
"memory",
"points",
"status",
"result",
) )
use_straight_join(queryset) use_straight_join(queryset)
if not request.user.has_perm('judge.edit_all_problem'): if not request.user.has_perm("judge.edit_all_problem"):
id = request.profile.id id = request.profile.id
queryset = queryset.filter(Q(problem__authors__id=id) | Q(problem__curators__id=id)).distinct() queryset = queryset.filter(
Q(problem__authors__id=id) | Q(problem__curators__id=id)
).distinct()
return queryset return queryset
def has_add_permission(self, request): def has_add_permission(self, request):
return False return False
def has_change_permission(self, request, obj=None):
if not request.user.has_perm('judge.edit_own_problem'):
return False
if request.user.has_perm('judge.edit_all_problem') or obj is None:
return True
return obj.problem.is_editor(request.profile)
def lookup_allowed(self, key, value): def lookup_allowed(self, key, value):
return super(SubmissionAdmin, self).lookup_allowed(key, value) or key in ('problem__code',) return super(SubmissionAdmin, self).lookup_allowed(key, value) or key in (
"problem__code",
)
def save_model(self, request, obj, form, change):
super().save_model(request, obj, form, change)
if "case_points" in form.changed_data or "case_total" in form.changed_data:
obj.update_contest()
def judge(self, request, queryset): def judge(self, request, queryset):
if not request.user.has_perm('judge.rejudge_submission') or not request.user.has_perm('judge.edit_own_problem'): if not request.user.has_perm(
self.message_user(request, gettext('You do not have the permission to rejudge submissions.'), "judge.rejudge_submission"
level=messages.ERROR) ) or not request.user.has_perm("judge.edit_own_problem"):
self.message_user(
request,
gettext("You do not have the permission to rejudge submissions."),
level=messages.ERROR,
)
return return
queryset = queryset.order_by('id') queryset = queryset.order_by("id")
if not request.user.has_perm('judge.rejudge_submission_lot') and \ if (
queryset.count() > settings.DMOJ_SUBMISSIONS_REJUDGE_LIMIT: not request.user.has_perm("judge.rejudge_submission_lot")
self.message_user(request, gettext('You do not have the permission to rejudge THAT many submissions.'), and queryset.count() > settings.DMOJ_SUBMISSIONS_REJUDGE_LIMIT
level=messages.ERROR) ):
self.message_user(
request,
gettext(
"You do not have the permission to rejudge THAT many submissions."
),
level=messages.ERROR,
)
return return
if not request.user.has_perm('judge.edit_all_problem'): if not request.user.has_perm("judge.edit_all_problem"):
id = request.profile.id id = request.profile.id
queryset = queryset.filter(Q(problem__authors__id=id) | Q(problem__curators__id=id)) queryset = queryset.filter(
Q(problem__authors__id=id) | Q(problem__curators__id=id)
)
judged = len(queryset) judged = len(queryset)
for model in queryset: for model in queryset:
model.judge(rejudge=True, batch_rejudge=True) model.judge(rejudge=True, batch_rejudge=True)
self.message_user(request, ungettext('%d submission was successfully scheduled for rejudging.', self.message_user(
'%d submissions were successfully scheduled for rejudging.', request,
judged) % judged) ungettext(
judge.short_description = _('Rejudge the selected submissions') "%d submission was successfully scheduled for rejudging.",
"%d submissions were successfully scheduled for rejudging.",
judged,
)
% judged,
)
judge.short_description = _("Rejudge the selected submissions")
def recalculate_score(self, request, queryset): def recalculate_score(self, request, queryset):
if not request.user.has_perm('judge.rejudge_submission'): if not request.user.has_perm("judge.rejudge_submission"):
self.message_user(request, gettext('You do not have the permission to rejudge submissions.'), self.message_user(
level=messages.ERROR) request,
gettext("You do not have the permission to rejudge submissions."),
level=messages.ERROR,
)
return return
submissions = list(queryset.defer(None).select_related(None).select_related('problem') submissions = list(
.only('points', 'case_points', 'case_total', 'problem__partial', 'problem__points')) queryset.defer(None)
.select_related(None)
.select_related("problem")
.only(
"points",
"case_points",
"case_total",
"problem__partial",
"problem__points",
)
)
for submission in submissions: for submission in submissions:
submission.points = round(submission.case_points / submission.case_total * submission.problem.points submission.points = round(
if submission.case_total else 0, 1) submission.case_points
if not submission.problem.partial and submission.points < submission.problem.points: / submission.case_total
* submission.problem.points
if submission.case_total
else 0,
1,
)
if (
not submission.problem.partial
and submission.points < submission.problem.points
):
submission.points = 0 submission.points = 0
submission.save() submission.save()
submission.update_contest() submission.update_contest()
for profile in Profile.objects.filter(id__in=queryset.values_list('user_id', flat=True).distinct()): for profile in Profile.objects.filter(
id__in=queryset.values_list("user_id", flat=True).distinct()
):
profile.calculate_points() profile.calculate_points()
cache.delete('user_complete:%d' % profile.id) cache.delete("user_complete:%d" % profile.id)
cache.delete('user_attempted:%d' % profile.id) cache.delete("user_attempted:%d" % profile.id)
for participation in ContestParticipation.objects.filter( for participation in ContestParticipation.objects.filter(
id__in=queryset.values_list('contest__participation_id')).prefetch_related('contest'): id__in=queryset.values_list("contest__participation_id")
).prefetch_related("contest"):
participation.recompute_results() participation.recompute_results()
self.message_user(request, ungettext('%d submission were successfully rescored.', self.message_user(
'%d submissions were successfully rescored.', request,
len(submissions)) % len(submissions)) ungettext(
recalculate_score.short_description = _('Rescore the selected submissions') "%d submission were successfully rescored.",
"%d submissions were successfully rescored.",
len(submissions),
)
% len(submissions),
)
recalculate_score.short_description = _("Rescore the selected submissions")
def problem_code(self, obj): def problem_code(self, obj):
return obj.problem.code return obj.problem.code
problem_code.short_description = _('Problem code')
problem_code.admin_order_field = 'problem__code' problem_code.short_description = _("Problem code")
problem_code.admin_order_field = "problem__code"
def problem_name(self, obj): def problem_name(self, obj):
return obj.problem.name return obj.problem.name
problem_name.short_description = _('Problem name')
problem_name.admin_order_field = 'problem__name' problem_name.short_description = _("Problem name")
problem_name.admin_order_field = "problem__name"
def user_column(self, obj): def user_column(self, obj):
return obj.user.user.username return obj.user.user.username
user_column.admin_order_field = 'user__user__username'
user_column.short_description = _('User') user_column.admin_order_field = "user__user__username"
user_column.short_description = _("User")
def execution_time(self, obj): def execution_time(self, obj):
return round(obj.time, 2) if obj.time is not None else 'None' return round(obj.time, 2) if obj.time is not None else "None"
execution_time.short_description = _('Time')
execution_time.admin_order_field = 'time' execution_time.short_description = _("Time")
execution_time.admin_order_field = "time"
def pretty_memory(self, obj): def pretty_memory(self, obj):
memory = obj.memory memory = obj.memory
if memory is None: if memory is None:
return gettext('None') return gettext("None")
if memory < 1000: if memory < 1000:
return gettext('%d KB') % memory return gettext("%d KB") % memory
else: else:
return gettext('%.2f MB') % (memory / 1024) return gettext("%.2f MB") % (memory / 1024)
pretty_memory.admin_order_field = 'memory'
pretty_memory.short_description = _('Memory') pretty_memory.admin_order_field = "memory"
pretty_memory.short_description = _("Memory")
def language_column(self, obj): def language_column(self, obj):
return obj.language.name return obj.language.name
language_column.admin_order_field = 'language__name'
language_column.short_description = _('Language') language_column.admin_order_field = "language__name"
language_column.short_description = _("Language")
def judge_column(self, obj): def judge_column(self, obj):
return format_html('<input type="button" value="Rejudge" onclick="location.href=\'{}/judge/\'" />', obj.id) return format_html(
judge_column.short_description = '' '<input type="button" value="Rejudge" onclick="location.href=\'{}/judge/\'" />',
obj.id,
)
judge_column.short_description = ""
def get_urls(self): def get_urls(self):
return [ return [
url(r'^(\d+)/judge/$', self.judge_view, name='judge_submission_rejudge'), url(r"^(\d+)/judge/$", self.judge_view, name="judge_submission_rejudge"),
] + super(SubmissionAdmin, self).get_urls() ] + super(SubmissionAdmin, self).get_urls()
def judge_view(self, request, id): def judge_view(self, request, id):
if not request.user.has_perm('judge.rejudge_submission') or not request.user.has_perm('judge.edit_own_problem'): if not request.user.has_perm(
"judge.rejudge_submission"
) or not request.user.has_perm("judge.edit_own_problem"):
raise PermissionDenied() raise PermissionDenied()
submission = get_object_or_404(Submission, id=id) submission = get_object_or_404(Submission, id=id)
if not request.user.has_perm('judge.edit_all_problem') and \ if not request.user.has_perm(
not submission.problem.is_editor(request.profile): "judge.edit_all_problem"
) and not submission.problem.is_editor(request.profile):
raise PermissionDenied() raise PermissionDenied()
submission.judge(rejudge=True) submission.judge(rejudge=True)
return HttpResponseRedirect(request.META.get('HTTP_REFERER', '/')) return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/"))

View file

@ -8,45 +8,59 @@ from judge.widgets import AdminHeavySelect2MultipleWidget
class ProblemGroupForm(ModelForm): class ProblemGroupForm(ModelForm):
problems = ModelMultipleChoiceField( problems = ModelMultipleChoiceField(
label=_('Included problems'), label=_("Included problems"),
queryset=Problem.objects.all(), queryset=Problem.objects.all(),
required=False, required=False,
help_text=_('These problems are included in this group of problems'), help_text=_("These problems are included in this group of problems"),
widget=AdminHeavySelect2MultipleWidget(data_view='problem_select2')) widget=AdminHeavySelect2MultipleWidget(data_view="problem_select2"),
)
class ProblemGroupAdmin(admin.ModelAdmin): class ProblemGroupAdmin(admin.ModelAdmin):
fields = ('name', 'full_name', 'problems') fields = ("name", "full_name", "problems")
form = ProblemGroupForm form = ProblemGroupForm
def save_model(self, request, obj, form, change): def save_model(self, request, obj, form, change):
super(ProblemGroupAdmin, self).save_model(request, obj, form, change) super(ProblemGroupAdmin, self).save_model(request, obj, form, change)
obj.problem_set.set(form.cleaned_data['problems']) obj.problem_set.set(form.cleaned_data["problems"])
obj.save() obj.save()
def get_form(self, request, obj=None, **kwargs): def get_form(self, request, obj=None, **kwargs):
self.form.base_fields['problems'].initial = [o.pk for o in obj.problem_set.all()] if obj else [] self.form.base_fields["problems"].initial = (
[o.pk for o in obj.problem_set.all()] if obj else []
)
return super(ProblemGroupAdmin, self).get_form(request, obj, **kwargs) return super(ProblemGroupAdmin, self).get_form(request, obj, **kwargs)
class ProblemTypeForm(ModelForm): class ProblemTypeForm(ModelForm):
problems = ModelMultipleChoiceField( problems = ModelMultipleChoiceField(
label=_('Included problems'), label=_("Included problems"),
queryset=Problem.objects.all(), queryset=Problem.objects.all(),
required=False, required=False,
help_text=_('These problems are included in this type of problems'), help_text=_("These problems are included in this type of problems"),
widget=AdminHeavySelect2MultipleWidget(data_view='problem_select2')) widget=AdminHeavySelect2MultipleWidget(data_view="problem_select2"),
)
class ProblemTypeAdmin(admin.ModelAdmin): class ProblemTypeAdmin(admin.ModelAdmin):
fields = ('name', 'full_name', 'problems') fields = ("name", "full_name", "problems")
form = ProblemTypeForm form = ProblemTypeForm
def save_model(self, request, obj, form, change): def save_model(self, request, obj, form, change):
super(ProblemTypeAdmin, self).save_model(request, obj, form, change) super(ProblemTypeAdmin, self).save_model(request, obj, form, change)
obj.problem_set.set(form.cleaned_data['problems']) obj.problem_set.set(form.cleaned_data["problems"])
obj.save() obj.save()
def get_form(self, request, obj=None, **kwargs): def get_form(self, request, obj=None, **kwargs):
self.form.base_fields['problems'].initial = [o.pk for o in obj.problem_set.all()] if obj else [] self.form.base_fields["problems"].initial = (
[o.pk for o in obj.problem_set.all()] if obj else []
)
return super(ProblemTypeAdmin, self).get_form(request, obj, **kwargs) return super(ProblemTypeAdmin, self).get_form(request, obj, **kwargs)
class OfficialContestCategoryAdmin(admin.ModelAdmin):
fields = ("name",)
class OfficialContestLocationAdmin(admin.ModelAdmin):
fields = ("name",)

View file

@ -4,36 +4,56 @@ from django.forms import ModelForm
from django.urls import reverse_lazy from django.urls import reverse_lazy
from judge.models import TicketMessage from judge.models import TicketMessage
from judge.widgets import AdminHeavySelect2MultipleWidget, AdminHeavySelect2Widget, HeavyPreviewAdminPageDownWidget from judge.widgets import (
AdminHeavySelect2MultipleWidget,
AdminHeavySelect2Widget,
HeavyPreviewAdminPageDownWidget,
)
class TicketMessageForm(ModelForm): class TicketMessageForm(ModelForm):
class Meta: class Meta:
widgets = { widgets = {
'user': AdminHeavySelect2Widget(data_view='profile_select2', attrs={'style': 'width: 100%'}), "user": AdminHeavySelect2Widget(
data_view="profile_select2", attrs={"style": "width: 100%"}
),
} }
if HeavyPreviewAdminPageDownWidget is not None: if HeavyPreviewAdminPageDownWidget is not None:
widgets['body'] = HeavyPreviewAdminPageDownWidget(preview=reverse_lazy('ticket_preview')) widgets["body"] = HeavyPreviewAdminPageDownWidget(
preview=reverse_lazy("ticket_preview")
)
class TicketMessageInline(StackedInline): class TicketMessageInline(StackedInline):
model = TicketMessage model = TicketMessage
form = TicketMessageForm form = TicketMessageForm
fields = ('user', 'body') fields = ("user", "body")
class TicketForm(ModelForm): class TicketForm(ModelForm):
class Meta: class Meta:
widgets = { widgets = {
'user': AdminHeavySelect2Widget(data_view='profile_select2', attrs={'style': 'width: 100%'}), "user": AdminHeavySelect2Widget(
'assignees': AdminHeavySelect2MultipleWidget(data_view='profile_select2', attrs={'style': 'width: 100%'}), data_view="profile_select2", attrs={"style": "width: 100%"}
),
"assignees": AdminHeavySelect2MultipleWidget(
data_view="profile_select2", attrs={"style": "width: 100%"}
),
} }
class TicketAdmin(ModelAdmin): class TicketAdmin(ModelAdmin):
fields = ('title', 'time', 'user', 'assignees', 'content_type', 'object_id', 'notes') fields = (
readonly_fields = ('time',) "title",
list_display = ('title', 'user', 'time', 'linked_item') "time",
"user",
"assignees",
"content_type",
"object_id",
"notes",
)
readonly_fields = ("time",)
list_display = ("title", "user", "time", "linked_item")
inlines = [TicketMessageInline] inlines = [TicketMessageInline]
form = TicketForm form = TicketForm
date_hierarchy = 'time' date_hierarchy = "time"

67
judge/admin/volunteer.py Normal file
View file

@ -0,0 +1,67 @@
from operator import attrgetter
from django.contrib import admin
from django.utils.html import format_html
from django.urls import reverse
from django.utils.translation import gettext, gettext_lazy as _, ungettext
from django.forms import ModelForm
from judge.models import VolunteerProblemVote
from judge.widgets import AdminSelect2MultipleWidget
class VolunteerProblemVoteForm(ModelForm):
class Meta:
widgets = {
"types": AdminSelect2MultipleWidget,
}
class VolunteerProblemVoteAdmin(admin.ModelAdmin):
form = VolunteerProblemVoteForm
fields = (
"voter",
"problem_link",
"time",
"thinking_points",
"knowledge_points",
"types",
"feedback",
)
readonly_fields = ("time", "problem_link", "voter")
list_display = (
"voter",
"problem_link",
"thinking_points",
"knowledge_points",
"show_types",
"feedback",
)
search_fields = (
"voter__user__username",
"problem__code",
"problem__name",
)
date_hierarchy = "time"
def problem_link(self, obj):
if self.request.user.is_superuser:
url = reverse("admin:judge_problem_change", args=(obj.problem.id,))
else:
url = reverse("problem_detail", args=(obj.problem.code,))
return format_html(f"<a href='{url}'>{obj.problem}</a>")
problem_link.short_description = _("Problem")
problem_link.admin_order_field = "problem__code"
def show_types(self, obj):
return ", ".join(map(attrgetter("name"), obj.types.all()))
show_types.short_description = _("Types")
def get_queryset(self, request):
self.request = request
if request.user.is_superuser:
return super().get_queryset(request)
queryset = VolunteerProblemVote.objects.prefetch_related("voter")
return queryset.filter(voter=request.profile).distinct()

View file

@ -4,15 +4,15 @@ from django.utils.translation import gettext_lazy
class JudgeAppConfig(AppConfig): class JudgeAppConfig(AppConfig):
name = 'judge' name = "judge"
verbose_name = gettext_lazy('Online Judge') verbose_name = gettext_lazy("Online Judge")
def ready(self): def ready(self):
# WARNING: AS THIS IS NOT A FUNCTIONAL PROGRAMMING LANGUAGE, # WARNING: AS THIS IS NOT A FUNCTIONAL PROGRAMMING LANGUAGE,
# OPERATIONS MAY HAVE SIDE EFFECTS. # OPERATIONS MAY HAVE SIDE EFFECTS.
# DO NOT REMOVE THINKING THE IMPORT IS UNUSED. # DO NOT REMOVE THINKING THE IMPORT IS UNUSED.
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
from . import signals, jinja2 # noqa: F401, imported for side effects from . import models, signals, jinja2 # noqa: F401, imported for side effects
from django.contrib.flatpages.models import FlatPage from django.contrib.flatpages.models import FlatPage
from django.contrib.flatpages.admin import FlatPageAdmin from django.contrib.flatpages.admin import FlatPageAdmin

48
judge/authentication.py Normal file
View file

@ -0,0 +1,48 @@
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth.models import User
from django.contrib.auth.forms import PasswordChangeForm
from django.contrib.auth.views import PasswordChangeView
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
class CustomModelBackend(ModelBackend):
def authenticate(self, request, username=None, password=None, **kwargs):
try:
# Check if the username is an email
user = User.objects.get(username=username)
except User.DoesNotExist:
# If the username is not an email, try authenticating with the username field
user = User.objects.filter(email=username).first()
if user and user.check_password(password):
return user
class CustomPasswordChangeForm(PasswordChangeForm):
def __init__(self, *args, **kwargs):
super(CustomPasswordChangeForm, self).__init__(*args, **kwargs)
if not self.user.has_usable_password():
self.fields.pop("old_password")
def clean_old_password(self):
if "old_password" not in self.cleaned_data:
return
return super(CustomPasswordChangeForm, self).clean_old_password()
def clean(self):
cleaned_data = super(CustomPasswordChangeForm, self).clean()
if "old_password" not in self.cleaned_data and not self.errors:
cleaned_data["old_password"] = ""
return cleaned_data
class CustomPasswordChangeView(PasswordChangeView):
form_class = CustomPasswordChangeForm
success_url = reverse_lazy("password_change_done")
template_name = "registration/password_change_form.html"
def get_form_kwargs(self):
kwargs = super(CustomPasswordChangeView, self).get_form_kwargs()
kwargs["user"] = self.request.user
return kwargs

View file

@ -8,9 +8,9 @@ from netaddr import IPGlob, IPSet
from judge.utils.unicode import utf8text from judge.utils.unicode import utf8text
logger = logging.getLogger('judge.bridge') logger = logging.getLogger("judge.bridge")
size_pack = struct.Struct('!I') size_pack = struct.Struct("!I")
assert size_pack.size == 4 assert size_pack.size == 4
MAX_ALLOWED_PACKET_SIZE = 8 * 1024 * 1024 MAX_ALLOWED_PACKET_SIZE = 8 * 1024 * 1024
@ -20,7 +20,7 @@ def proxy_list(human_readable):
globs = [] globs = []
addrs = [] addrs = []
for item in human_readable: for item in human_readable:
if '*' in item or '-' in item: if "*" in item or "-" in item:
globs.append(IPGlob(item)) globs.append(IPGlob(item))
else: else:
addrs.append(item) addrs.append(item)
@ -43,7 +43,7 @@ class RequestHandlerMeta(type):
try: try:
handler.handle() handler.handle()
except BaseException: except BaseException:
logger.exception('Error in base packet handling') logger.exception("Error in base packet handling")
raise raise
finally: finally:
handler.on_disconnect() handler.on_disconnect()
@ -70,8 +70,12 @@ class ZlibPacketHandler(metaclass=RequestHandlerMeta):
def read_sized_packet(self, size, initial=None): def read_sized_packet(self, size, initial=None):
if size > MAX_ALLOWED_PACKET_SIZE: if size > MAX_ALLOWED_PACKET_SIZE:
logger.log(logging.WARNING if self._got_packet else logging.INFO, logger.log(
'Disconnecting client due to too-large message size (%d bytes): %s', size, self.client_address) logging.WARNING if self._got_packet else logging.INFO,
"Disconnecting client due to too-large message size (%d bytes): %s",
size,
self.client_address,
)
raise Disconnect() raise Disconnect()
buffer = [] buffer = []
@ -86,7 +90,7 @@ class ZlibPacketHandler(metaclass=RequestHandlerMeta):
data = self.request.recv(remainder) data = self.request.recv(remainder)
remainder -= len(data) remainder -= len(data)
buffer.append(data) buffer.append(data)
self._on_packet(b''.join(buffer)) self._on_packet(b"".join(buffer))
def parse_proxy_protocol(self, line): def parse_proxy_protocol(self, line):
words = line.split() words = line.split()
@ -94,18 +98,18 @@ class ZlibPacketHandler(metaclass=RequestHandlerMeta):
if len(words) < 2: if len(words) < 2:
raise Disconnect() raise Disconnect()
if words[1] == b'TCP4': if words[1] == b"TCP4":
if len(words) != 6: if len(words) != 6:
raise Disconnect() raise Disconnect()
self.client_address = (utf8text(words[2]), utf8text(words[4])) self.client_address = (utf8text(words[2]), utf8text(words[4]))
self.server_address = (utf8text(words[3]), utf8text(words[5])) self.server_address = (utf8text(words[3]), utf8text(words[5]))
elif words[1] == b'TCP6': elif words[1] == b"TCP6":
self.client_address = (utf8text(words[2]), utf8text(words[4]), 0, 0) self.client_address = (utf8text(words[2]), utf8text(words[4]), 0, 0)
self.server_address = (utf8text(words[3]), utf8text(words[5]), 0, 0) self.server_address = (utf8text(words[3]), utf8text(words[5]), 0, 0)
elif words[1] != b'UNKNOWN': elif words[1] != b"UNKNOWN":
raise Disconnect() raise Disconnect()
def read_size(self, buffer=b''): def read_size(self, buffer=b""):
while len(buffer) < size_pack.size: while len(buffer) < size_pack.size:
recv = self.request.recv(size_pack.size - len(buffer)) recv = self.request.recv(size_pack.size - len(buffer))
if not recv: if not recv:
@ -113,9 +117,9 @@ class ZlibPacketHandler(metaclass=RequestHandlerMeta):
buffer += recv buffer += recv
return size_pack.unpack(buffer)[0] return size_pack.unpack(buffer)[0]
def read_proxy_header(self, buffer=b''): def read_proxy_header(self, buffer=b""):
# Max line length for PROXY protocol is 107, and we received 4 already. # Max line length for PROXY protocol is 107, and we received 4 already.
while b'\r\n' not in buffer: while b"\r\n" not in buffer:
if len(buffer) > 107: if len(buffer) > 107:
raise Disconnect() raise Disconnect()
data = self.request.recv(107) data = self.request.recv(107)
@ -125,7 +129,7 @@ class ZlibPacketHandler(metaclass=RequestHandlerMeta):
return buffer return buffer
def _on_packet(self, data): def _on_packet(self, data):
decompressed = zlib.decompress(data).decode('utf-8') decompressed = zlib.decompress(data).decode("utf-8")
self._got_packet = True self._got_packet = True
self.on_packet(decompressed) self.on_packet(decompressed)
@ -141,12 +145,17 @@ class ZlibPacketHandler(metaclass=RequestHandlerMeta):
def on_timeout(self): def on_timeout(self):
pass pass
def on_cleanup(self):
pass
def handle(self): def handle(self):
try: try:
tag = self.read_size() tag = self.read_size()
self._initial_tag = size_pack.pack(tag) self._initial_tag = size_pack.pack(tag)
if self.client_address[0] in self.proxies and self._initial_tag == b'PROX': if self.client_address[0] in self.proxies and self._initial_tag == b"PROX":
proxy, _, remainder = self.read_proxy_header(self._initial_tag).partition(b'\r\n') proxy, _, remainder = self.read_proxy_header(
self._initial_tag
).partition(b"\r\n")
self.parse_proxy_protocol(proxy) self.parse_proxy_protocol(proxy)
while remainder: while remainder:
@ -171,25 +180,38 @@ class ZlibPacketHandler(metaclass=RequestHandlerMeta):
return return
except zlib.error: except zlib.error:
if self._got_packet: if self._got_packet:
logger.warning('Encountered zlib error during packet handling, disconnecting client: %s', logger.warning(
self.client_address, exc_info=True) "Encountered zlib error during packet handling, disconnecting client: %s",
self.client_address,
exc_info=True,
)
else: else:
logger.info('Potentially wrong protocol (zlib error): %s: %r', self.client_address, self._initial_tag, logger.info(
exc_info=True) "Potentially wrong protocol (zlib error): %s: %r",
self.client_address,
self._initial_tag,
exc_info=True,
)
except socket.timeout: except socket.timeout:
if self._got_packet: if self._got_packet:
logger.info('Socket timed out: %s', self.client_address) logger.info("Socket timed out: %s", self.client_address)
self.on_timeout() self.on_timeout()
else: else:
logger.info('Potentially wrong protocol: %s: %r', self.client_address, self._initial_tag) logger.info(
"Potentially wrong protocol: %s: %r",
self.client_address,
self._initial_tag,
)
except socket.error as e: except socket.error as e:
# When a gevent socket is shutdown, gevent cancels all waits, causing recv to raise cancel_wait_ex. # When a gevent socket is shutdown, gevent cancels all waits, causing recv to raise cancel_wait_ex.
if e.__class__.__name__ == 'cancel_wait_ex': if e.__class__.__name__ == "cancel_wait_ex":
return return
raise raise
finally:
self.on_cleanup()
def send(self, data): def send(self, data):
compressed = zlib.compress(data.encode('utf-8')) compressed = zlib.compress(data.encode("utf-8"))
self.request.sendall(size_pack.pack(len(compressed)) + compressed) self.request.sendall(size_pack.pack(len(compressed)) + compressed)
def close(self): def close(self):

View file

@ -11,7 +11,7 @@ from judge.bridge.judge_list import JudgeList
from judge.bridge.server import Server from judge.bridge.server import Server
from judge.models import Judge, Submission from judge.models import Judge, Submission
logger = logging.getLogger('judge.bridge') logger = logging.getLogger("judge.bridge")
def reset_judges(): def reset_judges():
@ -20,12 +20,17 @@ def reset_judges():
def judge_daemon(): def judge_daemon():
reset_judges() reset_judges()
Submission.objects.filter(status__in=Submission.IN_PROGRESS_GRADING_STATUS) \ Submission.objects.filter(status__in=Submission.IN_PROGRESS_GRADING_STATUS).update(
.update(status='IE', result='IE', error=None) status="IE", result="IE", error=None
)
judges = JudgeList() judges = JudgeList()
judge_server = Server(settings.BRIDGED_JUDGE_ADDRESS, partial(JudgeHandler, judges=judges)) judge_server = Server(
django_server = Server(settings.BRIDGED_DJANGO_ADDRESS, partial(DjangoHandler, judges=judges)) settings.BRIDGED_JUDGE_ADDRESS, partial(JudgeHandler, judges=judges)
)
django_server = Server(
settings.BRIDGED_DJANGO_ADDRESS, partial(DjangoHandler, judges=judges)
)
threading.Thread(target=django_server.serve_forever).start() threading.Thread(target=django_server.serve_forever).start()
threading.Thread(target=judge_server.serve_forever).start() threading.Thread(target=judge_server.serve_forever).start()
@ -33,7 +38,7 @@ def judge_daemon():
stop = threading.Event() stop = threading.Event()
def signal_handler(signum, _): def signal_handler(signum, _):
logger.info('Exiting due to %s', signal.Signals(signum).name) logger.info("Exiting due to %s", signal.Signals(signum).name)
stop.set() stop.set()
signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGINT, signal_handler)

View file

@ -2,10 +2,12 @@ import json
import logging import logging
import struct import struct
from django import db
from judge.bridge.base_handler import Disconnect, ZlibPacketHandler from judge.bridge.base_handler import Disconnect, ZlibPacketHandler
logger = logging.getLogger('judge.bridge') logger = logging.getLogger("judge.bridge")
size_pack = struct.Struct('!I') size_pack = struct.Struct("!I")
class DjangoHandler(ZlibPacketHandler): class DjangoHandler(ZlibPacketHandler):
@ -13,47 +15,52 @@ class DjangoHandler(ZlibPacketHandler):
super().__init__(request, client_address, server) super().__init__(request, client_address, server)
self.handlers = { self.handlers = {
'submission-request': self.on_submission, "submission-request": self.on_submission,
'terminate-submission': self.on_termination, "terminate-submission": self.on_termination,
'disconnect-judge': self.on_disconnect_request, "disconnect-judge": self.on_disconnect_request,
} }
self.judges = judges self.judges = judges
def send(self, data): def send(self, data):
super().send(json.dumps(data, separators=(',', ':'))) super().send(json.dumps(data, separators=(",", ":")))
def on_packet(self, packet): def on_packet(self, packet):
packet = json.loads(packet) packet = json.loads(packet)
try: try:
result = self.handlers.get(packet.get('name', None), self.on_malformed)(packet) result = self.handlers.get(packet.get("name", None), self.on_malformed)(
packet
)
except Exception: except Exception:
logger.exception('Error in packet handling (Django-facing)') logger.exception("Error in packet handling (Django-facing)")
result = {'name': 'bad-request'} result = {"name": "bad-request"}
self.send(result) self.send(result)
raise Disconnect() raise Disconnect()
def on_submission(self, data): def on_submission(self, data):
id = data['submission-id'] id = data["submission-id"]
problem = data['problem-id'] problem = data["problem-id"]
language = data['language'] language = data["language"]
source = data['source'] source = data["source"]
judge_id = data['judge-id'] judge_id = data["judge-id"]
priority = data['priority'] priority = data["priority"]
if not self.judges.check_priority(priority): if not self.judges.check_priority(priority):
return {'name': 'bad-request'} return {"name": "bad-request"}
self.judges.judge(id, problem, language, source, judge_id, priority) self.judges.judge(id, problem, language, source, judge_id, priority)
return {'name': 'submission-received', 'submission-id': id} return {"name": "submission-received", "submission-id": id}
def on_termination(self, data): def on_termination(self, data):
return {'name': 'submission-received', 'judge-aborted': self.judges.abort(data['submission-id'])} return {
"name": "submission-received",
"judge-aborted": self.judges.abort(data["submission-id"]),
}
def on_disconnect_request(self, data): def on_disconnect_request(self, data):
judge_id = data['judge-id'] judge_id = data["judge-id"]
force = data['force'] force = data["force"]
self.judges.disconnect(judge_id, force=force) self.judges.disconnect(judge_id, force=force)
def on_malformed(self, packet): def on_malformed(self, packet):
logger.error('Malformed packet: %s', packet) logger.error("Malformed packet: %s", packet)
def on_close(self): def on_cleanup(self):
self._to_kill = False db.connection.close()

View file

@ -4,7 +4,7 @@ import struct
import time import time
import zlib import zlib
size_pack = struct.Struct('!I') size_pack = struct.Struct("!I")
def open_connection(): def open_connection():
@ -13,69 +13,70 @@ def open_connection():
def zlibify(data): def zlibify(data):
data = zlib.compress(data.encode('utf-8')) data = zlib.compress(data.encode("utf-8"))
return size_pack.pack(len(data)) + data return size_pack.pack(len(data)) + data
def dezlibify(data, skip_head=True): def dezlibify(data, skip_head=True):
if skip_head: if skip_head:
data = data[size_pack.size :] data = data[size_pack.size :]
return zlib.decompress(data).decode('utf-8') return zlib.decompress(data).decode("utf-8")
def main(): def main():
global host, port global host, port
import argparse import argparse
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('-l', '--host', default='localhost') parser.add_argument("-l", "--host", default="localhost")
parser.add_argument('-p', '--port', default=9999, type=int) parser.add_argument("-p", "--port", default=9999, type=int)
args = parser.parse_args() args = parser.parse_args()
host, port = args.host, args.port host, port = args.host, args.port
print('Opening idle connection:', end=' ') print("Opening idle connection:", end=" ")
s1 = open_connection() s1 = open_connection()
print('Success') print("Success")
print('Opening hello world connection:', end=' ') print("Opening hello world connection:", end=" ")
s2 = open_connection() s2 = open_connection()
print('Success') print("Success")
print('Sending Hello, World!', end=' ') print("Sending Hello, World!", end=" ")
s2.sendall(zlibify('Hello, World!')) s2.sendall(zlibify("Hello, World!"))
print('Success') print("Success")
print('Testing blank connection:', end=' ') print("Testing blank connection:", end=" ")
s3 = open_connection() s3 = open_connection()
s3.close() s3.close()
print('Success') print("Success")
result = dezlibify(s2.recv(1024)) result = dezlibify(s2.recv(1024))
assert result == 'Hello, World!' assert result == "Hello, World!"
print(result) print(result)
s2.close() s2.close()
print('Large random data test:', end=' ') print("Large random data test:", end=" ")
s4 = open_connection() s4 = open_connection()
data = os.urandom(1000000).decode('iso-8859-1') data = os.urandom(1000000).decode("iso-8859-1")
print('Generated', end=' ') print("Generated", end=" ")
s4.sendall(zlibify(data)) s4.sendall(zlibify(data))
print('Sent', end=' ') print("Sent", end=" ")
result = b'' result = b""
while len(result) < size_pack.size: while len(result) < size_pack.size:
result += s4.recv(1024) result += s4.recv(1024)
size = size_pack.unpack(result[: size_pack.size])[0] size = size_pack.unpack(result[: size_pack.size])[0]
result = result[size_pack.size :] result = result[size_pack.size :]
while len(result) < size: while len(result) < size:
result += s4.recv(1024) result += s4.recv(1024)
print('Received', end=' ') print("Received", end=" ")
assert dezlibify(result, False) == data assert dezlibify(result, False) == data
print('Success') print("Success")
s4.close() s4.close()
print('Test malformed connection:', end=' ') print("Test malformed connection:", end=" ")
s5 = open_connection() s5 = open_connection()
s5.sendall(data[:100000].encode('utf-8')) s5.sendall(data[:100000].encode("utf-8"))
s5.close() s5.close()
print('Success') print("Success")
print('Waiting for timeout to close idle connection:', end=' ') print("Waiting for timeout to close idle connection:", end=" ")
time.sleep(6) time.sleep(6)
print('Done') print("Done")
s1.close() s1.close()
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View file

@ -3,19 +3,22 @@ from judge.bridge.base_handler import ZlibPacketHandler
class EchoPacketHandler(ZlibPacketHandler): class EchoPacketHandler(ZlibPacketHandler):
def on_connect(self): def on_connect(self):
print('New client:', self.client_address) print("New client:", self.client_address)
self.timeout = 5 self.timeout = 5
def on_timeout(self): def on_timeout(self):
print('Inactive client:', self.client_address) print("Inactive client:", self.client_address)
def on_packet(self, data): def on_packet(self, data):
self.timeout = None self.timeout = None
print('Data from %s: %r' % (self.client_address, data[:30] if len(data) > 30 else data)) print(
"Data from %s: %r"
% (self.client_address, data[:30] if len(data) > 30 else data)
)
self.send(data) self.send(data)
def on_disconnect(self): def on_disconnect(self):
print('Closed client:', self.client_address) print("Closed client:", self.client_address)
def main(): def main():
@ -23,9 +26,9 @@ def main():
from judge.bridge.server import Server from judge.bridge.server import Server
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument('-l', '--host', action='append') parser.add_argument("-l", "--host", action="append")
parser.add_argument('-p', '--port', type=int, action='append') parser.add_argument("-p", "--port", type=int, action="append")
parser.add_argument('-P', '--proxy', action='append') parser.add_argument("-P", "--proxy", action="append")
args = parser.parse_args() args = parser.parse_args()
class Handler(EchoPacketHandler): class Handler(EchoPacketHandler):
@ -35,5 +38,5 @@ def main():
server.serve_forever() server.serve_forever()
if __name__ == '__main__': if __name__ == "__main__":
main() main()

File diff suppressed because it is too large Load diff

View file

@ -3,14 +3,16 @@ from collections import namedtuple
from operator import attrgetter from operator import attrgetter
from threading import RLock from threading import RLock
from judge.bridge.utils import VanishedSubmission
try: try:
from llist import dllist from llist import dllist
except ImportError: except ImportError:
from pyllist import dllist from pyllist import dllist
logger = logging.getLogger('judge.bridge') logger = logging.getLogger("judge.bridge")
PriorityMarker = namedtuple('PriorityMarker', 'priority') PriorityMarker = namedtuple("PriorityMarker", "priority")
class JudgeList(object): class JudgeList(object):
@ -18,7 +20,9 @@ class JudgeList(object):
def __init__(self): def __init__(self):
self.queue = dllist() self.queue = dllist()
self.priority = [self.queue.append(PriorityMarker(i)) for i in range(self.priorities)] self.priority = [
self.queue.append(PriorityMarker(i)) for i in range(self.priorities)
]
self.judges = set() self.judges = set()
self.node_map = {} self.node_map = {}
self.submission_map = {} self.submission_map = {}
@ -32,11 +36,21 @@ class JudgeList(object):
id, problem, language, source, judge_id = node.value id, problem, language, source, judge_id = node.value
if judge.can_judge(problem, language, judge_id): if judge.can_judge(problem, language, judge_id):
self.submission_map[id] = judge self.submission_map[id] = judge
logger.info('Dispatched queued submission %d: %s', id, judge.name) logger.info(
"Dispatched queued submission %d: %s", id, judge.name
)
try: try:
judge.submit(id, problem, language, source) judge.submit(id, problem, language, source)
except VanishedSubmission:
pass
except Exception: except Exception:
logger.exception('Failed to dispatch %d (%s, %s) to %s', id, problem, language, judge.name) logger.exception(
"Failed to dispatch %d (%s, %s) to %s",
id,
problem,
language,
judge.name,
)
self.judges.remove(judge) self.judges.remove(judge)
return return
self.queue.remove(node) self.queue.remove(node)
@ -76,14 +90,15 @@ class JudgeList(object):
def on_judge_free(self, judge, submission): def on_judge_free(self, judge, submission):
with self.lock: with self.lock:
logger.info('Judge available after grading %d: %s', submission, judge.name) logger.info("Judge available after grading %d: %s", submission, judge.name)
del self.submission_map[submission] del self.submission_map[submission]
judge._working = False judge._working = False
judge._working_data = {}
self._handle_free_judge(judge) self._handle_free_judge(judge)
def abort(self, submission): def abort(self, submission):
with self.lock: with self.lock:
logger.info('Abort request: %d', submission) logger.info("Abort request: %d", submission)
try: try:
self.submission_map[submission].abort() self.submission_map[submission].abort()
return True return True
@ -108,21 +123,33 @@ class JudgeList(object):
return return
candidates = [ candidates = [
judge for judge in self.judges if not judge.working and judge.can_judge(problem, language, judge_id) judge
for judge in self.judges
if not judge.working and judge.can_judge(problem, language, judge_id)
] ]
if judge_id: if judge_id:
logger.info('Specified judge %s is%savailable', judge_id, ' ' if candidates else ' not ') logger.info(
"Specified judge %s is%savailable",
judge_id,
" " if candidates else " not ",
)
else: else:
logger.info('Free judges: %d', len(candidates)) logger.info("Free judges: %d", len(candidates))
if candidates: if candidates:
# Schedule the submission on the judge reporting least load. # Schedule the submission on the judge reporting least load.
judge = min(candidates, key=attrgetter('load')) judge = min(candidates, key=attrgetter("load"))
logger.info('Dispatched submission %d to: %s', id, judge.name) logger.info("Dispatched submission %d to: %s", id, judge.name)
self.submission_map[id] = judge self.submission_map[id] = judge
try: try:
judge.submit(id, problem, language, source) judge.submit(id, problem, language, source)
except Exception: except Exception:
logger.exception('Failed to dispatch %d (%s, %s) to %s', id, problem, language, judge.name) logger.exception(
"Failed to dispatch %d (%s, %s) to %s",
id,
problem,
language,
judge.name,
)
self.judges.discard(judge) self.judges.discard(judge)
return self.judge(id, problem, language, source, judge_id, priority) return self.judge(id, problem, language, source, judge_id, priority)
else: else:
@ -130,4 +157,4 @@ class JudgeList(object):
(id, problem, language, source, judge_id), (id, problem, language, source, judge_id),
self.priority[priority], self.priority[priority],
) )
logger.info('Queued submission: %d', id) logger.info("Queued submission: %d", id)

View file

@ -12,7 +12,9 @@ class Server:
self._shutdown = threading.Event() self._shutdown = threading.Event()
def serve_forever(self): def serve_forever(self):
threads = [threading.Thread(target=server.serve_forever) for server in self.servers] threads = [
threading.Thread(target=server.serve_forever) for server in self.servers
]
for thread in threads: for thread in threads:
thread.daemon = True thread.daemon = True
thread.start() thread.start()

2
judge/bridge/utils.py Normal file
View file

@ -0,0 +1,2 @@
class VanishedSubmission(Exception):
pass

View file

@ -1,10 +1,117 @@
from django.core.cache import cache from inspect import signature
from django.core.cache import cache, caches
from django.db.models.query import QuerySet
from django.core.handlers.wsgi import WSGIRequest
import hashlib
from judge.logging import log_debug
MAX_NUM_CHAR = 50
NONE_RESULT = "__None__"
def finished_submission(sub): def arg_to_str(arg):
keys = ['user_complete:%d' % sub.user_id, 'user_attempted:%s' % sub.user_id] if hasattr(arg, "id"):
if hasattr(sub, 'contest'): return str(arg.id)
participation = sub.contest.participation if isinstance(arg, list) or isinstance(arg, QuerySet):
keys += ['contest_complete:%d' % participation.id] return hashlib.sha1(str(list(arg)).encode()).hexdigest()[:MAX_NUM_CHAR]
keys += ['contest_attempted:%d' % participation.id] if len(str(arg)) > MAX_NUM_CHAR:
return str(arg)[:MAX_NUM_CHAR]
return str(arg)
def filter_args(args_list):
return [x for x in args_list if not isinstance(x, WSGIRequest)]
l0_cache = caches["l0"] if "l0" in caches else None
def cache_wrapper(prefix, timeout=None, expected_type=None):
def get_key(func, *args, **kwargs):
args_list = list(args)
signature_args = list(signature(func).parameters.keys())
args_list += [kwargs.get(k) for k in signature_args[len(args) :]]
args_list = filter_args(args_list)
args_list = [arg_to_str(i) for i in args_list]
key = prefix + ":" + ":".join(args_list)
key = key.replace(" ", "_")
return key
def _get(key):
if not l0_cache:
return cache.get(key)
result = l0_cache.get(key)
if result is None:
result = cache.get(key)
return result
def _set_l0(key, value):
if l0_cache:
l0_cache.set(key, value, 30)
def _set(key, value, timeout):
_set_l0(key, value)
cache.set(key, value, timeout)
def decorator(func):
def _validate_type(cache_key, result):
if expected_type and not isinstance(result, expected_type):
data = {
"function": f"{func.__module__}.{func.__qualname__}",
"result": str(result)[:30],
"expected_type": expected_type,
"type": type(result),
"key": cache_key,
}
log_debug("invalid_key", data)
return False
return True
def wrapper(*args, **kwargs):
cache_key = get_key(func, *args, **kwargs)
result = _get(cache_key)
if result is not None and _validate_type(cache_key, result):
_set_l0(cache_key, result)
if type(result) == str and result == NONE_RESULT:
result = None
return result
result = func(*args, **kwargs)
if result is None:
cache_result = NONE_RESULT
else:
cache_result = result
_set(cache_key, cache_result, timeout)
return result
def dirty(*args, **kwargs):
cache_key = get_key(func, *args, **kwargs)
cache.delete(cache_key)
if l0_cache:
l0_cache.delete(cache_key)
def prefetch_multi(args_list):
keys = []
for args in args_list:
keys.append(get_key(func, *args))
results = cache.get_many(keys)
for key, result in results.items():
if result is not None:
_set_l0(key, result)
def dirty_multi(args_list):
keys = []
for args in args_list:
keys.append(get_key(func, *args))
cache.delete_many(keys) cache.delete_many(keys)
if l0_cache:
l0_cache.delete_many(keys)
wrapper.dirty = dirty
wrapper.prefetch_multi = prefetch_multi
wrapper.dirty_multi = dirty_multi
return wrapper
return decorator

View file

@ -1,155 +0,0 @@
from django import forms
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.db.models import Count
from django.db.models.expressions import F, Value
from django.db.models.functions import Coalesce
from django.forms import ModelForm
from django.http import HttpResponseForbidden, HttpResponseNotFound, HttpResponseRedirect
from django.urls import reverse_lazy
from django.utils.decorators import method_decorator
from django.utils.translation import gettext as _
from django.views.generic import View
from django.views.generic.base import TemplateResponseMixin
from django.views.generic.detail import SingleObjectMixin
from reversion import revisions
from reversion.models import Revision, Version
from judge.dblock import LockModel
from judge.models import Comment, CommentLock, CommentVote, Notification
from judge.utils.raw_sql import RawSQLColumn, unique_together_left_join
from judge.widgets import HeavyPreviewPageDownWidget
from judge.jinja2.reference import get_user_from_text
def add_mention_notifications(comment):
user_referred = get_user_from_text(comment.body).exclude(id=comment.author.id)
for user in user_referred:
notification_ref = Notification(owner=user,
comment=comment,
category='Mention')
notification_ref.save()
def del_mention_notifications(comment):
query = {
'comment': comment,
'category': 'Mention'
}
Notification.objects.filter(**query).delete()
class CommentForm(ModelForm):
class Meta:
model = Comment
fields = ['body', 'parent']
widgets = {
'parent': forms.HiddenInput(),
}
if HeavyPreviewPageDownWidget is not None:
widgets['body'] = HeavyPreviewPageDownWidget(preview=reverse_lazy('comment_preview'),
preview_timeout=1000, hide_preview_button=True)
def __init__(self, request, *args, **kwargs):
self.request = request
super(CommentForm, self).__init__(*args, **kwargs)
self.fields['body'].widget.attrs.update({'placeholder': _('Comment body')})
def clean(self):
if self.request is not None and self.request.user.is_authenticated:
profile = self.request.profile
if profile.mute:
raise ValidationError(_('Your part is silent, little toad.'))
elif (not self.request.user.is_staff and
not profile.submission_set.filter(points=F('problem__points')).exists()):
raise ValidationError(_('You need to have solved at least one problem '
'before your voice can be heard.'))
return super(CommentForm, self).clean()
class CommentedDetailView(TemplateResponseMixin, SingleObjectMixin, View):
comment_page = None
def get_comment_page(self):
if self.comment_page is None:
raise NotImplementedError()
return self.comment_page
def is_comment_locked(self):
if self.request.user.has_perm('judge.override_comment_lock'):
return False
return (CommentLock.objects.filter(page=self.get_comment_page()).exists()
or (self.request.in_contest and self.request.participation.contest.use_clarifications))
@method_decorator(login_required)
def post(self, request, *args, **kwargs):
self.object = self.get_object()
page = self.get_comment_page()
if self.is_comment_locked():
return HttpResponseForbidden()
parent = request.POST.get('parent')
if parent:
try:
parent = int(parent)
except ValueError:
return HttpResponseNotFound()
else:
if not Comment.objects.filter(hidden=False, id=parent, page=page).exists():
return HttpResponseNotFound()
form = CommentForm(request, request.POST)
if form.is_valid():
comment = form.save(commit=False)
comment.author = request.profile
comment.page = page
with LockModel(write=(Comment, Revision, Version), read=(ContentType,)), revisions.create_revision():
revisions.set_user(request.user)
revisions.set_comment(_('Posted comment'))
comment.save()
# add notification for reply
if comment.parent and comment.parent.author != comment.author:
notification_rep = Notification(owner=comment.parent.author,
comment=comment,
category='Reply')
notification_rep.save()
add_mention_notifications(comment)
return HttpResponseRedirect(request.path)
context = self.get_context_data(object=self.object, comment_form=form)
return self.render_to_response(context)
def get(self, request, *args, **kwargs):
self.object = self.get_object()
return self.render_to_response(self.get_context_data(
object=self.object,
comment_form=CommentForm(request, initial={'page': self.get_comment_page(), 'parent': None}),
))
def get_context_data(self, **kwargs):
context = super(CommentedDetailView, self).get_context_data(**kwargs)
queryset = Comment.objects.filter(hidden=False, page=self.get_comment_page())
context['has_comments'] = queryset.exists()
context['comment_lock'] = self.is_comment_locked()
queryset = queryset.select_related('author__user').defer('author__about').annotate(revisions=Count('versions'))
if self.request.user.is_authenticated:
queryset = queryset.annotate(vote_score=Coalesce(RawSQLColumn(CommentVote, 'score'), Value(0)))
profile = self.request.profile
unique_together_left_join(queryset, CommentVote, 'comment', 'voter', profile.id)
context['is_new_user'] = (not self.request.user.is_staff and
not profile.submission_set.filter(points=F('problem__points')).exists())
context['comment_list'] = queryset
context['vote_hide_threshold'] = settings.DMOJ_COMMENT_VOTE_HIDE_THRESHOLD
return context

View file

@ -3,4 +3,6 @@ from judge.contest_format.default import DefaultContestFormat
from judge.contest_format.ecoo import ECOOContestFormat from judge.contest_format.ecoo import ECOOContestFormat
from judge.contest_format.icpc import ICPCContestFormat from judge.contest_format.icpc import ICPCContestFormat
from judge.contest_format.ioi import IOIContestFormat from judge.contest_format.ioi import IOIContestFormat
from judge.contest_format.new_ioi import NewIOIContestFormat
from judge.contest_format.ultimate import UltimateContestFormat
from judge.contest_format.registry import choices, formats from judge.contest_format.registry import choices, formats

View file

@ -10,18 +10,18 @@ from django.utils.translation import gettext_lazy
from judge.contest_format.default import DefaultContestFormat from judge.contest_format.default import DefaultContestFormat
from judge.contest_format.registry import register_contest_format from judge.contest_format.registry import register_contest_format
from judge.timezone import from_database_time from judge.timezone import from_database_time, to_database_time
from judge.utils.timedelta import nice_repr from judge.utils.timedelta import nice_repr
@register_contest_format('atcoder') @register_contest_format("atcoder")
class AtCoderContestFormat(DefaultContestFormat): class AtCoderContestFormat(DefaultContestFormat):
name = gettext_lazy('AtCoder') name = gettext_lazy("AtCoder")
config_defaults = {'penalty': 5} config_defaults = {"penalty": 5}
config_validators = {'penalty': lambda x: x >= 0} config_validators = {"penalty": lambda x: x >= 0}
''' """
penalty: Number of penalty minutes each incorrect submission adds. Defaults to 5. penalty: Number of penalty minutes each incorrect submission adds. Defaults to 5.
''' """
@classmethod @classmethod
def validate(cls, config): def validate(cls, config):
@ -29,7 +29,9 @@ class AtCoderContestFormat(DefaultContestFormat):
return return
if not isinstance(config, dict): if not isinstance(config, dict):
raise ValidationError('AtCoder-styled contest expects no config or dict as config') raise ValidationError(
"AtCoder-styled contest expects no config or dict as config"
)
for key, value in config.items(): for key, value in config.items():
if key not in cls.config_defaults: if key not in cls.config_defaults:
@ -37,7 +39,9 @@ class AtCoderContestFormat(DefaultContestFormat):
if not isinstance(value, type(cls.config_defaults[key])): if not isinstance(value, type(cls.config_defaults[key])):
raise ValidationError('invalid type for config key "%s"' % key) raise ValidationError('invalid type for config key "%s"' % key)
if not cls.config_validators[key](value): if not cls.config_validators[key](value):
raise ValidationError('invalid value "%s" for config key "%s"' % (value, key)) raise ValidationError(
'invalid value "%s" for config key "%s"' % (value, key)
)
def __init__(self, contest, config): def __init__(self, contest, config):
self.config = self.config_defaults.copy() self.config = self.config_defaults.copy()
@ -50,8 +54,13 @@ class AtCoderContestFormat(DefaultContestFormat):
points = 0 points = 0
format_data = {} format_data = {}
frozen_time = self.contest.end_time
if self.contest.freeze_after:
frozen_time = participation.start + self.contest.freeze_after
with connection.cursor() as cursor: with connection.cursor() as cursor:
cursor.execute(''' cursor.execute(
"""
SELECT MAX(cs.points) as `score`, ( SELECT MAX(cs.points) as `score`, (
SELECT MIN(csub.date) SELECT MIN(csub.date)
FROM judge_contestsubmission ccs LEFT OUTER JOIN FROM judge_contestsubmission ccs LEFT OUTER JOIN
@ -61,22 +70,29 @@ class AtCoderContestFormat(DefaultContestFormat):
FROM judge_contestproblem cp INNER JOIN FROM judge_contestproblem cp INNER JOIN
judge_contestsubmission cs ON (cs.problem_id = cp.id AND cs.participation_id = %s) LEFT OUTER JOIN judge_contestsubmission cs ON (cs.problem_id = cp.id AND cs.participation_id = %s) LEFT OUTER JOIN
judge_submission sub ON (sub.id = cs.submission_id) judge_submission sub ON (sub.id = cs.submission_id)
WHERE sub.date < %s
GROUP BY cp.id GROUP BY cp.id
''', (participation.id, participation.id)) """,
(participation.id, participation.id, to_database_time(frozen_time)),
)
for score, time, prob in cursor.fetchall(): for score, time, prob in cursor.fetchall():
time = from_database_time(time) time = from_database_time(time)
dt = (time - participation.start).total_seconds() dt = (time - participation.start).total_seconds()
# Compute penalty # Compute penalty
if self.config['penalty']: if self.config["penalty"]:
# An IE can have a submission result of `None` # An IE can have a submission result of `None`
subs = participation.submissions.exclude(submission__result__isnull=True) \ subs = (
.exclude(submission__result__in=['IE', 'CE']) \ participation.submissions.exclude(
submission__result__isnull=True
)
.exclude(submission__result__in=["IE", "CE"])
.filter(problem_id=prob) .filter(problem_id=prob)
)
if score: if score:
prev = subs.filter(submission__date__lte=time).count() - 1 prev = subs.filter(submission__date__lte=time).count() - 1
penalty += prev * self.config['penalty'] * 60 penalty += prev * self.config["penalty"] * 60
else: else:
# We should always display the penalty, even if the user has a score of 0 # We should always display the penalty, even if the user has a score of 0
prev = subs.count() prev = subs.count()
@ -86,29 +102,52 @@ class AtCoderContestFormat(DefaultContestFormat):
if score: if score:
cumtime = max(cumtime, dt) cumtime = max(cumtime, dt)
format_data[str(prob)] = {'time': dt, 'points': score, 'penalty': prev} format_data[str(prob)] = {"time": dt, "points": score, "penalty": prev}
points += score points += score
self.handle_frozen_state(participation, format_data)
participation.cumtime = cumtime + penalty participation.cumtime = cumtime + penalty
participation.score = points participation.score = round(points, self.contest.points_precision)
participation.tiebreaker = 0 participation.tiebreaker = 0
participation.format_data = format_data participation.format_data = format_data
participation.save() participation.save()
def display_user_problem(self, participation, contest_problem): def display_user_problem(self, participation, contest_problem, show_final=False):
format_data = (participation.format_data or {}).get(str(contest_problem.id)) format_data = (participation.format_data or {}).get(str(contest_problem.id))
if format_data: if format_data:
penalty = format_html('<small style="color:red"> ({penalty})</small>', penalty = (
penalty=floatformat(format_data['penalty'])) if format_data['penalty'] else '' format_html(
'<small style="color:red"> ({penalty})</small>',
penalty=floatformat(format_data["penalty"]),
)
if format_data.get("penalty")
else ""
)
return format_html( return format_html(
'<td class="{state} problem-score-col"><a href="{url}">{points}{penalty}<div class="solving-time">{time}</div></a></td>', '<td class="{state} problem-score-col"><a data-featherlight="{url}" href="#">{points}{penalty}<div class="solving-time">{time}</div></a></td>',
state=(('pretest-' if self.contest.run_pretests_only and contest_problem.is_pretested else '') + state=(
self.best_solution_state(format_data['points'], contest_problem.points)), (
url=reverse('contest_user_submissions', "pretest-"
args=[self.contest.key, participation.user.user.username, contest_problem.problem.code]), if self.contest.run_pretests_only
points=floatformat(format_data['points']), and contest_problem.is_pretested
else ""
)
+ self.best_solution_state(
format_data["points"], contest_problem.points
)
+ (" frozen" if format_data.get("frozen") else "")
),
url=reverse(
"contest_user_submissions_ajax",
args=[
self.contest.key,
participation.id,
contest_problem.problem.code,
],
),
points=floatformat(format_data["points"]),
penalty=penalty, penalty=penalty,
time=nice_repr(timedelta(seconds=format_data['time']), 'noday'), time=nice_repr(timedelta(seconds=format_data["time"]), "noday"),
) )
else: else:
return mark_safe('<td class="problem-score-col"></td>') return mark_safe('<td class="problem-score-col"></td>')

View file

@ -1,6 +1,5 @@
from abc import ABCMeta, abstractmethod, abstractproperty from abc import ABCMeta, abstractmethod, abstractproperty
from django.db.models import Max
from django.utils import six
class abstractclassmethod(classmethod): class abstractclassmethod(classmethod):
@ -11,7 +10,9 @@ class abstractclassmethod(classmethod):
super(abstractclassmethod, self).__init__(callable) super(abstractclassmethod, self).__init__(callable)
class BaseContestFormat(six.with_metaclass(ABCMeta)): class BaseContestFormat(metaclass=ABCMeta):
has_hidden_subtasks = False
@abstractmethod @abstractmethod
def __init__(self, contest, config): def __init__(self, contest, config):
self.config = config self.config = config
@ -49,7 +50,7 @@ class BaseContestFormat(six.with_metaclass(ABCMeta)):
raise NotImplementedError() raise NotImplementedError()
@abstractmethod @abstractmethod
def display_user_problem(self, participation, contest_problem): def display_user_problem(self, participation, contest_problem, show_final):
""" """
Returns the HTML fragment to show a user's performance on an individual problem. This is expected to use Returns the HTML fragment to show a user's performance on an individual problem. This is expected to use
information from the format_data field instead of computing it from scratch. information from the format_data field instead of computing it from scratch.
@ -61,7 +62,7 @@ class BaseContestFormat(six.with_metaclass(ABCMeta)):
raise NotImplementedError() raise NotImplementedError()
@abstractmethod @abstractmethod
def display_participation_result(self, participation): def display_participation_result(self, participation, show_final):
""" """
Returns the HTML fragment to show a user's performance on the whole contest. This is expected to use Returns the HTML fragment to show a user's performance on the whole contest. This is expected to use
information from the format_data field instead of computing it from scratch. information from the format_data field instead of computing it from scratch.
@ -93,7 +94,30 @@ class BaseContestFormat(six.with_metaclass(ABCMeta)):
@classmethod @classmethod
def best_solution_state(cls, points, total): def best_solution_state(cls, points, total):
if not points: if not points:
return 'failed-score' return "failed-score"
if points == total: if points == total:
return 'full-score' return "full-score"
return 'partial-score' return "partial-score"
def handle_frozen_state(self, participation, format_data):
hidden_subtasks = {}
if hasattr(self, "get_hidden_subtasks"):
hidden_subtasks = self.get_hidden_subtasks()
queryset = participation.submissions.values("problem_id").annotate(
time=Max("submission__date")
)
for result in queryset:
problem = str(result["problem_id"])
if not (self.contest.freeze_after or hidden_subtasks.get(problem)):
continue
if format_data.get(problem):
is_after_freeze = (
self.contest.freeze_after
and result["time"]
>= self.contest.freeze_after + participation.start
)
if is_after_freeze or hidden_subtasks.get(problem):
format_data[problem]["frozen"] = True
else:
format_data[problem] = {"time": 0, "points": 0, "frozen": True}

View file

@ -13,14 +13,16 @@ from judge.contest_format.registry import register_contest_format
from judge.utils.timedelta import nice_repr from judge.utils.timedelta import nice_repr
@register_contest_format('default') @register_contest_format("default")
class DefaultContestFormat(BaseContestFormat): class DefaultContestFormat(BaseContestFormat):
name = gettext_lazy('Default') name = gettext_lazy("Default")
@classmethod @classmethod
def validate(cls, config): def validate(cls, config):
if config is not None and (not isinstance(config, dict) or config): if config is not None and (not isinstance(config, dict) or config):
raise ValidationError('default contest expects no config or empty dict as config') raise ValidationError(
"default contest expects no config or empty dict as config"
)
def __init__(self, contest, config): def __init__(self, contest, config):
super(DefaultContestFormat, self).__init__(contest, config) super(DefaultContestFormat, self).__init__(contest, config)
@ -30,49 +32,84 @@ class DefaultContestFormat(BaseContestFormat):
points = 0 points = 0
format_data = {} format_data = {}
for result in participation.submissions.values('problem_id').annotate( queryset = participation.submissions
time=Max('submission__date'), points=Max('points'),
):
dt = (result['time'] - participation.start).total_seconds()
if result['points']:
cumtime += dt
format_data[str(result['problem_id'])] = {'time': dt, 'points': result['points']}
points += result['points']
if self.contest.freeze_after:
queryset = queryset.filter(
submission__date__lt=participation.start + self.contest.freeze_after
)
queryset = queryset.values("problem_id").annotate(
time=Max("submission__date"),
points=Max("points"),
)
for result in queryset:
dt = (result["time"] - participation.start).total_seconds()
if result["points"]:
cumtime += dt
format_data[str(result["problem_id"])] = {
"time": dt,
"points": result["points"],
}
points += result["points"]
self.handle_frozen_state(participation, format_data)
participation.cumtime = max(cumtime, 0) participation.cumtime = max(cumtime, 0)
participation.score = points participation.score = round(points, self.contest.points_precision)
participation.tiebreaker = 0 participation.tiebreaker = 0
participation.format_data = format_data participation.format_data = format_data
participation.save() participation.save()
def display_user_problem(self, participation, contest_problem): def display_user_problem(self, participation, contest_problem, show_final=False):
format_data = (participation.format_data or {}).get(str(contest_problem.id)) format_data = (participation.format_data or {}).get(str(contest_problem.id))
if format_data: if format_data:
return format_html( return format_html(
u'<td class="{state} problem-score-col"><a href="{url}">{points}<div class="solving-time">{time}</div></a></td>', '<td class="{state} problem-score-col"><a data-featherlight="{url}" href="#">{points}<div class="solving-time">{time}</div></a></td>',
state=(('pretest-' if self.contest.run_pretests_only and contest_problem.is_pretested else '') + state=(
self.best_solution_state(format_data['points'], contest_problem.points)), (
url=reverse('contest_user_submissions', "pretest-"
args=[self.contest.key, participation.user.user.username, contest_problem.problem.code]), if self.contest.run_pretests_only
points=floatformat(format_data['points'], -self.contest.points_precision), and contest_problem.is_pretested
time=nice_repr(timedelta(seconds=format_data['time']), 'noday'), else ""
)
+ self.best_solution_state(
format_data["points"], contest_problem.points
)
+ (" frozen" if format_data.get("frozen") else "")
),
url=reverse(
"contest_user_submissions_ajax",
args=[
self.contest.key,
participation.id,
contest_problem.problem.code,
],
),
points=floatformat(
format_data["points"], -self.contest.points_precision
),
time=nice_repr(timedelta(seconds=format_data["time"]), "noday"),
) )
else: else:
return mark_safe('<td class="problem-score-col"></td>') return mark_safe('<td class="problem-score-col"></td>')
def display_participation_result(self, participation): def display_participation_result(self, participation, show_final=False):
return format_html( return format_html(
u'<td class="user-points">{points}<div class="solving-time">{cumtime}</div></td>', '<td class="user-points">{points}<div class="solving-time">{cumtime}</div></td>',
points=floatformat(participation.score, -self.contest.points_precision), points=floatformat(participation.score, -self.contest.points_precision),
cumtime=nice_repr(timedelta(seconds=participation.cumtime), 'noday'), cumtime=nice_repr(timedelta(seconds=participation.cumtime), "noday"),
) )
def get_problem_breakdown(self, participation, contest_problems): def get_problem_breakdown(self, participation, contest_problems):
return [(participation.format_data or {}).get(str(contest_problem.id)) for contest_problem in contest_problems] return [
(participation.format_data or {}).get(str(contest_problem.id))
for contest_problem in contest_problems
]
def get_contest_problem_label_script(self): def get_contest_problem_label_script(self):
return ''' return """
function(n) function(n)
return tostring(math.floor(n + 1)) return tostring(math.floor(n + 1))
end end
''' """

View file

@ -10,21 +10,25 @@ from django.utils.translation import gettext_lazy
from judge.contest_format.default import DefaultContestFormat from judge.contest_format.default import DefaultContestFormat
from judge.contest_format.registry import register_contest_format from judge.contest_format.registry import register_contest_format
from judge.timezone import from_database_time from judge.timezone import from_database_time, to_database_time
from judge.utils.timedelta import nice_repr from judge.utils.timedelta import nice_repr
@register_contest_format('ecoo') @register_contest_format("ecoo")
class ECOOContestFormat(DefaultContestFormat): class ECOOContestFormat(DefaultContestFormat):
name = gettext_lazy('ECOO') name = gettext_lazy("ECOO")
config_defaults = {'cumtime': False, 'first_ac_bonus': 10, 'time_bonus': 5} config_defaults = {"cumtime": False, "first_ac_bonus": 10, "time_bonus": 5}
config_validators = {'cumtime': lambda x: True, 'first_ac_bonus': lambda x: x >= 0, 'time_bonus': lambda x: x >= 0} config_validators = {
''' "cumtime": lambda x: True,
"first_ac_bonus": lambda x: x >= 0,
"time_bonus": lambda x: x >= 0,
}
"""
cumtime: Specify True if cumulative time is to be used in breaking ties. Defaults to False. cumtime: Specify True if cumulative time is to be used in breaking ties. Defaults to False.
first_ac_bonus: The number of points to award if a solution gets AC on its first non-IE/CE run. Defaults to 10. first_ac_bonus: The number of points to award if a solution gets AC on its first non-IE/CE run. Defaults to 10.
time_bonus: Number of minutes to award an extra point for submitting before the contest end. time_bonus: Number of minutes to award an extra point for submitting before the contest end.
Specify 0 to disable. Defaults to 5. Specify 0 to disable. Defaults to 5.
''' """
@classmethod @classmethod
def validate(cls, config): def validate(cls, config):
@ -32,7 +36,9 @@ class ECOOContestFormat(DefaultContestFormat):
return return
if not isinstance(config, dict): if not isinstance(config, dict):
raise ValidationError('ECOO-styled contest expects no config or dict as config') raise ValidationError(
"ECOO-styled contest expects no config or dict as config"
)
for key, value in config.items(): for key, value in config.items():
if key not in cls.config_defaults: if key not in cls.config_defaults:
@ -40,7 +46,9 @@ class ECOOContestFormat(DefaultContestFormat):
if not isinstance(value, type(cls.config_defaults[key])): if not isinstance(value, type(cls.config_defaults[key])):
raise ValidationError('invalid type for config key "%s"' % key) raise ValidationError('invalid type for config key "%s"' % key)
if not cls.config_validators[key](value): if not cls.config_validators[key](value):
raise ValidationError('invalid value "%s" for config key "%s"' % (value, key)) raise ValidationError(
'invalid value "%s" for config key "%s"' % (value, key)
)
def __init__(self, contest, config): def __init__(self, contest, config):
self.config = self.config_defaults.copy() self.config = self.config_defaults.copy()
@ -52,8 +60,13 @@ class ECOOContestFormat(DefaultContestFormat):
points = 0 points = 0
format_data = {} format_data = {}
frozen_time = self.contest.end_time
if self.contest.freeze_after:
frozen_time = participation.start + self.contest.freeze_after
with connection.cursor() as cursor: with connection.cursor() as cursor:
cursor.execute(''' cursor.execute(
"""
SELECT ( SELECT (
SELECT MAX(ccs.points) SELECT MAX(ccs.points)
FROM judge_contestsubmission ccs LEFT OUTER JOIN FROM judge_contestsubmission ccs LEFT OUTER JOIN
@ -68,56 +81,92 @@ class ECOOContestFormat(DefaultContestFormat):
FROM judge_contestproblem cp INNER JOIN FROM judge_contestproblem cp INNER JOIN
judge_contestsubmission cs ON (cs.problem_id = cp.id AND cs.participation_id = %s) LEFT OUTER JOIN judge_contestsubmission cs ON (cs.problem_id = cp.id AND cs.participation_id = %s) LEFT OUTER JOIN
judge_submission sub ON (sub.id = cs.submission_id) judge_submission sub ON (sub.id = cs.submission_id)
WHERE sub.date < %s
GROUP BY cp.id GROUP BY cp.id
''', (participation.id, participation.id, participation.id)) """,
(
participation.id,
participation.id,
participation.id,
to_database_time(frozen_time),
),
)
for score, time, prob, subs, max_score in cursor.fetchall(): for score, time, prob, subs, max_score in cursor.fetchall():
time = from_database_time(time) time = from_database_time(time)
dt = (time - participation.start).total_seconds() dt = (time - participation.start).total_seconds()
if self.config['cumtime']: if self.config["cumtime"]:
cumtime += dt cumtime += dt
bonus = 0 bonus = 0
if score > 0: if score > 0:
# First AC bonus # First AC bonus
if subs == 1 and score == max_score: if subs == 1 and score == max_score:
bonus += self.config['first_ac_bonus'] bonus += self.config["first_ac_bonus"]
# Time bonus # Time bonus
if self.config['time_bonus']: if self.config["time_bonus"]:
bonus += (participation.end_time - time).total_seconds() // 60 // self.config['time_bonus'] bonus += (
(participation.end_time - time).total_seconds()
// 60
// self.config["time_bonus"]
)
points += bonus points += bonus
format_data[str(prob)] = {'time': dt, 'points': score, 'bonus': bonus} format_data[str(prob)] = {"time": dt, "points": score, "bonus": bonus}
points += score points += score
self.handle_frozen_state(participation, format_data)
participation.cumtime = cumtime participation.cumtime = cumtime
participation.score = points participation.score = round(points, self.contest.points_precision)
participation.tiebreaker = 0 participation.tiebreaker = 0
participation.format_data = format_data participation.format_data = format_data
participation.save() participation.save()
def display_user_problem(self, participation, contest_problem): def display_user_problem(self, participation, contest_problem, show_final=False):
format_data = (participation.format_data or {}).get(str(contest_problem.id)) format_data = (participation.format_data or {}).get(str(contest_problem.id))
if format_data: if format_data:
bonus = format_html('<small> +{bonus}</small>', bonus = (
bonus=floatformat(format_data['bonus'])) if format_data['bonus'] else '' format_html(
"<small> +{bonus}</small>", bonus=floatformat(format_data["bonus"])
)
if format_data.get("bonus")
else ""
)
return format_html( return format_html(
'<td class="{state}"><a href="{url}">{points}{bonus}<div class="solving-time">{time}</div></a></td>', '<td class="{state}"><a data-featherlight="{url}" href="#">{points}{bonus}<div class="solving-time">{time}</div></a></td>',
state=(('pretest-' if self.contest.run_pretests_only and contest_problem.is_pretested else '') + state=(
self.best_solution_state(format_data['points'], contest_problem.points)), (
url=reverse('contest_user_submissions', "pretest-"
args=[self.contest.key, participation.user.user.username, contest_problem.problem.code]), if self.contest.run_pretests_only
points=floatformat(format_data['points']), and contest_problem.is_pretested
else ""
)
+ self.best_solution_state(
format_data["points"], contest_problem.points
)
+ (" frozen" if format_data.get("frozen") else "")
),
url=reverse(
"contest_user_submissions_ajax",
args=[
self.contest.key,
participation.id,
contest_problem.problem.code,
],
),
points=floatformat(format_data["points"]),
bonus=bonus, bonus=bonus,
time=nice_repr(timedelta(seconds=format_data['time']), 'noday'), time=nice_repr(timedelta(seconds=format_data["time"]), "noday"),
) )
else: else:
return mark_safe('<td></td>') return mark_safe("<td></td>")
def display_participation_result(self, participation): def display_participation_result(self, participation, show_final=False):
return format_html( return format_html(
'<td class="user-points">{points}<div class="solving-time">{cumtime}</div></td>', '<td class="user-points">{points}<div class="solving-time">{cumtime}</div></td>',
points=floatformat(participation.score), points=floatformat(participation.score),
cumtime=nice_repr(timedelta(seconds=participation.cumtime), 'noday') if self.config['cumtime'] else '', cumtime=nice_repr(timedelta(seconds=participation.cumtime), "noday")
if self.config["cumtime"]
else "",
) )

View file

@ -10,18 +10,18 @@ from django.utils.translation import gettext_lazy
from judge.contest_format.default import DefaultContestFormat from judge.contest_format.default import DefaultContestFormat
from judge.contest_format.registry import register_contest_format from judge.contest_format.registry import register_contest_format
from judge.timezone import from_database_time from judge.timezone import from_database_time, to_database_time
from judge.utils.timedelta import nice_repr from judge.utils.timedelta import nice_repr
@register_contest_format('icpc') @register_contest_format("icpc")
class ICPCContestFormat(DefaultContestFormat): class ICPCContestFormat(DefaultContestFormat):
name = gettext_lazy('ICPC') name = gettext_lazy("ICPC")
config_defaults = {'penalty': 20} config_defaults = {"penalty": 20}
config_validators = {'penalty': lambda x: x >= 0} config_validators = {"penalty": lambda x: x >= 0}
''' """
penalty: Number of penalty minutes each incorrect submission adds. Defaults to 20. penalty: Number of penalty minutes each incorrect submission adds. Defaults to 20.
''' """
@classmethod @classmethod
def validate(cls, config): def validate(cls, config):
@ -29,7 +29,9 @@ class ICPCContestFormat(DefaultContestFormat):
return return
if not isinstance(config, dict): if not isinstance(config, dict):
raise ValidationError('ICPC-styled contest expects no config or dict as config') raise ValidationError(
"ICPC-styled contest expects no config or dict as config"
)
for key, value in config.items(): for key, value in config.items():
if key not in cls.config_defaults: if key not in cls.config_defaults:
@ -37,7 +39,9 @@ class ICPCContestFormat(DefaultContestFormat):
if not isinstance(value, type(cls.config_defaults[key])): if not isinstance(value, type(cls.config_defaults[key])):
raise ValidationError('invalid type for config key "%s"' % key) raise ValidationError('invalid type for config key "%s"' % key)
if not cls.config_validators[key](value): if not cls.config_validators[key](value):
raise ValidationError('invalid value "%s" for config key "%s"' % (value, key)) raise ValidationError(
'invalid value "%s" for config key "%s"' % (value, key)
)
def __init__(self, contest, config): def __init__(self, contest, config):
self.config = self.config_defaults.copy() self.config = self.config_defaults.copy()
@ -51,8 +55,13 @@ class ICPCContestFormat(DefaultContestFormat):
score = 0 score = 0
format_data = {} format_data = {}
frozen_time = self.contest.end_time
if self.contest.freeze_after:
frozen_time = participation.start + self.contest.freeze_after
with connection.cursor() as cursor: with connection.cursor() as cursor:
cursor.execute(''' cursor.execute(
"""
SELECT MAX(cs.points) as `points`, ( SELECT MAX(cs.points) as `points`, (
SELECT MIN(csub.date) SELECT MIN(csub.date)
FROM judge_contestsubmission ccs LEFT OUTER JOIN FROM judge_contestsubmission ccs LEFT OUTER JOIN
@ -62,22 +71,29 @@ class ICPCContestFormat(DefaultContestFormat):
FROM judge_contestproblem cp INNER JOIN FROM judge_contestproblem cp INNER JOIN
judge_contestsubmission cs ON (cs.problem_id = cp.id AND cs.participation_id = %s) LEFT OUTER JOIN judge_contestsubmission cs ON (cs.problem_id = cp.id AND cs.participation_id = %s) LEFT OUTER JOIN
judge_submission sub ON (sub.id = cs.submission_id) judge_submission sub ON (sub.id = cs.submission_id)
WHERE sub.date < %s
GROUP BY cp.id GROUP BY cp.id
''', (participation.id, participation.id)) """,
(participation.id, participation.id, to_database_time(frozen_time)),
)
for points, time, prob in cursor.fetchall(): for points, time, prob in cursor.fetchall():
time = from_database_time(time) time = from_database_time(time)
dt = (time - participation.start).total_seconds() dt = (time - participation.start).total_seconds()
# Compute penalty # Compute penalty
if self.config['penalty']: if self.config["penalty"]:
# An IE can have a submission result of `None` # An IE can have a submission result of `None`
subs = participation.submissions.exclude(submission__result__isnull=True) \ subs = (
.exclude(submission__result__in=['IE', 'CE']) \ participation.submissions.exclude(
submission__result__isnull=True
)
.exclude(submission__result__in=["IE", "CE"])
.filter(problem_id=prob) .filter(problem_id=prob)
)
if points: if points:
prev = subs.filter(submission__date__lte=time).count() - 1 prev = subs.filter(submission__date__lte=time).count() - 1
penalty += prev * self.config['penalty'] * 60 penalty += prev * self.config["penalty"] * 60
else: else:
# We should always display the penalty, even if the user has a score of 0 # We should always display the penalty, even if the user has a score of 0
prev = subs.count() prev = subs.count()
@ -88,35 +104,58 @@ class ICPCContestFormat(DefaultContestFormat):
cumtime += dt cumtime += dt
last = max(last, dt) last = max(last, dt)
format_data[str(prob)] = {'time': dt, 'points': points, 'penalty': prev} format_data[str(prob)] = {"time": dt, "points": points, "penalty": prev}
score += points score += points
self.handle_frozen_state(participation, format_data)
participation.cumtime = max(0, cumtime + penalty) participation.cumtime = max(0, cumtime + penalty)
participation.score = score participation.score = round(score, self.contest.points_precision)
participation.tiebreaker = last # field is sorted from least to greatest participation.tiebreaker = last # field is sorted from least to greatest
participation.format_data = format_data participation.format_data = format_data
participation.save() participation.save()
def display_user_problem(self, participation, contest_problem): def display_user_problem(self, participation, contest_problem, show_final=False):
format_data = (participation.format_data or {}).get(str(contest_problem.id)) format_data = (participation.format_data or {}).get(str(contest_problem.id))
if format_data: if format_data:
penalty = format_html('<small style="color:red"> ({penalty})</small>', penalty = (
penalty=floatformat(format_data['penalty'])) if format_data['penalty'] else '' format_html(
'<small style="color:red"> +{penalty}</small>',
penalty=floatformat(format_data["penalty"]),
)
if format_data.get("penalty")
else ""
)
return format_html( return format_html(
'<td class="{state}"><a href="{url}">{points}{penalty}<div class="solving-time">{time}</div></a></td>', '<td class="{state}"><a data-featherlight="{url}" href="#">{points}{penalty}<div class="solving-time">{time}</div></a></td>',
state=(('pretest-' if self.contest.run_pretests_only and contest_problem.is_pretested else '') + state=(
self.best_solution_state(format_data['points'], contest_problem.points)), (
url=reverse('contest_user_submissions', "pretest-"
args=[self.contest.key, participation.user.user.username, contest_problem.problem.code]), if self.contest.run_pretests_only
points=floatformat(format_data['points']), and contest_problem.is_pretested
else ""
)
+ self.best_solution_state(
format_data["points"], contest_problem.points
)
+ (" frozen" if format_data.get("frozen") else "")
),
url=reverse(
"contest_user_submissions_ajax",
args=[
self.contest.key,
participation.id,
contest_problem.problem.code,
],
),
points=floatformat(format_data["points"]),
penalty=penalty, penalty=penalty,
time=nice_repr(timedelta(seconds=format_data['time']), 'noday'), time=nice_repr(timedelta(seconds=format_data["time"]), "noday"),
) )
else: else:
return mark_safe('<td></td>') return mark_safe("<td></td>")
def get_contest_problem_label_script(self): def get_contest_problem_label_script(self):
return ''' return """
function(n) function(n)
n = n + 1 n = n + 1
ret = "" ret = ""
@ -126,4 +165,4 @@ class ICPCContestFormat(DefaultContestFormat):
end end
return ret return ret
end end
''' """

View file

@ -12,15 +12,16 @@ from judge.contest_format.default import DefaultContestFormat
from judge.contest_format.registry import register_contest_format from judge.contest_format.registry import register_contest_format
from judge.timezone import from_database_time from judge.timezone import from_database_time
from judge.utils.timedelta import nice_repr from judge.utils.timedelta import nice_repr
from django.db.models import Min, OuterRef, Subquery
@register_contest_format('ioi') @register_contest_format("ioi")
class IOIContestFormat(DefaultContestFormat): class IOIContestFormat(DefaultContestFormat):
name = gettext_lazy('IOI') name = gettext_lazy("IOI")
config_defaults = {'cumtime': False} config_defaults = {"cumtime": False}
''' """
cumtime: Specify True if time penalties are to be computed. Defaults to False. cumtime: Specify True if time penalties are to be computed. Defaults to False.
''' """
@classmethod @classmethod
def validate(cls, config): def validate(cls, config):
@ -28,7 +29,9 @@ class IOIContestFormat(DefaultContestFormat):
return return
if not isinstance(config, dict): if not isinstance(config, dict):
raise ValidationError('IOI-styled contest expects no config or dict as config') raise ValidationError(
"IOI-styled contest expects no config or dict as config"
)
for key, value in config.items(): for key, value in config.items():
if key not in cls.config_defaults: if key not in cls.config_defaults:
@ -43,58 +46,97 @@ class IOIContestFormat(DefaultContestFormat):
def update_participation(self, participation): def update_participation(self, participation):
cumtime = 0 cumtime = 0
points = 0 score = 0
format_data = {} format_data = {}
with connection.cursor() as cursor: queryset = participation.submissions
cursor.execute(''' if self.contest.freeze_after:
SELECT MAX(cs.points) as `score`, ( queryset = queryset.filter(
SELECT MIN(csub.date) submission__date__lt=participation.start + self.contest.freeze_after
FROM judge_contestsubmission ccs LEFT OUTER JOIN )
judge_submission csub ON (csub.id = ccs.submission_id)
WHERE ccs.problem_id = cp.id AND ccs.participation_id = %s AND ccs.points = MAX(cs.points)
) AS `time`, cp.id AS `prob`
FROM judge_contestproblem cp INNER JOIN
judge_contestsubmission cs ON (cs.problem_id = cp.id AND cs.participation_id = %s) LEFT OUTER JOIN
judge_submission sub ON (sub.id = cs.submission_id)
GROUP BY cp.id
''', (participation.id, participation.id))
for score, time, prob in cursor.fetchall(): queryset = (
if self.config['cumtime']: queryset.values("problem_id")
dt = (from_database_time(time) - participation.start).total_seconds() .filter(
if score: points=Subquery(
queryset.filter(problem_id=OuterRef("problem_id"))
.order_by("-points")
.values("points")[:1]
)
)
.annotate(time=Min("submission__date"))
.values_list("problem_id", "time", "points")
)
for problem_id, time, points in queryset:
if self.config["cumtime"]:
dt = (time - participation.start).total_seconds()
if points:
cumtime += dt cumtime += dt
else: else:
dt = 0 dt = 0
format_data[str(prob)] = {'time': dt, 'points': score} format_data[str(problem_id)] = {"points": points, "time": dt}
points += score score += points
self.handle_frozen_state(participation, format_data)
participation.cumtime = max(cumtime, 0) participation.cumtime = max(cumtime, 0)
participation.score = points participation.score = round(score, self.contest.points_precision)
participation.tiebreaker = 0 participation.tiebreaker = 0
participation.format_data = format_data participation.format_data = format_data
participation.save() participation.save()
def display_user_problem(self, participation, contest_problem): def display_user_problem(self, participation, contest_problem, show_final=False):
if show_final:
format_data = (participation.format_data_final or {}).get(
str(contest_problem.id)
)
else:
format_data = (participation.format_data or {}).get(str(contest_problem.id)) format_data = (participation.format_data or {}).get(str(contest_problem.id))
if format_data: if format_data:
return format_html( return format_html(
'<td class="{state} problem-score-col"><a href="{url}">{points}<div class="solving-time">{time}</div></a></td>', '<td class="{state} problem-score-col"><a data-featherlight="{url}" href="#">{points}<div class="solving-time">{time}</div></a></td>',
state=(('pretest-' if self.contest.run_pretests_only and contest_problem.is_pretested else '') + state=(
self.best_solution_state(format_data['points'], contest_problem.points)), (
url=reverse('contest_user_submissions', "pretest-"
args=[self.contest.key, participation.user.user.username, contest_problem.problem.code]), if self.contest.run_pretests_only
points=floatformat(format_data['points']), and contest_problem.is_pretested
time=nice_repr(timedelta(seconds=format_data['time']), 'noday') if self.config['cumtime'] else '', else ""
)
+ self.best_solution_state(
format_data["points"], contest_problem.points
)
+ (" frozen" if format_data.get("frozen") else "")
),
url=reverse(
"contest_user_submissions_ajax",
args=[
self.contest.key,
participation.id,
contest_problem.problem.code,
],
),
points=floatformat(
format_data["points"], -self.contest.points_precision
),
time=nice_repr(timedelta(seconds=format_data["time"]), "noday")
if self.config["cumtime"]
else "",
) )
else: else:
return mark_safe('<td class="problem-score-col"></td>') return mark_safe('<td class="problem-score-col"></td>')
def display_participation_result(self, participation): def display_participation_result(self, participation, show_final=False):
if show_final:
score = participation.score_final
cumtime = participation.cumtime_final
else:
score = participation.score
cumtime = participation.cumtime
return format_html( return format_html(
'<td class="user-points">{points}<div class="solving-time">{cumtime}</div></td>', '<td class="user-points">{points}<div class="solving-time">{cumtime}</div></td>',
points=floatformat(participation.score), points=floatformat(score, -self.contest.points_precision),
cumtime=nice_repr(timedelta(seconds=participation.cumtime), 'noday') if self.config['cumtime'] else '', cumtime=nice_repr(timedelta(seconds=cumtime), "noday")
if self.config["cumtime"]
else "",
) )

View file

@ -0,0 +1,173 @@
from django.db import connection
from django.utils.translation import gettext as _, gettext_lazy
from judge.contest_format.ioi import IOIContestFormat
from judge.contest_format.registry import register_contest_format
from judge.timezone import from_database_time, to_database_time
@register_contest_format("ioi16")
class NewIOIContestFormat(IOIContestFormat):
name = gettext_lazy("New IOI")
config_defaults = {"cumtime": False}
has_hidden_subtasks = True
"""
cumtime: Specify True if time penalties are to be computed. Defaults to False.
"""
def get_hidden_subtasks(self):
queryset = self.contest.contest_problems.values_list("id", "hidden_subtasks")
res = {}
for problem_id, hidden_subtasks in queryset:
subtasks = set()
if hidden_subtasks:
hidden_subtasks = hidden_subtasks.split(",")
for i in hidden_subtasks:
try:
subtasks.add(int(i))
except Exception as e:
pass
res[str(problem_id)] = subtasks
return res
def get_results_by_subtask(self, participation, include_frozen=False):
frozen_time = self.contest.end_time
if self.contest.freeze_after and not include_frozen:
frozen_time = participation.start + self.contest.freeze_after
with connection.cursor() as cursor:
cursor.execute(
"""
SELECT q.prob,
q.prob_points,
MIN(q.date) as `date`,
q.batch_points,
q.total_batch_points,
q.batch,
q.subid
FROM (
SELECT cp.id as `prob`,
cp.points as `prob_points`,
sub.id as `subid`,
sub.date as `date`,
tc.points as `points`,
tc.batch as `batch`,
SUM(tc.points) as `batch_points`,
SUM(tc.total) as `total_batch_points`
FROM judge_contestproblem cp
INNER JOIN
judge_contestsubmission cs
ON (cs.problem_id = cp.id AND cs.participation_id = %s)
LEFT OUTER JOIN
judge_submission sub
ON (sub.id = cs.submission_id AND sub.status = 'D')
INNER JOIN judge_submissiontestcase tc
ON sub.id = tc.submission_id
WHERE sub.date < %s
GROUP BY cp.id, tc.batch, sub.id
) q
INNER JOIN (
SELECT prob, batch, MAX(r.batch_points) as max_batch_points
FROM (
SELECT cp.id as `prob`,
tc.batch as `batch`,
SUM(tc.points) as `batch_points`
FROM judge_contestproblem cp
INNER JOIN
judge_contestsubmission cs
ON (cs.problem_id = cp.id AND cs.participation_id = %s)
LEFT OUTER JOIN
judge_submission sub
ON (sub.id = cs.submission_id AND sub.status = 'D')
INNER JOIN judge_submissiontestcase tc
ON sub.id = tc.submission_id
WHERE sub.date < %s
GROUP BY cp.id, tc.batch, sub.id
) r
GROUP BY prob, batch
) p
ON p.prob = q.prob AND (p.batch = q.batch OR p.batch is NULL AND q.batch is NULL)
WHERE p.max_batch_points = q.batch_points
GROUP BY q.prob, q.batch
""",
(
participation.id,
to_database_time(frozen_time),
participation.id,
to_database_time(frozen_time),
),
)
return cursor.fetchall()
def update_participation(self, participation):
hidden_subtasks = self.get_hidden_subtasks()
def calculate_format_data(participation, include_frozen):
format_data = {}
for (
problem_id,
problem_points,
time,
subtask_points,
total_subtask_points,
subtask,
sub_id,
) in self.get_results_by_subtask(participation, include_frozen):
problem_id = str(problem_id)
time = from_database_time(time)
if self.config["cumtime"]:
dt = (time - participation.start).total_seconds()
else:
dt = 0
if format_data.get(problem_id) is None:
format_data[problem_id] = {
"points": 0,
"time": 0,
"total_points": 0,
}
if (
subtask not in hidden_subtasks.get(problem_id, set())
or include_frozen
):
format_data[problem_id]["points"] += subtask_points
format_data[problem_id]["total_points"] += total_subtask_points
format_data[problem_id]["time"] = max(
dt, format_data[problem_id]["time"]
)
format_data[problem_id]["problem_points"] = problem_points
return format_data
def recalculate_results(format_data):
cumtime = 0
score = 0
for problem_data in format_data.values():
if not problem_data["total_points"]:
continue
penalty = problem_data["time"]
problem_data["points"] = (
problem_data["points"]
/ problem_data["total_points"]
* problem_data["problem_points"]
)
if self.config["cumtime"] and problem_data["points"]:
cumtime += penalty
score += problem_data["points"]
return score, cumtime
format_data = calculate_format_data(participation, False)
score, cumtime = recalculate_results(format_data)
self.handle_frozen_state(participation, format_data)
participation.cumtime = max(cumtime, 0)
participation.score = round(score, self.contest.points_precision)
participation.tiebreaker = 0
participation.format_data = format_data
format_data_final = calculate_format_data(participation, True)
score_final, cumtime_final = recalculate_results(format_data_final)
participation.cumtime_final = max(cumtime_final, 0)
participation.score_final = round(score_final, self.contest.points_precision)
participation.format_data_final = format_data_final
participation.save()

View file

@ -1,5 +1,3 @@
from django.utils import six
formats = {} formats = {}
@ -13,4 +11,4 @@ def register_contest_format(name):
def choices(): def choices():
return [(key, value.name) for key, value in sorted(six.iteritems(formats))] return [(key, value.name) for key, value in sorted(formats.items())]

View file

@ -0,0 +1,55 @@
from django.utils.translation import gettext_lazy
from judge.contest_format.ioi import IOIContestFormat
from judge.contest_format.registry import register_contest_format
from django.db.models import Min, OuterRef, Subquery
# This contest format only counts last submission for each problem.
@register_contest_format("ultimate")
class UltimateContestFormat(IOIContestFormat):
name = gettext_lazy("Ultimate")
def update_participation(self, participation):
cumtime = 0
score = 0
format_data = {}
queryset = participation.submissions
if self.contest.freeze_after:
queryset = queryset.filter(
submission__date__lt=participation.start + self.contest.freeze_after
)
queryset = (
queryset.values("problem_id")
.filter(
id=Subquery(
queryset.filter(problem_id=OuterRef("problem_id"))
.order_by("-id")
.values("id")[:1]
)
)
.values_list("problem_id", "submission__date", "points")
)
for problem_id, time, points in queryset:
if self.config["cumtime"]:
dt = (time - participation.start).total_seconds()
if points:
cumtime += dt
else:
dt = 0
format_data[str(problem_id)] = {
"time": dt,
"points": points,
}
score += points
self.handle_frozen_state(participation, format_data)
participation.cumtime = max(cumtime, 0)
participation.score = round(score, self.contest.points_precision)
participation.tiebreaker = 0
participation.format_data = format_data
participation.save()

View file

@ -0,0 +1,22 @@
from django.utils.translation import gettext_lazy as _, ngettext
def custom_trans():
return [
# Password reset
ngettext(
"This password is too short. It must contain at least %(min_length)d character.",
"This password is too short. It must contain at least %(min_length)d characters.",
0,
),
ngettext(
"Your password must contain at least %(min_length)d character.",
"Your password must contain at least %(min_length)d characters.",
0,
),
_("The two password fields didnt match."),
_("Your password cant be entirely numeric."),
# Navbar
_("Bug Report"),
_("Courses"),
]

View file

@ -5,19 +5,21 @@ from django.db import connection, transaction
class LockModel(object): class LockModel(object):
def __init__(self, write, read=()): def __init__(self, write, read=()):
self.tables = ', '.join(chain( self.tables = ", ".join(
('`%s` WRITE' % model._meta.db_table for model in write), chain(
('`%s` READ' % model._meta.db_table for model in read), ("`%s` WRITE" % model._meta.db_table for model in write),
)) ("`%s` READ" % model._meta.db_table for model in read),
)
)
self.cursor = connection.cursor() self.cursor = connection.cursor()
def __enter__(self): def __enter__(self):
self.cursor.execute('LOCK TABLES ' + self.tables) self.cursor.execute("LOCK TABLES " + self.tables)
def __exit__(self, exc_type, exc_val, exc_tb): def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is None: if exc_type is None:
transaction.commit() transaction.commit()
else: else:
transaction.rollback() transaction.rollback()
self.cursor.execute('UNLOCK TABLES') self.cursor.execute("UNLOCK TABLES")
self.cursor.close() self.cursor.close()

View file

@ -1,6 +1,6 @@
from django.conf import settings from django.conf import settings
__all__ = ['last', 'post'] __all__ = ["last", "post"]
if not settings.EVENT_DAEMON_USE: if not settings.EVENT_DAEMON_USE:
real = False real = False
@ -10,9 +10,12 @@ if not settings.EVENT_DAEMON_USE:
def last(): def last():
return 0 return 0
elif hasattr(settings, 'EVENT_DAEMON_AMQP'):
elif hasattr(settings, "EVENT_DAEMON_AMQP"):
from .event_poster_amqp import last, post from .event_poster_amqp import last, post
real = True real = True
else: else:
from .event_poster_ws import last, post from .event_poster_ws import last, post
real = True real = True

View file

@ -6,7 +6,7 @@ import pika
from django.conf import settings from django.conf import settings
from pika.exceptions import AMQPError from pika.exceptions import AMQPError
__all__ = ['EventPoster', 'post', 'last'] __all__ = ["EventPoster", "post", "last"]
class EventPoster(object): class EventPoster(object):
@ -15,14 +15,19 @@ class EventPoster(object):
self._exchange = settings.EVENT_DAEMON_AMQP_EXCHANGE self._exchange = settings.EVENT_DAEMON_AMQP_EXCHANGE
def _connect(self): def _connect(self):
self._conn = pika.BlockingConnection(pika.URLParameters(settings.EVENT_DAEMON_AMQP)) self._conn = pika.BlockingConnection(
pika.URLParameters(settings.EVENT_DAEMON_AMQP),
)
self._chan = self._conn.channel() self._chan = self._conn.channel()
def post(self, channel, message, tries=0): def post(self, channel, message, tries=0):
try: try:
id = int(time() * 1000000) id = int(time() * 1000000)
self._chan.basic_publish(self._exchange, '', self._chan.basic_publish(
json.dumps({'id': id, 'channel': channel, 'message': message})) self._exchange,
"#",
json.dumps({"id": id, "channel": channel, "message": message}),
)
return id return id
except AMQPError: except AMQPError:
if tries > 10: if tries > 10:
@ -35,7 +40,7 @@ _local = threading.local()
def _get_poster(): def _get_poster():
if 'poster' not in _local.__dict__: if "poster" not in _local.__dict__:
_local.poster = EventPoster() _local.poster = EventPoster()
return _local.poster return _local.poster

View file

@ -5,7 +5,7 @@ import threading
from django.conf import settings from django.conf import settings
from websocket import WebSocketException, create_connection from websocket import WebSocketException, create_connection
__all__ = ['EventPostingError', 'EventPoster', 'post', 'last'] __all__ = ["EventPostingError", "EventPoster", "post", "last"]
_local = threading.local() _local = threading.local()
@ -20,19 +20,23 @@ class EventPoster(object):
def _connect(self): def _connect(self):
self._conn = create_connection(settings.EVENT_DAEMON_POST) self._conn = create_connection(settings.EVENT_DAEMON_POST)
if settings.EVENT_DAEMON_KEY is not None: if settings.EVENT_DAEMON_KEY is not None:
self._conn.send(json.dumps({'command': 'auth', 'key': settings.EVENT_DAEMON_KEY})) self._conn.send(
json.dumps({"command": "auth", "key": settings.EVENT_DAEMON_KEY})
)
resp = json.loads(self._conn.recv()) resp = json.loads(self._conn.recv())
if resp['status'] == 'error': if resp["status"] == "error":
raise EventPostingError(resp['code']) raise EventPostingError(resp["code"])
def post(self, channel, message, tries=0): def post(self, channel, message, tries=0):
try: try:
self._conn.send(json.dumps({'command': 'post', 'channel': channel, 'message': message})) self._conn.send(
json.dumps({"command": "post", "channel": channel, "message": message})
)
resp = json.loads(self._conn.recv()) resp = json.loads(self._conn.recv())
if resp['status'] == 'error': if resp["status"] == "error":
raise EventPostingError(resp['code']) raise EventPostingError(resp["code"])
else: else:
return resp['id'] return resp["id"]
except WebSocketException: except WebSocketException:
if tries > 10: if tries > 10:
raise raise
@ -43,10 +47,10 @@ class EventPoster(object):
try: try:
self._conn.send('{"command": "last-msg"}') self._conn.send('{"command": "last-msg"}')
resp = json.loads(self._conn.recv()) resp = json.loads(self._conn.recv())
if resp['status'] == 'error': if resp["status"] == "error":
raise EventPostingError(resp['code']) raise EventPostingError(resp["code"])
else: else:
return resp['id'] return resp["id"]
except WebSocketException: except WebSocketException:
if tries > 10: if tries > 10:
raise raise
@ -55,7 +59,7 @@ class EventPoster(object):
def _get_poster(): def _get_poster():
if 'poster' not in _local.__dict__: if "poster" not in _local.__dict__:
_local.poster = EventPoster() _local.poster = EventPoster()
return _local.poster return _local.poster

View file

@ -1,110 +0,0 @@
from django.conf import settings
from django.contrib.auth.models import AnonymousUser
from django.contrib.syndication.views import Feed
from django.core.cache import cache
from django.utils import timezone
from django.utils.feedgenerator import Atom1Feed
from judge.jinja2.markdown import markdown
from judge.models import BlogPost, Comment, Problem
import re
# https://lsimons.wordpress.com/2011/03/17/stripping-illegal-characters-out-of-xml-in-python/
def escape_xml_illegal_chars(val, replacement='?'):
_illegal_xml_chars_RE = re.compile(u'[\x00-\x08\x0b\x0c\x0e-\x1F\uD800-\uDFFF\uFFFE\uFFFF]')
return _illegal_xml_chars_RE.sub(replacement, val)
class ProblemFeed(Feed):
title = 'Recently Added %s Problems' % settings.SITE_NAME
link = '/'
description = 'The latest problems added on the %s website' % settings.SITE_LONG_NAME
def items(self):
return Problem.objects.filter(is_public=True, is_organization_private=False).defer('description')\
.order_by('-date', '-id')[:25]
def item_title(self, problem):
return problem.name
def item_description(self, problem):
key = 'problem_feed:%d' % problem.id
desc = cache.get(key)
if desc is None:
desc = str(markdown(problem.description, 'problem'))[:500] + '...'
desc = escape_xml_illegal_chars(desc)
cache.set(key, desc, 86400)
return desc
def item_pubdate(self, problem):
return problem.date
item_updateddate = item_pubdate
class AtomProblemFeed(ProblemFeed):
feed_type = Atom1Feed
subtitle = ProblemFeed.description
class CommentFeed(Feed):
title = 'Latest %s Comments' % settings.SITE_NAME
link = '/'
description = 'The latest comments on the %s website' % settings.SITE_LONG_NAME
def items(self):
return Comment.most_recent(AnonymousUser(), 25)
def item_title(self, comment):
return '%s -> %s' % (comment.author.user.username, comment.page_title)
def item_description(self, comment):
key = 'comment_feed:%d' % comment.id
desc = cache.get(key)
if desc is None:
desc = str(markdown(comment.body, 'comment'))
desc = escape_xml_illegal_chars(desc)
cache.set(key, desc, 86400)
return desc
def item_pubdate(self, comment):
return comment.time
item_updateddate = item_pubdate
class AtomCommentFeed(CommentFeed):
feed_type = Atom1Feed
subtitle = CommentFeed.description
class BlogFeed(Feed):
title = 'Latest %s Blog Posts' % settings.SITE_NAME
link = '/'
description = 'The latest blog posts from the %s' % settings.SITE_LONG_NAME
def items(self):
return BlogPost.objects.filter(visible=True, publish_on__lte=timezone.now()).order_by('-sticky', '-publish_on')
def item_title(self, post):
return post.title
def item_description(self, post):
key = 'blog_feed:%d' % post.id
summary = cache.get(key)
if summary is None:
summary = str(markdown(post.summary or post.content, 'blog'))
summary = escape_xml_illegal_chars(summary)
cache.set(key, summary, 86400)
return summary
def item_pubdate(self, post):
return post.publish_on
item_updateddate = item_pubdate
class AtomBlogFeed(BlogFeed):
feed_type = Atom1Feed
subtitle = BlogFeed.description

View file

@ -8,7 +8,6 @@
"ip": "10.0.2.2", "ip": "10.0.2.2",
"language": 1, "language": 1,
"last_access": "2017-12-02T08:57:10.093Z", "last_access": "2017-12-02T08:57:10.093Z",
"math_engine": "auto",
"mute": false, "mute": false,
"organizations": [ "organizations": [
1 1
@ -18,8 +17,7 @@
"problem_count": 0, "problem_count": 0,
"rating": null, "rating": null,
"timezone": "America/Toronto", "timezone": "America/Toronto",
"user": 1, "user": 1
"user_script": ""
}, },
"model": "judge.profile", "model": "judge.profile",
"pk": 1 "pk": 1
@ -147,25 +145,8 @@
}, },
{ {
"fields": { "fields": {
"author": 1, "domain": "localhost:8000",
"body": "This is your first comment!", "name": "LQDOJ"
"hidden": false,
"level": 0,
"lft": 1,
"page": "b:1",
"parent": null,
"rght": 2,
"score": 0,
"time": "2017-12-02T08:46:54.007Z",
"tree_id": 1
},
"model": "judge.comment",
"pk": 1
},
{
"fields": {
"domain": "localhost:8081",
"name": "DMOJ: Modern Online Judge"
}, },
"model": "sites.site", "model": "sites.site",
"pk": 1 "pk": 1

View file

@ -1,170 +1,599 @@
import os
import secrets
from operator import attrgetter from operator import attrgetter
import pyotp import pyotp
import time
import datetime
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User
from django.contrib.auth.forms import AuthenticationForm from django.contrib.auth.forms import AuthenticationForm
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError, ObjectDoesNotExist
from django.core.validators import RegexValidator from django.core.validators import RegexValidator
from django.db.models import Q from django.db.models import Q
from django.forms import CharField, ChoiceField, Form, ModelForm from django.forms import (
from django.urls import reverse_lazy CharField,
ChoiceField,
Form,
ModelForm,
formset_factory,
BaseModelFormSet,
FileField,
)
from django.urls import reverse_lazy, reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.utils import timezone
from django_ace import AceWidget from django_ace import AceWidget
from judge.models import Contest, Language, Organization, PrivateMessage, Problem, ProblemPointsVote, Profile, Submission from judge.models import (
from judge.utils.subscription import newsletter_id Contest,
from judge.widgets import HeavyPreviewPageDownWidget, MathJaxPagedownWidget, PagedownWidget, Select2MultipleWidget, \ Language,
Select2Widget TestFormatterModel,
Organization,
PrivateMessage,
Problem,
ProblemPointsVote,
Profile,
Submission,
BlogPost,
ContestProblem,
TestFormatterModel,
ProfileInfo,
)
from judge.widgets import (
HeavyPreviewPageDownWidget,
PagedownWidget,
Select2MultipleWidget,
Select2Widget,
HeavySelect2MultipleWidget,
HeavySelect2Widget,
Select2MultipleWidget,
DateTimePickerWidget,
ImageWidget,
DatePickerWidget,
)
def fix_unicode(string, unsafe=tuple('\u202a\u202b\u202d\u202e')): def fix_unicode(string, unsafe=tuple("\u202a\u202b\u202d\u202e")):
return string + (sum(k in unsafe for k in string) - string.count('\u202c')) * '\u202c' return (
string + (sum(k in unsafe for k in string) - string.count("\u202c")) * "\u202c"
)
class UserForm(ModelForm):
class Meta:
model = User
fields = [
"first_name",
"last_name",
]
class ProfileInfoForm(ModelForm):
class Meta:
model = ProfileInfo
fields = ["tshirt_size", "date_of_birth", "address"]
widgets = {
"tshirt_size": Select2Widget(attrs={"style": "width:100%"}),
"date_of_birth": DatePickerWidget,
"address": forms.TextInput(attrs={"style": "width:100%"}),
}
class ProfileForm(ModelForm): class ProfileForm(ModelForm):
if newsletter_id is not None:
newsletter = forms.BooleanField(label=_('Subscribe to contest updates'), initial=False, required=False)
test_site = forms.BooleanField(label=_('Enable experimental features'), initial=False, required=False)
class Meta: class Meta:
model = Profile model = Profile
fields = ['about', 'organizations', 'timezone', 'language', 'ace_theme', 'user_script'] fields = [
"about",
"timezone",
"language",
"ace_theme",
"profile_image",
"css_background",
]
widgets = { widgets = {
'user_script': AceWidget(theme='github'), "timezone": Select2Widget(attrs={"style": "width:200px"}),
'timezone': Select2Widget(attrs={'style': 'width:200px'}), "language": Select2Widget(attrs={"style": "width:200px"}),
'language': Select2Widget(attrs={'style': 'width:200px'}), "ace_theme": Select2Widget(attrs={"style": "width:200px"}),
'ace_theme': Select2Widget(attrs={'style': 'width:200px'}), "profile_image": ImageWidget,
"css_background": forms.TextInput(),
} }
has_math_config = bool(settings.MATHOID_URL)
if has_math_config:
fields.append('math_engine')
widgets['math_engine'] = Select2Widget(attrs={'style': 'width:200px'})
if HeavyPreviewPageDownWidget is not None: if HeavyPreviewPageDownWidget is not None:
widgets['about'] = HeavyPreviewPageDownWidget( widgets["about"] = HeavyPreviewPageDownWidget(
preview=reverse_lazy('profile_preview'), preview=reverse_lazy("profile_preview"),
attrs={'style': 'max-width:700px;min-width:700px;width:700px'}, attrs={"style": "max-width:700px;min-width:700px;width:700px"},
) )
def clean(self):
organizations = self.cleaned_data.get('organizations') or []
max_orgs = settings.DMOJ_USER_MAX_ORGANIZATION_COUNT
if sum(org.is_open for org in organizations) > max_orgs:
raise ValidationError(
_('You may not be part of more than {count} public organizations.').format(count=max_orgs))
return self.cleaned_data
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
user = kwargs.pop('user', None) user = kwargs.pop("user", None)
super(ProfileForm, self).__init__(*args, **kwargs) super(ProfileForm, self).__init__(*args, **kwargs)
if not user.has_perm('judge.edit_all_organization'): self.fields["profile_image"].required = False
self.fields['organizations'].queryset = Organization.objects.filter(
Q(is_open=True) | Q(id__in=user.profile.organizations.all()), def clean_profile_image(self):
profile_image = self.cleaned_data.get("profile_image")
if profile_image:
if profile_image.size > 5 * 1024 * 1024:
raise ValidationError(
_("File size exceeds the maximum allowed limit of 5MB.")
) )
return profile_image
def file_size_validator(file):
limit = 10 * 1024 * 1024
if file.size > limit:
raise ValidationError("File too large. Size should not exceed 10MB.")
class ProblemSubmitForm(ModelForm): class ProblemSubmitForm(ModelForm):
source = CharField(max_length=65536, widget=AceWidget(theme='twilight', no_ace_media=True)) source = CharField(
max_length=65536, widget=AceWidget(theme="twilight", no_ace_media=True)
)
judge = ChoiceField(choices=(), widget=forms.HiddenInput(), required=False) judge = ChoiceField(choices=(), widget=forms.HiddenInput(), required=False)
source_file = FileField(required=False, validators=[file_size_validator])
def __init__(self, *args, judge_choices=(), **kwargs): def __init__(self, *args, judge_choices=(), request=None, problem=None, **kwargs):
super(ProblemSubmitForm, self).__init__(*args, **kwargs) super(ProblemSubmitForm, self).__init__(*args, **kwargs)
self.fields['language'].empty_label = None self.source_file_name = None
self.fields['language'].label_from_instance = attrgetter('display_name') self.request = request
self.fields['language'].queryset = Language.objects.filter(judges__online=True).distinct() self.problem = problem
self.fields["language"].empty_label = None
self.fields["language"].label_from_instance = attrgetter("display_name")
self.fields["language"].queryset = Language.objects.filter(
judges__online=True
).distinct()
if judge_choices: if judge_choices:
self.fields['judge'].widget = Select2Widget( self.fields["judge"].widget = Select2Widget(
attrs={'style': 'width: 150px', 'data-placeholder': _('Any judge')}, attrs={"style": "width: 150px", "data-placeholder": _("Any judge")},
) )
self.fields['judge'].choices = judge_choices self.fields["judge"].choices = judge_choices
def allow_url_as_source(self):
key = self.cleaned_data["language"].key
filename = self.files["source_file"].name
if key == "OUTPUT" and self.problem.data_files.output_only:
return filename.endswith(".zip")
if key == "SCAT":
return filename.endswith(".sb3")
return False
def clean(self):
if "source_file" in self.files:
if self.allow_url_as_source():
filename = self.files["source_file"].name
now = datetime.datetime.now()
timestamp = str(int(time.mktime(now.timetuple())))
self.source_file_name = (
timestamp + secrets.token_hex(5) + "." + filename.split(".")[-1]
)
filepath = os.path.join(
settings.DMOJ_SUBMISSION_ROOT, self.source_file_name
)
with open(filepath, "wb+") as destination:
for chunk in self.files["source_file"].chunks():
destination.write(chunk)
self.cleaned_data["source"] = self.request.build_absolute_uri(
reverse("submission_source_file", args=(self.source_file_name,))
)
del self.files["source_file"]
return self.cleaned_data
class Meta: class Meta:
model = Submission model = Submission
fields = ['language'] fields = ["language"]
class EditOrganizationForm(ModelForm): class EditOrganizationForm(ModelForm):
class Meta: class Meta:
model = Organization model = Organization
fields = ['about', 'logo_override_image', 'admins'] fields = [
widgets = {'admins': Select2MultipleWidget()} "name",
"slug",
"short_name",
"about",
"organization_image",
"admins",
"is_open",
]
widgets = {
"admins": Select2MultipleWidget(),
"organization_image": ImageWidget,
}
if HeavyPreviewPageDownWidget is not None: if HeavyPreviewPageDownWidget is not None:
widgets['about'] = HeavyPreviewPageDownWidget(preview=reverse_lazy('organization_preview')) widgets["about"] = HeavyPreviewPageDownWidget(
preview=reverse_lazy("organization_preview")
)
def __init__(self, *args, **kwargs):
super(EditOrganizationForm, self).__init__(*args, **kwargs)
self.fields["organization_image"].required = False
def clean_organization_image(self):
organization_image = self.cleaned_data.get("organization_image")
if organization_image:
if organization_image.size > 5 * 1024 * 1024:
raise ValidationError(
_("File size exceeds the maximum allowed limit of 5MB.")
)
return organization_image
class AddOrganizationForm(ModelForm):
class Meta:
model = Organization
fields = [
"name",
"slug",
"short_name",
"about",
"organization_image",
"is_open",
]
widgets = {}
if HeavyPreviewPageDownWidget is not None:
widgets["about"] = HeavyPreviewPageDownWidget(
preview=reverse_lazy("organization_preview")
)
def __init__(self, *args, **kwargs):
self.request = kwargs.pop("request", None)
super(AddOrganizationForm, self).__init__(*args, **kwargs)
self.fields["organization_image"].required = False
def save(self, commit=True):
res = super(AddOrganizationForm, self).save(commit=False)
res.registrant = self.request.profile
if commit:
res.save()
return res
class AddOrganizationContestForm(ModelForm):
def __init__(self, *args, **kwargs):
self.request = kwargs.pop("request", None)
super(AddOrganizationContestForm, self).__init__(*args, **kwargs)
def save(self, commit=True):
contest = super(AddOrganizationContestForm, self).save(commit=False)
old_save_m2m = self.save_m2m
def save_m2m():
for i, problem in enumerate(self.cleaned_data["problems"]):
contest_problem = ContestProblem(
contest=contest, problem=problem, points=100, order=i + 1
)
contest_problem.save()
contest.contest_problems.add(contest_problem)
old_save_m2m()
self.save_m2m = save_m2m
contest.save()
self.save_m2m()
return contest
class Meta:
model = Contest
fields = (
"key",
"name",
"start_time",
"end_time",
"problems",
)
widgets = {
"start_time": DateTimePickerWidget(),
"end_time": DateTimePickerWidget(),
"problems": HeavySelect2MultipleWidget(data_view="problem_select2"),
}
class EditOrganizationContestForm(ModelForm):
def __init__(self, *args, **kwargs):
self.org_id = kwargs.pop("org_id", 0)
super(EditOrganizationContestForm, self).__init__(*args, **kwargs)
for field in [
"authors",
"curators",
"testers",
"private_contestants",
"banned_users",
"view_contest_scoreboard",
]:
self.fields[field].widget.data_url = (
self.fields[field].widget.get_url() + f"?org_id={self.org_id}"
)
class Meta:
model = Contest
fields = (
"is_visible",
"key",
"name",
"start_time",
"end_time",
"format_name",
"authors",
"curators",
"testers",
"time_limit",
"freeze_after",
"use_clarifications",
"hide_problem_tags",
"public_scoreboard",
"scoreboard_visibility",
"points_precision",
"rate_limit",
"description",
"og_image",
"logo_override_image",
"summary",
"access_code",
"private_contestants",
"view_contest_scoreboard",
"banned_users",
)
widgets = {
"authors": HeavySelect2MultipleWidget(data_view="profile_select2"),
"curators": HeavySelect2MultipleWidget(data_view="profile_select2"),
"testers": HeavySelect2MultipleWidget(data_view="profile_select2"),
"private_contestants": HeavySelect2MultipleWidget(
data_view="profile_select2"
),
"banned_users": HeavySelect2MultipleWidget(data_view="profile_select2"),
"view_contest_scoreboard": HeavySelect2MultipleWidget(
data_view="profile_select2"
),
"organizations": HeavySelect2MultipleWidget(
data_view="organization_select2"
),
"tags": Select2MultipleWidget,
"description": HeavyPreviewPageDownWidget(
preview=reverse_lazy("contest_preview")
),
"start_time": DateTimePickerWidget(),
"end_time": DateTimePickerWidget(),
"format_name": Select2Widget(),
"scoreboard_visibility": Select2Widget(),
}
class AddOrganizationMemberForm(ModelForm):
new_users = CharField(
max_length=65536,
widget=forms.Textarea,
help_text=_("Enter usernames separating by space"),
label=_("New users"),
)
def clean_new_users(self):
new_users = self.cleaned_data.get("new_users") or ""
usernames = new_users.split()
invalid_usernames = []
valid_usernames = []
for username in usernames:
try:
valid_usernames.append(Profile.objects.get(user__username=username))
except ObjectDoesNotExist:
invalid_usernames.append(username)
if invalid_usernames:
raise ValidationError(
_("These usernames don't exist: {usernames}").format(
usernames=str(invalid_usernames)
)
)
return valid_usernames
class Meta:
model = Organization
fields = ()
class OrganizationBlogForm(ModelForm):
class Meta:
model = BlogPost
fields = ("title", "content", "publish_on")
widgets = {
"publish_on": forms.HiddenInput,
}
if HeavyPreviewPageDownWidget is not None:
widgets["content"] = HeavyPreviewPageDownWidget(
preview=reverse_lazy("organization_preview")
)
def __init__(self, *args, **kwargs):
super(OrganizationBlogForm, self).__init__(*args, **kwargs)
self.fields["publish_on"].required = False
self.fields["publish_on"].is_hidden = True
def clean(self):
self.cleaned_data["publish_on"] = timezone.now()
return self.cleaned_data
class OrganizationAdminBlogForm(OrganizationBlogForm):
class Meta:
model = BlogPost
fields = ("visible", "sticky", "title", "content", "publish_on")
widgets = {
"publish_on": forms.HiddenInput,
}
if HeavyPreviewPageDownWidget is not None:
widgets["content"] = HeavyPreviewPageDownWidget(
preview=reverse_lazy("organization_preview")
)
class NewMessageForm(ModelForm): class NewMessageForm(ModelForm):
class Meta: class Meta:
model = PrivateMessage model = PrivateMessage
fields = ['title', 'content'] fields = ["title", "content"]
widgets = {} widgets = {}
if PagedownWidget is not None: if PagedownWidget is not None:
widgets['content'] = MathJaxPagedownWidget() widgets["content"] = PagedownWidget()
class CustomAuthenticationForm(AuthenticationForm): class CustomAuthenticationForm(AuthenticationForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(CustomAuthenticationForm, self).__init__(*args, **kwargs) super(CustomAuthenticationForm, self).__init__(*args, **kwargs)
self.fields['username'].widget.attrs.update({'placeholder': _('Username')}) self.fields["username"].widget.attrs.update(
self.fields['password'].widget.attrs.update({'placeholder': _('Password')}) {"placeholder": _("Username/Email")}
)
self.fields["password"].widget.attrs.update({"placeholder": _("Password")})
self.has_google_auth = self._has_social_auth('GOOGLE_OAUTH2') self.has_google_auth = self._has_social_auth("GOOGLE_OAUTH2")
self.has_facebook_auth = self._has_social_auth('FACEBOOK') self.has_facebook_auth = self._has_social_auth("FACEBOOK")
self.has_github_auth = self._has_social_auth('GITHUB_SECURE') self.has_github_auth = self._has_social_auth("GITHUB_SECURE")
def _has_social_auth(self, key): def _has_social_auth(self, key):
return (getattr(settings, 'SOCIAL_AUTH_%s_KEY' % key, None) and return getattr(settings, "SOCIAL_AUTH_%s_KEY" % key, None) and getattr(
getattr(settings, 'SOCIAL_AUTH_%s_SECRET' % key, None)) settings, "SOCIAL_AUTH_%s_SECRET" % key, None
)
class NoAutoCompleteCharField(forms.CharField): class NoAutoCompleteCharField(forms.CharField):
def widget_attrs(self, widget): def widget_attrs(self, widget):
attrs = super(NoAutoCompleteCharField, self).widget_attrs(widget) attrs = super(NoAutoCompleteCharField, self).widget_attrs(widget)
attrs['autocomplete'] = 'off' attrs["autocomplete"] = "off"
return attrs return attrs
class TOTPForm(Form): class TOTPForm(Form):
TOLERANCE = settings.DMOJ_TOTP_TOLERANCE_HALF_MINUTES TOLERANCE = settings.DMOJ_TOTP_TOLERANCE_HALF_MINUTES
totp_token = NoAutoCompleteCharField(validators=[ totp_token = NoAutoCompleteCharField(
RegexValidator('^[0-9]{6}$', _('Two Factor Authentication tokens must be 6 decimal digits.')), validators=[
]) RegexValidator(
"^[0-9]{6}$",
_("Two Factor Authentication tokens must be 6 decimal digits."),
),
]
)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.totp_key = kwargs.pop('totp_key') self.totp_key = kwargs.pop("totp_key")
super(TOTPForm, self).__init__(*args, **kwargs) super(TOTPForm, self).__init__(*args, **kwargs)
def clean_totp_token(self): def clean_totp_token(self):
if not pyotp.TOTP(self.totp_key).verify(self.cleaned_data['totp_token'], valid_window=self.TOLERANCE): if not pyotp.TOTP(self.totp_key).verify(
raise ValidationError(_('Invalid Two Factor Authentication token.')) self.cleaned_data["totp_token"], valid_window=self.TOLERANCE
):
raise ValidationError(_("Invalid Two Factor Authentication token."))
class ProblemCloneForm(Form): class ProblemCloneForm(Form):
code = CharField(max_length=20, validators=[RegexValidator('^[a-z0-9]+$', _('Problem code must be ^[a-z0-9]+$'))]) code = CharField(
max_length=20,
validators=[
RegexValidator("^[a-z0-9]+$", _("Problem code must be ^[a-z0-9]+$"))
],
)
def clean_code(self): def clean_code(self):
code = self.cleaned_data['code'] code = self.cleaned_data["code"]
if Problem.objects.filter(code=code).exists(): if Problem.objects.filter(code=code).exists():
raise ValidationError(_('Problem with code already exists.')) raise ValidationError(_("Problem with code already exists."))
return code return code
class ContestCloneForm(Form): class ContestCloneForm(Form):
key = CharField(max_length=20, validators=[RegexValidator('^[a-z0-9]+$', _('Contest id must be ^[a-z0-9]+$'))]) key = CharField(
max_length=20,
validators=[RegexValidator("^[a-z0-9]+$", _("Contest id must be ^[a-z0-9]+$"))],
)
organization = ChoiceField(choices=(), required=True)
def __init__(self, *args, org_choices=(), profile=None, **kwargs):
super(ContestCloneForm, self).__init__(*args, **kwargs)
self.fields["organization"].widget = Select2Widget(
attrs={"style": "width: 100%", "data-placeholder": _("Group")},
)
self.fields["organization"].choices = org_choices
self.profile = profile
def clean_key(self): def clean_key(self):
key = self.cleaned_data['key'] key = self.cleaned_data["key"]
if Contest.objects.filter(key=key).exists(): if Contest.objects.filter(key=key).exists():
raise ValidationError(_('Contest with key already exists.')) raise ValidationError(_("Contest with key already exists."))
return key return key
def clean_organization(self):
organization_id = self.cleaned_data["organization"]
try:
organization = Organization.objects.get(id=organization_id)
except Exception:
raise ValidationError(_("Group doesn't exist."))
if not organization.admins.filter(id=self.profile.id).exists():
raise ValidationError(_("You don't have permission in this group."))
return organization
class ProblemPointsVoteForm(ModelForm): class ProblemPointsVoteForm(ModelForm):
class Meta: class Meta:
model = ProblemPointsVote model = ProblemPointsVote
fields = ['points'] fields = ["points"]
class ContestProblemForm(ModelForm):
class Meta:
model = ContestProblem
fields = (
"order",
"problem",
"points",
"partial",
"show_testcases",
"max_submissions",
)
widgets = {
"problem": HeavySelect2Widget(
data_view="problem_select2", attrs={"style": "width: 100%"}
),
}
class ContestProblemModelFormSet(BaseModelFormSet):
def is_valid(self):
valid = super().is_valid()
if not valid:
return valid
problems = set()
duplicates = []
for form in self.forms:
if form.cleaned_data and not form.cleaned_data.get("DELETE", False):
problem = form.cleaned_data.get("problem")
if problem in problems:
duplicates.append(problem)
else:
problems.add(problem)
if duplicates:
for form in self.forms:
problem = form.cleaned_data.get("problem")
if problem in duplicates:
form.add_error("problem", _("This problem is duplicated."))
return False
return True
class ContestProblemFormSet(
formset_factory(
ContestProblemForm, formset=ContestProblemModelFormSet, extra=6, can_delete=True
)
):
model = ContestProblem
class TestFormatterForm(ModelForm):
class Meta:
model = TestFormatterModel
fields = ["file"]

View file

@ -5,10 +5,10 @@ from django.db.models.query import QuerySet
class SearchQuerySet(QuerySet): class SearchQuerySet(QuerySet):
DEFAULT = '' DEFAULT = ""
BOOLEAN = ' IN BOOLEAN MODE' BOOLEAN = " IN BOOLEAN MODE"
NATURAL_LANGUAGE = ' IN NATURAL LANGUAGE MODE' NATURAL_LANGUAGE = " IN NATURAL LANGUAGE MODE"
QUERY_EXPANSION = ' WITH QUERY EXPANSION' QUERY_EXPANSION = " WITH QUERY EXPANSION"
def __init__(self, fields=None, **kwargs): def __init__(self, fields=None, **kwargs):
super(SearchQuerySet, self).__init__(**kwargs) super(SearchQuerySet, self).__init__(**kwargs)
@ -25,20 +25,26 @@ class SearchQuerySet(QuerySet):
# Get the table name and column names from the model # Get the table name and column names from the model
# in `table_name`.`column_name` style # in `table_name`.`column_name` style
columns = [meta.get_field(name).column for name in self._search_fields] columns = [meta.get_field(name).column for name in self._search_fields]
full_names = ['%s.%s' % full_names = [
(connection.ops.quote_name(meta.db_table), "%s.%s"
connection.ops.quote_name(column)) % (
for column in columns] connection.ops.quote_name(meta.db_table),
connection.ops.quote_name(column),
)
for column in columns
]
# Create the MATCH...AGAINST expressions # Create the MATCH...AGAINST expressions
fulltext_columns = ', '.join(full_names) fulltext_columns = ", ".join(full_names)
match_expr = ('MATCH(%s) AGAINST (%%s%s)' % (fulltext_columns, mode)) match_expr = "MATCH(%s) AGAINST (%%s%s)" % (fulltext_columns, mode)
# Add the extra SELECT and WHERE options # Add the extra SELECT and WHERE options
return self.extra(select={'relevance': match_expr}, return self.extra(
select={"relevance": match_expr},
select_params=[query], select_params=[query],
where=[match_expr], where=[match_expr],
params=[query]) params=[query],
)
class SearchManager(models.Manager): class SearchManager(models.Manager):

View file

@ -1,38 +1,13 @@
from django.utils.html import escape, mark_safe from django.utils.html import escape, mark_safe
from judge.markdown import markdown
__all__ = ['highlight_code'] __all__ = ["highlight_code"]
def _make_pre_code(code): def highlight_code(code, language, linenos=True, title=None):
return mark_safe('<pre>' + escape(code) + '</pre>') linenos_option = 'linenums="1"' if linenos else ""
title_option = f'title="{title}"' if title else ""
options = f"{{.{language} {linenos_option} {title_option}}}"
value = f"```{options}\n{code}\n```\n"
def _wrap_code(inner): return mark_safe(markdown(value))
yield 0, "<code>"
for tup in inner:
yield tup
yield 0, "</code>"
try:
import pygments
import pygments.lexers
import pygments.formatters.html
import pygments.util
except ImportError:
def highlight_code(code, language, cssclass=None):
return _make_pre_code(code)
else:
class HtmlCodeFormatter(pygments.formatters.HtmlFormatter):
def wrap(self, source, outfile):
return self._wrap_div(self._wrap_pre(_wrap_code(source)))
def highlight_code(code, language, cssclass='codehilite', linenos=True):
try:
lexer = pygments.lexers.get_lexer_by_name(language)
except pygments.util.ClassNotFound:
return _make_pre_code(code)
if linenos:
return mark_safe(pygments.highlight(code, lexer, HtmlCodeFormatter(cssclass=cssclass, linenos='table')))
return mark_safe(pygments.highlight(code, lexer, HtmlCodeFormatter(cssclass=cssclass)))

View file

@ -8,19 +8,33 @@ from statici18n.templatetags.statici18n import inlinei18n
from judge.highlight_code import highlight_code from judge.highlight_code import highlight_code
from judge.user_translations import gettext from judge.user_translations import gettext
from . import (camo, chat, datetime, filesize, gravatar, language, markdown, rating, reference, render, social, from . import (
spaceless, submission, timedelta) camo,
chat,
datetime,
filesize,
gravatar,
language,
markdown,
rating,
reference,
render,
social,
spaceless,
timedelta,
comment,
)
from . import registry from . import registry
registry.function('str', str) registry.function("str", str)
registry.filter('str', str) registry.filter("str", str)
registry.filter('json', json.dumps) registry.filter("json", json.dumps)
registry.filter('highlight', highlight_code) registry.filter("highlight", highlight_code)
registry.filter('urlquote', urlquote) registry.filter("urlquote", urlquote)
registry.filter('roundfloat', round) registry.filter("roundfloat", round)
registry.function('inlinei18n', inlinei18n) registry.function("inlinei18n", inlinei18n)
registry.function('mptt_tree', get_cached_trees) registry.function("mptt_tree", get_cached_trees)
registry.function('user_trans', gettext) registry.function("user_trans", gettext)
@registry.function @registry.function

View file

@ -1,6 +1,7 @@
from judge.utils.camo import client as camo_client from judge.utils.camo import client as camo_client
from . import registry from . import registry
@registry.filter @registry.filter
def camo(url): def camo(url):
if camo_client is None: if camo_client is None:

View file

@ -1,6 +1,7 @@
from . import registry from . import registry
from chat_box.utils import encrypt_url from chat_box.utils import encrypt_url
@registry.function @registry.function
def chat_param(request_profile, profile): def chat_param(request_profile, profile):
return encrypt_url(request_profile.id, profile.id) return encrypt_url(request_profile.id, profile.id)

12
judge/jinja2/comment.py Normal file
View file

@ -0,0 +1,12 @@
from . import registry
from django.contrib.contenttypes.models import ContentType
from judge.models.comment import get_visible_comment_count
from judge.caching import cache_wrapper
@registry.function
def comment_count(obj):
content_type = ContentType.objects.get_for_model(obj)
return get_visible_comment_count(content_type, obj.pk)

View file

@ -10,7 +10,7 @@ from . import registry
def localtime_wrapper(func): def localtime_wrapper(func):
@functools.wraps(func) @functools.wraps(func)
def wrapper(datetime, *args, **kwargs): def wrapper(datetime, *args, **kwargs):
if getattr(datetime, 'convert_to_local_time', True): if getattr(datetime, "convert_to_local_time", True):
datetime = localtime(datetime) datetime = localtime(datetime)
return func(datetime, *args, **kwargs) return func(datetime, *args, **kwargs)
@ -22,6 +22,6 @@ registry.filter(localtime_wrapper(time))
@registry.function @registry.function
@registry.render_with('widgets/relative-time.html') @registry.render_with("widgets/relative-time.html")
def relative_time(time, format=_('N j, Y, g:i a'), rel=_('{time}'), abs=_('on {time}')): def relative_time(time, format=_("N j, Y, g:i a"), rel=_("{time}"), abs=_("{time}")):
return {'time': time, 'format': format, 'rel_format': rel, 'abs_format': abs} return {"time": time, "format": format, "rel_format": rel, "abs_format": abs}

View file

@ -13,24 +13,28 @@ def _format_size(bytes, callback):
PB = 1 << 50 PB = 1 << 50
if bytes < KB: if bytes < KB:
return callback('', bytes) return callback("", bytes)
elif bytes < MB: elif bytes < MB:
return callback('K', bytes / KB) return callback("K", bytes / KB)
elif bytes < GB: elif bytes < GB:
return callback('M', bytes / MB) return callback("M", bytes / MB)
elif bytes < TB: elif bytes < TB:
return callback('G', bytes / GB) return callback("G", bytes / GB)
elif bytes < PB: elif bytes < PB:
return callback('T', bytes / TB) return callback("T", bytes / TB)
else: else:
return callback('P', bytes / PB) return callback("P", bytes / PB)
@registry.filter @registry.filter
def kbdetailformat(bytes): def kbdetailformat(bytes):
return avoid_wrapping(_format_size(bytes * 1024, lambda x, y: ['%d %sB', '%.2f %sB'][bool(x)] % (y, x))) return avoid_wrapping(
_format_size(
bytes * 1024, lambda x, y: ["%d %sB", "%.2f %sB"][bool(x)] % (y, x)
)
)
@registry.filter @registry.filter
def kbsimpleformat(kb): def kbsimpleformat(kb):
return _format_size(kb * 1024, lambda x, y: '%.0f%s' % (y, x or 'B')) return _format_size(kb * 1024, lambda x, y: "%.0f%s" % (y, x or "B"))

View file

@ -9,17 +9,23 @@ from . import registry
@registry.function @registry.function
def gravatar(email, size=80, default=None): def gravatar(profile, size=80, default=None, profile_image=None, email=None):
if isinstance(email, Profile): if profile and not profile.is_muted:
if profile_image:
return profile_image
if profile and profile.profile_image_url:
return profile.profile_image_url
if profile:
email = email or profile.email
if default is None: if default is None:
default = email.mute default = profile.is_muted
email = email.user.email gravatar_url = (
elif isinstance(email, AbstractUser): "//www.gravatar.com/avatar/"
email = email.email + hashlib.md5(utf8bytes(email.strip().lower())).hexdigest()
+ "?"
gravatar_url = '//www.gravatar.com/avatar/' + hashlib.md5(utf8bytes(email.strip().lower())).hexdigest() + '?' )
args = {'d': 'identicon', 's': str(size)} args = {"d": "identicon", "s": str(size)}
if default: if default:
args['f'] = 'y' args["f"] = "y"
gravatar_url += urlencode(args) gravatar_url += urlencode(args)
return gravatar_url return gravatar_url

View file

@ -3,7 +3,7 @@ from django.utils import translation
from . import registry from . import registry
@registry.function('language_info') @registry.function("language_info")
def get_language_info(language): def get_language_info(language):
# ``language`` is either a language code string or a sequence # ``language`` is either a language code string or a sequence
# with the language code as its first item # with the language code as its first item
@ -13,6 +13,6 @@ def get_language_info(language):
return translation.get_language_info(str(language)) return translation.get_language_info(str(language))
@registry.function('language_info_list') @registry.function("language_info_list")
def get_language_info_list(langs): def get_language_info_list(langs):
return [get_language_info(lang) for lang in langs] return [get_language_info(lang) for lang in langs]

View file

@ -1,142 +1,7 @@
import logging
import re
from html.parser import HTMLParser
from urllib.parse import urlparse
import mistune
from django.conf import settings
from jinja2 import Markup
from lxml import html
from lxml.etree import ParserError, XMLSyntaxError
from judge.highlight_code import highlight_code
from judge.jinja2.markdown.lazy_load import lazy_load as lazy_load_processor
from judge.jinja2.markdown.math import MathInlineGrammar, MathInlineLexer, MathRenderer
from judge.jinja2.markdown.spoiler import SpoilerInlineGrammar, SpoilerInlineLexer, SpoilerRenderer
from judge.utils.camo import client as camo_client
from judge.utils.texoid import TEXOID_ENABLED, TexoidRenderer
from .. import registry from .. import registry
from judge.markdown import markdown as _markdown
logger = logging.getLogger('judge.html')
NOFOLLOW_WHITELIST = settings.NOFOLLOW_EXCLUDED
class CodeSafeInlineGrammar(mistune.InlineGrammar):
double_emphasis = re.compile(r'^\*{2}([\s\S]+?)()\*{2}(?!\*)') # **word**
emphasis = re.compile(r'^\*((?:\*\*|[^\*])+?)()\*(?!\*)') # *word*
class AwesomeInlineGrammar(MathInlineGrammar, SpoilerInlineGrammar, CodeSafeInlineGrammar):
pass
class AwesomeInlineLexer(MathInlineLexer, SpoilerInlineLexer, mistune.InlineLexer):
grammar_class = AwesomeInlineGrammar
class AwesomeRenderer(MathRenderer, SpoilerRenderer, mistune.Renderer):
def __init__(self, *args, **kwargs):
self.nofollow = kwargs.pop('nofollow', True)
self.texoid = TexoidRenderer() if kwargs.pop('texoid', False) else None
self.parser = HTMLParser()
super(AwesomeRenderer, self).__init__(*args, **kwargs)
def _link_rel(self, href):
if href:
try:
url = urlparse(href)
except ValueError:
return ' rel="nofollow"'
else:
if url.netloc and url.netloc not in NOFOLLOW_WHITELIST:
return ' rel="nofollow"'
return ''
def autolink(self, link, is_email=False):
text = link = mistune.escape(link)
if is_email:
link = 'mailto:%s' % link
return '<a href="%s"%s>%s</a>' % (link, self._link_rel(link), text)
def table(self, header, body):
return (
'<table class="table">\n<thead>%s</thead>\n'
'<tbody>\n%s</tbody>\n</table>\n'
) % (header, body)
def link(self, link, title, text):
link = mistune.escape_link(link)
if not title:
return '<a href="%s"%s>%s</a>' % (link, self._link_rel(link), text)
title = mistune.escape(title, quote=True)
return '<a href="%s" title="%s"%s>%s</a>' % (link, title, self._link_rel(link), text)
def block_code(self, code, lang=None):
if not lang:
return '\n<pre><code>%s</code></pre>\n' % mistune.escape(code).rstrip()
return highlight_code(code, lang)
def block_html(self, html):
if self.texoid and html.startswith('<latex'):
attr = html[6:html.index('>')]
latex = html[html.index('>') + 1:html.rindex('<')]
latex = self.parser.unescape(latex)
result = self.texoid.get_result(latex)
if not result:
return '<pre>%s</pre>' % mistune.escape(latex, smart_amp=False)
elif 'error' not in result:
img = ('''<img src="%(svg)s" onerror="this.src='%(png)s';this.onerror=null"'''
'width="%(width)s" height="%(height)s"%(tail)s>') % {
'svg': result['svg'], 'png': result['png'],
'width': result['meta']['width'], 'height': result['meta']['height'],
'tail': ' /' if self.options.get('use_xhtml') else '',
}
style = ['max-width: 100%',
'height: %s' % result['meta']['height'],
'max-height: %s' % result['meta']['height'],
'width: %s' % result['meta']['height']]
if 'inline' in attr:
tag = 'span'
else:
tag = 'div'
style += ['text-align: center']
return '<%s style="%s">%s</%s>' % (tag, ';'.join(style), img, tag)
else:
return '<pre>%s</pre>' % mistune.escape(result['error'], smart_amp=False)
return super(AwesomeRenderer, self).block_html(html)
def header(self, text, level, *args, **kwargs):
return super(AwesomeRenderer, self).header(text, level + 2, *args, **kwargs)
@registry.filter @registry.filter
def markdown(value, style, math_engine=None, lazy_load=False): def markdown(value, lazy_load=False):
styles = settings.MARKDOWN_STYLES.get(style, settings.MARKDOWN_DEFAULT_STYLE) return _markdown(value, lazy_load)
escape = styles.get('safe_mode', True)
nofollow = styles.get('nofollow', True)
texoid = TEXOID_ENABLED and styles.get('texoid', False)
math = hasattr(settings, 'MATHOID_URL') and styles.get('math', False)
post_processors = []
if styles.get('use_camo', False) and camo_client is not None:
post_processors.append(camo_client.update_tree)
if lazy_load:
post_processors.append(lazy_load_processor)
renderer = AwesomeRenderer(escape=escape, nofollow=nofollow, texoid=texoid,
math=math and math_engine is not None, math_engine=math_engine)
markdown = mistune.Markdown(renderer=renderer, inline=AwesomeInlineLexer,
parse_block_html=1, parse_inline_html=1)
result = markdown(value)
if post_processors:
try:
tree = html.fromstring(result, parser=html.HTMLParser(recover=True))
except (XMLSyntaxError, ParserError) as e:
if result and (not isinstance(e, ParserError) or e.args[0] != 'Document is empty'):
logger.exception('Failed to parse HTML string')
tree = html.Element('div')
for processor in post_processors:
processor(tree)
result = html.tostring(tree, encoding='unicode')
return Markup(result)

View file

@ -1,20 +0,0 @@
from copy import deepcopy
from django.templatetags.static import static
from lxml import html
def lazy_load(tree):
blank = static('blank.gif')
for img in tree.xpath('.//img'):
src = img.get('src', '')
if src.startswith('data') or '-math' in img.get('class', ''):
continue
noscript = html.Element('noscript')
copy = deepcopy(img)
copy.tail = ''
noscript.append(copy)
img.addprevious(noscript)
img.set('data-src', src)
img.set('src', blank)
img.set('class', img.get('class') + ' unveil' if img.get('class') else 'unveil')

View file

@ -1,67 +0,0 @@
import re
import mistune
from django.conf import settings
from judge.utils.mathoid import MathoidMathParser
mistune._pre_tags.append('latex')
class MathInlineGrammar(mistune.InlineGrammar):
block_math = re.compile(r'^\$\$(.*?)\$\$|^\\\[(.*?)\\\]', re.DOTALL)
math = re.compile(r'^~(.*?)~|^\\\((.*?)\\\)', re.DOTALL)
text = re.compile(r'^[\s\S]+?(?=[\\<!\[_*`~$]|\\[\[(]|https?://| {2,}\n|$)')
class MathInlineLexer(mistune.InlineLexer):
grammar_class = MathInlineGrammar
def __init__(self, *args, **kwargs):
self.default_rules = self.default_rules[:]
self.inline_html_rules = self.default_rules
self.default_rules.insert(self.default_rules.index('strikethrough') + 1, 'math')
self.default_rules.insert(self.default_rules.index('strikethrough') + 1, 'block_math')
super(MathInlineLexer, self).__init__(*args, **kwargs)
def output_block_math(self, m):
return self.renderer.block_math(m.group(1) or m.group(2))
def output_math(self, m):
return self.renderer.math(m.group(1) or m.group(2))
def output_inline_html(self, m):
tag = m.group(1)
text = m.group(3)
if self._parse_inline_html and text:
if tag == 'a':
self._in_link = True
text = self.output(text)
self._in_link = False
else:
text = self.output(text)
extra = m.group(2) or ''
html = '<%s%s>%s</%s>' % (tag, extra, text, tag)
else:
html = m.group(0)
return self.renderer.inline_html(html)
class MathRenderer(mistune.Renderer):
def __init__(self, *args, **kwargs):
if kwargs.pop('math', False) and settings.MATHOID_URL != False:
self.mathoid = MathoidMathParser(kwargs.pop('math_engine', None) or 'svg')
else:
self.mathoid = None
super(MathRenderer, self).__init__(*args, **kwargs)
def block_math(self, math):
if self.mathoid is None or not math:
return r'\[%s\]' % mistune.escape(str(math))
return self.mathoid.display_math(math)
def math(self, math):
if self.mathoid is None or not math:
return r'\(%s\)' % mistune.escape(str(math))
return self.mathoid.inline_math(math)

Some files were not shown because too many files have changed in this diff Show more