feat(regulatory): 增加适用条件确认暂停恢复
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
# Generated by Django 5.2.14 on 2026-06-07 01:15
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("review_agent", "0003_regulatoryartifact_regulatoryissue_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="regulatoryreviewbatch",
|
||||
name="condition_json",
|
||||
field=models.JSONField(blank=True, default=dict),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="regulatoryreviewbatch",
|
||||
name="status",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("pending", "待执行"),
|
||||
("running", "执行中"),
|
||||
("waiting_user", "等待用户确认"),
|
||||
("success", "成功"),
|
||||
("failed", "失败"),
|
||||
],
|
||||
default="pending",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="workflownoderun",
|
||||
name="status",
|
||||
field=models.CharField(
|
||||
choices=[
|
||||
("pending", "等待中"),
|
||||
("running", "执行中"),
|
||||
("waiting_user", "等待用户确认"),
|
||||
("retrying", "重试中"),
|
||||
("success", "成功"),
|
||||
("failed", "失败"),
|
||||
("skipped", "跳过"),
|
||||
],
|
||||
default="pending",
|
||||
max_length=20,
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -253,6 +253,7 @@ class WorkflowNodeRun(models.Model):
|
||||
class Status(models.TextChoices):
|
||||
PENDING = "pending", "等待中"
|
||||
RUNNING = "running", "执行中"
|
||||
WAITING_USER = "waiting_user", "等待用户确认"
|
||||
RETRYING = "retrying", "重试中"
|
||||
SUCCESS = "success", "成功"
|
||||
FAILED = "failed", "失败"
|
||||
@@ -402,6 +403,7 @@ class RegulatoryReviewBatch(models.Model):
|
||||
class Status(models.TextChoices):
|
||||
PENDING = "pending", "待执行"
|
||||
RUNNING = "running", "执行中"
|
||||
WAITING_USER = "waiting_user", "等待用户确认"
|
||||
SUCCESS = "success", "成功"
|
||||
FAILED = "failed", "失败"
|
||||
|
||||
@@ -436,6 +438,7 @@ class RegulatoryReviewBatch(models.Model):
|
||||
)
|
||||
batch_no = models.CharField(max_length=64, unique=True)
|
||||
status = models.CharField(max_length=20, choices=Status.choices, default=Status.PENDING)
|
||||
condition_json = models.JSONField(default=dict, blank=True)
|
||||
risk_summary = models.JSONField(default=dict, blank=True)
|
||||
work_dir = models.CharField(max_length=500, blank=True, default="")
|
||||
error_message = models.TextField(blank=True, default="")
|
||||
|
||||
81
review_agent/regulatory_review/services/info_extract.py
Normal file
81
review_agent/regulatory_review/services/info_extract.py
Normal file
@@ -0,0 +1,81 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from review_agent.models import FileSummaryBatch
|
||||
|
||||
|
||||
OPTION_FIELDS = {
|
||||
"product_category": ["体外诊断试剂", "医疗器械", "其他"],
|
||||
"registration_type": ["首次注册", "变更注册", "延续注册"],
|
||||
"clinical_evaluation_path": ["临床试验", "免临床", "同品种比对", "待确认"],
|
||||
}
|
||||
|
||||
|
||||
def detect_regulatory_condition_candidates(summary_batch: FileSummaryBatch) -> dict[str, dict[str, object]]:
|
||||
"""Infers review-scope conditions from the summary batch and file names."""
|
||||
|
||||
corpus_parts = [summary_batch.product_name or ""]
|
||||
for item in summary_batch.items.order_by("file_index"):
|
||||
corpus_parts.extend([item.directory_level, item.file_name, item.relative_path])
|
||||
corpus = "\n".join(part for part in corpus_parts if part)
|
||||
|
||||
return {
|
||||
"product_category": {
|
||||
"label": "产品类别",
|
||||
"input_type": "select",
|
||||
"options": OPTION_FIELDS["product_category"],
|
||||
"suggested": _detect_product_category(corpus),
|
||||
},
|
||||
"registration_type": {
|
||||
"label": "注册类型",
|
||||
"input_type": "select",
|
||||
"options": OPTION_FIELDS["registration_type"],
|
||||
"suggested": _detect_registration_type(corpus),
|
||||
},
|
||||
"clinical_evaluation_path": {
|
||||
"label": "临床评价路径",
|
||||
"input_type": "select",
|
||||
"options": OPTION_FIELDS["clinical_evaluation_path"],
|
||||
"suggested": _detect_clinical_path(corpus),
|
||||
},
|
||||
"product_name": {
|
||||
"label": "产品名称",
|
||||
"input_type": "text",
|
||||
"suggested": summary_batch.product_name or "",
|
||||
},
|
||||
"model_spec": {
|
||||
"label": "型号规格",
|
||||
"input_type": "text",
|
||||
"suggested": "",
|
||||
},
|
||||
"intended_use": {
|
||||
"label": "预期用途",
|
||||
"input_type": "text",
|
||||
"suggested": "",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _detect_product_category(corpus: str) -> str:
|
||||
if any(keyword in corpus for keyword in ["体外诊断", "检测试剂", "试剂盒", "IVD"]):
|
||||
return "体外诊断试剂"
|
||||
if "医疗器械" in corpus:
|
||||
return "医疗器械"
|
||||
return "其他"
|
||||
|
||||
|
||||
def _detect_registration_type(corpus: str) -> str:
|
||||
if "延续" in corpus:
|
||||
return "延续注册"
|
||||
if "变更" in corpus:
|
||||
return "变更注册"
|
||||
return "首次注册"
|
||||
|
||||
|
||||
def _detect_clinical_path(corpus: str) -> str:
|
||||
if "免临床" in corpus or "免于临床" in corpus:
|
||||
return "免临床"
|
||||
if "同品种" in corpus or "同类" in corpus:
|
||||
return "同品种比对"
|
||||
if "临床试验" in corpus:
|
||||
return "临床试验"
|
||||
return "待确认"
|
||||
@@ -1,10 +1,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import Http404, JsonResponse
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from django.contrib.auth.decorators import login_required
|
||||
|
||||
from review_agent.models import RegulatoryReviewBatch, WorkflowNodeRun
|
||||
from review_agent.regulatory_review.events import record_event
|
||||
from review_agent.regulatory_review.workflow import start_regulatory_review_workflow
|
||||
|
||||
|
||||
@require_http_methods(["GET"])
|
||||
@@ -43,6 +48,59 @@ def batch_status(request, batch_id: int):
|
||||
)
|
||||
|
||||
|
||||
@require_http_methods(["POST"])
|
||||
@login_required
|
||||
def confirm_conditions(request, batch_id: int):
|
||||
batch = RegulatoryReviewBatch.objects.filter(pk=batch_id, user=request.user).first()
|
||||
if not batch:
|
||||
raise Http404("批次不存在。")
|
||||
try:
|
||||
payload = json.loads(request.body.decode("utf-8") or "{}")
|
||||
except json.JSONDecodeError:
|
||||
return JsonResponse({"error": "请求体不是有效 JSON。"}, status=400)
|
||||
conditions = payload.get("conditions")
|
||||
if not isinstance(conditions, dict):
|
||||
return JsonResponse({"error": "conditions 必须是对象。"}, status=400)
|
||||
|
||||
batch.condition_json = {
|
||||
**(batch.condition_json or {}),
|
||||
"confirmed": True,
|
||||
"confirmed_conditions": _normalize_conditions(conditions),
|
||||
}
|
||||
batch.status = RegulatoryReviewBatch.Status.RUNNING
|
||||
batch.save(update_fields=["condition_json", "status"])
|
||||
WorkflowNodeRun.objects.filter(
|
||||
workflow_type="regulatory_review",
|
||||
workflow_batch_id=batch.pk,
|
||||
node_code="condition_confirm",
|
||||
).update(
|
||||
status=WorkflowNodeRun.Status.SUCCESS,
|
||||
progress=100,
|
||||
message="适用条件已确认",
|
||||
)
|
||||
record_event(
|
||||
batch,
|
||||
"condition_confirmed",
|
||||
{"conditions": batch.condition_json["confirmed_conditions"], "resume_from": "rule_scope"},
|
||||
)
|
||||
start_regulatory_review_workflow(
|
||||
batch,
|
||||
async_run=getattr(settings, "REGULATORY_REVIEW_ASYNC", True),
|
||||
)
|
||||
batch.refresh_from_db()
|
||||
return JsonResponse(
|
||||
{
|
||||
"batch": {
|
||||
"id": batch.pk,
|
||||
"workflow_type": "regulatory_review",
|
||||
"batch_no": batch.batch_no,
|
||||
"status": batch.status,
|
||||
"condition_json": batch.condition_json,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _format_risk_summary(risk_summary: dict) -> str:
|
||||
labels = [
|
||||
("blocking", "阻断项"),
|
||||
@@ -56,3 +114,15 @@ def _format_risk_summary(risk_summary: dict) -> str:
|
||||
for key, label in labels
|
||||
if int(risk_summary.get(key) or 0)
|
||||
)
|
||||
|
||||
|
||||
def _normalize_conditions(conditions: dict) -> dict[str, str]:
|
||||
allowed = [
|
||||
"product_category",
|
||||
"registration_type",
|
||||
"clinical_evaluation_path",
|
||||
"product_name",
|
||||
"model_spec",
|
||||
"intended_use",
|
||||
]
|
||||
return {key: str(conditions.get(key) or "").strip() for key in allowed}
|
||||
|
||||
@@ -19,6 +19,7 @@ from review_agent.models import (
|
||||
from review_agent.regulatory_review.services.completeness_check import run_completeness_check
|
||||
from review_agent.regulatory_review.services.consistency_check import run_consistency_check
|
||||
from review_agent.regulatory_review.services.export import build_assistant_summary, export_review_results
|
||||
from review_agent.regulatory_review.services.info_extract import detect_regulatory_condition_candidates
|
||||
from review_agent.regulatory_review.services.risk_assess import persist_findings
|
||||
from review_agent.regulatory_review.services.rule_loader import load_rule_file
|
||||
from review_agent.regulatory_review.services.structure_check import run_structure_check
|
||||
@@ -29,6 +30,7 @@ from .events import record_event
|
||||
|
||||
NODE_DEFINITIONS = [
|
||||
("prepare", "准备", "prepare"),
|
||||
("condition_confirm", "适用条件确认", "condition_confirm"),
|
||||
("rule_scope", "规则范围", "rule_scope"),
|
||||
("completeness_check", "完整性核查", "completeness_check"),
|
||||
("text_extract", "文本抽取", "text_extract"),
|
||||
@@ -43,6 +45,10 @@ NODE_DEFINITIONS = [
|
||||
logger = logging.getLogger("review_agent.regulatory_review.workflow")
|
||||
|
||||
|
||||
class WorkflowPausedForUser(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def build_batch_no() -> str:
|
||||
return f"RR-{timezone.localtime().strftime('%Y%m%d%H%M%S')}-{uuid4().hex[:6]}"
|
||||
|
||||
@@ -108,7 +114,11 @@ class RegulatoryWorkflowExecutor:
|
||||
|
||||
try:
|
||||
for node in self._nodes():
|
||||
if node.status == WorkflowNodeRun.Status.SUCCESS:
|
||||
continue
|
||||
self._run_node(node)
|
||||
except WorkflowPausedForUser:
|
||||
return
|
||||
except Exception as exc:
|
||||
logger.exception("Regulatory workflow failed", extra={"batch_id": self.batch.pk})
|
||||
self.batch.status = RegulatoryReviewBatch.Status.FAILED
|
||||
@@ -155,6 +165,9 @@ class RegulatoryWorkflowExecutor:
|
||||
)
|
||||
|
||||
def _execute_node(self, node_code: str) -> None:
|
||||
if node_code == "condition_confirm":
|
||||
self._pause_for_condition_confirmation()
|
||||
return
|
||||
if node_code == "rule_scope":
|
||||
self.rule_set = load_rule_file()
|
||||
return
|
||||
@@ -181,6 +194,34 @@ class RegulatoryWorkflowExecutor:
|
||||
content=build_assistant_summary(self.batch, exports),
|
||||
)
|
||||
|
||||
def _pause_for_condition_confirmation(self) -> None:
|
||||
if self.batch.condition_json.get("confirmed"):
|
||||
return
|
||||
candidates = detect_regulatory_condition_candidates(self.batch.source_summary_batch)
|
||||
self.batch.condition_json = {
|
||||
**(self.batch.condition_json or {}),
|
||||
"confirmed": False,
|
||||
"resume_from": "rule_scope",
|
||||
"candidates": candidates,
|
||||
}
|
||||
self.batch.status = RegulatoryReviewBatch.Status.WAITING_USER
|
||||
self.batch.save(update_fields=["status", "condition_json"])
|
||||
node = WorkflowNodeRun.objects.get(
|
||||
workflow_type="regulatory_review",
|
||||
workflow_batch_id=self.batch.pk,
|
||||
node_code="condition_confirm",
|
||||
)
|
||||
node.status = WorkflowNodeRun.Status.WAITING_USER
|
||||
node.progress = 50
|
||||
node.message = "请确认产品类别、注册类型、临床评价路径等适用条件"
|
||||
node.save(update_fields=["status", "progress", "message"])
|
||||
record_event(
|
||||
self.batch,
|
||||
"waiting_user",
|
||||
{"node_code": "condition_confirm", "candidates": candidates, "resume_from": "rule_scope"},
|
||||
)
|
||||
raise WorkflowPausedForUser()
|
||||
|
||||
def _rules(self) -> dict:
|
||||
if self.rule_set is None:
|
||||
self.rule_set = load_rule_file()
|
||||
|
||||
@@ -10,7 +10,10 @@ from .file_summary.views import (
|
||||
conversation_messages,
|
||||
export_download,
|
||||
)
|
||||
from .regulatory_review.views import batch_status as regulatory_review_batch_status
|
||||
from .regulatory_review.views import (
|
||||
batch_status as regulatory_review_batch_status,
|
||||
confirm_conditions as regulatory_review_confirm_conditions,
|
||||
)
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
@@ -64,4 +67,9 @@ urlpatterns = [
|
||||
regulatory_review_batch_status,
|
||||
name="regulatory_review_batch_status",
|
||||
),
|
||||
path(
|
||||
"api/review-agent/regulatory-review/<int:batch_id>/conditions/",
|
||||
regulatory_review_confirm_conditions,
|
||||
name="regulatory_review_confirm_conditions",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -138,6 +138,8 @@ def build_workflow_cards(conversation: Conversation) -> list[dict[str, object]]:
|
||||
"status": batch.status,
|
||||
"error_message": batch.error_message,
|
||||
"risk_label": _format_risk_label(batch.risk_summary or {}),
|
||||
"condition_json": batch.condition_json or {},
|
||||
"condition_candidates": (batch.condition_json or {}).get("candidates") or {},
|
||||
"created_at": batch.created_at,
|
||||
"nodes": list(
|
||||
WorkflowNodeRun.objects.filter(
|
||||
|
||||
@@ -796,6 +796,60 @@
|
||||
});
|
||||
}
|
||||
|
||||
function bindConditionConfirmForms() {
|
||||
document.querySelectorAll("[data-condition-confirm-form]").forEach(function (form) {
|
||||
if (form.dataset.bound === "true") {
|
||||
return;
|
||||
}
|
||||
form.dataset.bound = "true";
|
||||
form.addEventListener("submit", async function (event) {
|
||||
event.preventDefault();
|
||||
var batchId = form.getAttribute("data-batch-id");
|
||||
var status = form.querySelector("[data-condition-confirm-status]");
|
||||
var submitButton = form.querySelector('button[type="submit"]');
|
||||
var formData = new FormData(form);
|
||||
var conditions = {};
|
||||
formData.forEach(function (value, key) {
|
||||
if (key !== "csrfmiddlewaretoken") {
|
||||
conditions[key] = value;
|
||||
}
|
||||
});
|
||||
if (submitButton) {
|
||||
submitButton.disabled = true;
|
||||
}
|
||||
if (status) {
|
||||
status.textContent = "正在恢复法规核查...";
|
||||
}
|
||||
try {
|
||||
var response = await fetch(form.getAttribute("data-confirm-url"), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"X-CSRFToken": formData.get("csrfmiddlewaretoken"),
|
||||
},
|
||||
body: JSON.stringify({ conditions: conditions }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error("确认失败。");
|
||||
}
|
||||
if (status) {
|
||||
status.textContent = "已确认,工作流继续执行。";
|
||||
}
|
||||
form.classList.add("confirmed");
|
||||
startWorkflowPolling(batchId);
|
||||
await refreshWorkflowCard(batchId, "regulatory_review");
|
||||
} catch (error) {
|
||||
if (status) {
|
||||
status.textContent = "确认失败,请稍后重试。";
|
||||
}
|
||||
if (submitButton) {
|
||||
submitButton.disabled = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function streamChat(event) {
|
||||
event.preventDefault();
|
||||
if (!composer || !promptInput || !sendButton || !chatStage) {
|
||||
@@ -955,6 +1009,7 @@
|
||||
renderExistingAssistantMessages();
|
||||
refreshWorkflowBatchCarousel(0);
|
||||
bindWorkflowBatchCarouselControls();
|
||||
bindConditionConfirmForms();
|
||||
refreshRunningWorkflowCards();
|
||||
|
||||
if (chatScroll) {
|
||||
|
||||
@@ -240,6 +240,33 @@
|
||||
{% if batch.error_message %}
|
||||
<p class="workflow-error">{{ batch.error_message }}</p>
|
||||
{% endif %}
|
||||
{% if batch.workflow_type == "regulatory_review" and batch.status == "waiting_user" and batch.condition_candidates %}
|
||||
<form
|
||||
class="condition-confirm-form"
|
||||
data-condition-confirm-form
|
||||
data-batch-id="{{ batch.id }}"
|
||||
data-confirm-url="/api/review-agent/regulatory-review/{{ batch.id }}/conditions/"
|
||||
>
|
||||
{% csrf_token %}
|
||||
<strong>适用条件确认</strong>
|
||||
{% for field, config in batch.condition_candidates.items %}
|
||||
<label>
|
||||
<span>{{ config.label }}</span>
|
||||
{% if config.input_type == "select" %}
|
||||
<select name="{{ field }}">
|
||||
{% for option in config.options %}
|
||||
<option value="{{ option }}"{% if option == config.suggested %} selected{% endif %}>{{ option }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% else %}
|
||||
<input type="text" name="{{ field }}" value="{{ config.suggested|default:'' }}">
|
||||
{% endif %}
|
||||
</label>
|
||||
{% endfor %}
|
||||
<button type="submit">确认并继续</button>
|
||||
<p class="condition-confirm-status" data-condition-confirm-status></p>
|
||||
</form>
|
||||
{% endif %}
|
||||
<ol>
|
||||
{% for node in batch.nodes %}
|
||||
<li class="node-status status-{{ node.status }}" data-node-code="{{ node.node_code }}">
|
||||
|
||||
139
tests/test_regulatory_condition.py
Normal file
139
tests/test_regulatory_condition.py
Normal file
@@ -0,0 +1,139 @@
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from django.urls import reverse
|
||||
|
||||
from review_agent.models import (
|
||||
Conversation,
|
||||
FileSummaryBatch,
|
||||
FileSummaryItem,
|
||||
RegulatoryReviewBatch,
|
||||
WorkflowEvent,
|
||||
WorkflowNodeRun,
|
||||
)
|
||||
from review_agent.regulatory_review.services.info_extract import detect_regulatory_condition_candidates
|
||||
from review_agent.regulatory_review.workflow import (
|
||||
create_regulatory_review_batch,
|
||||
start_regulatory_review_workflow,
|
||||
)
|
||||
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_detect_regulatory_condition_candidates_from_summary_items(django_user_model):
|
||||
user = django_user_model.objects.create_user(username="owner", password="pass")
|
||||
conversation = Conversation.objects.create(user=user, title="会话")
|
||||
summary = FileSummaryBatch.objects.create(
|
||||
conversation=conversation,
|
||||
user=user,
|
||||
batch_no="FS-COND",
|
||||
status=FileSummaryBatch.Status.SUCCESS,
|
||||
product_name="甲胎蛋白检测试剂盒",
|
||||
)
|
||||
FileSummaryItem.objects.create(
|
||||
batch=summary,
|
||||
file_index=1,
|
||||
directory_level="临床评价资料",
|
||||
file_name="免临床评价资料.docx",
|
||||
file_type="docx",
|
||||
relative_path="4.临床评价资料/免临床评价资料.docx",
|
||||
storage_path="missing.docx",
|
||||
)
|
||||
|
||||
candidates = detect_regulatory_condition_candidates(summary)
|
||||
|
||||
assert candidates["product_category"]["suggested"] == "体外诊断试剂"
|
||||
assert candidates["registration_type"]["suggested"] == "首次注册"
|
||||
assert candidates["clinical_evaluation_path"]["suggested"] == "免临床"
|
||||
assert candidates["product_name"]["suggested"] == "甲胎蛋白检测试剂盒"
|
||||
|
||||
|
||||
def test_workflow_pauses_before_rule_scope_until_conditions_confirmed(settings, tmp_path, django_user_model):
|
||||
settings.MEDIA_ROOT = tmp_path
|
||||
user = django_user_model.objects.create_user(username="owner", password="pass")
|
||||
conversation = Conversation.objects.create(user=user, title="会话")
|
||||
summary = FileSummaryBatch.objects.create(
|
||||
conversation=conversation,
|
||||
user=user,
|
||||
batch_no="FS-COND",
|
||||
status=FileSummaryBatch.Status.SUCCESS,
|
||||
product_name="甲胎蛋白检测试剂盒",
|
||||
)
|
||||
batch = create_regulatory_review_batch(
|
||||
conversation=conversation,
|
||||
user=user,
|
||||
source_summary_batch=summary,
|
||||
)
|
||||
|
||||
start_regulatory_review_workflow(batch, async_run=False)
|
||||
|
||||
batch.refresh_from_db()
|
||||
condition_node = WorkflowNodeRun.objects.get(
|
||||
workflow_type="regulatory_review",
|
||||
workflow_batch_id=batch.pk,
|
||||
node_code="condition_confirm",
|
||||
)
|
||||
rule_scope_node = WorkflowNodeRun.objects.get(
|
||||
workflow_type="regulatory_review",
|
||||
workflow_batch_id=batch.pk,
|
||||
node_code="rule_scope",
|
||||
)
|
||||
assert batch.status == RegulatoryReviewBatch.Status.WAITING_USER
|
||||
assert condition_node.status == WorkflowNodeRun.Status.WAITING_USER
|
||||
assert rule_scope_node.status == WorkflowNodeRun.Status.PENDING
|
||||
assert batch.condition_json["candidates"]["product_category"]["suggested"] == "体外诊断试剂"
|
||||
assert WorkflowEvent.objects.filter(
|
||||
workflow_type="regulatory_review",
|
||||
workflow_batch_id=batch.pk,
|
||||
event_type="waiting_user",
|
||||
).exists()
|
||||
|
||||
|
||||
def test_confirm_conditions_endpoint_resumes_workflow(client, settings, tmp_path, django_user_model):
|
||||
settings.MEDIA_ROOT = tmp_path
|
||||
settings.REGULATORY_REVIEW_ASYNC = False
|
||||
user = django_user_model.objects.create_user(username="owner", password="pass")
|
||||
conversation = Conversation.objects.create(user=user, title="会话")
|
||||
summary = FileSummaryBatch.objects.create(
|
||||
conversation=conversation,
|
||||
user=user,
|
||||
batch_no="FS-COND",
|
||||
status=FileSummaryBatch.Status.SUCCESS,
|
||||
product_name="甲胎蛋白检测试剂盒",
|
||||
)
|
||||
batch = create_regulatory_review_batch(
|
||||
conversation=conversation,
|
||||
user=user,
|
||||
source_summary_batch=summary,
|
||||
)
|
||||
start_regulatory_review_workflow(batch, async_run=False)
|
||||
client.force_login(user)
|
||||
|
||||
response = client.post(
|
||||
reverse("regulatory_review_confirm_conditions", args=[batch.pk]),
|
||||
data=json.dumps(
|
||||
{
|
||||
"conditions": {
|
||||
"product_category": "体外诊断试剂",
|
||||
"registration_type": "首次注册",
|
||||
"clinical_evaluation_path": "免临床",
|
||||
"product_name": "甲胎蛋白检测试剂盒",
|
||||
"model_spec": "卡型",
|
||||
"intended_use": "用于甲胎蛋白检测",
|
||||
}
|
||||
}
|
||||
),
|
||||
content_type="application/json",
|
||||
)
|
||||
|
||||
batch.refresh_from_db()
|
||||
assert response.status_code == 200
|
||||
assert response.json()["batch"]["status"] == RegulatoryReviewBatch.Status.SUCCESS
|
||||
assert batch.condition_json["confirmed"] is True
|
||||
assert batch.condition_json["confirmed_conditions"]["model_spec"] == "卡型"
|
||||
assert WorkflowNodeRun.objects.get(
|
||||
workflow_type="regulatory_review",
|
||||
workflow_batch_id=batch.pk,
|
||||
node_code="condition_confirm",
|
||||
).status == WorkflowNodeRun.Status.SUCCESS
|
||||
@@ -50,9 +50,63 @@ def test_workspace_renders_regulatory_workflow_card(client, django_user_model):
|
||||
assert "data-regulatory-status-url-template" in content
|
||||
|
||||
|
||||
def test_workspace_renders_condition_confirmation_form(client, django_user_model):
|
||||
user = django_user_model.objects.create_user(username="owner", password="pass")
|
||||
conversation = Conversation.objects.create(user=user, title="会话")
|
||||
summary = FileSummaryBatch.objects.create(
|
||||
conversation=conversation,
|
||||
user=user,
|
||||
batch_no="FS-OK",
|
||||
status=FileSummaryBatch.Status.SUCCESS,
|
||||
)
|
||||
regulatory = RegulatoryReviewBatch.objects.create(
|
||||
conversation=conversation,
|
||||
user=user,
|
||||
source_summary_batch=summary,
|
||||
batch_no="RR-WAIT",
|
||||
status=RegulatoryReviewBatch.Status.WAITING_USER,
|
||||
condition_json={
|
||||
"confirmed": False,
|
||||
"candidates": {
|
||||
"product_category": {
|
||||
"label": "产品类别",
|
||||
"input_type": "select",
|
||||
"options": ["体外诊断试剂", "医疗器械", "其他"],
|
||||
"suggested": "体外诊断试剂",
|
||||
},
|
||||
"product_name": {
|
||||
"label": "产品名称",
|
||||
"input_type": "text",
|
||||
"suggested": "甲胎蛋白检测试剂盒",
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
WorkflowNodeRun.objects.create(
|
||||
workflow_type="regulatory_review",
|
||||
workflow_batch_id=regulatory.pk,
|
||||
node_group="condition_confirm",
|
||||
node_code="condition_confirm",
|
||||
node_name="适用条件确认",
|
||||
status=WorkflowNodeRun.Status.WAITING_USER,
|
||||
progress=50,
|
||||
)
|
||||
client.force_login(user)
|
||||
|
||||
response = client.get(f"{reverse('home')}?conversation={conversation.pk}")
|
||||
|
||||
content = response.content.decode("utf-8")
|
||||
assert "适用条件确认" in content
|
||||
assert "data-condition-confirm-form" in content
|
||||
assert "体外诊断试剂" in content
|
||||
assert "甲胎蛋白检测试剂盒" in content
|
||||
|
||||
|
||||
def test_frontend_selects_status_url_by_workflow_type():
|
||||
script = open("static/js/app.js", encoding="utf-8").read()
|
||||
|
||||
assert "workflow_type" in script
|
||||
assert "data-regulatory-status-url-template" in script
|
||||
assert "statusUrlForWorkflow" in script
|
||||
assert "bindConditionConfirmForms" in script
|
||||
assert "data-condition-confirm-form" in script
|
||||
|
||||
@@ -102,6 +102,8 @@ def test_start_regulatory_review_workflow_runs_synchronously(django_user_model):
|
||||
user=user,
|
||||
source_summary_batch=summary,
|
||||
)
|
||||
batch.condition_json = {"confirmed": True, "confirmed_conditions": {"product_category": "体外诊断试剂"}}
|
||||
batch.save(update_fields=["condition_json"])
|
||||
|
||||
start_regulatory_review_workflow(batch, async_run=False)
|
||||
|
||||
@@ -187,6 +189,8 @@ def test_workflow_generates_issues_exports_and_assistant_summary(settings, tmp_p
|
||||
user=user,
|
||||
source_summary_batch=summary,
|
||||
)
|
||||
batch.condition_json = {"confirmed": True, "confirmed_conditions": {"product_category": "体外诊断试剂"}}
|
||||
batch.save(update_fields=["condition_json"])
|
||||
|
||||
start_regulatory_review_workflow(batch, async_run=False)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user