feat: add profile api
This commit is contained in:
@@ -38,6 +38,9 @@ USER_CHANGE_PWD_URL = '/api/user/users/change-password/'
|
||||
USER_ME_URL = '/api/user/users/me/'
|
||||
USER_LIST_URL = '/api/user/users/'
|
||||
USER_INSTITUTION_LIST_URL = '/api/user/institution_list/'
|
||||
USER_INSTITUTION_INFO_URL = '/api/user/institution_info/'
|
||||
USER_MY_DEPARTMENTS_URL = '/api/user/my_departments/'
|
||||
USER_PROFILE_CONFIG_URL = '/api/user/profile/config/'
|
||||
|
||||
# 病例
|
||||
CASE_PARSE_URL = '/api/case/cases/parse-pdf/'
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
"""
|
||||
Swagger Try-it-out 等效脚本:仅测配置页三接口。
|
||||
1) GET /api/user/institution_info/ 机构信息获取
|
||||
2) GET /api/user/my_departments/ 所属机构科室列表(不分页)
|
||||
3) POST /api/user/profile/config/ 医学生信息配置
|
||||
运行方式:.venv\\Scripts\\python.exe test/swagger_profile_config.py
|
||||
前提:Django dev server 已在 http://127.0.0.1:8000 运行,Redis 已启动。
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import subprocess
|
||||
|
||||
import requests
|
||||
|
||||
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'
|
||||
PASS, FAIL = 'PASS', 'FAIL'
|
||||
results = []
|
||||
|
||||
TRIAL_INST_CODE = 'PKU_LAB_TRIAL'
|
||||
STUDENT_PHONE = '13700000055'
|
||||
|
||||
|
||||
def log(api_id, method, url, expected, actual, detail='', resp_body=None):
|
||||
exp = expected if isinstance(expected, (list, tuple)) else [expected]
|
||||
status = PASS if actual in exp else FAIL
|
||||
results.append((api_id, status))
|
||||
print(f' {status} {api_id:<14} {method:<5} {url:<38} expect={str(expected):<10} got={actual} {detail}')
|
||||
if resp_body is not None:
|
||||
body = json.dumps(resp_body, ensure_ascii=False, indent=2)
|
||||
if len(body) > 1200:
|
||||
body = body[:1200] + f'... (truncated, {len(body)} chars)'
|
||||
print(f' <<< {body}')
|
||||
|
||||
|
||||
def django_eval(code):
|
||||
preamble = (
|
||||
'import django, os; '
|
||||
'os.environ.setdefault("DJANGO_SETTINGS_MODULE","config.settings"); '
|
||||
'django.setup(); '
|
||||
)
|
||||
proc = subprocess.run([PYTHON, '-c', preamble + code], capture_output=True, text=True, cwd=CWD)
|
||||
return proc.stdout.strip()
|
||||
|
||||
|
||||
# ─── 准备:试用机构 + banner + 两个科室 + 学生用户 + token ─────────────────────
|
||||
|
||||
print('\n[准备] 配置试用机构 banner、科室、学生用户...')
|
||||
setup = django_eval(
|
||||
f'from apps.user.models import User, Institution, Department; '
|
||||
f'from rest_framework_simplejwt.tokens import RefreshToken; '
|
||||
f'inst, _ = Institution.objects.get_or_create(code="{TRIAL_INST_CODE}", '
|
||||
f' defaults={{"name":"北大医学部(实验室)试用","type":"hospital"}}); '
|
||||
f'inst.banner_url = "institutions/default_hospital.png"; inst.save(update_fields=["banner_url"]); '
|
||||
f'd1, _ = Department.objects.get_or_create(institution=inst, name="内科", defaults={{"category":"临床"}}); '
|
||||
f'd2, _ = Department.objects.get_or_create(institution=inst, name="外科", defaults={{"category":"临床"}}); '
|
||||
f'User.objects.filter(phone="{STUDENT_PHONE}").delete(); '
|
||||
f'u = User.objects.create_user(username="{STUDENT_PHONE}", password=None, phone="{STUDENT_PHONE}", '
|
||||
f' real_name="配置页测试学生", role_type="student", institution=inst, status=1); '
|
||||
f'print(str(RefreshToken.for_user(u).access_token) + "|" + str(d1.id) + "|" + str(d2.id) + "|" + str(inst.id))'
|
||||
)
|
||||
access, dept1_id, dept2_id, inst_id = setup.split('|')
|
||||
auth = {'Authorization': f'Bearer {access}'}
|
||||
print(f'[准备] 完成 inst_id={inst_id} dept1={dept1_id} dept2={dept2_id}\n')
|
||||
|
||||
print('=' * 100)
|
||||
print(' 配置页三接口 Swagger Try-it-out')
|
||||
print('=' * 100)
|
||||
|
||||
# ── 1. 机构信息获取 ───────────────────────────────────────────────────────────
|
||||
|
||||
r = s = requests.get(f'{BASE}/api/user/institution_info/', headers=auth)
|
||||
body = r.json() if r.headers.get('content-type', '').startswith('application/json') else None
|
||||
banner = (body or {}).get('banner_url', '')
|
||||
detail = f'name={body.get("name","")}, banner_ok={banner.endswith("/static/institutions/default_hospital.png")}' if body else ''
|
||||
log('institution_info', 'GET', '/api/user/institution_info/', 200, r.status_code, detail, resp_body=body)
|
||||
|
||||
# 1b. 未登录 → 401
|
||||
r = requests.get(f'{BASE}/api/user/institution_info/')
|
||||
log('inst_info-401', 'GET', '/api/user/institution_info/ (anon)', 401, r.status_code)
|
||||
|
||||
# ── 2. 所属机构科室列表(不分页)─────────────────────────────────────────────
|
||||
|
||||
r = requests.get(f'{BASE}/api/user/my_departments/', headers=auth)
|
||||
body = r.json() if r.headers.get('content-type', '').startswith('application/json') else None
|
||||
is_list = isinstance(body, list)
|
||||
names = [d.get('name') for d in body] if is_list else []
|
||||
detail = f'not_paginated={is_list}, count={len(names)}, names={names}'
|
||||
log('my_departments', 'GET', '/api/user/my_departments/', 200, r.status_code, detail, resp_body=body)
|
||||
|
||||
# ── 3. 医学生信息配置 ─────────────────────────────────────────────────────────
|
||||
|
||||
cfg_body = {'department': int(dept1_id), 'title_name': '住院医师', 'practice_years': '1-3年'}
|
||||
r = requests.post(f'{BASE}/api/user/profile/config/', json=cfg_body, headers=auth)
|
||||
body = r.json() if r.headers.get('content-type', '').startswith('application/json') else None
|
||||
u = (body or {}).get('user', {})
|
||||
detail = (f'dept={u.get("department")}, title={u.get("title_name")}, '
|
||||
f'years={u.get("practice_years")}') if body else ''
|
||||
log('profile_config', 'POST', '/api/user/profile/config/', 200, r.status_code, detail, resp_body=body)
|
||||
|
||||
# 3b. 跨机构科室 → 400(用一个属于别的机构的科室)
|
||||
other_dept = django_eval(
|
||||
'from apps.user.models import Institution, Department; '
|
||||
'o, _ = Institution.objects.get_or_create(code="SWAG_OTHER_CFG", defaults={"name":"配置页其它医院","type":"hospital"}); '
|
||||
'd, _ = Department.objects.get_or_create(institution=o, name="放射科", defaults={"category":"医技"}); '
|
||||
'print(d.id)'
|
||||
)
|
||||
r = requests.post(f'{BASE}/api/user/profile/config/',
|
||||
json={'department': int(other_dept), 'title_name': '住院医师', 'practice_years': '1-3年'},
|
||||
headers=auth)
|
||||
body = r.json() if r.headers.get('content-type', '').startswith('application/json') else None
|
||||
log('cfg-cross-inst', 'POST', '/api/user/profile/config/ (other inst dept)', 400, r.status_code,
|
||||
f'code={(body or {}).get("code","")}', resp_body=body)
|
||||
|
||||
# 3c. 缺字段 → 400
|
||||
r = requests.post(f'{BASE}/api/user/profile/config/', json={'title_name': '住院医师'}, headers=auth)
|
||||
log('cfg-missing', 'POST', '/api/user/profile/config/ (missing fields)', 400, r.status_code)
|
||||
|
||||
# 验证落库
|
||||
db = django_eval(
|
||||
f'from apps.user.models import User; u=User.objects.get(phone="{STUDENT_PHONE}"); '
|
||||
f'print(f"{{u.department_id}}|{{u.title_name}}|{{u.practice_years}}")'
|
||||
)
|
||||
log('cfg-db-check', 'CHECK', 'user.department/title/years persisted',
|
||||
f'{dept1_id}|住院医师|1-3年', db)
|
||||
|
||||
# ─── 清理 ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
django_eval(f'from apps.user.models import User; User.objects.filter(phone="{STUDENT_PHONE}").delete(); print("cleaned")')
|
||||
|
||||
# ─── 汇总 ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
print('=' * 100)
|
||||
total = len(results)
|
||||
passed = sum(1 for _, st in results if st == PASS)
|
||||
failed = total - passed
|
||||
print(f' 总计: {total} | 通过: {passed} | 失败: {failed}')
|
||||
if failed:
|
||||
print(' 失败:', [aid for aid, st in results if st == FAIL])
|
||||
sys.exit(1)
|
||||
print(' ALL PASSED — 配置页三接口验证通过!')
|
||||
sys.exit(0)
|
||||
@@ -0,0 +1,146 @@
|
||||
"""配置页三接口测试:机构信息获取 / 所属机构科室列表 / 医学生信息配置。"""
|
||||
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from apps.user.models import Department, Institution
|
||||
from .conftest import (
|
||||
CacheTestCase,
|
||||
USER_INSTITUTION_INFO_URL, USER_MY_DEPARTMENTS_URL, USER_PROFILE_CONFIG_URL,
|
||||
create_test_user, get_auth_client, ensure_institution,
|
||||
)
|
||||
|
||||
|
||||
class InstitutionInfoTest(CacheTestCase):
|
||||
"""机构信息获取接口。"""
|
||||
|
||||
def test_returns_institution_with_banner(self):
|
||||
"""配置了 banner_url 的机构 → 返回拼好的完整 URL。"""
|
||||
inst = ensure_institution(name='测试医院', code='TEST-HOSP-001')
|
||||
inst.banner_url = 'institutions/test.png'
|
||||
inst.save(update_fields=['banner_url'])
|
||||
user = create_test_user(phone='13900200001', institution=inst)
|
||||
|
||||
client = get_auth_client(user)
|
||||
resp = client.get(USER_INSTITUTION_INFO_URL)
|
||||
self.assertEqual(resp.status_code, 200, resp.content)
|
||||
|
||||
data = resp.json()
|
||||
self.assertEqual(data['id'], inst.id)
|
||||
self.assertEqual(data['name'], '测试医院')
|
||||
self.assertTrue(data['banner_url'].startswith('http'))
|
||||
self.assertTrue(data['banner_url'].endswith('/static/institutions/test.png'))
|
||||
|
||||
def test_empty_banner_falls_back_to_default(self):
|
||||
"""机构未配 banner_url → 回退到默认医院图。"""
|
||||
inst = ensure_institution(name='无图医院', code='TEST-HOSP-002')
|
||||
user = create_test_user(phone='13900200002', institution=inst)
|
||||
|
||||
client = get_auth_client(user)
|
||||
resp = client.get(USER_INSTITUTION_INFO_URL)
|
||||
self.assertEqual(resp.status_code, 200, resp.content)
|
||||
self.assertTrue(
|
||||
resp.json()['banner_url'].endswith('/static/institutions/default_hospital.png')
|
||||
)
|
||||
|
||||
def test_full_url_banner_passthrough(self):
|
||||
"""banner_url 已是完整 URL → 原样返回。"""
|
||||
inst = ensure_institution(name='CDN医院', code='TEST-HOSP-003')
|
||||
inst.banner_url = 'https://cdn.example.com/a.png'
|
||||
inst.save(update_fields=['banner_url'])
|
||||
user = create_test_user(phone='13900200003', institution=inst)
|
||||
|
||||
client = get_auth_client(user)
|
||||
resp = client.get(USER_INSTITUTION_INFO_URL)
|
||||
self.assertEqual(resp.json()['banner_url'], 'https://cdn.example.com/a.png')
|
||||
|
||||
def test_user_without_institution_404(self):
|
||||
"""账号未关联机构 → 404。"""
|
||||
user = create_test_user(phone='13900200004', institution=None)
|
||||
client = get_auth_client(user)
|
||||
resp = client.get(USER_INSTITUTION_INFO_URL)
|
||||
self.assertEqual(resp.status_code, 404, resp.content)
|
||||
|
||||
def test_requires_auth(self):
|
||||
"""未登录 → 401。"""
|
||||
resp = APIClient().get(USER_INSTITUTION_INFO_URL)
|
||||
self.assertEqual(resp.status_code, 401, resp.content)
|
||||
|
||||
|
||||
class MyDepartmentsTest(CacheTestCase):
|
||||
"""所属机构科室列表接口(不分页)。"""
|
||||
|
||||
def test_returns_all_departments_no_pagination(self):
|
||||
"""返回本机构全部科室,且不分页(直接为列表)。"""
|
||||
inst = ensure_institution(name='测试医院', code='TEST-HOSP-001')
|
||||
Department.objects.create(institution=inst, name='内科', category='临床')
|
||||
Department.objects.create(institution=inst, name='外科', category='临床')
|
||||
# 另一机构的科室不应出现
|
||||
other = ensure_institution(name='其他医院', code='TEST-HOSP-OTHER')
|
||||
Department.objects.create(institution=other, name='儿科', category='临床')
|
||||
|
||||
user = create_test_user(phone='13900200010', institution=inst)
|
||||
client = get_auth_client(user)
|
||||
resp = client.get(USER_MY_DEPARTMENTS_URL)
|
||||
self.assertEqual(resp.status_code, 200, resp.content)
|
||||
|
||||
data = resp.json()
|
||||
self.assertIsInstance(data, list) # 不分页:顶层是列表
|
||||
names = {d['name'] for d in data}
|
||||
self.assertEqual(names, {'内科', '外科'})
|
||||
|
||||
def test_user_without_institution_404(self):
|
||||
user = create_test_user(phone='13900200011', institution=None)
|
||||
client = get_auth_client(user)
|
||||
resp = client.get(USER_MY_DEPARTMENTS_URL)
|
||||
self.assertEqual(resp.status_code, 404, resp.content)
|
||||
|
||||
|
||||
class ProfileConfigTest(CacheTestCase):
|
||||
"""医学生信息配置接口。"""
|
||||
|
||||
def test_config_success(self):
|
||||
"""录入 科室/职称/执业年限 → 落库成功。"""
|
||||
inst = ensure_institution(name='测试医院', code='TEST-HOSP-001')
|
||||
dept = Department.objects.create(institution=inst, name='内科', category='临床')
|
||||
user = create_test_user(phone='13900200020', institution=inst)
|
||||
|
||||
client = get_auth_client(user)
|
||||
resp = client.post(USER_PROFILE_CONFIG_URL, {
|
||||
'department': dept.id,
|
||||
'title_name': '住院医师',
|
||||
'practice_years': '1-3年',
|
||||
})
|
||||
self.assertEqual(resp.status_code, 200, resp.content)
|
||||
|
||||
user.refresh_from_db()
|
||||
self.assertEqual(user.department_id, dept.id)
|
||||
self.assertEqual(user.title_name, '住院医师')
|
||||
self.assertEqual(user.practice_years, '1-3年')
|
||||
self.assertEqual(resp.json()['user']['practice_years'], '1-3年')
|
||||
|
||||
def test_department_from_other_institution_rejected(self):
|
||||
"""所选科室不属于本机构 → 校验失败 400。"""
|
||||
inst = ensure_institution(name='测试医院', code='TEST-HOSP-001')
|
||||
other = ensure_institution(name='其他医院', code='TEST-HOSP-OTHER')
|
||||
other_dept = Department.objects.create(institution=other, name='外科', category='临床')
|
||||
user = create_test_user(phone='13900200021', institution=inst)
|
||||
|
||||
client = get_auth_client(user)
|
||||
resp = client.post(USER_PROFILE_CONFIG_URL, {
|
||||
'department': other_dept.id,
|
||||
'title_name': '住院医师',
|
||||
'practice_years': '1-3年',
|
||||
})
|
||||
self.assertEqual(resp.status_code, 400, resp.content)
|
||||
|
||||
def test_missing_fields_rejected(self):
|
||||
"""缺少必填字段 → 400。"""
|
||||
inst = ensure_institution(name='测试医院', code='TEST-HOSP-001')
|
||||
user = create_test_user(phone='13900200022', institution=inst)
|
||||
client = get_auth_client(user)
|
||||
resp = client.post(USER_PROFILE_CONFIG_URL, {'title_name': '住院医师'})
|
||||
self.assertEqual(resp.status_code, 400, resp.content)
|
||||
|
||||
def test_requires_auth(self):
|
||||
resp = APIClient().post(USER_PROFILE_CONFIG_URL, {})
|
||||
self.assertEqual(resp.status_code, 401, resp.content)
|
||||
Reference in New Issue
Block a user