添加最近提交
This commit is contained in:
@@ -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()),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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(
|
||||||
to_email=user.email,
|
from_name=SysOptions.website_name_shortcut,
|
||||||
to_name=user.username,
|
to_email=user.email,
|
||||||
subject="Reset your password",
|
to_name=user.username,
|
||||||
content=email_html)
|
subject="Reset your password",
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user