feat: ADRF async views (phase1) + native async serializers (phase2) + async cache infra
This commit is contained in:
+49
-47
@@ -1,16 +1,18 @@
|
||||
from rest_framework import serializers
|
||||
from adrf.serializers import (
|
||||
ModelSerializer, SerializerMethodField,
|
||||
)
|
||||
from .models import Course, Chapter, ChapterContent, CourseMaterial
|
||||
|
||||
|
||||
class ChapterSerializer(serializers.ModelSerializer):
|
||||
video_poster_url = serializers.SerializerMethodField()
|
||||
video_local_url = serializers.SerializerMethodField()
|
||||
class ChapterSerializer(ModelSerializer):
|
||||
video_poster_url = SerializerMethodField()
|
||||
video_local_url = SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Chapter
|
||||
fields = ['id', 'title', 'sort_order', 'duration', 'is_free', 'video_url', 'video_source', 'video_local_url', 'video_poster_url', 'created_at', 'updated_at']
|
||||
|
||||
def get_video_poster_url(self, obj):
|
||||
async def get_video_poster_url(self, obj):
|
||||
if obj.video_poster:
|
||||
request = self.context.get('request')
|
||||
if request:
|
||||
@@ -18,7 +20,7 @@ class ChapterSerializer(serializers.ModelSerializer):
|
||||
return obj.video_poster.url
|
||||
return ''
|
||||
|
||||
def get_video_local_url(self, obj):
|
||||
async def get_video_local_url(self, obj):
|
||||
if obj.video_local:
|
||||
request = self.context.get('request')
|
||||
if request:
|
||||
@@ -27,22 +29,22 @@ class ChapterSerializer(serializers.ModelSerializer):
|
||||
return ''
|
||||
|
||||
|
||||
class ChapterContentSerializer(serializers.ModelSerializer):
|
||||
class ChapterContentSerializer(ModelSerializer):
|
||||
class Meta:
|
||||
model = ChapterContent
|
||||
fields = ['id', 'chapter', 'content_md', 'content_html', 'md_file_path', 'updated_at']
|
||||
|
||||
|
||||
class MaterialSerializer(serializers.ModelSerializer):
|
||||
file_url = serializers.SerializerMethodField()
|
||||
file_size_display = serializers.SerializerMethodField()
|
||||
file_type_display = serializers.SerializerMethodField()
|
||||
class MaterialSerializer(ModelSerializer):
|
||||
file_url = SerializerMethodField()
|
||||
file_size_display = SerializerMethodField()
|
||||
file_type_display = SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = CourseMaterial
|
||||
fields = ['id', 'title', 'file_url', 'file_type', 'file_type_display', 'file_size', 'file_size_display', 'download_count', 'sort_order', 'created_at']
|
||||
|
||||
def get_file_url(self, obj):
|
||||
async def get_file_url(self, obj):
|
||||
if obj.file:
|
||||
request = self.context.get('request')
|
||||
if request:
|
||||
@@ -50,7 +52,7 @@ class MaterialSerializer(serializers.ModelSerializer):
|
||||
return obj.file.url
|
||||
return ''
|
||||
|
||||
def get_file_size_display(self, obj):
|
||||
async def get_file_size_display(self, obj):
|
||||
size = obj.file_size
|
||||
if size < 1024:
|
||||
return f'{size} B'
|
||||
@@ -59,7 +61,7 @@ class MaterialSerializer(serializers.ModelSerializer):
|
||||
else:
|
||||
return f'{size / (1024 * 1024):.1f} MB'
|
||||
|
||||
def get_file_type_display(self, obj):
|
||||
async def get_file_type_display(self, obj):
|
||||
type_map = {
|
||||
'pdf': 'PDF',
|
||||
'code': '代码',
|
||||
@@ -72,14 +74,14 @@ class MaterialSerializer(serializers.ModelSerializer):
|
||||
return type_map.get(obj.file_type, '其他')
|
||||
|
||||
|
||||
class ChapterSerializer(serializers.ModelSerializer):
|
||||
video_poster_url = serializers.SerializerMethodField()
|
||||
class ChapterSerializer(ModelSerializer):
|
||||
video_poster_url = SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Chapter
|
||||
fields = ['id', 'title', 'sort_order', 'duration', 'is_free', 'video_url', 'video_poster_url', 'created_at', 'updated_at']
|
||||
|
||||
def get_video_poster_url(self, obj):
|
||||
async def get_video_poster_url(self, obj):
|
||||
if obj.video_poster:
|
||||
request = self.context.get('request')
|
||||
if request:
|
||||
@@ -88,17 +90,17 @@ class ChapterSerializer(serializers.ModelSerializer):
|
||||
return ''
|
||||
|
||||
|
||||
class ChapterContentSerializer(serializers.ModelSerializer):
|
||||
class ChapterContentSerializer(ModelSerializer):
|
||||
class Meta:
|
||||
model = ChapterContent
|
||||
fields = ['id', 'chapter', 'content_md', 'content_html', 'md_file_path', 'updated_at']
|
||||
|
||||
|
||||
class CourseListSerializer(serializers.ModelSerializer):
|
||||
author_name = serializers.SerializerMethodField()
|
||||
chapters_count = serializers.SerializerMethodField()
|
||||
students_count = serializers.SerializerMethodField()
|
||||
cover_image_url = serializers.SerializerMethodField()
|
||||
class CourseListSerializer(ModelSerializer):
|
||||
author_name = SerializerMethodField()
|
||||
chapters_count = SerializerMethodField()
|
||||
students_count = SerializerMethodField()
|
||||
cover_image_url = SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Course
|
||||
@@ -109,13 +111,13 @@ class CourseListSerializer(serializers.ModelSerializer):
|
||||
'sort_order', 'is_hot', 'is_new', 'created_at', 'updated_at',
|
||||
]
|
||||
|
||||
def get_chapters_count(self, obj):
|
||||
return obj.chapters.count()
|
||||
async def get_chapters_count(self, obj):
|
||||
return await obj.chapters.acount()
|
||||
|
||||
def get_students_count(self, obj):
|
||||
async def get_students_count(self, obj):
|
||||
return 0
|
||||
|
||||
def get_cover_image_url(self, obj):
|
||||
async def get_cover_image_url(self, obj):
|
||||
if obj.cover_image:
|
||||
request = self.context.get('request')
|
||||
if request:
|
||||
@@ -123,18 +125,18 @@ class CourseListSerializer(serializers.ModelSerializer):
|
||||
return obj.cover_image.url
|
||||
return ''
|
||||
|
||||
def get_author_name(self, obj):
|
||||
async def get_author_name(self, obj):
|
||||
if obj.author:
|
||||
return obj.author.get_full_name() or obj.author.username
|
||||
return ''
|
||||
|
||||
|
||||
class CourseDetailSerializer(serializers.ModelSerializer):
|
||||
author_name = serializers.SerializerMethodField()
|
||||
class CourseDetailSerializer(ModelSerializer):
|
||||
author_name = SerializerMethodField()
|
||||
chapters = ChapterSerializer(many=True, read_only=True)
|
||||
chapters_count = serializers.SerializerMethodField()
|
||||
students_count = serializers.SerializerMethodField()
|
||||
cover_image_url = serializers.SerializerMethodField()
|
||||
chapters_count = SerializerMethodField()
|
||||
students_count = SerializerMethodField()
|
||||
cover_image_url = SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Course
|
||||
@@ -145,13 +147,13 @@ class CourseDetailSerializer(serializers.ModelSerializer):
|
||||
'sort_order', 'is_hot', 'is_new', 'created_at', 'updated_at',
|
||||
]
|
||||
|
||||
def get_chapters_count(self, obj):
|
||||
return obj.chapters.count()
|
||||
async def get_chapters_count(self, obj):
|
||||
return await obj.chapters.acount()
|
||||
|
||||
def get_students_count(self, obj):
|
||||
async def get_students_count(self, obj):
|
||||
return 0
|
||||
|
||||
def get_cover_image_url(self, obj):
|
||||
async def get_cover_image_url(self, obj):
|
||||
if obj.cover_image:
|
||||
request = self.context.get('request')
|
||||
if request:
|
||||
@@ -159,25 +161,25 @@ class CourseDetailSerializer(serializers.ModelSerializer):
|
||||
return obj.cover_image.url
|
||||
return ''
|
||||
|
||||
def get_author_name(self, obj):
|
||||
async def get_author_name(self, obj):
|
||||
if obj.author:
|
||||
return obj.author.get_full_name() or obj.author.username
|
||||
return ''
|
||||
|
||||
|
||||
class CourseCreateUpdateSerializer(serializers.ModelSerializer):
|
||||
class CourseCreateUpdateSerializer(ModelSerializer):
|
||||
class Meta:
|
||||
model = Course
|
||||
fields = ['title', 'description', 'category', 'level', 'cover_image', 'color', 'icon_name', 'status', 'sort_order', 'is_hot', 'is_new']
|
||||
|
||||
def create(self, validated_data):
|
||||
async def acreate(self, validated_data):
|
||||
validated_data['author'] = self.context['request'].user
|
||||
return super().create(validated_data)
|
||||
return await super().acreate(validated_data)
|
||||
|
||||
|
||||
class CourseManageSerializer(serializers.ModelSerializer):
|
||||
author_name = serializers.SerializerMethodField()
|
||||
chapters_count = serializers.SerializerMethodField()
|
||||
class CourseManageSerializer(ModelSerializer):
|
||||
author_name = SerializerMethodField()
|
||||
chapters_count = SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Course
|
||||
@@ -187,10 +189,10 @@ class CourseManageSerializer(serializers.ModelSerializer):
|
||||
'author_name', 'created_at', 'updated_at',
|
||||
]
|
||||
|
||||
def get_chapters_count(self, obj):
|
||||
return obj.chapters.count()
|
||||
async def get_chapters_count(self, obj):
|
||||
return await obj.chapters.acount()
|
||||
|
||||
def get_author_name(self, obj):
|
||||
async def get_author_name(self, obj):
|
||||
if obj.author:
|
||||
return obj.author.get_full_name() or obj.author.username
|
||||
return ''
|
||||
|
||||
+136
-103
@@ -3,11 +3,14 @@ from django.conf import settings
|
||||
from django.db.models import Count
|
||||
from django.utils import timezone
|
||||
from django.http import FileResponse, Http404
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework import generics, status
|
||||
from adrf import generics
|
||||
from adrf.generics import aget_object_or_404
|
||||
from adrf.mixins import get_data
|
||||
from adrf.views import APIView
|
||||
from asgiref.sync import sync_to_async
|
||||
from rest_framework import status
|
||||
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from drf_yasg import openapi
|
||||
from drf_yasg.utils import swagger_auto_schema
|
||||
@@ -65,6 +68,12 @@ class CourseListCreateView(generics.ListCreateAPIView):
|
||||
|
||||
return qs
|
||||
|
||||
async def get(self, request, *args, **kwargs):
|
||||
return await self.list(request, *args, **kwargs)
|
||||
|
||||
async def post(self, request, *args, **kwargs):
|
||||
return await self.create(request, *args, **kwargs)
|
||||
|
||||
@swagger_auto_schema(
|
||||
tags=['学习'],
|
||||
operation_summary='获取课程列表',
|
||||
@@ -76,22 +85,22 @@ class CourseListCreateView(generics.ListCreateAPIView):
|
||||
],
|
||||
responses={200: success_response},
|
||||
)
|
||||
def list(self, request, *args, **kwargs):
|
||||
async def list(self, request, *args, **kwargs):
|
||||
queryset = self.get_queryset()
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
data = serializer.data
|
||||
data = await get_data(serializer)
|
||||
|
||||
if request.user.is_authenticated:
|
||||
favorite_course_ids = set(
|
||||
CourseFavorite.objects.filter(user=request.user).values_list('course_id', flat=True)
|
||||
)
|
||||
favorite_course_ids = set([
|
||||
v async for v in CourseFavorite.objects.filter(user=request.user).values_list('course_id', flat=True)
|
||||
])
|
||||
for item in data:
|
||||
item['is_favorited'] = item['id'] in favorite_course_ids
|
||||
item['favorites_count'] = CourseFavorite.objects.filter(course_id=item['id']).count()
|
||||
item['favorites_count'] = await CourseFavorite.objects.filter(course_id=item['id']).acount()
|
||||
else:
|
||||
for item in data:
|
||||
item['is_favorited'] = False
|
||||
item['favorites_count'] = CourseFavorite.objects.filter(course_id=item['id']).count()
|
||||
item['favorites_count'] = await CourseFavorite.objects.filter(course_id=item['id']).acount()
|
||||
|
||||
return create_standardized_response(data=data, code=ResponseCode.SUCCESS)
|
||||
|
||||
@@ -106,12 +115,13 @@ class CourseListCreateView(generics.ListCreateAPIView):
|
||||
401: unauthorized_response,
|
||||
},
|
||||
)
|
||||
def create(self, request, *args, **kwargs):
|
||||
async def create(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
if await sync_to_async(serializer.is_valid)():
|
||||
await sync_to_async(serializer.save)()
|
||||
data = await get_data(serializer)
|
||||
return create_standardized_response(
|
||||
data=serializer.data,
|
||||
data=data,
|
||||
code=ResponseCode.SUCCESS,
|
||||
message='课程创建成功',
|
||||
status_code=status.HTTP_201_CREATED
|
||||
@@ -140,17 +150,17 @@ class CourseDetailView(APIView):
|
||||
404: not_found_response,
|
||||
},
|
||||
)
|
||||
def get(self, request, pk):
|
||||
course = get_object_or_404(Course, pk=pk)
|
||||
async def get(self, request, pk):
|
||||
course = await aget_object_or_404(Course, pk=pk)
|
||||
serializer = CourseDetailSerializer(course, context={'request': request})
|
||||
data = serializer.data
|
||||
data = await get_data(serializer)
|
||||
if request.user.is_authenticated:
|
||||
data['is_favorited'] = CourseFavorite.objects.filter(
|
||||
data['is_favorited'] = await CourseFavorite.objects.filter(
|
||||
user=request.user, course=course
|
||||
).exists()
|
||||
).aexists()
|
||||
else:
|
||||
data['is_favorited'] = False
|
||||
data['favorites_count'] = CourseFavorite.objects.filter(course=course).count()
|
||||
data['favorites_count'] = await CourseFavorite.objects.filter(course=course).acount()
|
||||
return create_standardized_response(data=data, code=ResponseCode.SUCCESS)
|
||||
|
||||
@swagger_auto_schema(
|
||||
@@ -169,8 +179,8 @@ class CourseDetailView(APIView):
|
||||
404: not_found_response,
|
||||
},
|
||||
)
|
||||
def put(self, request, pk):
|
||||
course = get_object_or_404(Course, pk=pk)
|
||||
async def put(self, request, pk):
|
||||
course = await aget_object_or_404(Course, pk=pk)
|
||||
if course.author != request.user:
|
||||
return create_standardized_error_response(
|
||||
code=ResponseCode.VALIDATION_ERROR,
|
||||
@@ -178,9 +188,10 @@ class CourseDetailView(APIView):
|
||||
status_code=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
serializer = CourseCreateUpdateSerializer(course, data=request.data, partial=True, context={'request': request})
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return create_standardized_response(data=serializer.data, code=ResponseCode.SUCCESS, message='课程更新成功')
|
||||
if await sync_to_async(serializer.is_valid)():
|
||||
await sync_to_async(serializer.save)()
|
||||
data = await get_data(serializer)
|
||||
return create_standardized_response(data=data, code=ResponseCode.SUCCESS, message='课程更新成功')
|
||||
return create_standardized_error_response(
|
||||
data=serializer.errors,
|
||||
code=ResponseCode.VALIDATION_ERROR,
|
||||
@@ -201,15 +212,15 @@ class CourseDetailView(APIView):
|
||||
404: not_found_response,
|
||||
},
|
||||
)
|
||||
def delete(self, request, pk):
|
||||
course = get_object_or_404(Course, pk=pk)
|
||||
async def delete(self, request, pk):
|
||||
course = await aget_object_or_404(Course, pk=pk)
|
||||
if course.author != request.user:
|
||||
return create_standardized_error_response(
|
||||
code=ResponseCode.VALIDATION_ERROR,
|
||||
message='无权操作',
|
||||
status_code=status.HTTP_403_FORBIDDEN
|
||||
)
|
||||
course.delete()
|
||||
await course.adelete()
|
||||
return create_standardized_response(code=ResponseCode.SUCCESS, message='课程删除成功', status_code=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
@@ -227,6 +238,12 @@ class ChapterListCreateView(generics.ListCreateAPIView):
|
||||
course_id = self.kwargs['course_id']
|
||||
return Chapter.objects.filter(course_id=course_id)
|
||||
|
||||
async def get(self, request, *args, **kwargs):
|
||||
return await self.list(request, *args, **kwargs)
|
||||
|
||||
async def post(self, request, *args, **kwargs):
|
||||
return await self.create(request, *args, **kwargs)
|
||||
|
||||
@swagger_auto_schema(
|
||||
tags=['学习'],
|
||||
operation_summary='获取章节列表',
|
||||
@@ -236,10 +253,11 @@ class ChapterListCreateView(generics.ListCreateAPIView):
|
||||
],
|
||||
responses={200: success_response, 404: not_found_response},
|
||||
)
|
||||
def list(self, request, *args, **kwargs):
|
||||
async def list(self, request, *args, **kwargs):
|
||||
queryset = self.get_queryset()
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
return create_standardized_response(data=serializer.data, code=ResponseCode.SUCCESS)
|
||||
data = await get_data(serializer)
|
||||
return create_standardized_response(data=data, code=ResponseCode.SUCCESS)
|
||||
|
||||
@swagger_auto_schema(
|
||||
tags=['学习'],
|
||||
@@ -256,14 +274,15 @@ class ChapterListCreateView(generics.ListCreateAPIView):
|
||||
404: not_found_response,
|
||||
},
|
||||
)
|
||||
def create(self, request, *args, **kwargs):
|
||||
async def create(self, request, *args, **kwargs):
|
||||
course_id = self.kwargs['course_id']
|
||||
course = get_object_or_404(Course, pk=course_id)
|
||||
course = await aget_object_or_404(Course, pk=course_id)
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
serializer.save(course=course)
|
||||
if await sync_to_async(serializer.is_valid)():
|
||||
await sync_to_async(serializer.save)(course=course)
|
||||
data = await get_data(serializer)
|
||||
return create_standardized_response(
|
||||
data=serializer.data,
|
||||
data=data,
|
||||
code=ResponseCode.SUCCESS,
|
||||
message='章节创建成功',
|
||||
status_code=status.HTTP_201_CREATED
|
||||
@@ -293,10 +312,11 @@ class ChapterDetailView(APIView):
|
||||
404: not_found_response,
|
||||
},
|
||||
)
|
||||
def get(self, request, pk):
|
||||
chapter = get_object_or_404(Chapter, pk=pk)
|
||||
async def get(self, request, pk):
|
||||
chapter = await aget_object_or_404(Chapter, pk=pk)
|
||||
serializer = ChapterSerializer(chapter)
|
||||
return create_standardized_response(data=serializer.data, code=ResponseCode.SUCCESS)
|
||||
data = await get_data(serializer)
|
||||
return create_standardized_response(data=data, code=ResponseCode.SUCCESS)
|
||||
|
||||
@swagger_auto_schema(
|
||||
tags=['学习'],
|
||||
@@ -313,12 +333,13 @@ class ChapterDetailView(APIView):
|
||||
404: not_found_response,
|
||||
},
|
||||
)
|
||||
def put(self, request, pk):
|
||||
chapter = get_object_or_404(Chapter, pk=pk)
|
||||
async def put(self, request, pk):
|
||||
chapter = await aget_object_or_404(Chapter, pk=pk)
|
||||
serializer = ChapterSerializer(chapter, data=request.data, partial=True)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return create_standardized_response(data=serializer.data, code=ResponseCode.SUCCESS, message='章节更新成功')
|
||||
if await sync_to_async(serializer.is_valid)():
|
||||
await sync_to_async(serializer.save)()
|
||||
data = await get_data(serializer)
|
||||
return create_standardized_response(data=data, code=ResponseCode.SUCCESS, message='章节更新成功')
|
||||
return create_standardized_error_response(
|
||||
data=serializer.errors,
|
||||
code=ResponseCode.VALIDATION_ERROR,
|
||||
@@ -338,9 +359,9 @@ class ChapterDetailView(APIView):
|
||||
404: not_found_response,
|
||||
},
|
||||
)
|
||||
def delete(self, request, pk):
|
||||
chapter = get_object_or_404(Chapter, pk=pk)
|
||||
chapter.delete()
|
||||
async def delete(self, request, pk):
|
||||
chapter = await aget_object_or_404(Chapter, pk=pk)
|
||||
await chapter.adelete()
|
||||
return create_standardized_response(code=ResponseCode.SUCCESS, message='章节删除成功', status_code=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
@@ -361,14 +382,15 @@ class ChapterContentView(APIView):
|
||||
404: not_found_response,
|
||||
},
|
||||
)
|
||||
def get(self, request, chapter_id):
|
||||
chapter = get_object_or_404(Chapter, pk=chapter_id)
|
||||
content, created = ChapterContent.objects.get_or_create(
|
||||
async def get(self, request, chapter_id):
|
||||
chapter = await aget_object_or_404(Chapter, pk=chapter_id)
|
||||
content, created = await ChapterContent.objects.aget_or_create(
|
||||
chapter=chapter,
|
||||
defaults={'content_md': '', 'content_html': ''}
|
||||
)
|
||||
serializer = ChapterContentSerializer(content)
|
||||
return create_standardized_response(data=serializer.data, code=ResponseCode.SUCCESS)
|
||||
data = await get_data(serializer)
|
||||
return create_standardized_response(data=data, code=ResponseCode.SUCCESS)
|
||||
|
||||
@swagger_auto_schema(
|
||||
tags=['学习'],
|
||||
@@ -385,16 +407,17 @@ class ChapterContentView(APIView):
|
||||
404: not_found_response,
|
||||
},
|
||||
)
|
||||
def put(self, request, chapter_id):
|
||||
chapter = get_object_or_404(Chapter, pk=chapter_id)
|
||||
content, created = ChapterContent.objects.get_or_create(
|
||||
async def put(self, request, chapter_id):
|
||||
chapter = await aget_object_or_404(Chapter, pk=chapter_id)
|
||||
content, created = await ChapterContent.objects.aget_or_create(
|
||||
chapter=chapter,
|
||||
defaults={'content_md': '', 'content_html': ''}
|
||||
)
|
||||
serializer = ChapterContentSerializer(content, data=request.data, partial=True)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return create_standardized_response(data=serializer.data, code=ResponseCode.SUCCESS, message='内容保存成功')
|
||||
if await sync_to_async(serializer.is_valid)():
|
||||
await sync_to_async(serializer.save)()
|
||||
data = await get_data(serializer)
|
||||
return create_standardized_response(data=data, code=ResponseCode.SUCCESS, message='内容保存成功')
|
||||
return create_standardized_error_response(
|
||||
data=serializer.errors,
|
||||
code=ResponseCode.VALIDATION_ERROR,
|
||||
@@ -414,6 +437,9 @@ class MyCourseListView(generics.ListAPIView):
|
||||
qs = qs.filter(status=st)
|
||||
return qs.order_by('-updated_at')
|
||||
|
||||
async def get(self, request, *args, **kwargs):
|
||||
return await self.list(request, *args, **kwargs)
|
||||
|
||||
@swagger_auto_schema(
|
||||
tags=['学习'],
|
||||
operation_summary='获取我的课程列表',
|
||||
@@ -426,10 +452,11 @@ class MyCourseListView(generics.ListAPIView):
|
||||
401: unauthorized_response,
|
||||
},
|
||||
)
|
||||
def list(self, request, *args, **kwargs):
|
||||
async def list(self, request, *args, **kwargs):
|
||||
queryset = self.get_queryset()
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
return create_standardized_response(data=serializer.data, code=ResponseCode.SUCCESS)
|
||||
data = await get_data(serializer)
|
||||
return create_standardized_response(data=data, code=ResponseCode.SUCCESS)
|
||||
|
||||
|
||||
class MyCourseBatchView(APIView):
|
||||
@@ -454,7 +481,7 @@ class MyCourseBatchView(APIView):
|
||||
401: unauthorized_response,
|
||||
},
|
||||
)
|
||||
def post(self, request):
|
||||
async def post(self, request):
|
||||
ids = request.data.get('ids', [])
|
||||
action = request.data.get('action', '')
|
||||
if not ids or action not in ('publish', 'draft', 'delete'):
|
||||
@@ -465,11 +492,11 @@ class MyCourseBatchView(APIView):
|
||||
)
|
||||
qs = Course.objects.filter(id__in=ids, author=request.user)
|
||||
if action == 'delete':
|
||||
count = qs.delete()[0]
|
||||
count = (await qs.adelete())[0]
|
||||
elif action == 'publish':
|
||||
count = qs.update(status='published')
|
||||
count = await qs.aupdate(status='published')
|
||||
elif action == 'draft':
|
||||
count = qs.update(status='draft')
|
||||
count = await qs.aupdate(status='draft')
|
||||
return create_standardized_response(
|
||||
data={'affected': count},
|
||||
code=ResponseCode.SUCCESS,
|
||||
@@ -493,21 +520,26 @@ class CDNStaticFileView(APIView):
|
||||
404: not_found_response,
|
||||
},
|
||||
)
|
||||
def get(self, request, course_id, chapter_id):
|
||||
async def get(self, request, course_id, chapter_id):
|
||||
file_path = os.path.join(
|
||||
settings.MEDIA_ROOT,
|
||||
'learn', 'courses', str(course_id), 'chapters', f'{chapter_id}.md'
|
||||
)
|
||||
if not os.path.exists(file_path):
|
||||
raise Http404
|
||||
fh = open(file_path, "rb")
|
||||
try:
|
||||
response = FileResponse(fh, content_type="text/markdown; charset=utf-8")
|
||||
response["Cache-Control"] = "max-age=86400"
|
||||
return response
|
||||
except Exception:
|
||||
fh.close()
|
||||
raise
|
||||
|
||||
def _open_file():
|
||||
if not os.path.exists(file_path):
|
||||
raise Http404
|
||||
fh = open(file_path, "rb")
|
||||
try:
|
||||
response = FileResponse(fh, content_type="text/markdown; charset=utf-8")
|
||||
response["Cache-Control"] = "max-age=86400"
|
||||
return response
|
||||
except Exception:
|
||||
fh.close()
|
||||
raise
|
||||
# TODO: aiohttp 化(当前用 sync_to_async 兜底避免阻塞事件循环)
|
||||
return await sync_to_async(_open_file)()
|
||||
|
||||
|
||||
class CourseFavoriteToggleView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
@@ -525,22 +557,22 @@ class CourseFavoriteToggleView(APIView):
|
||||
404: not_found_response,
|
||||
},
|
||||
)
|
||||
def post(self, request, pk):
|
||||
course = Course.objects.filter(pk=pk, status='published').first()
|
||||
async def post(self, request, pk):
|
||||
course = await Course.objects.filter(pk=pk, status='published').afirst()
|
||||
if not course:
|
||||
return create_standardized_error_response(
|
||||
code=ResponseCode.VALIDATION_ERROR,
|
||||
message='课程不存在',
|
||||
status_code=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
fav, created = CourseFavorite.objects.get_or_create(user=request.user, course=course)
|
||||
fav, created = await CourseFavorite.objects.aget_or_create(user=request.user, course=course)
|
||||
if not created:
|
||||
fav.delete()
|
||||
count = CourseFavorite.objects.filter(course=course).count()
|
||||
await fav.adelete()
|
||||
count = await CourseFavorite.objects.filter(course=course).acount()
|
||||
return create_standardized_response(
|
||||
data={'favorited': False, 'favorites_count': count}
|
||||
)
|
||||
count = CourseFavorite.objects.filter(course=course).count()
|
||||
count = await CourseFavorite.objects.filter(course=course).acount()
|
||||
return create_standardized_response(
|
||||
data={'favorited': True, 'favorites_count': count}
|
||||
)
|
||||
@@ -568,8 +600,8 @@ class ChapterMarkCompletedView(APIView):
|
||||
404: not_found_response,
|
||||
},
|
||||
)
|
||||
def post(self, request, pk):
|
||||
chapter = Chapter.objects.filter(pk=pk).select_related('course').first()
|
||||
async def post(self, request, pk):
|
||||
chapter = await Chapter.objects.filter(pk=pk).select_related('course').afirst()
|
||||
if not chapter:
|
||||
return create_standardized_error_response(
|
||||
code=ResponseCode.VALIDATION_ERROR,
|
||||
@@ -580,7 +612,7 @@ class ChapterMarkCompletedView(APIView):
|
||||
if not isinstance(completed, bool):
|
||||
completed = str(completed).lower() in ('true', '1', 'yes')
|
||||
|
||||
read_record, created = ChapterRead.objects.get_or_create(
|
||||
read_record, created = await ChapterRead.objects.aget_or_create(
|
||||
user=request.user,
|
||||
chapter=chapter,
|
||||
defaults={
|
||||
@@ -592,12 +624,12 @@ class ChapterMarkCompletedView(APIView):
|
||||
if not created:
|
||||
read_record.completed = completed
|
||||
read_record.completed_at = timezone.now() if completed else None
|
||||
read_record.save(update_fields=['completed', 'completed_at', 'updated_at'])
|
||||
await read_record.asave(update_fields=['completed', 'completed_at', 'updated_at'])
|
||||
|
||||
total_chapters = chapter.course.chapters.count()
|
||||
completed_chapters = ChapterRead.objects.filter(
|
||||
total_chapters = await chapter.course.chapters.acount()
|
||||
completed_chapters = await ChapterRead.objects.filter(
|
||||
user=request.user, course=chapter.course, completed=True
|
||||
).count()
|
||||
).acount()
|
||||
progress = round((completed_chapters / total_chapters) * 100) if total_chapters > 0 else 0
|
||||
|
||||
return create_standardized_response(
|
||||
@@ -627,20 +659,20 @@ class CourseProgressView(APIView):
|
||||
404: not_found_response,
|
||||
},
|
||||
)
|
||||
def get(self, request, course_id):
|
||||
course = Course.objects.filter(pk=course_id, status='published').first()
|
||||
async def get(self, request, course_id):
|
||||
course = await Course.objects.filter(pk=course_id, status='published').afirst()
|
||||
if not course:
|
||||
return create_standardized_error_response(
|
||||
code=ResponseCode.VALIDATION_ERROR,
|
||||
message='课程不存在',
|
||||
status_code=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
completed_ids = set(
|
||||
ChapterRead.objects.filter(
|
||||
completed_ids = set([
|
||||
v async for v in ChapterRead.objects.filter(
|
||||
user=request.user, course=course, completed=True
|
||||
).values_list('chapter_id', flat=True)
|
||||
)
|
||||
total_chapters = course.chapters.count()
|
||||
])
|
||||
total_chapters = await course.chapters.acount()
|
||||
completed_count = len(completed_ids)
|
||||
progress = round((completed_count / total_chapters) * 100) if total_chapters > 0 else 0
|
||||
|
||||
@@ -667,7 +699,7 @@ class MaterialListView(APIView):
|
||||
],
|
||||
responses={200: success_response},
|
||||
)
|
||||
def get(self, request):
|
||||
async def get(self, request):
|
||||
chapter_id = request.query_params.get('chapter_id')
|
||||
if not chapter_id:
|
||||
return create_standardized_error_response(
|
||||
@@ -677,7 +709,8 @@ class MaterialListView(APIView):
|
||||
)
|
||||
materials = CourseMaterial.objects.filter(chapter_id=chapter_id)
|
||||
serializer = MaterialSerializer(materials, many=True, context={'request': request})
|
||||
return create_standardized_response(data=serializer.data, code=ResponseCode.SUCCESS)
|
||||
data = await get_data(serializer)
|
||||
return create_standardized_response(data=data, code=ResponseCode.SUCCESS)
|
||||
|
||||
|
||||
class MaterialDownloadView(APIView):
|
||||
@@ -692,12 +725,12 @@ class MaterialDownloadView(APIView):
|
||||
],
|
||||
responses={200: openapi.Response(description='文件流'), 404: not_found_response},
|
||||
)
|
||||
def get(self, request, pk):
|
||||
material = CourseMaterial.objects.filter(pk=pk).first()
|
||||
async def get(self, request, pk):
|
||||
material = await CourseMaterial.objects.filter(pk=pk).afirst()
|
||||
if not material or not material.file:
|
||||
raise Http404
|
||||
material.download_count += 1
|
||||
material.save(update_fields=['download_count'])
|
||||
await material.asave(update_fields=['download_count'])
|
||||
response = FileResponse(material.file.open('rb'), content_type='application/octet-stream')
|
||||
response['Content-Disposition'] = f'attachment; filename="{material.title}"'
|
||||
return response
|
||||
@@ -715,8 +748,8 @@ class ChapterVideoDownloadView(APIView):
|
||||
],
|
||||
responses={200: openapi.Response(description='文件流'), 404: not_found_response},
|
||||
)
|
||||
def get(self, request, pk):
|
||||
chapter = Chapter.objects.filter(pk=pk).first()
|
||||
async def get(self, request, pk):
|
||||
chapter = await Chapter.objects.filter(pk=pk).afirst()
|
||||
if not chapter or not chapter.video_local:
|
||||
raise Http404
|
||||
response = FileResponse(chapter.video_local.open('rb'), content_type='application/octet-stream')
|
||||
@@ -733,11 +766,11 @@ class MyProgressView(APIView):
|
||||
operation_description='获取当前用户所有课程的学习进度列表,需要登录',
|
||||
responses={200: success_response, 401: unauthorized_response},
|
||||
)
|
||||
def get(self, request):
|
||||
async def get(self, request):
|
||||
user = request.user
|
||||
reads = ChapterRead.objects.filter(user=user, completed=True).select_related('course', 'chapter')
|
||||
course_map = {}
|
||||
for read in reads:
|
||||
async for read in reads:
|
||||
course_id = read.course.id
|
||||
if course_id not in course_map:
|
||||
course_map[course_id] = {
|
||||
@@ -745,7 +778,7 @@ class MyProgressView(APIView):
|
||||
'title': read.course.title,
|
||||
'cover_image': read.course.cover_image.url if read.course.cover_image else '',
|
||||
'completed_chapters': 0,
|
||||
'total_chapters': read.course.chapters.count(),
|
||||
'total_chapters': await read.course.chapters.acount(),
|
||||
'last_studied_at': read.completed_at,
|
||||
}
|
||||
course_map[course_id]['completed_chapters'] += 1
|
||||
@@ -772,4 +805,4 @@ class MyProgressView(APIView):
|
||||
'total_courses': len(courses),
|
||||
'total_completed': total_completed,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user