Compare commits

...

389 commits

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
2483 changed files with 39714 additions and 17401 deletions

View file

@ -11,3 +11,7 @@ repos:
rev: 22.12.0 rev: 22.12.0
hooks: hooks:
- id: black - 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>

191
README.md
View file

@ -31,6 +31,197 @@ 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`.
- Bước 1: cài các thư viện cần thiết
- $ ở đâ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:
1. Missing the `local_settings.py`. You need to copy the `local_settings.py` in order to pass the check. 1. Missing the `local_settings.py`. You need to copy the `local_settings.py` in order to pass the check.

View file

@ -3,3 +3,6 @@ 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

@ -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,9 +1,11 @@
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", "Room", "UserRoom", "Ignore"] __all__ = ["Message", "Room", "UserRoom", "Ignore"]
@ -16,20 +18,44 @@ class Room(models.Model):
user_two = models.ForeignKey( user_two = models.ForeignKey(
Profile, related_name="user_two", verbose_name="user 2", on_delete=CASCADE 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(
verbose_name=_("posted time"), auto_now_add=True, db_index=True
)
body = models.TextField(verbose_name=_("body of comment"), max_length=8192) body = models.TextField(verbose_name=_("body of comment"), max_length=8192)
hidden = models.BooleanField(verbose_name="is hidden", default=False) hidden = models.BooleanField(verbose_name="is hidden", default=False)
room = models.ForeignKey( room = models.ForeignKey(
@ -37,7 +63,6 @@ class Message(models.Model):
) )
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)
@ -48,6 +73,7 @@ class Message(models.Model):
indexes = [ indexes = [
models.Index(fields=["hidden", "room", "-id"]), models.Index(fields=["hidden", "room", "-id"]),
] ]
app_label = "chat_box"
class UserRoom(models.Model): class UserRoom(models.Model):
@ -56,9 +82,11 @@ class UserRoom(models.Model):
Room, verbose_name="room id", on_delete=CASCADE, default=None, null=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) last_seen = models.DateTimeField(verbose_name=_("last seen"), auto_now_add=True)
unread_count = models.IntegerField(default=0, db_index=True)
class Meta: class Meta:
unique_together = ("user", "room") unique_together = ("user", "room")
app_label = "chat_box"
class Ignore(models.Model): class Ignore(models.Model):
@ -71,14 +99,15 @@ class Ignore(models.Model):
) )
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 ( return current_user.ignored_chat_users.ignored_users.filter(
current_user.ignored_chat_users.get() id=new_friend.id
.ignored_users.filter(id=new_friend.id) ).exists()
.exists()
)
except: except:
return False return False
@ -89,6 +118,16 @@ class Ignore(models.Model):
except: except:
return Profile.objects.none() return Profile.objects.none()
@classmethod
def get_ignored_rooms(self, user):
try:
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 @classmethod
def add_ignore(self, current_user, friend): def add_ignore(self, current_user, friend):
ignore, created = self.objects.get_or_create(user=current_user) ignore, created = self.objects.get_or_create(user=current_user)
@ -105,3 +144,11 @@ class Ignore(models.Model):
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,10 +1,14 @@
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 from django.db.models import OuterRef, Count, Subquery, IntegerField, Q
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce
from chat_box.models import Ignore, Message, UserRoom 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)
@ -24,25 +28,23 @@ def decrypt_url(message_encrypted):
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): def get_unread_boxes(profile):
ignored_users = Ignore.get_ignored_users(profile) ignored_rooms = Ignore.get_ignored_rooms(profile)
mess = (
Message.objects.filter(room=OuterRef("room"), time__gte=OuterRef("last_seen"))
.exclude(author=profile)
.exclude(author__in=ignored_users)
.order_by()
.values("room")
.annotate(unread_count=Count("pk"))
.values("unread_count")
)
unread_boxes = ( unread_boxes = (
UserRoom.objects.filter(user=profile, room__isnull=False) UserRoom.objects.filter(user=profile, unread_count__gt=0)
.annotate( .exclude(room__in=ignored_rooms)
unread_count=Coalesce(Subquery(mess, output_field=IntegerField()), 0),
)
.filter(unread_count__gte=1)
.count() .count()
) )

View file

@ -21,22 +21,23 @@ from django.db.models import (
Exists, Exists,
Count, Count,
IntegerField, 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):
@ -49,7 +50,8 @@ class ChatView(ListView):
self.room_id = None self.room_id = None
self.room = None self.room = None
self.messages = None self.messages = None
self.page_size = 20 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
@ -63,10 +65,12 @@ class ChatView(ListView):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
request_room = kwargs["room_id"] request_room = kwargs["room_id"]
page_size = self.follow_up_page_size
try: try:
last_id = int(request.GET.get("last_id")) last_id = int(request.GET.get("last_id"))
except Exception: except Exception:
last_id = 1e15 last_id = 1e15
page_size = self.first_page_size
only_messages = request.GET.get("only_messages") only_messages = request.GET.get("only_messages")
if request_room: if request_room:
@ -80,11 +84,12 @@ class ChatView(ListView):
request_room = None request_room = None
self.room_id = request_room self.room_id = request_room
self.messages = Message.objects.filter( self.messages = (
hidden=False, room=self.room_id, id__lt=last_id Message.objects.filter(hidden=False, room=self.room_id, id__lt=last_id)
)[: self.page_size] .select_related("author")
.only("body", "time", "author__rating", "author__display_rank")[:page_size]
)
if not only_messages: if not only_messages:
update_last_seen(request, **kwargs)
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
return render( return render(
@ -101,10 +106,14 @@ class ChatView(ListView):
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["has_next"] = self.has_next() context["has_next"] = self.has_next()
context["unread_count_lobby"] = get_unread_count(None, self.request.profile) 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)
@ -130,15 +139,15 @@ def delete_message(request):
if request.method == "GET": if request.method == "GET":
return HttpResponseBadRequest() return HttpResponseBadRequest()
if not request.user.is_staff:
return HttpResponseBadRequest()
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()
@ -160,26 +169,57 @@ def mute_message(request):
except: except:
return HttpResponseBadRequest() return HttpResponseBadRequest()
with revisions.create_revision():
revisions.set_comment(_("Mute chat") + ": " + mess.body)
revisions.set_user(request.user)
mess.author.mute = True mess.author.mute = True
mess.author.save() mess.author.save()
Message.objects.filter(room=None, author=mess.author).update(hidden=True) 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() return HttpResponseBadRequest()
if len(request.POST["body"]) > 5000: 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, body=request.POST["body"], room=room) new_message = Message(author=request.profile, body=request.POST["body"], room=room)
@ -187,7 +227,7 @@ def post_message(request):
if not room: if not room:
event.post( event.post(
"chat_lobby", encrypt_channel("chat_lobby"),
{ {
"type": "lobby", "type": "lobby",
"author_id": request.profile.id, "author_id": request.profile.id,
@ -197,9 +237,13 @@ def post_message(request):
}, },
) )
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( event.post(
"chat_" + str(user.id), encrypt_channel("chat_" + str(user.id)),
{ {
"type": "private", "type": "private",
"author_id": request.profile.id, "author_id": request.profile.id,
@ -208,14 +252,17 @@ def post_message(request):
"tmp_id": request.POST.get("tmp_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 ( return not room or room.contain(request.profile)
not room or room.user_one == request.profile or room.user_two == request.profile
)
@login_required @login_required
@ -231,7 +278,7 @@ def chat_message_ajax(request):
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()
@ -254,35 +301,35 @@ def update_last_seen(request, **kwargs):
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()
get_unread_boxes.dirty(profile)
return JsonResponse({"msg": "updated"}) 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
@ -319,47 +366,66 @@ def user_online_status_ajax(request):
) )
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 = ( recent_profile = (
Room.objects.filter(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",
), ),
) )
@ -369,50 +435,24 @@ def get_status_context(request, include_ignored=False):
.values("other_user", "id")[:20] .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]
recent_list = None Room.prefetch_room_cache(recent_rooms)
if joined_id:
recent_list = Profile.objects.raw(
f"SELECT * from judge_profile where id in ({joined_id}) order by field(id,{joined_id})"
)
friend_list = (
Friend.get_friend_profiles(request.profile)
.exclude(id__in=recent_profile_id)
.exclude(id__in=ignored_users)
.order_by("-last_access")
)
admin_list = ( admin_list = (
queryset.filter(display_rank="admin") queryset.filter(display_rank="admin")
.exclude(id__in=friend_list) .exclude(id__in=recent_profile_ids)
.exclude(id__in=recent_profile_id) .values_list("id", flat=True)
)
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),
}, },
] ]
@ -423,7 +463,7 @@ def online_status_ajax(request):
request, request,
"chat/online_status.html", "chat/online_status.html",
{ {
"status_sections": get_status_context(request), "status_sections": get_status_context(request.profile),
"unread_count_lobby": get_unread_count(None, request.profile), "unread_count_lobby": get_unread_count(None, request.profile),
}, },
) )
@ -447,7 +487,6 @@ def get_or_create_room(request):
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()
@ -468,55 +507,36 @@ def get_or_create_room(request):
user_room.last_seen = timezone.now() user_room.last_seen = timezone.now()
user_room.save() user_room.save()
room_url = reverse("chat", kwargs={"room_id": room.id})
if request.method == "GET": if request.method == "GET":
return JsonResponse({"room": room.id, "other_user_id": other_user.id}) return JsonResponse(
return HttpResponseRedirect(reverse("chat", kwargs={"room_id": room.id})) {
"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 = ( return UserRoom.objects.filter(
Message.objects.filter( user=user, room__in=rooms, unread_count__gt=0
room=OuterRef("room"), time__gte=OuterRef("last_seen") ).values("unread_count", "room")
)
.exclude(author=user)
.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 = ( user_room = UserRoom.objects.filter(user=user, room__isnull=True).first()
Message.objects.filter(room__isnull=True, time__gte=OuterRef("last_seen")) if not user_room:
.exclude(author=user) return 0
.order_by() last_seen = user_room.last_seen
.values("room")
.annotate(unread_count=Count("pk"))
.values("unread_count")
)
res = ( res = (
UserRoom.objects.filter(user=user, room__isnull=True) Message.objects.filter(room__isnull=True, time__gte=last_seen)
.annotate( .exclude(author=user)
unread_count=Coalesce(Subquery(mess, output_field=IntegerField()), 0), .exclude(hidden=True)
) .count()
.values_list("unread_count", flat=True)
) )
return res[0] if len(res) else 0 return res
@login_required @login_required

View file

@ -66,7 +66,7 @@ class AceWidget(forms.Textarea):
if self.toolbar: if self.toolbar:
toolbar = ( toolbar = (
'<div style="width: {}" class="django-ace-toolbar">' '<div style="width: {}" class="django-ace-toolbar">'
'<a href="./" class="django-ace-max_min"></a>' '<a href="#" class="django-ace-max_min"></a>'
"</div>" "</div>"
).format(self.width) ).format(self.width)
html = toolbar + html html = toolbar + html

View file

@ -33,6 +33,7 @@ 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
@ -83,6 +84,9 @@ DMOJ_STATS_SUBMISSION_RESULT_COLORS = {
"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 = {}
@ -128,13 +132,10 @@ 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 = ( FONTAWESOME_CSS = "//cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css"
"//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css"
)
DMOJ_CANONICAL = "" DMOJ_CANONICAL = ""
# Application definition # Application definition
@ -168,7 +169,7 @@ else:
}, },
{ {
"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",
@ -276,8 +277,11 @@ LANGUAGE_COOKIE_AGE = 8640000
FORM_RENDERER = "django.forms.renderers.TemplatesSetting" FORM_RENDERER = "django.forms.renderers.TemplatesSetting"
IMPERSONATE_REQUIRE_SUPERUSER = True IMPERSONATE = {
IMPERSONATE_DISABLE_LOGGING = True "REQUIRE_SUPERUSER": True,
"DISABLE_LOGGING": True,
"ADMIN_DELETE_PERMISSION": True,
}
ACCOUNT_ACTIVATION_DAYS = 7 ACCOUNT_ACTIVATION_DAYS = 7
@ -321,7 +325,6 @@ TEMPLATES = [
"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",
], ],
@ -381,6 +384,7 @@ 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
@ -428,7 +432,7 @@ 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 = (
@ -474,10 +478,24 @@ ML_OUTPUT_PATH = None
# Use subdomain for organizations # Use subdomain for organizations
USE_SUBDOMAIN = False USE_SUBDOMAIN = False
# Chat
CHAT_SECRET_KEY = "QUdVFsxk6f5-Hd8g9BXv81xMqvIZFRqMl-KbRzztW-U="
# Nginx
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"
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
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"

View file

@ -15,14 +15,6 @@ from django.contrib.auth.decorators import login_required
from django.conf.urls.static import static as url_static from django.conf.urls.static import static as url_static
from judge.feed import (
AtomBlogFeed,
AtomCommentFeed,
AtomProblemFeed,
BlogFeed,
CommentFeed,
ProblemFeed,
)
from judge.forms import CustomAuthenticationForm from judge.forms import CustomAuthenticationForm
from judge.sitemap import ( from judge.sitemap import (
BlogPostSitemap, BlogPostSitemap,
@ -44,6 +36,8 @@ from judge.views import (
language, language,
license, license,
mailgun, mailgun,
markdown_editor,
test_formatter,
notification, notification,
organization, organization,
preview, preview,
@ -65,7 +59,13 @@ from judge.views import (
internal, internal,
resolver, resolver,
course, course,
email,
custom_file_upload,
) )
from judge import authentication
from judge.views.test_formatter import test_formatter
from judge.views.problem_data import ( from judge.views.problem_data import (
ProblemDataView, ProblemDataView,
ProblemSubmissionDiff, ProblemSubmissionDiff,
@ -77,7 +77,6 @@ from judge.views.register import ActivationView, RegistrationView
from judge.views.select2 import ( from judge.views.select2 import (
AssigneeSelect2View, AssigneeSelect2View,
ChatUserSearchSelect2View, ChatUserSearchSelect2View,
CommentSelect2View,
ContestSelect2View, ContestSelect2View,
ContestUserSearchSelect2View, ContestUserSearchSelect2View,
OrganizationSelect2View, OrganizationSelect2View,
@ -85,6 +84,7 @@ from judge.views.select2 import (
TicketUserSelect2View, TicketUserSelect2View,
UserSearchSelect2View, UserSearchSelect2View,
UserSelect2View, UserSelect2View,
ProblemAuthorSearchSelect2View,
) )
admin.autodiscover() admin.autodiscover()
@ -104,19 +104,19 @@ register_patterns = [
# confusing 404. # confusing 404.
url( url(
r"^activate/(?P<activation_key>\w+)/$", r"^activate/(?P<activation_key>\w+)/$",
ActivationView.as_view(title="Activation key invalid"), ActivationView.as_view(title=_("Activation key invalid")),
name="registration_activate", name="registration_activate",
), ),
url( url(
r"^register/$", r"^register/$",
RegistrationView.as_view(title="Register"), RegistrationView.as_view(title=_("Register")),
name="registration_register", name="registration_register",
), ),
url( url(
r"^register/complete/$", r"^register/complete/$",
TitledTemplateView.as_view( TitledTemplateView.as_view(
template_name="registration/registration_complete.html", template_name="registration/registration_complete.html",
title="Registration Completed", title=_("Registration Completed"),
), ),
name="registration_complete", name="registration_complete",
), ),
@ -124,7 +124,7 @@ register_patterns = [
r"^register/closed/$", r"^register/closed/$",
TitledTemplateView.as_view( TitledTemplateView.as_view(
template_name="registration/registration_closed.html", template_name="registration/registration_closed.html",
title="Registration not allowed", title=_("Registration not allowed"),
), ),
name="registration_disallowed", name="registration_disallowed",
), ),
@ -141,9 +141,7 @@ register_patterns = [
url(r"^logout/$", user.UserLogoutView.as_view(), name="auth_logout"), url(r"^logout/$", user.UserLogoutView.as_view(), name="auth_logout"),
url( url(
r"^password/change/$", r"^password/change/$",
auth_views.PasswordChangeView.as_view( authentication.CustomPasswordChangeView.as_view(),
template_name="registration/password_change_form.html",
),
name="password_change", name="password_change",
), ),
url( url(
@ -183,6 +181,17 @@ register_patterns = [
), ),
name="password_reset_done", name="password_reset_done",
), ),
url(r"^email/change/$", email.email_change_view, name="email_change"),
url(
r"^email/change/verify/(?P<uidb64>[0-9A-Za-z]+)-(?P<token>.+)/$",
email.verify_email_view,
name="email_change_verify",
),
url(
r"^email/change/pending$",
email.email_change_pending_view,
name="email_change_pending",
),
url(r"^social/error/$", register.social_auth_error, name="social_auth_error"), url(r"^social/error/$", register.social_auth_error, name="social_auth_error"),
url(r"^2fa/$", totp.TOTPLoginView.as_view(), name="login_2fa"), url(r"^2fa/$", totp.TOTPLoginView.as_view(), name="login_2fa"),
url(r"^2fa/enable/$", totp.TOTPEnableView.as_view(), name="enable_2fa"), url(r"^2fa/enable/$", totp.TOTPEnableView.as_view(), name="enable_2fa"),
@ -389,10 +398,36 @@ urlpatterns = [
name="submission_status", name="submission_status",
), ),
url(r"^/abort$", submission.abort_submission, name="submission_abort"), url(r"^/abort$", submission.abort_submission, name="submission_abort"),
url(r"^/html$", submission.single_submission),
] ]
), ),
), ),
url(
r"^test_formatter/",
include(
[
url(
r"^$",
login_required(test_formatter.TestFormatter.as_view()),
name="test_formatter",
),
url(
r"^edit_page$",
login_required(test_formatter.EditTestFormatter.as_view()),
name="test_formatter_edit",
),
url(
r"^download_page$",
login_required(test_formatter.DownloadTestFormatter.as_view()),
name="test_formatter_download",
),
]
),
),
url(
r"^markdown_editor/",
markdown_editor.MarkdownEditor.as_view(),
name="markdown_editor",
),
url( url(
r"^submission_source_file/(?P<filename>(\w|\.)+)", r"^submission_source_file/(?P<filename>(\w|\.)+)",
submission.SubmissionSourceFileView.as_view(), submission.SubmissionSourceFileView.as_view(),
@ -452,6 +487,7 @@ urlpatterns = [
reverse("all_user_submissions", args=[user]) reverse("all_user_submissions", args=[user])
), ),
), ),
url(r"^/toggle_follow/", user.toggle_follow, name="user_toggle_follow"),
url( url(
r"^/$", r"^/$",
lambda _, user: HttpResponsePermanentRedirect( lambda _, user: HttpResponsePermanentRedirect(
@ -468,6 +504,8 @@ urlpatterns = [
url(r"^comments/upvote/$", comment.upvote_comment, name="comment_upvote"), url(r"^comments/upvote/$", comment.upvote_comment, name="comment_upvote"),
url(r"^comments/downvote/$", comment.downvote_comment, name="comment_downvote"), url(r"^comments/downvote/$", comment.downvote_comment, name="comment_downvote"),
url(r"^comments/hide/$", comment.comment_hide, name="comment_hide"), url(r"^comments/hide/$", comment.comment_hide, name="comment_hide"),
url(r"^comments/get_replies/$", comment.get_replies, name="comment_get_replies"),
url(r"^comments/show_more/$", comment.get_show_more, name="comment_show_more"),
url( url(
r"^comments/(?P<id>\d+)/", r"^comments/(?P<id>\d+)/",
include( include(
@ -497,7 +535,58 @@ urlpatterns = [
), ),
), ),
url(r"^contests/", paged_list_view(contests.ContestList, "contest_list")), url(r"^contests/", paged_list_view(contests.ContestList, "contest_list")),
url(r"^course/", paged_list_view(course.CourseList, "course_list")), url(
r"^contests/summary/(?P<key>\w+)/",
paged_list_view(contests.ContestsSummaryView, "contests_summary"),
),
url(
r"^contests/official",
paged_list_view(contests.OfficialContestList, "official_contest_list"),
),
url(r"^courses/", paged_list_view(course.CourseList, "course_list")),
url(
r"^course/(?P<slug>[\w-]*)",
include(
[
url(r"^$", course.CourseDetail.as_view(), name="course_detail"),
url(
r"^/lesson/(?P<id>\d+)$",
course.CourseLessonDetail.as_view(),
name="course_lesson_detail",
),
url(
r"^/edit_lessons$",
course.EditCourseLessonsView.as_view(),
name="edit_course_lessons",
),
url(
r"^/grades$",
course.CourseStudentResults.as_view(),
name="course_grades",
),
url(
r"^/grades/lesson/(?P<id>\d+)$",
course.CourseStudentResultsLesson.as_view(),
name="course_grades_lesson",
),
url(
r"^/add_contest$",
course.AddCourseContest.as_view(),
name="add_course_contest",
),
url(
r"^/edit_contest/(?P<contest>\w+)$",
course.EditCourseContest.as_view(),
name="edit_course_contest",
),
url(
r"^/contests$",
course.CourseContestList.as_view(),
name="course_contest_list",
),
]
),
),
url( url(
r"^contests/(?P<year>\d+)/(?P<month>\d+)/$", r"^contests/(?P<year>\d+)/(?P<month>\d+)/$",
contests.ContestCalendar.as_view(), contests.ContestCalendar.as_view(),
@ -540,11 +629,6 @@ urlpatterns = [
contests.ContestFinalRanking.as_view(), contests.ContestFinalRanking.as_view(),
name="contest_final_ranking", name="contest_final_ranking",
), ),
url(
r"^/ranking/ajax$",
contests.contest_ranking_ajax,
name="contest_ranking_ajax",
),
url(r"^/join$", contests.ContestJoin.as_view(), name="contest_join"), url(r"^/join$", contests.ContestJoin.as_view(), name="contest_join"),
url(r"^/leave$", contests.ContestLeave.as_view(), name="contest_leave"), url(r"^/leave$", contests.ContestLeave.as_view(), name="contest_leave"),
url(r"^/stats$", contests.ContestStats.as_view(), name="contest_stats"), url(r"^/stats$", contests.ContestStats.as_view(), name="contest_stats"),
@ -561,6 +645,13 @@ urlpatterns = [
"contest_user_submissions_ajax", "contest_user_submissions_ajax",
), ),
), ),
url(
r"^/submissions",
paged_list_view(
submission.ContestSubmissions,
"contest_submissions",
),
),
url( url(
r"^/participations$", r"^/participations$",
contests.ContestParticipationList.as_view(), contests.ContestParticipationList.as_view(),
@ -826,6 +917,11 @@ urlpatterns = [
AssigneeSelect2View.as_view(), AssigneeSelect2View.as_view(),
name="ticket_assignee_select2_ajax", name="ticket_assignee_select2_ajax",
), ),
url(
r"^problem_authors$",
ProblemAuthorSearchSelect2View.as_view(),
name="problem_authors_select2_ajax",
),
] ]
), ),
), ),
@ -884,19 +980,6 @@ urlpatterns = [
] ]
), ),
), ),
url(
r"^feed/",
include(
[
url(r"^problems/rss/$", ProblemFeed(), name="problem_rss"),
url(r"^problems/atom/$", AtomProblemFeed(), name="problem_atom"),
url(r"^comment/rss/$", CommentFeed(), name="comment_rss"),
url(r"^comment/atom/$", AtomCommentFeed(), name="comment_atom"),
url(r"^blog/rss/$", BlogFeed(), name="blog_rss"),
url(r"^blog/atom/$", AtomBlogFeed(), name="blog_atom"),
]
),
),
url( url(
r"^stats/", r"^stats/",
include( include(
@ -997,9 +1080,6 @@ urlpatterns = [
url( url(
r"^contest/$", ContestSelect2View.as_view(), name="contest_select2" r"^contest/$", ContestSelect2View.as_view(), name="contest_select2"
), ),
url(
r"^comment/$", CommentSelect2View.as_view(), name="comment_select2"
),
] ]
), ),
), ),
@ -1075,6 +1155,11 @@ urlpatterns = [
internal.InternalProblem.as_view(), internal.InternalProblem.as_view(),
name="internal_problem", name="internal_problem",
), ),
url(
r"^problem_votes$",
internal.get_problem_votes,
name="internal_problem_votes",
),
url( url(
r"^request_time$", r"^request_time$",
internal.InternalRequestTime.as_view(), internal.InternalRequestTime.as_view(),
@ -1100,8 +1185,7 @@ urlpatterns = [
), ),
url( url(
r"^notifications/", r"^notifications/",
login_required(notification.NotificationList.as_view()), paged_list_view(notification.NotificationList, "notification"),
name="notification",
), ),
url( url(
r"^import_users/", r"^import_users/",
@ -1131,6 +1215,7 @@ urlpatterns = [
), ),
), ),
url(r"^resolver/(?P<contest>\w+)", resolver.Resolver.as_view(), name="resolver"), url(r"^resolver/(?P<contest>\w+)", resolver.Resolver.as_view(), name="resolver"),
url(r"^upload/$", custom_file_upload.file_upload, name="custom_file_upload"),
] + url_static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) ] + url_static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
# if hasattr(settings, "INTERNAL_IPS"): # if hasattr(settings, "INTERNAL_IPS"):
@ -1158,6 +1243,7 @@ favicon_paths = [
"favicon-32x32.png", "favicon-32x32.png",
"favicon-16x16.png", "favicon-16x16.png",
"android-chrome-192x192.png", "android-chrome-192x192.png",
"android-chrome-512x512.png",
"android-chrome-48x48.png", "android-chrome-48x48.png",
"mstile-310x150.png", "mstile-310x150.png",
"apple-touch-icon-144x144.png", "apple-touch-icon-144x144.png",

View file

@ -1,8 +1,14 @@
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 (
ContestAdmin,
ContestParticipationAdmin,
ContestTagAdmin,
ContestsSummaryAdmin,
)
from judge.admin.interface import ( from judge.admin.interface import (
BlogPostAdmin, BlogPostAdmin,
LicenseAdmin, LicenseAdmin,
@ -11,12 +17,18 @@ from judge.admin.interface import (
) )
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.admin.volunteer import VolunteerProblemVoteAdmin from judge.admin.volunteer import VolunteerProblemVoteAdmin
from judge.admin.course import CourseAdmin
from judge.models import ( from judge.models import (
BlogPost, BlogPost,
Comment, Comment,
@ -40,6 +52,9 @@ from judge.models import (
Ticket, Ticket,
VolunteerProblemVote, VolunteerProblemVote,
Course, Course,
ContestsSummary,
OfficialContestCategory,
OfficialContestLocation,
) )
@ -65,4 +80,9 @@ 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(VolunteerProblemVote, VolunteerProblemVoteAdmin)
admin.site.register(Course) 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

@ -12,7 +12,6 @@ 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( widgets["body"] = HeavyPreviewAdminPageDownWidget(
@ -39,7 +38,7 @@ class CommentAdmin(VersionAdmin):
) )
list_display = ["author", "linked_object", "time"] list_display = ["author", "linked_object", "time"]
search_fields = ["author__user__username", "body"] search_fields = ["author__user__username", "body"]
readonly_fields = ["score"] readonly_fields = ["score", "parent"]
actions = ["hide_comment", "unhide_comment"] actions = ["hide_comment", "unhide_comment"]
list_filter = ["hidden"] list_filter = ["hidden"]
actions_on_top = True actions_on_top = True

View file

@ -14,7 +14,14 @@ from reversion.admin import VersionAdmin
from reversion_compare.admin import CompareVersionAdmin 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 ( from judge.widgets import (
AdminHeavySelect2MultipleWidget, AdminHeavySelect2MultipleWidget,
@ -24,6 +31,8 @@ from judge.widgets import (
AdminSelect2Widget, AdminSelect2Widget,
HeavyPreviewAdminPageDownWidget, 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):
@ -71,7 +80,6 @@ class ContestProblemInlineForm(ModelForm):
"hidden_subtasks": TextInput(attrs={"size": "3"}), "hidden_subtasks": TextInput(attrs={"size": "3"}),
"points": TextInput(attrs={"size": "1"}), "points": TextInput(attrs={"size": "1"}),
"order": TextInput(attrs={"size": "1"}), "order": TextInput(attrs={"size": "1"}),
"output_prefix_override": TextInput(attrs={"size": "1"}),
} }
@ -86,7 +94,7 @@ class ContestProblemInline(admin.TabularInline):
"is_pretested", "is_pretested",
"max_submissions", "max_submissions",
"hidden_subtasks", "hidden_subtasks",
"output_prefix_override", "show_testcases",
"order", "order",
"rejudge_column", "rejudge_column",
) )
@ -149,6 +157,26 @@ class ContestForm(ModelForm):
) )
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): class ContestAdmin(CompareVersionAdmin):
fieldsets = ( fieldsets = (
(None, {"fields": ("key", "name", "authors", "curators", "testers")}), (None, {"fields": ("key", "name", "authors", "curators", "testers")}),
@ -159,9 +187,11 @@ class ContestAdmin(CompareVersionAdmin):
"is_visible", "is_visible",
"use_clarifications", "use_clarifications",
"hide_problem_tags", "hide_problem_tags",
"public_scoreboard",
"scoreboard_visibility", "scoreboard_visibility",
"run_pretests_only", "run_pretests_only",
"points_precision", "points_precision",
"rate_limit",
) )
}, },
), ),
@ -221,7 +251,7 @@ class ContestAdmin(CompareVersionAdmin):
"user_count", "user_count",
) )
search_fields = ("key", "name") search_fields = ("key", "name")
inlines = [ContestProblemInline] inlines = [ContestProblemInline, OfficialContestInline]
actions_on_top = True actions_on_top = True
actions_on_bottom = True actions_on_bottom = True
form = ContestForm form = ContestForm
@ -281,31 +311,14 @@ class ContestAdmin(CompareVersionAdmin):
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 (
"start_time",
"end_time",
"time_limit",
"format_config",
"format_name",
"freeze_after",
)
):
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):
obj = form.instance formset_changed = True
obj.is_organization_private = obj.organizations.count() > 0
obj.is_private = obj.private_contestants.count() > 0 maybe_trigger_contest_rescore(form, form.instance, formset_changed)
obj.save()
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"):
@ -314,11 +327,6 @@ class ContestAdmin(CompareVersionAdmin):
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( queryset = queryset.filter(
@ -502,3 +510,25 @@ class ContestParticipationAdmin(admin.ModelAdmin):
show_virtual.short_description = _("virtual") show_virtual.short_description = _("virtual")
show_virtual.admin_order_field = "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

@ -53,6 +53,7 @@ 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)
if "authors" in self.fields:
self.fields["authors"].widget.can_add_related = False self.fields["authors"].widget.can_add_related = False
class Meta: class Meta:

View file

@ -33,7 +33,6 @@ class OrganizationAdmin(VersionAdmin):
"short_name", "short_name",
"is_open", "is_open",
"about", "about",
"logo_override_image",
"slots", "slots",
"registrant", "registrant",
"creation_date", "creation_date",

View file

@ -1,8 +1,8 @@
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, Avg, Count from django.db.models import Q, Avg, Count
from django.db.models.aggregates import StdDev from django.db.models.aggregates import StdDev
from django.forms import ModelForm, TextInput from django.forms import ModelForm, TextInput
@ -11,6 +11,7 @@ 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 django_ace import AceWidget from django_ace import AceWidget
from django.utils import timezone from django.utils import timezone
from django.core.exceptions import ValidationError
from reversion.admin import VersionAdmin from reversion.admin import VersionAdmin
from reversion_compare.admin import CompareVersionAdmin from reversion_compare.admin import CompareVersionAdmin
@ -25,6 +26,7 @@ from judge.models import (
Solution, Solution,
Notification, Notification,
) )
from judge.models.notification import make_notification
from judge.widgets import ( from judge.widgets import (
AdminHeavySelect2MultipleWidget, AdminHeavySelect2MultipleWidget,
AdminSelect2MultipleWidget, AdminSelect2MultipleWidget,
@ -32,6 +34,7 @@ from judge.widgets import (
CheckboxSelectMultipleWithSelectAll, CheckboxSelectMultipleWithSelectAll,
HeavyPreviewAdminPageDownWidget, HeavyPreviewAdminPageDownWidget,
) )
from judge.utils.problems import user_editable_ids, user_tester_ids
MEMORY_UNITS = (("KB", "KB"), ("MB", "MB")) MEMORY_UNITS = (("KB", "KB"), ("MB", "MB"))
@ -54,6 +57,16 @@ class ProblemForm(ModelForm):
} }
) )
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): def clean(self):
memory_unit = self.cleaned_data.get("memory_unit", "KB") memory_unit = self.cleaned_data.get("memory_unit", "KB")
if memory_unit == "MB": if memory_unit == "MB":
@ -129,6 +142,7 @@ class LanguageLimitInline(admin.TabularInline):
model = LanguageLimit model = LanguageLimit
fields = ("language", "time_limit", "memory_limit", "memory_unit") fields = ("language", "time_limit", "memory_limit", "memory_unit")
form = LanguageLimitInlineForm form = LanguageLimitInlineForm
extra = 0
class LanguageTemplateInlineForm(ModelForm): class LanguageTemplateInlineForm(ModelForm):
@ -143,6 +157,7 @@ class LanguageTemplateInline(admin.TabularInline):
model = LanguageTemplate model = LanguageTemplate
fields = ("language", "source") fields = ("language", "source")
form = LanguageTemplateInlineForm form = LanguageTemplateInlineForm
extra = 0
class ProblemSolutionForm(ModelForm): class ProblemSolutionForm(ModelForm):
@ -358,12 +373,29 @@ class ProblemAdmin(CompareVersionAdmin):
self._rescore(request, obj.id) self._rescore(request, obj.id)
def save_related(self, request, form, formsets, change): 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) super().save_related(request, form, formsets, change)
# Only rescored if we did not already do so in `save_model`
obj = form.instance obj = form.instance
obj.curators.add(request.profile) obj.curators.add(request.profile)
obj.is_organization_private = obj.organizations.count() > 0
obj.save() 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 # Create notification
if "is_public" in form.changed_data or "organizations" in form.changed_data: if "is_public" in form.changed_data or "organizations" in form.changed_data:
users = set(obj.authors.all()) users = set(obj.authors.all())
@ -381,14 +413,7 @@ class ProblemAdmin(CompareVersionAdmin):
category = "Problem public: " + str(obj.is_public) category = "Problem public: " + str(obj.is_public)
if orgs: if orgs:
category += " (" + ", ".join(orgs) + ")" category += " (" + ", ".join(orgs) + ")"
for user in users: make_notification(users, category, html, request.profile)
notification = Notification(
owner=user,
html_link=html,
category=category,
author=request.profile,
)
notification.save()
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"):

View file

@ -1,13 +1,20 @@
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):
@ -53,6 +60,13 @@ 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 = ( fields = (
"user", "user",
@ -62,15 +76,12 @@ class ProfileAdmin(VersionAdmin):
"timezone", "timezone",
"language", "language",
"ace_theme", "ace_theme",
"math_engine",
"last_access", "last_access",
"ip", "ip",
"mute", "mute",
"is_unlisted", "is_unlisted",
"is_banned_problem_voting",
"notes", "notes",
"is_totp_enabled", "is_totp_enabled",
"user_script",
"current_contest", "current_contest",
) )
readonly_fields = ("user",) readonly_fields = ("user",)
@ -91,6 +102,7 @@ class ProfileAdmin(VersionAdmin):
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")
@ -125,7 +137,7 @@ class ProfileAdmin(VersionAdmin):
admin_user_admin.short_description = _("User") 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.admin_order_field = "user__email"
email.short_description = _("Email") email.short_description = _("Email")
@ -159,11 +171,57 @@ class ProfileAdmin(VersionAdmin):
recalculate_points.short_description = _("Recalculate scores") recalculate_points.short_description = _("Recalculate scores")
def get_form(self, request, obj=None, **kwargs):
form = super(ProfileAdmin, self).get_form(request, obj, **kwargs) class UserForm(UserChangeForm):
if "user_script" in form.base_fields: def __init__(self, *args, **kwargs):
# form.base_fields['user_script'] does not exist when the user has only view permission on the model. super().__init__(*args, **kwargs)
form.base_fields["user_script"].widget = AceWidget( self.fields["username"].help_text = _(
"javascript", request.profile.ace_theme "Username can only contain letters, digits, and underscores."
) )
return form
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

@ -194,13 +194,6 @@ class SubmissionAdmin(admin.ModelAdmin):
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 ( return super(SubmissionAdmin, self).lookup_allowed(key, value) or key in (
"problem__code", "problem__code",

View file

@ -56,3 +56,11 @@ class ProblemTypeAdmin(admin.ModelAdmin):
[o.pk for o in obj.problem_set.all()] if obj else [] [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

@ -12,7 +12,7 @@ class JudgeAppConfig(AppConfig):
# 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

@ -3,7 +3,6 @@ import json
import logging import logging
import threading import threading
import time import time
import os
from collections import deque, namedtuple from collections import deque, namedtuple
from operator import itemgetter from operator import itemgetter
@ -25,6 +24,8 @@ from judge.models import (
Submission, Submission,
SubmissionTestCase, SubmissionTestCase,
) )
from judge.bridge.utils import VanishedSubmission
from judge.caching import cache_wrapper
logger = logging.getLogger("judge.bridge") logger = logging.getLogger("judge.bridge")
json_log = logging.getLogger("judge.json.bridge") json_log = logging.getLogger("judge.json.bridge")
@ -64,10 +65,10 @@ class JudgeHandler(ZlibPacketHandler):
"handshake": self.on_handshake, "handshake": self.on_handshake,
} }
self._working = False self._working = False
self._working_data = {}
self._no_response_job = None self._no_response_job = None
self._problems = []
self.executors = {} self.executors = {}
self.problems = {} self.problems = set()
self.latency = None self.latency = None
self.time_delta = None self.time_delta = None
self.load = 1e100 self.load = 1e100
@ -93,12 +94,6 @@ class JudgeHandler(ZlibPacketHandler):
def on_disconnect(self): def on_disconnect(self):
self._stop_ping.set() self._stop_ping.set()
if self._working:
logger.error(
"Judge %s disconnected while handling submission %s",
self.name,
self._working,
)
self.judges.remove(self) self.judges.remove(self)
if self.name is not None: if self.name is not None:
self._disconnected() self._disconnected()
@ -110,21 +105,29 @@ class JudgeHandler(ZlibPacketHandler):
self._make_json_log(action="disconnect", info="judge disconnected") self._make_json_log(action="disconnect", info="judge disconnected")
) )
if self._working: if self._working:
Submission.objects.filter(id=self._working).update( self.judges.judge(
status="IE", result="IE", error="" self._working,
) self._working_data["problem"],
json_log.error( self._working_data["language"],
self._make_json_log( self._working_data["source"],
sub=self._working, None,
action="close", 0,
info="IE due to shutdown on grading",
)
) )
def _authenticate(self, id, key): def _authenticate(self, id, key):
try: try:
judge = Judge.objects.get(name=id, is_blocked=False) judge = Judge.objects.get(name=id)
except Judge.DoesNotExist: except Judge.DoesNotExist:
if settings.BRIDGED_AUTO_CREATE_JUDGE:
judge = Judge()
judge.name = id
judge.auth_key = key
judge.save()
result = True
else:
result = False
else:
if judge.is_blocked:
result = False result = False
else: else:
result = hmac.compare_digest(judge.auth_key, key) result = hmac.compare_digest(judge.auth_key, key)
@ -137,11 +140,52 @@ class JudgeHandler(ZlibPacketHandler):
) )
return result return result
def _update_supported_problems(self, problem_packet):
# problem_packet is a dict {code: mtimes} from judge-server
self.problems = set(p for p, _ in problem_packet)
def _update_judge_problems(self):
chunk_size = 500
target_problem_codes = self.problems
current_problems = _get_judge_problems(self.judge)
updated = False
problems_to_add = list(target_problem_codes - current_problems)
problems_to_remove = list(current_problems - target_problem_codes)
if problems_to_add:
for i in range(0, len(problems_to_add), chunk_size):
chunk = problems_to_add[i : i + chunk_size]
problem_ids = Problem.objects.filter(code__in=chunk).values_list(
"id", flat=True
)
if not problem_ids:
continue
logger.info("%s: Add %d problems", self.name, len(problem_ids))
self.judge.problems.add(*problem_ids)
updated = True
if problems_to_remove:
for i in range(0, len(problems_to_remove), chunk_size):
chunk = problems_to_remove[i : i + chunk_size]
problem_ids = Problem.objects.filter(code__in=chunk).values_list(
"id", flat=True
)
if not problem_ids:
continue
logger.info("%s: Remove %d problems", self.name, len(problem_ids))
self.judge.problems.remove(*problem_ids)
updated = True
if updated:
_get_judge_problems.dirty(self.judge)
def _connected(self): def _connected(self):
judge = self.judge = Judge.objects.get(name=self.name) judge = self.judge = Judge.objects.get(name=self.name)
judge.start_time = timezone.now() judge.start_time = timezone.now()
judge.online = True judge.online = True
judge.problems.set(Problem.objects.filter(code__in=list(self.problems.keys()))) self._update_judge_problems()
judge.runtimes.set(Language.objects.filter(key__in=list(self.executors.keys()))) judge.runtimes.set(Language.objects.filter(key__in=list(self.executors.keys())))
# Delete now in case we somehow crashed and left some over from the last connection # Delete now in case we somehow crashed and left some over from the last connection
@ -176,6 +220,8 @@ class JudgeHandler(ZlibPacketHandler):
def _disconnected(self): def _disconnected(self):
Judge.objects.filter(id=self.judge.id).update(online=False) Judge.objects.filter(id=self.judge.id).update(online=False)
RuntimeVersion.objects.filter(judge=self.judge).delete() RuntimeVersion.objects.filter(judge=self.judge).delete()
self.judge.problems.clear()
_get_judge_problems.dirty(self.judge)
def _update_ping(self): def _update_ping(self):
try: try:
@ -206,8 +252,7 @@ class JudgeHandler(ZlibPacketHandler):
return return
self.timeout = 60 self.timeout = 60
self._problems = packet["problems"] self._update_supported_problems(packet["problems"])
self.problems = dict(self._problems)
self.executors = packet["executors"] self.executors = packet["executors"]
self.name = packet["id"] self.name = packet["id"]
@ -308,7 +353,15 @@ class JudgeHandler(ZlibPacketHandler):
def submit(self, id, problem, language, source): def submit(self, id, problem, language, source):
data = self.get_related_submission_data(id) data = self.get_related_submission_data(id)
if not data:
self._update_internal_error_submission(id, "Submission vanished")
raise VanishedSubmission()
self._working = id self._working = id
self._working_data = {
"problem": problem,
"language": language,
"source": source,
}
self._no_response_job = threading.Timer(20, self._kill_if_no_response) self._no_response_job = threading.Timer(20, self._kill_if_no_response)
self.send( self.send(
{ {
@ -427,14 +480,12 @@ class JudgeHandler(ZlibPacketHandler):
def on_supported_problems(self, packet): def on_supported_problems(self, packet):
logger.info("%s: Updated problem list", self.name) logger.info("%s: Updated problem list", self.name)
self._problems = packet["problems"] self._update_supported_problems(packet["problems"])
self.problems = dict(self._problems)
if not self.working: if not self.working:
self.judges.update_problems(self) self.judges.update_problems(self)
self.judge.problems.set( self._update_judge_problems()
Problem.objects.filter(code__in=list(self.problems.keys()))
)
json_log.info( json_log.info(
self._make_json_log(action="update-problems", count=len(self.problems)) self._make_json_log(action="update-problems", count=len(self.problems))
) )
@ -651,8 +702,11 @@ class JudgeHandler(ZlibPacketHandler):
self._free_self(packet) self._free_self(packet)
id = packet["submission-id"] id = packet["submission-id"]
self._update_internal_error_submission(id, packet["message"])
def _update_internal_error_submission(self, id, message):
if Submission.objects.filter(id=id).update( if Submission.objects.filter(id=id).update(
status="IE", result="IE", error=packet["message"] status="IE", result="IE", error=message
): ):
event.post( event.post(
"sub_%s" % Submission.get_id_secret(id), {"type": "internal-error"} "sub_%s" % Submission.get_id_secret(id), {"type": "internal-error"}
@ -660,9 +714,9 @@ class JudgeHandler(ZlibPacketHandler):
self._post_update_submission(id, "internal-error", done=True) self._post_update_submission(id, "internal-error", done=True)
json_log.info( json_log.info(
self._make_json_log( self._make_json_log(
packet, sub=id,
action="internal-error", action="internal-error",
message=packet["message"], message=message,
finish=True, finish=True,
result="IE", result="IE",
) )
@ -671,10 +725,10 @@ class JudgeHandler(ZlibPacketHandler):
logger.warning("Unknown submission: %s", id) logger.warning("Unknown submission: %s", id)
json_log.error( json_log.error(
self._make_json_log( self._make_json_log(
packet, sub=id,
action="internal-error", action="internal-error",
info="unknown submission", info="unknown submission",
message=packet["message"], message=message,
finish=True, finish=True,
result="IE", result="IE",
) )
@ -905,3 +959,8 @@ class JudgeHandler(ZlibPacketHandler):
def on_cleanup(self): def on_cleanup(self):
db.connection.close() db.connection.close()
@cache_wrapper(prefix="gjp", timeout=3600)
def _get_judge_problems(judge):
return set(judge.problems.values_list("code", flat=True))

View file

@ -3,6 +3,8 @@ 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:
@ -39,6 +41,8 @@ class JudgeList(object):
) )
try: try:
judge.submit(id, problem, language, source) judge.submit(id, problem, language, source)
except VanishedSubmission:
pass
except Exception: except Exception:
logger.exception( logger.exception(
"Failed to dispatch %d (%s, %s) to %s", "Failed to dispatch %d (%s, %s) to %s",
@ -89,6 +93,7 @@ class JudgeList(object):
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):

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

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

View file

@ -1,13 +1,16 @@
from inspect import signature from inspect import signature
from django.core.cache import cache from django.core.cache import cache, caches
from django.db.models.query import QuerySet from django.db.models.query import QuerySet
from django.core.handlers.wsgi import WSGIRequest
import hashlib import hashlib
MAX_NUM_CHAR = 15 from judge.logging import log_debug
MAX_NUM_CHAR = 50
NONE_RESULT = "__None__"
def cache_wrapper(prefix, timeout=86400):
def arg_to_str(arg): def arg_to_str(arg):
if hasattr(arg, "id"): if hasattr(arg, "id"):
return str(arg.id) return str(arg.id)
@ -17,31 +20,97 @@ def cache_wrapper(prefix, timeout=86400):
return str(arg)[:MAX_NUM_CHAR] return str(arg)[:MAX_NUM_CHAR]
return str(arg) 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): def get_key(func, *args, **kwargs):
args_list = list(args) args_list = list(args)
signature_args = list(signature(func).parameters.keys()) signature_args = list(signature(func).parameters.keys())
args_list += [kwargs.get(k) for k in signature_args[len(args) :]] 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] args_list = [arg_to_str(i) for i in args_list]
key = prefix + ":" + ":".join(args_list) key = prefix + ":" + ":".join(args_list)
key = key.replace(" ", "_") key = key.replace(" ", "_")
return key return key
def decorator(func): def _get(key):
def wrapper(*args, **kwargs): if not l0_cache:
cache_key = get_key(func, *args, **kwargs) return cache.get(key)
result = cache.get(cache_key) result = l0_cache.get(key)
if result is not None: if result is None:
result = cache.get(key)
return result 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) result = func(*args, **kwargs)
cache.set(cache_key, result, timeout) if result is None:
cache_result = NONE_RESULT
else:
cache_result = result
_set(cache_key, cache_result, timeout)
return result return result
def dirty(*args, **kwargs): def dirty(*args, **kwargs):
cache_key = get_key(func, *args, **kwargs) cache_key = get_key(func, *args, **kwargs)
cache.delete(cache_key) 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)
if l0_cache:
l0_cache.delete_many(keys)
wrapper.dirty = dirty wrapper.dirty = dirty
wrapper.prefetch_multi = prefetch_multi
wrapper.dirty_multi = dirty_multi
return wrapper return wrapper

View file

@ -1,190 +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, FilteredRelation, Q
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, Notification
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 (
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()
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 self.object.comments.filter(hidden=False, id=parent).exists():
return HttpResponseNotFound()
form = CommentForm(request, request.POST)
if form.is_valid():
comment = form.save(commit=False)
comment.author = request.profile
comment.linked_object = self.object
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_reply = Notification(
owner=comment.parent.author, comment=comment, category="Reply"
)
notification_reply.save()
# add notification for page authors
page_authors = comment.linked_object.authors.all()
for user in page_authors:
if user == comment.author:
continue
notification = Notification(
owner=user, comment=comment, category="Comment"
)
notification.save()
# except Exception:
# pass
add_mention_notifications(comment)
return HttpResponseRedirect(comment.get_absolute_url())
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={"parent": None}),
)
)
def get_context_data(self, **kwargs):
context = super(CommentedDetailView, self).get_context_data(**kwargs)
queryset = self.object.comments
context["has_comments"] = queryset.exists()
context["comment_lock"] = self.is_comment_locked()
queryset = (
queryset.select_related("author__user")
.filter(hidden=False)
.defer("author__about")
.annotate(revisions=Count("versions"))
)
if self.request.user.is_authenticated:
profile = self.request.profile
queryset = queryset.annotate(
my_vote=FilteredRelation(
"votes", condition=Q(votes__voter_id=profile.id)
),
).annotate(vote_score=Coalesce(F("my_vote__score"), Value(0)))
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["comment_count"] = len(queryset)
context["vote_hide_threshold"] = settings.DMOJ_COMMENT_VOTE_HIDE_THRESHOLD
return context

View file

@ -4,4 +4,5 @@ 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.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

@ -109,6 +109,8 @@ class BaseContestFormat(metaclass=ABCMeta):
) )
for result in queryset: for result in queryset:
problem = str(result["problem_id"]) problem = str(result["problem_id"])
if not (self.contest.freeze_after or hidden_subtasks.get(problem)):
continue
if format_data.get(problem): if format_data.get(problem):
is_after_freeze = ( is_after_freeze = (
self.contest.freeze_after self.contest.freeze_after

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

@ -16,7 +16,7 @@ class EventPoster(object):
def _connect(self): def _connect(self):
self._conn = pika.BlockingConnection( self._conn = pika.BlockingConnection(
pika.URLParameters(settings.EVENT_DAEMON_AMQP) pika.URLParameters(settings.EVENT_DAEMON_AMQP),
) )
self._chan = self._conn.channel() self._chan = self._conn.channel()
@ -25,7 +25,7 @@ class EventPoster(object):
id = int(time() * 1000000) id = int(time() * 1000000)
self._chan.basic_publish( self._chan.basic_publish(
self._exchange, self._exchange,
"", "#",
json.dumps({"id": id, "channel": channel, "message": message}), json.dumps({"id": id, "channel": channel, "message": message}),
) )
return id return id

View file

@ -1,120 +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(
"[\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))[: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))
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))
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

View file

@ -11,7 +11,6 @@ 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, ObjectDoesNotExist from django.core.exceptions import ValidationError, ObjectDoesNotExist
from django.core.validators import RegexValidator from django.core.validators import RegexValidator
from django.db import transaction
from django.db.models import Q from django.db.models import Q
from django.forms import ( from django.forms import (
CharField, CharField,
@ -30,6 +29,7 @@ from django_ace import AceWidget
from judge.models import ( from judge.models import (
Contest, Contest,
Language, Language,
TestFormatterModel,
Organization, Organization,
PrivateMessage, PrivateMessage,
Problem, Problem,
@ -38,11 +38,12 @@ from judge.models import (
Submission, Submission,
BlogPost, BlogPost,
ContestProblem, ContestProblem,
TestFormatterModel,
ProfileInfo,
) )
from judge.widgets import ( from judge.widgets import (
HeavyPreviewPageDownWidget, HeavyPreviewPageDownWidget,
MathJaxPagedownWidget,
PagedownWidget, PagedownWidget,
Select2MultipleWidget, Select2MultipleWidget,
Select2Widget, Select2Widget,
@ -50,8 +51,9 @@ from judge.widgets import (
HeavySelect2Widget, HeavySelect2Widget,
Select2MultipleWidget, Select2MultipleWidget,
DateTimePickerWidget, DateTimePickerWidget,
ImageWidget,
DatePickerWidget,
) )
from judge.tasks import rescore_contest
def fix_unicode(string, unsafe=tuple("\u202a\u202b\u202d\u202e")): def fix_unicode(string, unsafe=tuple("\u202a\u202b\u202d\u202e")):
@ -69,61 +71,61 @@ class UserForm(ModelForm):
] ]
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):
class Meta: class Meta:
model = Profile model = Profile
fields = [ fields = [
"about", "about",
"organizations",
"timezone", "timezone",
"language", "language",
"ace_theme", "ace_theme",
"user_script", "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 groups.").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): def file_size_validator(file):
limit = 1 * 1024 * 1024 limit = 10 * 1024 * 1024
if file.size > limit: if file.size > limit:
raise ValidationError("File too large. Size should not exceed 1MB.") raise ValidationError("File too large. Size should not exceed 10MB.")
class ProblemSubmitForm(ModelForm): class ProblemSubmitForm(ModelForm):
@ -193,16 +195,32 @@ class EditOrganizationForm(ModelForm):
"slug", "slug",
"short_name", "short_name",
"about", "about",
"logo_override_image", "organization_image",
"admins", "admins",
"is_open", "is_open",
] ]
widgets = {"admins": Select2MultipleWidget()} widgets = {
"admins": Select2MultipleWidget(),
"organization_image": ImageWidget,
}
if HeavyPreviewPageDownWidget is not None: if HeavyPreviewPageDownWidget is not None:
widgets["about"] = HeavyPreviewPageDownWidget( widgets["about"] = HeavyPreviewPageDownWidget(
preview=reverse_lazy("organization_preview") 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 AddOrganizationForm(ModelForm):
class Meta: class Meta:
@ -212,7 +230,7 @@ class AddOrganizationForm(ModelForm):
"slug", "slug",
"short_name", "short_name",
"about", "about",
"logo_override_image", "organization_image",
"is_open", "is_open",
] ]
widgets = {} widgets = {}
@ -224,6 +242,7 @@ class AddOrganizationForm(ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.request = kwargs.pop("request", None) self.request = kwargs.pop("request", None)
super(AddOrganizationForm, self).__init__(*args, **kwargs) super(AddOrganizationForm, self).__init__(*args, **kwargs)
self.fields["organization_image"].required = False
def save(self, commit=True): def save(self, commit=True):
res = super(AddOrganizationForm, self).save(commit=False) res = super(AddOrganizationForm, self).save(commit=False)
@ -285,16 +304,9 @@ class EditOrganizationContestForm(ModelForm):
"view_contest_scoreboard", "view_contest_scoreboard",
]: ]:
self.fields[field].widget.data_url = ( self.fields[field].widget.data_url = (
self.fields[field].widget.get_url() + "?org_id=1" self.fields[field].widget.get_url() + f"?org_id={self.org_id}"
) )
def save(self, commit=True):
res = super(EditOrganizationContestForm, self).save(commit=False)
if commit:
res.save()
transaction.on_commit(rescore_contest.s(res.key).delay)
return res
class Meta: class Meta:
model = Contest model = Contest
fields = ( fields = (
@ -311,9 +323,10 @@ class EditOrganizationContestForm(ModelForm):
"freeze_after", "freeze_after",
"use_clarifications", "use_clarifications",
"hide_problem_tags", "hide_problem_tags",
"public_scoreboard",
"scoreboard_visibility", "scoreboard_visibility",
"run_pretests_only",
"points_precision", "points_precision",
"rate_limit",
"description", "description",
"og_image", "og_image",
"logo_override_image", "logo_override_image",
@ -356,7 +369,7 @@ class AddOrganizationMemberForm(ModelForm):
label=_("New users"), label=_("New users"),
) )
def clean(self): def clean_new_users(self):
new_users = self.cleaned_data.get("new_users") or "" new_users = self.cleaned_data.get("new_users") or ""
usernames = new_users.split() usernames = new_users.split()
invalid_usernames = [] invalid_usernames = []
@ -374,8 +387,7 @@ class AddOrganizationMemberForm(ModelForm):
usernames=str(invalid_usernames) usernames=str(invalid_usernames)
) )
) )
self.cleaned_data["new_users"] = valid_usernames return valid_usernames
return self.cleaned_data
class Meta: class Meta:
model = Organization model = Organization
@ -423,13 +435,15 @@ class NewMessageForm(ModelForm):
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(
{"placeholder": _("Username/Email")}
)
self.fields["password"].widget.attrs.update({"placeholder": _("Password")}) 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")
@ -492,6 +506,15 @@ class ContestCloneForm(Form):
max_length=20, max_length=20,
validators=[RegexValidator("^[a-z0-9]+$", _("Contest id must be ^[a-z0-9]+$"))], 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"]
@ -499,6 +522,16 @@ class ContestCloneForm(Form):
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:
@ -514,7 +547,7 @@ class ContestProblemForm(ModelForm):
"problem", "problem",
"points", "points",
"partial", "partial",
"output_prefix_override", "show_testcases",
"max_submissions", "max_submissions",
) )
widgets = { widgets = {
@ -524,9 +557,43 @@ class ContestProblemForm(ModelForm):
} }
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( class ContestProblemFormSet(
formset_factory( formset_factory(
ContestProblemForm, formset=BaseModelFormSet, extra=6, can_delete=True ContestProblemForm, formset=ContestProblemModelFormSet, extra=6, can_delete=True
) )
): ):
model = ContestProblem model = ContestProblem
class TestFormatterForm(ModelForm):
class Meta:
model = TestFormatterModel
fields = ["file"]

View file

@ -1,44 +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"
try: return mark_safe(markdown(value))
import pygments
import pygments.lexers
import pygments.formatters
import pygments.util
except ImportError:
def highlight_code(code, language, cssclass=None):
return _make_pre_code(code)
else:
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,
pygments.formatters.HtmlFormatter(
cssclass=cssclass, linenos="table", wrapcode=True
),
)
)
return mark_safe(
pygments.highlight(
code,
lexer,
pygments.formatters.HtmlFormatter(cssclass=cssclass, wrapcode=True),
)
)

View file

@ -21,8 +21,8 @@ from . import (
render, render,
social, social,
spaceless, spaceless,
submission,
timedelta, timedelta,
comment,
) )
from . import registry from . import registry

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

@ -23,5 +23,5 @@ 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

@ -9,14 +9,16 @@ 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
elif isinstance(email, AbstractUser):
email = email.email
gravatar_url = ( gravatar_url = (
"//www.gravatar.com/avatar/" "//www.gravatar.com/avatar/"
+ hashlib.md5(utf8bytes(email.strip().lower())).hexdigest() + hashlib.md5(utf8bytes(email.strip().lower())).hexdigest()

View file

@ -1,112 +1,7 @@
from .. import registry from .. import registry
import markdown as _markdown from judge.markdown import markdown as _markdown
import bleach
from django.utils.html import escape
from bs4 import BeautifulSoup
from pymdownx import superfences
EXTENSIONS = [
"pymdownx.arithmatex",
"pymdownx.magiclink",
"pymdownx.betterem",
"pymdownx.details",
"pymdownx.emoji",
"pymdownx.inlinehilite",
"pymdownx.superfences",
"pymdownx.tasklist",
"markdown.extensions.footnotes",
"markdown.extensions.attr_list",
"markdown.extensions.def_list",
"markdown.extensions.tables",
"markdown.extensions.admonition",
"nl2br",
"mdx_breakless_lists",
]
EXTENSION_CONFIGS = {
"pymdownx.superfences": {
"custom_fences": [
{
"name": "sample",
"class": "no-border",
"format": superfences.fence_code_format,
}
]
},
}
ALLOWED_TAGS = list(bleach.sanitizer.ALLOWED_TAGS) + [
"img",
"center",
"iframe",
"div",
"span",
"table",
"tr",
"td",
"th",
"tr",
"pre",
"code",
"p",
"hr",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"thead",
"tbody",
"sup",
"dl",
"dt",
"dd",
"br",
"details",
"summary",
]
ALLOWED_ATTRS = ["src", "width", "height", "href", "class", "open"]
@registry.filter @registry.filter
def markdown(value, lazy_load=False): def markdown(value, lazy_load=False):
extensions = EXTENSIONS return _markdown(value, lazy_load)
html = _markdown.markdown(
value, extensions=extensions, extension_configs=EXTENSION_CONFIGS
)
# Don't clean mathjax
hash_script_tag = {}
soup = BeautifulSoup(html, "html.parser")
for script_tag in soup.find_all("script"):
allow_math_types = ["math/tex", "math/tex; mode=display"]
if script_tag.attrs.get("type", False) in allow_math_types:
hash_script_tag[str(hash(str(script_tag)))] = str(script_tag)
for hashed_tag in hash_script_tag:
tag = hash_script_tag[hashed_tag]
html = html.replace(tag, hashed_tag)
html = bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRS)
for hashed_tag in hash_script_tag:
tag = hash_script_tag[hashed_tag]
html = html.replace(hashed_tag, tag)
if not html:
html = escape(value)
if lazy_load:
soup = BeautifulSoup(html, features="html.parser")
for img in soup.findAll("img"):
if img.get("src"):
img["data-src"] = img["src"]
img["src"] = ""
for img in soup.findAll("iframe"):
if img.get("src"):
img["data-src"] = img["src"]
img["src"] = ""
html = str(soup)
return '<div class="md-typeset">%s</div>' % html

View file

@ -155,16 +155,16 @@ def item_title(item):
@registry.function @registry.function
@registry.render_with("user/link.html") @registry.render_with("user/link.html")
def link_user(user): def link_user(user, show_image=False):
if isinstance(user, Profile): if isinstance(user, Profile):
user, profile = user.user, user profile = user
elif isinstance(user, AbstractUser): elif isinstance(user, AbstractUser):
profile = user.profile profile = user.profile
elif type(user).__name__ == "ContestRankingProfile": elif isinstance(user, int):
user, profile = user.user, user profile = Profile(id=user)
else: else:
raise ValueError("Expected profile or user, got %s" % (type(user),)) raise ValueError("Expected profile or user, got %s" % (type(user),))
return {"user": user, "profile": profile} return {"profile": profile, "show_image": show_image}
@registry.function @registry.function

View file

@ -48,5 +48,9 @@ for name, template, url_func in SHARES:
@registry.function @registry.function
def recaptcha_init(language=None): def recaptcha_init(language=None):
return get_template("snowpenguin/recaptcha/recaptcha_init.html").render( return get_template("snowpenguin/recaptcha/recaptcha_init.html").render(
{"explicit": False, "language": language} {
"explicit": False,
"language": language,
"recaptcha_host": "https://google.com",
}
) )

View file

@ -1,41 +0,0 @@
from . import registry
@registry.function
def submission_layout(
submission,
profile_id,
user,
editable_problem_ids,
completed_problem_ids,
tester_problem_ids,
):
problem_id = submission.problem_id
if problem_id in editable_problem_ids:
return True
if problem_id in tester_problem_ids:
return True
if profile_id == submission.user_id:
return True
if user.has_perm("judge.change_submission"):
return True
if user.has_perm("judge.view_all_submission"):
return True
if submission.problem.is_public and user.has_perm("judge.view_public_submission"):
return True
if hasattr(submission, "contest"):
contest = submission.contest.participation.contest
if contest.is_editable_by(user):
return True
if submission.problem_id in completed_problem_ids and submission.problem.is_public:
return True
return False

12
judge/logging.py Normal file
View file

@ -0,0 +1,12 @@
import logging
error_log = logging.getLogger("judge.errors")
debug_log = logging.getLogger("judge.debug")
def log_exception(msg):
error_log.exception(msg)
def log_debug(category, data):
debug_log.info(f"{category}: {data}")

View file

@ -1,6 +1,5 @@
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from judge.models import * from judge.models import *
from collections import defaultdict
import csv import csv
import os import os
from django.conf import settings from django.conf import settings

View file

@ -89,14 +89,13 @@ class Command(BaseCommand):
if trans is None if trans is None
else trans.description, else trans.description,
"url": "", "url": "",
"math_engine": maker.math_engine,
} }
) )
.replace('"//', '"https://') .replace('"//', '"https://')
.replace("'//", "'https://") .replace("'//", "'https://")
) )
maker.title = problem_name maker.title = problem_name
for file in ("style.css", "pygment-github.css", "mathjax3_config.js"): for file in "style.css":
maker.load(file, os.path.join(settings.DMOJ_RESOURCES, file)) maker.load(file, os.path.join(settings.DMOJ_RESOURCES, file))
maker.make(debug=True) maker.make(debug=True)
if not maker.success: if not maker.success:

149
judge/markdown.py Normal file
View file

@ -0,0 +1,149 @@
import markdown as _markdown
import bleach
from django.utils.html import escape
from bs4 import BeautifulSoup
from pymdownx import superfences
from django.conf import settings
from urllib.parse import urlparse
from judge.markdown_extensions import YouTubeExtension, EmoticonExtension
EXTENSIONS = [
"pymdownx.arithmatex",
"pymdownx.magiclink",
"pymdownx.betterem",
"pymdownx.details",
"pymdownx.emoji",
"pymdownx.inlinehilite",
"pymdownx.superfences",
"pymdownx.highlight",
"pymdownx.tasklist",
"markdown.extensions.footnotes",
"markdown.extensions.attr_list",
"markdown.extensions.def_list",
"markdown.extensions.tables",
"markdown.extensions.admonition",
"nl2br",
"mdx_breakless_lists",
YouTubeExtension(),
EmoticonExtension(),
]
EXTENSION_CONFIGS = {
"pymdownx.arithmatex": {
"generic": True,
},
"pymdownx.superfences": {
"custom_fences": [
{
"name": "sample",
"class": "no-border",
"format": superfences.fence_code_format,
}
],
},
"pymdownx.highlight": {
"auto_title": True,
"auto_title_map": {
"Text Only": "",
},
},
}
ALLOWED_TAGS = list(bleach.sanitizer.ALLOWED_TAGS) + [
"img",
"center",
"iframe",
"div",
"span",
"table",
"tr",
"td",
"th",
"tr",
"pre",
"code",
"p",
"hr",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"thead",
"tbody",
"sup",
"dl",
"dt",
"dd",
"br",
"details",
"summary",
]
ALLOWED_ATTRS = [
"src",
"width",
"height",
"href",
"class",
"open",
"title",
"frameborder",
"allow",
"allowfullscreen",
"loading",
]
def _wrap_img_iframe_with_lazy_load(soup):
for img in soup.findAll("img"):
if img.get("src"):
img["loading"] = "lazy"
for img in soup.findAll("iframe"):
if img.get("src"):
img["loading"] = "lazy"
return soup
def _wrap_images_with_featherlight(soup):
for img in soup.findAll("img"):
if img.get("src"):
link = soup.new_tag("a", href=img["src"], **{"data-featherlight": "image"})
img.wrap(link)
return soup
def _open_external_links_in_new_tab(soup):
domain = settings.SITE_DOMAIN.lower()
for a in soup.findAll("a", href=True):
href = a["href"]
if href.startswith("http://") or href.startswith("https://"):
link_domain = urlparse(href).netloc.lower()
if link_domain != domain:
a["target"] = "_blank"
return soup
def markdown(value, lazy_load=False):
extensions = EXTENSIONS
html = _markdown.markdown(
value, extensions=extensions, extension_configs=EXTENSION_CONFIGS
)
html = bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRS)
if not html:
html = escape(value)
soup = BeautifulSoup(html, features="html.parser")
if lazy_load:
soup = _wrap_img_iframe_with_lazy_load(soup)
soup = _wrap_images_with_featherlight(soup)
soup = _open_external_links_in_new_tab(soup)
html = str(soup)
return '<div class="md-typeset content-description">%s</div>' % html

View file

@ -0,0 +1,2 @@
from .youtube import YouTubeExtension
from .emoticon import EmoticonExtension

View file

@ -0,0 +1,112 @@
import markdown
from markdown.extensions import Extension
from markdown.inlinepatterns import InlineProcessor
import xml.etree.ElementTree as etree
import re
EMOTICON_EMOJI_MAP = {
":D": "\U0001F603", # Smiling Face with Open Mouth
":)": "\U0001F642", # Slightly Smiling Face
":-)": "\U0001F642", # Slightly Smiling Face with Nose
":(": "\U0001F641", # Slightly Frowning Face
":-(": "\U0001F641", # Slightly Frowning Face with Nose
";)": "\U0001F609", # Winking Face
";-)": "\U0001F609", # Winking Face with Nose
":P": "\U0001F61B", # Face with Tongue
":-P": "\U0001F61B", # Face with Tongue and Nose
":p": "\U0001F61B", # Face with Tongue
":-p": "\U0001F61B", # Face with Tongue and Nose
";P": "\U0001F61C", # Winking Face with Tongue
";-P": "\U0001F61C", # Winking Face with Tongue and Nose
";p": "\U0001F61C", # Winking Face with Tongue
";-p": "\U0001F61C", # Winking Face with Tongue and Nose
":'(": "\U0001F622", # Crying Face
":o": "\U0001F62E", # Face with Open Mouth
":-o": "\U0001F62E", # Face with Open Mouth and Nose
":O": "\U0001F62E", # Face with Open Mouth
":-O": "\U0001F62E", # Face with Open Mouth and Nose
":-0": "\U0001F62E", # Face with Open Mouth and Nose
">:(": "\U0001F620", # Angry Face
">:-(": "\U0001F620", # Angry Face with Nose
">:)": "\U0001F608", # Smiling Face with Horns
">:-)": "\U0001F608", # Smiling Face with Horns and Nose
"XD": "\U0001F606", # Grinning Squinting Face
"xD": "\U0001F606", # Grinning Squinting Face
"B)": "\U0001F60E", # Smiling Face with Sunglasses
"B-)": "\U0001F60E", # Smiling Face with Sunglasses and Nose
"O:)": "\U0001F607", # Smiling Face with Halo
"O:-)": "\U0001F607", # Smiling Face with Halo and Nose
"0:)": "\U0001F607", # Smiling Face with Halo
"0:-)": "\U0001F607", # Smiling Face with Halo and Nose
">:P": "\U0001F92A", # Zany Face (sticking out tongue and winking)
">:-P": "\U0001F92A", # Zany Face with Nose
">:p": "\U0001F92A", # Zany Face (sticking out tongue and winking)
">:-p": "\U0001F92A", # Zany Face with Nose
":/": "\U0001F615", # Confused Face
":-/": "\U0001F615", # Confused Face with Nose
":\\": "\U0001F615", # Confused Face
":-\\": "\U0001F615", # Confused Face with Nose
"3:)": "\U0001F608", # Smiling Face with Horns
"3:-)": "\U0001F608", # Smiling Face with Horns and Nose
"<3": "\u2764\uFE0F", # Red Heart
"</3": "\U0001F494", # Broken Heart
":*": "\U0001F618", # Face Blowing a Kiss
":-*": "\U0001F618", # Face Blowing a Kiss with Nose
";P": "\U0001F61C", # Winking Face with Tongue
";-P": "\U0001F61C",
">:P": "\U0001F61D", # Face with Stuck-Out Tongue and Tightly-Closed Eyes
":-/": "\U0001F615", # Confused Face
":/": "\U0001F615",
":\\": "\U0001F615",
":-\\": "\U0001F615",
":|": "\U0001F610", # Neutral Face
":-|": "\U0001F610",
"8)": "\U0001F60E", # Smiling Face with Sunglasses
"8-)": "\U0001F60E",
"O:)": "\U0001F607", # Smiling Face with Halo
"O:-)": "\U0001F607",
":3": "\U0001F60A", # Smiling Face with Smiling Eyes
"^.^": "\U0001F60A",
"-_-": "\U0001F611", # Expressionless Face
"T_T": "\U0001F62D", # Loudly Crying Face
"T.T": "\U0001F62D",
">.<": "\U0001F623", # Persevering Face
"x_x": "\U0001F635", # Dizzy Face
"X_X": "\U0001F635",
":]": "\U0001F600", # Grinning Face
":[": "\U0001F641", # Slightly Frowning Face
"=]": "\U0001F600",
"=[": "\U0001F641",
"D:<": "\U0001F621", # Pouting Face
"D:": "\U0001F629", # Weary Face
"D=": "\U0001F6AB", # No Entry Sign (sometimes used to denote dismay or frustration)
":'D": "\U0001F602", # Face with Tears of Joy
"D':": "\U0001F625", # Disappointed but Relieved Face
"D8": "\U0001F631", # Face Screaming in Fear
"-.-": "\U0001F644", # Face with Rolling Eyes
"-_-;": "\U0001F612", # Unamused
}
class EmoticonEmojiInlineProcessor(InlineProcessor):
def handleMatch(self, m, data):
emoticon = m.group(1)
emoji = EMOTICON_EMOJI_MAP.get(emoticon, "")
if emoji:
el = etree.Element("span")
el.text = markdown.util.AtomicString(emoji)
el.set("class", "big-emoji")
return el, m.start(0), m.end(0)
else:
return None, m.start(0), m.end(0)
class EmoticonExtension(Extension):
def extendMarkdown(self, md):
emoticon_pattern = (
r"(?:(?<=\s)|^)" # Lookbehind for a whitespace character or the start of the string
r"(" + "|".join(map(re.escape, EMOTICON_EMOJI_MAP.keys())) + r")"
r"(?=\s|$)" # Lookahead for a whitespace character or the end of the string
)
emoticon_processor = EmoticonEmojiInlineProcessor(emoticon_pattern, md)
md.inlinePatterns.register(emoticon_processor, "emoticon_to_emoji", 1)

View file

@ -0,0 +1,36 @@
import markdown
from markdown.inlinepatterns import InlineProcessor
from markdown.extensions import Extension
import xml.etree.ElementTree as etree
YOUTUBE_REGEX = (
r"(https?://)?(www\.)?" "(youtube\.com/watch\?v=|youtu\.be/)" "([\w-]+)(&[\w=]*)?"
)
class YouTubeEmbedProcessor(InlineProcessor):
def handleMatch(self, m, data):
youtube_id = m.group(4)
if not youtube_id:
return None, None, None
# Create an iframe element with the YouTube embed URL
iframe = etree.Element("iframe")
iframe.set("width", "100%")
iframe.set("height", "360")
iframe.set("src", f"https://www.youtube.com/embed/{youtube_id}")
iframe.set("frameborder", "0")
iframe.set("allowfullscreen", "true")
center = etree.Element("center")
center.append(iframe)
# Return the iframe as the element to replace the match, along with the start and end indices
return center, m.start(0), m.end(0)
class YouTubeExtension(Extension):
def extendMarkdown(self, md):
# Create the YouTube link pattern
YOUTUBE_PATTERN = YouTubeEmbedProcessor(YOUTUBE_REGEX, md)
# Register the pattern to apply the YouTubeEmbedProcessor
md.inlinePatterns.register(YOUTUBE_PATTERN, "youtube", 175)

View file

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import lxml.html as lh import lxml.html as lh
from django.db import migrations from django.db import migrations
from lxml.html.clean import clean_html from lxml_html_clean import clean_html
def strip_error_html(apps, schema_editor): def strip_error_html(apps, schema_editor):

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,48 @@
# Generated by Django 3.2.18 on 2023-07-31 18:07
import django.core.validators
from django.db import migrations, models
import judge.models.problem_data
import judge.utils.problem_data
class Migration(migrations.Migration):
dependencies = [
("judge", "0155_output_only"),
]
operations = [
migrations.AddField(
model_name="problemdata",
name="signature_handler",
field=models.FileField(
blank=True,
null=True,
storage=judge.utils.problem_data.ProblemDataStorage(),
upload_to=judge.models.problem_data.problem_directory_file,
validators=[
django.core.validators.FileExtensionValidator(
allowed_extensions=["cpp"]
)
],
verbose_name="signature handler",
),
),
migrations.AddField(
model_name="problemdata",
name="signature_header",
field=models.FileField(
blank=True,
null=True,
storage=judge.utils.problem_data.ProblemDataStorage(),
upload_to=judge.models.problem_data.problem_directory_file,
validators=[
django.core.validators.FileExtensionValidator(
allowed_extensions=["h"]
)
],
verbose_name="signature header",
),
),
]

View file

@ -0,0 +1,22 @@
# Generated by Django 3.2.18 on 2023-08-01 04:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("judge", "0156_auto_20230801_0107"),
]
operations = [
migrations.AddField(
model_name="problemdata",
name="use_ioi_signature",
field=models.BooleanField(
help_text="Use IOI Signature",
null=True,
verbose_name="is IOI signature",
),
),
]

View file

@ -0,0 +1,70 @@
# Generated by Django 3.2.18 on 2023-08-03 07:40
from django.db import migrations, models
import django.db.models.deletion
from django.core.exceptions import ObjectDoesNotExist
def migrate_pagevote(apps, schema_editor):
PageVote = apps.get_model("judge", "PageVote")
Problem = apps.get_model("judge", "Problem")
Solution = apps.get_model("judge", "Solution")
BlogPost = apps.get_model("judge", "BlogPost")
Contest = apps.get_model("judge", "Contest")
for vote in PageVote.objects.all():
page = vote.page
try:
if page.startswith("p:"):
code = page[2:]
vote.linked_object = Problem.objects.get(code=code)
elif page.startswith("s:"):
code = page[2:]
vote.linked_object = Solution.objects.get(problem__code=code)
elif page.startswith("c:"):
key = page[2:]
vote.linked_object = Contest.objects.get(key=key)
elif page.startswith("b:"):
blog_id = page[2:]
vote.linked_object = BlogPost.objects.get(id=blog_id)
vote.save()
except ObjectDoesNotExist:
vote.delete()
class Migration(migrations.Migration):
dependencies = [
("contenttypes", "0002_remove_content_type_name"),
("judge", "0157_auto_20230801_1145"),
]
operations = [
migrations.AddField(
model_name="pagevote",
name="content_type",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="contenttypes.contenttype",
),
),
migrations.AddField(
model_name="pagevote",
name="object_id",
field=models.PositiveIntegerField(default=None),
preserve_default=False,
),
migrations.AlterUniqueTogether(
name="pagevote",
unique_together={("content_type", "object_id")},
),
migrations.AddIndex(
model_name="pagevote",
index=models.Index(
fields=["content_type", "object_id"],
name="judge_pagev_content_ed8899_idx",
),
),
migrations.RunPython(migrate_pagevote, migrations.RunPython.noop, atomic=True),
]

View file

@ -0,0 +1,23 @@
# Generated by Django 3.2.18 on 2023-08-03 08:18
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("contenttypes", "0002_remove_content_type_name"),
("judge", "0158_migrate_pagevote"),
]
operations = [
migrations.AlterField(
model_name="pagevote",
name="content_type",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="contenttypes.contenttype",
),
),
]

View file

@ -0,0 +1,80 @@
# Generated by Django 3.2.18 on 2023-08-03 08:32
from django.db import migrations, models
import django.db.models.deletion
from django.core.exceptions import ObjectDoesNotExist
def migrate_bookmark(apps, schema_editor):
BookMark = apps.get_model("judge", "BookMark")
Problem = apps.get_model("judge", "Problem")
Solution = apps.get_model("judge", "Solution")
BlogPost = apps.get_model("judge", "BlogPost")
Contest = apps.get_model("judge", "Contest")
for bookmark in BookMark.objects.all():
page = bookmark.page
try:
if page.startswith("p:"):
code = page[2:]
bookmark.linked_object = Problem.objects.get(code=code)
elif page.startswith("s:"):
code = page[2:]
bookmark.linked_object = Solution.objects.get(problem__code=code)
elif page.startswith("c:"):
key = page[2:]
bookmark.linked_object = Contest.objects.get(key=key)
elif page.startswith("b:"):
blog_id = page[2:]
bookmark.linked_object = BlogPost.objects.get(id=blog_id)
bookmark.save()
except ObjectDoesNotExist:
bookmark.delete()
class Migration(migrations.Migration):
dependencies = [
("contenttypes", "0002_remove_content_type_name"),
("judge", "0159_auto_20230803_1518"),
]
operations = [
migrations.AddField(
model_name="bookmark",
name="content_type",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="contenttypes.contenttype",
),
),
migrations.AddField(
model_name="bookmark",
name="object_id",
field=models.PositiveIntegerField(default=1),
),
migrations.AddField(
model_name="bookmark",
name="score",
field=models.IntegerField(default=0, verbose_name="votes"),
),
migrations.AlterUniqueTogether(
name="bookmark",
unique_together={("content_type", "object_id")},
),
migrations.AddIndex(
model_name="bookmark",
index=models.Index(
fields=["content_type", "object_id"],
name="judge_bookm_content_964329_idx",
),
),
migrations.AddIndex(
model_name="makebookmark",
index=models.Index(
fields=["user", "bookmark"], name="judge_makeb_user_id_f0e226_idx"
),
),
migrations.RunPython(migrate_bookmark, migrations.RunPython.noop, atomic=True),
]

View file

@ -0,0 +1,28 @@
# Generated by Django 3.2.18 on 2023-08-03 08:36
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("contenttypes", "0002_remove_content_type_name"),
("judge", "0160_migrate_bookmark"),
]
operations = [
migrations.AlterField(
model_name="bookmark",
name="content_type",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="contenttypes.contenttype",
),
),
migrations.AlterField(
model_name="bookmark",
name="object_id",
field=models.PositiveIntegerField(),
),
]

View file

@ -0,0 +1,21 @@
# Generated by Django 3.2.18 on 2023-08-24 00:50
from django.db import migrations, models
import judge.models.profile
class Migration(migrations.Migration):
dependencies = [
("judge", "0161_auto_20230803_1536"),
]
operations = [
migrations.AddField(
model_name="profile",
name="profile_image",
field=models.ImageField(
null=True, upload_to=judge.models.profile.profile_image_path
),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 3.2.18 on 2023-08-25 00:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("judge", "0162_profile_image"),
]
operations = [
migrations.AddField(
model_name="profile",
name="email_change_pending",
field=models.EmailField(blank=True, max_length=254, null=True),
),
]

View file

@ -0,0 +1,30 @@
# Generated by Django 3.2.18 on 2023-08-25 23:03
from django.db import migrations, models
def migrate_show_testcases(apps, schema_editor):
ContestProblem = apps.get_model("judge", "ContestProblem")
for c in ContestProblem.objects.all():
if c.output_prefix_override == 1:
c.show_testcases = True
c.save()
class Migration(migrations.Migration):
dependencies = [
("judge", "0163_email_change"),
]
operations = [
migrations.AddField(
model_name="contestproblem",
name="show_testcases",
field=models.BooleanField(default=False, verbose_name="visible testcases"),
),
migrations.RunPython(
migrate_show_testcases, migrations.RunPython.noop, atomic=True
),
]

View file

@ -0,0 +1,17 @@
# Generated by Django 3.2.18 on 2023-08-25 23:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("judge", "0164_show_testcase"),
]
operations = [
migrations.RemoveField(
model_name="contestproblem",
name="output_prefix_override",
),
]

View file

@ -0,0 +1,28 @@
# Generated by Django 3.2.18 on 2023-08-28 01:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("judge", "0165_drop_output_prefix_override"),
]
operations = [
migrations.AlterField(
model_name="profile",
name="display_rank",
field=models.CharField(
choices=[
("user", "Normal User"),
("setter", "Problem Setter"),
("admin", "Admin"),
],
db_index=True,
default="user",
max_length=10,
verbose_name="display rank",
),
),
]

View file

@ -0,0 +1,32 @@
# Generated by Django 3.2.18 on 2023-09-01 00:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("judge", "0166_display_rank_index"),
]
operations = [
migrations.AlterField(
model_name="contest",
name="format_name",
field=models.CharField(
choices=[
("atcoder", "AtCoder"),
("default", "Default"),
("ecoo", "ECOO"),
("icpc", "ICPC"),
("ioi", "IOI"),
("ioi16", "New IOI"),
("ultimate", "Ultimate"),
],
default="default",
help_text="The contest format module to use.",
max_length=32,
verbose_name="contest format",
),
),
]

View file

@ -0,0 +1,24 @@
# Generated by Django 3.2.18 on 2023-09-02 00:30
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("judge", "0167_ultimate_contest_format"),
]
operations = [
migrations.AddField(
model_name="profile",
name="css_background",
field=models.TextField(
blank=True,
help_text='CSS custom background properties: url("image_url"), color, etc',
max_length=300,
null=True,
verbose_name="Custom background",
),
),
]

View file

@ -0,0 +1,22 @@
# Generated by Django 3.2.18 on 2023-09-17 01:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("judge", "0168_css_background"),
]
operations = [
migrations.AddField(
model_name="contest",
name="public_scoreboard",
field=models.BooleanField(
default=False,
help_text="Ranking page is public even for private contests.",
verbose_name="public scoreboard",
),
),
]

View file

@ -0,0 +1,30 @@
# Generated by Django 3.2.21 on 2023-10-02 03:25
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("judge", "0169_public_scoreboard"),
]
operations = [
migrations.CreateModel(
name="ContestsSummary",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("scores", models.JSONField(blank=True, null=True)),
("key", models.CharField(max_length=20, unique=True)),
("contests", models.ManyToManyField(to="judge.Contest")),
],
),
]

View file

@ -0,0 +1,68 @@
# Generated by Django 3.2.18 on 2023-10-10 21:17
from django.db import migrations, models
import django.db.models.deletion
from django.urls import reverse
from collections import defaultdict
# Run this in shell
def migrate_notif(apps, schema_editor):
Notification = apps.get_model("judge", "Notification")
Profile = apps.get_model("judge", "Profile")
NotificationProfile = apps.get_model("judge", "NotificationProfile")
unread_count = defaultdict(int)
for c in Notification.objects.all():
if c.comment:
c.html_link = (
f'<a href="{c.comment.get_absolute_url()}">{c.comment.page_title}</a>'
)
c.author = c.comment.author
c.save()
if c.read is False:
unread_count[c.author] += 1
for user in unread_count:
np = NotificationProfile(user=user)
np.unread_count = unread_count[user]
np.save()
class Migration(migrations.Migration):
dependencies = [
("judge", "0170_contests_summary"),
]
operations = [
migrations.AlterModelOptions(
name="contestssummary",
options={
"verbose_name": "contests summary",
"verbose_name_plural": "contests summaries",
},
),
migrations.CreateModel(
name="NotificationProfile",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("unread_count", models.IntegerField(default=0)),
(
"user",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE, to="judge.profile"
),
),
],
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 3.2.18 on 2023-10-10 23:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("judge", "0171_update_notification"),
]
operations = [
migrations.AlterField(
model_name="profile",
name="rating",
field=models.IntegerField(db_index=True, default=None, null=True),
),
]

View file

@ -0,0 +1,25 @@
# Generated by Django 3.2.18 on 2023-10-14 00:53
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("judge", "0172_index_rating"),
]
operations = [
migrations.RunSQL(
(
"CREATE FULLTEXT INDEX IF NOT EXISTS code_name_index ON judge_problem (code, name)",
),
reverse_sql=migrations.RunSQL.noop,
),
migrations.RunSQL(
(
"CREATE FULLTEXT INDEX IF NOT EXISTS key_name_index ON judge_contest (`key`, name)",
),
reverse_sql=migrations.RunSQL.noop,
),
]

View file

@ -0,0 +1,50 @@
# Generated by Django 3.2.18 on 2023-11-24 05:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("judge", "0173_fulltext"),
]
operations = [
migrations.AddField(
model_name="contestssummary",
name="results",
field=models.JSONField(blank=True, null=True),
),
migrations.AlterField(
model_name="contest",
name="authors",
field=models.ManyToManyField(
help_text="These users will be able to edit the contest.",
related_name="_judge_contest_authors_+",
to="judge.Profile",
verbose_name="authors",
),
),
migrations.AlterField(
model_name="contest",
name="curators",
field=models.ManyToManyField(
blank=True,
help_text="These users will be able to edit the contest, but will not be listed as authors.",
related_name="_judge_contest_curators_+",
to="judge.Profile",
verbose_name="curators",
),
),
migrations.AlterField(
model_name="contest",
name="testers",
field=models.ManyToManyField(
blank=True,
help_text="These users will be able to view the contest, but not edit it.",
related_name="_judge_contest_testers_+",
to="judge.Profile",
verbose_name="testers",
),
),
]

View file

@ -0,0 +1,20 @@
# Generated by Django 3.2.18 on 2023-11-29 02:26
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("judge", "0174_contest_summary_result"),
]
operations = [
migrations.AddIndex(
model_name="profile",
index=models.Index(
fields=["is_unlisted", "performance_points"],
name="judge_profi_is_unli_d4034c_idx",
),
),
]

View file

@ -0,0 +1,30 @@
# Generated by Django 3.2.18 on 2023-12-06 01:28
from django.db import migrations, models
# Run this in shell
def migrate_revision(apps, schema_editor):
Comment = apps.get_model("judge", "Comment")
for c in Comment.objects.all():
c.revision_count = c.versions.count()
c.save()
class Migration(migrations.Migration):
dependencies = [
("judge", "0175_add_profile_index"),
]
operations = [
migrations.AddField(
model_name="comment",
name="revision_count",
field=models.PositiveIntegerField(default=1),
),
# migrations.RunPython(
# migrate_revision, migrations.RunPython.noop, atomic=True
# ),
]

View file

@ -0,0 +1,35 @@
from django.db import migrations, models
import judge.models.test_formatter
class Migration(migrations.Migration):
dependencies = [
("judge", "0176_comment_revision_count"),
]
operations = [
migrations.CreateModel(
name="TestFormatterModel",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"file",
models.FileField(
blank=True,
null=True,
upload_to=judge.models.test_formatter.test_formatter_path,
verbose_name="testcase file",
),
),
],
)
]

View file

@ -0,0 +1,17 @@
# Generated by Django 3.2.18 on 2024-01-14 01:04
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("judge", "0177_test_formatter"),
]
operations = [
migrations.RemoveField(
model_name="profile",
name="user_script",
),
]

View file

@ -0,0 +1,19 @@
# Generated by Django 3.2.18 on 2024-01-23 00:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("judge", "0178_remove_user_script"),
]
operations = [
migrations.AddIndex(
model_name="submission",
index=models.Index(
fields=["language", "result"], name="judge_submi_languag_874af4_idx"
),
),
]

View file

@ -0,0 +1,78 @@
# Generated by Django 3.2.18 on 2024-02-15 02:12
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("judge", "0179_submission_result_lang_index"),
]
operations = [
migrations.CreateModel(
name="CourseLesson",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("title", models.TextField(verbose_name="course title")),
("content", models.TextField(verbose_name="course content")),
("order", models.IntegerField(default=0, verbose_name="order")),
("points", models.IntegerField(verbose_name="points")),
],
),
migrations.RemoveField(
model_name="courseresource",
name="course",
),
migrations.RemoveField(
model_name="course",
name="ending_time",
),
migrations.AlterField(
model_name="courserole",
name="course",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="judge.course",
verbose_name="course",
),
),
migrations.DeleteModel(
name="CourseAssignment",
),
migrations.DeleteModel(
name="CourseResource",
),
migrations.AddField(
model_name="courselesson",
name="course",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="judge.course",
verbose_name="course",
),
),
migrations.AddField(
model_name="courselesson",
name="problems",
field=models.ManyToManyField(to="judge.Problem"),
),
migrations.AlterUniqueTogether(
name="courserole",
unique_together={("course", "user")},
),
migrations.AlterField(
model_name="courselesson",
name="problems",
field=models.ManyToManyField(blank=True, to="judge.Problem"),
),
]

View file

@ -0,0 +1,50 @@
# Generated by Django 3.2.18 on 2024-02-26 20:43
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("judge", "0180_course"),
]
operations = [
migrations.RemoveField(
model_name="profile",
name="math_engine",
),
migrations.AlterField(
model_name="course",
name="about",
field=models.TextField(verbose_name="course description"),
),
migrations.AlterField(
model_name="courselesson",
name="course",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="lessons",
to="judge.course",
verbose_name="course",
),
),
migrations.AlterField(
model_name="courselesson",
name="problems",
field=models.ManyToManyField(
blank=True, to="judge.Problem", verbose_name="problem"
),
),
migrations.AlterField(
model_name="courserole",
name="user",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="course_roles",
to="judge.profile",
verbose_name="user",
),
),
]

View file

@ -0,0 +1,75 @@
# Generated by Django 3.2.18 on 2024-03-19 04:28
from django.db import migrations, models
def migrate_checker(apps, schema_editor):
ProblemData = apps.get_model("judge", "ProblemData")
ProblemTestCase = apps.get_model("judge", "ProblemTestCase")
for p in ProblemData.objects.all():
if p.checker == "customval":
p.checker = "customcpp"
p.save()
for p in ProblemTestCase.objects.all():
if p.checker == "customval":
p.checker = "customcpp"
p.save()
class Migration(migrations.Migration):
dependencies = [
("judge", "0181_remove_math_engine"),
]
operations = [
migrations.RunPython(migrate_checker, migrations.RunPython.noop, atomic=True),
migrations.AlterField(
model_name="problemdata",
name="checker",
field=models.CharField(
blank=True,
choices=[
("standard", "Standard"),
("floats", "Floats"),
("floatsabs", "Floats (absolute)"),
("floatsrel", "Floats (relative)"),
("rstripped", "Non-trailing spaces"),
("sorted", "Unordered"),
("identical", "Byte identical"),
("linecount", "Line-by-line"),
("custom", "Custom checker (PY)"),
("customcpp", "Custom checker (CPP)"),
("interact", "Interactive"),
("testlib", "Testlib"),
],
max_length=10,
verbose_name="checker",
),
),
migrations.AlterField(
model_name="problemtestcase",
name="checker",
field=models.CharField(
blank=True,
choices=[
("standard", "Standard"),
("floats", "Floats"),
("floatsabs", "Floats (absolute)"),
("floatsrel", "Floats (relative)"),
("rstripped", "Non-trailing spaces"),
("sorted", "Unordered"),
("identical", "Byte identical"),
("linecount", "Line-by-line"),
("custom", "Custom checker (PY)"),
("customcpp", "Custom checker (CPP)"),
("interact", "Interactive"),
("testlib", "Testlib"),
],
max_length=10,
verbose_name="checker",
),
),
]

View file

@ -0,0 +1,45 @@
# Generated by Django 3.2.18 on 2024-03-19 04:45
import django.core.validators
from django.db import migrations, models
import judge.models.problem_data
import judge.utils.problem_data
def migrate_checker(apps, schema_editor):
ProblemData = apps.get_model("judge", "ProblemData")
for p in ProblemData.objects.all():
p.custom_checker_cpp = p.custom_validator
p.save()
class Migration(migrations.Migration):
dependencies = [
("judge", "0182_rename_customcpp"),
]
operations = [
migrations.AddField(
model_name="problemdata",
name="custom_checker_cpp",
field=models.FileField(
blank=True,
null=True,
storage=judge.utils.problem_data.ProblemDataStorage(),
upload_to=judge.models.problem_data.problem_directory_file,
validators=[
django.core.validators.FileExtensionValidator(
allowed_extensions=["cpp"]
)
],
verbose_name="custom cpp checker file",
),
),
migrations.RunPython(migrate_checker, migrations.RunPython.noop, atomic=True),
migrations.RemoveField(
model_name="problemdata",
name="custom_validator",
),
]

View file

@ -0,0 +1,28 @@
# Generated by Django 3.2.18 on 2024-03-23 04:07
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("judge", "0183_rename_custom_checker_cpp"),
]
operations = [
migrations.AddField(
model_name="contest",
name="rate_limit",
field=models.PositiveIntegerField(
blank=True,
help_text="Maximum number of submissions per minute. Leave empty if you don't want rate limit.",
null=True,
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(5),
],
verbose_name="rate limit",
),
),
]

View file

@ -0,0 +1,18 @@
# Generated by Django 3.2.18 on 2024-04-12 05:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("judge", "0184_contest_rate_limit"),
]
operations = [
migrations.RenameField(
model_name="organizationprofile",
old_name="users",
new_name="profile",
),
]

View file

@ -0,0 +1,43 @@
# Generated by Django 3.2.18 on 2024-04-12 17:04
from django.db import migrations, models
def truncate_about_text(apps, schema_editor):
Organization = apps.get_model("judge", "Organization")
Profile = apps.get_model("judge", "Profile")
for org in Organization.objects.all():
if len(org.about) > 10000:
org.about = org.about[:10000]
org.save()
for profile in Profile.objects.all():
if profile.about and len(profile.about) > 10000:
profile.about = profile.about[:10000]
profile.save()
class Migration(migrations.Migration):
dependencies = [
("judge", "0185_rename_org_profile_colum"),
]
operations = [
migrations.RunPython(truncate_about_text),
migrations.AlterField(
model_name="organization",
name="about",
field=models.CharField(
max_length=10000, verbose_name="organization description"
),
),
migrations.AlterField(
model_name="profile",
name="about",
field=models.CharField(
blank=True, max_length=10000, null=True, verbose_name="self-description"
),
),
]

View file

@ -0,0 +1,69 @@
# Generated by Django 3.2.18 on 2024-04-27 03:35
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("judge", "0186_change_about_fields_max_len"),
]
operations = [
migrations.RemoveField(
model_name="profile",
name="is_banned_problem_voting",
),
migrations.CreateModel(
name="ProfileInfo",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"tshirt_size",
models.CharField(
blank=True,
choices=[
("S", "Small (S)"),
("M", "Medium (M)"),
("L", "Large (L)"),
("XL", "Extra Large (XL)"),
("XXL", "2 Extra Large (XXL)"),
],
max_length=5,
null=True,
verbose_name="t-shirt size",
),
),
(
"date_of_birth",
models.DateField(
blank=True, null=True, verbose_name="date of birth"
),
),
(
"address",
models.CharField(
blank=True, max_length=255, null=True, verbose_name="address"
),
),
(
"profile",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name="info",
to="judge.profile",
verbose_name="profile associated",
),
),
],
),
]

View file

@ -0,0 +1,110 @@
# Generated by Django 3.2.18 on 2024-05-30 04:32
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("judge", "0187_profile_info"),
]
operations = [
migrations.CreateModel(
name="OfficialContestCategory",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"name",
models.CharField(
max_length=50,
unique=True,
verbose_name="official contest category",
),
),
],
options={
"verbose_name": "official contest category",
"verbose_name_plural": "official contest categories",
},
),
migrations.CreateModel(
name="OfficialContestLocation",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"name",
models.CharField(
max_length=50,
unique=True,
verbose_name="official contest location",
),
),
],
options={
"verbose_name": "official contest location",
"verbose_name_plural": "official contest locations",
},
),
migrations.CreateModel(
name="OfficialContest",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("year", models.PositiveIntegerField(verbose_name="year")),
(
"category",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="judge.officialcontestcategory",
verbose_name="contest category",
),
),
(
"contest",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name="official",
to="judge.contest",
verbose_name="contest",
),
),
(
"location",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="judge.officialcontestlocation",
verbose_name="contest location",
),
),
],
options={
"verbose_name": "official contest",
"verbose_name_plural": "official contests",
},
),
]

View file

@ -0,0 +1,21 @@
# Generated by Django 3.2.21 on 2024-07-08 00:05
from django.db import migrations, models
import judge.models.profile
class Migration(migrations.Migration):
dependencies = [
("judge", "0188_official_contest"),
]
operations = [
migrations.AddField(
model_name="organization",
name="organization_image",
field=models.ImageField(
null=True, upload_to=judge.models.profile.organization_image_path
),
),
]

View file

@ -0,0 +1,21 @@
# Generated by Django 3.2.18 on 2024-08-13 10:36
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("judge", "0189_organization_image"),
]
operations = [
migrations.RemoveField(
model_name="notification",
name="comment",
),
migrations.RemoveField(
model_name="notification",
name="read",
),
]

View file

@ -0,0 +1,17 @@
# Generated by Django 3.2.18 on 2024-08-22 03:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("judge", "0190_deprecate_old_notif_fields"),
]
operations = [
migrations.RemoveField(
model_name="organization",
name="logo_override_image",
),
]

View file

@ -0,0 +1,44 @@
# Generated by Django 3.2.21 on 2024-09-02 05:28
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("judge", "0191_deprecate_old_org_image"),
]
operations = [
migrations.CreateModel(
name="CourseLessonProblem",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("order", models.IntegerField(default=0, verbose_name="order")),
("score", models.IntegerField(default=0, verbose_name="score")),
(
"lesson",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="lesson_problems",
to="judge.courselesson",
),
),
(
"problem",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="judge.problem"
),
),
],
),
]

View file

@ -0,0 +1,29 @@
# Generated by Django 3.2.18 on 2024-09-03 14:33
from django.db import migrations, models
def migrate_problems_to_courselessonproblem(apps, schema_editor):
CourseLesson = apps.get_model("judge", "CourseLesson")
CourseLessonProblem = apps.get_model("judge", "CourseLessonProblem")
for lesson in CourseLesson.objects.all():
for problem in lesson.problems.all():
CourseLessonProblem.objects.create(
lesson=lesson, problem=problem, order=1, score=1
)
class Migration(migrations.Migration):
dependencies = [
("judge", "0192_course_lesson_problem"),
]
operations = [
migrations.RunPython(migrate_problems_to_courselessonproblem),
migrations.RemoveField(
model_name="courselesson",
name="problems",
),
]

View file

@ -0,0 +1,67 @@
# Generated by Django 3.2.21 on 2024-09-30 22:31
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("judge", "0193_remove_old_course_problems"),
]
operations = [
migrations.AddField(
model_name="contest",
name="is_in_course",
field=models.BooleanField(default=False, verbose_name="contest in course"),
),
migrations.AddField(
model_name="courselesson",
name="is_visible",
field=models.BooleanField(default=True, verbose_name="publicly visible"),
),
migrations.AlterField(
model_name="courselesson",
name="content",
field=models.TextField(verbose_name="lesson content"),
),
migrations.AlterField(
model_name="courselesson",
name="title",
field=models.TextField(verbose_name="lesson title"),
),
migrations.CreateModel(
name="CourseContest",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("order", models.IntegerField(default=0, verbose_name="order")),
("points", models.IntegerField(verbose_name="points")),
(
"contest",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="course",
to="judge.contest",
unique=True,
),
),
(
"course",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="contests",
to="judge.course",
),
),
],
),
]

View file

@ -1,9 +1,10 @@
import numpy as np import numpy as np
from django.conf import settings
import os import os
from django.core.cache import cache
import hashlib import hashlib
from django.core.cache import cache
from django.conf import settings
from judge.caching import cache_wrapper from judge.caching import cache_wrapper
@ -13,67 +14,69 @@ class CollabFilter:
# name = 'collab_filter' or 'collab_filter_time' # name = 'collab_filter' or 'collab_filter_time'
def __init__(self, name): def __init__(self, name):
embeddings = np.load( self.embeddings = np.load(
os.path.join(settings.ML_OUTPUT_PATH, name + "/embeddings.npz"), os.path.join(settings.ML_OUTPUT_PATH, name + "/embeddings.npz"),
allow_pickle=True, allow_pickle=True,
) )
arr0, arr1 = embeddings.files _, problem_arr = self.embeddings.files
self.name = name self.name = name
self.user_embeddings = embeddings[arr0] self.problem_embeddings = self.embeddings[problem_arr].item()
self.problem_embeddings = embeddings[arr1]
def __str__(self): def __str__(self):
return self.name return self.name
def compute_scores(self, query_embedding, item_embeddings, measure=DOT): def compute_scores(self, query_embedding, item_embeddings, measure=DOT):
"""Computes the scores of the candidates given a query. """Return {id: score}"""
Args:
query_embedding: a vector of shape [k], representing the query embedding.
item_embeddings: a matrix of shape [N, k], such that row i is the embedding
of item i.
measure: a string specifying the similarity measure to be used. Can be
either DOT or COSINE.
Returns:
scores: a vector of shape [N], such that scores[i] is the score of item i.
"""
u = query_embedding u = query_embedding
V = item_embeddings V = np.stack(list(item_embeddings.values()))
if measure == self.COSINE: if measure == self.COSINE:
V = V / np.linalg.norm(V, axis=1, keepdims=True) V = V / np.linalg.norm(V, axis=1, keepdims=True)
u = u / np.linalg.norm(u) u = u / np.linalg.norm(u)
scores = u.dot(V.T) scores = u.dot(V.T)
return scores scores_by_id = {id_: s for id_, s in zip(item_embeddings.keys(), scores)}
return scores_by_id
def _get_embedding_version(self):
first_problem = self.problem_embeddings[0]
array_bytes = first_problem.tobytes()
hash_object = hashlib.sha256(array_bytes)
hash_bytes = hash_object.digest()
return hash_bytes.hex()[:5]
@cache_wrapper(prefix="CFgue", timeout=86400)
def _get_user_embedding(self, user_id, embedding_version):
user_arr, _ = self.embeddings.files
user_embeddings = self.embeddings[user_arr].item()
if user_id not in user_embeddings:
return user_embeddings[0]
return user_embeddings[user_id]
def get_user_embedding(self, user_id):
version = self._get_embedding_version()
return self._get_user_embedding(user_id, version)
@cache_wrapper(prefix="user_recommendations", timeout=3600) @cache_wrapper(prefix="user_recommendations", timeout=3600)
def user_recommendations(self, user, problems, measure=DOT, limit=None): def user_recommendations(self, user_id, problems, measure=DOT, limit=None):
uid = user.id user_embedding = self.get_user_embedding(user_id)
if uid >= len(self.user_embeddings): scores = self.compute_scores(user_embedding, self.problem_embeddings, measure)
uid = 0
scores = self.compute_scores(
self.user_embeddings[uid], self.problem_embeddings, measure
)
res = [] # [(score, problem)] res = [] # [(score, problem)]
for pid in problems: for pid in problems:
# pid = problem.id if pid in scores:
if pid < len(scores):
res.append((scores[pid], pid)) res.append((scores[pid], pid))
res.sort(reverse=True, key=lambda x: x[0]) res.sort(reverse=True, key=lambda x: x[0])
res = res[:limit] return res[:limit]
return res
# return a list of pid # return a list of pid
def problem_neighbors(self, problem, problemset, measure=DOT, limit=None): def problem_neighbors(self, problem, problemset, measure=DOT, limit=None):
pid = problem.id pid = problem.id
if pid >= len(self.problem_embeddings): if pid not in self.problem_embeddings:
return [] return []
scores = self.compute_scores( embedding = self.problem_embeddings[pid]
self.problem_embeddings[pid], self.problem_embeddings, measure scores = self.compute_scores(embedding, self.problem_embeddings, measure)
)
res = [] res = []
for p in problemset: for p in problemset:
if p < len(scores): if p in scores:
res.append((scores[p], p)) res.append((scores[p], p))
res.sort(reverse=True, key=lambda x: x[0]) res.sort(reverse=True, key=lambda x: x[0])
return res[:limit] return res[:limit]

View file

@ -2,11 +2,9 @@ from reversion import revisions
from judge.models.choices import ( from judge.models.choices import (
ACE_THEMES, ACE_THEMES,
EFFECTIVE_MATH_ENGINES,
MATH_ENGINES_CHOICES,
TIMEZONE, TIMEZONE,
) )
from judge.models.comment import Comment, CommentLock, CommentVote, Notification from judge.models.comment import Comment, CommentLock, CommentVote
from judge.models.contest import ( from judge.models.contest import (
Contest, Contest,
ContestMoss, ContestMoss,
@ -16,6 +14,10 @@ from judge.models.contest import (
ContestTag, ContestTag,
Rating, Rating,
ContestProblemClarification, ContestProblemClarification,
ContestsSummary,
OfficialContestCategory,
OfficialContestLocation,
OfficialContest,
) )
from judge.models.interface import BlogPost, MiscConfig, NavigationBar, validate_regex from judge.models.interface import BlogPost, MiscConfig, NavigationBar, validate_regex
from judge.models.message import PrivateMessage, PrivateMessageThread from judge.models.message import PrivateMessage, PrivateMessageThread
@ -44,6 +46,7 @@ from judge.models.profile import (
Profile, Profile,
Friend, Friend,
OrganizationProfile, OrganizationProfile,
ProfileInfo,
) )
from judge.models.runtime import Judge, Language, RuntimeVersion from judge.models.runtime import Judge, Language, RuntimeVersion
from judge.models.submission import ( from judge.models.submission import (
@ -52,11 +55,21 @@ from judge.models.submission import (
SubmissionSource, SubmissionSource,
SubmissionTestCase, SubmissionTestCase,
) )
from judge.models.test_formatter import TestFormatterModel
from judge.models.ticket import Ticket, TicketMessage from judge.models.ticket import Ticket, TicketMessage
from judge.models.volunteer import VolunteerProblemVote from judge.models.volunteer import VolunteerProblemVote
from judge.models.pagevote import PageVote, PageVoteVoter from judge.models.pagevote import PageVote, PageVoteVoter
from judge.models.bookmark import BookMark, MakeBookMark from judge.models.bookmark import BookMark, MakeBookMark
from judge.models.course import Course from judge.models.course import (
Course,
CourseRole,
CourseLesson,
CourseLessonProblem,
CourseContest,
)
from judge.models.notification import Notification, NotificationProfile
from judge.models.test_formatter import TestFormatterModel
revisions.register(Profile, exclude=["points", "last_access", "ip", "rating"]) revisions.register(Profile, exclude=["points", "last_access", "ip", "rating"])
revisions.register(Problem, follow=["language_limits"]) revisions.register(Problem, follow=["language_limits"])

View file

@ -2,8 +2,11 @@ from django.db import models
from django.db.models import CASCADE from django.db.models import CASCADE
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from judge.models.profile import Profile from judge.models.profile import Profile
from judge.caching import cache_wrapper
__all__ = ["BookMark"] __all__ = ["BookMark"]
@ -13,40 +16,26 @@ class BookMark(models.Model):
max_length=30, max_length=30,
verbose_name=_("associated page"), verbose_name=_("associated page"),
db_index=True, db_index=True,
) ) # deprecated
score = models.IntegerField(verbose_name=_("votes"), default=0)
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
linked_object = GenericForeignKey("content_type", "object_id")
def get_bookmark(self, user): @cache_wrapper(prefix="BMgb")
userqueryset = MakeBookMark.objects.filter(bookmark=self, user=user) def is_bookmarked_by(self, user):
if userqueryset.exists(): return MakeBookMark.objects.filter(bookmark=self, user=user).exists()
return True
else:
return False
def page_object(self):
from judge.models.contest import Contest
from judge.models.interface import BlogPost
from judge.models.problem import Problem, Solution
try:
page = self.page
if page.startswith("p:"):
return Problem.objects.get(code=page[2:])
elif page.startswith("c:"):
return Contest.objects.get(key=page[2:])
elif page.startswith("b:"):
return BlogPost.objects.get(id=page[2:])
elif page.startswith("s:"):
return Solution.objects.get(problem__code=page[2:])
return None
except ObjectDoesNotExist:
return None
class Meta: class Meta:
verbose_name = _("bookmark") verbose_name = _("bookmark")
verbose_name_plural = _("bookmarks") verbose_name_plural = _("bookmarks")
indexes = [
models.Index(fields=["content_type", "object_id"]),
]
unique_together = ("content_type", "object_id")
def __str__(self): def __str__(self):
return self.page return f"bookmark for {self.linked_object}"
class MakeBookMark(models.Model): class MakeBookMark(models.Model):
@ -56,6 +45,30 @@ class MakeBookMark(models.Model):
) )
class Meta: class Meta:
indexes = [
models.Index(fields=["user", "bookmark"]),
]
unique_together = ["user", "bookmark"] unique_together = ["user", "bookmark"]
verbose_name = _("make bookmark") verbose_name = _("make bookmark")
verbose_name_plural = _("make bookmarks") verbose_name_plural = _("make bookmarks")
@cache_wrapper(prefix="gocb", expected_type=BookMark)
def _get_or_create_bookmark(content_type, object_id):
bookmark, created = BookMark.objects.get_or_create(
content_type=content_type,
object_id=object_id,
)
return bookmark
class Bookmarkable:
def get_or_create_bookmark(self):
content_type = ContentType.objects.get_for_model(self)
object_id = self.pk
return _get_or_create_bookmark(content_type, object_id)
def dirty_bookmark(bookmark, profile):
bookmark.is_bookmarked_by.dirty(bookmark, profile)
_get_or_create_bookmark.dirty(bookmark.content_type, bookmark.object_id)

View file

@ -54,13 +54,3 @@ ACE_THEMES = (
("vibrant_ink", "Vibrant Ink"), ("vibrant_ink", "Vibrant Ink"),
("xcode", "XCode"), ("xcode", "XCode"),
) )
MATH_ENGINES_CHOICES = (
("tex", _("Leave as LaTeX")),
("svg", _("SVG with PNG fallback")),
("mml", _("MathML only")),
("jax", _("MathJax with SVG/PNG fallback")),
("auto", _("Detect best quality")),
)
EFFECTIVE_MATH_ENGINES = ("svg", "mml", "tex", "jax")

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