diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 0fc0318..0000000 --- a/.flake8 +++ /dev/null @@ -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 diff --git a/account/decorators.py b/account/decorators.py index 0b6f236..7102b17 100644 --- a/account/decorators.py +++ b/account/decorators.py @@ -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 diff --git a/account/middleware.py b/account/middleware.py index 91eed27..3fc218c 100644 --- a/account/middleware.py +++ b/account/middleware.py @@ -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): diff --git a/account/models.py b/account/models.py index 7a2c8eb..50ed357 100644 --- a/account/models.py +++ b/account/models.py @@ -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 diff --git a/account/serializers.py b/account/serializers.py index 228c6ca..2c8b513 100644 --- a/account/serializers.py +++ b/account/serializers.py @@ -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 diff --git a/account/tasks.py b/account/tasks.py index 5135999..9f8f25c 100644 --- a/account/tasks.py +++ b/account/tasks.py @@ -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__) diff --git a/account/tests.py b/account/tests.py deleted file mode 100644 index c727f03..0000000 --- a/account/tests.py +++ /dev/null @@ -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) diff --git a/account/urls/admin.py b/account/urls/admin.py index 7390b4a..34ed640 100644 --- a/account/urls/admin.py +++ b/account/urls/admin.py @@ -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()), diff --git a/account/urls/oj.py b/account/urls/oj.py index 61d9db2..b5f6ac4 100644 --- a/account/urls/oj.py +++ b/account/urls/oj.py @@ -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()), diff --git a/account/views/admin.py b/account/views/admin.py index d0e666b..95140d4 100644 --- a/account/views/admin.py +++ b/account/views/admin.py @@ -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 diff --git a/account/views/oj.py b/account/views/oj.py index d53a9f9..dbe3727 100644 --- a/account/views/oj.py +++ b/account/views/oj.py @@ -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 diff --git a/ai/views/oj.py b/ai/views/oj.py index 60f7666..a3fae6a 100644 --- a/ai/views/oj.py +++ b/ai/views/oj.py @@ -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): diff --git a/announcement/tests.py b/announcement/tests.py deleted file mode 100644 index 98caa1c..0000000 --- a/announcement/tests.py +++ /dev/null @@ -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) diff --git a/announcement/views/admin.py b/announcement/views/admin.py index 938ac3c..32bbee6 100644 --- a/announcement/views/admin.py +++ b/announcement/views/admin.py @@ -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): diff --git a/announcement/views/oj.py b/announcement/views/oj.py index e70c0dd..a987c90 100644 --- a/announcement/views/oj.py +++ b/announcement/views/oj.py @@ -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): diff --git a/class_pk/admin.py b/class_pk/admin.py index ea5d68b..c3779ed 100644 --- a/class_pk/admin.py +++ b/class_pk/admin.py @@ -1,3 +1,2 @@ -from django.contrib import admin # Register your models here. diff --git a/class_pk/models.py b/class_pk/models.py index fcd29fa..5be8fa6 100644 --- a/class_pk/models.py +++ b/class_pk/models.py @@ -1,4 +1,3 @@ -from django.db import models # 如果需要存储班级PK历史记录,可以在这里定义模型 # 目前暂时不需要,因为都是实时计算 diff --git a/class_pk/urls/oj.py b/class_pk/urls/oj.py index a0410c3..e0de529 100644 --- a/class_pk/urls/oj.py +++ b/class_pk/urls/oj.py @@ -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()), diff --git a/class_pk/views/oj.py b/class_pk/views/oj.py index de12117..4f9114e 100644 --- a/class_pk/views/oj.py +++ b/class_pk/views/oj.py @@ -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): diff --git a/comment/urls/admin.py b/comment/urls/admin.py index c0716e9..34139ba 100644 --- a/comment/urls/admin.py +++ b/comment/urls/admin.py @@ -2,7 +2,6 @@ from django.urls import path from ..views.admin import CommentAPI - urlpatterns = [ path("comment", CommentAPI.as_view()), ] diff --git a/comment/urls/oj.py b/comment/urls/oj.py index c30631f..0282308 100644 --- a/comment/urls/oj.py +++ b/comment/urls/oj.py @@ -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()), diff --git a/comment/views/admin.py b/comment/views/admin.py index 23d8249..74806c1 100644 --- a/comment/views/admin.py +++ b/comment/views/admin.py @@ -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): diff --git a/comment/views/oj.py b/comment/views/oj.py index deb968c..f361194 100644 --- a/comment/views/oj.py +++ b/comment/views/oj.py @@ -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): diff --git a/conf/consumers.py b/conf/consumers.py index 2e14406..602ab16 100644 --- a/conf/consumers.py +++ b/conf/consumers.py @@ -3,6 +3,7 @@ WebSocket consumers for configuration updates """ import json import logging + from channels.generic.websocket import AsyncWebsocketConsumer logger = logging.getLogger(__name__) diff --git a/conf/tests.py b/conf/tests.py deleted file mode 100644 index fbb4022..0000000 --- a/conf/tests.py +++ /dev/null @@ -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": "test", - "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": "", - "allow_register": True, "submission_list_show_all": False} - resp = self.client.post(url, data=data) - self.assertSuccess(resp) - self.assertEqual(SysOptions.website_footer, '') - - 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) diff --git a/conf/urls/admin.py b/conf/urls/admin.py index f33c976..ab268e6 100644 --- a/conf/urls/admin.py +++ b/conf/urls/admin.py @@ -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 = [ diff --git a/conf/urls/oj.py b/conf/urls/oj.py index 73a85cf..c3438c8 100644 --- a/conf/urls/oj.py +++ b/conf/urls/oj.py @@ -1,11 +1,11 @@ from django.urls import path from ..views import ( + ClassUsernamesAPI, HitokotoAPI, JudgeServerHeartbeatAPI, LanguagesAPI, WebsiteConfigAPI, - ClassUsernamesAPI, ) urlpatterns = [ diff --git a/conf/views.py b/conf/views.py index 42c63b2..abe268d 100644 --- a/conf/views.py +++ b/conf/views.py @@ -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, ) diff --git a/contest/serializers.py b/contest/serializers.py index bdca7ed..f96653e 100644 --- a/contest/serializers.py +++ b/contest/serializers.py @@ -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): diff --git a/contest/tests.py b/contest/tests.py deleted file mode 100644 index a92e09a..0000000 --- a/contest/tests.py +++ /dev/null @@ -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) diff --git a/contest/urls/admin.py b/contest/urls/admin.py index 20ae6cb..d83c772 100644 --- a/contest/urls/admin.py +++ b/contest/urls/admin.py @@ -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()), diff --git a/contest/urls/oj.py b/contest/urls/oj.py index 7235f4f..7377019 100644 --- a/contest/urls/oj.py +++ b/contest/urls/oj.py @@ -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()), diff --git a/contest/views/admin.py b/contest/views/admin.py index 6cdf49a..e2e4989 100644 --- a/contest/views/admin.py +++ b/contest/views/admin.py @@ -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, ) diff --git a/contest/views/oj.py b/contest/views/oj.py index d388f2a..0cfa551 100644 --- a/contest/views/oj.py +++ b/contest/views/oj.py @@ -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): diff --git a/dev.py b/dev.py index 0afcca9..fc58265 100644 --- a/dev.py +++ b/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(): diff --git a/flowchart/consumers.py b/flowchart/consumers.py index 330e511..315197f 100644 --- a/flowchart/consumers.py +++ b/flowchart/consumers.py @@ -3,6 +3,7 @@ WebSocket consumers for flowchart evaluation updates """ import json import logging + from channels.generic.websocket import AsyncWebsocketConsumer logger = logging.getLogger(__name__) diff --git a/flowchart/models.py b/flowchart/models.py index 81ce65e..514d47d 100644 --- a/flowchart/models.py +++ b/flowchart/models.py @@ -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() diff --git a/flowchart/serializers.py b/flowchart/serializers.py index e240d75..96ee9b6 100644 --- a/flowchart/serializers.py +++ b/flowchart/serializers.py @@ -1,4 +1,5 @@ from rest_framework import serializers + from .models import FlowchartSubmission diff --git a/flowchart/tasks.py b/flowchart/tasks.py index 8a3939e..09ad4eb 100644 --- a/flowchart/tasks.py +++ b/flowchart/tasks.py @@ -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评分任务""" diff --git a/flowchart/urls/oj.py b/flowchart/urls/oj.py index 2148a0e..c77b9e5 100644 --- a/flowchart/urls/oj.py +++ b/flowchart/urls/oj.py @@ -1,10 +1,11 @@ from django.urls import path + from ..views.oj import ( FlowchartSubmissionAPI, - FlowchartSubmissionListAPI, - FlowchartSubmissionRetryAPI, FlowchartSubmissionCurrentAPI, FlowchartSubmissionDetailAPI, + FlowchartSubmissionListAPI, + FlowchartSubmissionRetryAPI, ) urlpatterns = [ diff --git a/flowchart/views.py b/flowchart/views.py index 91ea44a..b8e4ee0 100644 --- a/flowchart/views.py +++ b/flowchart/views.py @@ -1,3 +1,2 @@ -from django.shortcuts import render # Create your views here. diff --git a/flowchart/views/oj.py b/flowchart/views/oj.py index 9100c3c..c8e0566 100644 --- a/flowchart/views/oj.py +++ b/flowchart/views/oj.py @@ -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): diff --git a/fps/parser.py b/fps/parser.py index 63d3559..02cb129 100644 --- a/fps/parser.py +++ b/fps/parser.py @@ -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 diff --git a/judge/dispatcher.py b/judge/dispatcher.py index db15832..312ae3b 100644 --- a/judge/dispatcher.py +++ b/judge/dispatcher.py @@ -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 diff --git a/judge/languages.py b/judge/languages.py index 0324b13..9627d0a 100644 --- a/judge/languages.py +++ b/judge/languages.py @@ -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 = { diff --git a/judge/tasks.py b/judge/tasks.py index 8a1794a..bd13305 100644 --- a/judge/tasks.py +++ b/judge/tasks.py @@ -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 diff --git a/manage.py b/manage.py index 16b46a5..46a2483 100755 --- a/manage.py +++ b/manage.py @@ -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) diff --git a/message/models.py b/message/models.py index 7f90350..a93b0ae 100644 --- a/message/models.py +++ b/message/models.py @@ -1,4 +1,5 @@ from django.db import models + from account.models import User from submission.models import Submission from utils.models import RichTextField diff --git a/message/serializers.py b/message/serializers.py index 93b8bc2..c972c6c 100644 --- a/message/serializers.py +++ b/message/serializers.py @@ -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): diff --git a/message/views/oj.py b/message/views/oj.py index 72e6711..e3d200d 100644 --- a/message/views/oj.py +++ b/message/views/oj.py @@ -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 diff --git a/oj/asgi.py b/oj/asgi.py index 69bc4f5..2c221fc 100644 --- a/oj/asgi.py +++ b/oj/asgi.py @@ -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( { diff --git a/oj/routing.py b/oj/routing.py index 96cefd6..466ac0c 100644 --- a/oj/routing.py +++ b/oj/routing.py @@ -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()), diff --git a/options/models.py b/options/models.py index 30db2f6..0adc46f 100644 --- a/options/models.py +++ b/options/models.py @@ -1,4 +1,5 @@ from django.db import models + from utils.models import JSONField diff --git a/options/options.py b/options/options.py index e5373ef..b3c5a05 100644 --- a/options/options.py +++ b/options/options.py @@ -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 diff --git a/options/tests.py b/options/tests.py deleted file mode 100644 index a39b155..0000000 --- a/options/tests.py +++ /dev/null @@ -1 +0,0 @@ -# Create your tests here. diff --git a/problem/models.py b/problem/models.py index bd1e3dd..f32b336 100644 --- a/problem/models.py +++ b/problem/models.py @@ -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): diff --git a/problem/serializers.py b/problem/serializers.py index 3a70c00..38073c8 100644 --- a/problem/serializers.py +++ b/problem/serializers.py @@ -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 diff --git a/problem/urls/admin.py b/problem/urls/admin.py index d7953a7..20e735d 100644 --- a/problem/urls/admin.py +++ b/problem/urls/admin.py @@ -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 = [ diff --git a/problem/urls/oj.py b/problem/urls/oj.py index b91d861..7e11893 100644 --- a/problem/urls/oj.py +++ b/problem/urls/oj.py @@ -1,12 +1,12 @@ from django.urls import path from ..views.oj import ( - ProblemSolvedPeopleCount, - ProblemTagAPI, - ProblemAPI, ContestProblemAPI, PickOneAPI, + ProblemAPI, ProblemAuthorAPI, + ProblemSolvedPeopleCount, + ProblemTagAPI, SimilarProblemAPI, ) diff --git a/problem/utils.py b/problem/utils.py index c530263..8769e8c 100644 --- a/problem/utils.py +++ b/problem/utils.py @@ -1,7 +1,6 @@ import re from functools import lru_cache - TEMPLATE_BASE = """//PREPEND BEGIN {} //PREPEND END diff --git a/problem/views/admin.py b/problem/views/admin.py index 17b3d74..e71c201 100644 --- a/problem/views/admin.py +++ b/problem/views/admin.py @@ -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, ) diff --git a/problem/views/oj.py b/problem/views/oj.py index 2da9452..02d409d 100644 --- a/problem/views/oj.py +++ b/problem/views/oj.py @@ -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): diff --git a/problemset/models.py b/problemset/models.py index e235700..e0d5c1d 100644 --- a/problemset/models.py +++ b/problemset/models.py @@ -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): diff --git a/problemset/serializers.py b/problemset/serializers.py index 526c6bb..3adab99 100644 --- a/problemset/serializers.py +++ b/problemset/serializers.py @@ -1,8 +1,9 @@ from utils.api import UsernameSerializer, serializers + from .models import ( ProblemSet, - ProblemSetProblem, ProblemSetBadge, + ProblemSetProblem, ProblemSetProgress, UserBadge, ) diff --git a/problemset/signals.py b/problemset/signals.py index 265fe82..fe46746 100644 --- a/problemset/signals.py +++ b/problemset/signals.py @@ -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__) diff --git a/problemset/urls/oj.py b/problemset/urls/oj.py index 3fa8e4d..0f20df8 100644 --- a/problemset/urls/oj.py +++ b/problemset/urls/oj.py @@ -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 = [ diff --git a/problemset/views/admin.py b/problemset/views/admin.py index fc09d31..d36e6ac 100644 --- a/problemset/views/admin.py +++ b/problemset/views/admin.py @@ -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): diff --git a/problemset/views/oj.py b/problemset/views/oj.py index eb4194b..8bfb7bc 100644 --- a/problemset/views/oj.py +++ b/problemset/views/oj.py @@ -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): diff --git a/pyproject.toml b/pyproject.toml index 1eb6029..aa24de0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"] diff --git a/run_test.py b/run_test.py deleted file mode 100644 index a6c714f..0000000 --- a/run_test.py +++ /dev/null @@ -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") diff --git a/submission/consumers.py b/submission/consumers.py index b496432..2dadcee 100644 --- a/submission/consumers.py +++ b/submission/consumers.py @@ -3,6 +3,7 @@ WebSocket consumers for submission updates """ import json import logging + from channels.generic.websocket import AsyncWebsocketConsumer logger = logging.getLogger(__name__) diff --git a/submission/models.py b/submission/models.py index c7ca45d..05dbf13 100644 --- a/submission/models.py +++ b/submission/models.py @@ -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 diff --git a/submission/serializers.py b/submission/serializers.py index 1f929d2..3e27db0 100644 --- a/submission/serializers.py +++ b/submission/serializers.py @@ -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): diff --git a/submission/urls/oj.py b/submission/urls/oj.py index cafc4d0..c0b03ca 100644 --- a/submission/urls/oj.py +++ b/submission/urls/oj.py @@ -1,10 +1,10 @@ from django.urls import path from ..views.oj import ( - SubmissionAPI, - SubmissionListAPI, ContestSubmissionListAPI, + SubmissionAPI, SubmissionExistsAPI, + SubmissionListAPI, SubmissionsTodayCount, ) diff --git a/submission/views/admin.py b/submission/views/admin.py index 7371604..b0b36d2 100644 --- a/submission/views/admin.py +++ b/submission/views/admin.py @@ -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"): diff --git a/submission/views/oj.py b/submission/views/oj.py index e72a72b..ee799cb 100644 --- a/submission/views/oj.py +++ b/submission/views/oj.py @@ -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): diff --git a/tutorial/models.py b/tutorial/models.py index baff521..350dc73 100644 --- a/tutorial/models.py +++ b/tutorial/models.py @@ -1,6 +1,8 @@ from django.db import models + from account.models import User + class Tutorial(models.Model): TYPE_CHOICES = [ ('python', 'Python'), diff --git a/tutorial/serializers.py b/tutorial/serializers.py index 0c5ef3a..8e0ef00 100644 --- a/tutorial/serializers.py +++ b/tutorial/serializers.py @@ -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) diff --git a/tutorial/urls/admin.py b/tutorial/urls/admin.py index e035689..d4221b7 100644 --- a/tutorial/urls/admin.py +++ b/tutorial/urls/admin.py @@ -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()), diff --git a/tutorial/urls/tutorial.py b/tutorial/urls/tutorial.py index 78324f7..4a2919c 100644 --- a/tutorial/urls/tutorial.py +++ b/tutorial/urls/tutorial.py @@ -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()), diff --git a/tutorial/views/admin.py b/tutorial/views/admin.py index 63eb799..72b135c 100644 --- a/tutorial/views/admin.py +++ b/tutorial/views/admin.py @@ -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): diff --git a/tutorial/views/oj.py b/tutorial/views/oj.py index 176611f..b02d4cb 100644 --- a/tutorial/views/oj.py +++ b/tutorial/views/oj.py @@ -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): diff --git a/utils/api/tests.py b/utils/api/tests.py deleted file mode 100644 index 0f0a79b..0000000 --- a/utils/api/tests.py +++ /dev/null @@ -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) diff --git a/utils/cache.py b/utils/cache.py index ca4ca13..3d5e74f 100644 --- a/utils/cache.py +++ b/utils/cache.py @@ -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 diff --git a/utils/captcha/__init__.py b/utils/captcha/__init__.py index 8b8375c..124404b 100644 --- a/utils/captcha/__init__.py +++ b/utils/captcha/__init__.py @@ -12,8 +12,8 @@ limitations under the License. """ import os -import time import random +import time from PIL import Image, ImageDraw, ImageFont diff --git a/utils/captcha/views.py b/utils/captcha/views.py index 4642e32..4981db0 100644 --- a/utils/captcha/views.py +++ b/utils/captcha/views.py @@ -1,6 +1,6 @@ -from . import Captcha from ..api import APIView from ..shortcuts import img2base64 +from . import Captcha class CaptchaAPIView(APIView): diff --git a/utils/shortcuts.py b/utils/shortcuts.py index e21e406..0d6e888 100644 --- a/utils/shortcuts.py +++ b/utils/shortcuts.py @@ -1,7 +1,6 @@ import os -import re -import datetime import random +import re from base64 import b64encode from io import BytesIO diff --git a/utils/tasks.py b/utils/tasks.py index 26a2180..d431157 100644 --- a/utils/tasks.py +++ b/utils/tasks.py @@ -1,4 +1,5 @@ import os + import dramatiq from utils.shortcuts import DRAMATIQ_WORKER_ARGS diff --git a/utils/urls.py b/utils/urls.py index 35d955e..5668741 100644 --- a/utils/urls.py +++ b/utils/urls.py @@ -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()), diff --git a/utils/views.py b/utils/views.py index ce30c71..17de959 100644 --- a/utils/views.py +++ b/utils/views.py @@ -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__) diff --git a/utils/websocket.py b/utils/websocket.py index c3ce299..b049be2 100644 --- a/utils/websocket.py +++ b/utils/websocket.py @@ -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__) diff --git a/utils/xss_filter.py b/utils/xss_filter.py index fe4f7aa..7e4a4b0 100644 --- a/utils/xss_filter.py +++ b/utils/xss_filter.py @@ -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 diff --git a/uv.lock b/uv.lock index b3f5ada..f4f97fa 100644 --- a/uv.lock +++ b/uv.lock @@ -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"