Files
medical_training/apps/user/cms_relation.py
T

166 lines
8.6 KiB
Python

"""CMS 师生关系管理(CMS-REL-1~4)—— 医院管理员(本院)/ 超管(全平台)。"""
from rest_framework import viewsets, filters, status, serializers
from rest_framework.decorators import action
from rest_framework.parsers import MultiPartParser, FormParser
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from django_filters.rest_framework import DjangoFilterBackend
from drf_spectacular.utils import extend_schema, extend_schema_view
from config.exceptions import AppError
from apps.cms.permissions import IsSuperOrHospitalAdmin, is_super
from apps.common.excel import xlsx_response, rows_from_xlsx
from .models import User, TeacherStudentRelation
REL_STATUS_LABEL = {0: '已结束', 1: '进行中'}
REL_IMPORT_HEADERS = ['带教医生手机号', '学生手机号']
REL_EXPORT_HEADERS = ['ID', '带教医生', '带教医生手机号', '学生', '学生手机号', '状态']
class CmsRelationSerializer(serializers.ModelSerializer):
"""读取(列表 / 详情)。"""
teacher_name = serializers.CharField(source='teacher.real_name', read_only=True, default=None)
teacher_phone = serializers.CharField(source='teacher.phone', read_only=True, default=None)
student_name = serializers.CharField(source='student.real_name', read_only=True, default=None)
student_phone = serializers.CharField(source='student.phone', read_only=True, default=None)
class Meta:
model = TeacherStudentRelation
fields = [
'id', 'teacher', 'teacher_name', 'teacher_phone',
'student', 'student_name', 'student_phone',
'relation_type', 'status', 'created_at',
]
class CmsRelationWriteSerializer(serializers.ModelSerializer):
"""新增 / 编辑:teacher 必须是医生、student 必须是学生;医院管理员限本院。"""
teacher = serializers.PrimaryKeyRelatedField(queryset=User.objects.filter(role_type='doctor'))
student = serializers.PrimaryKeyRelatedField(queryset=User.objects.filter(role_type='student'))
class Meta:
model = TeacherStudentRelation
fields = ['teacher', 'student', 'relation_type', 'status']
def validate(self, attrs):
actor = self.context['request'].user
teacher = attrs.get('teacher') or (self.instance.teacher if self.instance else None)
student = attrs.get('student') or (self.instance.student if self.instance else None)
if teacher is None or student is None:
raise AppError('CMS_VALIDATION_ERROR', '带教医生和学生均必填', status_code=400)
if not is_super(actor):
if not actor.institution_id:
raise AppError('CMS_NO_INSTITUTION', '当前医院管理员无所属机构', status_code=403)
if teacher.institution_id != actor.institution_id:
raise AppError('CMS_REL_SCOPE_FORBIDDEN', '带教医生不属于本院', status_code=403)
if student.institution_id != actor.institution_id:
raise AppError('CMS_REL_SCOPE_FORBIDDEN', '学生不属于本院', status_code=403)
dup = TeacherStudentRelation.objects.filter(teacher=teacher, student=student)
if self.instance is not None:
dup = dup.exclude(pk=self.instance.pk)
if dup.exists():
raise AppError('CMS_REL_EXISTS', '该师生关系已存在', status_code=400)
return attrs
@extend_schema_view(
list=extend_schema(summary='CMS-REL-1 师生关系列表', tags=['CMS-师生关系']),
create=extend_schema(summary='CMS-REL-2 新增师生关系', tags=['CMS-师生关系']),
retrieve=extend_schema(summary='师生关系详情', tags=['CMS-师生关系']),
partial_update=extend_schema(summary='CMS-REL-2 编辑师生关系', tags=['CMS-师生关系']),
destroy=extend_schema(summary='CMS-REL-3 停用师生关系(逻辑删除)', tags=['CMS-师生关系']),
)
class CmsTeacherStudentRelationViewSet(viewsets.ModelViewSet):
"""CMS 师生关系管理。超管全平台、医院管理员仅本院。停用为逻辑删除。"""
permission_classes = [IsAuthenticated, IsSuperOrHospitalAdmin]
filter_backends = [DjangoFilterBackend, filters.SearchFilter]
filterset_fields = ['teacher', 'student', 'status']
search_fields = ['teacher__real_name', 'teacher__phone', 'student__real_name', 'student__phone']
http_method_names = ['get', 'post', 'patch', 'delete', 'head', 'options']
def get_queryset(self):
qs = TeacherStudentRelation.objects.select_related('teacher', 'student').all().order_by('-created_at')
user = self.request.user
if is_super(user):
return qs
return qs.filter(teacher__institution_id=user.institution_id)
def get_serializer_class(self):
if self.action in ('create', 'update', 'partial_update'):
return CmsRelationWriteSerializer
return CmsRelationSerializer
def create(self, request, *args, **kwargs):
write = self.get_serializer(data=request.data)
write.is_valid(raise_exception=True)
rel = write.save()
return Response(CmsRelationSerializer(rel).data, status=status.HTTP_201_CREATED)
def update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False)
instance = self.get_object()
write = self.get_serializer(instance, data=request.data, partial=partial)
write.is_valid(raise_exception=True)
rel = write.save()
return Response(CmsRelationSerializer(rel).data)
@extend_schema(summary='CMS-REL-4 下载师生关系导入模板', tags=['CMS-师生关系'])
@action(detail=False, methods=['get'], url_path='import-template')
def import_template(self, request):
return xlsx_response('师生关系导入模板.xlsx', REL_IMPORT_HEADERS, [])
@extend_schema(summary='CMS-REL-4 导出师生关系', tags=['CMS-师生关系'])
@action(detail=False, methods=['get'], url_path='export')
def export(self, request):
qs = self.filter_queryset(self.get_queryset())
rows = [
[r.id,
r.teacher.real_name if r.teacher_id else '', r.teacher.phone if r.teacher_id else '',
r.student.real_name if r.student_id else '', r.student.phone if r.student_id else '',
REL_STATUS_LABEL.get(r.status, r.status)]
for r in qs
]
return xlsx_response('师生关系列表.xlsx', REL_EXPORT_HEADERS, rows)
@extend_schema(summary='CMS-REL-4 导入师生关系', tags=['CMS-师生关系'])
@action(detail=False, methods=['post'], url_path='import',
parser_classes=[MultiPartParser, FormParser])
def import_relations(self, request):
"""Excel 批量导入师生关系。列:带教医生手机号 | 学生手机号。"""
actor = request.user
actor_is_super = is_super(actor)
if not actor_is_super and not actor.institution_id:
raise AppError('CMS_NO_INSTITUTION', '当前医院管理员无所属机构,无法导入', status_code=403)
file = request.FILES.get('file')
if not file:
raise AppError('CMS_IMPORT_FILE_REQUIRED', '请上传 .xlsx 文件(字段名 file)', status_code=400)
try:
rows = rows_from_xlsx(file)
except Exception:
raise AppError('CMS_IMPORT_BAD_FILE', '文件解析失败,请使用导入模板', status_code=400)
success, errors = 0, []
for idx, row in enumerate(rows, start=2):
t_phone = (row.get('带教医生手机号') or '').strip()
s_phone = (row.get('学生手机号') or '').strip()
teacher = User.objects.filter(phone=t_phone, role_type='doctor').first()
if teacher is None:
errors.append({'row': idx, 'reason': f'带教医生不存在或非医生:{t_phone}'}); continue
student = User.objects.filter(phone=s_phone, role_type='student').first()
if student is None:
errors.append({'row': idx, 'reason': f'学生不存在或非学生:{s_phone}'}); continue
if not actor_is_super and (
teacher.institution_id != actor.institution_id
or student.institution_id != actor.institution_id
):
errors.append({'row': idx, 'reason': '师生不属于本院'}); continue
if TeacherStudentRelation.objects.filter(teacher=teacher, student=student).exists():
errors.append({'row': idx, 'reason': '师生关系已存在'}); continue
TeacherStudentRelation.objects.create(teacher=teacher, student=student, status=1)
success += 1
return Response({'total': len(rows), 'success': success, 'failed': len(errors), 'errors': errors})