This commit is contained in:
2026-04-23 13:57:56 -06:00
parent 0c6de0babe
commit 028ea6e5f9
93 changed files with 321 additions and 1454 deletions

10
.flake8
View File

@@ -1,10 +0,0 @@
[flake8]
exclude =
xss_filter.py,
*/migrations/,
*settings.py
*/apps.py
venv/
max-line-length = 180
inline-quotes = "
no-accept-encodings = True

View File

@@ -2,10 +2,11 @@ import functools
import hashlib import hashlib
import time import time
from contest.models import Contest, ContestRuleType, ContestStatus, ContestType
from problem.models import Problem from problem.models import Problem
from contest.models import Contest, ContestType, ContestStatus, ContestRuleType from utils.api import APIError, JSONResponse
from utils.api import JSONResponse, APIError
from utils.constants import CONTEST_PASSWORD_SESSION_KEY from utils.constants import CONTEST_PASSWORD_SESSION_KEY
from .models import ProblemPermission from .models import ProblemPermission

View File

@@ -1,10 +1,10 @@
from django.conf import settings from django.conf import settings
from django.db import connection from django.db import connection
from django.utils.timezone import now
from django.utils.deprecation import MiddlewareMixin from django.utils.deprecation import MiddlewareMixin
from django.utils.timezone import now
from utils.api import JSONResponse
from account.models import User from account.models import User
from utils.api import JSONResponse
class APITokenAuthMiddleware(MiddlewareMixin): class APITokenAuthMiddleware(MiddlewareMixin):

View File

@@ -1,6 +1,7 @@
from django.contrib.auth.models import AbstractBaseUser
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import AbstractBaseUser
from django.db import models from django.db import models
from utils.models import JSONField from utils.models import JSONField

View File

@@ -1,6 +1,6 @@
from django import forms from django import forms
from utils.api import serializers, UsernameSerializer from utils.api import UsernameSerializer, serializers
from .models import AdminType, ProblemPermission, User, UserProfile from .models import AdminType, ProblemPermission, User, UserProfile

View File

@@ -1,8 +1,9 @@
import logging import logging
import dramatiq import dramatiq
from options.options import SysOptions from options.options import SysOptions
from utils.shortcuts import send_email, DRAMATIQ_WORKER_ARGS from utils.shortcuts import DRAMATIQ_WORKER_ARGS, send_email
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@@ -1,646 +0,0 @@
import time
from unittest import mock
from datetime import timedelta
from copy import deepcopy
from django.contrib import auth
from django.utils.timezone import now
from otpauth import OtpAuth
from utils.api.tests import APIClient, APITestCase
from utils.shortcuts import rand_str
from options.options import SysOptions
from .models import AdminType, ProblemPermission, User
from utils.constants import ContestRuleType
class PermissionDecoratorTest(APITestCase):
def setUp(self):
self.regular_user = User.objects.create(username="regular_user")
self.admin = User.objects.create(username="admin")
self.super_admin = User.objects.create(username="super_admin")
self.request = mock.MagicMock()
self.request.user.is_authenticated = mock.MagicMock()
def test_login_required(self):
self.request.user.is_authenticated.return_value = False
def test_admin_required(self):
pass
def test_super_admin_required(self):
pass
class DuplicateUserCheckAPITest(APITestCase):
def setUp(self):
user = self.create_user("test", "test123", login=False)
user.email = "test@test.com"
user.save()
self.url = self.reverse("check_username_or_email")
def test_duplicate_username(self):
resp = self.client.post(self.url, data={"username": "test"})
data = resp.data["data"]
self.assertEqual(data["username"], True)
resp = self.client.post(self.url, data={"username": "Test"})
self.assertEqual(resp.data["data"]["username"], True)
def test_ok_username(self):
resp = self.client.post(self.url, data={"username": "test1"})
data = resp.data["data"]
self.assertFalse(data["username"])
def test_duplicate_email(self):
resp = self.client.post(self.url, data={"email": "test@test.com"})
self.assertEqual(resp.data["data"]["email"], True)
resp = self.client.post(self.url, data={"email": "Test@Test.com"})
self.assertTrue(resp.data["data"]["email"])
def test_ok_email(self):
resp = self.client.post(self.url, data={"email": "aa@test.com"})
self.assertFalse(resp.data["data"]["email"])
class TFARequiredCheckAPITest(APITestCase):
def setUp(self):
self.url = self.reverse("tfa_required_check")
self.create_user("test", "test123", login=False)
def test_not_required_tfa(self):
resp = self.client.post(self.url, data={"username": "test"})
self.assertSuccess(resp)
self.assertEqual(resp.data["data"]["result"], False)
def test_required_tfa(self):
user = User.objects.first()
user.two_factor_auth = True
user.save()
resp = self.client.post(self.url, data={"username": "test"})
self.assertEqual(resp.data["data"]["result"], True)
class UserLoginAPITest(APITestCase):
def setUp(self):
self.username = self.password = "test"
self.user = self.create_user(username=self.username, password=self.password, login=False)
self.login_url = self.reverse("user_login_api")
def _set_tfa(self):
self.user.two_factor_auth = True
tfa_token = rand_str(32)
self.user.tfa_token = tfa_token
self.user.save()
return tfa_token
def test_login_with_correct_info(self):
response = self.client.post(self.login_url,
data={"username": self.username, "password": self.password})
self.assertDictEqual(response.data, {"error": None, "data": "Succeeded"})
user = auth.get_user(self.client)
self.assertTrue(user.is_authenticated)
def test_login_with_correct_info_upper_username(self):
resp = self.client.post(self.login_url, data={"username": self.username.upper(), "password": self.password})
self.assertDictEqual(resp.data, {"error": None, "data": "Succeeded"})
user = auth.get_user(self.client)
self.assertTrue(user.is_authenticated)
def test_login_with_wrong_info(self):
response = self.client.post(self.login_url,
data={"username": self.username, "password": "invalid_password"})
self.assertDictEqual(response.data, {"error": "error", "data": "Invalid username or password"})
user = auth.get_user(self.client)
self.assertFalse(user.is_authenticated)
def test_tfa_login(self):
token = self._set_tfa()
code = OtpAuth(token).totp()
if len(str(code)) < 6:
code = (6 - len(str(code))) * "0" + str(code)
response = self.client.post(self.login_url,
data={"username": self.username,
"password": self.password,
"tfa_code": code})
self.assertDictEqual(response.data, {"error": None, "data": "Succeeded"})
user = auth.get_user(self.client)
self.assertTrue(user.is_authenticated)
def test_tfa_login_wrong_code(self):
self._set_tfa()
response = self.client.post(self.login_url,
data={"username": self.username,
"password": self.password,
"tfa_code": "qqqqqq"})
self.assertDictEqual(response.data, {"error": "error", "data": "Invalid two factor verification code"})
user = auth.get_user(self.client)
self.assertFalse(user.is_authenticated)
def test_tfa_login_without_code(self):
self._set_tfa()
response = self.client.post(self.login_url,
data={"username": self.username,
"password": self.password})
self.assertDictEqual(response.data, {"error": "error", "data": "tfa_required"})
user = auth.get_user(self.client)
self.assertFalse(user.is_authenticated)
def test_user_disabled(self):
self.user.is_disabled = True
self.user.save()
resp = self.client.post(self.login_url, data={"username": self.username,
"password": self.password})
self.assertDictEqual(resp.data, {"error": "error", "data": "Your account has been disabled"})
class CaptchaTest(APITestCase):
def _set_captcha(self, session):
captcha = rand_str(4)
session["_django_captcha_key"] = captcha
session["_django_captcha_expires_time"] = int(time.time()) + 30
session.save()
return captcha
class UserRegisterAPITest(CaptchaTest):
def setUp(self):
self.client = APIClient()
self.register_url = self.reverse("user_register_api")
self.captcha = rand_str(4)
self.data = {"username": "test_user", "password": "testuserpassword",
"real_name": "real_name", "email": "test@qduoj.com",
"captcha": self._set_captcha(self.client.session)}
def test_website_config_limit(self):
SysOptions.allow_register = False
resp = self.client.post(self.register_url, data=self.data)
self.assertDictEqual(resp.data, {"error": "error", "data": "Register function has been disabled by admin"})
def test_invalid_captcha(self):
self.data["captcha"] = "****"
response = self.client.post(self.register_url, data=self.data)
self.assertDictEqual(response.data, {"error": "error", "data": "Invalid captcha"})
self.data.pop("captcha")
response = self.client.post(self.register_url, data=self.data)
self.assertTrue(response.data["error"] is not None)
def test_register_with_correct_info(self):
response = self.client.post(self.register_url, data=self.data)
self.assertDictEqual(response.data, {"error": None, "data": "Succeeded"})
def test_username_already_exists(self):
self.test_register_with_correct_info()
self.data["captcha"] = self._set_captcha(self.client.session)
self.data["email"] = "test1@qduoj.com"
response = self.client.post(self.register_url, data=self.data)
self.assertDictEqual(response.data, {"error": "error", "data": "Username already exists"})
def test_email_already_exists(self):
self.test_register_with_correct_info()
self.data["captcha"] = self._set_captcha(self.client.session)
self.data["username"] = "test_user1"
response = self.client.post(self.register_url, data=self.data)
self.assertDictEqual(response.data, {"error": "error", "data": "Email already exists"})
class SessionManagementAPITest(APITestCase):
def setUp(self):
self.create_user("test", "test123")
self.url = self.reverse("session_management_api")
# launch a request to provide session data
login_url = self.reverse("user_login_api")
self.client.post(login_url, data={"username": "test", "password": "test123"})
def test_get_sessions(self):
resp = self.client.get(self.url)
self.assertSuccess(resp)
data = resp.data["data"]
self.assertEqual(len(data), 1)
# def test_delete_session_key(self):
# resp = self.client.delete(self.url + "?session_key=" + self.session_key)
# self.assertSuccess(resp)
def test_delete_session_with_invalid_key(self):
resp = self.client.delete(self.url + "?session_key=aaaaaaaaaa")
self.assertDictEqual(resp.data, {"error": "error", "data": "Invalid session_key"})
class UserProfileAPITest(APITestCase):
def setUp(self):
self.url = self.reverse("user_profile_api")
def test_get_profile_without_login(self):
resp = self.client.get(self.url)
self.assertDictEqual(resp.data, {"error": None, "data": None})
def test_get_profile(self):
self.create_user("test", "test123")
resp = self.client.get(self.url)
self.assertSuccess(resp)
def test_update_profile(self):
self.create_user("test", "test123")
update_data = {"real_name": "zemal", "submission_number": 233, "language": "en-US"}
resp = self.client.put(self.url, data=update_data)
self.assertSuccess(resp)
data = resp.data["data"]
self.assertEqual(data["real_name"], "zemal")
self.assertEqual(data["submission_number"], 0)
self.assertEqual(data["language"], "en-US")
class TwoFactorAuthAPITest(APITestCase):
def setUp(self):
self.url = self.reverse("two_factor_auth_api")
self.create_user("test", "test123")
def _get_tfa_code(self):
user = User.objects.first()
code = OtpAuth(user.tfa_token).totp()
if len(str(code)) < 6:
code = (6 - len(str(code))) * "0" + str(code)
return code
def test_get_image(self):
resp = self.client.get(self.url)
self.assertSuccess(resp)
def test_open_tfa_with_invalid_code(self):
self.test_get_image()
resp = self.client.post(self.url, data={"code": "000000"})
self.assertDictEqual(resp.data, {"error": "error", "data": "Invalid code"})
def test_open_tfa_with_correct_code(self):
self.test_get_image()
code = self._get_tfa_code()
resp = self.client.post(self.url, data={"code": code})
self.assertSuccess(resp)
user = User.objects.first()
self.assertEqual(user.two_factor_auth, True)
def test_close_tfa_with_invalid_code(self):
self.test_open_tfa_with_correct_code()
resp = self.client.post(self.url, data={"code": "000000"})
self.assertDictEqual(resp.data, {"error": "error", "data": "Invalid code"})
def test_close_tfa_with_correct_code(self):
self.test_open_tfa_with_correct_code()
code = self._get_tfa_code()
resp = self.client.put(self.url, data={"code": code})
self.assertSuccess(resp)
user = User.objects.first()
self.assertEqual(user.two_factor_auth, False)
@mock.patch("account.views.oj.send_email_async.send")
class ApplyResetPasswordAPITest(CaptchaTest):
def setUp(self):
self.create_user("test", "test123", login=False)
user = User.objects.first()
user.email = "test@oj.com"
user.save()
self.url = self.reverse("apply_reset_password_api")
self.data = {"email": "test@oj.com", "captcha": self._set_captcha(self.client.session)}
def _refresh_captcha(self):
self.data["captcha"] = self._set_captcha(self.client.session)
def test_apply_reset_password(self, send_email_send):
resp = self.client.post(self.url, data=self.data)
self.assertSuccess(resp)
send_email_send.assert_called()
def test_apply_reset_password_twice_in_20_mins(self, send_email_send):
self.test_apply_reset_password()
send_email_send.reset_mock()
self._refresh_captcha()
resp = self.client.post(self.url, data=self.data)
self.assertDictEqual(resp.data, {"error": "error", "data": "You can only reset password once per 20 minutes"})
send_email_send.assert_not_called()
def test_apply_reset_password_again_after_20_mins(self, send_email_send):
self.test_apply_reset_password()
user = User.objects.first()
user.reset_password_token_expire_time = now() - timedelta(minutes=21)
user.save()
self._refresh_captcha()
self.test_apply_reset_password()
class ResetPasswordAPITest(CaptchaTest):
def setUp(self):
self.create_user("test", "test123", login=False)
self.url = self.reverse("reset_password_api")
user = User.objects.first()
user.reset_password_token = "online_judge?"
user.reset_password_token_expire_time = now() + timedelta(minutes=20)
user.save()
self.data = {"token": user.reset_password_token,
"captcha": self._set_captcha(self.client.session),
"password": "test456"}
def test_reset_password_with_correct_token(self):
resp = self.client.post(self.url, data=self.data)
self.assertSuccess(resp)
self.assertTrue(self.client.login(username="test", password="test456"))
def test_reset_password_with_invalid_token(self):
self.data["token"] = "aaaaaaaaaaa"
resp = self.client.post(self.url, data=self.data)
self.assertDictEqual(resp.data, {"error": "error", "data": "Token does not exist"})
def test_reset_password_with_expired_token(self):
user = User.objects.first()
user.reset_password_token_expire_time = now() - timedelta(seconds=30)
user.save()
resp = self.client.post(self.url, data=self.data)
self.assertDictEqual(resp.data, {"error": "error", "data": "Token has expired"})
class UserChangeEmailAPITest(APITestCase):
def setUp(self):
self.url = self.reverse("user_change_email_api")
self.user = self.create_user("test", "test123")
self.new_mail = "test@oj.com"
self.data = {"password": "test123", "new_email": self.new_mail}
def test_change_email_success(self):
resp = self.client.post(self.url, data=self.data)
self.assertSuccess(resp)
def test_wrong_password(self):
self.data["password"] = "aaaa"
resp = self.client.post(self.url, data=self.data)
self.assertDictEqual(resp.data, {"error": "error", "data": "Wrong password"})
def test_duplicate_email(self):
u = self.create_user("aa", "bb", login=False)
u.email = self.new_mail
u.save()
resp = self.client.post(self.url, data=self.data)
self.assertDictEqual(resp.data, {"error": "error", "data": "The email is owned by other account"})
class UserChangePasswordAPITest(APITestCase):
def setUp(self):
self.url = self.reverse("user_change_password_api")
# Create user at first
self.username = "test_user"
self.old_password = "testuserpassword"
self.new_password = "new_password"
self.user = self.create_user(username=self.username, password=self.old_password, login=False)
self.data = {"old_password": self.old_password, "new_password": self.new_password}
def _get_tfa_code(self):
user = User.objects.first()
code = OtpAuth(user.tfa_token).totp()
if len(str(code)) < 6:
code = (6 - len(str(code))) * "0" + str(code)
return code
def test_login_required(self):
response = self.client.post(self.url, data=self.data)
self.assertEqual(response.data, {"error": "permission-denied", "data": "Please login first"})
def test_valid_ola_password(self):
self.assertTrue(self.client.login(username=self.username, password=self.old_password))
response = self.client.post(self.url, data=self.data)
self.assertEqual(response.data, {"error": None, "data": "Succeeded"})
self.assertTrue(self.client.login(username=self.username, password=self.new_password))
def test_invalid_old_password(self):
self.assertTrue(self.client.login(username=self.username, password=self.old_password))
self.data["old_password"] = "invalid"
response = self.client.post(self.url, data=self.data)
self.assertEqual(response.data, {"error": "error", "data": "Invalid old password"})
def test_tfa_code_required(self):
self.user.two_factor_auth = True
self.user.tfa_token = "tfa_token"
self.user.save()
self.assertTrue(self.client.login(username=self.username, password=self.old_password))
self.data["tfa_code"] = rand_str(6)
resp = self.client.post(self.url, data=self.data)
self.assertEqual(resp.data, {"error": "error", "data": "Invalid two factor verification code"})
self.data["tfa_code"] = self._get_tfa_code()
resp = self.client.post(self.url, data=self.data)
self.assertSuccess(resp)
class UserRankAPITest(APITestCase):
def setUp(self):
self.url = self.reverse("user_rank_api")
self.create_user("test1", "test123", login=False)
self.create_user("test2", "test123", login=False)
test1 = User.objects.get(username="test1")
profile1 = test1.userprofile
profile1.submission_number = 10
profile1.accepted_number = 10
profile1.total_score = 240
profile1.save()
test2 = User.objects.get(username="test2")
profile2 = test2.userprofile
profile2.submission_number = 15
profile2.accepted_number = 10
profile2.total_score = 700
profile2.save()
def test_get_acm_rank(self):
resp = self.client.get(self.url, data={"rule": ContestRuleType.ACM})
self.assertSuccess(resp)
data = resp.data["data"]["results"]
self.assertEqual(data[0]["user"]["username"], "test1")
self.assertEqual(data[1]["user"]["username"], "test2")
def test_get_oi_rank(self):
resp = self.client.get(self.url, data={"rule": ContestRuleType.OI})
self.assertSuccess(resp)
data = resp.data["data"]["results"]
self.assertEqual(data[0]["user"]["username"], "test2")
self.assertEqual(data[1]["user"]["username"], "test1")
def test_admin_role_filted(self):
self.create_admin("admin", "admin123")
admin = User.objects.get(username="admin")
profile1 = admin.userprofile
profile1.submission_number = 20
profile1.accepted_number = 5
profile1.total_score = 300
profile1.save()
resp = self.client.get(self.url, data={"rule": ContestRuleType.ACM})
self.assertSuccess(resp)
self.assertEqual(len(resp.data["data"]), 2)
resp = self.client.get(self.url, data={"rule": ContestRuleType.OI})
self.assertSuccess(resp)
self.assertEqual(len(resp.data["data"]), 2)
class ProfileProblemDisplayIDRefreshAPITest(APITestCase):
def setUp(self):
pass
class AdminUserTest(APITestCase):
def setUp(self):
self.user = self.create_super_admin(login=True)
self.username = self.password = "test"
self.regular_user = self.create_user(username=self.username, password=self.password, login=False)
self.url = self.reverse("user_admin_api")
self.data = {"id": self.regular_user.id, "username": self.username, "real_name": "test_name",
"email": "test@qq.com", "admin_type": AdminType.REGULAR_USER,
"problem_permission": ProblemPermission.OWN, "open_api": True,
"two_factor_auth": False, "is_disabled": False}
def test_user_list(self):
response = self.client.get(self.url)
self.assertSuccess(response)
def test_edit_user_successfully(self):
response = self.client.put(self.url, data=self.data)
self.assertSuccess(response)
resp_data = response.data["data"]
self.assertEqual(resp_data["username"], self.username)
self.assertEqual(resp_data["email"], "test@qq.com")
self.assertEqual(resp_data["open_api"], True)
self.assertEqual(resp_data["two_factor_auth"], False)
self.assertEqual(resp_data["is_disabled"], False)
self.assertEqual(resp_data["problem_permission"], ProblemPermission.NONE)
self.assertTrue(self.regular_user.check_password("test"))
def test_edit_user_password(self):
data = self.data
new_password = "testpassword"
data["password"] = new_password
response = self.client.put(self.url, data=data)
self.assertSuccess(response)
user = User.objects.get(id=self.regular_user.id)
self.assertFalse(user.check_password(self.password))
self.assertTrue(user.check_password(new_password))
def test_edit_user_tfa(self):
data = self.data
self.assertIsNone(self.regular_user.tfa_token)
data["two_factor_auth"] = True
response = self.client.put(self.url, data=data)
self.assertSuccess(response)
resp_data = response.data["data"]
# if `tfa_token` is None, a new value will be generated
self.assertTrue(resp_data["two_factor_auth"])
token = User.objects.get(id=self.regular_user.id).tfa_token
self.assertIsNotNone(token)
response = self.client.put(self.url, data=data)
self.assertSuccess(response)
resp_data = response.data["data"]
# if `tfa_token` is not None, the value is not changed
self.assertTrue(resp_data["two_factor_auth"])
self.assertEqual(User.objects.get(id=self.regular_user.id).tfa_token, token)
def test_edit_user_openapi(self):
data = self.data
self.assertIsNone(self.regular_user.open_api_appkey)
data["open_api"] = True
response = self.client.put(self.url, data=data)
self.assertSuccess(response)
resp_data = response.data["data"]
# if `open_api_appkey` is None, a new value will be generated
self.assertTrue(resp_data["open_api"])
key = User.objects.get(id=self.regular_user.id).open_api_appkey
self.assertIsNotNone(key)
response = self.client.put(self.url, data=data)
self.assertSuccess(response)
resp_data = response.data["data"]
# if `openapi_app_key` is not None, the value is not changed
self.assertTrue(resp_data["open_api"])
self.assertEqual(User.objects.get(id=self.regular_user.id).open_api_appkey, key)
def test_import_users(self):
data = {"users": [["user1", "pass1", "eami1@e.com", "user1"],
["user2", "pass3", "eamil3@e.com", "user2"]]
}
resp = self.client.post(self.url, data)
self.assertSuccess(resp)
# successfully created 2 users
self.assertEqual(User.objects.all().count(), 4)
def test_import_duplicate_user(self):
data = {"users": [["user1", "pass1", "eami1@e.com", "user1"],
["user1", "pass1", "eami1@e.com", "user1"]]
}
resp = self.client.post(self.url, data)
self.assertFailed(resp, "DETAIL: Key (username)=(user1) already exists.")
# no user is created
self.assertEqual(User.objects.all().count(), 2)
def test_delete_users(self):
self.test_import_users()
user_ids = User.objects.filter(username__in=["user1", "user2"]).values_list("id", flat=True)
user_ids = ",".join([str(id) for id in user_ids])
resp = self.client.delete(self.url + "?id=" + user_ids)
self.assertSuccess(resp)
self.assertEqual(User.objects.all().count(), 2)
class GenerateUserAPITest(APITestCase):
def setUp(self):
self.create_super_admin()
self.url = self.reverse("generate_user_api")
self.data = {
"number_from": 100, "number_to": 105,
"prefix": "pre", "suffix": "suf",
"default_email": "test@test.com",
"password_length": 8
}
def test_error_case(self):
data = deepcopy(self.data)
data["prefix"] = "t" * 16
data["suffix"] = "s" * 14
resp = self.client.post(self.url, data=data)
self.assertEqual(resp.data["data"], "Username should not more than 32 characters")
data2 = deepcopy(self.data)
data2["number_from"] = 106
resp = self.client.post(self.url, data=data2)
self.assertEqual(resp.data["data"], "Start number must be lower than end number")
@mock.patch("account.views.admin.xlsxwriter.Workbook")
def test_generate_user_success(self, mock_workbook):
resp = self.client.post(self.url, data=self.data)
self.assertSuccess(resp)
mock_workbook.assert_called()
class OpenAPIAppkeyAPITest(APITestCase):
def setUp(self):
self.user = self.create_super_admin()
self.url = self.reverse("open_api_appkey_api")
def test_reset_appkey(self):
resp = self.client.post(self.url, data={})
self.assertFailed(resp)
self.user.open_api = True
self.user.save()
resp = self.client.post(self.url, data={})
self.assertSuccess(resp)
self.assertEqual(resp.data["data"]["appkey"], User.objects.get(username=self.user.username).open_api_appkey)

View File

@@ -1,6 +1,6 @@
from django.urls import path from django.urls import path
from ..views.admin import UserAdminAPI, GenerateUserAPI, ResetUserPasswordAPI from ..views.admin import GenerateUserAPI, ResetUserPasswordAPI, UserAdminAPI
urlpatterns = [ urlpatterns = [
path("user", UserAdminAPI.as_view()), path("user", UserAdminAPI.as_view()),

View File

@@ -1,30 +1,30 @@
from django.urls import path from django.urls import path
from utils.captcha.views import CaptchaAPIView
from ..views.oj import ( from ..views.oj import (
SSOAPI,
ApplyResetPasswordAPI, ApplyResetPasswordAPI,
ResetPasswordAPI, AvatarUploadAPI,
UserChangePasswordAPI, CheckTFARequiredAPI,
Metrics, Metrics,
UserRegisterAPI, OpenAPIAppkeyAPI,
ProfileProblemDisplayIDRefreshAPI,
ResetPasswordAPI,
SessionManagementAPI,
TwoFactorAuthAPI,
UserActivityRankAPI,
UserChangeEmailAPI, UserChangeEmailAPI,
UserChangePasswordAPI,
UserLoginAPI, UserLoginAPI,
UserLogoutAPI, UserLogoutAPI,
UsernameOrEmailCheck, UsernameOrEmailCheck,
AvatarUploadAPI, UserProblemRankAPI,
TwoFactorAuthAPI,
UserProfileAPI, UserProfileAPI,
UserRankAPI, UserRankAPI,
UserActivityRankAPI, UserRegisterAPI,
UserProblemRankAPI,
CheckTFARequiredAPI,
SessionManagementAPI,
ProfileProblemDisplayIDRefreshAPI,
OpenAPIAppkeyAPI,
SSOAPI,
) )
from utils.captcha.views import CaptchaAPIView
urlpatterns = [ urlpatterns = [
path("login", UserLoginAPI.as_view()), path("login", UserLoginAPI.as_view()),
path("logout", UserLogoutAPI.as_view()), path("logout", UserLogoutAPI.as_view()),

View File

@@ -1,11 +1,11 @@
import os import os
import re import re
import xlsxwriter
from django.db import transaction, IntegrityError import xlsxwriter
from django.db.models import Q, F
from django.http import HttpResponse
from django.contrib.auth.hashers import make_password from django.contrib.auth.hashers import make_password
from django.db import IntegrityError, transaction
from django.db.models import F, Q
from django.http import HttpResponse
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from submission.models import Submission from submission.models import Submission
@@ -16,10 +16,10 @@ from ..decorators import super_admin_required
from ..models import AdminType, ProblemPermission, User, UserProfile from ..models import AdminType, ProblemPermission, User, UserProfile
from ..serializers import ( from ..serializers import (
EditUserSerializer, EditUserSerializer,
UserAdminSerializer,
GenerateUserSerializer, GenerateUserSerializer,
ImportUserSerializer,
UserAdminSerializer,
) )
from ..serializers import ImportUserSerializer
# ks251XXX 或者 ks2510XX 返回 251 或者 2510 # ks251XXX 或者 ks2510XX 返回 251 或者 2510

View File

@@ -2,44 +2,42 @@ import os
from datetime import timedelta from datetime import timedelta
from importlib import import_module from importlib import import_module
import qrcode
from django.conf import settings from django.conf import settings
from django.contrib import auth from django.contrib import auth
from django.core.cache import cache
from django.db.models import Count, Q
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils import timezone
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.utils.timezone import now from django.utils.timezone import now
from django.views.decorators.csrf import ensure_csrf_cookie, csrf_exempt from django.views.decorators.csrf import csrf_exempt, ensure_csrf_cookie
from django.db.models import Count, Q
from django.utils import timezone
import qrcode
from otpauth import OtpAuth from otpauth import OtpAuth
from problem.models import Problem
from submission.models import Submission, JudgeStatus
from django.core.cache import cache
from utils.constants import ContestRuleType, CacheKey
from options.options import SysOptions from options.options import SysOptions
from utils.api import APIView, validate_serializer, CSRFExemptAPIView from problem.models import Problem
from submission.models import JudgeStatus, Submission
from utils.api import APIView, CSRFExemptAPIView, validate_serializer
from utils.captcha import Captcha from utils.captcha import Captcha
from utils.shortcuts import rand_str, img2base64, datetime2str from utils.constants import CacheKey, ContestRuleType
from utils.shortcuts import datetime2str, img2base64, rand_str
from ..decorators import login_required from ..decorators import login_required
from ..models import User, UserProfile, AdminType from ..models import AdminType, User, UserProfile
from ..serializers import ( from ..serializers import (
ApplyResetPasswordSerializer, ApplyResetPasswordSerializer,
ResetPasswordSerializer,
UserChangePasswordSerializer,
UserLoginSerializer,
UserRegisterSerializer,
UsernameOrEmailCheckSerializer,
RankInfoSerializer,
UserChangeEmailSerializer,
SSOSerializer,
)
from ..serializers import (
TwoFactorAuthCodeSerializer,
UserProfileSerializer,
EditUserProfileSerializer, EditUserProfileSerializer,
ImageUploadForm, ImageUploadForm,
RankInfoSerializer,
ResetPasswordSerializer,
SSOSerializer,
TwoFactorAuthCodeSerializer,
UserChangeEmailSerializer,
UserChangePasswordSerializer,
UserLoginSerializer,
UsernameOrEmailCheckSerializer,
UserProfileSerializer,
UserRegisterSerializer,
) )
from ..tasks import send_email_async from ..tasks import send_email_async

View File

@@ -1,27 +1,26 @@
from collections import defaultdict
from datetime import datetime, timedelta
import hashlib import hashlib
import json import json
from collections import defaultdict
from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from django.core.cache import cache from django.core.cache import cache
from django.db.models import Min, Count from django.db.models import Count, Min
from django.db.models.functions import TruncDate from django.db.models.functions import TruncDate
from django.http import StreamingHttpResponse from django.http import StreamingHttpResponse
from django.utils import timezone from django.utils import timezone
from django.utils.dateparse import parse_datetime from django.utils.dateparse import parse_datetime
from account.decorators import login_required
from account.models import User
from ai.models import AIAnalysis
from flowchart.models import FlowchartSubmission, FlowchartSubmissionStatus
from problem.models import Problem
from submission.models import JudgeStatus, Submission
from utils.api import APIView from utils.api import APIView
from utils.openai import get_ai_client from utils.openai import get_ai_client
from utils.shortcuts import datetime2str from utils.shortcuts import datetime2str
from account.models import User
from problem.models import Problem
from submission.models import Submission, JudgeStatus
from flowchart.models import FlowchartSubmission, FlowchartSubmissionStatus
from account.decorators import login_required
from ai.models import AIAnalysis
CACHE_TIMEOUT = 300 CACHE_TIMEOUT = 300
DIFFICULTY_MAP = {"Low": "简单", "Mid": "中等", "High": "困难"} DIFFICULTY_MAP = {"Low": "简单", "Mid": "中等", "High": "困难"}
DEFAULT_CLASS_SIZE = 45 DEFAULT_CLASS_SIZE = 45
@@ -598,7 +597,11 @@ class AIAnalysisAPI(APIView):
client = get_ai_client() client = get_ai_client()
system_prompt = "你是一个风趣的编程老师,学生使用判题狗平台进行编程练习。请根据学生提供的详细数据和每周数据,给出用户的学习建议,最后写一句鼓励学生的话。请使用 markdown 格式输出,不要在代码块中输出。" system_prompt = (
"你是一个风趣的编程老师,学生使用判题狗平台进行编程练习。"
"请根据学生提供的详细数据和每周数据,给出用户的学习建议,最后写一句鼓励学生的话。"
"请使用 markdown 格式输出,不要在代码块中输出。"
)
user_prompt = f"这段时间内的详细数据: {details}\n(其中部分字段含义是 flowcharts:流程图的提交,solved:代码的提交)\n每周或每月的数据: {duration}" user_prompt = f"这段时间内的详细数据: {details}\n(其中部分字段含义是 flowcharts:流程图的提交,solved:代码的提交)\n每周或每月的数据: {duration}"
def on_complete(full_text): def on_complete(full_text):

View File

@@ -1,48 +0,0 @@
from utils.api.tests import APITestCase
from .models import Announcement
class AnnouncementAdminTest(APITestCase):
def setUp(self):
self.user = self.create_super_admin()
self.url = self.reverse("announcement_admin_api")
def test_announcement_list(self):
response = self.client.get(self.url)
self.assertSuccess(response)
def create_announcement(self):
return self.client.post(self.url, data={"title": "test", "content": "test", "visible": True})
def test_create_announcement(self):
resp = self.create_announcement()
self.assertSuccess(resp)
return resp
def test_edit_announcement(self):
data = {"id": self.create_announcement().data["data"]["id"], "title": "ahaha", "content": "test content",
"visible": False}
resp = self.client.put(self.url, data=data)
self.assertSuccess(resp)
resp_data = resp.data["data"]
self.assertEqual(resp_data["title"], "ahaha")
self.assertEqual(resp_data["content"], "test content")
self.assertEqual(resp_data["visible"], False)
def test_delete_announcement(self):
id = self.test_create_announcement().data["data"]["id"]
resp = self.client.delete(self.url + "?id=" + str(id))
self.assertSuccess(resp)
self.assertFalse(Announcement.objects.filter(id=id).exists())
class AnnouncementAPITest(APITestCase):
def setUp(self):
self.user = self.create_super_admin()
Announcement.objects.create(title="title", content="content", visible=True, created_by=self.user)
self.url = self.reverse("announcement_api")
def test_get_announcement_list(self):
resp = self.client.get(self.url)
self.assertSuccess(resp)

View File

@@ -1,12 +1,11 @@
from account.decorators import super_admin_required from account.decorators import super_admin_required
from utils.api import APIView, validate_serializer
from announcement.models import Announcement from announcement.models import Announcement
from announcement.serializers import ( from announcement.serializers import (
AnnouncementSerializer, AnnouncementSerializer,
CreateAnnouncementSerializer, CreateAnnouncementSerializer,
EditAnnouncementSerializer, EditAnnouncementSerializer,
) )
from utils.api import APIView, validate_serializer
class AnnouncementAdminAPI(APIView): class AnnouncementAdminAPI(APIView):

View File

@@ -1,7 +1,6 @@
from utils.api import APIView
from announcement.models import Announcement from announcement.models import Announcement
from announcement.serializers import AnnouncementSerializer, AnnouncementListSerializer from announcement.serializers import AnnouncementListSerializer, AnnouncementSerializer
from utils.api import APIView
class AnnouncementAPI(APIView): class AnnouncementAPI(APIView):

View File

@@ -1,3 +1,2 @@
from django.contrib import admin
# Register your models here. # Register your models here.

View File

@@ -1,4 +1,3 @@
from django.db import models
# 如果需要存储班级PK历史记录可以在这里定义模型 # 如果需要存储班级PK历史记录可以在这里定义模型
# 目前暂时不需要,因为都是实时计算 # 目前暂时不需要,因为都是实时计算

View File

@@ -1,6 +1,6 @@
from django.urls import path from django.urls import path
from ..views.oj import ClassRankAPI, UserClassRankAPI, ClassPKAPI from ..views.oj import ClassPKAPI, ClassRankAPI, UserClassRankAPI
urlpatterns = [ urlpatterns = [
path("class_rank", ClassRankAPI.as_view()), path("class_rank", ClassRankAPI.as_view()),

View File

@@ -1,12 +1,13 @@
import re
import statistics import statistics
from datetime import datetime from datetime import datetime
from django.db.models import Sum, Avg
from django.db.models import Avg, Sum
from django.utils import timezone from django.utils import timezone
from utils.api import APIView
from account.decorators import login_required from account.decorators import login_required
from account.models import User, UserProfile, AdminType from account.models import AdminType, User, UserProfile
from submission.models import Submission, JudgeStatus from submission.models import JudgeStatus, Submission
from utils.api import APIView
class ClassRankAPI(APIView): class ClassRankAPI(APIView):

View File

@@ -2,7 +2,6 @@ from django.urls import path
from ..views.admin import CommentAPI from ..views.admin import CommentAPI
urlpatterns = [ urlpatterns = [
path("comment", CommentAPI.as_view()), path("comment", CommentAPI.as_view()),
] ]

View File

@@ -2,7 +2,6 @@ from django.urls import path
from ..views.oj import CommentAPI, CommentStatisticsAPI from ..views.oj import CommentAPI, CommentStatisticsAPI
urlpatterns = [ urlpatterns = [
path("comment", CommentAPI.as_view()), path("comment", CommentAPI.as_view()),
path("comment/statistics", CommentStatisticsAPI.as_view()), path("comment/statistics", CommentStatisticsAPI.as_view()),

View File

@@ -1,8 +1,8 @@
from account.decorators import super_admin_required from account.decorators import super_admin_required
from comment.models import Comment
from comment.serializers import CommentListSerializer from comment.serializers import CommentListSerializer
from problem.models import Problem from problem.models import Problem
from utils.api import APIView from utils.api import APIView
from comment.models import Comment
class CommentAPI(APIView): class CommentAPI(APIView):

View File

@@ -1,14 +1,15 @@
from django.core.cache import cache from django.core.cache import cache
from django.db.models import Avg, Count from django.db.models import Avg, Count
from django.db.models.functions import Round from django.db.models.functions import Round
from comment.models import Comment
from problem.models import Problem
from utils.api import APIView
from utils.constants import CacheKey
from account.decorators import login_required from account.decorators import login_required
from comment.models import Comment
from comment.serializers import CommentSerializer, CreateCommentSerializer
from problem.models import Problem
from submission.models import JudgeStatus, Submission
from utils.api import APIView
from utils.api.api import validate_serializer from utils.api.api import validate_serializer
from comment.serializers import CreateCommentSerializer, CommentSerializer from utils.constants import CacheKey
from submission.models import Submission, JudgeStatus
class CommentAPI(APIView): class CommentAPI(APIView):

View File

@@ -3,6 +3,7 @@ WebSocket consumers for configuration updates
""" """
import json import json
import logging import logging
from channels.generic.websocket import AsyncWebsocketConsumer from channels.generic.websocket import AsyncWebsocketConsumer
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@@ -1,185 +0,0 @@
import hashlib
from unittest import mock
from django.conf import settings
from django.utils import timezone
from options.options import SysOptions
from utils.api.tests import APITestCase
from .models import JudgeServer
class SMTPConfigTest(APITestCase):
def setUp(self):
self.user = self.create_super_admin()
self.url = self.reverse("smtp_admin_api")
self.password = "testtest"
def test_create_smtp_config(self):
data = {"server": "smtp.test.com", "email": "test@test.com", "port": 465,
"tls": True, "password": self.password}
resp = self.client.post(self.url, data=data)
self.assertSuccess(resp)
self.assertTrue("password" not in resp.data)
return resp
def test_edit_without_password(self):
self.test_create_smtp_config()
data = {"server": "smtp1.test.com", "email": "test2@test.com", "port": 465,
"tls": True}
resp = self.client.put(self.url, data=data)
self.assertSuccess(resp)
def test_edit_without_password1(self):
self.test_create_smtp_config()
data = {"server": "smtp.test.com", "email": "test@test.com", "port": 465,
"tls": True, "password": ""}
resp = self.client.put(self.url, data=data)
self.assertSuccess(resp)
def test_edit_with_password(self):
self.test_create_smtp_config()
data = {"server": "smtp1.test.com", "email": "test2@test.com", "port": 465,
"tls": True, "password": "newpassword"}
resp = self.client.put(self.url, data=data)
self.assertSuccess(resp)
@mock.patch("conf.views.send_email")
def test_test_smtp(self, mocked_send_email):
url = self.reverse("smtp_test_api")
self.test_create_smtp_config()
resp = self.client.post(url, data={"email": "test@test.com"})
self.assertSuccess(resp)
mocked_send_email.assert_called_once()
class WebsiteConfigAPITest(APITestCase):
def test_create_website_config(self):
self.create_super_admin()
url = self.reverse("website_config_api")
data = {"website_base_url": "http://test.com", "website_name": "test name",
"website_name_shortcut": "test oj", "website_footer": "<a>test</a>",
"allow_register": True, "submission_list_show_all": False}
resp = self.client.post(url, data=data)
self.assertSuccess(resp)
def test_edit_website_config(self):
self.create_super_admin()
url = self.reverse("website_config_api")
data = {"website_base_url": "http://test.com", "website_name": "test name",
"website_name_shortcut": "test oj", "website_footer": "<img onerror=alert(1) src=#>",
"allow_register": True, "submission_list_show_all": False}
resp = self.client.post(url, data=data)
self.assertSuccess(resp)
self.assertEqual(SysOptions.website_footer, '<img src="#" />')
def test_get_website_config(self):
# do not need to login
url = self.reverse("website_info_api")
resp = self.client.get(url)
self.assertSuccess(resp)
class JudgeServerHeartbeatTest(APITestCase):
def setUp(self):
self.url = self.reverse("judge_server_heartbeat_api")
self.data = {"hostname": "testhostname", "judger_version": "1.0.4", "cpu_core": 4,
"cpu": 90.5, "memory": 80.3, "action": "heartbeat", "service_url": "http://127.0.0.1"}
self.token = "test"
self.hashed_token = hashlib.sha256(self.token.encode("utf-8")).hexdigest()
SysOptions.judge_server_token = self.token
self.headers = {"HTTP_X_JUDGE_SERVER_TOKEN": self.hashed_token, settings.IP_HEADER: "1.2.3.4"}
def test_new_heartbeat(self):
resp = self.client.post(self.url, data=self.data, **self.headers)
self.assertSuccess(resp)
server = JudgeServer.objects.first()
self.assertEqual(server.ip, "127.0.0.1")
def test_update_heartbeat(self):
self.test_new_heartbeat()
data = self.data
data["judger_version"] = "2.0.0"
resp = self.client.post(self.url, data=data, **self.headers)
self.assertSuccess(resp)
self.assertEqual(JudgeServer.objects.get(hostname=self.data["hostname"]).judger_version, data["judger_version"])
class JudgeServerAPITest(APITestCase):
def setUp(self):
self.server = JudgeServer.objects.create(**{"hostname": "testhostname", "judger_version": "1.0.4",
"cpu_core": 4, "cpu_usage": 90.5, "memory_usage": 80.3,
"last_heartbeat": timezone.now()})
self.url = self.reverse("judge_server_api")
self.create_super_admin()
def test_get_judge_server(self):
resp = self.client.get(self.url)
self.assertSuccess(resp)
self.assertEqual(len(resp.data["data"]["servers"]), 1)
def test_delete_judge_server(self):
resp = self.client.delete(self.url + "?hostname=testhostname")
self.assertSuccess(resp)
self.assertFalse(JudgeServer.objects.filter(hostname="testhostname").exists())
def test_disabled_judge_server(self):
resp = self.client.put(self.url, data={"is_disabled": True, "id": self.server.id})
self.assertSuccess(resp)
self.assertTrue(JudgeServer.objects.get(id=self.server.id).is_disabled)
class LanguageListAPITest(APITestCase):
def test_get_languages(self):
resp = self.client.get(self.reverse("language_list_api"))
self.assertSuccess(resp)
class TestCasePruneAPITest(APITestCase):
def setUp(self):
self.url = self.reverse("prune_test_case_api")
self.create_super_admin()
def test_get_isolated_test_case(self):
resp = self.client.get(self.url)
self.assertSuccess(resp)
@mock.patch("conf.views.TestCasePruneAPI.delete_one")
@mock.patch("conf.views.os.listdir")
@mock.patch("conf.views.Problem")
def test_delete_test_case(self, mocked_problem, mocked_listdir, mocked_delete_one):
valid_id = "1172980672983b2b49820be3a741b109"
mocked_problem.return_value = [valid_id, ]
mocked_listdir.return_value = [valid_id, ".test", "aaa"]
resp = self.client.delete(self.url)
self.assertSuccess(resp)
mocked_delete_one.assert_called_once_with(valid_id)
class ReleaseNoteAPITest(APITestCase):
def setUp(self):
self.url = self.reverse("get_release_notes_api")
self.create_super_admin()
self.latest_data = {"update": [
{
"version": "2099-12-25",
"level": 1,
"title": "Update at 2099-12-25",
"details": ["test get", ]
}
]}
def test_get_versions(self):
resp = self.client.get(self.url)
self.assertSuccess(resp)
class DashboardInfoAPITest(APITestCase):
def setUp(self):
self.url = self.reverse("dashboard_info_api")
self.create_admin()
def test_get_info(self):
resp = self.client.get(self.url)
self.assertSuccess(resp)
self.assertEqual(resp.data["data"]["user_count"], 1)

View File

@@ -2,13 +2,13 @@ from django.urls import path
from ..views import ( from ..views import (
SMTPAPI, SMTPAPI,
JudgeServerAPI,
WebsiteConfigAPI,
TestCasePruneAPI,
SMTPTestAPI,
ReleaseNotesAPI,
DashboardInfoAPI, DashboardInfoAPI,
JudgeServerAPI,
RandomUsernameAPI, RandomUsernameAPI,
ReleaseNotesAPI,
SMTPTestAPI,
TestCasePruneAPI,
WebsiteConfigAPI,
) )
urlpatterns = [ urlpatterns = [

View File

@@ -1,11 +1,11 @@
from django.urls import path from django.urls import path
from ..views import ( from ..views import (
ClassUsernamesAPI,
HitokotoAPI, HitokotoAPI,
JudgeServerHeartbeatAPI, JudgeServerHeartbeatAPI,
LanguagesAPI, LanguagesAPI,
WebsiteConfigAPI, WebsiteConfigAPI,
ClassUsernamesAPI,
) )
urlpatterns = [ urlpatterns = [

View File

@@ -22,18 +22,19 @@ from problem.models import Problem
from submission.models import Submission from submission.models import Submission
from utils.api import APIView, CSRFExemptAPIView, validate_serializer from utils.api import APIView, CSRFExemptAPIView, validate_serializer
from utils.cache import JsonDataLoader from utils.cache import JsonDataLoader
from utils.shortcuts import send_email, get_env from utils.shortcuts import get_env, send_email
from utils.xss_filter import XSSHtml
from utils.websocket import push_config_update from utils.websocket import push_config_update
from utils.xss_filter import XSSHtml
from .models import JudgeServer from .models import JudgeServer
from .serializers import ( from .serializers import (
CreateEditWebsiteConfigSerializer, CreateEditWebsiteConfigSerializer,
CreateSMTPConfigSerializer, CreateSMTPConfigSerializer,
EditJudgeServerSerializer,
EditSMTPConfigSerializer, EditSMTPConfigSerializer,
JudgeServerHeartbeatSerializer, JudgeServerHeartbeatSerializer,
JudgeServerSerializer, JudgeServerSerializer,
TestSMTPConfigSerializer, TestSMTPConfigSerializer,
EditJudgeServerSerializer,
) )

View File

@@ -1,7 +1,6 @@
from utils.api import UsernameSerializer, serializers from utils.api import UsernameSerializer, serializers
from .models import Contest, ContestAnnouncement, ContestRuleType from .models import ACMContestRank, Contest, ContestAnnouncement, ContestRuleType, OIContestRank
from .models import ACMContestRank, OIContestRank
class CreateConetestSeriaizer(serializers.Serializer): class CreateConetestSeriaizer(serializers.Serializer):

View File

@@ -1,162 +0,0 @@
import copy
from datetime import datetime, timedelta
from django.utils import timezone
from utils.api.tests import APITestCase
from .models import ContestAnnouncement, ContestRuleType, Contest
DEFAULT_CONTEST_DATA = {"title": "test title", "description": "test description",
"start_time": timezone.localtime(timezone.now()),
"end_time": timezone.localtime(timezone.now()) + timedelta(days=1),
"rule_type": ContestRuleType.ACM,
"password": "123",
"allowed_ip_ranges": [],
"visible": True, "real_time_rank": True}
class ContestAdminAPITest(APITestCase):
def setUp(self):
self.create_super_admin()
self.url = self.reverse("contest_admin_api")
self.data = copy.deepcopy(DEFAULT_CONTEST_DATA)
def test_create_contest(self):
response = self.client.post(self.url, data=self.data)
self.assertSuccess(response)
return response
def test_create_contest_with_invalid_cidr(self):
self.data["allowed_ip_ranges"] = ["127.0.0"]
resp = self.client.post(self.url, data=self.data)
self.assertTrue(resp.data["data"].endswith("is not a valid cidr network"))
def test_update_contest(self):
id = self.test_create_contest().data["data"]["id"]
update_data = {"id": id, "title": "update title",
"description": "update description",
"password": "12345",
"visible": False, "real_time_rank": False}
data = copy.deepcopy(self.data)
data.update(update_data)
response = self.client.put(self.url, data=data)
self.assertSuccess(response)
response_data = response.data["data"]
for k in data.keys():
if isinstance(data[k], datetime):
continue
self.assertEqual(response_data[k], data[k])
def test_get_contests(self):
self.test_create_contest()
response = self.client.get(self.url)
self.assertSuccess(response)
def test_get_one_contest(self):
id = self.test_create_contest().data["data"]["id"]
response = self.client.get("{}?id={}".format(self.url, id))
self.assertSuccess(response)
class ContestAPITest(APITestCase):
def setUp(self):
user = self.create_admin()
self.contest = Contest.objects.create(created_by=user, **DEFAULT_CONTEST_DATA)
self.url = self.reverse("contest_api") + "?id=" + str(self.contest.id)
def test_get_contest_list(self):
url = self.reverse("contest_list_api")
response = self.client.get(url + "?limit=10")
self.assertSuccess(response)
self.assertEqual(len(response.data["data"]["results"]), 1)
def test_get_one_contest(self):
resp = self.client.get(self.url)
self.assertSuccess(resp)
def test_regular_user_validate_contest_password(self):
self.create_user("test", "test123")
url = self.reverse("contest_password_api")
resp = self.client.post(url, {"contest_id": self.contest.id, "password": "error_password"})
self.assertDictEqual(resp.data, {"error": "error", "data": "Wrong password or password expired"})
resp = self.client.post(url, {"contest_id": self.contest.id, "password": DEFAULT_CONTEST_DATA["password"]})
self.assertSuccess(resp)
def test_regular_user_access_contest(self):
self.create_user("test", "test123")
url = self.reverse("contest_access_api")
resp = self.client.get(url + "?contest_id=" + str(self.contest.id))
self.assertFalse(resp.data["data"]["access"])
password_url = self.reverse("contest_password_api")
resp = self.client.post(password_url,
{"contest_id": self.contest.id, "password": DEFAULT_CONTEST_DATA["password"]})
self.assertSuccess(resp)
resp = self.client.get(self.url)
self.assertSuccess(resp)
class ContestAnnouncementAdminAPITest(APITestCase):
def setUp(self):
self.create_super_admin()
self.url = self.reverse("contest_announcement_admin_api")
contest_id = self.create_contest().data["data"]["id"]
self.data = {"title": "test title", "content": "test content", "contest_id": contest_id, "visible": True}
def create_contest(self):
url = self.reverse("contest_admin_api")
data = DEFAULT_CONTEST_DATA
return self.client.post(url, data=data)
def test_create_contest_announcement(self):
response = self.client.post(self.url, data=self.data)
self.assertSuccess(response)
return response
def test_delete_contest_announcement(self):
id = self.test_create_contest_announcement().data["data"]["id"]
response = self.client.delete("{}?id={}".format(self.url, id))
self.assertSuccess(response)
self.assertFalse(ContestAnnouncement.objects.filter(id=id).exists())
def test_get_contest_announcements(self):
self.test_create_contest_announcement()
response = self.client.get(self.url + "?contest_id=" + str(self.data["contest_id"]))
self.assertSuccess(response)
def test_get_one_contest_announcement(self):
id = self.test_create_contest_announcement().data["data"]["id"]
response = self.client.get("{}?id={}".format(self.url, id))
self.assertSuccess(response)
class ContestAnnouncementListAPITest(APITestCase):
def setUp(self):
self.create_super_admin()
self.url = self.reverse("contest_announcement_api")
def create_contest_announcements(self):
contest_id = self.client.post(self.reverse("contest_admin_api"), data=DEFAULT_CONTEST_DATA).data["data"]["id"]
url = self.reverse("contest_announcement_admin_api")
self.client.post(url, data={"title": "test title1", "content": "test content1", "contest_id": contest_id})
self.client.post(url, data={"title": "test title2", "content": "test content2", "contest_id": contest_id})
return contest_id
def test_get_contest_announcement_list(self):
contest_id = self.create_contest_announcements()
response = self.client.get(self.url, data={"contest_id": contest_id})
self.assertSuccess(response)
class ContestRankAPITest(APITestCase):
def setUp(self):
user = self.create_admin()
self.acm_contest = Contest.objects.create(created_by=user, **DEFAULT_CONTEST_DATA)
self.create_user("test", "test123")
self.url = self.reverse("contest_rank_api")
def get_contest_rank(self):
resp = self.client.get(self.url + "?contest_id=" + self.acm_contest.id)
self.assertSuccess(resp)

View File

@@ -1,6 +1,6 @@
from django.urls import path from django.urls import path
from ..views.admin import ContestAnnouncementAPI, ContestAPI, ACMContestHelper, DownloadContestSubmissions from ..views.admin import ACMContestHelper, ContestAnnouncementAPI, ContestAPI, DownloadContestSubmissions
urlpatterns = [ urlpatterns = [
path("contest", ContestAPI.as_view()), path("contest", ContestAPI.as_view()),

View File

@@ -1,9 +1,6 @@
from django.urls import path from django.urls import path
from ..views.oj import ContestAnnouncementListAPI from ..views.oj import ContestAccessAPI, ContestAnnouncementListAPI, ContestAPI, ContestListAPI, ContestPasswordVerifyAPI, ContestRankAPI
from ..views.oj import ContestPasswordVerifyAPI, ContestAccessAPI
from ..views.oj import ContestListAPI, ContestAPI
from ..views.oj import ContestRankAPI
urlpatterns = [ urlpatterns = [
path("contests", ContestListAPI.as_view()), path("contests", ContestListAPI.as_view()),

View File

@@ -6,25 +6,25 @@ from ipaddress import ip_network
import dateutil.parser import dateutil.parser
from django.http import FileResponse from django.http import FileResponse
from problem.models import Problem
from account.decorators import super_admin_required from account.decorators import super_admin_required
from account.models import User from account.models import User
from submission.models import Submission, JudgeStatus from problem.models import Problem
from submission.models import JudgeStatus, Submission
from utils.api import APIView, validate_serializer from utils.api import APIView, validate_serializer
from utils.cache import cache from utils.cache import cache
from utils.constants import CacheKey from utils.constants import CacheKey
from utils.shortcuts import rand_str from utils.shortcuts import rand_str
from utils.tasks import delete_files from utils.tasks import delete_files
from ..models import Contest, ContestAnnouncement, ACMContestRank
from ..models import ACMContestRank, Contest, ContestAnnouncement
from ..serializers import ( from ..serializers import (
ContestAnnouncementSerializer, ACMContesHelperSerializer,
ContestAdminSerializer, ContestAdminSerializer,
ContestAnnouncementSerializer,
CreateConetestSeriaizer, CreateConetestSeriaizer,
CreateContestAnnouncementSerializer, CreateContestAnnouncementSerializer,
EditConetestSeriaizer, EditConetestSeriaizer,
EditContestAnnouncementSerializer, EditContestAnnouncementSerializer,
ACMContesHelperSerializer,
) )

View File

@@ -1,26 +1,23 @@
import io import io
import xlsxwriter import xlsxwriter
from django.core.cache import cache
from django.http import HttpResponse from django.http import HttpResponse
from django.utils.timezone import now from django.utils.timezone import now
from django.core.cache import cache
from account.decorators import (
check_contest_password,
check_contest_permission,
login_required,
)
from account.models import AdminType
from problem.models import Problem from problem.models import Problem
from utils.api import APIView, validate_serializer from utils.api import APIView, validate_serializer
from utils.constants import CacheKey, CONTEST_PASSWORD_SESSION_KEY from utils.constants import CONTEST_PASSWORD_SESSION_KEY, CacheKey, ContestRuleType, ContestStatus
from utils.shortcuts import datetime2str, check_is_id from utils.shortcuts import check_is_id, datetime2str
from account.models import AdminType
from account.decorators import (
login_required,
check_contest_permission,
check_contest_password,
)
from utils.constants import ContestRuleType, ContestStatus from ..models import ACMContestRank, Contest, ContestAnnouncement, OIContestRank
from ..models import ContestAnnouncement, Contest, OIContestRank, ACMContestRank from ..serializers import ACMContestRankSerializer, ContestAnnouncementSerializer, ContestPasswordVerifySerializer, ContestSerializer, OIContestRankSerializer
from ..serializers import ContestAnnouncementSerializer
from ..serializers import ContestSerializer, ContestPasswordVerifySerializer
from ..serializers import OIContestRankSerializer, ACMContestRankSerializer
class ContestAnnouncementListAPI(APIView): class ContestAnnouncementListAPI(APIView):

6
dev.py
View File

@@ -6,13 +6,13 @@ WebSocket 开发服务器启动脚本
""" """
import os import os
import sys
import subprocess
import platform import platform
import signal import signal
import subprocess
import sys
import time
from pathlib import Path from pathlib import Path
from threading import Thread from threading import Thread
import time
def main(): def main():

View File

@@ -3,6 +3,7 @@ WebSocket consumers for flowchart evaluation updates
""" """
import json import json
import logging import logging
from channels.generic.websocket import AsyncWebsocketConsumer from channels.generic.websocket import AsyncWebsocketConsumer
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@@ -1,7 +1,8 @@
from django.db import models
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from utils.shortcuts import rand_str from django.db import models
from problem.models import Problem from problem.models import Problem
from utils.shortcuts import rand_str
User = get_user_model() User = get_user_model()

View File

@@ -1,4 +1,5 @@
from rest_framework import serializers from rest_framework import serializers
from .models import FlowchartSubmission from .models import FlowchartSubmission

View File

@@ -1,12 +1,16 @@
import dramatiq
import json import json
import time import time
import dramatiq
from django.db import transaction from django.db import transaction
from django.utils import timezone from django.utils import timezone
from utils.openai import get_ai_client from utils.openai import get_ai_client
from utils.shortcuts import DRAMATIQ_WORKER_ARGS from utils.shortcuts import DRAMATIQ_WORKER_ARGS
from .models import FlowchartSubmission, FlowchartSubmissionStatus from .models import FlowchartSubmission, FlowchartSubmissionStatus
@dramatiq.actor(**DRAMATIQ_WORKER_ARGS(max_retries=3)) @dramatiq.actor(**DRAMATIQ_WORKER_ARGS(max_retries=3))
def evaluate_flowchart_task(submission_id): def evaluate_flowchart_task(submission_id):
"""异步AI评分任务""" """异步AI评分任务"""

View File

@@ -1,10 +1,11 @@
from django.urls import path from django.urls import path
from ..views.oj import ( from ..views.oj import (
FlowchartSubmissionAPI, FlowchartSubmissionAPI,
FlowchartSubmissionListAPI,
FlowchartSubmissionRetryAPI,
FlowchartSubmissionCurrentAPI, FlowchartSubmissionCurrentAPI,
FlowchartSubmissionDetailAPI, FlowchartSubmissionDetailAPI,
FlowchartSubmissionListAPI,
FlowchartSubmissionRetryAPI,
) )
urlpatterns = [ urlpatterns = [

View File

@@ -1,3 +1,2 @@
from django.shortcuts import render
# Create your views here. # Create your views here.

View File

@@ -1,13 +1,13 @@
from utils.api import APIView
from account.decorators import login_required from account.decorators import login_required
from flowchart.models import FlowchartSubmission, FlowchartSubmissionStatus from flowchart.models import FlowchartSubmission, FlowchartSubmissionStatus
from flowchart.serializers import ( from flowchart.serializers import (
CreateFlowchartSubmissionSerializer, CreateFlowchartSubmissionSerializer,
FlowchartSubmissionSerializer,
FlowchartSubmissionListSerializer, FlowchartSubmissionListSerializer,
FlowchartSubmissionSerializer,
) )
from flowchart.tasks import evaluate_flowchart_task from flowchart.tasks import evaluate_flowchart_task
from problem.models import Problem from problem.models import Problem
from utils.api import APIView
class FlowchartSubmissionAPI(APIView): class FlowchartSubmissionAPI(APIView):

View File

@@ -1,11 +1,11 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import base64 import base64
import copy import copy
import random
import string
import hashlib import hashlib
import json import json
import os import os
import random
import string
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET

View File

@@ -4,12 +4,12 @@ import logging
from urllib.parse import urljoin from urllib.parse import urljoin
import requests import requests
from django.db import transaction, IntegrityError from django.db import IntegrityError, transaction
from django.db.models import F from django.db.models import F
from account.models import User from account.models import User
from conf.models import JudgeServer from conf.models import JudgeServer
from contest.models import ContestRuleType, ACMContestRank, OIContestRank, ContestStatus from contest.models import ACMContestRank, ContestRuleType, ContestStatus, OIContestRank
from options.options import SysOptions from options.options import SysOptions
from problem.models import Problem, ProblemRuleType from problem.models import Problem, ProblemRuleType
from problem.utils import parse_problem_template from problem.utils import parse_problem_template

View File

@@ -1,6 +1,5 @@
from problem.models import ProblemIOMode from problem.models import ProblemIOMode
default_env = ["LANG=en_US.UTF-8", "LANGUAGE=en_US:en", "LC_ALL=en_US.UTF-8"] default_env = ["LANG=en_US.UTF-8", "LANGUAGE=en_US:en", "LC_ALL=en_US.UTF-8"]
_c_lang_config = { _c_lang_config = {

View File

@@ -1,8 +1,8 @@
import dramatiq import dramatiq
from account.models import User from account.models import User
from submission.models import Submission
from judge.dispatcher import JudgeDispatcher from judge.dispatcher import JudgeDispatcher
from submission.models import Submission
from utils.shortcuts import DRAMATIQ_WORKER_ARGS from utils.shortcuts import DRAMATIQ_WORKER_ARGS

View File

@@ -5,8 +5,8 @@ import sys
if __name__ == "__main__": if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "oj.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "oj.settings")
from django.core.management import execute_from_command_line
import django import django
from django.core.management import execute_from_command_line
sys.stdout.write("Django VERSION " + str(django.VERSION) + "\n") sys.stdout.write("Django VERSION " + str(django.VERSION) + "\n")
execute_from_command_line(sys.argv) execute_from_command_line(sys.argv)

View File

@@ -1,4 +1,5 @@
from django.db import models from django.db import models
from account.models import User from account.models import User
from submission.models import Submission from submission.models import Submission
from utils.models import RichTextField from utils.models import RichTextField

View File

@@ -1,7 +1,7 @@
from submission.serializers import SubmissionSafeModelSerializer from submission.serializers import SubmissionSafeModelSerializer
from utils.api import UsernameSerializer, serializers from utils.api import UsernameSerializer, serializers
from .models import Message
from .models import Message
class MessageSerializer(serializers.ModelSerializer): class MessageSerializer(serializers.ModelSerializer):

View File

@@ -1,11 +1,9 @@
from account.decorators import super_admin_required, login_required from account.decorators import login_required, super_admin_required
from account.models import User from account.models import User
from message.models import Message
from message.serializers import CreateMessageSerializer, MessageSerializer from message.serializers import CreateMessageSerializer, MessageSerializer
from submission.models import Submission from submission.models import Submission
from utils.api import APIView from utils.api import APIView
from message.models import Message
from utils.api.api import validate_serializer from utils.api.api import validate_serializer

View File

@@ -8,9 +8,10 @@ https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
""" """
import os import os
from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from django.core.asgi import get_asgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "oj.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "oj.settings")
@@ -19,7 +20,7 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "oj.settings")
django_asgi_app = get_asgi_application() django_asgi_app = get_asgi_application()
# Import routing after Django setup # Import routing after Django setup
from oj.routing import websocket_urlpatterns from oj.routing import websocket_urlpatterns # noqa: E402
application = ProtocolTypeRouter( application = ProtocolTypeRouter(
{ {

View File

@@ -3,9 +3,10 @@ WebSocket URL Configuration for oj project.
""" """
from django.urls import path from django.urls import path
from submission.consumers import SubmissionConsumer
from conf.consumers import ConfigConsumer from conf.consumers import ConfigConsumer
from flowchart.consumers import FlowchartConsumer from flowchart.consumers import FlowchartConsumer
from submission.consumers import SubmissionConsumer
websocket_urlpatterns = [ websocket_urlpatterns = [
path("ws/submission/", SubmissionConsumer.as_asgi()), path("ws/submission/", SubmissionConsumer.as_asgi()),

View File

@@ -1,4 +1,5 @@
from django.db import models from django.db import models
from utils.models import JSONField from utils.models import JSONField

View File

@@ -3,10 +3,11 @@ import os
import threading import threading
import time import time
from django.db import transaction, IntegrityError from django.db import IntegrityError, transaction
from utils.shortcuts import rand_str
from judge.languages import languages from judge.languages import languages
from utils.shortcuts import rand_str
from .models import SysOptions as SysOptionsModel from .models import SysOptions as SysOptionsModel

View File

@@ -1 +0,0 @@
# Create your tests here.

View File

@@ -2,8 +2,8 @@ from django.db import models
from account.models import User from account.models import User
from contest.models import Contest from contest.models import Contest
from utils.models import RichTextField
from utils.constants import Choices from utils.constants import Choices
from utils.models import RichTextField
class ProblemTag(models.Model): class ProblemTag(models.Model):

View File

@@ -5,11 +5,11 @@ from django import forms
from utils.api import UsernameSerializer, serializers from utils.api import UsernameSerializer, serializers
from utils.constants import Difficulty from utils.constants import Difficulty
from utils.serializers import ( from utils.serializers import (
LanguageNameMultiChoiceField,
LanguageNameChoiceField, LanguageNameChoiceField,
LanguageNameMultiChoiceField,
) )
from .models import Problem, ProblemRuleType, ProblemTag, ProblemIOMode from .models import Problem, ProblemIOMode, ProblemRuleType, ProblemTag
from .utils import parse_problem_template from .utils import parse_problem_template

View File

@@ -1,14 +1,14 @@
from django.urls import path from django.urls import path
from ..views.admin import ( from ..views.admin import (
AddContestProblemAPI,
ContestProblemAPI, ContestProblemAPI,
MakeContestProblemPublicAPIView,
ProblemAPI, ProblemAPI,
ProblemFlowchartAIGen, ProblemFlowchartAIGen,
ProblemVisibleAPI,
StuckProblemsAPI, StuckProblemsAPI,
TestCaseAPI, TestCaseAPI,
MakeContestProblemPublicAPIView,
AddContestProblemAPI,
ProblemVisibleAPI,
) )
urlpatterns = [ urlpatterns = [

View File

@@ -1,12 +1,12 @@
from django.urls import path from django.urls import path
from ..views.oj import ( from ..views.oj import (
ProblemSolvedPeopleCount,
ProblemTagAPI,
ProblemAPI,
ContestProblemAPI, ContestProblemAPI,
PickOneAPI, PickOneAPI,
ProblemAPI,
ProblemAuthorAPI, ProblemAuthorAPI,
ProblemSolvedPeopleCount,
ProblemTagAPI,
SimilarProblemAPI, SimilarProblemAPI,
) )

View File

@@ -1,7 +1,6 @@
import re import re
from functools import lru_cache from functools import lru_cache
TEMPLATE_BASE = """//PREPEND BEGIN TEMPLATE_BASE = """//PREPEND BEGIN
{} {}
//PREPEND END //PREPEND END

View File

@@ -7,28 +7,27 @@ import zipfile
from wsgiref.util import FileWrapper from wsgiref.util import FileWrapper
from django.conf import settings from django.conf import settings
from django.db.models import Q from django.db.models import Count, Q
from django.http import StreamingHttpResponse from django.http import StreamingHttpResponse
from django.db.models import Count from account.decorators import ensure_created_by, problem_permission_required, super_admin_required
from account.decorators import problem_permission_required, ensure_created_by, super_admin_required
from contest.models import Contest, ContestStatus from contest.models import Contest, ContestStatus
from submission.models import Submission from submission.models import Submission
from utils.api import APIView, CSRFExemptAPIView, validate_serializer, APIError from utils.api import APIError, APIView, CSRFExemptAPIView, validate_serializer
from utils.shortcuts import rand_str, natural_sort_key
from utils.openai import get_ai_client from utils.openai import get_ai_client
from utils.shortcuts import natural_sort_key, rand_str
from ..models import Problem, ProblemRuleType, ProblemTag from ..models import Problem, ProblemRuleType, ProblemTag
from ..serializers import ( from ..serializers import (
AddContestProblemSerializer,
ContestProblemMakePublicSerializer,
CreateContestProblemSerializer, CreateContestProblemSerializer,
CreateProblemSerializer, CreateProblemSerializer,
EditProblemSerializer,
EditContestProblemSerializer, EditContestProblemSerializer,
ProblemAdminSerializer, EditProblemSerializer,
ProblemAdminListSerializer, ProblemAdminListSerializer,
ProblemAdminSerializer,
TestCaseUploadForm, TestCaseUploadForm,
ContestProblemMakePublicSerializer,
AddContestProblemSerializer,
) )

View File

@@ -1,20 +1,23 @@
from datetime import datetime
import random import random
from django.db.models import Q, Count from datetime import datetime
from django.core.cache import cache from django.core.cache import cache
from account.models import User from django.db.models import Count, Q
from submission.models import Submission, JudgeStatus
from utils.api import APIView
from account.decorators import check_contest_permission from account.decorators import check_contest_permission
from account.models import User
from contest.models import ContestRuleType
from submission.models import JudgeStatus, Submission
from utils.api import APIView
from utils.constants import CacheKey from utils.constants import CacheKey
from ..models import ProblemTag, Problem
from ..models import Problem, ProblemTag
from ..serializers import ( from ..serializers import (
ProblemListSerializer,
ProblemSafeSerializer,
ProblemSerializer, ProblemSerializer,
TagSerializer, TagSerializer,
ProblemSafeSerializer,
ProblemListSerializer,
) )
from contest.models import ContestRuleType
class ProblemTagAPI(APIView): class ProblemTagAPI(APIView):

View File

@@ -1,8 +1,9 @@
from django.db import models from django.db import models
from django.utils.timezone import now from django.utils.timezone import now
from account.models import User from account.models import User
from problem.models import Problem from problem.models import Problem
from utils.models import RichTextField, JSONField from utils.models import JSONField, RichTextField
class ProblemSet(models.Model): class ProblemSet(models.Model):

View File

@@ -1,8 +1,9 @@
from utils.api import UsernameSerializer, serializers from utils.api import UsernameSerializer, serializers
from .models import ( from .models import (
ProblemSet, ProblemSet,
ProblemSetProblem,
ProblemSetBadge, ProblemSetBadge,
ProblemSetProblem,
ProblemSetProgress, ProblemSetProgress,
UserBadge, UserBadge,
) )

View File

@@ -1,10 +1,12 @@
# 题单应用信号处理 # 题单应用信号处理
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
from .models import ProblemSetProblem, ProblemSetProgress, ProblemSetBadge, UserBadge
from django.db import transaction
import logging import logging
from django.db import transaction
from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver
from .models import ProblemSetBadge, ProblemSetProblem, ProblemSetProgress, UserBadge
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@@ -1,13 +1,14 @@
from django.urls import path from django.urls import path
from problemset.views.oj import ( from problemset.views.oj import (
ProblemSetAPI, ProblemSetAPI,
ProblemSetBadgeAPI,
ProblemSetDetailAPI, ProblemSetDetailAPI,
ProblemSetProblemAPI, ProblemSetProblemAPI,
ProblemSetProgressAPI, ProblemSetProgressAPI,
ProblemSetUserProgressAPI,
UserBadgeAPI, UserBadgeAPI,
UserProgressAPI, UserProgressAPI,
ProblemSetBadgeAPI,
ProblemSetUserProgressAPI,
) )
urlpatterns = [ urlpatterns = [

View File

@@ -1,28 +1,27 @@
from django.db.models import Q from django.db.models import Q
from utils.api import APIView, validate_serializer from account.decorators import ensure_created_by, super_admin_required
from account.decorators import super_admin_required, ensure_created_by from problem.models import Problem
from problemset.models import ( from problemset.models import (
ProblemSet, ProblemSet,
ProblemSetProblem,
ProblemSetBadge, ProblemSetBadge,
ProblemSetProblem,
ProblemSetProgress, ProblemSetProgress,
) )
from problemset.serializers import ( from problemset.serializers import (
ProblemSetSerializer,
ProblemSetListSerializer,
CreateProblemSetSerializer,
EditProblemSetSerializer,
ProblemSetProblemSerializer,
AddProblemToSetSerializer, AddProblemToSetSerializer,
EditProblemInSetSerializer,
ProblemSetBadgeSerializer,
CreateProblemSetBadgeSerializer, CreateProblemSetBadgeSerializer,
CreateProblemSetSerializer,
EditProblemInSetSerializer,
EditProblemSetBadgeSerializer, EditProblemSetBadgeSerializer,
EditProblemSetSerializer,
ProblemSetBadgeSerializer,
ProblemSetListSerializer,
ProblemSetProblemSerializer,
ProblemSetProgressSerializer, ProblemSetProgressSerializer,
ProblemSetSerializer,
) )
from problem.models import Problem from utils.api import APIView, validate_serializer
class ProblemSetAdminAPI(APIView): class ProblemSetAdminAPI(APIView):

View File

@@ -1,31 +1,28 @@
from django.db.models import Q, Avg, Count, Prefetch from django.db.models import Avg, Count, Prefetch, Q
from django.utils import timezone from django.utils import timezone
from utils.api import APIView, validate_serializer
from account.models import User from account.models import User
from problem.models import Problem
from problemset.models import ( from problemset.models import (
ProblemSet, ProblemSet,
ProblemSetProblem,
ProblemSetBadge, ProblemSetBadge,
ProblemSetProblem,
ProblemSetProgress, ProblemSetProgress,
ProblemSetSubmission, ProblemSetSubmission,
UserBadge, UserBadge,
) )
from problemset.serializers import ( from problemset.serializers import (
ProblemSetSerializer, JoinProblemSetSerializer,
ProblemSetBadgeSerializer,
ProblemSetListSerializer, ProblemSetListSerializer,
ProblemSetProblemSerializer, ProblemSetProblemSerializer,
ProblemSetBadgeSerializer,
ProblemSetProgressSerializer, ProblemSetProgressSerializer,
UserBadgeSerializer, ProblemSetSerializer,
JoinProblemSetSerializer,
UpdateProgressSerializer, UpdateProgressSerializer,
UserBadgeSerializer,
) )
from submission.models import Submission from submission.models import Submission
from problem.models import Problem from utils.api import APIView, validate_serializer
class ProblemSetAPI(APIView): class ProblemSetAPI(APIView):

View File

@@ -7,7 +7,6 @@ requires-python = ">=3.12"
dependencies = [ dependencies = [
"channels>=4.2.0", "channels>=4.2.0",
"channels-redis>=4.2.0", "channels-redis>=4.2.0",
"coverage==6.5.0",
"daphne>=4.1.2", "daphne>=4.1.2",
"django>=5.2.3", "django>=5.2.3",
"django-cas-ng==5.0.1", "django-cas-ng==5.0.1",
@@ -18,9 +17,6 @@ dependencies = [
"dramatiq==1.17.0", "dramatiq==1.17.0",
"entrypoints==0.4", "entrypoints==0.4",
"envelopes==0.4", "envelopes==0.4",
"flake8==7.0.0",
"flake8-coding==1.3.2",
"flake8-quotes==3.3.2",
"gunicorn==22.0.0", "gunicorn==22.0.0",
"jsonfield==3.1.0", "jsonfield==3.1.0",
"openai>=1.108.1", "openai>=1.108.1",
@@ -33,3 +29,18 @@ dependencies = [
"raven==6.10.0", "raven==6.10.0",
"xlsxwriter==3.2.0", "xlsxwriter==3.2.0",
] ]
[dependency-groups]
dev = [
"ruff>=0.15.11",
]
[tool.ruff]
line-length = 180
exclude = ["*/migrations/*", "*settings.py", "*/apps.py", ".venv"]
[tool.ruff.format]
quote-style = "double"
[tool.ruff.lint]
select = ["E", "F", "I"]

View File

@@ -1,27 +0,0 @@
import getopt
import os
import sys
opts, args = getopt.getopt(sys.argv[1:], "cm:", ["coverage=", "module="])
is_coverage = False
test_module = ""
setting = "oj.settings"
for opt, arg in opts:
if opt in ["-c", "--coverage"]:
is_coverage = True
if opt in ["-m", "--module"]:
test_module = arg
print("Coverage: {cov}".format(cov=is_coverage))
print("Module: {mod}".format(mod=(test_module if test_module else "All")))
print("running flake8...")
if os.system("flake8 --statistics ."):
exit()
ret = os.system('coverage run --include="$PWD/*" manage.py test {module} --settings={setting}'.format(module=test_module, setting=setting))
if not ret and is_coverage:
os.system("coverage html && open htmlcov/index.html")

View File

@@ -3,6 +3,7 @@ WebSocket consumers for submission updates
""" """
import json import json
import logging import logging
from channels.generic.websocket import AsyncWebsocketConsumer from channels.generic.websocket import AsyncWebsocketConsumer
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@@ -1,10 +1,9 @@
from django.db import models from django.db import models
from contest.models import Contest
from problem.models import Problem
from utils.constants import ContestStatus from utils.constants import ContestStatus
from utils.models import JSONField from utils.models import JSONField
from problem.models import Problem
from contest.models import Contest
from utils.shortcuts import rand_str from utils.shortcuts import rand_str

View File

@@ -2,10 +2,11 @@ from django.db import models
from django.db.models import F from django.db.models import F
from django.utils import timezone from django.utils import timezone
from .models import Submission from problemset.models import ProblemSetProgress
from utils.api import serializers from utils.api import serializers
from utils.serializers import LanguageNameChoiceField from utils.serializers import LanguageNameChoiceField
from problemset.models import ProblemSetProgress
from .models import Submission
def bulk_fetch_problemset_progress(user, problem_ids): def bulk_fetch_problemset_progress(user, problem_ids):

View File

@@ -1,10 +1,10 @@
from django.urls import path from django.urls import path
from ..views.oj import ( from ..views.oj import (
SubmissionAPI,
SubmissionListAPI,
ContestSubmissionListAPI, ContestSubmissionListAPI,
SubmissionAPI,
SubmissionExistsAPI, SubmissionExistsAPI,
SubmissionListAPI,
SubmissionsTodayCount, SubmissionsTodayCount,
) )

View File

@@ -1,12 +1,13 @@
from account.decorators import super_admin_required
from judge.tasks import judge_task
from utils.api import APIView
from ..models import Submission, JudgeStatus
from account.models import User, AdminType
from problem.models import Problem
from django.db.models import Count, Q from django.db.models import Count, Q
from account.decorators import super_admin_required
from account.models import AdminType, User
from judge.tasks import judge_task
from problem.models import Problem
from utils.api import APIView
from ..models import JudgeStatus, Submission
def get_real_name(username, class_name): def get_real_name(username, class_name):
if class_name and username.startswith("ks"): if class_name and username.startswith("ks"):

View File

@@ -1,8 +1,8 @@
from datetime import datetime
import ipaddress import ipaddress
from datetime import datetime
from account.decorators import login_required, check_contest_permission from account.decorators import check_contest_permission, login_required
from contest.models import ContestStatus, ContestRuleType from contest.models import ContestRuleType, ContestStatus
from judge.tasks import judge_task from judge.tasks import judge_task
from options.options import SysOptions from options.options import SysOptions
@@ -12,13 +12,16 @@ from utils.api import APIView, validate_serializer
from utils.cache import cache from utils.cache import cache
from utils.captcha import Captcha from utils.captcha import Captcha
from utils.throttling import TokenBucket from utils.throttling import TokenBucket
from ..models import Submission from ..models import Submission
from ..serializers import ( from ..serializers import (
CreateSubmissionSerializer, CreateSubmissionSerializer,
SubmissionModelSerializer,
ShareSubmissionSerializer, ShareSubmissionSerializer,
SubmissionListSerializer,
SubmissionModelSerializer,
SubmissionSafeModelSerializer,
bulk_fetch_problemset_progress,
) )
from ..serializers import SubmissionSafeModelSerializer, SubmissionListSerializer, bulk_fetch_problemset_progress
class SubmissionAPI(APIView): class SubmissionAPI(APIView):

View File

@@ -1,6 +1,8 @@
from django.db import models from django.db import models
from account.models import User from account.models import User
class Tutorial(models.Model): class Tutorial(models.Model):
TYPE_CHOICES = [ TYPE_CHOICES = [
('python', 'Python'), ('python', 'Python'),

View File

@@ -1,7 +1,9 @@
from rest_framework import serializers from rest_framework import serializers
from .models import Tutorial, Exercise
from account.serializers import UserSerializer from account.serializers import UserSerializer
from .models import Exercise, Tutorial
class TutorialListSerializer(serializers.ModelSerializer): class TutorialListSerializer(serializers.ModelSerializer):
created_by = UserSerializer(read_only=True) created_by = UserSerializer(read_only=True)

View File

@@ -1,5 +1,6 @@
from django.urls import path from django.urls import path
from ..views.admin import TutorialAdminAPI, TutorialVisibilityAPI, ExerciseAdminAPI
from ..views.admin import ExerciseAdminAPI, TutorialAdminAPI, TutorialVisibilityAPI
urlpatterns = [ urlpatterns = [
path("tutorial", TutorialAdminAPI.as_view()), path("tutorial", TutorialAdminAPI.as_view()),

View File

@@ -1,5 +1,6 @@
from django.urls import path from django.urls import path
from ..views.oj import TutorialAPI, TutorialTitlesAPI, ExerciseAPI
from ..views.oj import ExerciseAPI, TutorialAPI, TutorialTitlesAPI
urlpatterns = [ urlpatterns = [
path("tutorial", TutorialAPI.as_view()), path("tutorial", TutorialAPI.as_view()),

View File

@@ -1,16 +1,15 @@
from account.decorators import super_admin_required from account.decorators import super_admin_required
from utils.api import APIView, validate_serializer from tutorial.models import Exercise, Tutorial
from tutorial.models import Tutorial, Exercise
from tutorial.serializers import ( from tutorial.serializers import (
TutorialSerializer, CreateExerciseSerializer,
TutorialListSerializer,
CreateTutorialSerializer, CreateTutorialSerializer,
EditExerciseSerializer,
EditTutorialSerializer, EditTutorialSerializer,
ExerciseSerializer, ExerciseSerializer,
CreateExerciseSerializer, TutorialListSerializer,
EditExerciseSerializer, TutorialSerializer,
) )
from utils.api import APIView, validate_serializer
class TutorialAdminAPI(APIView): class TutorialAdminAPI(APIView):

View File

@@ -1,8 +1,7 @@
from tutorial.models import Exercise, Tutorial
from tutorial.serializers import ExerciseSerializer, TutorialSerializer
from utils.api import APIView from utils.api import APIView
from tutorial.models import Tutorial, Exercise
from tutorial.serializers import TutorialSerializer, ExerciseSerializer
class TutorialAPI(APIView): class TutorialAPI(APIView):
def get(self, request): def get(self, request):

View File

@@ -1,40 +0,0 @@
from django.urls import reverse
from django.test.testcases import TestCase
from rest_framework.test import APIClient
from account.models import AdminType, ProblemPermission, User, UserProfile
class APITestCase(TestCase):
client_class = APIClient
def create_user(self, username, password, admin_type=AdminType.REGULAR_USER, login=True,
problem_permission=ProblemPermission.NONE):
user = User.objects.create(username=username, admin_type=admin_type, problem_permission=problem_permission)
user.set_password(password)
UserProfile.objects.create(user=user)
user.save()
if login:
self.client.login(username=username, password=password)
return user
def create_admin(self, username="admin", password="admin", login=True):
return self.create_user(username=username, password=password, admin_type=AdminType.ADMIN,
problem_permission=ProblemPermission.OWN,
login=login)
def create_super_admin(self, username="root", password="root", login=True):
return self.create_user(username=username, password=password, admin_type=AdminType.SUPER_ADMIN,
problem_permission=ProblemPermission.ALL, login=login)
def reverse(self, url_name, *args, **kwargs):
return reverse(url_name, *args, **kwargs)
def assertSuccess(self, response):
if not response.data["error"] is None:
raise AssertionError("response with errors, response: " + str(response.data))
def assertFailed(self, response, msg=None):
self.assertTrue(response.data["error"] is not None)
if msg:
self.assertEqual(response.data["data"], msg)

View File

@@ -1,7 +1,7 @@
import json import json
from pathlib import Path from pathlib import Path
from django.core.cache import cache
from django.core.cache import cache
from django_redis.cache import RedisCache from django_redis.cache import RedisCache
from django_redis.client.default import DefaultClient from django_redis.client.default import DefaultClient

View File

@@ -12,8 +12,8 @@ limitations under the License.
""" """
import os import os
import time
import random import random
import time
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont

View File

@@ -1,6 +1,6 @@
from . import Captcha
from ..api import APIView from ..api import APIView
from ..shortcuts import img2base64 from ..shortcuts import img2base64
from . import Captcha
class CaptchaAPIView(APIView): class CaptchaAPIView(APIView):

View File

@@ -1,7 +1,6 @@
import os import os
import re
import datetime
import random import random
import re
from base64 import b64encode from base64 import b64encode
from io import BytesIO from io import BytesIO

View File

@@ -1,4 +1,5 @@
import os import os
import dramatiq import dramatiq
from utils.shortcuts import DRAMATIQ_WORKER_ARGS from utils.shortcuts import DRAMATIQ_WORKER_ARGS

View File

@@ -1,6 +1,6 @@
from django.urls import path from django.urls import path
from .views import SimditorImageUploadAPIView, SimditorFileUploadAPIView from .views import SimditorFileUploadAPIView, SimditorImageUploadAPIView
urlpatterns = [ urlpatterns = [
path("upload_image", SimditorImageUploadAPIView.as_view()), path("upload_image", SimditorImageUploadAPIView.as_view()),

View File

@@ -1,9 +1,11 @@
import os
from django.conf import settings
from account.serializers import ImageUploadForm, FileUploadForm
from utils.shortcuts import rand_str
from utils.api import CSRFExemptAPIView
import logging import logging
import os
from django.conf import settings
from account.serializers import FileUploadForm, ImageUploadForm
from utils.api import CSRFExemptAPIView
from utils.shortcuts import rand_str
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@@ -2,8 +2,9 @@
WebSocket utility functions for pushing real-time updates WebSocket utility functions for pushing real-time updates
""" """
import logging import logging
from channels.layers import get_channel_layer
from asgiref.sync import async_to_sync from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@@ -25,8 +25,8 @@ Python 2.6+ or 3.2+
Cannot defense xss in browser which is belowed IE7 Cannot defense xss in browser which is belowed IE7
浏览器版本IE7+ 或其他浏览器无法防御IE6及以下版本浏览器中的XSS 浏览器版本IE7+ 或其他浏览器无法防御IE6及以下版本浏览器中的XSS
""" """
import re
import copy import copy
import re
from html.parser import HTMLParser from html.parser import HTMLParser

109
uv.lock generated
View File

@@ -281,12 +281,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b8/40/c199d095151addf69efdb4b9ca3a4f20f70e20508d6222bffb9b76f58573/constantly-23.10.4-py3-none-any.whl", hash = "sha256:3fd9b4d1c3dc1ec9757f3c52aef7e53ad9323dbe39f51dfd4c43853b68dfa3f9", size = 13547, upload-time = "2023-10-28T23:18:23.038Z" }, { url = "https://files.pythonhosted.org/packages/b8/40/c199d095151addf69efdb4b9ca3a4f20f70e20508d6222bffb9b76f58573/constantly-23.10.4-py3-none-any.whl", hash = "sha256:3fd9b4d1c3dc1ec9757f3c52aef7e53ad9323dbe39f51dfd4c43853b68dfa3f9", size = 13547, upload-time = "2023-10-28T23:18:23.038Z" },
] ]
[[package]]
name = "coverage"
version = "6.5.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5c/66/38d1870cb7cf62da49add1d6803fdbcdef632b2808b5c80bcac35b7634d8/coverage-6.5.0.tar.gz", hash = "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84", size = 775224, upload-time = "2022-09-29T20:05:58.509Z" }
[[package]] [[package]]
name = "cryptography" name = "cryptography"
version = "46.0.3" version = "46.0.3"
@@ -466,41 +460,6 @@ version = "0.4"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/2e/ac/0aaba34d717868729428bf4dca601c93cd6b0f9123894f2509911027b0dd/Envelopes-0.4.tar.gz", hash = "sha256:a4a02b4dc21467794d3a646f946d99a8c5b3311b2df8e211f96ca9e0b838e7e0", size = 33450, upload-time = "2013-11-13T20:02:09.033Z" } sdist = { url = "https://files.pythonhosted.org/packages/2e/ac/0aaba34d717868729428bf4dca601c93cd6b0f9123894f2509911027b0dd/Envelopes-0.4.tar.gz", hash = "sha256:a4a02b4dc21467794d3a646f946d99a8c5b3311b2df8e211f96ca9e0b838e7e0", size = 33450, upload-time = "2013-11-13T20:02:09.033Z" }
[[package]]
name = "flake8"
version = "7.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mccabe" },
{ name = "pycodestyle" },
{ name = "pyflakes" },
]
sdist = { url = "https://files.pythonhosted.org/packages/40/3c/3464b567aa367b221fa610bbbcce8015bf953977d21e52f2d711b526fb48/flake8-7.0.0.tar.gz", hash = "sha256:33f96621059e65eec474169085dc92bf26e7b2d47366b70be2f67ab80dc25132", size = 48219, upload-time = "2024-01-05T00:41:52.142Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e3/01/cc8cdec7b61db0315c2ab62d80677a138ef06832ec17f04d87e6ef858f7f/flake8-7.0.0-py2.py3-none-any.whl", hash = "sha256:a6dfbb75e03252917f2473ea9653f7cd799c3064e54d4c8140044c5c065f53c3", size = 57570, upload-time = "2024-01-05T00:41:49.837Z" },
]
[[package]]
name = "flake8-coding"
version = "1.3.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "flake8" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e8/0e/cbba2b2da4e0ccf4098e8bd333d39531ff0a6aed91d187bc762ac6b9d263/flake8-coding-1.3.2.tar.gz", hash = "sha256:b8f4d5157a8f74670e6cfea732c3d9f4291a4e994c8701d2c55f787c6e6cb741", size = 7308, upload-time = "2019-06-16T13:42:53.782Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/83/a8/0de26928c40727ec29289b4f5c751a75e4cdd639faed9ab01b835fd0883c/flake8_coding-1.3.2-py2.py3-none-any.whl", hash = "sha256:79704112c44d09d4ab6c8965e76a20c3f7073d52146db60303bce777d9612260", size = 7570, upload-time = "2019-06-16T13:42:56.32Z" },
]
[[package]]
name = "flake8-quotes"
version = "3.3.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "flake8" },
]
sdist = { url = "https://files.pythonhosted.org/packages/88/89/7c5a8e671c17ae49e4b89b06d9d2db5608eea7c66b04d70d27390da9ddb9/flake8-quotes-3.3.2.tar.gz", hash = "sha256:6e26892b632dacba517bf27219c459a8396dcfac0f5e8204904c5a4ba9b480e1", size = 12530, upload-time = "2022-12-19T16:48:09.308Z" }
[[package]] [[package]]
name = "gunicorn" name = "gunicorn"
version = "22.0.0" version = "22.0.0"
@@ -743,15 +702,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/92/aa/df863bcc39c5e0946263454aba394de8a9084dbaff8ad143846b0d844739/lxml-6.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:bb4c1847b303835d89d785a18801a883436cdfd5dc3d62947f9c49e24f0f5a2c", size = 3822205, upload-time = "2025-09-22T04:03:36.249Z" }, { url = "https://files.pythonhosted.org/packages/92/aa/df863bcc39c5e0946263454aba394de8a9084dbaff8ad143846b0d844739/lxml-6.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:bb4c1847b303835d89d785a18801a883436cdfd5dc3d62947f9c49e24f0f5a2c", size = 3822205, upload-time = "2025-09-22T04:03:36.249Z" },
] ]
[[package]]
name = "mccabe"
version = "0.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" },
]
[[package]] [[package]]
name = "msgpack" name = "msgpack"
version = "1.1.2" version = "1.1.2"
@@ -803,7 +753,6 @@ source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "channels" }, { name = "channels" },
{ name = "channels-redis" }, { name = "channels-redis" },
{ name = "coverage" },
{ name = "daphne" }, { name = "daphne" },
{ name = "django" }, { name = "django" },
{ name = "django-cas-ng" }, { name = "django-cas-ng" },
@@ -814,9 +763,6 @@ dependencies = [
{ name = "dramatiq" }, { name = "dramatiq" },
{ name = "entrypoints" }, { name = "entrypoints" },
{ name = "envelopes" }, { name = "envelopes" },
{ name = "flake8" },
{ name = "flake8-coding" },
{ name = "flake8-quotes" },
{ name = "gunicorn" }, { name = "gunicorn" },
{ name = "jsonfield" }, { name = "jsonfield" },
{ name = "openai" }, { name = "openai" },
@@ -830,11 +776,15 @@ dependencies = [
{ name = "xlsxwriter" }, { name = "xlsxwriter" },
] ]
[package.dev-dependencies]
dev = [
{ name = "ruff" },
]
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "channels", specifier = ">=4.2.0" }, { name = "channels", specifier = ">=4.2.0" },
{ name = "channels-redis", specifier = ">=4.2.0" }, { name = "channels-redis", specifier = ">=4.2.0" },
{ name = "coverage", specifier = "==6.5.0" },
{ name = "daphne", specifier = ">=4.1.2" }, { name = "daphne", specifier = ">=4.1.2" },
{ name = "django", specifier = ">=5.2.3" }, { name = "django", specifier = ">=5.2.3" },
{ name = "django-cas-ng", specifier = "==5.0.1" }, { name = "django-cas-ng", specifier = "==5.0.1" },
@@ -845,9 +795,6 @@ requires-dist = [
{ name = "dramatiq", specifier = "==1.17.0" }, { name = "dramatiq", specifier = "==1.17.0" },
{ name = "entrypoints", specifier = "==0.4" }, { name = "entrypoints", specifier = "==0.4" },
{ name = "envelopes", specifier = "==0.4" }, { name = "envelopes", specifier = "==0.4" },
{ name = "flake8", specifier = "==7.0.0" },
{ name = "flake8-coding", specifier = "==1.3.2" },
{ name = "flake8-quotes", specifier = "==3.3.2" },
{ name = "gunicorn", specifier = "==22.0.0" }, { name = "gunicorn", specifier = "==22.0.0" },
{ name = "jsonfield", specifier = "==3.1.0" }, { name = "jsonfield", specifier = "==3.1.0" },
{ name = "openai", specifier = ">=1.108.1" }, { name = "openai", specifier = ">=1.108.1" },
@@ -861,6 +808,9 @@ requires-dist = [
{ name = "xlsxwriter", specifier = "==3.2.0" }, { name = "xlsxwriter", specifier = "==3.2.0" },
] ]
[package.metadata.requires-dev]
dev = [{ name = "ruff", specifier = ">=0.15.11" }]
[[package]] [[package]]
name = "openai" name = "openai"
version = "2.14.0" version = "2.14.0"
@@ -995,15 +945,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" },
] ]
[[package]]
name = "pycodestyle"
version = "2.11.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/34/8f/fa09ae2acc737b9507b5734a9aec9a2b35fa73409982f57db1b42f8c3c65/pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f", size = 38974, upload-time = "2023-10-12T23:39:39.762Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b1/90/a998c550d0ddd07e38605bb5c455d00fcc177a800ff9cc3dafdcb3dd7b56/pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67", size = 31132, upload-time = "2023-10-12T23:39:38.242Z" },
]
[[package]] [[package]]
name = "pycparser" name = "pycparser"
version = "2.23" version = "2.23"
@@ -1099,15 +1040,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" },
] ]
[[package]]
name = "pyflakes"
version = "3.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/57/f9/669d8c9c86613c9d568757c7f5824bd3197d7b1c6c27553bc5618a27cce2/pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f", size = 63788, upload-time = "2024-01-05T00:28:47.703Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d4/d7/f1b7db88d8e4417c5d47adad627a93547f44bdc9028372dbd2313f34a855/pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a", size = 62725, upload-time = "2024-01-05T00:28:45.903Z" },
]
[[package]] [[package]]
name = "pyopenssl" name = "pyopenssl"
version = "25.3.0" version = "25.3.0"
@@ -1191,6 +1123,31 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
] ]
[[package]]
name = "ruff"
version = "0.15.11"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e4/8d/192f3d7103816158dfd5ea50d098ef2aec19194e6cbccd4b3485bdb2eb2d/ruff-0.15.11.tar.gz", hash = "sha256:f092b21708bf0e7437ce9ada249dfe688ff9a0954fc94abab05dcea7dcd29c33", size = 4637264, upload-time = "2026-04-16T18:46:26.58Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/02/1e/6aca3427f751295ab011828e15e9bf452200ac74484f1db4be0197b8170b/ruff-0.15.11-py3-none-linux_armv6l.whl", hash = "sha256:e927cfff503135c558eb581a0c9792264aae9507904eb27809cdcff2f2c847b7", size = 10607943, upload-time = "2026-04-16T18:46:05.967Z" },
{ url = "https://files.pythonhosted.org/packages/e7/26/1341c262e74f36d4e84f3d6f4df0ac68cd53331a66bfc5080daa17c84c0b/ruff-0.15.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7a1b5b2938d8f890b76084d4fa843604d787a912541eae85fd7e233398bbb73e", size = 10988592, upload-time = "2026-04-16T18:46:00.742Z" },
{ url = "https://files.pythonhosted.org/packages/03/71/850b1d6ffa9564fbb6740429bad53df1094082fe515c8c1e74b6d8d05f18/ruff-0.15.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d4176f3d194afbdaee6e41b9ccb1a2c287dba8700047df474abfbe773825d1cb", size = 10338501, upload-time = "2026-04-16T18:46:03.723Z" },
{ url = "https://files.pythonhosted.org/packages/f2/11/cc1284d3e298c45a817a6aadb6c3e1d70b45c9b36d8d9cce3387b495a03a/ruff-0.15.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b17c886fb88203ced3afe7f14e8d5ae96e9d2f4ccc0ee66aa19f2c2675a27e4", size = 10670693, upload-time = "2026-04-16T18:46:41.941Z" },
{ url = "https://files.pythonhosted.org/packages/ce/9e/f8288b034ab72b371513c13f9a41d9ba3effac54e24bfb467b007daee2ca/ruff-0.15.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:49fafa220220afe7758a487b048de4c8f9f767f37dfefad46b9dd06759d003eb", size = 10416177, upload-time = "2026-04-16T18:46:21.717Z" },
{ url = "https://files.pythonhosted.org/packages/85/71/504d79abfd3d92532ba6bbe3d1c19fada03e494332a59e37c7c2dabae427/ruff-0.15.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2ab8427e74a00d93b8bda1307b1e60970d40f304af38bccb218e056c220120d", size = 11221886, upload-time = "2026-04-16T18:46:15.086Z" },
{ url = "https://files.pythonhosted.org/packages/43/5a/947e6ab7a5ad603d65b474be15a4cbc6d29832db5d762cd142e4e3a74164/ruff-0.15.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:195072c0c8e1fc8f940652073df082e37a5d9cb43b4ab1e4d0566ab8977a13b7", size = 12075183, upload-time = "2026-04-16T18:46:07.944Z" },
{ url = "https://files.pythonhosted.org/packages/9f/a1/0b7bb6268775fdd3a0818aee8efd8f5b4e231d24dd4d528ced2534023182/ruff-0.15.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3a0996d486af3920dec930a2e7daed4847dfc12649b537a9335585ada163e9e", size = 11516575, upload-time = "2026-04-16T18:46:31.687Z" },
{ url = "https://files.pythonhosted.org/packages/30/c3/bb5168fc4d233cc06e95f482770d0f3c87945a0cd9f614b90ea8dc2f2833/ruff-0.15.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bef2cb556d509259f1fe440bb9cd33c756222cf0a7afe90d15edf0866702431", size = 11306537, upload-time = "2026-04-16T18:46:36.988Z" },
{ url = "https://files.pythonhosted.org/packages/e4/92/4cfae6441f3967317946f3b788136eecf093729b94d6561f963ed810c82e/ruff-0.15.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:030d921a836d7d4a12cf6e8d984a88b66094ccb0e0f17ddd55067c331191bf19", size = 11296813, upload-time = "2026-04-16T18:46:24.182Z" },
{ url = "https://files.pythonhosted.org/packages/43/26/972784c5dde8313acde8ac71ba8ac65475b85db4a2352a76c9934361f9bc/ruff-0.15.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0e783b599b4577788dbbb66b9addcef87e9a8832f4ce0c19e34bf55543a2f890", size = 10633136, upload-time = "2026-04-16T18:46:39.802Z" },
{ url = "https://files.pythonhosted.org/packages/5b/53/3985a4f185020c2f367f2e08a103032e12564829742a1b417980ce1514a0/ruff-0.15.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ae90592246625ba4a34349d68ec28d4400d75182b71baa196ddb9f82db025ef5", size = 10424701, upload-time = "2026-04-16T18:46:10.381Z" },
{ url = "https://files.pythonhosted.org/packages/d3/57/bf0dfb32241b56c83bb663a826133da4bf17f682ba8c096973065f6e6a68/ruff-0.15.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1f111d62e3c983ed20e0ca2e800f8d77433a5b1161947df99a5c2a3fb60514f0", size = 10873887, upload-time = "2026-04-16T18:46:29.157Z" },
{ url = "https://files.pythonhosted.org/packages/02/05/e48076b2a57dc33ee8c7a957296f97c744ca891a8ffb4ffb1aaa3b3f517d/ruff-0.15.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:06f483d6646f59eaffba9ae30956370d3a886625f511a3108994000480621d1c", size = 11404316, upload-time = "2026-04-16T18:46:19.462Z" },
{ url = "https://files.pythonhosted.org/packages/88/27/0195d15fe7a897cbcba0904792c4b7c9fdd958456c3a17d2ea6093716a9a/ruff-0.15.11-py3-none-win32.whl", hash = "sha256:476a2aa56b7da0b73a3ee80b6b2f0e19cce544245479adde7baa65466664d5f3", size = 10655535, upload-time = "2026-04-16T18:46:12.47Z" },
{ url = "https://files.pythonhosted.org/packages/3a/5e/c927b325bd4c1d3620211a4b96f47864633199feed60fa936025ab27e090/ruff-0.15.11-py3-none-win_amd64.whl", hash = "sha256:8b6756d88d7e234fb0c98c91511aae3cd519d5e3ed271cae31b20f39cb2a12a3", size = 11779692, upload-time = "2026-04-16T18:46:17.268Z" },
{ url = "https://files.pythonhosted.org/packages/63/b6/aeadee5443e49baa2facd51131159fd6301cc4ccfc1541e4df7b021c37dd/ruff-0.15.11-py3-none-win_arm64.whl", hash = "sha256:063fed18cc1bbe0ee7393957284a6fe8b588c6a406a285af3ee3f46da2391ee4", size = 11032614, upload-time = "2026-04-16T18:46:34.487Z" },
]
[[package]] [[package]]
name = "service-identity" name = "service-identity"
version = "24.2.0" version = "24.2.0"