feat: update init users
This commit is contained in:
@@ -0,0 +1,34 @@
|
|||||||
|
# Generated by Django 5.2.14 on 2026-06-13 05:00
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('training', '0003_alter_trainingrecord_options_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='TrainingSession',
|
||||||
|
fields=[
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||||
|
('id', models.BigAutoField(primary_key=True, serialize=False)),
|
||||||
|
('case_id', models.BigIntegerField(blank=True, null=True, verbose_name='病例ID')),
|
||||||
|
('case_type', models.CharField(blank=True, max_length=30, verbose_name='病例类型')),
|
||||||
|
('training_mode', models.CharField(blank=True, max_length=50, verbose_name='训练模式')),
|
||||||
|
('status', models.CharField(blank=True, max_length=30, verbose_name='状态')),
|
||||||
|
('external_user_id', models.CharField(blank=True, max_length=128, verbose_name='宿主系统用户ID')),
|
||||||
|
('started_at', models.DateTimeField(blank=True, null=True, verbose_name='开始时间')),
|
||||||
|
('completed_at', models.DateTimeField(blank=True, null=True, verbose_name='完成时间')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': '训练会话',
|
||||||
|
'verbose_name_plural': '训练会话',
|
||||||
|
'db_table': 'training_session',
|
||||||
|
'managed': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -2,7 +2,10 @@ from rest_framework_simplejwt.tokens import RefreshToken
|
|||||||
|
|
||||||
from config.exceptions import AppError
|
from config.exceptions import AppError
|
||||||
|
|
||||||
ALLOWED_ROLE_TYPES = ('student', 'doctor', 'teacher')
|
# 系统五类角色:super_admin / hospital_admin / content_admin / doctor(带教医生)/ student
|
||||||
|
ROLE_TYPES = ('super_admin', 'hospital_admin', 'content_admin', 'doctor', 'student')
|
||||||
|
# 移动端可自注册的角色(带教老师即 doctor,不单列 teacher)
|
||||||
|
ALLOWED_ROLE_TYPES = ('student', 'doctor')
|
||||||
|
|
||||||
# CMS 端可登录的角色(U3 密码登录):超级管理员 / 医院管理员 / 内容管理员 / 医生(带教老师)
|
# CMS 端可登录的角色(U3 密码登录):超级管理员 / 医院管理员 / 内容管理员 / 医生(带教老师)
|
||||||
CMS_ROLE_TYPES = ('super_admin', 'hospital_admin', 'content_admin', 'doctor')
|
CMS_ROLE_TYPES = ('super_admin', 'hospital_admin', 'content_admin', 'doctor')
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import os
|
|||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
|
from apps.user.models import TeacherStudentRelation
|
||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
|
||||||
|
|
||||||
@@ -24,9 +26,12 @@ class Command(BaseCommand):
|
|||||||
# 创建超级管理员
|
# 创建超级管理员
|
||||||
self._create_superadmin()
|
self._create_superadmin()
|
||||||
|
|
||||||
# 创建测试角色用户
|
# 创建测试角色用户(覆盖五类角色:医院管理员/内容管理员/带教医生/学生)
|
||||||
self._create_test_users()
|
self._create_test_users()
|
||||||
|
|
||||||
|
# 师生关系(只在 doctor 与 student 之间)
|
||||||
|
self._create_relation()
|
||||||
|
|
||||||
self.stdout.write(self.style.SUCCESS('\n[完成] 用户初始化完成'))
|
self.stdout.write(self.style.SUCCESS('\n[完成] 用户初始化完成'))
|
||||||
|
|
||||||
def _create_superadmin(self):
|
def _create_superadmin(self):
|
||||||
@@ -55,6 +60,13 @@ class Command(BaseCommand):
|
|||||||
def _create_test_users(self):
|
def _create_test_users(self):
|
||||||
"""创建测试用户"""
|
"""创建测试用户"""
|
||||||
test_users = [
|
test_users = [
|
||||||
|
{
|
||||||
|
'username': 'hospital_admin',
|
||||||
|
'password': 'hospital123',
|
||||||
|
'real_name': '医院管理员',
|
||||||
|
'role_type': 'hospital_admin',
|
||||||
|
'phone': '13800138003',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
'username': 'doctor1',
|
'username': 'doctor1',
|
||||||
'password': 'doctor123',
|
'password': 'doctor123',
|
||||||
@@ -103,3 +115,16 @@ class Command(BaseCommand):
|
|||||||
f'[已存在] 用户: {user_data["username"]} ({user_data["real_name"]})'
|
f'[已存在] 用户: {user_data["username"]} ({user_data["real_name"]})'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _create_relation(self):
|
||||||
|
"""初始化一条师生关系(带教医生 doctor1 → 学生 student1)。"""
|
||||||
|
teacher = User.objects.filter(username='doctor1', role_type='doctor').first()
|
||||||
|
student = User.objects.filter(username='student1', role_type='student').first()
|
||||||
|
if not teacher or not student:
|
||||||
|
return
|
||||||
|
_, created = TeacherStudentRelation.objects.get_or_create(
|
||||||
|
teacher=teacher, student=student,
|
||||||
|
defaults={'relation_type': '指导', 'status': 1},
|
||||||
|
)
|
||||||
|
msg = '[创建] 师生关系: doctor1 → student1' if created else '[已存在] 师生关系: doctor1 → student1'
|
||||||
|
self.stdout.write((self.style.SUCCESS if created else self.style.WARNING)(msg))
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ class IsUserListPermitted(BasePermission):
|
|||||||
user = request.user
|
user = request.user
|
||||||
if _is_admin(user):
|
if _is_admin(user):
|
||||||
return True
|
return True
|
||||||
if user.role_type == 'teacher':
|
if user.role_type == 'doctor': # 带教医生:可看名下学生
|
||||||
return True
|
return True
|
||||||
raise AppError('USER_NO_LIST_PERMISSION', '您没有查看用户列表的权限', status_code=403)
|
raise AppError('USER_NO_LIST_PERMISSION', '您没有查看用户列表的权限', status_code=403)
|
||||||
|
|
||||||
@@ -32,8 +32,8 @@ class IsUserDetailPermitted(BasePermission):
|
|||||||
# 本人:可查看自己
|
# 本人:可查看自己
|
||||||
if user.id == obj.id:
|
if user.id == obj.id:
|
||||||
return True
|
return True
|
||||||
# 教师:可查看自己名下活跃学生
|
# 带教医生:可查看自己名下活跃学生
|
||||||
if user.role_type == 'teacher':
|
if user.role_type == 'doctor':
|
||||||
if TeacherStudentRelation.objects.filter(
|
if TeacherStudentRelation.objects.filter(
|
||||||
teacher=user, student=obj, status=1
|
teacher=user, student=obj, status=1
|
||||||
).exists():
|
).exists():
|
||||||
|
|||||||
@@ -120,6 +120,16 @@ class TeacherStudentRelationSerializer(serializers.ModelSerializer):
|
|||||||
model = TeacherStudentRelation
|
model = TeacherStudentRelation
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
|
|
||||||
|
def validate(self, attrs):
|
||||||
|
# 师生关系只能在 带教医生(doctor) 与 学生(student) 之间建立
|
||||||
|
teacher = attrs.get('teacher') or getattr(self.instance, 'teacher', None)
|
||||||
|
student = attrs.get('student') or getattr(self.instance, 'student', None)
|
||||||
|
if teacher is not None and teacher.role_type != 'doctor':
|
||||||
|
raise serializers.ValidationError({'teacher': '带教方必须是带教医生(doctor)'})
|
||||||
|
if student is not None and student.role_type != 'student':
|
||||||
|
raise serializers.ValidationError({'student': '学生方必须是学生(student)'})
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
|
||||||
class InstitutionSerializer(serializers.ModelSerializer):
|
class InstitutionSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|||||||
+2
-2
@@ -67,8 +67,8 @@ class UserViewSet(viewsets.ModelViewSet):
|
|||||||
if self.action == 'list':
|
if self.action == 'list':
|
||||||
if user.role_type in ('super_admin', 'content_admin') or user.is_staff:
|
if user.role_type in ('super_admin', 'content_admin') or user.is_staff:
|
||||||
return qs # 管理员:全员
|
return qs # 管理员:全员
|
||||||
elif user.role_type == 'teacher':
|
elif user.role_type == 'doctor':
|
||||||
# 教师:仅自己名下活跃学生
|
# 带教医生:仅自己名下活跃学生
|
||||||
student_ids = TeacherStudentRelation.objects.filter(
|
student_ids = TeacherStudentRelation.objects.filter(
|
||||||
teacher=user, status=1
|
teacher=user, status=1
|
||||||
).values_list('student_id', flat=True)
|
).values_list('student_id', flat=True)
|
||||||
|
|||||||
@@ -432,7 +432,7 @@ django_eval(
|
|||||||
f'admin = User.objects.create_user(username="{ADMIN_PHONE}", password="{ROLE_PWD}", '
|
f'admin = User.objects.create_user(username="{ADMIN_PHONE}", password="{ROLE_PWD}", '
|
||||||
f' phone="{ADMIN_PHONE}", real_name="Swagger管理员", role_type="super_admin", status=1); '
|
f' phone="{ADMIN_PHONE}", real_name="Swagger管理员", role_type="super_admin", status=1); '
|
||||||
f'teacher = User.objects.create_user(username="{TEACHER_PHONE}", password="{ROLE_PWD}", '
|
f'teacher = User.objects.create_user(username="{TEACHER_PHONE}", password="{ROLE_PWD}", '
|
||||||
f' phone="{TEACHER_PHONE}", real_name="Swagger教师", role_type="teacher", status=1); '
|
f' phone="{TEACHER_PHONE}", real_name="Swagger带教医生", role_type="doctor", status=1); '
|
||||||
f'student = User.objects.create_user(username="{STUDENT_PHONE}", password="{ROLE_PWD}", '
|
f'student = User.objects.create_user(username="{STUDENT_PHONE}", password="{ROLE_PWD}", '
|
||||||
f' phone="{STUDENT_PHONE}", real_name="Swagger学生", role_type="student", status=1); '
|
f' phone="{STUDENT_PHONE}", real_name="Swagger学生", role_type="student", status=1); '
|
||||||
f'TeacherStudentRelation.objects.create(teacher=teacher, student=student, '
|
f'TeacherStudentRelation.objects.create(teacher=teacher, student=student, '
|
||||||
|
|||||||
@@ -245,7 +245,7 @@ class UserListDetailHappyPathTest(CacheTestCase):
|
|||||||
"""HP-6: teacher GET /users/ → 200,仅包含名下活跃学生"""
|
"""HP-6: teacher GET /users/ → 200,仅包含名下活跃学生"""
|
||||||
teacher = create_test_user(
|
teacher = create_test_user(
|
||||||
phone='13900100010', password='Teacher1',
|
phone='13900100010', password='Teacher1',
|
||||||
real_name='王老师', role_type='teacher',
|
real_name='王老师', role_type='doctor',
|
||||||
)
|
)
|
||||||
stu_own = create_test_user(
|
stu_own = create_test_user(
|
||||||
phone='13900100011', password='Stu12345',
|
phone='13900100011', password='Stu12345',
|
||||||
@@ -277,7 +277,7 @@ class UserListDetailHappyPathTest(CacheTestCase):
|
|||||||
"""HP-7: 已结束(status=0)的师生关系学生不出现在列表"""
|
"""HP-7: 已结束(status=0)的师生关系学生不出现在列表"""
|
||||||
teacher = create_test_user(
|
teacher = create_test_user(
|
||||||
phone='13900100020', password='Teacher1',
|
phone='13900100020', password='Teacher1',
|
||||||
real_name='李老师', role_type='teacher',
|
real_name='李老师', role_type='doctor',
|
||||||
)
|
)
|
||||||
stu_active = create_test_user(
|
stu_active = create_test_user(
|
||||||
phone='13900100021', password='Stu12345',
|
phone='13900100021', password='Stu12345',
|
||||||
@@ -339,7 +339,7 @@ class UserListDetailHappyPathTest(CacheTestCase):
|
|||||||
"""HP-10: teacher GET /users/{student.id}/ → 200,可查看名下学生"""
|
"""HP-10: teacher GET /users/{student.id}/ → 200,可查看名下学生"""
|
||||||
teacher = create_test_user(
|
teacher = create_test_user(
|
||||||
phone='13900100050', password='Teacher1',
|
phone='13900100050', password='Teacher1',
|
||||||
real_name='赵老师', role_type='teacher',
|
real_name='赵老师', role_type='doctor',
|
||||||
)
|
)
|
||||||
student = create_test_user(
|
student = create_test_user(
|
||||||
phone='13900100051', password='Stu12345',
|
phone='13900100051', password='Stu12345',
|
||||||
@@ -370,7 +370,7 @@ class UserListDetailHappyPathTest(CacheTestCase):
|
|||||||
)
|
)
|
||||||
teacher = create_test_user(
|
teacher = create_test_user(
|
||||||
phone='13900100063', password='Teacher1',
|
phone='13900100063', password='Teacher1',
|
||||||
real_name='张老师', role_type='teacher',
|
real_name='张老师', role_type='doctor',
|
||||||
)
|
)
|
||||||
|
|
||||||
client = get_auth_client(admin)
|
client = get_auth_client(admin)
|
||||||
|
|||||||
@@ -272,16 +272,16 @@ class UserListDetailNegativeTest(CacheTestCase):
|
|||||||
self.assertEqual(resp.status_code, 403, resp.content)
|
self.assertEqual(resp.status_code, 403, resp.content)
|
||||||
self.assertEqual(resp.json()['code'], 'USER_NO_LIST_PERMISSION')
|
self.assertEqual(resp.json()['code'], 'USER_NO_LIST_PERMISSION')
|
||||||
|
|
||||||
def test_doctor_list_403(self):
|
def test_doctor_list_returns_own_students_only(self):
|
||||||
"""N12: doctor GET /users/ → 403 USER_NO_LIST_PERMISSION"""
|
"""N12(新设计): doctor=带教医生 GET /users/ → 200,仅名下学生(无学生时为空列表)"""
|
||||||
doctor = create_test_user(
|
doctor = create_test_user(
|
||||||
phone='13800002002', password='Doc12345',
|
phone='13800002002', password='Doc12345',
|
||||||
real_name='医生', role_type='doctor',
|
real_name='医生', role_type='doctor',
|
||||||
)
|
)
|
||||||
client = get_auth_client(doctor)
|
client = get_auth_client(doctor)
|
||||||
resp = client.get(USER_LIST_URL)
|
resp = client.get(USER_LIST_URL)
|
||||||
self.assertEqual(resp.status_code, 403, resp.content)
|
self.assertEqual(resp.status_code, 200, resp.content)
|
||||||
self.assertEqual(resp.json()['code'], 'USER_NO_LIST_PERMISSION')
|
self.assertEqual(resp.json()['count'], 0) # 无名下学生 → 空
|
||||||
|
|
||||||
def test_unauth_list_401(self):
|
def test_unauth_list_401(self):
|
||||||
"""N13: 未登录 GET /users/ → 401"""
|
"""N13: 未登录 GET /users/ → 401"""
|
||||||
@@ -315,7 +315,7 @@ class UserListDetailNegativeTest(CacheTestCase):
|
|||||||
"""N16: teacher 查看非名下学生详情 → 403 USER_NO_VIEW_PERMISSION"""
|
"""N16: teacher 查看非名下学生详情 → 403 USER_NO_VIEW_PERMISSION"""
|
||||||
teacher = create_test_user(
|
teacher = create_test_user(
|
||||||
phone='13800002030', password='Teacher1',
|
phone='13800002030', password='Teacher1',
|
||||||
real_name='刘老师', role_type='teacher',
|
real_name='刘老师', role_type='doctor',
|
||||||
)
|
)
|
||||||
unrelated = create_test_user(
|
unrelated = create_test_user(
|
||||||
phone='13800002031', password='Stu12345',
|
phone='13800002031', password='Stu12345',
|
||||||
@@ -331,7 +331,7 @@ class UserListDetailNegativeTest(CacheTestCase):
|
|||||||
"""N17: teacher 查看已结束关系学生详情 → 403 USER_NO_VIEW_PERMISSION"""
|
"""N17: teacher 查看已结束关系学生详情 → 403 USER_NO_VIEW_PERMISSION"""
|
||||||
teacher = create_test_user(
|
teacher = create_test_user(
|
||||||
phone='13800002040', password='Teacher1',
|
phone='13800002040', password='Teacher1',
|
||||||
real_name='陈老师', role_type='teacher',
|
real_name='陈老师', role_type='doctor',
|
||||||
)
|
)
|
||||||
student = create_test_user(
|
student = create_test_user(
|
||||||
phone='13800002041', password='Stu12345',
|
phone='13800002041', password='Stu12345',
|
||||||
|
|||||||
Reference in New Issue
Block a user