feat: ADRF async views (phase1) + native async serializers (phase2) + async cache infra

This commit is contained in:
async-upgrade
2026-09-06 14:26:17 +08:00
parent 9a6577f71e
commit 8f488fcaaa
55 changed files with 2224 additions and 1513 deletions
+113 -105
View File
@@ -2,11 +2,12 @@ import os
import uuid
import logging
from asgiref.sync import sync_to_async
from utils.email_utils import validate_email_mx
from rest_framework.decorators import permission_classes
from rest_framework.parsers import MultiPartParser, FormParser, JSONParser
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.views import APIView
from adrf.views import APIView
from rest_framework.response import Response
from rest_framework import status
from django.contrib.auth import authenticate
@@ -50,6 +51,11 @@ def _create_login_record(request, user, record_status):
create_login_record(request, user, record_status)
def _serialize_user(user):
"""同步序列化助手:在 async 视图中通过 sync_to_async 调用,避免 SynchronousOnlyOperation。"""
return UserSerializer(user).data
class SendUserEmailAPIView(APIView):
permission_classes = [AllowAny]
@@ -66,7 +72,7 @@ class SendUserEmailAPIView(APIView):
),
responses={201: success_response, 200: success_response, 400: error_response, 500: error_response},
)
def post(self, request):
async def post(self, request):
to_email = request.data.get('to_email', None)
if to_email is None or to_email == "":
@@ -75,7 +81,7 @@ class SendUserEmailAPIView(APIView):
status_code=status.HTTP_400_BAD_REQUEST
)
if not validate_email_mx(to_email):
if not await sync_to_async(validate_email_mx)(to_email):
logger.warning(f'[Email] Domain MX check failed: email={to_email}')
return create_standardized_error_response(
code=ResponseCode.EMAIL_DOMAIN_INVALID,
@@ -83,13 +89,13 @@ class SendUserEmailAPIView(APIView):
)
try:
user_exists_result = FUser.objects.filter(email=to_email).exists()
user_exists_result = await FUser.objects.filter(email=to_email).aexists()
if not user_exists_result:
code = RandCode.get_digit_characters_code_8()
default_cache.set(f"register_{to_email}", code, timeout=600)
await sync_to_async(default_cache.set)(f"register_{to_email}", code, timeout=600)
result = submit_task(send_verification_email_task, to_email, code, 'register')
result = await sync_to_async(submit_task)(send_verification_email_task, to_email, code, 'register')
if result is None:
logger.warning(f'[Register] Email send failed: email={to_email}')
else:
@@ -102,9 +108,9 @@ class SendUserEmailAPIView(APIView):
)
else:
code = RandCode.get_digit_characters_code_8()
default_cache.set(f"login_{to_email}", code, timeout=600)
await sync_to_async(default_cache.set)(f"login_{to_email}", code, timeout=600)
result = submit_task(send_verification_email_task, to_email, code, 'login')
result = await sync_to_async(submit_task)(send_verification_email_task, to_email, code, 'login')
if result is None:
logger.warning(f'[Login] Email send failed: email={to_email}')
else:
@@ -141,7 +147,7 @@ class UserLoginOrRegisterAPIView(APIView):
),
responses={200: success_response, 400: error_response, 500: error_response},
)
def post(self, request):
async def post(self, request):
code = request.data.get('code', None)
to_email = request.data.get('email', None)
@@ -152,11 +158,11 @@ class UserLoginOrRegisterAPIView(APIView):
)
try:
user = FUser.objects.filter(email=to_email).first()
user = await FUser.objects.filter(email=to_email).afirst()
if user is None:
# Registration flow
vcode = default_cache.get(f"register_{to_email}")
vcode = await sync_to_async(default_cache.get)(f"register_{to_email}")
if vcode is None:
return create_standardized_error_response(
@@ -166,20 +172,21 @@ class UserLoginOrRegisterAPIView(APIView):
if code == vcode:
user_serializer = UserSerializer(data=request.data)
if user_serializer.is_valid():
user = user_serializer.create_by_email(request.data)
if await sync_to_async(user_serializer.is_valid)():
user = await sync_to_async(user_serializer.create_by_email)(request.data)
refresh = RefreshToken.for_user(user)
user_data = await sync_to_async(_serialize_user)(user)
# Prepare response data
response_data = {
'user': UserSerializer(user).data,
'user': user_data,
'refresh': str(refresh),
'access': str(refresh.access_token),
'token_type': 'bearer',
'expires_at_timestamp': refresh.access_token.payload['exp']
}
_create_login_record(request, user, 'success')
await sync_to_async(_create_login_record)(request, user, 'success')
return create_standardized_response(
data=response_data,
@@ -198,7 +205,7 @@ class UserLoginOrRegisterAPIView(APIView):
)
else:
# Login flow
vcode = default_cache.get(f"login_{to_email}")
vcode = await sync_to_async(default_cache.get)(f"login_{to_email}")
if vcode is None:
return create_standardized_error_response(
@@ -208,18 +215,18 @@ class UserLoginOrRegisterAPIView(APIView):
if code == vcode:
refresh = RefreshToken.for_user(user)
user_serializer = UserSerializer(user)
user_data = await sync_to_async(_serialize_user)(user)
# Prepare response data
response_data = {
'user': user_serializer.data,
'user': user_data,
'refresh': str(refresh),
'access': str(refresh.access_token),
'token_type': 'bearer',
'expires_at_timestamp': refresh.access_token.payload['exp']
}
_create_login_record(request, user, 'success')
await sync_to_async(_create_login_record)(request, user, 'success')
return create_standardized_response(
data=response_data,
@@ -227,7 +234,7 @@ class UserLoginOrRegisterAPIView(APIView):
status_code=status.HTTP_200_OK
)
else:
_create_login_record(request, user, 'failed')
await sync_to_async(_create_login_record)(request, user, 'failed')
return create_standardized_error_response(
code=ResponseCode.LOGIN_VERIFICATION_ERROR,
@@ -260,7 +267,7 @@ class ForgotPasswordSendCodeAPIView(APIView):
),
responses={200: success_response, 400: error_response, 500: error_response},
)
def post(self, request):
async def post(self, request):
from utils.captcha import check_captcha_required, verify_captcha, record_failure, reset_failures
to_email = request.data.get('email', None)
@@ -271,7 +278,7 @@ class ForgotPasswordSendCodeAPIView(APIView):
status_code=status.HTTP_400_BAD_REQUEST
)
if not validate_email_mx(to_email):
if not await sync_to_async(validate_email_mx)(to_email):
return create_standardized_error_response(
code=ResponseCode.EMAIL_DOMAIN_INVALID,
status_code=status.HTTP_400_BAD_REQUEST
@@ -279,7 +286,7 @@ class ForgotPasswordSendCodeAPIView(APIView):
identifier = to_email
operation = 'forgot_send'
captcha_required = check_captcha_required(operation, identifier)
captcha_required = await sync_to_async(check_captcha_required)(operation, identifier)
if captcha_required:
captcha_key = request.data.get('captcha_key', None)
@@ -291,39 +298,39 @@ class ForgotPasswordSendCodeAPIView(APIView):
status_code=status.HTTP_400_BAD_REQUEST
)
captcha_result = verify_captcha(captcha_key, captcha_code)
captcha_result = await sync_to_async(verify_captcha)(captcha_key, captcha_code)
if captcha_result == 'expired':
return create_standardized_error_response(
code=ResponseCode.CAPTCHA_EXPIRED,
status_code=status.HTTP_400_BAD_REQUEST
)
elif captcha_result == 'wrong':
record_failure(operation, identifier)
await sync_to_async(record_failure)(operation, identifier)
return create_standardized_error_response(
code=ResponseCode.CAPTCHA_ERROR,
status_code=status.HTTP_400_BAD_REQUEST
)
try:
user = FUser.objects.filter(email=to_email).first()
user = await FUser.objects.filter(email=to_email).afirst()
if user is None:
record_failure(operation, to_email)
await sync_to_async(record_failure)(operation, to_email)
return create_standardized_error_response(
code=ResponseCode.USER_NOT_FOUND,
status_code=status.HTTP_400_BAD_REQUEST
)
code = RandCode.get_digit_characters_code_8()
default_cache.set(f"reset_password_{to_email}", code, timeout=600)
await sync_to_async(default_cache.set)(f"reset_password_{to_email}", code, timeout=600)
result = submit_task(send_reset_password_email_task, to_email, code)
result = await sync_to_async(submit_task)(send_reset_password_email_task, to_email, code)
if result is None:
logger.warning(f'[ResetPassword] Email send failed (both sync and async): email={to_email}')
else:
logger.info(f'[ResetPassword] Email sent successfully: email={to_email}')
reset_failures(operation, to_email)
await sync_to_async(reset_failures)(operation, to_email)
return create_standardized_response(
data={"email_sent": True},
code=ResponseCode.RESET_CODE_SENT,
@@ -331,7 +338,7 @@ class ForgotPasswordSendCodeAPIView(APIView):
)
except Exception as e:
record_failure(operation, to_email)
await sync_to_async(record_failure)(operation, to_email)
return create_standardized_error_response(
message=f"邮件发送失败: {str(e)}",
code=ResponseCode.EMAIL_SEND_FAILED,
@@ -360,7 +367,7 @@ class ForgotPasswordResetAPIView(APIView):
),
responses={200: success_response, 400: error_response, 500: error_response},
)
def post(self, request):
async def post(self, request):
from utils.captcha import check_captcha_required, verify_captcha, record_failure, reset_failures
to_email = request.data.get('email', None)
@@ -377,7 +384,7 @@ class ForgotPasswordResetAPIView(APIView):
identifier = to_email
operation = 'forgot_reset'
captcha_required = check_captcha_required(operation, identifier)
captcha_required = await sync_to_async(check_captcha_required)(operation, identifier)
if captcha_required:
captcha_key = request.data.get('captcha_key', None)
@@ -389,28 +396,28 @@ class ForgotPasswordResetAPIView(APIView):
status_code=status.HTTP_400_BAD_REQUEST
)
captcha_result = verify_captcha(captcha_key, captcha_code)
captcha_result = await sync_to_async(verify_captcha)(captcha_key, captcha_code)
if captcha_result == 'expired':
return create_standardized_error_response(
code=ResponseCode.CAPTCHA_EXPIRED,
status_code=status.HTTP_400_BAD_REQUEST
)
elif captcha_result == 'wrong':
record_failure(operation, identifier)
await sync_to_async(record_failure)(operation, identifier)
return create_standardized_error_response(
code=ResponseCode.CAPTCHA_ERROR,
status_code=status.HTTP_400_BAD_REQUEST
)
if new_password != confirm_password:
record_failure(operation, to_email)
await sync_to_async(record_failure)(operation, to_email)
return create_standardized_error_response(
code=ResponseCode.PASSWORD_MISMATCH,
status_code=status.HTTP_400_BAD_REQUEST
)
if len(new_password) < 8:
record_failure(operation, to_email)
await sync_to_async(record_failure)(operation, to_email)
return create_standardized_error_response(
code=ResponseCode.PASSWORD_TOO_SHORT,
status_code=status.HTTP_400_BAD_REQUEST
@@ -419,51 +426,51 @@ class ForgotPasswordResetAPIView(APIView):
has_letter = any(c.isalpha() for c in new_password)
has_digit = any(c.isdigit() for c in new_password)
if not (has_letter and has_digit):
record_failure(operation, to_email)
await sync_to_async(record_failure)(operation, to_email)
return create_standardized_error_response(
code=ResponseCode.PASSWORD_TOO_WEAK,
status_code=status.HTTP_400_BAD_REQUEST
)
try:
vcode = default_cache.get(f"reset_password_{to_email}")
vcode = await sync_to_async(default_cache.get)(f"reset_password_{to_email}")
if vcode is None:
record_failure(operation, to_email)
await sync_to_async(record_failure)(operation, to_email)
return create_standardized_error_response(
code=ResponseCode.VERIFICATION_CODE_EXPIRED,
status_code=status.HTTP_400_BAD_REQUEST
)
if code != vcode:
record_failure(operation, to_email)
await sync_to_async(record_failure)(operation, to_email)
return create_standardized_error_response(
code=ResponseCode.VERIFICATION_CODE_ERROR,
status_code=status.HTTP_400_BAD_REQUEST
)
user = FUser.objects.filter(email=to_email).first()
user = await FUser.objects.filter(email=to_email).afirst()
if user is None:
record_failure(operation, to_email)
await sync_to_async(record_failure)(operation, to_email)
return create_standardized_error_response(
code=ResponseCode.USER_NOT_FOUND,
status_code=status.HTTP_400_BAD_REQUEST
)
user.set_password(new_password)
user.save()
await sync_to_async(user.set_password)(new_password)
await user.asave()
default_cache.delete(f"reset_password_{to_email}")
await sync_to_async(default_cache.delete)(f"reset_password_{to_email}")
reset_failures(operation, to_email)
await sync_to_async(reset_failures)(operation, to_email)
return create_standardized_response(
code=ResponseCode.PASSWORD_RESET_SUCCESS,
status_code=status.HTTP_200_OK
)
except Exception as e:
record_failure(operation, to_email)
await sync_to_async(record_failure)(operation, to_email)
return create_standardized_error_response(
message=f"服务器内部错误: {str(e)}",
code=ResponseCode.SERVER_INTERNAL_ERROR,
@@ -478,12 +485,12 @@ class UserUpdateAPIView(APIView):
operation_description='获取当前登录用户的个人信息',
responses={200: success_response, 401: unauthorized_response},
)
def get(self, request):
async def get(self, request):
user = request.user
user_serializer = UserSerializer(user)
user_data = await sync_to_async(_serialize_user)(user)
return create_standardized_response(
data={'user': user_serializer.data},
data={'user': user_data},
code=ResponseCode.SUCCESS,
status_code=status.HTTP_200_OK
)
@@ -495,7 +502,7 @@ class UserUpdateAPIView(APIView):
request_body=UserUpdateSerializer,
responses={200: success_response, 400: error_response, 401: unauthorized_response},
)
def put(self, request):
async def put(self, request):
user = request.user
serializer = UserUpdateSerializer(
user,
@@ -504,7 +511,7 @@ class UserUpdateAPIView(APIView):
context={'request': request}
)
if not serializer.is_valid():
if not await sync_to_async(serializer.is_valid)():
errors = serializer.errors
first_error = ''
for field, msgs in errors.items():
@@ -519,23 +526,23 @@ class UserUpdateAPIView(APIView):
status_code=status.HTTP_400_BAD_REQUEST
)
updated_user = serializer.save()
user_serializer = UserSerializer(updated_user)
updated_user = await sync_to_async(serializer.save)()
user_data = await sync_to_async(_serialize_user)(updated_user)
try:
log_event(
await sync_to_async(log_event)(
event_type='profile_update',
user=updated_user,
description=f'更新了{len(serializer.validated_data)}项个人资料',
metadata={'updated_fields': list(serializer.validated_data.keys())},
request=request
)
track_user_action(updated_user, 'profile', count=1)
await sync_to_async(track_user_action)(updated_user, 'profile', count=1)
except Exception as e:
logging.getLogger(__name__).warning(f'Profile track failed: {e}')
return create_standardized_response(
data={'user': user_serializer.data},
data={'user': user_data},
code=ResponseCode.SUCCESS,
status_code=status.HTTP_200_OK
)
@@ -547,8 +554,8 @@ class UserUpdateAPIView(APIView):
request_body=UserUpdateSerializer,
responses={200: success_response, 400: error_response, 401: unauthorized_response},
)
def patch(self, request):
return self.put(request)
async def patch(self, request):
return await self.put(request)
class ChangePasswordAPIView(APIView):
@@ -568,12 +575,12 @@ class ChangePasswordAPIView(APIView):
),
responses={200: success_response, 400: error_response, 401: unauthorized_response},
)
def post(self, request):
async def post(self, request):
from utils.captcha import check_captcha_required, verify_captcha, record_failure, reset_failures
identifier = str(request.user.id)
operation = 'change_pwd'
captcha_required = check_captcha_required(operation, identifier)
captcha_required = await sync_to_async(check_captcha_required)(operation, identifier)
if captcha_required:
captcha_key = request.data.get('captcha_key', None)
@@ -585,14 +592,14 @@ class ChangePasswordAPIView(APIView):
status_code=status.HTTP_400_BAD_REQUEST
)
captcha_result = verify_captcha(captcha_key, captcha_code)
captcha_result = await sync_to_async(verify_captcha)(captcha_key, captcha_code)
if captcha_result == 'expired':
return create_standardized_error_response(
code=ResponseCode.CAPTCHA_EXPIRED,
status_code=status.HTTP_400_BAD_REQUEST
)
elif captcha_result == 'wrong':
record_failure(operation, identifier)
await sync_to_async(record_failure)(operation, identifier)
return create_standardized_error_response(
code=ResponseCode.CAPTCHA_ERROR,
status_code=status.HTTP_400_BAD_REQUEST
@@ -603,7 +610,7 @@ class ChangePasswordAPIView(APIView):
context={'request': request}
)
if not serializer.is_valid():
if not await sync_to_async(serializer.is_valid)():
errors = serializer.errors
first_error = ''
for field, msgs in errors.items():
@@ -617,7 +624,7 @@ class ChangePasswordAPIView(APIView):
break
break
record_failure(operation, identifier)
await sync_to_async(record_failure)(operation, identifier)
return create_standardized_error_response(
data=errors,
code=ResponseCode.PARAMETER_ERROR,
@@ -625,15 +632,15 @@ class ChangePasswordAPIView(APIView):
status_code=status.HTTP_400_BAD_REQUEST
)
user = serializer.save()
user_serializer = UserSerializer(user)
user = await sync_to_async(serializer.save)()
user_data = await sync_to_async(_serialize_user)(user)
is_new_set = not request.user.has_usable_password() or request.data.get('old_password', '') == ''
reset_failures(operation, identifier)
await sync_to_async(reset_failures)(operation, identifier)
return create_standardized_response(
data={
'user': user_serializer.data,
'user': user_data,
'is_new_set': is_new_set,
},
code=ResponseCode.PASSWORD_SET if is_new_set else ResponseCode.PASSWORD_CHANGED,
@@ -658,7 +665,7 @@ class UserLoginAPIView(APIView):
),
responses={200: success_response, 400: error_response, 401: unauthorized_response, 403: error_response},
)
def post(self, request):
async def post(self, request):
account = request.data.get('account', None)
password = request.data.get('password', None)
@@ -670,22 +677,22 @@ class UserLoginAPIView(APIView):
)
try:
user = authenticate(username=account, password=password)
user = await sync_to_async(authenticate)(username=account, password=password)
if user is not None:
if user.is_active:
refresh = RefreshToken.for_user(user)
user_serializer = UserSerializer(user)
user_data = await sync_to_async(_serialize_user)(user)
response_data = {
'user': user_serializer.data,
'user': user_data,
'refresh': str(refresh),
'access': str(refresh.access_token),
'token_type': 'bearer',
'expires_at_timestamp': refresh.access_token.payload['exp']
}
_create_login_record(request, user, 'success')
await sync_to_async(_create_login_record)(request, user, 'success')
return create_standardized_response(
data=response_data,
@@ -693,7 +700,7 @@ class UserLoginAPIView(APIView):
status_code=status.HTTP_200_OK
)
else:
_create_login_record(request, user, 'failed')
await sync_to_async(_create_login_record)(request, user, 'failed')
return create_standardized_error_response(
code=ResponseCode.PARAMETER_ERROR,
@@ -701,9 +708,9 @@ class UserLoginAPIView(APIView):
status_code=status.HTTP_403_FORBIDDEN
)
else:
login_user = FUser.objects.filter(username=account).first() or FUser.objects.filter(email=account).first()
login_user = await FUser.objects.filter(username=account).afirst() or await FUser.objects.filter(email=account).afirst()
if login_user:
_create_login_record(request, login_user, 'failed')
await sync_to_async(_create_login_record)(request, login_user, 'failed')
return create_standardized_error_response(
code=ResponseCode.PARAMETER_ERROR,
@@ -744,7 +751,7 @@ class LoginView(APIView):
),
responses={200: success_response, 400: error_response, 401: unauthorized_response, 403: error_response},
)
def post(self, request):
async def post(self, request):
account = request.data.get('account', None)
password = request.data.get('password', None)
@@ -757,7 +764,7 @@ class LoginView(APIView):
identifier = self._get_identifier(request)
operation = 'login'
captcha_required = check_captcha_required(operation, identifier)
captcha_required = await sync_to_async(check_captcha_required)(operation, identifier)
if captcha_required:
slider_captcha_key = request.data.get('slider_captcha_key', None)
@@ -770,9 +777,9 @@ class LoginView(APIView):
)
try:
captcha_valid = verify_slider_captcha(slider_captcha_key, int(slider_captcha_x))
captcha_valid = await sync_to_async(verify_slider_captcha)(slider_captcha_key, int(slider_captcha_x))
if not captcha_valid:
record_failure(operation, identifier)
await sync_to_async(record_failure)(operation, identifier)
return create_standardized_error_response(
code=ResponseCode.CAPTCHA_ERROR,
status_code=status.HTTP_400_BAD_REQUEST
@@ -784,23 +791,23 @@ class LoginView(APIView):
)
try:
user = authenticate(username=account, password=password)
user = await sync_to_async(authenticate)(username=account, password=password)
if user is not None:
if user.is_active:
reset_failures(operation, identifier)
await sync_to_async(reset_failures)(operation, identifier)
refresh = RefreshToken.for_user(user)
user_serializer = UserSerializer(user)
user_data = await sync_to_async(_serialize_user)(user)
response_data = {
'user': user_serializer.data,
'user': user_data,
'refresh': str(refresh),
'access': str(refresh.access_token),
'token_type': 'bearer',
'expires_at_timestamp': refresh.access_token.payload['exp']
}
_create_login_record(request, user, 'success')
await sync_to_async(_create_login_record)(request, user, 'success')
return create_standardized_response(
data=response_data,
@@ -808,8 +815,8 @@ class LoginView(APIView):
status_code=status.HTTP_200_OK
)
else:
record_failure(operation, identifier)
_create_login_record(request, user, 'failed')
await sync_to_async(record_failure)(operation, identifier)
await sync_to_async(_create_login_record)(request, user, 'failed')
return create_standardized_error_response(
code=ResponseCode.PARAMETER_ERROR,
@@ -817,10 +824,10 @@ class LoginView(APIView):
status_code=status.HTTP_403_FORBIDDEN
)
else:
record_failure(operation, identifier)
login_user = FUser.objects.filter(username=account).first() or FUser.objects.filter(email=account).first()
await sync_to_async(record_failure)(operation, identifier)
login_user = await FUser.objects.filter(username=account).afirst() or await FUser.objects.filter(email=account).afirst()
if login_user:
_create_login_record(request, login_user, 'failed')
await sync_to_async(_create_login_record)(request, login_user, 'failed')
return create_standardized_error_response(
code=ResponseCode.PARAMETER_ERROR,
@@ -848,16 +855,17 @@ class PublicProfileAPIView(APIView):
],
responses={200: success_response, 404: not_found_response},
)
def get(self, request, user_id):
async def get(self, request, user_id):
try:
user = FUser.objects.get(id=user_id, is_active=True)
user = await FUser.objects.aget(id=user_id, is_active=True)
except FUser.DoesNotExist:
return Response({'error': '用户不存在'}, status=status.HTTP_404_NOT_FOUND)
try:
from article.models import Article
article_count = Article.objects.filter(author=user, status='published').count()
article_count = await Article.objects.filter(author=user, status='published').acount()
recent_articles = Article.objects.filter(author=user, status='published').order_by('-created_at')[:5]
recent_articles = [a async for a in recent_articles]
articles_data = []
for article in recent_articles:
articles_data.append({
@@ -945,7 +953,7 @@ class UploadAvatarAPIView(APIView):
],
responses={200: success_response, 400: error_response, 401: unauthorized_response, 500: error_response},
)
def post(self, request):
async def post(self, request):
file = request.FILES.get('avatar')
if not file:
@@ -979,17 +987,17 @@ class UploadAvatarAPIView(APIView):
try:
filename = f'avatars/{uuid.uuid4().hex}{ext}'
saved_path = default_storage.save(filename, file)
saved_path = await sync_to_async(default_storage.save)(filename, file)
user = request.user
if user.avatar and user.avatar.name:
try:
default_storage.delete(user.avatar.name)
await sync_to_async(default_storage.delete)(user.avatar.name)
except Exception:
pass
user.avatar = saved_path
user.save(update_fields=['avatar'])
await user.asave(update_fields=['avatar'])
avatar_url = request.build_absolute_uri(settings.MEDIA_URL + saved_path)
@@ -1024,9 +1032,9 @@ class FollowToggleView(APIView):
404: not_found_response,
},
)
def post(self, request, user_id):
async def post(self, request, user_id):
try:
target_user = FUser.objects.get(id=user_id, is_active=True)
target_user = await FUser.objects.aget(id=user_id, is_active=True)
except FUser.DoesNotExist:
return create_standardized_error_response(
code=ResponseCode.NOT_FOUND,
@@ -1041,19 +1049,19 @@ class FollowToggleView(APIView):
status_code=status.HTTP_400_BAD_REQUEST,
)
follow, created = Follow.objects.get_or_create(
follow, created = await Follow.objects.aget_or_create(
follower=request.user,
following=target_user,
)
if not created:
follow.delete()
await follow.adelete()
is_following = False
else:
is_following = True
follower_count = target_user.followers.count()
following_count = request.user.following.count()
follower_count = await target_user.followers.acount()
following_count = await request.user.following.acount()
return create_standardized_response(
data={