添加最近提交

This commit is contained in:
2025-04-13 16:48:40 +08:00
parent 8d6e9ff1f9
commit 47cf28eb67
2 changed files with 125 additions and 43 deletions

View File

@@ -1,11 +1,26 @@
from django.urls import path from django.urls import path
from ..views.oj import (ApplyResetPasswordAPI, ResetPasswordAPI, from ..views.oj import (
UserChangePasswordAPI, UserRegisterAPI, UserChangeEmailAPI, ApplyResetPasswordAPI,
UserLoginAPI, UserLogoutAPI, UsernameOrEmailCheck, ResetPasswordAPI,
AvatarUploadAPI, TwoFactorAuthAPI, UserProfileAPI, UserChangePasswordAPI,
UserRankAPI, UserActivityRankAPI, CheckTFARequiredAPI, SessionManagementAPI, UserProfileAnalyse,
ProfileProblemDisplayIDRefreshAPI, OpenAPIAppkeyAPI, SSOAPI) UserRegisterAPI,
UserChangeEmailAPI,
UserLoginAPI,
UserLogoutAPI,
UsernameOrEmailCheck,
AvatarUploadAPI,
TwoFactorAuthAPI,
UserProfileAPI,
UserRankAPI,
UserActivityRankAPI,
CheckTFARequiredAPI,
SessionManagementAPI,
ProfileProblemDisplayIDRefreshAPI,
OpenAPIAppkeyAPI,
SSOAPI,
)
from utils.captcha.views import CaptchaAPIView from utils.captcha.views import CaptchaAPIView
@@ -20,13 +35,20 @@ urlpatterns = [
path("captcha", CaptchaAPIView.as_view()), path("captcha", CaptchaAPIView.as_view()),
path("check_username_or_email", UsernameOrEmailCheck.as_view()), path("check_username_or_email", UsernameOrEmailCheck.as_view()),
path("profile", UserProfileAPI.as_view(), name="user_profile_api"), path("profile", UserProfileAPI.as_view(), name="user_profile_api"),
path("profile/analyse", UserProfileAnalyse.as_view()),
path("profile/fresh_display_id", ProfileProblemDisplayIDRefreshAPI.as_view()), path("profile/fresh_display_id", ProfileProblemDisplayIDRefreshAPI.as_view()),
path("upload_avatar", AvatarUploadAPI.as_view()), path("upload_avatar", AvatarUploadAPI.as_view()),
path("tfa_required", CheckTFARequiredAPI.as_view()), path("tfa_required", CheckTFARequiredAPI.as_view()),
path("two_factor_auth", TwoFactorAuthAPI.as_view(),), path(
"two_factor_auth",
TwoFactorAuthAPI.as_view(),
),
path("user_rank", UserRankAPI.as_view()), path("user_rank", UserRankAPI.as_view()),
path("user_activity_rank", UserActivityRankAPI.as_view()), path("user_activity_rank", UserActivityRankAPI.as_view()),
path("sessions", SessionManagementAPI.as_view()), path("sessions", SessionManagementAPI.as_view()),
path("open_api_appkey", OpenAPIAppkeyAPI.as_view(),), path(
path("sso", SSOAPI.as_view()) "open_api_appkey",
OpenAPIAppkeyAPI.as_view(),
),
path("sso", SSOAPI.as_view()),
] ]

View File

@@ -2,7 +2,6 @@ import os
from datetime import timedelta from datetime import timedelta
from importlib import import_module from importlib import import_module
import qrcode
from django.conf import settings from django.conf import settings
from django.contrib import auth from django.contrib import auth
from django.template.loader import render_to_string from django.template.loader import render_to_string
@@ -10,6 +9,9 @@ from django.utils.decorators import method_decorator
from django.utils.timezone import now from django.utils.timezone import now
from django.views.decorators.csrf import ensure_csrf_cookie, csrf_exempt from django.views.decorators.csrf import ensure_csrf_cookie, csrf_exempt
from django.db.models import Count from django.db.models import Count
from django.utils import timezone
import qrcode
from otpauth import OtpAuth from otpauth import OtpAuth
from problem.models import Problem from problem.models import Problem
@@ -21,12 +23,23 @@ from utils.captcha import Captcha
from utils.shortcuts import rand_str, img2base64, datetime2str from utils.shortcuts import rand_str, img2base64, datetime2str
from ..decorators import login_required from ..decorators import login_required
from ..models import User, UserProfile, AdminType from ..models import User, UserProfile, AdminType
from ..serializers import (ApplyResetPasswordSerializer, ResetPasswordSerializer, from ..serializers import (
UserChangePasswordSerializer, UserLoginSerializer, ApplyResetPasswordSerializer,
UserRegisterSerializer, UsernameOrEmailCheckSerializer, ResetPasswordSerializer,
RankInfoSerializer, UserChangeEmailSerializer, SSOSerializer) UserChangePasswordSerializer,
from ..serializers import (TwoFactorAuthCodeSerializer, UserProfileSerializer, UserLoginSerializer,
EditUserProfileSerializer, ImageUploadForm) UserRegisterSerializer,
UsernameOrEmailCheckSerializer,
RankInfoSerializer,
UserChangeEmailSerializer,
SSOSerializer,
)
from ..serializers import (
TwoFactorAuthCodeSerializer,
UserProfileSerializer,
EditUserProfileSerializer,
ImageUploadForm,
)
from ..tasks import send_email_async from ..tasks import send_email_async
@@ -50,7 +63,9 @@ class UserProfileAPI(APIView):
show_real_name = True show_real_name = True
except User.DoesNotExist: except User.DoesNotExist:
return self.error("User does not exist") return self.error("User does not exist")
return self.success(UserProfileSerializer(user.userprofile, show_real_name=show_real_name).data) return self.success(
UserProfileSerializer(user.userprofile, show_real_name=show_real_name).data
)
@validate_serializer(EditUserProfileSerializer) @validate_serializer(EditUserProfileSerializer)
@login_required @login_required
@@ -60,7 +75,19 @@ class UserProfileAPI(APIView):
for k, v in data.items(): for k, v in data.items():
setattr(user_profile, k, v) setattr(user_profile, k, v)
user_profile.save() user_profile.save()
return self.success(UserProfileSerializer(user_profile, show_real_name=True).data) return self.success(
UserProfileSerializer(user_profile, show_real_name=True).data
)
class UserProfileAnalyse(APIView):
def get(self, request):
userid = request.GET.get("userid")
submission = Submission.objects.filter(user_id=userid).latest("create_time")
if not submission:
return self.error("暂无提交")
else:
return self.success({"now": timezone.now(), "last": submission.create_time})
class AvatarUploadAPI(APIView): class AvatarUploadAPI(APIView):
@@ -104,7 +131,11 @@ class TwoFactorAuthAPI(APIView):
user.save() user.save()
label = f"{SysOptions.website_name_shortcut}:{user.username}" label = f"{SysOptions.website_name_shortcut}:{user.username}"
image = qrcode.make(OtpAuth(token).to_uri("totp", label, SysOptions.website_name.replace(" ", ""))) image = qrcode.make(
OtpAuth(token).to_uri(
"totp", label, SysOptions.website_name.replace(" ", "")
)
)
return self.success(img2base64(image)) return self.success(img2base64(image))
@login_required @login_required
@@ -197,12 +228,11 @@ class UsernameOrEmailCheck(APIView):
""" """
data = request.data data = request.data
# True means already exist. # True means already exist.
result = { result = {"username": False, "email": False}
"username": False,
"email": False
}
if data.get("username"): if data.get("username"):
result["username"] = User.objects.filter(username=data["username"].lower()).exists() result["username"] = User.objects.filter(
username=data["username"].lower()
).exists()
if data.get("email"): if data.get("email"):
result["email"] = User.objects.filter(email=data["email"].lower()).exists() result["email"] = User.objects.filter(email=data["email"].lower()).exists()
return self.success(result) return self.success(result)
@@ -239,7 +269,9 @@ class UserChangeEmailAPI(APIView):
@login_required @login_required
def post(self, request): def post(self, request):
data = request.data data = request.data
user = auth.authenticate(username=request.user.username, password=data["password"]) user = auth.authenticate(
username=request.user.username, password=data["password"]
)
if user: if user:
if user.two_factor_auth: if user.two_factor_auth:
if "tfa_code" not in data: if "tfa_code" not in data:
@@ -292,8 +324,12 @@ class ApplyResetPasswordAPI(APIView):
user = User.objects.get(email__iexact=data["email"]) user = User.objects.get(email__iexact=data["email"])
except User.DoesNotExist: except User.DoesNotExist:
return self.error("User does not exist") return self.error("User does not exist")
if user.reset_password_token_expire_time and 0 < int( if (
(user.reset_password_token_expire_time - now()).total_seconds()) < 20 * 60: user.reset_password_token_expire_time
and 0
< int((user.reset_password_token_expire_time - now()).total_seconds())
< 20 * 60
):
return self.error("You can only reset password once per 20 minutes") return self.error("You can only reset password once per 20 minutes")
user.reset_password_token = rand_str() user.reset_password_token = rand_str()
user.reset_password_token_expire_time = now() + timedelta(minutes=20) user.reset_password_token_expire_time = now() + timedelta(minutes=20)
@@ -301,14 +337,16 @@ class ApplyResetPasswordAPI(APIView):
render_data = { render_data = {
"username": user.username, "username": user.username,
"website_name": SysOptions.website_name, "website_name": SysOptions.website_name,
"link": f"{SysOptions.website_base_url}/reset-password/{user.reset_password_token}" "link": f"{SysOptions.website_base_url}/reset-password/{user.reset_password_token}",
} }
email_html = render_to_string("reset_password_email.html", render_data) email_html = render_to_string("reset_password_email.html", render_data)
send_email_async.send(from_name=SysOptions.website_name_shortcut, send_email_async.send(
from_name=SysOptions.website_name_shortcut,
to_email=user.email, to_email=user.email,
to_name=user.username, to_name=user.username,
subject="Reset your password", subject="Reset your password",
content=email_html) content=email_html,
)
return self.success("Succeeded") return self.success("Succeeded")
@@ -385,13 +423,18 @@ class UserRankAPI(APIView):
n = 0 n = 0
if rule_type not in ContestRuleType.choices(): if rule_type not in ContestRuleType.choices():
rule_type = ContestRuleType.ACM rule_type = ContestRuleType.ACM
profiles = UserProfile.objects.filter(user__admin_type=AdminType.REGULAR_USER, user__is_disabled=False, profiles = UserProfile.objects.filter(
user__username__icontains=username).select_related("user") user__admin_type=AdminType.REGULAR_USER,
user__is_disabled=False,
user__username__icontains=username,
).select_related("user")
if rule_type == ContestRuleType.ACM: if rule_type == ContestRuleType.ACM:
profiles = profiles.filter(accepted_number__gte=0).order_by("-accepted_number", "submission_number") profiles = profiles.filter(accepted_number__gte=0).order_by(
"-accepted_number", "submission_number"
)
else: else:
profiles = profiles.filter(total_score__gt=0).order_by("-total_score") profiles = profiles.filter(total_score__gt=0).order_by("-total_score")
if n>0: if n > 0:
profiles = profiles[:n] profiles = profiles[:n]
return self.success(self.paginate_data(request, profiles, RankInfoSerializer)) return self.success(self.paginate_data(request, profiles, RankInfoSerializer))
@@ -401,11 +444,20 @@ class UserActivityRankAPI(APIView):
start = request.GET.get("start") start = request.GET.get("start")
if not start: if not start:
return self.error("start time is required") return self.error("start time is required")
admin_usernames = User.objects.filter(is_disabled=False).exclude( admin_usernames = (
admin_type=AdminType.REGULAR_USER).values_list("username", flat=True) User.objects.filter(is_disabled=False)
.exclude(admin_type=AdminType.REGULAR_USER)
.values_list("username", flat=True)
)
admin_len = len(admin_usernames) admin_len = len(admin_usernames)
submissions = Submission.objects.filter(contest_id__isnull=True, create_time__gte=start, result=JudgeStatus.ACCEPTED) submissions = Submission.objects.filter(
counts = submissions.values("username").annotate(count=Count("problem_id", distinct=True)).order_by("-count")[:10+admin_len] contest_id__isnull=True, create_time__gte=start, result=JudgeStatus.ACCEPTED
)
counts = (
submissions.values("username")
.annotate(count=Count("problem_id", distinct=True))
.order_by("-count")[: 10 + admin_len]
)
data = [] data = []
for count in counts: for count in counts:
if count["username"] not in admin_usernames: if count["username"] not in admin_usernames:
@@ -422,7 +474,9 @@ class ProfileProblemDisplayIDRefreshAPI(APIView):
ids = list(acm_problems.keys()) + list(oi_problems.keys()) ids = list(acm_problems.keys()) + list(oi_problems.keys())
if not ids: if not ids:
return self.success() return self.success()
display_ids = Problem.objects.filter(id__in=ids, visible=True).values_list("_id", flat=True) display_ids = Problem.objects.filter(id__in=ids, visible=True).values_list(
"_id", flat=True
)
id_map = dict(zip(ids, display_ids)) id_map = dict(zip(ids, display_ids))
for k, v in acm_problems.items(): for k, v in acm_problems.items():
v["_id"] = id_map[k] v["_id"] = id_map[k]
@@ -459,4 +513,10 @@ class SSOAPI(CSRFExemptAPIView):
user = User.objects.get(auth_token=request.data["token"]) user = User.objects.get(auth_token=request.data["token"])
except User.DoesNotExist: except User.DoesNotExist:
return self.error("User does not exist") return self.error("User does not exist")
return self.success({"username": user.username, "avatar": user.userprofile.avatar, "admin_type": user.admin_type}) return self.success(
{
"username": user.username,
"avatar": user.userprofile.avatar,
"admin_type": user.admin_type,
}
)