feat:cms relation hospital bug fix
This commit is contained in:
@@ -9,10 +9,15 @@ class CmsInstitutionSerializer(serializers.ModelSerializer):
|
|||||||
"""CMS 机构(医院)序列化器。
|
"""CMS 机构(医院)序列化器。
|
||||||
|
|
||||||
- `code` 显式声明以去掉默认 UniqueValidator,改用自定义错误码校验唯一性。
|
- `code` 显式声明以去掉默认 UniqueValidator,改用自定义错误码校验唯一性。
|
||||||
- `banner_url` 返回可访问的完整 URL(未配置时为空串,便于管理端识别「未设图」)。
|
- `banner_url` **可写**(新增/编辑可传,可选):入参可为静态相对路径(如
|
||||||
|
`institutions/xxx.png`)或完整 `http(s)` URL;**输出**统一转为可访问的完整 URL
|
||||||
|
(未配置时为空串,便于管理端识别「未设图」)。传空串则清空。
|
||||||
"""
|
"""
|
||||||
code = serializers.CharField(max_length=100)
|
code = serializers.CharField(max_length=100)
|
||||||
banner_url = serializers.SerializerMethodField()
|
banner_url = serializers.CharField(
|
||||||
|
max_length=500, required=False, allow_blank=True,
|
||||||
|
help_text='机构 Banner 图:相对静态路径或完整 http(s) URL;可选,传空串清空',
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Institution
|
model = Institution
|
||||||
@@ -22,7 +27,12 @@ class CmsInstitutionSerializer(serializers.ModelSerializer):
|
|||||||
]
|
]
|
||||||
read_only_fields = ['id', 'created_at', 'updated_at']
|
read_only_fields = ['id', 'created_at', 'updated_at']
|
||||||
|
|
||||||
def get_banner_url(self, obj):
|
def to_representation(self, instance):
|
||||||
|
data = super().to_representation(instance)
|
||||||
|
data['banner_url'] = self._full_banner_url(instance)
|
||||||
|
return data
|
||||||
|
|
||||||
|
def _full_banner_url(self, obj):
|
||||||
if not obj.banner_url:
|
if not obj.banner_url:
|
||||||
return ''
|
return ''
|
||||||
value = obj.banner_url
|
value = obj.banner_url
|
||||||
@@ -33,6 +43,9 @@ class CmsInstitutionSerializer(serializers.ModelSerializer):
|
|||||||
path = prefix + '/' + settings.STATIC_URL.strip('/') + '/' + value.lstrip('/')
|
path = prefix + '/' + settings.STATIC_URL.strip('/') + '/' + value.lstrip('/')
|
||||||
return request.build_absolute_uri(path) if request else path
|
return request.build_absolute_uri(path) if request else path
|
||||||
|
|
||||||
|
def validate_banner_url(self, value):
|
||||||
|
return (value or '').strip()
|
||||||
|
|
||||||
def validate_code(self, value):
|
def validate_code(self, value):
|
||||||
value = (value or '').strip()
|
value = (value or '').strip()
|
||||||
if not value:
|
if not value:
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from drf_spectacular.utils import extend_schema, extend_schema_view
|
|||||||
|
|
||||||
from config.exceptions import AppError
|
from config.exceptions import AppError
|
||||||
from apps.user.models import Institution, Department
|
from apps.user.models import Institution, Department
|
||||||
from apps.cms.permissions import IsSuperAdmin
|
from apps.cms.permissions import IsSuperAdmin, IsSuperOrHospitalAdmin, is_super
|
||||||
from apps.common.excel import xlsx_response, rows_from_xlsx
|
from apps.common.excel import xlsx_response, rows_from_xlsx
|
||||||
from .serializers import CmsInstitutionSerializer, CmsDepartmentSerializer
|
from .serializers import CmsInstitutionSerializer, CmsDepartmentSerializer
|
||||||
|
|
||||||
@@ -50,11 +50,25 @@ class CmsInstitutionViewSet(viewsets.ModelViewSet):
|
|||||||
# 仅 GET / POST:查=GET,增删改=POST(编辑→{id}/update/,停用→{id}/disable/)
|
# 仅 GET / POST:查=GET,增删改=POST(编辑→{id}/update/,停用→{id}/disable/)
|
||||||
http_method_names = ['get', 'post', 'head', 'options']
|
http_method_names = ['get', 'post', 'head', 'options']
|
||||||
|
|
||||||
|
def get_permissions(self):
|
||||||
|
# 编辑机构 / 上传 Banner:超管 + 医院管理员(医院管理员仅限本院,在动作内收口);
|
||||||
|
# 其余动作(列表/新增/停用/导入导出等)仍仅超级管理员。
|
||||||
|
if getattr(self, 'action', None) in ('update_inst', 'banner'):
|
||||||
|
return [IsAuthenticated(), IsSuperOrHospitalAdmin()]
|
||||||
|
return [IsAuthenticated(), IsSuperAdmin()]
|
||||||
|
|
||||||
@extend_schema(summary='CMS-INST-3 编辑机构', tags=['CMS-机构'])
|
@extend_schema(summary='CMS-INST-3 编辑机构', tags=['CMS-机构'])
|
||||||
@action(detail=True, methods=['post'], url_path='update')
|
@action(detail=True, methods=['post'], url_path='update')
|
||||||
def update_inst(self, request, pk=None):
|
def update_inst(self, request, pk=None):
|
||||||
"""编辑机构(POST 局部更新,等价旧 PATCH /{id}/)。"""
|
"""编辑机构(POST 局部更新,等价旧 PATCH /{id}/)。
|
||||||
|
|
||||||
|
- 超级管理员:可编辑任意机构。
|
||||||
|
- 医院管理员:仅能编辑**本院**机构(非本院 → 403),可改除机构 ID 外的信息;
|
||||||
|
不能停用/删除机构(停用动作仍仅超管)。
|
||||||
|
"""
|
||||||
instance = self.get_object()
|
instance = self.get_object()
|
||||||
|
if not is_super(request.user) and instance.id != request.user.institution_id:
|
||||||
|
raise AppError('CMS_PERMISSION_DENIED', '医院管理员只能修改本机构信息', status_code=403)
|
||||||
serializer = self.get_serializer(instance, data=request.data, partial=True)
|
serializer = self.get_serializer(instance, data=request.data, partial=True)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
serializer.save()
|
serializer.save()
|
||||||
@@ -71,8 +85,14 @@ class CmsInstitutionViewSet(viewsets.ModelViewSet):
|
|||||||
@action(detail=True, methods=['post'], url_path='banner',
|
@action(detail=True, methods=['post'], url_path='banner',
|
||||||
parser_classes=[MultiPartParser, FormParser])
|
parser_classes=[MultiPartParser, FormParser])
|
||||||
def banner(self, request, pk=None):
|
def banner(self, request, pk=None):
|
||||||
"""上传机构 Banner 图(multipart/form-data,字段名 file)。"""
|
"""上传机构 Banner 图(multipart/form-data,字段名 file)。
|
||||||
|
|
||||||
|
- 超级管理员:可为任意机构上传。
|
||||||
|
- 医院管理员:仅可为**本院**机构上传(非本院 → 403)。
|
||||||
|
"""
|
||||||
inst = self.get_object()
|
inst = self.get_object()
|
||||||
|
if not is_super(request.user) and inst.id != request.user.institution_id:
|
||||||
|
raise AppError('CMS_PERMISSION_DENIED', '医院管理员只能上传本机构 Banner 图', status_code=403)
|
||||||
file = request.FILES.get('file')
|
file = request.FILES.get('file')
|
||||||
if not file:
|
if not file:
|
||||||
raise AppError('CMS_BANNER_FILE_REQUIRED', '请上传图片文件(字段名 file)', status_code=400)
|
raise AppError('CMS_BANNER_FILE_REQUIRED', '请上传图片文件(字段名 file)', status_code=400)
|
||||||
|
|||||||
+51
-18
@@ -13,7 +13,7 @@ from apps.common.excel import xlsx_response, rows_from_xlsx
|
|||||||
from .models import User, TeacherStudentRelation
|
from .models import User, TeacherStudentRelation
|
||||||
|
|
||||||
REL_STATUS_LABEL = {0: '已结束', 1: '进行中'}
|
REL_STATUS_LABEL = {0: '已结束', 1: '进行中'}
|
||||||
REL_IMPORT_HEADERS = ['带教医生手机号', '学生手机号']
|
REL_IMPORT_HEADERS = ['带教医生姓名', '带教医生手机号', '学生姓名', '学生手机号']
|
||||||
REL_EXPORT_HEADERS = ['ID', '带教医生', '带教医生手机号', '学生', '学生手机号', '状态']
|
REL_EXPORT_HEADERS = ['ID', '带教医生', '带教医生手机号', '学生', '学生手机号', '状态']
|
||||||
|
|
||||||
|
|
||||||
@@ -33,36 +33,48 @@ class CmsRelationSerializer(serializers.ModelSerializer):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_rel_user(name, phone, role, label, code):
|
||||||
|
"""按 姓名 + 手机号 + 角色 解析用户;不匹配抛 400。"""
|
||||||
|
u = User.objects.filter(phone=phone, real_name=name, role_type=role).first()
|
||||||
|
if u is None:
|
||||||
|
raise AppError(code, f'{label}不存在或姓名与手机号不符:{name} / {phone}', status_code=400)
|
||||||
|
return u
|
||||||
|
|
||||||
|
|
||||||
class CmsRelationWriteSerializer(serializers.ModelSerializer):
|
class CmsRelationWriteSerializer(serializers.ModelSerializer):
|
||||||
"""新增 / 编辑:入参用**带教老师手机号 / 学生手机号**;按手机号解析为用户
|
"""新增 / 编辑:入参用**带教老师姓名+手机号 / 学生姓名+手机号**;按姓名+手机号解析为用户
|
||||||
(带教老师须 doctor、学生须 student);医院管理员限本院。"""
|
(带教老师须 doctor、学生须 student);医院管理员限本院。"""
|
||||||
|
teacher_name = serializers.CharField(write_only=True, required=False, help_text='带教老师姓名(新增必填)')
|
||||||
teacher_phone = serializers.CharField(write_only=True, required=False, help_text='带教老师手机号(新增必填)')
|
teacher_phone = serializers.CharField(write_only=True, required=False, help_text='带教老师手机号(新增必填)')
|
||||||
|
student_name = serializers.CharField(write_only=True, required=False, help_text='学生姓名(新增必填)')
|
||||||
student_phone = serializers.CharField(write_only=True, required=False, help_text='学生手机号(新增必填)')
|
student_phone = serializers.CharField(write_only=True, required=False, help_text='学生手机号(新增必填)')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = TeacherStudentRelation
|
model = TeacherStudentRelation
|
||||||
fields = ['teacher_phone', 'student_phone', 'relation_type', 'status']
|
fields = ['teacher_name', 'teacher_phone', 'student_name', 'student_phone', 'relation_type', 'status']
|
||||||
|
|
||||||
def validate(self, attrs):
|
def validate(self, attrs):
|
||||||
actor = self.context['request'].user
|
actor = self.context['request'].user
|
||||||
creating = self.instance is None
|
creating = self.instance is None
|
||||||
|
t_name = (attrs.pop('teacher_name', None) or '').strip()
|
||||||
t_phone = (attrs.pop('teacher_phone', None) or '').strip()
|
t_phone = (attrs.pop('teacher_phone', None) or '').strip()
|
||||||
|
s_name = (attrs.pop('student_name', None) or '').strip()
|
||||||
s_phone = (attrs.pop('student_phone', None) or '').strip()
|
s_phone = (attrs.pop('student_phone', None) or '').strip()
|
||||||
|
|
||||||
if creating and (not t_phone or not s_phone):
|
if creating and not (t_name and t_phone and s_name and s_phone):
|
||||||
raise AppError('CMS_VALIDATION_ERROR', '带教老师手机号和学生手机号均必填', status_code=400)
|
raise AppError('CMS_VALIDATION_ERROR', '带教老师姓名/手机号、学生姓名/手机号均必填', status_code=400)
|
||||||
|
|
||||||
# 按手机号解析(带教老师须 doctor、学生须 student);编辑时未传则沿用原值
|
# 解析(编辑时该方未传任何字段则沿用原值;传了则姓名与手机号需成对)
|
||||||
teacher = self.instance.teacher if self.instance else None
|
teacher = self.instance.teacher if self.instance else None
|
||||||
|
if t_name or t_phone:
|
||||||
|
if not (t_name and t_phone):
|
||||||
|
raise AppError('CMS_VALIDATION_ERROR', '带教老师姓名与手机号需同时提供', status_code=400)
|
||||||
|
teacher = _resolve_rel_user(t_name, t_phone, 'doctor', '带教老师', 'CMS_REL_TEACHER_NOT_FOUND')
|
||||||
student = self.instance.student if self.instance else None
|
student = self.instance.student if self.instance else None
|
||||||
if t_phone:
|
if s_name or s_phone:
|
||||||
teacher = User.objects.filter(phone=t_phone, role_type='doctor').first()
|
if not (s_name and s_phone):
|
||||||
if teacher is None:
|
raise AppError('CMS_VALIDATION_ERROR', '学生姓名与手机号需同时提供', status_code=400)
|
||||||
raise AppError('CMS_REL_TEACHER_NOT_FOUND', f'带教老师不存在或非医生:{t_phone}', status_code=400)
|
student = _resolve_rel_user(s_name, s_phone, 'student', '学生', 'CMS_REL_STUDENT_NOT_FOUND')
|
||||||
if s_phone:
|
|
||||||
student = User.objects.filter(phone=s_phone, role_type='student').first()
|
|
||||||
if student is None:
|
|
||||||
raise AppError('CMS_REL_STUDENT_NOT_FOUND', f'学生不存在或非学生:{s_phone}', status_code=400)
|
|
||||||
if teacher is None or student is None:
|
if teacher is None or student is None:
|
||||||
raise AppError('CMS_VALIDATION_ERROR', '带教老师和学生均必填', status_code=400)
|
raise AppError('CMS_VALIDATION_ERROR', '带教老师和学生均必填', status_code=400)
|
||||||
|
|
||||||
@@ -136,6 +148,25 @@ class CmsTeacherStudentRelationViewSet(viewsets.ModelViewSet):
|
|||||||
self.get_object().delete()
|
self.get_object().delete()
|
||||||
return Response({'message': '已停用'})
|
return Response({'message': '已停用'})
|
||||||
|
|
||||||
|
# ── 下拉数据源(医院管理员本院 / 超管全平台;姓名+手机号一并返回)──────────
|
||||||
|
def _role_users(self, role):
|
||||||
|
qs = User.objects.filter(role_type=role).order_by('real_name', 'id')
|
||||||
|
if not is_super(self.request.user):
|
||||||
|
qs = qs.filter(institution_id=self.request.user.institution_id)
|
||||||
|
return [{'id': u.id, 'real_name': u.real_name, 'phone': u.phone} for u in qs]
|
||||||
|
|
||||||
|
@extend_schema(summary='CMS-REL-6 带教医生下拉(姓名+手机号)', tags=['CMS-师生关系'])
|
||||||
|
@action(detail=False, methods=['get'], url_path='doctors')
|
||||||
|
def doctors(self, request):
|
||||||
|
"""新增/编辑师生关系的「带教医生」下拉数据源,返回 [{id, real_name, phone}]。"""
|
||||||
|
return Response(self._role_users('doctor'))
|
||||||
|
|
||||||
|
@extend_schema(summary='CMS-REL-7 学生下拉(姓名+手机号)', tags=['CMS-师生关系'])
|
||||||
|
@action(detail=False, methods=['get'], url_path='students')
|
||||||
|
def students(self, request):
|
||||||
|
"""新增/编辑师生关系的「学生」下拉数据源,返回 [{id, real_name, phone}]。"""
|
||||||
|
return Response(self._role_users('student'))
|
||||||
|
|
||||||
@extend_schema(summary='CMS-REL-4 下载师生关系导入模板', tags=['CMS-师生关系'])
|
@extend_schema(summary='CMS-REL-4 下载师生关系导入模板', tags=['CMS-师生关系'])
|
||||||
@action(detail=False, methods=['get'], url_path='import-template')
|
@action(detail=False, methods=['get'], url_path='import-template')
|
||||||
def import_template(self, request):
|
def import_template(self, request):
|
||||||
@@ -174,14 +205,16 @@ class CmsTeacherStudentRelationViewSet(viewsets.ModelViewSet):
|
|||||||
|
|
||||||
success, errors = 0, []
|
success, errors = 0, []
|
||||||
for idx, row in enumerate(rows, start=2):
|
for idx, row in enumerate(rows, start=2):
|
||||||
|
t_name = (row.get('带教医生姓名') or '').strip()
|
||||||
t_phone = (row.get('带教医生手机号') or '').strip()
|
t_phone = (row.get('带教医生手机号') or '').strip()
|
||||||
|
s_name = (row.get('学生姓名') or '').strip()
|
||||||
s_phone = (row.get('学生手机号') or '').strip()
|
s_phone = (row.get('学生手机号') or '').strip()
|
||||||
teacher = User.objects.filter(phone=t_phone, role_type='doctor').first()
|
teacher = User.objects.filter(phone=t_phone, real_name=t_name, role_type='doctor').first()
|
||||||
if teacher is None:
|
if teacher is None:
|
||||||
errors.append({'row': idx, 'reason': f'带教医生不存在或非医生:{t_phone}'}); continue
|
errors.append({'row': idx, 'reason': f'带教医生不存在或姓名手机号不符:{t_name} / {t_phone}'}); continue
|
||||||
student = User.objects.filter(phone=s_phone, role_type='student').first()
|
student = User.objects.filter(phone=s_phone, real_name=s_name, role_type='student').first()
|
||||||
if student is None:
|
if student is None:
|
||||||
errors.append({'row': idx, 'reason': f'学生不存在或非学生:{s_phone}'}); continue
|
errors.append({'row': idx, 'reason': f'学生不存在或姓名手机号不符:{s_name} / {s_phone}'}); continue
|
||||||
if not actor_is_super and (
|
if not actor_is_super and (
|
||||||
teacher.institution_id != actor.institution_id
|
teacher.institution_id != actor.institution_id
|
||||||
or student.institution_id != actor.institution_id
|
or student.institution_id != actor.institution_id
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
"""Swagger Try-it-out 等效脚本:CMS 医院管理员·师生关系管理(姓名+手机号版)。
|
||||||
|
|
||||||
|
覆盖本轮改动:CMS-REL-2 新增/编辑(必填 带教老师姓名+手机号 / 学生姓名+手机号)、
|
||||||
|
CMS-REL-4 导入/模板(4 列:带教医生姓名|手机号|学生姓名|手机号)、
|
||||||
|
CMS-REL-6/7 下拉数据源(doctors / students,姓名+手机号一并返回)。
|
||||||
|
运行方式:.venv\\Scripts\\python.exe test/swagger_cms_relation.py
|
||||||
|
前提:Django dev server 已在 http://127.0.0.1:8000 运行,Redis 已启动。
|
||||||
|
"""
|
||||||
|
import io
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from openpyxl import Workbook
|
||||||
|
|
||||||
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
|
sys.stderr.reconfigure(encoding='utf-8')
|
||||||
|
|
||||||
|
BASE = 'http://127.0.0.1:8000'
|
||||||
|
PYTHON = r'D:\01Agent\medical_training\.venv\Scripts\python.exe'
|
||||||
|
CWD = r'D:\01Agent\medical_training'
|
||||||
|
REL = '/api/cms/teacher-student-relations/'
|
||||||
|
XLSX_CT = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||||
|
PASS, FAIL = 'PASS', 'FAIL'
|
||||||
|
results = []
|
||||||
|
|
||||||
|
PH = ['13700007001', '13700007002', '13700007003', '13700007004', '13700007009'] # D1 D2 S1 S2 admin
|
||||||
|
|
||||||
|
|
||||||
|
def log(api_id, method, url, expected, actual, detail=''):
|
||||||
|
exp = expected if isinstance(expected, (list, tuple)) else [expected]
|
||||||
|
st = PASS if actual in exp else FAIL
|
||||||
|
results.append((api_id, st))
|
||||||
|
print(f' {st} {api_id:<16} {method:<5} {url:<44} expect={str(expected):<10} got={actual} {detail}')
|
||||||
|
|
||||||
|
|
||||||
|
def django_eval(code):
|
||||||
|
pre = 'import django, os; os.environ.setdefault("DJANGO_SETTINGS_MODULE","config.settings"); django.setup()\n'
|
||||||
|
p = subprocess.run([PYTHON, '-c', pre + code], capture_output=True, text=True, cwd=CWD)
|
||||||
|
if p.returncode != 0:
|
||||||
|
print('[django_eval ERROR]', p.stderr[-1200:])
|
||||||
|
return p.stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def jb(r):
|
||||||
|
return r.json() if r.headers.get('content-type', '').startswith('application/json') else None
|
||||||
|
|
||||||
|
|
||||||
|
def make_xlsx(headers, rows):
|
||||||
|
wb = Workbook(); ws = wb.active
|
||||||
|
ws.append(headers)
|
||||||
|
for r in rows:
|
||||||
|
ws.append(r)
|
||||||
|
buf = io.BytesIO(); wb.save(buf); buf.seek(0)
|
||||||
|
return buf.read()
|
||||||
|
|
||||||
|
|
||||||
|
print('\n[准备] 建机构 + 医院管理员 + 2 医生 + 2 学生...')
|
||||||
|
setup = django_eval(
|
||||||
|
'from apps.user.models import User, Institution; from rest_framework_simplejwt.tokens import RefreshToken; '
|
||||||
|
'inst,_=Institution.objects.get_or_create(code="SWG_REL", defaults={"name":"师生关系联调院","type":"hospital"}); '
|
||||||
|
f'[User.all_objects.filter(phone__in={PH!r}).delete()]; '
|
||||||
|
f'd1=User.objects.create_user(username="{PH[0]}",password=None,phone="{PH[0]}",real_name="王医生",role_type="doctor",institution=inst,status=1); '
|
||||||
|
f'd2=User.objects.create_user(username="{PH[1]}",password=None,phone="{PH[1]}",real_name="李医生",role_type="doctor",institution=inst,status=1); '
|
||||||
|
f's1=User.objects.create_user(username="{PH[2]}",password=None,phone="{PH[2]}",real_name="赵同学",role_type="student",institution=inst,status=1); '
|
||||||
|
f's2=User.objects.create_user(username="{PH[3]}",password=None,phone="{PH[3]}",real_name="钱同学",role_type="student",institution=inst,status=1); '
|
||||||
|
f'hu=User.objects.create_user(username="{PH[4]}",password=None,phone="{PH[4]}",real_name="师生关系院管",role_type="hospital_admin",institution=inst,status=1); '
|
||||||
|
'print(str(RefreshToken.for_user(hu).access_token))'
|
||||||
|
)
|
||||||
|
HU = {'Authorization': f'Bearer {setup}'}
|
||||||
|
print('[准备] 完成\n')
|
||||||
|
|
||||||
|
print('=' * 100)
|
||||||
|
print(' CMS 师生关系管理(姓名+手机号)Swagger Try-it-out')
|
||||||
|
print('=' * 100)
|
||||||
|
|
||||||
|
# 权限
|
||||||
|
log('anon-401', 'GET', REL, 401, requests.get(f'{BASE}{REL}').status_code)
|
||||||
|
|
||||||
|
# CMS-REL-6/7 下拉数据源
|
||||||
|
r = requests.get(f'{BASE}{REL}doctors/', headers=HU); b = jb(r) or []
|
||||||
|
log('CMS-REL-6', 'GET', REL + 'doctors/', 200, r.status_code,
|
||||||
|
f'count={len(b)} sample={b[0] if b else None}')
|
||||||
|
r = requests.get(f'{BASE}{REL}students/', headers=HU); b = jb(r) or []
|
||||||
|
log('CMS-REL-7', 'GET', REL + 'students/', 200, r.status_code,
|
||||||
|
f'count={len(b)} sample={b[0] if b else None}')
|
||||||
|
|
||||||
|
# CMS-REL-2 新增(姓名+手机号)
|
||||||
|
payload = {'teacher_name': '王医生', 'teacher_phone': PH[0], 'student_name': '赵同学', 'student_phone': PH[2], 'relation_type': '指导'}
|
||||||
|
r = requests.post(f'{BASE}{REL}', json=payload, headers=HU); b = jb(r) or {}
|
||||||
|
rid = b.get('id')
|
||||||
|
log('CMS-REL-2-new', 'POST', REL, 201, r.status_code,
|
||||||
|
f'id={rid} t={b.get("teacher_name")}/{b.get("teacher_phone")} s={b.get("student_name")}/{b.get("student_phone")}')
|
||||||
|
|
||||||
|
# 缺姓名 → 400
|
||||||
|
r = requests.post(f'{BASE}{REL}', json={**payload, 'student_name': ''}, headers=HU)
|
||||||
|
log('REL-2-missing', 'POST', REL + '(缺学生姓名)', 400, r.status_code, f'code={(jb(r) or {}).get("code")}')
|
||||||
|
|
||||||
|
# 姓名与手机号不符 → 400
|
||||||
|
r = requests.post(f'{BASE}{REL}', json={**payload, 'teacher_name': '不存在'}, headers=HU)
|
||||||
|
log('REL-2-mismatch', 'POST', REL + '(姓名手机号不符)', 400, r.status_code, f'code={(jb(r) or {}).get("code")}')
|
||||||
|
|
||||||
|
# 重复 → 400
|
||||||
|
r = requests.post(f'{BASE}{REL}', json=payload, headers=HU)
|
||||||
|
log('REL-2-dup', 'POST', REL + '(重复)', 400, r.status_code, f'code={(jb(r) or {}).get("code")}')
|
||||||
|
|
||||||
|
# CMS-REL-2 编辑(改 status)
|
||||||
|
if rid:
|
||||||
|
r = requests.post(f'{BASE}{REL}{rid}/update/', json={'status': 0}, headers=HU)
|
||||||
|
log('CMS-REL-2-edit', 'POST', f'{REL}{rid}/update/', 200, r.status_code, f'status={(jb(r) or {}).get("status")}')
|
||||||
|
|
||||||
|
# CMS-REL-4 模板
|
||||||
|
r = requests.get(f'{BASE}{REL}import-template/', headers=HU)
|
||||||
|
log('CMS-REL-4-tmpl', 'GET', REL + 'import-template/', 200, r.status_code, f'ct={r.headers.get("content-type","")[:40]}')
|
||||||
|
|
||||||
|
# CMS-REL-4 导入(4 列)
|
||||||
|
xlsx = make_xlsx(['带教医生姓名', '带教医生手机号', '学生姓名', '学生手机号'], [
|
||||||
|
['李医生', PH[1], '钱同学', PH[3]], # ✅
|
||||||
|
['不存在', '00000000000', '钱同学', PH[3]], # 医生不符 → 失败
|
||||||
|
])
|
||||||
|
r = requests.post(f'{BASE}{REL}import/', files={'file': ('rel.xlsx', xlsx, XLSX_CT)}, headers=HU)
|
||||||
|
b = jb(r) or {}
|
||||||
|
log('CMS-REL-4-import', 'POST', REL + 'import/', 200, r.status_code,
|
||||||
|
f'success={b.get("success")} failed={b.get("failed")}')
|
||||||
|
|
||||||
|
# CMS-REL-1 列表
|
||||||
|
r = requests.get(f'{BASE}{REL}', headers=HU)
|
||||||
|
log('CMS-REL-1-list', 'GET', REL, 200, r.status_code, f'count={(jb(r) or {}).get("count")}')
|
||||||
|
|
||||||
|
# 清理
|
||||||
|
django_eval(
|
||||||
|
'from apps.user.models import User; from apps.user.models import TeacherStudentRelation as T; '
|
||||||
|
f'T.all_objects.filter(teacher__phone__in={PH!r}).delete(); '
|
||||||
|
f'User.all_objects.filter(phone__in={PH!r}).delete(); print("cleaned")'
|
||||||
|
)
|
||||||
|
|
||||||
|
print('=' * 100)
|
||||||
|
total = len(results); passed = sum(1 for _, s in results if s == PASS); failed = total - passed
|
||||||
|
print(f' 总计: {total} | 通过: {passed} | 失败: {failed}')
|
||||||
|
if failed:
|
||||||
|
print(' 失败:', [a for a, s in results if s == FAIL]); sys.exit(1)
|
||||||
|
print(' ALL PASSED — 师生关系(姓名+手机号)接口验证通过!')
|
||||||
|
sys.exit(0)
|
||||||
+40
-15
@@ -36,9 +36,16 @@ class CmsRelationTest(CacheTestCase):
|
|||||||
self.other = ensure_institution(name='他院', code='REL-B')
|
self.other = ensure_institution(name='他院', code='REL-B')
|
||||||
self.admin = create_test_user(phone='13932000001', role_type='hospital_admin', institution=self.inst)
|
self.admin = create_test_user(phone='13932000001', role_type='hospital_admin', institution=self.inst)
|
||||||
self.client = get_auth_client(self.admin)
|
self.client = get_auth_client(self.admin)
|
||||||
self.doc = create_test_user(phone='13932000002', role_type='doctor', institution=self.inst)
|
self.doc = create_test_user(phone='13932000002', real_name='张医生', role_type='doctor', institution=self.inst)
|
||||||
self.stu = create_test_user(phone='13932000003', role_type='student', institution=self.inst)
|
self.stu = create_test_user(phone='13932000003', real_name='李同学', role_type='student', institution=self.inst)
|
||||||
self.other_stu = create_test_user(phone='13932000004', role_type='student', institution=self.other)
|
self.other_stu = create_test_user(phone='13932000004', real_name='王同学', role_type='student', institution=self.other)
|
||||||
|
|
||||||
|
# 完整新增载荷(姓名+手机号均必填)
|
||||||
|
def _payload(self, **over):
|
||||||
|
p = {'teacher_name': '张医生', 'teacher_phone': '13932000002',
|
||||||
|
'student_name': '李同学', 'student_phone': '13932000003'}
|
||||||
|
p.update(over)
|
||||||
|
return p
|
||||||
|
|
||||||
def test_permission(self):
|
def test_permission(self):
|
||||||
self.assertEqual(APIClient().get(REL_URL).status_code, 401)
|
self.assertEqual(APIClient().get(REL_URL).status_code, 401)
|
||||||
@@ -46,37 +53,55 @@ class CmsRelationTest(CacheTestCase):
|
|||||||
self.assertEqual(stu_client.get(REL_URL).status_code, 403)
|
self.assertEqual(stu_client.get(REL_URL).status_code, 403)
|
||||||
|
|
||||||
def test_create_and_list(self):
|
def test_create_and_list(self):
|
||||||
resp = self.client.post(REL_URL, {'teacher_phone': '13932000002', 'student_phone': '13932000003'})
|
resp = self.client.post(REL_URL, self._payload())
|
||||||
self.assertEqual(resp.status_code, 201, resp.content)
|
self.assertEqual(resp.status_code, 201, resp.content)
|
||||||
self.assertEqual(resp.json()['teacher_phone'], '13932000002')
|
self.assertEqual(resp.json()['teacher_phone'], '13932000002')
|
||||||
self.assertEqual(resp.json()['student_phone'], '13932000003')
|
self.assertEqual(resp.json()['student_phone'], '13932000003')
|
||||||
|
self.assertEqual(resp.json()['teacher_name'], '张医生')
|
||||||
|
self.assertEqual(resp.json()['student_name'], '李同学')
|
||||||
# 列表
|
# 列表
|
||||||
resp = self.client.get(REL_URL)
|
resp = self.client.get(REL_URL)
|
||||||
self.assertEqual(resp.status_code, 200)
|
self.assertEqual(resp.status_code, 200)
|
||||||
self.assertEqual(len(resp.json()['results']), 1)
|
self.assertEqual(len(resp.json()['results']), 1)
|
||||||
|
|
||||||
def test_create_missing_phone_400(self):
|
def test_create_missing_name_400(self):
|
||||||
resp = self.client.post(REL_URL, {'teacher_phone': '13932000002'})
|
resp = self.client.post(REL_URL, self._payload(student_name=''))
|
||||||
self.assertEqual(resp.status_code, 400, resp.content)
|
self.assertEqual(resp.status_code, 400, resp.content)
|
||||||
self.assertEqual(resp.json()['code'], 'CMS_VALIDATION_ERROR')
|
self.assertEqual(resp.json()['code'], 'CMS_VALIDATION_ERROR')
|
||||||
|
|
||||||
|
def test_create_name_phone_mismatch_400(self):
|
||||||
|
# 姓名与手机号不符 → 解析不到
|
||||||
|
resp = self.client.post(REL_URL, self._payload(teacher_name='不存在'))
|
||||||
|
self.assertEqual(resp.status_code, 400, resp.content)
|
||||||
|
self.assertEqual(resp.json()['code'], 'CMS_REL_TEACHER_NOT_FOUND')
|
||||||
|
|
||||||
def test_create_duplicate(self):
|
def test_create_duplicate(self):
|
||||||
self.client.post(REL_URL, {'teacher_phone': '13932000002', 'student_phone': '13932000003'})
|
self.client.post(REL_URL, self._payload())
|
||||||
resp = self.client.post(REL_URL, {'teacher_phone': '13932000002', 'student_phone': '13932000003'})
|
resp = self.client.post(REL_URL, self._payload())
|
||||||
self.assertEqual(resp.status_code, 400, resp.content)
|
self.assertEqual(resp.status_code, 400, resp.content)
|
||||||
self.assertEqual(resp.json()['code'], 'CMS_REL_EXISTS')
|
self.assertEqual(resp.json()['code'], 'CMS_REL_EXISTS')
|
||||||
|
|
||||||
def test_teacher_must_be_doctor(self):
|
def test_teacher_must_be_doctor(self):
|
||||||
# 用学生手机号当带教老师 → 解析不到 doctor
|
# 用学生姓名+手机号当带教老师 → 解析不到 doctor
|
||||||
resp = self.client.post(REL_URL, {'teacher_phone': '13932000003', 'student_phone': '13932000003'})
|
resp = self.client.post(REL_URL, self._payload(teacher_name='李同学', teacher_phone='13932000003'))
|
||||||
self.assertEqual(resp.status_code, 400, resp.content)
|
self.assertEqual(resp.status_code, 400, resp.content)
|
||||||
self.assertEqual(resp.json()['code'], 'CMS_REL_TEACHER_NOT_FOUND')
|
self.assertEqual(resp.json()['code'], 'CMS_REL_TEACHER_NOT_FOUND')
|
||||||
|
|
||||||
def test_scope_other_institution_student_rejected(self):
|
def test_scope_other_institution_student_rejected(self):
|
||||||
resp = self.client.post(REL_URL, {'teacher_phone': '13932000002', 'student_phone': '13932000004'})
|
resp = self.client.post(REL_URL, self._payload(student_name='王同学', student_phone='13932000004'))
|
||||||
self.assertEqual(resp.status_code, 403, resp.content)
|
self.assertEqual(resp.status_code, 403, resp.content)
|
||||||
self.assertEqual(resp.json()['code'], 'CMS_REL_SCOPE_FORBIDDEN')
|
self.assertEqual(resp.json()['code'], 'CMS_REL_SCOPE_FORBIDDEN')
|
||||||
|
|
||||||
|
def test_dropdown_lists(self):
|
||||||
|
# 2 个下拉数据源:本院 doctor / student,姓名+手机号一并返回
|
||||||
|
docs = self.client.get(REL_URL + 'doctors/').json()
|
||||||
|
self.assertEqual(docs, [{'id': self.doc.id, 'real_name': '张医生', 'phone': '13932000002'}])
|
||||||
|
studs = self.client.get(REL_URL + 'students/').json()
|
||||||
|
# 他院学生不在内(本院仅李同学)
|
||||||
|
self.assertEqual(studs, [{'id': self.stu.id, 'real_name': '李同学', 'phone': '13932000003'}])
|
||||||
|
# 学生无权访问
|
||||||
|
self.assertEqual(get_auth_client(self.stu).get(REL_URL + 'doctors/').status_code, 403)
|
||||||
|
|
||||||
def test_soft_delete(self):
|
def test_soft_delete(self):
|
||||||
r = TeacherStudentRelation.objects.create(teacher=self.doc, student=self.stu, status=1)
|
r = TeacherStudentRelation.objects.create(teacher=self.doc, student=self.stu, status=1)
|
||||||
resp = self.client.post(rel_disable(r.id))
|
resp = self.client.post(rel_disable(r.id))
|
||||||
@@ -85,10 +110,10 @@ class CmsRelationTest(CacheTestCase):
|
|||||||
self.assertTrue(TeacherStudentRelation.all_objects.get(id=r.id).is_deleted)
|
self.assertTrue(TeacherStudentRelation.all_objects.get(id=r.id).is_deleted)
|
||||||
|
|
||||||
def test_import_and_export(self):
|
def test_import_and_export(self):
|
||||||
f = make_xlsx(['带教医生手机号', '学生手机号'], [
|
f = make_xlsx(['带教医生姓名', '带教医生手机号', '学生姓名', '学生手机号'], [
|
||||||
['13932000002', '13932000003'], # ✅
|
['张医生', '13932000002', '李同学', '13932000003'], # ✅
|
||||||
['13932000002', '13932000004'], # 学生他院 → 失败
|
['张医生', '13932000002', '王同学', '13932000004'], # 学生他院 → 失败
|
||||||
['00000000000', '13932000003'], # 医生不存在 → 失败
|
['不存在', '00000000000', '李同学', '13932000003'], # 医生不存在 → 失败
|
||||||
])
|
])
|
||||||
resp = self.client.post('/api/cms/teacher-student-relations/import/', {'file': f}, format='multipart')
|
resp = self.client.post('/api/cms/teacher-student-relations/import/', {'file': f}, format='multipart')
|
||||||
self.assertEqual(resp.status_code, 200, resp.content)
|
self.assertEqual(resp.status_code, 200, resp.content)
|
||||||
|
|||||||
Reference in New Issue
Block a user