Compare commits

..

3 commits

Author SHA1 Message Date
Phuoc Dinh Le
da2a11adf9 Revert "Revert "Change comment style (#67)" (#68)"
This reverts commit 0494a36681.
2023-05-20 08:54:17 +09:00
Phuoc Dinh Le
0494a36681
Revert "Change comment style (#67)" (#68)
This reverts commit 411f3da45e.
2023-05-20 08:53:43 +09:00
Dung T.Bui
411f3da45e
Change comment style (#67) 2023-05-20 08:52:37 +09:00
2974 changed files with 17545 additions and 39469 deletions

0
.browserslistrc Normal file → Executable file
View file

0
.flake8 Normal file → Executable file
View file

0
.github/workflows/init.yml vendored Normal file → Executable file
View file

0
.gitignore vendored Normal file → Executable file
View file

4
.pre-commit-config.yaml Normal file → Executable file
View file

@ -11,7 +11,3 @@ repos:
rev: 22.12.0
hooks:
- id: black
- repo: https://github.com/hadialqattan/pycln
rev: 'v2.3.0'
hooks:
- id: pycln

2
502.html Normal file → Executable file
View file

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

0
LICENSE Normal file → Executable file
View file

191
README.md Normal file → Executable file
View file

@ -31,197 +31,6 @@ 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.
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:
1. Missing the `local_settings.py`. You need to copy the `local_settings.py` in order to pass the check.

0
chat_box/__init__.py Normal file → Executable file
View file

3
chat_box/apps.py Normal file → Executable file
View file

@ -3,6 +3,3 @@ from django.apps import AppConfig
class ChatBoxConfig(AppConfig):
name = "chat_box"
def ready(self):
from . import models

0
chat_box/migrations/0001_initial.py Normal file → Executable file
View file

0
chat_box/migrations/0002_message_hidden.py Normal file → Executable file
View file

0
chat_box/migrations/0003_auto_20200505_2306.py Normal file → Executable file
View file

0
chat_box/migrations/0004_auto_20200505_2336.py Normal file → Executable file
View file

0
chat_box/migrations/0005_auto_20211011_0714.py Normal file → Executable file
View file

0
chat_box/migrations/0006_userroom.py Normal file → Executable file
View file

0
chat_box/migrations/0007_auto_20211112_1255.py Normal file → Executable file
View file

0
chat_box/migrations/0008_ignore.py Normal file → Executable file
View file

0
chat_box/migrations/0009_auto_20220618_1452.py Normal file → Executable file
View file

0
chat_box/migrations/0010_auto_20221028_0300.py Normal file → Executable file
View file

0
chat_box/migrations/0011_alter_message_hidden.py Normal file → Executable file
View file

0
chat_box/migrations/0012_auto_20230308_1417.py Normal file → Executable file
View file

View file

@ -1,20 +0,0 @@
# 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

@ -1,38 +0,0 @@
# 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

@ -1,33 +0,0 @@
# 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

@ -1,32 +0,0 @@
# 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")},
),
]

0
chat_box/migrations/__init__.py Normal file → Executable file
View file

65
chat_box/models.py Normal file → Executable file
View file

@ -1,11 +1,9 @@
from django.db import models
from django.db.models import CASCADE, Q
from django.db.models import CASCADE
from django.utils.translation import gettext_lazy as _
from django.utils.functional import cached_property
from judge.models.profile import Profile
from judge.caching import cache_wrapper
__all__ = ["Message", "Room", "UserRoom", "Ignore"]
@ -18,44 +16,20 @@ class Room(models.Model):
user_two = models.ForeignKey(
Profile, related_name="user_two", verbose_name="user 2", on_delete=CASCADE
)
last_msg_time = models.DateTimeField(
verbose_name=_("last seen"), null=True, db_index=True
)
class Meta:
app_label = "chat_box"
unique_together = ("user_one", "user_two")
@cached_property
def _cached_info(self):
return get_room_info(self.id)
def contain(self, profile):
return profile.id in [self.user_one_id, self.user_two_id]
return self.user_one == profile or self.user_two == profile
def other_user(self, profile):
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):
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):
author = models.ForeignKey(Profile, verbose_name=_("user"), on_delete=CASCADE)
time = models.DateTimeField(
verbose_name=_("posted time"), auto_now_add=True, db_index=True
)
time = models.DateTimeField(verbose_name=_("posted time"), auto_now_add=True)
body = models.TextField(verbose_name=_("body of comment"), max_length=8192)
hidden = models.BooleanField(verbose_name="is hidden", default=False)
room = models.ForeignKey(
@ -63,6 +37,7 @@ class Message(models.Model):
)
def save(self, *args, **kwargs):
new_message = self.id
self.body = self.body.strip()
super(Message, self).save(*args, **kwargs)
@ -73,7 +48,6 @@ class Message(models.Model):
indexes = [
models.Index(fields=["hidden", "room", "-id"]),
]
app_label = "chat_box"
class UserRoom(models.Model):
@ -82,11 +56,9 @@ class UserRoom(models.Model):
Room, verbose_name="room id", on_delete=CASCADE, default=None, null=True
)
last_seen = models.DateTimeField(verbose_name=_("last seen"), auto_now_add=True)
unread_count = models.IntegerField(default=0, db_index=True)
class Meta:
unique_together = ("user", "room")
app_label = "chat_box"
class Ignore(models.Model):
@ -99,15 +71,14 @@ class Ignore(models.Model):
)
ignored_users = models.ManyToManyField(Profile)
class Meta:
app_label = "chat_box"
@classmethod
def is_ignored(self, current_user, new_friend):
try:
return current_user.ignored_chat_users.ignored_users.filter(
id=new_friend.id
).exists()
return (
current_user.ignored_chat_users.get()
.ignored_users.filter(id=new_friend.id)
.exists()
)
except:
return False
@ -118,16 +89,6 @@ class Ignore(models.Model):
except:
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
def add_ignore(self, current_user, friend):
ignore, created = self.objects.get_or_create(user=current_user)
@ -144,11 +105,3 @@ class Ignore(models.Model):
self.remove_ignore(current_user, friend)
else:
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,
}

38
chat_box/utils.py Normal file → Executable file
View file

@ -1,14 +1,10 @@
from cryptography.fernet import Fernet
import hmac
import hashlib
from django.conf import settings
from django.db.models import OuterRef, Count, Subquery, IntegerField, Q
from django.db.models import OuterRef, Count, Subquery, IntegerField
from django.db.models.functions import Coalesce
from chat_box.models import Ignore, Message, UserRoom, Room
from judge.caching import cache_wrapper
from chat_box.models import Ignore, Message, UserRoom
secret_key = settings.CHAT_SECRET_KEY
fernet = Fernet(secret_key)
@ -28,23 +24,25 @@ def decrypt_url(message_encrypted):
return None, None
def encrypt_channel(channel):
return (
hmac.new(
settings.CHAT_SECRET_KEY.encode(),
channel.encode(),
hashlib.sha512,
).hexdigest()[:16]
+ "%s" % channel
def get_unread_boxes(profile):
ignored_users = Ignore.get_ignored_users(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")
)
@cache_wrapper(prefix="gub")
def get_unread_boxes(profile):
ignored_rooms = Ignore.get_ignored_rooms(profile)
unread_boxes = (
UserRoom.objects.filter(user=profile, unread_count__gt=0)
.exclude(room__in=ignored_rooms)
UserRoom.objects.filter(user=profile, room__isnull=False)
.annotate(
unread_count=Coalesce(Subquery(mess, output_field=IntegerField()), 0),
)
.filter(unread_count__gte=1)
.count()
)

284
chat_box/views.py Normal file → Executable file
View file

@ -21,23 +21,22 @@ from django.db.models import (
Exists,
Count,
IntegerField,
F,
Max,
)
from django.db.models.functions import Coalesce
from django.utils import timezone
from django.contrib.auth.decorators import login_required
from django.urls import reverse
import datetime
from judge import event_poster as event
from judge.jinja2.gravatar import gravatar
from judge.models import Friend
from chat_box.models import Message, Profile, Room, UserRoom, Ignore, get_room_info
from chat_box.utils import encrypt_url, decrypt_url, encrypt_channel, get_unread_boxes
from chat_box.models import Message, Profile, Room, UserRoom, Ignore
from chat_box.utils import encrypt_url, decrypt_url
from reversion import revisions
import json
class ChatView(ListView):
@ -50,8 +49,7 @@ class ChatView(ListView):
self.room_id = None
self.room = None
self.messages = None
self.first_page_size = 20 # only for first request
self.follow_up_page_size = 50
self.page_size = 20
def get_queryset(self):
return self.messages
@ -65,12 +63,10 @@ class ChatView(ListView):
def get(self, request, *args, **kwargs):
request_room = kwargs["room_id"]
page_size = self.follow_up_page_size
try:
last_id = int(request.GET.get("last_id"))
except Exception:
last_id = 1e15
page_size = self.first_page_size
only_messages = request.GET.get("only_messages")
if request_room:
@ -84,12 +80,11 @@ class ChatView(ListView):
request_room = None
self.room_id = request_room
self.messages = (
Message.objects.filter(hidden=False, room=self.room_id, id__lt=last_id)
.select_related("author")
.only("body", "time", "author__rating", "author__display_rank")[:page_size]
)
self.messages = Message.objects.filter(
hidden=False, room=self.room_id, id__lt=last_id
)[: self.page_size]
if not only_messages:
update_last_seen(request, **kwargs)
return super().get(request, *args, **kwargs)
return render(
@ -106,14 +101,10 @@ class ChatView(ListView):
context["title"] = self.title
context["last_msg"] = event.last()
context["status_sections"] = get_status_context(self.request.profile)
context["status_sections"] = get_status_context(self.request)
context["room"] = self.room_id
context["has_next"] = self.has_next()
context["unread_count_lobby"] = get_unread_count(None, self.request.profile)
context["chat_channel"] = encrypt_channel(
"chat_" + str(self.request.profile.id)
)
context["chat_lobby_channel"] = encrypt_channel("chat_lobby")
if self.room:
users_room = [self.room.user_one, self.room.user_two]
users_room.remove(self.request.profile)
@ -139,15 +130,15 @@ def delete_message(request):
if request.method == "GET":
return HttpResponseBadRequest()
if not request.user.is_staff:
return HttpResponseBadRequest()
try:
messid = int(request.POST.get("message"))
mess = Message.objects.get(id=messid)
except:
return HttpResponseBadRequest()
if not request.user.is_staff and request.profile != mess.author:
return HttpResponseBadRequest()
mess.hidden = True
mess.save()
@ -169,57 +160,26 @@ def mute_message(request):
except:
return HttpResponseBadRequest()
with revisions.create_revision():
revisions.set_comment(_("Mute chat") + ": " + mess.body)
revisions.set_user(request.user)
mess.author.mute = True
mess.author.save()
Message.objects.filter(room=None, author=mess.author).update(hidden=True)
return JsonResponse(ret)
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
def post_message(request):
ret = {"msg": "posted"}
if request.method != "POST":
return HttpResponseBadRequest()
if len(request.POST["body"]) > 5000 or len(request.POST["body"].strip()) == 0:
if len(request.POST["body"]) > 5000:
return HttpResponseBadRequest()
room = None
if request.POST["room"]:
room = Room.objects.get(id=request.POST["room"])
if not check_valid_message(request, room):
if not can_access_room(request, room) or request.profile.mute:
return HttpResponseBadRequest()
new_message = Message(author=request.profile, body=request.POST["body"], room=room)
@ -227,7 +187,7 @@ def post_message(request):
if not room:
event.post(
encrypt_channel("chat_lobby"),
"chat_lobby",
{
"type": "lobby",
"author_id": request.profile.id,
@ -237,13 +197,9 @@ def post_message(request):
},
)
else:
get_room_info.dirty(room.id)
room.last_msg_time = new_message.time
room.save()
for user in room.users():
event.post(
encrypt_channel("chat_" + str(user.id)),
"chat_" + str(user.id),
{
"type": "private",
"author_id": request.profile.id,
@ -252,17 +208,14 @@ def post_message(request):
"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)
def can_access_room(request, room):
return not room or room.contain(request.profile)
return (
not room or room.user_one == request.profile or room.user_two == request.profile
)
@login_required
@ -278,7 +231,7 @@ def chat_message_ajax(request):
try:
message = Message.objects.filter(hidden=False).get(id=message_id)
room = message.room
if not can_access_room(request, room):
if room and not room.contain(request.profile):
return HttpResponse("Unauthorized", status=401)
except Message.DoesNotExist:
return HttpResponseBadRequest()
@ -301,35 +254,35 @@ def update_last_seen(request, **kwargs):
room_id = request.POST.get("room")
else:
return HttpResponseBadRequest()
try:
profile = request.profile
room = None
if room_id:
room = Room.objects.filter(id=int(room_id)).first()
room = Room.objects.get(id=int(room_id))
except Room.DoesNotExist:
return HttpResponseBadRequest()
except Exception as e:
return HttpResponseBadRequest()
if not can_access_room(request, room):
if room and not room.contain(profile):
return HttpResponseBadRequest()
user_room, _ = UserRoom.objects.get_or_create(user=profile, room=room)
user_room.last_seen = timezone.now()
user_room.unread_count = 0
user_room.save()
get_unread_boxes.dirty(profile)
return JsonResponse({"msg": "updated"})
def get_online_count():
last_5_minutes = timezone.now() - timezone.timedelta(minutes=5)
return Profile.objects.filter(last_access__gte=last_5_minutes).count()
last_two_minutes = timezone.now() - timezone.timedelta(minutes=2)
return Profile.objects.filter(last_access__gte=last_two_minutes).count()
def get_user_online_status(user):
time_diff = timezone.now() - user.last_access
is_online = time_diff <= timezone.timedelta(minutes=5)
is_online = time_diff <= timezone.timedelta(minutes=2)
return is_online
@ -366,66 +319,47 @@ def user_online_status_ajax(request):
)
def get_online_status(profile, other_profile_ids, rooms=None):
if not other_profile_ids:
def get_online_status(request_user, queryset, rooms=None):
if not queryset:
return None
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)
last_two_minutes = timezone.now() - timezone.timedelta(minutes=2)
ret = []
if rooms:
unread_count = get_unread_count(rooms, profile)
count = {}
last_msg = {}
room_of_user = {}
for i in 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 other_profile in other_profiles:
is_online = False
if other_profile.last_access >= last_5_minutes:
is_online = True
user_dict = {"user": other_profile, "is_online": is_online}
if rooms:
user_dict.update(
{
"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)
unread_count = get_unread_count(rooms, request_user)
count = {}
for i in unread_count:
count[i["other_user"]] = i["unread_count"]
for user in queryset:
is_online = False
if user.last_access >= last_two_minutes:
is_online = True
user_dict = {"user": user, "is_online": is_online}
if rooms and user.id in count:
user_dict["unread_count"] = count[user.id]
user_dict["url"] = encrypt_url(request_user.id, user.id)
ret.append(user_dict)
return ret
def get_status_context(profile, include_ignored=False):
def get_status_context(request, include_ignored=False):
if include_ignored:
ignored_users = []
ignored_users = Profile.objects.none()
queryset = Profile.objects
else:
ignored_users = list(
Ignore.get_ignored_users(profile).values_list("id", flat=True)
)
ignored_users = Ignore.get_ignored_users(request.profile)
queryset = Profile.objects.exclude(id__in=ignored_users)
last_5_minutes = timezone.now() - timezone.timedelta(minutes=5)
last_two_minutes = timezone.now() - timezone.timedelta(minutes=2)
recent_profile = (
Room.objects.filter(Q(user_one=profile) | Q(user_two=profile))
Room.objects.filter(Q(user_one=request.profile) | Q(user_two=request.profile))
.annotate(
last_msg_time=Subquery(
Message.objects.filter(room=OuterRef("pk")).values("time")[:1]
),
other_user=Case(
When(user_one=profile, then="user_two"),
When(user_one=request.profile, then="user_two"),
default="user_one",
),
)
@ -435,24 +369,50 @@ def get_status_context(profile, include_ignored=False):
.values("other_user", "id")[:20]
)
recent_profile_ids = [str(i["other_user"]) for i in recent_profile]
recent_profile_id = [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]
Room.prefetch_room_cache(recent_rooms)
recent_list = None
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 = (
queryset.filter(display_rank="admin")
.exclude(id__in=recent_profile_ids)
.values_list("id", flat=True)
.exclude(id__in=friend_list)
.exclude(id__in=recent_profile_id)
)
all_user_status = (
queryset.filter(display_rank="user", last_access__gte=last_two_minutes)
.annotate(is_online=Case(default=True, output_field=BooleanField()))
.order_by("-rating")
.exclude(id__in=friend_list)
.exclude(id__in=admin_list)
.exclude(id__in=recent_profile_id)[:30]
)
return [
{
"title": _("Recent"),
"user_list": get_online_status(profile, recent_profile_ids, recent_rooms),
"title": "Recent",
"user_list": get_online_status(request.profile, recent_list, recent_rooms),
},
{
"title": _("Admin"),
"user_list": get_online_status(profile, admin_list),
"title": "Following",
"user_list": get_online_status(request.profile, friend_list),
},
{
"title": "Admin",
"user_list": get_online_status(request.profile, admin_list),
},
{
"title": "Other",
"user_list": get_online_status(request.profile, all_user_status),
},
]
@ -463,7 +423,7 @@ def online_status_ajax(request):
request,
"chat/online_status.html",
{
"status_sections": get_status_context(request.profile),
"status_sections": get_status_context(request),
"unread_count_lobby": get_unread_count(None, request.profile),
},
)
@ -487,6 +447,7 @@ def get_or_create_room(request):
return HttpResponseBadRequest()
request_id, other_id = decrypt_url(decrypted_other_id)
if not other_id or not request_id or request_id != request.profile.id:
return HttpResponseBadRequest()
@ -507,36 +468,55 @@ def get_or_create_room(request):
user_room.last_seen = timezone.now()
user_room.save()
room_url = reverse("chat", kwargs={"room_id": room.id})
if request.method == "GET":
return JsonResponse(
{
"room": room.id,
"other_user_id": other_user.id,
"url": room_url,
}
)
return HttpResponseRedirect(room_url)
return JsonResponse({"room": room.id, "other_user_id": other_user.id})
return HttpResponseRedirect(reverse("chat", kwargs={"room_id": room.id}))
def get_unread_count(rooms, user):
if rooms:
return UserRoom.objects.filter(
user=user, room__in=rooms, unread_count__gt=0
).values("unread_count", "room")
else: # lobby
user_room = UserRoom.objects.filter(user=user, room__isnull=True).first()
if not user_room:
return 0
last_seen = user_room.last_seen
res = (
Message.objects.filter(room__isnull=True, time__gte=last_seen)
mess = (
Message.objects.filter(
room=OuterRef("room"), time__gte=OuterRef("last_seen")
)
.exclude(author=user)
.exclude(hidden=True)
.count()
.order_by()
.values("room")
.annotate(unread_count=Count("pk"))
.values("unread_count")
)
return res
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
mess = (
Message.objects.filter(room__isnull=True, time__gte=OuterRef("last_seen"))
.exclude(author=user)
.order_by()
.values("room")
.annotate(unread_count=Count("pk"))
.values("unread_count")
)
res = (
UserRoom.objects.filter(user=user, room__isnull=True)
.annotate(
unread_count=Coalesce(Subquery(mess, output_field=IntegerField()), 0),
)
.values_list("unread_count", flat=True)
)
return res[0] if len(res) else 0
@login_required

0
django_2_2_pymysql_patch.py Normal file → Executable file
View file

0
django_ace/__init__.py Normal file → Executable file
View file

0
django_ace/static/django_ace/img/contract.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 304 B

After

Width:  |  Height:  |  Size: 304 B

0
django_ace/static/django_ace/img/expand.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 285 B

After

Width:  |  Height:  |  Size: 285 B

0
django_ace/static/django_ace/widget.css Normal file → Executable file
View file

0
django_ace/static/django_ace/widget.js Normal file → Executable file
View file

2
django_ace/widgets.py Normal file → Executable file
View file

@ -66,7 +66,7 @@ class AceWidget(forms.Textarea):
if self.toolbar:
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>"
).format(self.width)
html = toolbar + html

0
dmoj/__init__.py Normal file → Executable file
View file

0
dmoj/celery.py Normal file → Executable file
View file

55
dmoj/settings.py Normal file → Executable file
View file

@ -33,7 +33,6 @@ SITE_ID = 1
SITE_NAME = "LQDOJ"
SITE_LONG_NAME = "LQDOJ: Le Quy Don Online Judge"
SITE_ADMIN_EMAIL = False
SITE_DOMAIN = "lqdoj.edu.vn"
DMOJ_REQUIRE_STAFF_2FA = True
@ -84,9 +83,6 @@ DMOJ_STATS_SUBMISSION_RESULT_COLORS = {
"CE": "#42586d",
"ERR": "#ffa71c",
}
DMOJ_PROFILE_IMAGE_ROOT = "profile_images"
DMOJ_ORGANIZATION_IMAGE_ROOT = "organization_images"
DMOJ_TEST_FORMATTER_ROOT = "test_formatter"
MARKDOWN_STYLES = {}
MARKDOWN_DEFAULT_STYLE = {}
@ -132,10 +128,13 @@ USE_SELENIUM = False
SELENIUM_CUSTOM_CHROME_PATH = None
SELENIUM_CHROMEDRIVER_PATH = "chromedriver"
PYGMENT_THEME = "pygment-github.css"
INLINE_JQUERY = True
INLINE_FONTAWESOME = True
JQUERY_JS = "//ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"
FONTAWESOME_CSS = "//cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css"
FONTAWESOME_CSS = (
"//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css"
)
DMOJ_CANONICAL = ""
# Application definition
@ -169,7 +168,7 @@ else:
},
{
"model": "judge.Submission",
"icon": "fa-check-square",
"icon": "fa-check-square-o",
"children": [
"judge.Language",
"judge.Judge",
@ -220,6 +219,7 @@ else:
}
INSTALLED_APPS += (
"debug_toolbar",
"django.contrib.admin",
"judge",
"django.contrib.auth",
@ -249,6 +249,7 @@ INSTALLED_APPS += (
)
MIDDLEWARE = (
"debug_toolbar.middleware.DebugToolbarMiddleware",
"judge.middleware.SlowRequestMiddleware",
"judge.middleware.ShortCircuitMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
@ -277,11 +278,8 @@ LANGUAGE_COOKIE_AGE = 8640000
FORM_RENDERER = "django.forms.renderers.TemplatesSetting"
IMPERSONATE = {
"REQUIRE_SUPERUSER": True,
"DISABLE_LOGGING": True,
"ADMIN_DELETE_PERMISSION": True,
}
IMPERSONATE_REQUIRE_SUPERUSER = True
IMPERSONATE_DISABLE_LOGGING = True
ACCOUNT_ACTIVATION_DAYS = 7
@ -325,6 +323,7 @@ TEMPLATES = [
"judge.template_context.site",
"judge.template_context.site_name",
"judge.template_context.misc_config",
"judge.template_context.math_setting",
"social_django.context_processors.backends",
"social_django.context_processors.login_redirect",
],
@ -384,7 +383,6 @@ BRIDGED_JUDGE_ADDRESS = [("localhost", 9999)]
BRIDGED_JUDGE_PROXIES = None
BRIDGED_DJANGO_ADDRESS = [("localhost", 9998)]
BRIDGED_DJANGO_CONNECT = None
BRIDGED_AUTO_CREATE_JUDGE = False
# Event Server configuration
EVENT_DAEMON_USE = False
@ -425,14 +423,19 @@ STATICFILES_DIRS = [
STATIC_URL = "/static/"
# Define a cache
CACHES = {}
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache",
"LOCATION": "127.0.0.1:11211",
}
}
# Authentication
AUTHENTICATION_BACKENDS = (
"social_core.backends.google.GoogleOAuth2",
"social_core.backends.facebook.FacebookOAuth2",
"judge.social_auth.GitHubSecureEmailOAuth2",
"judge.authentication.CustomModelBackend",
"django.contrib.auth.backends.ModelBackend",
)
SOCIAL_AUTH_PIPELINE = (
@ -478,24 +481,16 @@ ML_OUTPUT_PATH = None
# Use subdomain for organizations
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:
with open(os.path.join(os.path.dirname(__file__), "local_settings.py")) as f:
exec(f.read(), globals())
except IOError:
pass
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
INTERNAL_IPS = [
# ...
"127.0.0.1",
# ...
]

0
dmoj/throttle_mail.py Normal file → Executable file
View file

168
dmoj/urls.py Normal file → Executable file
View file

@ -1,5 +1,6 @@
import chat_box.views as chat
from django.urls import include, path
from django.conf import settings
from django.conf.urls import include, url
from django.contrib import admin
@ -15,6 +16,14 @@ from django.contrib.auth.decorators import login_required
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.sitemap import (
BlogPostSitemap,
@ -36,8 +45,6 @@ from judge.views import (
language,
license,
mailgun,
markdown_editor,
test_formatter,
notification,
organization,
preview,
@ -59,13 +66,7 @@ from judge.views import (
internal,
resolver,
course,
email,
custom_file_upload,
)
from judge import authentication
from judge.views.test_formatter import test_formatter
from judge.views.problem_data import (
ProblemDataView,
ProblemSubmissionDiff,
@ -77,6 +78,7 @@ from judge.views.register import ActivationView, RegistrationView
from judge.views.select2 import (
AssigneeSelect2View,
ChatUserSearchSelect2View,
CommentSelect2View,
ContestSelect2View,
ContestUserSearchSelect2View,
OrganizationSelect2View,
@ -84,7 +86,6 @@ from judge.views.select2 import (
TicketUserSelect2View,
UserSearchSelect2View,
UserSelect2View,
ProblemAuthorSearchSelect2View,
)
admin.autodiscover()
@ -104,19 +105,19 @@ register_patterns = [
# confusing 404.
url(
r"^activate/(?P<activation_key>\w+)/$",
ActivationView.as_view(title=_("Activation key invalid")),
ActivationView.as_view(title="Activation key invalid"),
name="registration_activate",
),
url(
r"^register/$",
RegistrationView.as_view(title=_("Register")),
RegistrationView.as_view(title="Register"),
name="registration_register",
),
url(
r"^register/complete/$",
TitledTemplateView.as_view(
template_name="registration/registration_complete.html",
title=_("Registration Completed"),
title="Registration Completed",
),
name="registration_complete",
),
@ -124,7 +125,7 @@ register_patterns = [
r"^register/closed/$",
TitledTemplateView.as_view(
template_name="registration/registration_closed.html",
title=_("Registration not allowed"),
title="Registration not allowed",
),
name="registration_disallowed",
),
@ -141,7 +142,9 @@ register_patterns = [
url(r"^logout/$", user.UserLogoutView.as_view(), name="auth_logout"),
url(
r"^password/change/$",
authentication.CustomPasswordChangeView.as_view(),
auth_views.PasswordChangeView.as_view(
template_name="registration/password_change_form.html",
),
name="password_change",
),
url(
@ -181,17 +184,6 @@ register_patterns = [
),
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"^2fa/$", totp.TOTPLoginView.as_view(), name="login_2fa"),
url(r"^2fa/enable/$", totp.TOTPEnableView.as_view(), name="enable_2fa"),
@ -215,6 +207,7 @@ def paged_list_view(view, name, **kwargs):
urlpatterns = [
path('__debug__/', include('debug_toolbar.urls')),
url("", include("pagedown.urls")),
url(
r"^$",
@ -398,36 +391,10 @@ urlpatterns = [
name="submission_status",
),
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(
r"^submission_source_file/(?P<filename>(\w|\.)+)",
submission.SubmissionSourceFileView.as_view(),
@ -487,7 +454,6 @@ urlpatterns = [
reverse("all_user_submissions", args=[user])
),
),
url(r"^/toggle_follow/", user.toggle_follow, name="user_toggle_follow"),
url(
r"^/$",
lambda _, user: HttpResponsePermanentRedirect(
@ -535,58 +501,7 @@ urlpatterns = [
),
),
url(r"^contests/", paged_list_view(contests.ContestList, "contest_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(r"^course/", paged_list_view(course.CourseList, "course_list")),
url(
r"^contests/(?P<year>\d+)/(?P<month>\d+)/$",
contests.ContestCalendar.as_view(),
@ -629,6 +544,11 @@ urlpatterns = [
contests.ContestFinalRanking.as_view(),
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"^/leave$", contests.ContestLeave.as_view(), name="contest_leave"),
url(r"^/stats$", contests.ContestStats.as_view(), name="contest_stats"),
@ -645,13 +565,6 @@ urlpatterns = [
"contest_user_submissions_ajax",
),
),
url(
r"^/submissions",
paged_list_view(
submission.ContestSubmissions,
"contest_submissions",
),
),
url(
r"^/participations$",
contests.ContestParticipationList.as_view(),
@ -917,11 +830,6 @@ urlpatterns = [
AssigneeSelect2View.as_view(),
name="ticket_assignee_select2_ajax",
),
url(
r"^problem_authors$",
ProblemAuthorSearchSelect2View.as_view(),
name="problem_authors_select2_ajax",
),
]
),
),
@ -980,6 +888,19 @@ 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(
r"^stats/",
include(
@ -1080,6 +1001,9 @@ urlpatterns = [
url(
r"^contest/$", ContestSelect2View.as_view(), name="contest_select2"
),
url(
r"^comment/$", CommentSelect2View.as_view(), name="comment_select2"
),
]
),
),
@ -1155,11 +1079,6 @@ urlpatterns = [
internal.InternalProblem.as_view(),
name="internal_problem",
),
url(
r"^problem_votes$",
internal.get_problem_votes,
name="internal_problem_votes",
),
url(
r"^request_time$",
internal.InternalRequestTime.as_view(),
@ -1185,7 +1104,8 @@ urlpatterns = [
),
url(
r"^notifications/",
paged_list_view(notification.NotificationList, "notification"),
login_required(notification.NotificationList.as_view()),
name="notification",
),
url(
r"^import_users/",
@ -1215,7 +1135,6 @@ urlpatterns = [
),
),
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)
# if hasattr(settings, "INTERNAL_IPS"):
@ -1243,7 +1162,6 @@ favicon_paths = [
"favicon-32x32.png",
"favicon-16x16.png",
"android-chrome-192x192.png",
"android-chrome-512x512.png",
"android-chrome-48x48.png",
"mstile-310x150.png",
"apple-touch-icon-144x144.png",

0
dmoj/wsgi.py Normal file → Executable file
View file

0
dmoj/wsgi_async.py Normal file → Executable file
View file

0
dmoj_bridge_async.py Normal file → Executable file
View file

0
dmoj_celery.py Normal file → Executable file
View file

0
dmoj_install_pymysql.py Normal file → Executable file
View file

0
judge/__init__.py Normal file → Executable file
View file

28
judge/admin/__init__.py Normal file → Executable file
View file

@ -1,14 +1,8 @@
from django.contrib import admin
from django.contrib.admin.models import LogEntry
from django.contrib.auth.models import User
from judge.admin.comments import CommentAdmin
from judge.admin.contest import (
ContestAdmin,
ContestParticipationAdmin,
ContestTagAdmin,
ContestsSummaryAdmin,
)
from judge.admin.contest import ContestAdmin, ContestParticipationAdmin, ContestTagAdmin
from judge.admin.interface import (
BlogPostAdmin,
LicenseAdmin,
@ -17,18 +11,12 @@ from judge.admin.interface import (
)
from judge.admin.organization import OrganizationAdmin, OrganizationRequestAdmin
from judge.admin.problem import ProblemAdmin, ProblemPointsVoteAdmin
from judge.admin.profile import ProfileAdmin, UserAdmin
from judge.admin.profile import ProfileAdmin
from judge.admin.runtime import JudgeAdmin, LanguageAdmin
from judge.admin.submission import SubmissionAdmin
from judge.admin.taxon import (
ProblemGroupAdmin,
ProblemTypeAdmin,
OfficialContestCategoryAdmin,
OfficialContestLocationAdmin,
)
from judge.admin.taxon import ProblemGroupAdmin, ProblemTypeAdmin
from judge.admin.ticket import TicketAdmin
from judge.admin.volunteer import VolunteerProblemVoteAdmin
from judge.admin.course import CourseAdmin
from judge.models import (
BlogPost,
Comment,
@ -52,9 +40,6 @@ from judge.models import (
Ticket,
VolunteerProblemVote,
Course,
ContestsSummary,
OfficialContestCategory,
OfficialContestLocation,
)
@ -80,9 +65,4 @@ admin.site.register(Profile, ProfileAdmin)
admin.site.register(Submission, SubmissionAdmin)
admin.site.register(Ticket, TicketAdmin)
admin.site.register(VolunteerProblemVote, VolunteerProblemVoteAdmin)
admin.site.register(Course, CourseAdmin)
admin.site.unregister(User)
admin.site.register(User, UserAdmin)
admin.site.register(ContestsSummary, ContestsSummaryAdmin)
admin.site.register(OfficialContestCategory, OfficialContestCategoryAdmin)
admin.site.register(OfficialContestLocation, OfficialContestLocationAdmin)
admin.site.register(Course)

3
judge/admin/comments.py Normal file → Executable file
View file

@ -12,6 +12,7 @@ class CommentForm(ModelForm):
class Meta:
widgets = {
"author": AdminHeavySelect2Widget(data_view="profile_select2"),
"parent": AdminHeavySelect2Widget(data_view="comment_select2"),
}
if HeavyPreviewAdminPageDownWidget is not None:
widgets["body"] = HeavyPreviewAdminPageDownWidget(
@ -38,7 +39,7 @@ class CommentAdmin(VersionAdmin):
)
list_display = ["author", "linked_object", "time"]
search_fields = ["author__user__username", "body"]
readonly_fields = ["score", "parent"]
readonly_fields = ["score"]
actions = ["hide_comment", "unhide_comment"]
list_filter = ["hidden"]
actions_on_top = True

92
judge/admin/contest.py Normal file → Executable file
View file

@ -14,14 +14,7 @@ from reversion.admin import VersionAdmin
from reversion_compare.admin import CompareVersionAdmin
from django_ace import AceWidget
from judge.models import (
Contest,
ContestProblem,
ContestSubmission,
Profile,
Rating,
OfficialContest,
)
from judge.models import Contest, ContestProblem, ContestSubmission, Profile, Rating
from judge.ratings import rate_contest
from judge.widgets import (
AdminHeavySelect2MultipleWidget,
@ -31,8 +24,6 @@ from judge.widgets import (
AdminSelect2Widget,
HeavyPreviewAdminPageDownWidget,
)
from judge.views.contests import recalculate_contest_summary_result
from judge.utils.contest import maybe_trigger_contest_rescore
class AdminHeavySelect2Widget(AdminHeavySelect2Widget):
@ -80,6 +71,7 @@ class ContestProblemInlineForm(ModelForm):
"hidden_subtasks": TextInput(attrs={"size": "3"}),
"points": TextInput(attrs={"size": "1"}),
"order": TextInput(attrs={"size": "1"}),
"output_prefix_override": TextInput(attrs={"size": "1"}),
}
@ -94,7 +86,7 @@ class ContestProblemInline(admin.TabularInline):
"is_pretested",
"max_submissions",
"hidden_subtasks",
"show_testcases",
"output_prefix_override",
"order",
"rejudge_column",
)
@ -157,26 +149,6 @@ 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):
fieldsets = (
(None, {"fields": ("key", "name", "authors", "curators", "testers")}),
@ -187,11 +159,9 @@ class ContestAdmin(CompareVersionAdmin):
"is_visible",
"use_clarifications",
"hide_problem_tags",
"public_scoreboard",
"scoreboard_visibility",
"run_pretests_only",
"points_precision",
"rate_limit",
)
},
),
@ -251,7 +221,7 @@ class ContestAdmin(CompareVersionAdmin):
"user_count",
)
search_fields = ("key", "name")
inlines = [ContestProblemInline, OfficialContestInline]
inlines = [ContestProblemInline]
actions_on_top = True
actions_on_bottom = True
form = ContestForm
@ -311,14 +281,31 @@ class ContestAdmin(CompareVersionAdmin):
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):
super().save_related(request, form, formsets, change)
# Only rescored if we did not already do so in `save_model`
formset_changed = False
if any(formset.has_changed() for formset in formsets):
formset_changed = True
maybe_trigger_contest_rescore(form, form.instance, formset_changed)
if not self._rescored and any(formset.has_changed() for formset in formsets):
self._rescore(form.cleaned_data["key"])
obj = form.instance
obj.is_organization_private = obj.organizations.count() > 0
obj.is_private = obj.private_contestants.count() > 0
obj.save()
def has_change_permission(self, request, obj=None):
if not request.user.has_perm("judge.edit_own_contest"):
@ -327,6 +314,11 @@ class ContestAdmin(CompareVersionAdmin):
return True
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):
if not request.user.has_perm("judge.change_contest_visibility"):
queryset = queryset.filter(
@ -510,25 +502,3 @@ class ContestParticipationAdmin(admin.ModelAdmin):
show_virtual.short_description = _("virtual")
show_virtual.admin_order_field = "virtual"
class ContestsSummaryForm(ModelForm):
class Meta:
widgets = {
"contests": AdminHeavySelect2MultipleWidget(
data_view="contest_select2", attrs={"style": "width: 100%"}
),
}
class ContestsSummaryAdmin(admin.ModelAdmin):
fields = ("key", "contests", "scores")
list_display = ("key",)
search_fields = ("key", "contests__key")
form = ContestsSummaryForm
def save_model(self, request, obj, form, change):
super(ContestsSummaryAdmin, self).save_model(request, obj, form, change)
obj.refresh_from_db()
obj.results = recalculate_contest_summary_result(request, obj)
obj.save()

View file

@ -1,52 +0,0 @@
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

1
judge/admin/interface.py Normal file → Executable file
View file

@ -53,7 +53,6 @@ class NavigationBarAdmin(DraggableMPTTAdmin):
class BlogPostForm(ModelForm):
def __init__(self, *args, **kwargs):
super(BlogPostForm, self).__init__(*args, **kwargs)
if "authors" in self.fields:
self.fields["authors"].widget.can_add_related = False
class Meta:

1
judge/admin/organization.py Normal file → Executable file
View file

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

51
judge/admin/problem.py Normal file → Executable file
View file

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

88
judge/admin/profile.py Normal file → Executable file
View file

@ -1,19 +1,12 @@
from django.contrib import admin
from django.forms import ModelForm, CharField, TextInput
from django.forms import ModelForm
from django.utils.html import format_html
from django.utils.translation import gettext, gettext_lazy as _, ungettext
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 judge.models import Profile, ProfileInfo
from judge.widgets import AdminPagedownWidget, AdminSelect2Widget
from reversion.admin import VersionAdmin
import re
from django_ace import AceWidget
from judge.models import Profile
from judge.widgets import AdminPagedownWidget, AdminSelect2Widget
class ProfileForm(ModelForm):
@ -60,13 +53,6 @@ class TimezoneFilter(admin.SimpleListFilter):
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):
fields = (
"user",
@ -76,12 +62,15 @@ class ProfileAdmin(VersionAdmin):
"timezone",
"language",
"ace_theme",
"math_engine",
"last_access",
"ip",
"mute",
"is_unlisted",
"is_banned_problem_voting",
"notes",
"is_totp_enabled",
"user_script",
"current_contest",
)
readonly_fields = ("user",)
@ -102,7 +91,6 @@ class ProfileAdmin(VersionAdmin):
actions_on_top = True
actions_on_bottom = True
form = ProfileForm
inlines = (ProfileInfoInline,)
def get_queryset(self, request):
return super(ProfileAdmin, self).get_queryset(request).select_related("user")
@ -137,7 +125,7 @@ class ProfileAdmin(VersionAdmin):
admin_user_admin.short_description = _("User")
def email(self, obj):
return obj.email
return obj.user.email
email.admin_order_field = "user__email"
email.short_description = _("Email")
@ -171,57 +159,11 @@ class ProfileAdmin(VersionAdmin):
recalculate_points.short_description = _("Recalculate scores")
class UserForm(UserChangeForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["username"].help_text = _(
"Username can only contain letters, digits, and underscores."
def get_form(self, request, obj=None, **kwargs):
form = super(ProfileAdmin, self).get_form(request, obj, **kwargs)
if "user_script" in form.base_fields:
# form.base_fields['user_script'] does not exist when the user has only view permission on the model.
form.base_fields["user_script"].widget = AceWidget(
"javascript", request.profile.ace_theme
)
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
return form

0
judge/admin/runtime.py Normal file → Executable file
View file

7
judge/admin/submission.py Normal file → Executable file
View file

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

8
judge/admin/taxon.py Normal file → Executable file
View file

@ -56,11 +56,3 @@ class ProblemTypeAdmin(admin.ModelAdmin):
[o.pk for o in obj.problem_set.all()] if obj else []
)
return super(ProblemTypeAdmin, self).get_form(request, obj, **kwargs)
class OfficialContestCategoryAdmin(admin.ModelAdmin):
fields = ("name",)
class OfficialContestLocationAdmin(admin.ModelAdmin):
fields = ("name",)

0
judge/admin/ticket.py Normal file → Executable file
View file

0
judge/admin/volunteer.py Normal file → Executable file
View file

2
judge/apps.py Normal file → Executable file
View file

@ -12,7 +12,7 @@ class JudgeAppConfig(AppConfig):
# OPERATIONS MAY HAVE SIDE EFFECTS.
# DO NOT REMOVE THINKING THE IMPORT IS UNUSED.
# noinspection PyUnresolvedReferences
from . import models, signals, jinja2 # noqa: F401, imported for side effects
from . import signals, jinja2 # noqa: F401, imported for side effects
from django.contrib.flatpages.models import FlatPage
from django.contrib.flatpages.admin import FlatPageAdmin

View file

@ -1,48 +0,0 @@
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

0
judge/bridge/__init__.py Normal file → Executable file
View file

0
judge/bridge/base_handler.py Normal file → Executable file
View file

0
judge/bridge/daemon.py Normal file → Executable file
View file

0
judge/bridge/django_handler.py Normal file → Executable file
View file

0
judge/bridge/echo_test_client.py Normal file → Executable file
View file

0
judge/bridge/echo_test_server.py Normal file → Executable file
View file

123
judge/bridge/judge_handler.py Normal file → Executable file
View file

@ -3,6 +3,7 @@ import json
import logging
import threading
import time
import os
from collections import deque, namedtuple
from operator import itemgetter
@ -24,8 +25,6 @@ from judge.models import (
Submission,
SubmissionTestCase,
)
from judge.bridge.utils import VanishedSubmission
from judge.caching import cache_wrapper
logger = logging.getLogger("judge.bridge")
json_log = logging.getLogger("judge.json.bridge")
@ -65,10 +64,10 @@ class JudgeHandler(ZlibPacketHandler):
"handshake": self.on_handshake,
}
self._working = False
self._working_data = {}
self._no_response_job = None
self._problems = []
self.executors = {}
self.problems = set()
self.problems = {}
self.latency = None
self.time_delta = None
self.load = 1e100
@ -94,6 +93,12 @@ class JudgeHandler(ZlibPacketHandler):
def on_disconnect(self):
self._stop_ping.set()
if self._working:
logger.error(
"Judge %s disconnected while handling submission %s",
self.name,
self._working,
)
self.judges.remove(self)
if self.name is not None:
self._disconnected()
@ -105,29 +110,21 @@ class JudgeHandler(ZlibPacketHandler):
self._make_json_log(action="disconnect", info="judge disconnected")
)
if self._working:
self.judges.judge(
self._working,
self._working_data["problem"],
self._working_data["language"],
self._working_data["source"],
None,
0,
Submission.objects.filter(id=self._working).update(
status="IE", result="IE", error=""
)
json_log.error(
self._make_json_log(
sub=self._working,
action="close",
info="IE due to shutdown on grading",
)
)
def _authenticate(self, id, key):
try:
judge = Judge.objects.get(name=id)
judge = Judge.objects.get(name=id, is_blocked=False)
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
else:
result = hmac.compare_digest(judge.auth_key, key)
@ -140,52 +137,11 @@ class JudgeHandler(ZlibPacketHandler):
)
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):
judge = self.judge = Judge.objects.get(name=self.name)
judge.start_time = timezone.now()
judge.online = True
self._update_judge_problems()
judge.problems.set(Problem.objects.filter(code__in=list(self.problems.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
@ -220,8 +176,6 @@ class JudgeHandler(ZlibPacketHandler):
def _disconnected(self):
Judge.objects.filter(id=self.judge.id).update(online=False)
RuntimeVersion.objects.filter(judge=self.judge).delete()
self.judge.problems.clear()
_get_judge_problems.dirty(self.judge)
def _update_ping(self):
try:
@ -252,7 +206,8 @@ class JudgeHandler(ZlibPacketHandler):
return
self.timeout = 60
self._update_supported_problems(packet["problems"])
self._problems = packet["problems"]
self.problems = dict(self._problems)
self.executors = packet["executors"]
self.name = packet["id"]
@ -353,15 +308,7 @@ class JudgeHandler(ZlibPacketHandler):
def submit(self, id, problem, language, source):
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_data = {
"problem": problem,
"language": language,
"source": source,
}
self._no_response_job = threading.Timer(20, self._kill_if_no_response)
self.send(
{
@ -480,12 +427,14 @@ class JudgeHandler(ZlibPacketHandler):
def on_supported_problems(self, packet):
logger.info("%s: Updated problem list", self.name)
self._update_supported_problems(packet["problems"])
self._problems = packet["problems"]
self.problems = dict(self._problems)
if not self.working:
self.judges.update_problems(self)
self._update_judge_problems()
self.judge.problems.set(
Problem.objects.filter(code__in=list(self.problems.keys()))
)
json_log.info(
self._make_json_log(action="update-problems", count=len(self.problems))
)
@ -702,11 +651,8 @@ class JudgeHandler(ZlibPacketHandler):
self._free_self(packet)
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(
status="IE", result="IE", error=message
status="IE", result="IE", error=packet["message"]
):
event.post(
"sub_%s" % Submission.get_id_secret(id), {"type": "internal-error"}
@ -714,9 +660,9 @@ class JudgeHandler(ZlibPacketHandler):
self._post_update_submission(id, "internal-error", done=True)
json_log.info(
self._make_json_log(
sub=id,
packet,
action="internal-error",
message=message,
message=packet["message"],
finish=True,
result="IE",
)
@ -725,10 +671,10 @@ class JudgeHandler(ZlibPacketHandler):
logger.warning("Unknown submission: %s", id)
json_log.error(
self._make_json_log(
sub=id,
packet,
action="internal-error",
info="unknown submission",
message=message,
message=packet["message"],
finish=True,
result="IE",
)
@ -959,8 +905,3 @@ class JudgeHandler(ZlibPacketHandler):
def on_cleanup(self):
db.connection.close()
@cache_wrapper(prefix="gjp", timeout=3600)
def _get_judge_problems(judge):
return set(judge.problems.values_list("code", flat=True))

5
judge/bridge/judge_list.py Normal file → Executable file
View file

@ -3,8 +3,6 @@ from collections import namedtuple
from operator import attrgetter
from threading import RLock
from judge.bridge.utils import VanishedSubmission
try:
from llist import dllist
except ImportError:
@ -41,8 +39,6 @@ class JudgeList(object):
)
try:
judge.submit(id, problem, language, source)
except VanishedSubmission:
pass
except Exception:
logger.exception(
"Failed to dispatch %d (%s, %s) to %s",
@ -93,7 +89,6 @@ class JudgeList(object):
logger.info("Judge available after grading %d: %s", submission, judge.name)
del self.submission_map[submission]
judge._working = False
judge._working_data = {}
self._handle_free_judge(judge)
def abort(self, submission):

0
judge/bridge/server.py Normal file → Executable file
View file

View file

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

83
judge/caching.py Normal file → Executable file
View file

@ -1,16 +1,13 @@
from inspect import signature
from django.core.cache import cache, caches
from django.core.cache import cache
from django.db.models.query import QuerySet
from django.core.handlers.wsgi import WSGIRequest
import hashlib
from judge.logging import log_debug
MAX_NUM_CHAR = 50
NONE_RESULT = "__None__"
MAX_NUM_CHAR = 15
def cache_wrapper(prefix, timeout=86400):
def arg_to_str(arg):
if hasattr(arg, "id"):
return str(arg.id)
@ -20,97 +17,31 @@ def arg_to_str(arg):
return str(arg)[:MAX_NUM_CHAR]
return str(arg)
def filter_args(args_list):
return [x for x in args_list if not isinstance(x, WSGIRequest)]
l0_cache = caches["l0"] if "l0" in caches else None
def cache_wrapper(prefix, timeout=None, expected_type=None):
def get_key(func, *args, **kwargs):
args_list = list(args)
signature_args = list(signature(func).parameters.keys())
args_list += [kwargs.get(k) for k in signature_args[len(args) :]]
args_list = filter_args(args_list)
args_list = [arg_to_str(i) for i in args_list]
key = prefix + ":" + ":".join(args_list)
key = key.replace(" ", "_")
return key
def _get(key):
if not l0_cache:
return cache.get(key)
result = l0_cache.get(key)
if result is None:
result = cache.get(key)
return result
def _set_l0(key, value):
if l0_cache:
l0_cache.set(key, value, 30)
def _set(key, value, timeout):
_set_l0(key, value)
cache.set(key, value, timeout)
def decorator(func):
def _validate_type(cache_key, result):
if expected_type and not isinstance(result, expected_type):
data = {
"function": f"{func.__module__}.{func.__qualname__}",
"result": str(result)[:30],
"expected_type": expected_type,
"type": type(result),
"key": cache_key,
}
log_debug("invalid_key", data)
return False
return True
def wrapper(*args, **kwargs):
cache_key = get_key(func, *args, **kwargs)
result = _get(cache_key)
if result is not None and _validate_type(cache_key, result):
_set_l0(cache_key, result)
if type(result) == str and result == NONE_RESULT:
result = None
result = cache.get(cache_key)
if result is not None:
return result
result = func(*args, **kwargs)
if result is None:
cache_result = NONE_RESULT
else:
cache_result = result
_set(cache_key, cache_result, timeout)
cache.set(cache_key, result, timeout)
return result
def dirty(*args, **kwargs):
cache_key = get_key(func, *args, **kwargs)
cache.delete(cache_key)
if l0_cache:
l0_cache.delete(cache_key)
def prefetch_multi(args_list):
keys = []
for args in args_list:
keys.append(get_key(func, *args))
results = cache.get_many(keys)
for key, result in results.items():
if result is not None:
_set_l0(key, result)
def dirty_multi(args_list):
keys = []
for args in args_list:
keys.append(get_key(func, *args))
cache.delete_many(keys)
if l0_cache:
l0_cache.delete_many(keys)
wrapper.dirty = dirty
wrapper.prefetch_multi = prefetch_multi
wrapper.dirty_multi = dirty_multi
return wrapper

243
judge/comments.py Executable file
View file

@ -0,0 +1,243 @@
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,
Http404,
)
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):
pre_query = None
if "comment-id" in request.GET:
comment_id = int(request.GET["comment-id"])
try:
comment_obj = Comment.objects.get(pk=comment_id)
except Comment.DoesNotExist:
raise Http404
pre_query = comment_obj
while comment_obj is not None:
pre_query = comment_obj
comment_obj = comment_obj.parent
self.object = self.get_object()
return self.render_to_response(
self.get_context_data(
object=self.object,
pre_query=pre_query,
comment_form=CommentForm(request, initial={"parent": None}),
)
)
def get_context_data(self, pre_query=None, **kwargs):
context = super(CommentedDetailView, self).get_context_data(**kwargs)
queryset = self.object.comments
queryset = queryset.filter(parent=None, hidden=False)
queryset_all = None
comment_count = len(queryset)
context["comment_remove"] = -1
if (pre_query != None):
comment_remove = pre_query.id
queryset_all = pre_query.get_descendants(include_self=True)
queryset_all = (
queryset_all.select_related("author__user")
.filter(hidden=False)
.defer("author__about")
.annotate(revisions=Count("versions", distinct=True))
)
context["comment_remove"] = comment_remove
else:
queryset = (
queryset.select_related("author__user")
.defer("author__about")
.filter(hidden=False)
.annotate(
count_replies=Count("replies", distinct=True),
revisions=Count("versions", distinct=True),
)[:10]
)
if self.request.user.is_authenticated:
profile = self.request.profile
if (pre_query != None):
queryset_all = queryset_all.annotate(
my_vote=FilteredRelation(
"votes", condition=Q(votes__voter_id=profile.id)
),
).annotate(vote_score=Coalesce(F("my_vote__score"), Value(0)))
else:
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["has_comments"] = queryset.exists()
context["comment_lock"] = self.is_comment_locked()
context["comment_list"] = queryset
context["comment_all_list"] = queryset_all
context["vote_hide_threshold"] = settings.DMOJ_COMMENT_VOTE_HIDE_THRESHOLD
if queryset.exists():
context["comment_root_id"] = queryset[0].id
else:
context["comment_root_id"] = 0
context["comment_parrent_none"] = 1
if (pre_query != None):
context["offset"] = 1
else:
context["offset"] = 10
context["limit"] = 10
context["comment_count"] = comment_count
return context

1
judge/contest_format/__init__.py Normal file → Executable file
View file

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

0
judge/contest_format/atcoder.py Normal file → Executable file
View file

2
judge/contest_format/base.py Normal file → Executable file
View file

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

0
judge/contest_format/default.py Normal file → Executable file
View file

0
judge/contest_format/ecoo.py Normal file → Executable file
View file

0
judge/contest_format/icpc.py Normal file → Executable file
View file

0
judge/contest_format/ioi.py Normal file → Executable file
View file

0
judge/contest_format/new_ioi.py Normal file → Executable file
View file

0
judge/contest_format/registry.py Normal file → Executable file
View file

View file

@ -1,55 +0,0 @@
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

@ -1,22 +0,0 @@
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"),
]

0
judge/dblock.py Normal file → Executable file
View file

0
judge/event_poster.py Normal file → Executable file
View file

4
judge/event_poster_amqp.py Normal file → Executable file
View file

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

0
judge/event_poster_ws.py Normal file → Executable file
View file

120
judge/feed.py Executable file
View file

@ -0,0 +1,120 @@
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

4
judge/fixtures/demo.json Normal file → Executable file
View file

@ -8,6 +8,7 @@
"ip": "10.0.2.2",
"language": 1,
"last_access": "2017-12-02T08:57:10.093Z",
"math_engine": "auto",
"mute": false,
"organizations": [
1
@ -17,7 +18,8 @@
"problem_count": 0,
"rating": null,
"timezone": "America/Toronto",
"user": 1
"user": 1,
"user_script": ""
},
"model": "judge.profile",
"pk": 1

0
judge/fixtures/language_small.json Normal file → Executable file
View file

0
judge/fixtures/navbar.json Normal file → Executable file
View file

163
judge/forms.py Normal file → Executable file
View file

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

0
judge/fulltext.py Normal file → Executable file
View file

45
judge/highlight_code.py Normal file → Executable file
View file

@ -1,13 +1,44 @@
from django.utils.html import escape, mark_safe
from judge.markdown import markdown
__all__ = ["highlight_code"]
def highlight_code(code, language, linenos=True, title=None):
linenos_option = 'linenums="1"' if linenos else ""
title_option = f'title="{title}"' if title else ""
options = f"{{.{language} {linenos_option} {title_option}}}"
def _make_pre_code(code):
return mark_safe("<pre>" + escape(code) + "</pre>")
value = f"```{options}\n{code}\n```\n"
return mark_safe(markdown(value))
try:
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),
)
)

2
judge/jinja2/__init__.py Normal file → Executable file
View file

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

0
judge/jinja2/camo.py Normal file → Executable file
View file

0
judge/jinja2/chat.py Normal file → Executable file
View file

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