From c2b3a3b4f761cfb10fa35d9c5038ba6a5ee95d88 Mon Sep 17 00:00:00 2001 From: bruce Date: Sat, 30 May 2026 00:29:03 +0800 Subject: [PATCH] =?UTF-8?q?feat(documents):=20=E5=A2=9E=E5=BC=BA=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0=E5=8F=8D=E9=A6=88=E4=B8=8E=E7=8A=B6=E6=80=81=E5=B1=95?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/documents/forms.py | 12 ++++++- apps/documents/models.py | 9 +++++ apps/documents/views.py | 10 +++++- templates/base.html | 7 ++++ templates/documents/document_list.html | 3 +- templates/documents/upload.html | 11 +++--- tests/test_documents.py | 48 ++++++++++++++++++++++++++ 7 files changed, 91 insertions(+), 9 deletions(-) diff --git a/apps/documents/forms.py b/apps/documents/forms.py index a0e90c6..92f7f33 100644 --- a/apps/documents/forms.py +++ b/apps/documents/forms.py @@ -3,14 +3,24 @@ from pathlib import Path from django import forms from apps.scenarios.services import ScenarioNotFound, get_scenario +from apps.scenarios.services import list_scenarios SUPPORTED_EXTENSIONS = {".txt", ".md", ".pdf", ".docx"} class DocumentUploadForm(forms.Form): - scenario_id = forms.CharField(label="场景") + # 使用 ChoiceField 让表单自己维护场景选项, + # 这样模板、校验和后续扩展都能围绕一个入口完成。 + scenario_id = forms.ChoiceField(label="场景", choices=()) file = forms.FileField(label="文件") + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["scenario_id"].choices = [ + (scenario["id"], scenario["name"]) + for scenario in list_scenarios() + ] + def clean_scenario_id(self): scenario_id = self.cleaned_data["scenario_id"] try: diff --git a/apps/documents/models.py b/apps/documents/models.py index fd2500e..e111005 100644 --- a/apps/documents/models.py +++ b/apps/documents/models.py @@ -2,6 +2,7 @@ from django.db import models class UploadedDocument(models.Model): + # 文档状态用于驱动前端提示和后续可操作项。 STATUS_UPLOADED = "uploaded" STATUS_INDEXED = "indexed" STATUS_FAILED = "failed" @@ -21,3 +22,11 @@ class UploadedDocument(models.Model): def __str__(self) -> str: return self.original_name + + def get_status_display_text(self) -> str: + """为模板提供更适合演示的中文状态文案。""" + return { + self.STATUS_UPLOADED: "已上传,待入库", + self.STATUS_INDEXED: "已入库,可检索", + self.STATUS_FAILED: "入库失败", + }.get(self.status, self.status) diff --git a/apps/documents/views.py b/apps/documents/views.py index 56fda0a..ee71adf 100644 --- a/apps/documents/views.py +++ b/apps/documents/views.py @@ -1,3 +1,4 @@ +from django.contrib import messages from django.shortcuts import get_object_or_404, redirect, render from django.views.decorators.http import require_POST @@ -9,15 +10,18 @@ from .services import create_uploaded_document, index_document def document_list(request): + # 列表页只负责展示文档元数据和可执行操作,不处理入库细节。 documents = UploadedDocument.objects.all() return render(request, "documents/document_list.html", {"documents": documents}) def upload(request): + # 上传成功后仅保存文件和元数据,是否入库由用户显式触发。 if request.method == "POST": form = DocumentUploadForm(request.POST, request.FILES) if form.is_valid(): create_uploaded_document(form.cleaned_data["scenario_id"], form.cleaned_data["file"]) + messages.success(request, "文件已上传,可继续执行入库。") return redirect("documents:list") else: form = DocumentUploadForm() @@ -31,5 +35,9 @@ def upload(request): @require_POST def index(request, document_id: int): document = get_object_or_404(UploadedDocument, pk=document_id) - index_document(document) + document = index_document(document) + if document.status == UploadedDocument.STATUS_INDEXED: + messages.success(request, "文档入库成功,当前文档已可参与检索。") + else: + messages.error(request, "文档入库失败,请检查错误原因后重试。") return redirect("documents:list") diff --git a/templates/base.html b/templates/base.html index 1545237..1f915fc 100644 --- a/templates/base.html +++ b/templates/base.html @@ -338,6 +338,13 @@ 审计日志 + {% if messages %} +
+ {% for message in messages %} +
{{ message }}
+ {% endfor %} +
+ {% endif %} {% block content %}{% endblock %} diff --git a/templates/documents/document_list.html b/templates/documents/document_list.html index 5e76cfb..3d9eca1 100644 --- a/templates/documents/document_list.html +++ b/templates/documents/document_list.html @@ -29,7 +29,7 @@ {{ document.scenario_id }} {{ document.file_type }} {{ document.size }} - {{ document.status }} + {{ document.get_status_display_text }} {% if document.status != "indexed" %}
@@ -42,6 +42,7 @@ {% if document.error_message %}
{{ document.error_message }}
{% endif %} +

上传时间:{{ document.created_at|date:"Y-m-d H:i" }}

{% empty %} diff --git a/templates/documents/upload.html b/templates/documents/upload.html index 72499b7..37883be 100644 --- a/templates/documents/upload.html +++ b/templates/documents/upload.html @@ -13,12 +13,11 @@ {% csrf_token %}
- - + {{ form.scenario_id.label_tag }} + {{ form.scenario_id }} + {% if form.scenario_id.errors %} +

{{ form.scenario_id.errors|join:" " }}

+ {% endif %}
{{ form.file.label_tag }} diff --git a/tests/test_documents.py b/tests/test_documents.py index 7429540..a6b2b3c 100644 --- a/tests/test_documents.py +++ b/tests/test_documents.py @@ -1,6 +1,7 @@ from django.core.files.uploadedfile import SimpleUploadedFile from django.urls import reverse +from apps.documents.forms import DocumentUploadForm from apps.documents.models import UploadedDocument from apps.documents.services import extract_text @@ -20,6 +21,19 @@ def test_upload_txt_document_creates_uploaded_record(client, db): assert document.scenario_id == "knowledge_qa" +def test_upload_redirect_shows_success_message(client, db): + file = SimpleUploadedFile("notice.txt", "hello".encode("utf-8"), content_type="text/plain") + + response = client.post( + reverse("documents:upload"), + {"scenario_id": "knowledge_qa", "file": file}, + follow=True, + ) + + assert response.status_code == 200 + assert "文件已上传,可继续执行入库" in response.content.decode("utf-8") + + def test_upload_accepts_pdf_and_docx_documents(client, db): for filename, payload in [ ("policy.pdf", b"%PDF-1.4\nplain policy text"), @@ -80,3 +94,37 @@ def test_extract_text_supports_pdf_and_docx_plain_text_fallback(db): assert "Safety policy" in extract_text(pdf_document) assert "Contract clause review" in extract_text(docx_document) + + +def test_document_upload_form_builds_scenario_choices(): + form = DocumentUploadForm() + + choice_values = [value for value, _label in form.fields["scenario_id"].choices] + + assert "knowledge_qa" in choice_values + assert "quality_analysis" in choice_values + + +def test_index_failure_message_is_visible_on_document_list(client, db, monkeypatch): + document = UploadedDocument.objects.create( + scenario_id="knowledge_qa", + original_name="broken.md", + file_type="md", + size=5, + status="uploaded", + ) + + def fake_index_document(target_document): + target_document.status = UploadedDocument.STATUS_FAILED + target_document.error_message = "模拟入库失败" + target_document.save(update_fields=["status", "error_message", "updated_at"]) + return target_document + + monkeypatch.setattr("apps.documents.views.index_document", fake_index_document) + + response = client.post(reverse("documents:index", args=[document.id]), follow=True) + + content = response.content.decode("utf-8") + assert response.status_code == 200 + assert "文档入库失败,请检查错误原因后重试" in content + assert "模拟入库失败" in content