fmt
This commit is contained in:
10
.flake8
10
.flake8
@@ -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
|
||||
@@ -2,10 +2,11 @@ import functools
|
||||
import hashlib
|
||||
import time
|
||||
|
||||
from contest.models import Contest, ContestRuleType, ContestStatus, ContestType
|
||||
from problem.models import Problem
|
||||
from contest.models import Contest, ContestType, ContestStatus, ContestRuleType
|
||||
from utils.api import JSONResponse, APIError
|
||||
from utils.api import APIError, JSONResponse
|
||||
from utils.constants import CONTEST_PASSWORD_SESSION_KEY
|
||||
|
||||
from .models import ProblemPermission
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
from django.conf import settings
|
||||
from django.db import connection
|
||||
from django.utils.timezone import now
|
||||
from django.utils.deprecation import MiddlewareMixin
|
||||
from django.utils.timezone import now
|
||||
|
||||
from utils.api import JSONResponse
|
||||
from account.models import User
|
||||
from utils.api import JSONResponse
|
||||
|
||||
|
||||
class APITokenAuthMiddleware(MiddlewareMixin):
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from django.contrib.auth.models import AbstractBaseUser
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AbstractBaseUser
|
||||
from django.db import models
|
||||
|
||||
from utils.models import JSONField
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from django import forms
|
||||
|
||||
from utils.api import serializers, UsernameSerializer
|
||||
from utils.api import UsernameSerializer, serializers
|
||||
|
||||
from .models import AdminType, ProblemPermission, User, UserProfile
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import logging
|
||||
|
||||
import dramatiq
|
||||
|
||||
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__)
|
||||
|
||||
|
||||
646
account/tests.py
646
account/tests.py
@@ -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)
|
||||
@@ -1,6 +1,6 @@
|
||||
from django.urls import path
|
||||
|
||||
from ..views.admin import UserAdminAPI, GenerateUserAPI, ResetUserPasswordAPI
|
||||
from ..views.admin import GenerateUserAPI, ResetUserPasswordAPI, UserAdminAPI
|
||||
|
||||
urlpatterns = [
|
||||
path("user", UserAdminAPI.as_view()),
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
from django.urls import path
|
||||
|
||||
from utils.captcha.views import CaptchaAPIView
|
||||
|
||||
from ..views.oj import (
|
||||
SSOAPI,
|
||||
ApplyResetPasswordAPI,
|
||||
ResetPasswordAPI,
|
||||
UserChangePasswordAPI,
|
||||
AvatarUploadAPI,
|
||||
CheckTFARequiredAPI,
|
||||
Metrics,
|
||||
UserRegisterAPI,
|
||||
OpenAPIAppkeyAPI,
|
||||
ProfileProblemDisplayIDRefreshAPI,
|
||||
ResetPasswordAPI,
|
||||
SessionManagementAPI,
|
||||
TwoFactorAuthAPI,
|
||||
UserActivityRankAPI,
|
||||
UserChangeEmailAPI,
|
||||
UserChangePasswordAPI,
|
||||
UserLoginAPI,
|
||||
UserLogoutAPI,
|
||||
UsernameOrEmailCheck,
|
||||
AvatarUploadAPI,
|
||||
TwoFactorAuthAPI,
|
||||
UserProblemRankAPI,
|
||||
UserProfileAPI,
|
||||
UserRankAPI,
|
||||
UserActivityRankAPI,
|
||||
UserProblemRankAPI,
|
||||
CheckTFARequiredAPI,
|
||||
SessionManagementAPI,
|
||||
ProfileProblemDisplayIDRefreshAPI,
|
||||
OpenAPIAppkeyAPI,
|
||||
SSOAPI,
|
||||
UserRegisterAPI,
|
||||
)
|
||||
|
||||
from utils.captcha.views import CaptchaAPIView
|
||||
|
||||
urlpatterns = [
|
||||
path("login", UserLoginAPI.as_view()),
|
||||
path("logout", UserLogoutAPI.as_view()),
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import os
|
||||
import re
|
||||
import xlsxwriter
|
||||
|
||||
from django.db import transaction, IntegrityError
|
||||
from django.db.models import Q, F
|
||||
from django.http import HttpResponse
|
||||
import xlsxwriter
|
||||
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 submission.models import Submission
|
||||
@@ -16,10 +16,10 @@ from ..decorators import super_admin_required
|
||||
from ..models import AdminType, ProblemPermission, User, UserProfile
|
||||
from ..serializers import (
|
||||
EditUserSerializer,
|
||||
UserAdminSerializer,
|
||||
GenerateUserSerializer,
|
||||
ImportUserSerializer,
|
||||
UserAdminSerializer,
|
||||
)
|
||||
from ..serializers import ImportUserSerializer
|
||||
|
||||
|
||||
# ks251XXX 或者 ks2510XX 返回 251 或者 2510
|
||||
|
||||
@@ -2,44 +2,42 @@ import os
|
||||
from datetime import timedelta
|
||||
from importlib import import_module
|
||||
|
||||
import qrcode
|
||||
from django.conf import settings
|
||||
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.utils import timezone
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.timezone import now
|
||||
from django.views.decorators.csrf import ensure_csrf_cookie, csrf_exempt
|
||||
from django.db.models import Count, Q
|
||||
from django.utils import timezone
|
||||
|
||||
import qrcode
|
||||
from django.views.decorators.csrf import csrf_exempt, ensure_csrf_cookie
|
||||
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 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.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 ..models import User, UserProfile, AdminType
|
||||
from ..models import AdminType, User, UserProfile
|
||||
from ..serializers import (
|
||||
ApplyResetPasswordSerializer,
|
||||
ResetPasswordSerializer,
|
||||
UserChangePasswordSerializer,
|
||||
UserLoginSerializer,
|
||||
UserRegisterSerializer,
|
||||
UsernameOrEmailCheckSerializer,
|
||||
RankInfoSerializer,
|
||||
UserChangeEmailSerializer,
|
||||
SSOSerializer,
|
||||
)
|
||||
from ..serializers import (
|
||||
TwoFactorAuthCodeSerializer,
|
||||
UserProfileSerializer,
|
||||
EditUserProfileSerializer,
|
||||
ImageUploadForm,
|
||||
RankInfoSerializer,
|
||||
ResetPasswordSerializer,
|
||||
SSOSerializer,
|
||||
TwoFactorAuthCodeSerializer,
|
||||
UserChangeEmailSerializer,
|
||||
UserChangePasswordSerializer,
|
||||
UserLoginSerializer,
|
||||
UsernameOrEmailCheckSerializer,
|
||||
UserProfileSerializer,
|
||||
UserRegisterSerializer,
|
||||
)
|
||||
from ..tasks import send_email_async
|
||||
|
||||
|
||||
@@ -1,27 +1,26 @@
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timedelta
|
||||
import hashlib
|
||||
import json
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
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.http import StreamingHttpResponse
|
||||
from django.utils import timezone
|
||||
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.openai import get_ai_client
|
||||
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
|
||||
DIFFICULTY_MAP = {"Low": "简单", "Mid": "中等", "High": "困难"}
|
||||
DEFAULT_CLASS_SIZE = 45
|
||||
@@ -598,7 +597,11 @@ class AIAnalysisAPI(APIView):
|
||||
|
||||
client = get_ai_client()
|
||||
|
||||
system_prompt = "你是一个风趣的编程老师,学生使用判题狗平台进行编程练习。请根据学生提供的详细数据和每周数据,给出用户的学习建议,最后写一句鼓励学生的话。请使用 markdown 格式输出,不要在代码块中输出。"
|
||||
system_prompt = (
|
||||
"你是一个风趣的编程老师,学生使用判题狗平台进行编程练习。"
|
||||
"请根据学生提供的详细数据和每周数据,给出用户的学习建议,最后写一句鼓励学生的话。"
|
||||
"请使用 markdown 格式输出,不要在代码块中输出。"
|
||||
)
|
||||
user_prompt = f"这段时间内的详细数据: {details}\n(其中部分字段含义是 flowcharts:流程图的提交,solved:代码的提交)\n每周或每月的数据: {duration}"
|
||||
|
||||
def on_complete(full_text):
|
||||
|
||||
@@ -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)
|
||||
@@ -1,12 +1,11 @@
|
||||
from account.decorators import super_admin_required
|
||||
from utils.api import APIView, validate_serializer
|
||||
|
||||
from announcement.models import Announcement
|
||||
from announcement.serializers import (
|
||||
AnnouncementSerializer,
|
||||
CreateAnnouncementSerializer,
|
||||
EditAnnouncementSerializer,
|
||||
)
|
||||
from utils.api import APIView, validate_serializer
|
||||
|
||||
|
||||
class AnnouncementAdminAPI(APIView):
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from utils.api import APIView
|
||||
|
||||
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):
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from django.db import models
|
||||
|
||||
# 如果需要存储班级PK历史记录,可以在这里定义模型
|
||||
# 目前暂时不需要,因为都是实时计算
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from django.urls import path
|
||||
|
||||
from ..views.oj import ClassRankAPI, UserClassRankAPI, ClassPKAPI
|
||||
from ..views.oj import ClassPKAPI, ClassRankAPI, UserClassRankAPI
|
||||
|
||||
urlpatterns = [
|
||||
path("class_rank", ClassRankAPI.as_view()),
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import re
|
||||
import statistics
|
||||
from datetime import datetime
|
||||
from django.db.models import Sum, Avg
|
||||
|
||||
from django.db.models import Avg, Sum
|
||||
from django.utils import timezone
|
||||
from utils.api import APIView
|
||||
|
||||
from account.decorators import login_required
|
||||
from account.models import User, UserProfile, AdminType
|
||||
from submission.models import Submission, JudgeStatus
|
||||
from account.models import AdminType, User, UserProfile
|
||||
from submission.models import JudgeStatus, Submission
|
||||
from utils.api import APIView
|
||||
|
||||
|
||||
class ClassRankAPI(APIView):
|
||||
|
||||
@@ -2,7 +2,6 @@ from django.urls import path
|
||||
|
||||
from ..views.admin import CommentAPI
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path("comment", CommentAPI.as_view()),
|
||||
]
|
||||
|
||||
@@ -2,7 +2,6 @@ from django.urls import path
|
||||
|
||||
from ..views.oj import CommentAPI, CommentStatisticsAPI
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path("comment", CommentAPI.as_view()),
|
||||
path("comment/statistics", CommentStatisticsAPI.as_view()),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from account.decorators import super_admin_required
|
||||
from comment.models import Comment
|
||||
from comment.serializers import CommentListSerializer
|
||||
from problem.models import Problem
|
||||
from utils.api import APIView
|
||||
from comment.models import Comment
|
||||
|
||||
|
||||
class CommentAPI(APIView):
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
from django.core.cache import cache
|
||||
from django.db.models import Avg, Count
|
||||
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 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 comment.serializers import CreateCommentSerializer, CommentSerializer
|
||||
from submission.models import Submission, JudgeStatus
|
||||
from utils.constants import CacheKey
|
||||
|
||||
|
||||
class CommentAPI(APIView):
|
||||
|
||||
@@ -3,6 +3,7 @@ WebSocket consumers for configuration updates
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
|
||||
from channels.generic.websocket import AsyncWebsocketConsumer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
185
conf/tests.py
185
conf/tests.py
@@ -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)
|
||||
@@ -2,13 +2,13 @@ from django.urls import path
|
||||
|
||||
from ..views import (
|
||||
SMTPAPI,
|
||||
JudgeServerAPI,
|
||||
WebsiteConfigAPI,
|
||||
TestCasePruneAPI,
|
||||
SMTPTestAPI,
|
||||
ReleaseNotesAPI,
|
||||
DashboardInfoAPI,
|
||||
JudgeServerAPI,
|
||||
RandomUsernameAPI,
|
||||
ReleaseNotesAPI,
|
||||
SMTPTestAPI,
|
||||
TestCasePruneAPI,
|
||||
WebsiteConfigAPI,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
from django.urls import path
|
||||
|
||||
from ..views import (
|
||||
ClassUsernamesAPI,
|
||||
HitokotoAPI,
|
||||
JudgeServerHeartbeatAPI,
|
||||
LanguagesAPI,
|
||||
WebsiteConfigAPI,
|
||||
ClassUsernamesAPI,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
|
||||
@@ -22,18 +22,19 @@ from problem.models import Problem
|
||||
from submission.models import Submission
|
||||
from utils.api import APIView, CSRFExemptAPIView, validate_serializer
|
||||
from utils.cache import JsonDataLoader
|
||||
from utils.shortcuts import send_email, get_env
|
||||
from utils.xss_filter import XSSHtml
|
||||
from utils.shortcuts import get_env, send_email
|
||||
from utils.websocket import push_config_update
|
||||
from utils.xss_filter import XSSHtml
|
||||
|
||||
from .models import JudgeServer
|
||||
from .serializers import (
|
||||
CreateEditWebsiteConfigSerializer,
|
||||
CreateSMTPConfigSerializer,
|
||||
EditJudgeServerSerializer,
|
||||
EditSMTPConfigSerializer,
|
||||
JudgeServerHeartbeatSerializer,
|
||||
JudgeServerSerializer,
|
||||
TestSMTPConfigSerializer,
|
||||
EditJudgeServerSerializer,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from utils.api import UsernameSerializer, serializers
|
||||
|
||||
from .models import Contest, ContestAnnouncement, ContestRuleType
|
||||
from .models import ACMContestRank, OIContestRank
|
||||
from .models import ACMContestRank, Contest, ContestAnnouncement, ContestRuleType, OIContestRank
|
||||
|
||||
|
||||
class CreateConetestSeriaizer(serializers.Serializer):
|
||||
|
||||
162
contest/tests.py
162
contest/tests.py
@@ -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)
|
||||
@@ -1,6 +1,6 @@
|
||||
from django.urls import path
|
||||
|
||||
from ..views.admin import ContestAnnouncementAPI, ContestAPI, ACMContestHelper, DownloadContestSubmissions
|
||||
from ..views.admin import ACMContestHelper, ContestAnnouncementAPI, ContestAPI, DownloadContestSubmissions
|
||||
|
||||
urlpatterns = [
|
||||
path("contest", ContestAPI.as_view()),
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
from django.urls import path
|
||||
|
||||
from ..views.oj import ContestAnnouncementListAPI
|
||||
from ..views.oj import ContestPasswordVerifyAPI, ContestAccessAPI
|
||||
from ..views.oj import ContestListAPI, ContestAPI
|
||||
from ..views.oj import ContestRankAPI
|
||||
from ..views.oj import ContestAccessAPI, ContestAnnouncementListAPI, ContestAPI, ContestListAPI, ContestPasswordVerifyAPI, ContestRankAPI
|
||||
|
||||
urlpatterns = [
|
||||
path("contests", ContestListAPI.as_view()),
|
||||
|
||||
@@ -6,25 +6,25 @@ from ipaddress import ip_network
|
||||
import dateutil.parser
|
||||
from django.http import FileResponse
|
||||
|
||||
from problem.models import Problem
|
||||
|
||||
from account.decorators import super_admin_required
|
||||
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.cache import cache
|
||||
from utils.constants import CacheKey
|
||||
from utils.shortcuts import rand_str
|
||||
from utils.tasks import delete_files
|
||||
from ..models import Contest, ContestAnnouncement, ACMContestRank
|
||||
|
||||
from ..models import ACMContestRank, Contest, ContestAnnouncement
|
||||
from ..serializers import (
|
||||
ContestAnnouncementSerializer,
|
||||
ACMContesHelperSerializer,
|
||||
ContestAdminSerializer,
|
||||
ContestAnnouncementSerializer,
|
||||
CreateConetestSeriaizer,
|
||||
CreateContestAnnouncementSerializer,
|
||||
EditConetestSeriaizer,
|
||||
EditContestAnnouncementSerializer,
|
||||
ACMContesHelperSerializer,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,26 +1,23 @@
|
||||
import io
|
||||
|
||||
import xlsxwriter
|
||||
from django.core.cache import cache
|
||||
from django.http import HttpResponse
|
||||
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 utils.api import APIView, validate_serializer
|
||||
from utils.constants import CacheKey, CONTEST_PASSWORD_SESSION_KEY
|
||||
from utils.shortcuts import datetime2str, check_is_id
|
||||
from account.models import AdminType
|
||||
from account.decorators import (
|
||||
login_required,
|
||||
check_contest_permission,
|
||||
check_contest_password,
|
||||
)
|
||||
from utils.constants import CONTEST_PASSWORD_SESSION_KEY, CacheKey, ContestRuleType, ContestStatus
|
||||
from utils.shortcuts import check_is_id, datetime2str
|
||||
|
||||
from utils.constants import ContestRuleType, ContestStatus
|
||||
from ..models import ContestAnnouncement, Contest, OIContestRank, ACMContestRank
|
||||
from ..serializers import ContestAnnouncementSerializer
|
||||
from ..serializers import ContestSerializer, ContestPasswordVerifySerializer
|
||||
from ..serializers import OIContestRankSerializer, ACMContestRankSerializer
|
||||
from ..models import ACMContestRank, Contest, ContestAnnouncement, OIContestRank
|
||||
from ..serializers import ACMContestRankSerializer, ContestAnnouncementSerializer, ContestPasswordVerifySerializer, ContestSerializer, OIContestRankSerializer
|
||||
|
||||
|
||||
class ContestAnnouncementListAPI(APIView):
|
||||
|
||||
6
dev.py
6
dev.py
@@ -6,13 +6,13 @@ WebSocket 开发服务器启动脚本
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
import platform
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from threading import Thread
|
||||
import time
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@@ -3,6 +3,7 @@ WebSocket consumers for flowchart evaluation updates
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
|
||||
from channels.generic.websocket import AsyncWebsocketConsumer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from django.db import models
|
||||
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 utils.shortcuts import rand_str
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from .models import FlowchartSubmission
|
||||
|
||||
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import dramatiq
|
||||
import json
|
||||
import time
|
||||
|
||||
import dramatiq
|
||||
from django.db import transaction
|
||||
from django.utils import timezone
|
||||
|
||||
from utils.openai import get_ai_client
|
||||
from utils.shortcuts import DRAMATIQ_WORKER_ARGS
|
||||
|
||||
from .models import FlowchartSubmission, FlowchartSubmissionStatus
|
||||
|
||||
|
||||
@dramatiq.actor(**DRAMATIQ_WORKER_ARGS(max_retries=3))
|
||||
def evaluate_flowchart_task(submission_id):
|
||||
"""异步AI评分任务"""
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
from django.urls import path
|
||||
|
||||
from ..views.oj import (
|
||||
FlowchartSubmissionAPI,
|
||||
FlowchartSubmissionListAPI,
|
||||
FlowchartSubmissionRetryAPI,
|
||||
FlowchartSubmissionCurrentAPI,
|
||||
FlowchartSubmissionDetailAPI,
|
||||
FlowchartSubmissionListAPI,
|
||||
FlowchartSubmissionRetryAPI,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
from utils.api import APIView
|
||||
from account.decorators import login_required
|
||||
from flowchart.models import FlowchartSubmission, FlowchartSubmissionStatus
|
||||
from flowchart.serializers import (
|
||||
CreateFlowchartSubmissionSerializer,
|
||||
FlowchartSubmissionSerializer,
|
||||
FlowchartSubmissionListSerializer,
|
||||
FlowchartSubmissionSerializer,
|
||||
)
|
||||
from flowchart.tasks import evaluate_flowchart_task
|
||||
from problem.models import Problem
|
||||
from utils.api import APIView
|
||||
|
||||
|
||||
class FlowchartSubmissionAPI(APIView):
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
#!/usr/bin/env python3
|
||||
import base64
|
||||
import copy
|
||||
import random
|
||||
import string
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import string
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
||||
|
||||
@@ -4,12 +4,12 @@ import logging
|
||||
from urllib.parse import urljoin
|
||||
|
||||
import requests
|
||||
from django.db import transaction, IntegrityError
|
||||
from django.db import IntegrityError, transaction
|
||||
from django.db.models import F
|
||||
|
||||
from account.models import User
|
||||
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 problem.models import Problem, ProblemRuleType
|
||||
from problem.utils import parse_problem_template
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from problem.models import ProblemIOMode
|
||||
|
||||
|
||||
default_env = ["LANG=en_US.UTF-8", "LANGUAGE=en_US:en", "LC_ALL=en_US.UTF-8"]
|
||||
|
||||
_c_lang_config = {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import dramatiq
|
||||
|
||||
from account.models import User
|
||||
from submission.models import Submission
|
||||
from judge.dispatcher import JudgeDispatcher
|
||||
from submission.models import Submission
|
||||
from utils.shortcuts import DRAMATIQ_WORKER_ARGS
|
||||
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ import sys
|
||||
if __name__ == "__main__":
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "oj.settings")
|
||||
|
||||
from django.core.management import execute_from_command_line
|
||||
import django
|
||||
from django.core.management import execute_from_command_line
|
||||
sys.stdout.write("Django VERSION " + str(django.VERSION) + "\n")
|
||||
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from django.db import models
|
||||
|
||||
from account.models import User
|
||||
from submission.models import Submission
|
||||
from utils.models import RichTextField
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from submission.serializers import SubmissionSafeModelSerializer
|
||||
from utils.api import UsernameSerializer, serializers
|
||||
from .models import Message
|
||||
|
||||
from .models import Message
|
||||
|
||||
|
||||
class MessageSerializer(serializers.ModelSerializer):
|
||||
|
||||
@@ -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 message.models import Message
|
||||
from message.serializers import CreateMessageSerializer, MessageSerializer
|
||||
from submission.models import Submission
|
||||
from utils.api import APIView
|
||||
|
||||
from message.models import Message
|
||||
|
||||
from utils.api.api import validate_serializer
|
||||
|
||||
|
||||
|
||||
@@ -8,9 +8,10 @@ https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
from django.core.asgi import get_asgi_application
|
||||
from channels.routing import ProtocolTypeRouter, URLRouter
|
||||
|
||||
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")
|
||||
|
||||
@@ -19,7 +20,7 @@ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "oj.settings")
|
||||
django_asgi_app = get_asgi_application()
|
||||
|
||||
# Import routing after Django setup
|
||||
from oj.routing import websocket_urlpatterns
|
||||
from oj.routing import websocket_urlpatterns # noqa: E402
|
||||
|
||||
application = ProtocolTypeRouter(
|
||||
{
|
||||
|
||||
@@ -3,9 +3,10 @@ WebSocket URL Configuration for oj project.
|
||||
"""
|
||||
|
||||
from django.urls import path
|
||||
from submission.consumers import SubmissionConsumer
|
||||
|
||||
from conf.consumers import ConfigConsumer
|
||||
from flowchart.consumers import FlowchartConsumer
|
||||
from submission.consumers import SubmissionConsumer
|
||||
|
||||
websocket_urlpatterns = [
|
||||
path("ws/submission/", SubmissionConsumer.as_asgi()),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from django.db import models
|
||||
|
||||
from utils.models import JSONField
|
||||
|
||||
|
||||
|
||||
@@ -3,10 +3,11 @@ import os
|
||||
import threading
|
||||
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 utils.shortcuts import rand_str
|
||||
|
||||
from .models import SysOptions as SysOptionsModel
|
||||
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
# Create your tests here.
|
||||
@@ -2,8 +2,8 @@ from django.db import models
|
||||
|
||||
from account.models import User
|
||||
from contest.models import Contest
|
||||
from utils.models import RichTextField
|
||||
from utils.constants import Choices
|
||||
from utils.models import RichTextField
|
||||
|
||||
|
||||
class ProblemTag(models.Model):
|
||||
|
||||
@@ -5,11 +5,11 @@ from django import forms
|
||||
from utils.api import UsernameSerializer, serializers
|
||||
from utils.constants import Difficulty
|
||||
from utils.serializers import (
|
||||
LanguageNameMultiChoiceField,
|
||||
LanguageNameChoiceField,
|
||||
LanguageNameMultiChoiceField,
|
||||
)
|
||||
|
||||
from .models import Problem, ProblemRuleType, ProblemTag, ProblemIOMode
|
||||
from .models import Problem, ProblemIOMode, ProblemRuleType, ProblemTag
|
||||
from .utils import parse_problem_template
|
||||
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
from django.urls import path
|
||||
|
||||
from ..views.admin import (
|
||||
AddContestProblemAPI,
|
||||
ContestProblemAPI,
|
||||
MakeContestProblemPublicAPIView,
|
||||
ProblemAPI,
|
||||
ProblemFlowchartAIGen,
|
||||
ProblemVisibleAPI,
|
||||
StuckProblemsAPI,
|
||||
TestCaseAPI,
|
||||
MakeContestProblemPublicAPIView,
|
||||
AddContestProblemAPI,
|
||||
ProblemVisibleAPI,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
from django.urls import path
|
||||
|
||||
from ..views.oj import (
|
||||
ProblemSolvedPeopleCount,
|
||||
ProblemTagAPI,
|
||||
ProblemAPI,
|
||||
ContestProblemAPI,
|
||||
PickOneAPI,
|
||||
ProblemAPI,
|
||||
ProblemAuthorAPI,
|
||||
ProblemSolvedPeopleCount,
|
||||
ProblemTagAPI,
|
||||
SimilarProblemAPI,
|
||||
)
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import re
|
||||
from functools import lru_cache
|
||||
|
||||
|
||||
TEMPLATE_BASE = """//PREPEND BEGIN
|
||||
{}
|
||||
//PREPEND END
|
||||
|
||||
@@ -7,28 +7,27 @@ import zipfile
|
||||
from wsgiref.util import FileWrapper
|
||||
|
||||
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.db.models import Count
|
||||
|
||||
from account.decorators import problem_permission_required, ensure_created_by, super_admin_required
|
||||
from account.decorators import ensure_created_by, problem_permission_required, super_admin_required
|
||||
from contest.models import Contest, ContestStatus
|
||||
from submission.models import Submission
|
||||
from utils.api import APIView, CSRFExemptAPIView, validate_serializer, APIError
|
||||
from utils.shortcuts import rand_str, natural_sort_key
|
||||
from utils.api import APIError, APIView, CSRFExemptAPIView, validate_serializer
|
||||
from utils.openai import get_ai_client
|
||||
from utils.shortcuts import natural_sort_key, rand_str
|
||||
|
||||
from ..models import Problem, ProblemRuleType, ProblemTag
|
||||
from ..serializers import (
|
||||
AddContestProblemSerializer,
|
||||
ContestProblemMakePublicSerializer,
|
||||
CreateContestProblemSerializer,
|
||||
CreateProblemSerializer,
|
||||
EditProblemSerializer,
|
||||
EditContestProblemSerializer,
|
||||
ProblemAdminSerializer,
|
||||
EditProblemSerializer,
|
||||
ProblemAdminListSerializer,
|
||||
ProblemAdminSerializer,
|
||||
TestCaseUploadForm,
|
||||
ContestProblemMakePublicSerializer,
|
||||
AddContestProblemSerializer,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
from datetime import datetime
|
||||
import random
|
||||
from django.db.models import Q, Count
|
||||
from datetime import datetime
|
||||
|
||||
from django.core.cache import cache
|
||||
from account.models import User
|
||||
from submission.models import Submission, JudgeStatus
|
||||
from utils.api import APIView
|
||||
from django.db.models import Count, Q
|
||||
|
||||
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 ..models import ProblemTag, Problem
|
||||
|
||||
from ..models import Problem, ProblemTag
|
||||
from ..serializers import (
|
||||
ProblemListSerializer,
|
||||
ProblemSafeSerializer,
|
||||
ProblemSerializer,
|
||||
TagSerializer,
|
||||
ProblemSafeSerializer,
|
||||
ProblemListSerializer,
|
||||
)
|
||||
from contest.models import ContestRuleType
|
||||
|
||||
|
||||
class ProblemTagAPI(APIView):
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from django.db import models
|
||||
from django.utils.timezone import now
|
||||
|
||||
from account.models import User
|
||||
from problem.models import Problem
|
||||
from utils.models import RichTextField, JSONField
|
||||
from utils.models import JSONField, RichTextField
|
||||
|
||||
|
||||
class ProblemSet(models.Model):
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from utils.api import UsernameSerializer, serializers
|
||||
|
||||
from .models import (
|
||||
ProblemSet,
|
||||
ProblemSetProblem,
|
||||
ProblemSetBadge,
|
||||
ProblemSetProblem,
|
||||
ProblemSetProgress,
|
||||
UserBadge,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
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__)
|
||||
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
from django.urls import path
|
||||
|
||||
from problemset.views.oj import (
|
||||
ProblemSetAPI,
|
||||
ProblemSetBadgeAPI,
|
||||
ProblemSetDetailAPI,
|
||||
ProblemSetProblemAPI,
|
||||
ProblemSetProgressAPI,
|
||||
ProblemSetUserProgressAPI,
|
||||
UserBadgeAPI,
|
||||
UserProgressAPI,
|
||||
ProblemSetBadgeAPI,
|
||||
ProblemSetUserProgressAPI,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
from django.db.models import Q
|
||||
|
||||
from utils.api import APIView, validate_serializer
|
||||
from account.decorators import super_admin_required, ensure_created_by
|
||||
|
||||
from account.decorators import ensure_created_by, super_admin_required
|
||||
from problem.models import Problem
|
||||
from problemset.models import (
|
||||
ProblemSet,
|
||||
ProblemSetProblem,
|
||||
ProblemSetBadge,
|
||||
ProblemSetProblem,
|
||||
ProblemSetProgress,
|
||||
)
|
||||
from problemset.serializers import (
|
||||
ProblemSetSerializer,
|
||||
ProblemSetListSerializer,
|
||||
CreateProblemSetSerializer,
|
||||
EditProblemSetSerializer,
|
||||
ProblemSetProblemSerializer,
|
||||
AddProblemToSetSerializer,
|
||||
EditProblemInSetSerializer,
|
||||
ProblemSetBadgeSerializer,
|
||||
CreateProblemSetBadgeSerializer,
|
||||
CreateProblemSetSerializer,
|
||||
EditProblemInSetSerializer,
|
||||
EditProblemSetBadgeSerializer,
|
||||
EditProblemSetSerializer,
|
||||
ProblemSetBadgeSerializer,
|
||||
ProblemSetListSerializer,
|
||||
ProblemSetProblemSerializer,
|
||||
ProblemSetProgressSerializer,
|
||||
ProblemSetSerializer,
|
||||
)
|
||||
from problem.models import Problem
|
||||
from utils.api import APIView, validate_serializer
|
||||
|
||||
|
||||
class ProblemSetAdminAPI(APIView):
|
||||
|
||||
@@ -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 utils.api import APIView, validate_serializer
|
||||
|
||||
from account.models import User
|
||||
|
||||
from problem.models import Problem
|
||||
from problemset.models import (
|
||||
ProblemSet,
|
||||
ProblemSetProblem,
|
||||
ProblemSetBadge,
|
||||
ProblemSetProblem,
|
||||
ProblemSetProgress,
|
||||
ProblemSetSubmission,
|
||||
UserBadge,
|
||||
)
|
||||
from problemset.serializers import (
|
||||
ProblemSetSerializer,
|
||||
JoinProblemSetSerializer,
|
||||
ProblemSetBadgeSerializer,
|
||||
ProblemSetListSerializer,
|
||||
ProblemSetProblemSerializer,
|
||||
ProblemSetBadgeSerializer,
|
||||
ProblemSetProgressSerializer,
|
||||
UserBadgeSerializer,
|
||||
JoinProblemSetSerializer,
|
||||
ProblemSetSerializer,
|
||||
UpdateProgressSerializer,
|
||||
UserBadgeSerializer,
|
||||
)
|
||||
|
||||
from submission.models import Submission
|
||||
from problem.models import Problem
|
||||
from utils.api import APIView, validate_serializer
|
||||
|
||||
|
||||
class ProblemSetAPI(APIView):
|
||||
|
||||
@@ -7,7 +7,6 @@ requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"channels>=4.2.0",
|
||||
"channels-redis>=4.2.0",
|
||||
"coverage==6.5.0",
|
||||
"daphne>=4.1.2",
|
||||
"django>=5.2.3",
|
||||
"django-cas-ng==5.0.1",
|
||||
@@ -18,9 +17,6 @@ dependencies = [
|
||||
"dramatiq==1.17.0",
|
||||
"entrypoints==0.4",
|
||||
"envelopes==0.4",
|
||||
"flake8==7.0.0",
|
||||
"flake8-coding==1.3.2",
|
||||
"flake8-quotes==3.3.2",
|
||||
"gunicorn==22.0.0",
|
||||
"jsonfield==3.1.0",
|
||||
"openai>=1.108.1",
|
||||
@@ -33,3 +29,18 @@ dependencies = [
|
||||
"raven==6.10.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"]
|
||||
|
||||
27
run_test.py
27
run_test.py
@@ -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")
|
||||
@@ -3,6 +3,7 @@ WebSocket consumers for submission updates
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
|
||||
from channels.generic.websocket import AsyncWebsocketConsumer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
from django.db import models
|
||||
|
||||
from contest.models import Contest
|
||||
from problem.models import Problem
|
||||
from utils.constants import ContestStatus
|
||||
from utils.models import JSONField
|
||||
from problem.models import Problem
|
||||
from contest.models import Contest
|
||||
|
||||
from utils.shortcuts import rand_str
|
||||
|
||||
|
||||
|
||||
@@ -2,10 +2,11 @@ from django.db import models
|
||||
from django.db.models import F
|
||||
from django.utils import timezone
|
||||
|
||||
from .models import Submission
|
||||
from problemset.models import ProblemSetProgress
|
||||
from utils.api import serializers
|
||||
from utils.serializers import LanguageNameChoiceField
|
||||
from problemset.models import ProblemSetProgress
|
||||
|
||||
from .models import Submission
|
||||
|
||||
|
||||
def bulk_fetch_problemset_progress(user, problem_ids):
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
from django.urls import path
|
||||
|
||||
from ..views.oj import (
|
||||
SubmissionAPI,
|
||||
SubmissionListAPI,
|
||||
ContestSubmissionListAPI,
|
||||
SubmissionAPI,
|
||||
SubmissionExistsAPI,
|
||||
SubmissionListAPI,
|
||||
SubmissionsTodayCount,
|
||||
)
|
||||
|
||||
|
||||
@@ -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 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):
|
||||
if class_name and username.startswith("ks"):
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from datetime import datetime
|
||||
import ipaddress
|
||||
from datetime import datetime
|
||||
|
||||
from account.decorators import login_required, check_contest_permission
|
||||
from contest.models import ContestStatus, ContestRuleType
|
||||
from account.decorators import check_contest_permission, login_required
|
||||
from contest.models import ContestRuleType, ContestStatus
|
||||
from judge.tasks import judge_task
|
||||
from options.options import SysOptions
|
||||
|
||||
@@ -12,13 +12,16 @@ from utils.api import APIView, validate_serializer
|
||||
from utils.cache import cache
|
||||
from utils.captcha import Captcha
|
||||
from utils.throttling import TokenBucket
|
||||
|
||||
from ..models import Submission
|
||||
from ..serializers import (
|
||||
CreateSubmissionSerializer,
|
||||
SubmissionModelSerializer,
|
||||
ShareSubmissionSerializer,
|
||||
SubmissionListSerializer,
|
||||
SubmissionModelSerializer,
|
||||
SubmissionSafeModelSerializer,
|
||||
bulk_fetch_problemset_progress,
|
||||
)
|
||||
from ..serializers import SubmissionSafeModelSerializer, SubmissionListSerializer, bulk_fetch_problemset_progress
|
||||
|
||||
|
||||
class SubmissionAPI(APIView):
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from django.db import models
|
||||
|
||||
from account.models import User
|
||||
|
||||
|
||||
class Tutorial(models.Model):
|
||||
TYPE_CHOICES = [
|
||||
('python', 'Python'),
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
from rest_framework import serializers
|
||||
from .models import Tutorial, Exercise
|
||||
|
||||
from account.serializers import UserSerializer
|
||||
|
||||
from .models import Exercise, Tutorial
|
||||
|
||||
|
||||
class TutorialListSerializer(serializers.ModelSerializer):
|
||||
created_by = UserSerializer(read_only=True)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from django.urls import path
|
||||
from ..views.admin import TutorialAdminAPI, TutorialVisibilityAPI, ExerciseAdminAPI
|
||||
|
||||
from ..views.admin import ExerciseAdminAPI, TutorialAdminAPI, TutorialVisibilityAPI
|
||||
|
||||
urlpatterns = [
|
||||
path("tutorial", TutorialAdminAPI.as_view()),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from django.urls import path
|
||||
from ..views.oj import TutorialAPI, TutorialTitlesAPI, ExerciseAPI
|
||||
|
||||
from ..views.oj import ExerciseAPI, TutorialAPI, TutorialTitlesAPI
|
||||
|
||||
urlpatterns = [
|
||||
path("tutorial", TutorialAPI.as_view()),
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
from account.decorators import super_admin_required
|
||||
from utils.api import APIView, validate_serializer
|
||||
|
||||
from tutorial.models import Tutorial, Exercise
|
||||
from tutorial.models import Exercise, Tutorial
|
||||
from tutorial.serializers import (
|
||||
TutorialSerializer,
|
||||
TutorialListSerializer,
|
||||
CreateExerciseSerializer,
|
||||
CreateTutorialSerializer,
|
||||
EditExerciseSerializer,
|
||||
EditTutorialSerializer,
|
||||
ExerciseSerializer,
|
||||
CreateExerciseSerializer,
|
||||
EditExerciseSerializer,
|
||||
TutorialListSerializer,
|
||||
TutorialSerializer,
|
||||
)
|
||||
from utils.api import APIView, validate_serializer
|
||||
|
||||
|
||||
class TutorialAdminAPI(APIView):
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
from tutorial.models import Exercise, Tutorial
|
||||
from tutorial.serializers import ExerciseSerializer, TutorialSerializer
|
||||
from utils.api import APIView
|
||||
|
||||
from tutorial.models import Tutorial, Exercise
|
||||
from tutorial.serializers import TutorialSerializer, ExerciseSerializer
|
||||
|
||||
|
||||
class TutorialAPI(APIView):
|
||||
def get(self, request):
|
||||
|
||||
@@ -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)
|
||||
@@ -1,7 +1,7 @@
|
||||
import json
|
||||
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.client.default import DefaultClient
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ limitations under the License.
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
import random
|
||||
import time
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from . import Captcha
|
||||
from ..api import APIView
|
||||
from ..shortcuts import img2base64
|
||||
from . import Captcha
|
||||
|
||||
|
||||
class CaptchaAPIView(APIView):
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import os
|
||||
import re
|
||||
import datetime
|
||||
import random
|
||||
import re
|
||||
from base64 import b64encode
|
||||
from io import BytesIO
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import os
|
||||
|
||||
import dramatiq
|
||||
|
||||
from utils.shortcuts import DRAMATIQ_WORKER_ARGS
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import SimditorImageUploadAPIView, SimditorFileUploadAPIView
|
||||
from .views import SimditorFileUploadAPIView, SimditorImageUploadAPIView
|
||||
|
||||
urlpatterns = [
|
||||
path("upload_image", SimditorImageUploadAPIView.as_view()),
|
||||
|
||||
@@ -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 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__)
|
||||
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
WebSocket utility functions for pushing real-time updates
|
||||
"""
|
||||
import logging
|
||||
from channels.layers import get_channel_layer
|
||||
|
||||
from asgiref.sync import async_to_sync
|
||||
from channels.layers import get_channel_layer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -25,8 +25,8 @@ Python 2.6+ or 3.2+
|
||||
Cannot defense xss in browser which is belowed IE7
|
||||
浏览器版本:IE7+ 或其他浏览器,无法防御IE6及以下版本浏览器中的XSS
|
||||
"""
|
||||
import re
|
||||
import copy
|
||||
import re
|
||||
from html.parser import HTMLParser
|
||||
|
||||
|
||||
|
||||
109
uv.lock
generated
109
uv.lock
generated
@@ -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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "cryptography"
|
||||
version = "46.0.3"
|
||||
@@ -466,41 +460,6 @@ version = "0.4"
|
||||
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" }
|
||||
|
||||
[[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]]
|
||||
name = "gunicorn"
|
||||
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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "msgpack"
|
||||
version = "1.1.2"
|
||||
@@ -803,7 +753,6 @@ source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "channels" },
|
||||
{ name = "channels-redis" },
|
||||
{ name = "coverage" },
|
||||
{ name = "daphne" },
|
||||
{ name = "django" },
|
||||
{ name = "django-cas-ng" },
|
||||
@@ -814,9 +763,6 @@ dependencies = [
|
||||
{ name = "dramatiq" },
|
||||
{ name = "entrypoints" },
|
||||
{ name = "envelopes" },
|
||||
{ name = "flake8" },
|
||||
{ name = "flake8-coding" },
|
||||
{ name = "flake8-quotes" },
|
||||
{ name = "gunicorn" },
|
||||
{ name = "jsonfield" },
|
||||
{ name = "openai" },
|
||||
@@ -830,11 +776,15 @@ dependencies = [
|
||||
{ name = "xlsxwriter" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "ruff" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "channels", specifier = ">=4.2.0" },
|
||||
{ name = "channels-redis", specifier = ">=4.2.0" },
|
||||
{ name = "coverage", specifier = "==6.5.0" },
|
||||
{ name = "daphne", specifier = ">=4.1.2" },
|
||||
{ name = "django", specifier = ">=5.2.3" },
|
||||
{ name = "django-cas-ng", specifier = "==5.0.1" },
|
||||
@@ -845,9 +795,6 @@ requires-dist = [
|
||||
{ name = "dramatiq", specifier = "==1.17.0" },
|
||||
{ name = "entrypoints", 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 = "jsonfield", specifier = "==3.1.0" },
|
||||
{ name = "openai", specifier = ">=1.108.1" },
|
||||
@@ -861,6 +808,9 @@ requires-dist = [
|
||||
{ name = "xlsxwriter", specifier = "==3.2.0" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [{ name = "ruff", specifier = ">=0.15.11" }]
|
||||
|
||||
[[package]]
|
||||
name = "openai"
|
||||
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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "pycparser"
|
||||
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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "pyopenssl"
|
||||
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" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "service-identity"
|
||||
version = "24.2.0"
|
||||
|
||||
Reference in New Issue
Block a user