166 lines
8.6 KiB
Python
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})
|