Files
medical_training/apps/user/cms_relation.py
T

176 lines
9.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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']
# 仅 GET / POST:查=GET,增删改=POST(编辑→{id}/update/,停用→{id}/disable/
http_method_names = ['get', 'post', '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_relation'):
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)
@extend_schema(summary='CMS-REL-3 编辑师生关系', tags=['CMS-师生关系'])
@action(detail=True, methods=['post'], url_path='update')
def update_relation(self, request, pk=None):
"""编辑师生关系(POST 局部更新,等价旧 PATCH /{id}/)。"""
instance = self.get_object()
write = self.get_serializer(instance, data=request.data, partial=True)
write.is_valid(raise_exception=True)
rel = write.save()
return Response(CmsRelationSerializer(rel).data)
@extend_schema(summary='CMS-REL-4 停用师生关系(逻辑删除)', tags=['CMS-师生关系'])
@action(detail=True, methods=['post'], url_path='disable')
def disable(self, request, pk=None):
"""停用师生关系(软删除,等价旧 DELETE /{id}/)。"""
self.get_object().delete()
return Response({'message': '已停用'})
@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})