refactor(views): split file views into summary, detail, process, and annotations pages

- /files/<id> → new summary page with navigation cards
- /files/<id>/detail → document detail with metadata, preview, text
- /files/<id>/process → processing pipeline status and history
- /files/<id>/annotations → comments & annotations with EmbedPDF viewer
- /files/<id>/comments → redirects to /annotations
- Added embed-pdf-viewer as git submodule for PDF annotation viewer
- Updated all navigation links across templates
- Updated all tests to use new URL structure

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
Agent-Logs-Url: https://github.com/christianlouis/DocuElevate/sessions/12276514-bd3d-4e3e-84d9-5977d1f82b19
This commit is contained in:
copilot-swe-agent[bot]
2026-03-22 11:48:57 +00:00
parent 5a3ddcc1f0
commit f852ba9783
14 changed files with 1213 additions and 725 deletions
+61 -35
View File
@@ -1,4 +1,4 @@
"""Tests for the comments and annotations UI on the file view page."""
"""Tests for the comments and annotations UI on the file annotations page."""
import pytest
from fastapi.testclient import TestClient
@@ -7,7 +7,7 @@ from app.models import FileRecord
def _create_file(db_session, tmp_path) -> FileRecord:
"""Create a minimal FileRecord with a real file path for the view page."""
"""Create a minimal FileRecord with a real file path for the annotations page."""
file_path = tmp_path / "test.pdf"
file_path.write_bytes(b"%PDF-1.4")
f = FileRecord(
@@ -26,77 +26,77 @@ def _create_file(db_session, tmp_path) -> FileRecord:
@pytest.mark.unit
class TestCommentsUIRendering:
"""Verify the file view page includes the comments panel HTML."""
"""Verify the file annotations page includes the comments panel HTML."""
def test_view_page_contains_comments_section(self, client: TestClient, db_session, tmp_path):
"""The view page should render the comments panel container."""
def test_annotations_page_contains_comments_section(self, client: TestClient, db_session, tmp_path):
"""The annotations page should render the comments panel container."""
f = _create_file(db_session, tmp_path)
resp = client.get(f"/files/{f.id}")
resp = client.get(f"/files/{f.id}/annotations")
assert resp.status_code == 200
html = resp.text
assert 'id="comments-list"' in html
assert 'id="comment-form"' in html
assert 'id="comment-input"' in html
def test_view_page_contains_annotations_section(self, client: TestClient, db_session, tmp_path):
"""The view page should render the annotations panel container."""
def test_annotations_page_contains_annotations_section(self, client: TestClient, db_session, tmp_path):
"""The annotations page should render the annotations panel container."""
f = _create_file(db_session, tmp_path)
resp = client.get(f"/files/{f.id}")
resp = client.get(f"/files/{f.id}/annotations")
assert resp.status_code == 200
html = resp.text
assert 'id="annotations-list"' in html
assert 'id="annotation-form"' in html
assert 'id="annotation-content-input"' in html
def test_view_page_loads_comments_js(self, client: TestClient, db_session, tmp_path):
"""The view page should include the comments JavaScript file."""
def test_annotations_page_loads_comments_js(self, client: TestClient, db_session, tmp_path):
"""The annotations page should include the comments JavaScript file."""
f = _create_file(db_session, tmp_path)
resp = client.get(f"/files/{f.id}")
resp = client.get(f"/files/{f.id}/annotations")
assert resp.status_code == 200
assert "js/comments.js" in resp.text
def test_view_page_loads_annotations_js(self, client: TestClient, db_session, tmp_path):
"""The view page should include the annotations JavaScript file."""
def test_annotations_page_loads_annotations_js(self, client: TestClient, db_session, tmp_path):
"""The annotations page should include the annotations JavaScript file."""
f = _create_file(db_session, tmp_path)
resp = client.get(f"/files/{f.id}")
resp = client.get(f"/files/{f.id}/annotations")
assert resp.status_code == 200
assert "js/annotations.js" in resp.text
def test_view_page_has_mention_dropdown(self, client: TestClient, db_session, tmp_path):
def test_annotations_page_has_mention_dropdown(self, client: TestClient, db_session, tmp_path):
"""The mention autocomplete dropdown should be present."""
f = _create_file(db_session, tmp_path)
resp = client.get(f"/files/{f.id}")
resp = client.get(f"/files/{f.id}/annotations")
assert resp.status_code == 200
assert 'id="mention-dropdown"' in resp.text
def test_view_page_has_annotation_form_fields(self, client: TestClient, db_session, tmp_path):
def test_annotations_page_has_annotation_form_fields(self, client: TestClient, db_session, tmp_path):
"""Annotation form should have page, type, and color inputs."""
f = _create_file(db_session, tmp_path)
resp = client.get(f"/files/{f.id}")
resp = client.get(f"/files/{f.id}/annotations")
assert resp.status_code == 200
html = resp.text
assert 'id="annotation-page-input"' in html
assert 'id="annotation-type-input"' in html
assert 'id="annotation-color-input"' in html
def test_view_page_has_collab_grid(self, client: TestClient, db_session, tmp_path):
def test_annotations_page_has_collab_grid(self, client: TestClient, db_session, tmp_path):
"""Comments and annotations should be in a side-by-side grid layout."""
f = _create_file(db_session, tmp_path)
resp = client.get(f"/files/{f.id}")
resp = client.get(f"/files/{f.id}/annotations")
assert resp.status_code == 200
assert "collab-grid" in resp.text
def test_view_page_no_comments_for_missing_file(self, client: TestClient):
def test_annotations_page_no_comments_for_missing_file(self, client: TestClient):
"""When file is not found, no comments section should appear."""
resp = client.get("/files/99999")
resp = client.get("/files/99999/annotations")
assert resp.status_code == 200
# The error block is shown, not the main content
assert 'id="comments-list"' not in resp.text
def test_view_page_annotation_type_options(self, client: TestClient, db_session, tmp_path):
def test_annotations_page_annotation_type_options(self, client: TestClient, db_session, tmp_path):
"""Annotation type selector should include all four types."""
f = _create_file(db_session, tmp_path)
resp = client.get(f"/files/{f.id}")
resp = client.get(f"/files/{f.id}/annotations")
assert resp.status_code == 200
html = resp.text
assert 'value="note"' in html
@@ -104,38 +104,64 @@ class TestCommentsUIRendering:
assert 'value="underline"' in html
assert 'value="strikethrough"' in html
def test_view_page_comments_panel_accessibility(self, client: TestClient, db_session, tmp_path):
def test_annotations_page_comments_panel_accessibility(self, client: TestClient, db_session, tmp_path):
"""Comments panel should have proper ARIA attributes."""
f = _create_file(db_session, tmp_path)
resp = client.get(f"/files/{f.id}")
resp = client.get(f"/files/{f.id}/annotations")
assert resp.status_code == 200
html = resp.text
assert 'aria-live="polite"' in html
assert 'role="listbox"' in html
def test_view_page_init_script(self, client: TestClient, db_session, tmp_path):
def test_annotations_page_init_script(self, client: TestClient, db_session, tmp_path):
"""The init script should call initComments and initAnnotations."""
f = _create_file(db_session, tmp_path)
resp = client.get(f"/files/{f.id}")
resp = client.get(f"/files/{f.id}/annotations")
assert resp.status_code == 200
html = resp.text
assert "initComments" in html
assert "initAnnotations" in html
def test_detail_page_no_comments_section(self, client: TestClient, db_session, tmp_path):
"""The detail page should NOT render the comments panel (moved to view page)."""
def test_comments_url_redirects_to_annotations(self, client: TestClient, db_session, tmp_path):
"""The /comments URL should redirect to /annotations."""
f = _create_file(db_session, tmp_path)
resp = client.get(f"/files/{f.id}/detail")
resp = client.get(f"/files/{f.id}/comments", follow_redirects=False)
assert resp.status_code == 302
assert f"/files/{f.id}/annotations" in resp.headers["location"]
def test_process_page_no_comments_section(self, client: TestClient, db_session, tmp_path):
"""The process page should NOT render the comments panel."""
f = _create_file(db_session, tmp_path)
resp = client.get(f"/files/{f.id}/process")
assert resp.status_code == 200
html = resp.text
assert 'id="comments-list"' not in html
assert 'id="comment-form"' not in html
def test_detail_page_no_annotations_section(self, client: TestClient, db_session, tmp_path):
"""The detail page should NOT render the annotations panel (moved to view page)."""
def test_detail_page_no_comments_section(self, client: TestClient, db_session, tmp_path):
"""The detail page should NOT render the comments panel."""
f = _create_file(db_session, tmp_path)
resp = client.get(f"/files/{f.id}/detail")
assert resp.status_code == 200
html = resp.text
assert 'id="annotations-list"' not in html
assert 'id="comments-list"' not in html
assert 'id="annotation-form"' not in html
def test_annotations_page_has_embedpdf_viewer_for_pdf(self, client: TestClient, db_session, tmp_path):
"""The annotations page should include the EmbedPDF viewer for PDF files."""
f = _create_file(db_session, tmp_path)
resp = client.get(f"/files/{f.id}/annotations")
assert resp.status_code == 200
html = resp.text
assert 'id="embedpdf-viewer"' in html
assert "@embedpdf/snippet" in html
def test_summary_page_renders(self, client: TestClient, db_session, tmp_path):
"""The summary page at /files/{id} should render correctly."""
f = _create_file(db_session, tmp_path)
resp = client.get(f"/files/{f.id}")
assert resp.status_code == 200
html = resp.text
assert "Document Detail" in html
assert "Processing" in html
assert "Comments" in html or "Annotations" in html
+18 -18
View File
@@ -57,7 +57,7 @@ class TestFileViewPdfJs:
pdf.write_bytes(b"%PDF-1.4 test")
rec = _create_file_record(db_session, file_path=str(pdf), mime_type="application/pdf")
response = client.get(f"/files/{rec.id}")
response = client.get(f"/files/{rec.id}/detail")
assert response.status_code == 200
html = response.text
@@ -80,7 +80,7 @@ class TestFileViewPdfJs:
pdf.write_bytes(b"%PDF-1.4 test")
rec = _create_file_record(db_session, file_path=str(pdf), mime_type="application/pdf")
response = client.get(f"/files/{rec.id}")
response = client.get(f"/files/{rec.id}/detail")
html = response.text
# The preview section should use pdf-viewer, not iframe
@@ -93,7 +93,7 @@ class TestFileViewPdfJs:
pdf.write_bytes(b"%PDF-1.4 test")
rec = _create_file_record(db_session, file_path=str(pdf), mime_type="application/pdf")
response = client.get(f"/files/{rec.id}")
response = client.get(f"/files/{rec.id}/detail")
html = response.text
assert 'id="pdf-prev-btn"' in html
@@ -116,7 +116,7 @@ class TestFileViewImagePreview:
img.write_bytes(b"\xff\xd8\xff\xe0" + b"\x00" * 100) # minimal JPEG header
rec = _create_file_record(db_session, filename="photo.jpg", mime_type="image/jpeg", file_path=str(img))
response = client.get(f"/files/{rec.id}")
response = client.get(f"/files/{rec.id}/detail")
assert response.status_code == 200
html = response.text
@@ -132,7 +132,7 @@ class TestFileViewImagePreview:
img.write_bytes(b"\x89PNG\r\n\x1a\n" + b"\x00" * 50)
rec = _create_file_record(db_session, filename="photo.png", mime_type="image/png", file_path=str(img))
response = client.get(f"/files/{rec.id}")
response = client.get(f"/files/{rec.id}/detail")
html = response.text
assert 'aria-label="Zoom in"' in html
@@ -146,7 +146,7 @@ class TestFileViewImagePreview:
img.write_bytes(b"RIFF" + b"\x00" * 50)
rec = _create_file_record(db_session, filename="wide.webp", mime_type="image/webp", file_path=str(img))
response = client.get(f"/files/{rec.id}")
response = client.get(f"/files/{rec.id}/detail")
html = response.text
# Pan support is implemented via JavaScript on img-wrap
@@ -170,7 +170,7 @@ class TestFileViewTextPreview:
txt.write_text("Hello world\nSecond line\n")
rec = _create_file_record(db_session, filename="readme.txt", mime_type="text/plain", file_path=str(txt))
response = client.get(f"/files/{rec.id}")
response = client.get(f"/files/{rec.id}/detail")
assert response.status_code == 200
html = response.text
@@ -184,7 +184,7 @@ class TestFileViewTextPreview:
txt.write_text("print('hello')\n")
rec = _create_file_record(db_session, filename="code.py", mime_type="text/x-python", file_path=str(txt))
response = client.get(f"/files/{rec.id}")
response = client.get(f"/files/{rec.id}/detail")
html = response.text
assert "copyTextPreview" in html
@@ -196,7 +196,7 @@ class TestFileViewTextPreview:
txt.write_text("a,b,c\n1,2,3\n")
rec = _create_file_record(db_session, filename="data.csv", mime_type="text/csv", file_path=str(txt))
response = client.get(f"/files/{rec.id}")
response = client.get(f"/files/{rec.id}/detail")
html = response.text
# JS builds line-number spans
@@ -218,7 +218,7 @@ class TestFileViewPreviewIcon:
pdf.write_bytes(b"%PDF-1.4")
rec = _create_file_record(db_session, file_path=str(pdf), mime_type="application/pdf")
html = client.get(f"/files/{rec.id}").text
html = client.get(f"/files/{rec.id}/detail").text
assert "fa-file-pdf" in html
def test_image_icon(self, client: TestClient, db_session, tmp_path):
@@ -227,7 +227,7 @@ class TestFileViewPreviewIcon:
img.write_bytes(b"\xff\xd8\xff\xe0" + b"\x00" * 10)
rec = _create_file_record(db_session, filename="p.jpg", mime_type="image/jpeg", file_path=str(img))
html = client.get(f"/files/{rec.id}").text
html = client.get(f"/files/{rec.id}/detail").text
assert "fa-image" in html
def test_text_icon(self, client: TestClient, db_session, tmp_path):
@@ -236,7 +236,7 @@ class TestFileViewPreviewIcon:
txt.write_text("hello")
rec = _create_file_record(db_session, filename="t.txt", mime_type="text/plain", file_path=str(txt))
html = client.get(f"/files/{rec.id}").text
html = client.get(f"/files/{rec.id}/detail").text
assert "fa-file-code" in html
@@ -321,7 +321,7 @@ class TestFileDetailBottomPreview:
pdf.write_bytes(b"%PDF-1.4")
rec = _create_file_record(db_session, file_path=str(pdf), processed_path=str(pdf))
response = client.get(f"/files/{rec.id}/detail")
response = client.get(f"/files/{rec.id}/process")
assert response.status_code == 200
html = response.text
@@ -335,7 +335,7 @@ class TestFileDetailBottomPreview:
pdf.write_bytes(b"%PDF-1.4")
rec = _create_file_record(db_session, file_path=str(pdf), processed_path=str(pdf))
response = client.get(f"/files/{rec.id}/detail")
response = client.get(f"/files/{rec.id}/process")
html = response.text
assert f"/api/files/{rec.id}/download" in html
@@ -350,7 +350,7 @@ class TestFileDetailBottomPreview:
file_path=str(img),
)
response = client.get(f"/files/{rec.id}/detail")
response = client.get(f"/files/{rec.id}/process")
html = response.text
assert f"/api/files/{rec.id}/preview?version=original" in html
@@ -372,7 +372,7 @@ class TestFileViewOcrText:
rec.ocr_text = "Sample extracted OCR text content"
db_session.commit()
html = client.get(f"/files/{rec.id}").text
html = client.get(f"/files/{rec.id}/detail").text
assert "toggleOcrText" in html
assert "ocr-text-block" in html
assert "Sample extracted OCR text content" in html
@@ -383,7 +383,7 @@ class TestFileViewOcrText:
pdf.write_bytes(b"%PDF-1.4")
rec = _create_file_record(db_session, file_path=str(pdf))
html = client.get(f"/files/{rec.id}").text
html = client.get(f"/files/{rec.id}/detail").text
assert "loadText" in html or "Extract" in html
@@ -409,5 +409,5 @@ class TestFileViewNoFile:
db_session.commit()
db_session.refresh(rec)
html = client.get(f"/files/{rec.id}").text
html = client.get(f"/files/{rec.id}/detail").text
assert "No file available for preview" in html
+3 -3
View File
@@ -447,7 +447,7 @@ class TestFileDetailView:
db_session.commit()
# Test detail view
response = client.get(f"/files/{file_record.id}/detail")
response = client.get(f"/files/{file_record.id}/process")
assert response.status_code == 200
# Check that response contains HTML with file information
assert b"File Information" in response.content
@@ -504,7 +504,7 @@ class TestFileDetailView:
db_session.commit()
# Test detail view
response = client.get(f"/files/{file_record.id}/detail")
response = client.get(f"/files/{file_record.id}/process")
assert response.status_code == 200
# Check that response contains branching visualization elements
assert b"Process Flow Visualization" in response.content
@@ -514,7 +514,7 @@ class TestFileDetailView:
def test_file_detail_view_nonexistent(self, client: TestClient):
"""Test file detail view for nonexistent file."""
response = client.get("/files/99999/detail")
response = client.get("/files/99999/process")
assert response.status_code == 200 # Returns page with error message
assert b"not found" in response.content.lower()
+3 -3
View File
@@ -50,7 +50,7 @@ def test_file_detail_page_with_metadata(client: TestClient, db_session, sample_p
db_session.refresh(file_record)
# Get detail page
response = client.get(f"/files/{file_record.id}/detail")
response = client.get(f"/files/{file_record.id}/process")
assert response.status_code == 200
html = response.text
@@ -85,7 +85,7 @@ def test_file_detail_with_gpt_metadata(client: TestClient, db_session, sample_pd
db_session.refresh(file_record)
# Get detail page
response = client.get(f"/files/{file_record.id}/detail")
response = client.get(f"/files/{file_record.id}/process")
assert response.status_code == 200
html = response.text
@@ -190,7 +190,7 @@ def test_file_detail_shows_file_status_indicators(client: TestClient, db_session
db_session.commit()
db_session.refresh(file_record)
response = client.get(f"/files/{file_record.id}/detail")
response = client.get(f"/files/{file_record.id}/process")
assert response.status_code == 200
html = response.text
+4 -4
View File
@@ -142,7 +142,7 @@ class TestFileDetailPage:
db_session.commit()
# Test file detail page
response = client.get(f"/files/{file_record.id}/detail")
response = client.get(f"/files/{file_record.id}/process")
assert response.status_code == 200
content = response.text
assert "test.pdf" in content
@@ -150,7 +150,7 @@ class TestFileDetailPage:
def test_file_detail_page_with_missing_file(self, client: TestClient, db_session):
"""Test file detail page with non-existent file"""
# Try to access non-existent file
response = client.get("/files/99999/detail")
response = client.get("/files/99999/process")
assert response.status_code == 200
content = response.text
assert "not found" in content.lower()
@@ -193,7 +193,7 @@ class TestFileDetailPage:
db_session.commit()
# Test file detail page
response = client.get(f"/files/{file_record.id}/detail")
response = client.get(f"/files/{file_record.id}/process")
assert response.status_code == 200
content = response.text
assert "create_file_record" in content
@@ -232,7 +232,7 @@ class TestFileDetailPage:
db_session.commit()
# Test file detail page
response = client.get(f"/files/{file_record.id}/detail")
response = client.get(f"/files/{file_record.id}/process")
assert response.status_code == 200
content = response.text
# Should show metadata
+16 -16
View File
@@ -206,12 +206,12 @@ class TestFileDetailPage:
db_session.add(file)
db_session.commit()
response = client.get(f"/files/{file.id}/detail")
response = client.get(f"/files/{file.id}/process")
assert response.status_code == 200
def test_file_detail_page_not_found(self, client: TestClient, db_session):
"""Test file detail page for non-existent file."""
response = client.get("/files/99999/detail")
response = client.get("/files/99999/process")
assert response.status_code == 200 # Still renders template with error
def test_file_detail_page_with_processing_logs(self, client: TestClient, db_session, tmp_path):
@@ -244,7 +244,7 @@ class TestFileDetailPage:
db_session.add(log2)
db_session.commit()
response = client.get(f"/files/{file.id}/detail")
response = client.get(f"/files/{file.id}/process")
assert response.status_code == 200
def test_file_detail_page_with_metadata_json(self, client: TestClient, db_session, tmp_path):
@@ -272,7 +272,7 @@ class TestFileDetailPage:
db_session.add(file)
db_session.commit()
response = client.get(f"/files/{file.id}/detail")
response = client.get(f"/files/{file.id}/process")
assert response.status_code == 200
def test_file_detail_checks_original_file_exists(self, client: TestClient, db_session, tmp_path):
@@ -289,7 +289,7 @@ class TestFileDetailPage:
db_session.add(file)
db_session.commit()
response = client.get(f"/files/{file.id}/detail")
response = client.get(f"/files/{file.id}/process")
assert response.status_code == 200
def test_file_detail_error_handling(self, client: TestClient, db_session):
@@ -1113,7 +1113,7 @@ class TestFileDetailPageAdditional:
db_session.add(file)
db_session.commit()
response = client.get(f"/files/{file.id}/detail")
response = client.get(f"/files/{file.id}/process")
assert response.status_code == 200
def test_file_detail_step_summary_fallback(self, client: TestClient, db_session, tmp_path):
@@ -1144,7 +1144,7 @@ class TestFileDetailPageAdditional:
db_session.commit()
with patch("app.utils.step_manager.get_step_summary", side_effect=Exception("Table not found")):
response = client.get(f"/files/{file.id}/detail")
response = client.get(f"/files/{file.id}/process")
assert response.status_code == 200
def test_file_detail_error_handling(self, client: TestClient, db_session):
@@ -1155,7 +1155,7 @@ class TestFileDetailPageAdditional:
Mock(status_code=200),
]
try:
response = client.get("/files/1/detail")
response = client.get("/files/1/process")
assert response.status_code in (200, 500)
except Exception:
pass
@@ -1638,7 +1638,7 @@ class TestFileDetailNoJsonSidecar:
db_session.add(file)
db_session.commit()
response = client.get(f"/files/{file.id}/detail")
response = client.get(f"/files/{file.id}/process")
assert response.status_code == 200
@@ -1936,7 +1936,7 @@ class TestPipelineInfoInViews:
pipeline = self._make_system_pipeline(db_session)
file_rec = self._make_file(db_session, pipeline_id=None)
response = client.get(f"/files/{file_rec.id}/detail")
response = client.get(f"/files/{file_rec.id}/process")
assert response.status_code == 200
assert b"Standard Processing Pipeline" in response.content
@@ -1946,7 +1946,7 @@ class TestPipelineInfoInViews:
self._make_system_pipeline(db_session)
file_rec = self._make_file(db_session, pipeline_id=None)
response = client.get(f"/files/{file_rec.id}/detail")
response = client.get(f"/files/{file_rec.id}/process")
assert response.status_code == 200
assert b"System Default" in response.content
@@ -1956,28 +1956,28 @@ class TestPipelineInfoInViews:
pipeline = self._make_custom_pipeline(db_session)
file_rec = self._make_file(db_session, pipeline_id=pipeline.id)
response = client.get(f"/files/{file_rec.id}/detail")
response = client.get(f"/files/{file_rec.id}/process")
assert response.status_code == 200
assert b"My Custom Pipeline" in response.content
assert b"Custom" in response.content
def test_file_view_page_includes_pipeline_name(self, client, db_session):
"""GET /files/{id} response body contains the pipeline name in the sidebar."""
"""GET /files/{id}/detail response body contains the pipeline name in the sidebar."""
pipeline = self._make_system_pipeline(db_session)
file_rec = self._make_file(db_session, pipeline_id=None)
response = client.get(f"/files/{file_rec.id}")
response = client.get(f"/files/{file_rec.id}/detail")
assert response.status_code == 200
assert b"Standard Processing Pipeline" in response.content
def test_file_view_page_no_pipeline_shows_standard(self, client, db_session):
"""When no pipeline exists, file view shows 'Standard' fallback text."""
"""When no pipeline exists, file detail view shows 'Standard' fallback text."""
# No pipeline in DB
file_rec = self._make_file(db_session, pipeline_id=None)
response = client.get(f"/files/{file_rec.id}")
response = client.get(f"/files/{file_rec.id}/detail")
assert response.status_code == 200
assert b"Standard" in response.content