文章列表新增author_user_id/is_following与feed=following过滤;课程students_count改真实在学人数去重统计、新增total_minutes(不可解析不伪造);author_name走sync_to_async防ASGI同步阻塞。
270 lines
8.7 KiB
Python
270 lines
8.7 KiB
Python
from adrf.serializers import (
|
|
ModelSerializer, SerializerMethodField,
|
|
)
|
|
from asgiref.sync import sync_to_async
|
|
|
|
from .models import Course, Chapter, ChapterContent, CourseMaterial
|
|
|
|
|
|
def _author_display(obj):
|
|
"""同步取作者展示名(跑在 sync_to_async 线程里,避免 ASGI 下 SynchronousOnlyOperation)。"""
|
|
try:
|
|
author = obj.author
|
|
except Exception:
|
|
return ''
|
|
if author is None:
|
|
return ''
|
|
try:
|
|
full = author.get_full_name()
|
|
except Exception:
|
|
full = ''
|
|
try:
|
|
username = author.username
|
|
except Exception:
|
|
username = ''
|
|
return full or username or ''
|
|
|
|
|
|
def parse_duration_minutes(raw):
|
|
"""把章节 duration 字符串解析为分钟数;解析不出返回 None。
|
|
|
|
存量格式为中文字符串(如 '30分钟'),纯数字视为分钟。
|
|
total_minutes 只对可解析章节求和,不可解析章节不计入(不伪造)。
|
|
"""
|
|
import re
|
|
if raw is None:
|
|
return None
|
|
s = str(raw).strip()
|
|
if not s:
|
|
return None
|
|
m = re.search(r'(\d+(?:\.\d+)?)', s)
|
|
if not m:
|
|
return None
|
|
try:
|
|
return int(float(m.group(1)))
|
|
except (ValueError, OverflowError):
|
|
return None
|
|
|
|
|
|
def _course_total_minutes(obj):
|
|
"""同步聚合课程总分钟数(跑在 sync_to_async 线程里)。"""
|
|
try:
|
|
durations = list(obj.chapters.values_list('duration', flat=True))
|
|
except Exception:
|
|
return None
|
|
total = 0
|
|
any_parsed = False
|
|
for d in durations:
|
|
m = parse_duration_minutes(d)
|
|
if m is not None:
|
|
total += m
|
|
any_parsed = True
|
|
return total if any_parsed else None
|
|
|
|
|
|
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']
|
|
|
|
async def get_video_poster_url(self, obj):
|
|
if obj.video_poster:
|
|
request = self.context.get('request')
|
|
if request:
|
|
return request.build_absolute_uri(obj.video_poster.url)
|
|
return obj.video_poster.url
|
|
return ''
|
|
|
|
async def get_video_local_url(self, obj):
|
|
if obj.video_local:
|
|
request = self.context.get('request')
|
|
if request:
|
|
return request.build_absolute_uri(obj.video_local.url)
|
|
return obj.video_local.url
|
|
return ''
|
|
|
|
|
|
class ChapterContentSerializer(ModelSerializer):
|
|
class Meta:
|
|
model = ChapterContent
|
|
fields = ['id', 'chapter', 'content_md', 'content_html', 'md_file_path', 'updated_at']
|
|
|
|
|
|
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']
|
|
|
|
async def get_file_url(self, obj):
|
|
if obj.file:
|
|
request = self.context.get('request')
|
|
if request:
|
|
return request.build_absolute_uri(obj.file.url)
|
|
return obj.file.url
|
|
return ''
|
|
|
|
async def get_file_size_display(self, obj):
|
|
size = obj.file_size
|
|
if size < 1024:
|
|
return f'{size} B'
|
|
elif size < 1024 * 1024:
|
|
return f'{size / 1024:.1f} KB'
|
|
else:
|
|
return f'{size / (1024 * 1024):.1f} MB'
|
|
|
|
async def get_file_type_display(self, obj):
|
|
type_map = {
|
|
'pdf': 'PDF',
|
|
'code': '代码',
|
|
'doc': '文档',
|
|
'video': '视频',
|
|
'dataset': '数据集',
|
|
'zip': '压缩包',
|
|
'other': '其他',
|
|
}
|
|
return type_map.get(obj.file_type, '其他')
|
|
|
|
|
|
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']
|
|
|
|
async def get_video_poster_url(self, obj):
|
|
if obj.video_poster:
|
|
request = self.context.get('request')
|
|
if request:
|
|
return request.build_absolute_uri(obj.video_poster.url)
|
|
return obj.video_poster.url
|
|
return ''
|
|
|
|
|
|
class ChapterContentSerializer(ModelSerializer):
|
|
class Meta:
|
|
model = ChapterContent
|
|
fields = ['id', 'chapter', 'content_md', 'content_html', 'md_file_path', 'updated_at']
|
|
|
|
|
|
class CourseListSerializer(ModelSerializer):
|
|
author_name = SerializerMethodField()
|
|
chapters_count = SerializerMethodField()
|
|
students_count = SerializerMethodField()
|
|
total_minutes = SerializerMethodField()
|
|
cover_image_url = SerializerMethodField()
|
|
|
|
class Meta:
|
|
model = Course
|
|
fields = [
|
|
'id', 'title', 'description', 'category', 'level',
|
|
'cover_image_url', 'color', 'icon_name', 'author_name',
|
|
'chapters_count', 'students_count', 'total_minutes', 'status',
|
|
'sort_order', 'is_hot', 'is_new', 'created_at', 'updated_at',
|
|
]
|
|
|
|
async def get_chapters_count(self, obj):
|
|
return await obj.chapters.acount()
|
|
|
|
async def get_students_count(self, obj):
|
|
# 从章节阅读记录去重统计真实在学人数
|
|
from user.models import FUser
|
|
return await FUser.objects.filter(
|
|
chapter_reads__course=obj
|
|
).distinct().acount()
|
|
|
|
async def get_total_minutes(self, obj):
|
|
# 可解析章节 duration 求和;全不可解析返回 None(前端不显示,不伪造)
|
|
return await sync_to_async(_course_total_minutes)(obj)
|
|
|
|
async def get_cover_image_url(self, obj):
|
|
if obj.cover_image:
|
|
request = self.context.get('request')
|
|
if request:
|
|
return request.build_absolute_uri(obj.cover_image.url)
|
|
return obj.cover_image.url
|
|
return ''
|
|
|
|
async def get_author_name(self, obj):
|
|
return await sync_to_async(_author_display)(obj)
|
|
|
|
|
|
class CourseDetailSerializer(ModelSerializer):
|
|
author_name = SerializerMethodField()
|
|
chapters = ChapterSerializer(many=True, read_only=True)
|
|
chapters_count = SerializerMethodField()
|
|
students_count = SerializerMethodField()
|
|
total_minutes = SerializerMethodField()
|
|
cover_image_url = SerializerMethodField()
|
|
|
|
class Meta:
|
|
model = Course
|
|
fields = [
|
|
'id', 'title', 'description', 'category', 'level',
|
|
'cover_image_url', 'color', 'icon_name', 'author_name',
|
|
'chapters', 'chapters_count', 'students_count', 'total_minutes', 'status',
|
|
'sort_order', 'is_hot', 'is_new', 'created_at', 'updated_at',
|
|
]
|
|
|
|
async def get_chapters_count(self, obj):
|
|
return await obj.chapters.acount()
|
|
|
|
async def get_students_count(self, obj):
|
|
# 从章节阅读记录去重统计真实在学人数
|
|
from user.models import FUser
|
|
return await FUser.objects.filter(
|
|
chapter_reads__course=obj
|
|
).distinct().acount()
|
|
|
|
async def get_total_minutes(self, obj):
|
|
return await sync_to_async(_course_total_minutes)(obj)
|
|
|
|
async def get_cover_image_url(self, obj):
|
|
if obj.cover_image:
|
|
request = self.context.get('request')
|
|
if request:
|
|
return request.build_absolute_uri(obj.cover_image.url)
|
|
return obj.cover_image.url
|
|
return ''
|
|
|
|
async def get_author_name(self, obj):
|
|
return await sync_to_async(_author_display)(obj)
|
|
|
|
|
|
class CourseCreateUpdateSerializer(ModelSerializer):
|
|
class Meta:
|
|
model = Course
|
|
fields = ['title', 'description', 'category', 'level', 'cover_image', 'color', 'icon_name', 'status', 'sort_order', 'is_hot', 'is_new']
|
|
|
|
async def acreate(self, validated_data):
|
|
validated_data['author'] = self.context['request'].user
|
|
return await super().acreate(validated_data)
|
|
|
|
|
|
class CourseManageSerializer(ModelSerializer):
|
|
author_name = SerializerMethodField()
|
|
chapters_count = SerializerMethodField()
|
|
|
|
class Meta:
|
|
model = Course
|
|
fields = [
|
|
'id', 'title', 'category', 'level', 'status',
|
|
'sort_order', 'is_hot', 'is_new', 'chapters_count',
|
|
'author_name', 'created_at', 'updated_at',
|
|
]
|
|
|
|
async def get_chapters_count(self, obj):
|
|
return await obj.chapters.acount()
|
|
|
|
async def get_author_name(self, obj):
|
|
if obj.author:
|
|
return obj.author.get_full_name() or obj.author.username
|
|
return ''
|