Merge pull request #555 from christianlouis/copilot/restructure-help-page
feat(help): replace MkDocs redirect with user-facing Help Center + Zammad integration
This commit is contained in:
@@ -478,3 +478,15 @@ EMBEDDING_MODEL=text-embedding-3-small
|
||||
# Maximum tokens to send to the embedding model. Set below the model's
|
||||
# context window (e.g. 8000 for an 8192-token model).
|
||||
EMBEDDING_MAX_TOKENS=8000
|
||||
|
||||
# **Support / Help Center – Zammad Integration**
|
||||
# Base URL of your Zammad instance (required for chat and ticket form).
|
||||
# ZAMMAD_URL=https://zammad.example.com
|
||||
# Show a live-chat widget on the Help Center page (requires an online Zammad agent).
|
||||
# ZAMMAD_CHAT_ENABLED=false
|
||||
# Zammad chat topic ID (see Zammad → Channels → Chat → Topics).
|
||||
# ZAMMAD_CHAT_ID=1
|
||||
# Show a "Submit a Ticket" feedback form on the Help Center page.
|
||||
# ZAMMAD_FORM_ENABLED=false
|
||||
# Support e-mail address displayed on the Help Center page.
|
||||
# SUPPORT_EMAIL=support@example.com
|
||||
|
||||
@@ -914,6 +914,33 @@ class Settings(BaseSettings):
|
||||
description="Allowed request headers for CORS. Use ['*'] to allow all headers.",
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Support / Help Center – Zammad integration
|
||||
# ---------------------------------------------------------------------------
|
||||
zammad_url: Optional[str] = Field(
|
||||
default=None,
|
||||
description=(
|
||||
"Base URL of your Zammad instance (e.g. https://zammad.example.com). "
|
||||
"Required for the chat widget and feedback form on the Help Center page."
|
||||
),
|
||||
)
|
||||
zammad_chat_enabled: bool = Field(
|
||||
default=False,
|
||||
description="Show the Zammad live-chat widget on the Help Center page.",
|
||||
)
|
||||
zammad_chat_id: int = Field(
|
||||
default=1,
|
||||
description="Zammad chat topic ID to use for the live-chat widget.",
|
||||
)
|
||||
zammad_form_enabled: bool = Field(
|
||||
default=False,
|
||||
description="Show the Zammad feedback / ticket form on the Help Center page.",
|
||||
)
|
||||
support_email: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Support e-mail address displayed on the Help Center page.",
|
||||
)
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def strip_outer_quotes(cls, data: Any) -> Any:
|
||||
|
||||
+11
-4
@@ -137,7 +137,12 @@ async def lifespan(app: FastAPI):
|
||||
notify_shutdown()
|
||||
|
||||
|
||||
app = FastAPI(title="DocuElevate", lifespan=lifespan)
|
||||
app = FastAPI(
|
||||
title="DocuElevate",
|
||||
lifespan=lifespan,
|
||||
docs_url="/admin/api-docs",
|
||||
redoc_url="/admin/api-redoc",
|
||||
)
|
||||
|
||||
# Initialize rate limiter and attach to app state
|
||||
limiter = create_limiter(redis_url=settings.redis_url, enabled=settings.rate_limiting_enabled)
|
||||
@@ -202,14 +207,16 @@ if os.path.exists(static_dir):
|
||||
else:
|
||||
print(f"WARNING: Static directory not found at {static_dir}. Static files will not be served.")
|
||||
|
||||
# Mount the built MkDocs documentation site at /help/
|
||||
# Mount the built MkDocs developer documentation at /developer-docs/
|
||||
# These docs target administrators and developers, not end-users.
|
||||
# The user-facing Help Center is served by the /help view instead.
|
||||
# The docs are pre-built into docs_build/ during the Docker image build.
|
||||
# When running locally, run `mkdocs build` from the repo root first.
|
||||
docs_build_dir = pathlib.Path(__file__).parents[1] / "docs_build"
|
||||
if os.path.exists(docs_build_dir):
|
||||
app.mount("/help", StaticFiles(directory=str(docs_build_dir), html=True), name="help_docs")
|
||||
app.mount("/developer-docs", StaticFiles(directory=str(docs_build_dir), html=True), name="developer_docs")
|
||||
else:
|
||||
print(f"INFO: Help docs not found at {docs_build_dir}. Run 'mkdocs build' to generate them.")
|
||||
print(f"INFO: Developer docs not found at {docs_build_dir}. Run 'mkdocs build' to generate them.")
|
||||
|
||||
|
||||
# Custom exception handlers that return JSON for API routes and HTML for frontend routes
|
||||
|
||||
@@ -2223,6 +2223,47 @@ SETTING_METADATA = {
|
||||
"required": False,
|
||||
"restart_required": False,
|
||||
},
|
||||
# Support / Help Center – Zammad integration
|
||||
"zammad_url": {
|
||||
"category": "Support",
|
||||
"description": "Base URL of your Zammad instance (e.g. https://zammad.example.com). Required for the chat widget and feedback form.",
|
||||
"type": "string",
|
||||
"sensitive": False,
|
||||
"required": False,
|
||||
"restart_required": False,
|
||||
},
|
||||
"zammad_chat_enabled": {
|
||||
"category": "Support",
|
||||
"description": "Show a Zammad live-chat widget on the Help Center page.",
|
||||
"type": "boolean",
|
||||
"sensitive": False,
|
||||
"required": False,
|
||||
"restart_required": False,
|
||||
},
|
||||
"zammad_chat_id": {
|
||||
"category": "Support",
|
||||
"description": "Zammad chat topic ID (see Channels → Chat → Topics in Zammad admin). Default: 1.",
|
||||
"type": "integer",
|
||||
"sensitive": False,
|
||||
"required": False,
|
||||
"restart_required": False,
|
||||
},
|
||||
"zammad_form_enabled": {
|
||||
"category": "Support",
|
||||
"description": "Show a 'Submit a Ticket' feedback form on the Help Center page.",
|
||||
"type": "boolean",
|
||||
"sensitive": False,
|
||||
"required": False,
|
||||
"restart_required": False,
|
||||
},
|
||||
"support_email": {
|
||||
"category": "Support",
|
||||
"description": "Support e-mail address displayed on the Help Center page.",
|
||||
"type": "string",
|
||||
"sensitive": False,
|
||||
"required": False,
|
||||
"restart_required": False,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
+25
-9
@@ -1,27 +1,43 @@
|
||||
"""
|
||||
Help documentation view routes.
|
||||
Help Center view routes.
|
||||
|
||||
Serves the built MkDocs documentation site at /help.
|
||||
The static site is built during the Docker image build and placed at docs_build/.
|
||||
Renders a user-facing, SEO-optimised Help Center page at ``/help``.
|
||||
The page is designed for SaaS end-users and includes guidance on
|
||||
features, integrations, workflows, and optional Zammad support widgets
|
||||
(live-chat and feedback form).
|
||||
|
||||
The developer-oriented MkDocs documentation is served separately at
|
||||
``/docs`` and is intentionally **not** cross-linked from this page.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import pathlib
|
||||
|
||||
from fastapi import Request
|
||||
from fastapi.responses import RedirectResponse
|
||||
|
||||
from app.views.base import APIRouter
|
||||
from app.config import settings
|
||||
from app.views.base import APIRouter, templates
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
# Path to the built MkDocs documentation
|
||||
# Path to the built MkDocs documentation (kept for reference / backwards compat)
|
||||
_DOCS_BUILD_DIR = pathlib.Path(__file__).parents[2] / "docs_build"
|
||||
|
||||
|
||||
@router.get("/help", include_in_schema=False)
|
||||
async def help_redirect(request: Request) -> RedirectResponse:
|
||||
"""Redirect /help to /help/ so the MkDocs index is served correctly."""
|
||||
return RedirectResponse(url="/help/", status_code=301)
|
||||
async def help_center(request: Request):
|
||||
"""Render the end-user Help Center page."""
|
||||
return templates.TemplateResponse(
|
||||
"help.html",
|
||||
{
|
||||
"request": request,
|
||||
"external_hostname": settings.external_hostname,
|
||||
"zammad_url": settings.zammad_url,
|
||||
"zammad_chat_enabled": settings.zammad_chat_enabled,
|
||||
"zammad_chat_id": settings.zammad_chat_id,
|
||||
"zammad_form_enabled": settings.zammad_form_enabled,
|
||||
"support_email": settings.support_email,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -1118,6 +1118,30 @@ Administrators can set the **site-wide default** colour scheme that is applied w
|
||||
UI_DEFAULT_COLOR_SCHEME=dark
|
||||
```
|
||||
|
||||
### Support / Help Center – Zammad Integration
|
||||
|
||||
The Help Center page (`/help`) can optionally integrate with a [Zammad](https://zammad.com/) instance to offer live chat and a ticket-creation form directly within DocuElevate.
|
||||
|
||||
| **Variable** | **Description** | **Default** |
|
||||
|-------------------------|-------------------------------------------------------------------------------------------------------|-------------|
|
||||
| `ZAMMAD_URL` | Base URL of your Zammad instance (e.g. `https://zammad.example.com`). Required for chat and form. | *(unset)* |
|
||||
| `ZAMMAD_CHAT_ENABLED` | Show a Zammad live-chat widget on the Help Center page. | `false` |
|
||||
| `ZAMMAD_CHAT_ID` | Zammad chat topic ID (see *Channels → Chat → Topics* in Zammad admin). | `1` |
|
||||
| `ZAMMAD_FORM_ENABLED` | Show a "Submit a Ticket" feedback form on the Help Center page. | `false` |
|
||||
| `SUPPORT_EMAIL` | Support e-mail address displayed on the Help Center page. | *(unset)* |
|
||||
|
||||
**Example:**
|
||||
|
||||
```dotenv
|
||||
ZAMMAD_URL=https://zammad.example.com
|
||||
ZAMMAD_CHAT_ENABLED=true
|
||||
ZAMMAD_CHAT_ID=1
|
||||
ZAMMAD_FORM_ENABLED=true
|
||||
SUPPORT_EMAIL=support@example.com
|
||||
```
|
||||
|
||||
> **Note:** The live-chat widget requires at least one Zammad agent to be online. If no agent is available, the widget will not appear. Enable Zammad's debug mode (`debug: true`) for troubleshooting.
|
||||
|
||||
## Duplicate Document Detection
|
||||
|
||||
DocuElevate detects and flags documents that share the same content, even if they arrive as separate uploads.
|
||||
|
||||
@@ -172,6 +172,13 @@
|
||||
{% if request and request.url.path == '/status' %}aria-current="page"{% endif %}>
|
||||
<i class="fas fa-circle-dot w-4 mr-2 text-gray-500" aria-hidden="true"></i> Status
|
||||
</a>
|
||||
<div class="border-t border-gray-100 my-1"></div>
|
||||
<a href="/admin/api-docs" role="menuitem" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
||||
<i class="fas fa-code w-4 mr-2 text-teal-500" aria-hidden="true"></i> API Docs
|
||||
</a>
|
||||
<a href="/developer-docs/" role="menuitem" class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
|
||||
<i class="fas fa-book w-4 mr-2 text-teal-500" aria-hidden="true"></i> Developer Docs
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -179,11 +186,11 @@
|
||||
{% endif %}{# end multi_user_enabled / is_logged_in check #}
|
||||
|
||||
<!-- Help – always visible, for every visitor regardless of auth state -->
|
||||
<a href="/help/"
|
||||
<a href="/help"
|
||||
class="px-3 py-2 rounded-md text-sm font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-100"
|
||||
aria-label="Help and How-To Guides"
|
||||
title="Help & How-To Guides"
|
||||
{% if request and request.url.path.startswith('/help') %}aria-current="page"{% endif %}>
|
||||
aria-label="Help Center"
|
||||
title="Help Center"
|
||||
{% if request and request.url.path == '/help' %}aria-current="page"{% endif %}>
|
||||
<i class="fas fa-circle-question mr-1 text-gray-400" aria-hidden="true"></i>Help
|
||||
</a>
|
||||
|
||||
@@ -314,16 +321,22 @@
|
||||
{% if request and request.url.path == '/status' %}aria-current="page"{% endif %}>
|
||||
<i class="fas fa-circle-dot mr-2 text-gray-400" aria-hidden="true"></i> Status
|
||||
</a>
|
||||
<a href="/admin/api-docs" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
|
||||
<i class="fas fa-code mr-2 text-teal-400" aria-hidden="true"></i> API Docs
|
||||
</a>
|
||||
<a href="/developer-docs/" class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50">
|
||||
<i class="fas fa-book mr-2 text-teal-400" aria-hidden="true"></i> Developer Docs
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endif %}{# end multi_user_enabled / is_logged_in check #}
|
||||
|
||||
<!-- Help – always visible, for every visitor regardless of auth state -->
|
||||
<a href="/help/"
|
||||
<a href="/help"
|
||||
class="block px-3 py-3 rounded-md text-base font-medium text-gray-700 hover:text-gray-900 hover:bg-gray-50"
|
||||
aria-label="Help and How-To Guides"
|
||||
{% if request and request.url.path.startswith('/help') %}aria-current="page"{% endif %}>
|
||||
aria-label="Help Center"
|
||||
{% if request and request.url.path == '/help' %}aria-current="page"{% endif %}>
|
||||
<i class="fas fa-circle-question mr-2 text-gray-400" aria-hidden="true"></i>Help
|
||||
</a>
|
||||
|
||||
|
||||
@@ -0,0 +1,512 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Help Center – DocuElevate{% endblock %}
|
||||
|
||||
{% block head_extra %}
|
||||
<!-- SEO meta tags -->
|
||||
<meta name="description" content="DocuElevate Help Center – learn how to upload, process, and route your documents automatically. Get started with sources, destinations, workflows, and integrations.">
|
||||
<meta name="keywords" content="DocuElevate, help, support, document processing, OCR, cloud storage, workflow automation, SaaS">
|
||||
<meta name="robots" content="index, follow">
|
||||
<link rel="canonical" href="https://{{ external_hostname }}/help">
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:title" content="Help Center – DocuElevate">
|
||||
<meta property="og:description" content="Everything you need to get started with DocuElevate: upload documents, automate workflows, and connect your favourite cloud storage.">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="https://{{ external_hostname }}/help">
|
||||
|
||||
<!-- Structured data (JSON-LD) for FAQ rich results -->
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "FAQPage",
|
||||
"mainEntity": [
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "How do I upload documents to DocuElevate?",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "Navigate to the Upload page, drag-and-drop your files or click Choose File, and DocuElevate will automatically process them with OCR and AI metadata extraction."
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "Which cloud storage providers does DocuElevate support?",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "DocuElevate integrates with Dropbox, Google Drive, OneDrive, Amazon S3, Nextcloud, WebDAV, SFTP, FTP, and Paperless-ngx."
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "Can I automatically ingest documents from email?",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "Yes. Configure one or more IMAP mailboxes under Email Ingestion and DocuElevate will poll for new attachments and process them automatically."
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "Question",
|
||||
"name": "How do processing pipelines work?",
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": "Pipelines let you chain processing steps (OCR, AI extraction, format conversion) and route the result to one or more destinations. Create and manage them from the Pipelines page."
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-10 max-w-6xl">
|
||||
|
||||
<!-- ═══════════════════════ Hero Section ═══════════════════════ -->
|
||||
<header class="text-center mb-14">
|
||||
<h1 class="text-4xl md:text-5xl font-extrabold text-gray-900 mb-4">
|
||||
<i class="fas fa-life-ring text-blue-500 mr-2" aria-hidden="true"></i>Help Center
|
||||
</h1>
|
||||
<p class="text-lg md:text-xl text-gray-600 max-w-2xl mx-auto leading-relaxed">
|
||||
Everything you need to get the most out of DocuElevate.
|
||||
Browse topics below or search for what you need.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<!-- ═══════════════════════ Quick-start Cards ═══════════════════════ -->
|
||||
<section aria-labelledby="quickstart-heading" class="mb-16">
|
||||
<h2 id="quickstart-heading" class="text-2xl font-bold text-gray-800 mb-6">
|
||||
<i class="fas fa-rocket text-blue-500 mr-2" aria-hidden="true"></i>Quick Start
|
||||
</h2>
|
||||
<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
|
||||
<!-- Card: Upload -->
|
||||
<article class="bg-white shadow rounded-lg p-6 hover:shadow-lg transition-shadow">
|
||||
<div class="flex items-center mb-3">
|
||||
<span class="flex items-center justify-center w-10 h-10 rounded-full bg-blue-100 text-blue-600 mr-3" aria-hidden="true">
|
||||
<i class="fas fa-cloud-arrow-up"></i>
|
||||
</span>
|
||||
<h3 class="text-lg font-semibold text-gray-800">Upload Documents</h3>
|
||||
</div>
|
||||
<p class="text-gray-600 text-sm leading-relaxed">
|
||||
Drag & drop files onto the <strong>Upload</strong> page or click <em>Choose File</em>.
|
||||
DocuElevate converts images and office documents to PDF, runs OCR, and extracts metadata automatically.
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<!-- Card: Connect Storage -->
|
||||
<article class="bg-white shadow rounded-lg p-6 hover:shadow-lg transition-shadow">
|
||||
<div class="flex items-center mb-3">
|
||||
<span class="flex items-center justify-center w-10 h-10 rounded-full bg-green-100 text-green-600 mr-3" aria-hidden="true">
|
||||
<i class="fas fa-plug"></i>
|
||||
</span>
|
||||
<h3 class="text-lg font-semibold text-gray-800">Connect Storage</h3>
|
||||
</div>
|
||||
<p class="text-gray-600 text-sm leading-relaxed">
|
||||
Head to <strong>Settings → Integrations</strong> and link your cloud accounts.
|
||||
Processed documents are automatically routed to every destination you configure.
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<!-- Card: Automate -->
|
||||
<article class="bg-white shadow rounded-lg p-6 hover:shadow-lg transition-shadow">
|
||||
<div class="flex items-center mb-3">
|
||||
<span class="flex items-center justify-center w-10 h-10 rounded-full bg-purple-100 text-purple-600 mr-3" aria-hidden="true">
|
||||
<i class="fas fa-wand-magic-sparkles"></i>
|
||||
</span>
|
||||
<h3 class="text-lg font-semibold text-gray-800">Automate Workflows</h3>
|
||||
</div>
|
||||
<p class="text-gray-600 text-sm leading-relaxed">
|
||||
Create <strong>Pipelines</strong> to define multi-step processing and routing rules.
|
||||
Combine OCR, AI extraction, format conversion, and delivery in a single flow.
|
||||
</p>
|
||||
</article>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ═══════════════════════ Sources ═══════════════════════ -->
|
||||
<section aria-labelledby="sources-heading" class="mb-16">
|
||||
<h2 id="sources-heading" class="text-2xl font-bold text-gray-800 mb-6">
|
||||
<i class="fas fa-arrow-right-to-bracket text-blue-500 mr-2" aria-hidden="true"></i>Sources – Getting Documents In
|
||||
</h2>
|
||||
<div class="grid gap-6 sm:grid-cols-2">
|
||||
|
||||
<article class="bg-white shadow rounded-lg p-6">
|
||||
<h3 class="font-semibold text-gray-800 mb-2"><i class="fas fa-upload mr-2 text-gray-400" aria-hidden="true"></i>Web Upload</h3>
|
||||
<p class="text-gray-600 text-sm">
|
||||
The fastest way to get started. Open the <strong>Upload</strong> page, drop one or more files, and DocuElevate takes care of the rest.
|
||||
Supported formats include PDF, JPEG, PNG, TIFF, DOCX, XLSX, and more.
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article class="bg-white shadow rounded-lg p-6">
|
||||
<h3 class="font-semibold text-gray-800 mb-2"><i class="fas fa-envelope-open-text mr-2 text-gray-400" aria-hidden="true"></i>Email Ingestion (IMAP)</h3>
|
||||
<p class="text-gray-600 text-sm">
|
||||
Forward documents to a dedicated mailbox. Under <strong>Email Ingestion</strong>, add one or more IMAP accounts.
|
||||
DocuElevate polls for new messages and processes attachments automatically.
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article class="bg-white shadow rounded-lg p-6">
|
||||
<h3 class="font-semibold text-gray-800 mb-2"><i class="fas fa-code mr-2 text-gray-400" aria-hidden="true"></i>REST API</h3>
|
||||
<p class="text-gray-600 text-sm">
|
||||
Integrate programmatically by <code class="bg-gray-100 px-1 rounded text-xs">POST</code>-ing files to <code class="bg-gray-100 px-1 rounded text-xs">/api/upload</code>.
|
||||
Ideal for scripts, watched folders, scanners, or third-party tools like Zapier and n8n.
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article class="bg-white shadow rounded-lg p-6">
|
||||
<h3 class="font-semibold text-gray-800 mb-2"><i class="fas fa-print mr-2 text-gray-400" aria-hidden="true"></i>Scanner & Mobile</h3>
|
||||
<p class="text-gray-600 text-sm">
|
||||
Point network scanners (HP, Fujitsu ScanSnap, Brother) at DocuElevate's upload endpoint or
|
||||
use any mobile scanning app that supports custom HTTP destinations.
|
||||
</p>
|
||||
</article>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ═══════════════════════ Destinations ═══════════════════════ -->
|
||||
<section aria-labelledby="destinations-heading" class="mb-16">
|
||||
<h2 id="destinations-heading" class="text-2xl font-bold text-gray-800 mb-6">
|
||||
<i class="fas fa-arrow-right-from-bracket text-blue-500 mr-2" aria-hidden="true"></i>Destinations – Where Documents Go
|
||||
</h2>
|
||||
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
|
||||
<div class="flex items-start bg-white shadow rounded-lg p-5">
|
||||
<i class="fab fa-dropbox text-blue-500 text-xl mr-3 mt-0.5" aria-hidden="true"></i>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-800 text-sm">Dropbox</h3>
|
||||
<p class="text-gray-500 text-xs">OAuth-linked. Files land in your chosen folder.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start bg-white shadow rounded-lg p-5">
|
||||
<i class="fab fa-google-drive text-green-500 text-xl mr-3 mt-0.5" aria-hidden="true"></i>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-800 text-sm">Google Drive</h3>
|
||||
<p class="text-gray-500 text-xs">Service account or OAuth. Supports shared drives.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start bg-white shadow rounded-lg p-5">
|
||||
<i class="fab fa-microsoft text-blue-600 text-xl mr-3 mt-0.5" aria-hidden="true"></i>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-800 text-sm">OneDrive</h3>
|
||||
<p class="text-gray-500 text-xs">Microsoft Graph API integration.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start bg-white shadow rounded-lg p-5">
|
||||
<i class="fab fa-aws text-yellow-600 text-xl mr-3 mt-0.5" aria-hidden="true"></i>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-800 text-sm">Amazon S3</h3>
|
||||
<p class="text-gray-500 text-xs">Any S3-compatible bucket (AWS, MinIO, Wasabi).</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start bg-white shadow rounded-lg p-5">
|
||||
<i class="fas fa-cloud text-blue-400 text-xl mr-3 mt-0.5" aria-hidden="true"></i>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-800 text-sm">Nextcloud / WebDAV</h3>
|
||||
<p class="text-gray-500 text-xs">Self-hosted cloud storage via WebDAV.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start bg-white shadow rounded-lg p-5">
|
||||
<i class="fas fa-leaf text-green-600 text-xl mr-3 mt-0.5" aria-hidden="true"></i>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-800 text-sm">Paperless-ngx</h3>
|
||||
<p class="text-gray-500 text-xs">Push documents straight into Paperless for archival.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start bg-white shadow rounded-lg p-5">
|
||||
<i class="fas fa-server text-gray-500 text-xl mr-3 mt-0.5" aria-hidden="true"></i>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-800 text-sm">SFTP / FTP</h3>
|
||||
<p class="text-gray-500 text-xs">Secure file transfer to any server.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start bg-white shadow rounded-lg p-5">
|
||||
<i class="fas fa-envelope text-red-400 text-xl mr-3 mt-0.5" aria-hidden="true"></i>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-800 text-sm">Email Forwarding</h3>
|
||||
<p class="text-gray-500 text-xs">Processed files sent as SMTP attachments.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start bg-white shadow rounded-lg p-5">
|
||||
<i class="fas fa-link text-indigo-500 text-xl mr-3 mt-0.5" aria-hidden="true"></i>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-800 text-sm">Webhook</h3>
|
||||
<p class="text-gray-500 text-xs">POST metadata to any external endpoint.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ═══════════════════════ Workflows / Pipelines ═══════════════════════ -->
|
||||
<section aria-labelledby="workflows-heading" class="mb-16">
|
||||
<h2 id="workflows-heading" class="text-2xl font-bold text-gray-800 mb-6">
|
||||
<i class="fas fa-diagram-project text-blue-500 mr-2" aria-hidden="true"></i>Workflows & Pipelines
|
||||
</h2>
|
||||
<div class="bg-white shadow rounded-lg p-6">
|
||||
<div class="grid gap-6 md:grid-cols-2">
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-800 mb-2">What is a Pipeline?</h3>
|
||||
<p class="text-gray-600 text-sm leading-relaxed mb-4">
|
||||
A <strong>Pipeline</strong> is a series of processing steps that run automatically whenever a document is ingested.
|
||||
Each step can transform, enrich, or route the document.
|
||||
</p>
|
||||
<h3 class="font-semibold text-gray-800 mb-2">Typical Steps</h3>
|
||||
<ol class="list-decimal list-inside text-gray-600 text-sm space-y-1">
|
||||
<li>Convert to PDF (Gotenberg)</li>
|
||||
<li>OCR – extract text (Azure, Tesseract, Mistral …)</li>
|
||||
<li>AI metadata extraction (OpenAI, Claude, Gemini …)</li>
|
||||
<li>Deliver to one or more destinations</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-gray-800 mb-2">Creating a Pipeline</h3>
|
||||
<ol class="list-decimal list-inside text-gray-600 text-sm space-y-2 leading-relaxed">
|
||||
<li>Go to <strong>Pipelines</strong> in the main menu.</li>
|
||||
<li>Click <em>New Pipeline</em> and give it a name.</li>
|
||||
<li>Add the processing steps you need.</li>
|
||||
<li>Choose one or more delivery destinations.</li>
|
||||
<li>Save – new documents will be processed through this pipeline automatically.</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ═══════════════════════ Data Ingestion Tools ═══════════════════════ -->
|
||||
<section aria-labelledby="ingestion-heading" class="mb-16">
|
||||
<h2 id="ingestion-heading" class="text-2xl font-bold text-gray-800 mb-6">
|
||||
<i class="fas fa-toolbox text-blue-500 mr-2" aria-hidden="true"></i>Ingesting Data with Standard Tools
|
||||
</h2>
|
||||
<div class="bg-white shadow rounded-lg p-6 space-y-6">
|
||||
|
||||
<article>
|
||||
<h3 class="font-semibold text-gray-800 mb-1"><i class="fas fa-terminal mr-2 text-gray-400" aria-hidden="true"></i>cURL</h3>
|
||||
<p class="text-gray-600 text-sm mb-2">Upload a file from any terminal:</p>
|
||||
<pre class="bg-gray-50 text-sm rounded p-3 overflow-x-auto"><code>curl -X POST https://your-instance/api/upload \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-F "file=@/path/to/document.pdf"</code></pre>
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<h3 class="font-semibold text-gray-800 mb-1"><i class="fas fa-folder-open mr-2 text-gray-400" aria-hidden="true"></i>Watched Folders</h3>
|
||||
<p class="text-gray-600 text-sm">
|
||||
Use <strong>inotifywait</strong> (Linux), <strong>fswatch</strong> (macOS), or any file-watcher script to
|
||||
<code class="bg-gray-100 px-1 rounded text-xs">POST</code> new files as soon as they appear in a local directory.
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<h3 class="font-semibold text-gray-800 mb-1"><i class="fas fa-print mr-2 text-gray-400" aria-hidden="true"></i>Network Scanners</h3>
|
||||
<p class="text-gray-600 text-sm">
|
||||
Configure your HP, Fujitsu ScanSnap, or Brother scanner to use DocuElevate's upload URL as a
|
||||
<em>Scan-to-Network</em> destination. Scans are processed the moment they arrive.
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<h3 class="font-semibold text-gray-800 mb-1"><i class="fas fa-mobile-screen-button mr-2 text-gray-400" aria-hidden="true"></i>Mobile Scanning Apps</h3>
|
||||
<p class="text-gray-600 text-sm">
|
||||
Apps like <strong>Adobe Scan</strong>, <strong>Microsoft Lens</strong>, or <strong>Genius Scan</strong>
|
||||
can export to a custom HTTP endpoint. Point them at <code class="bg-gray-100 px-1 rounded text-xs">/api/upload</code>.
|
||||
</p>
|
||||
</article>
|
||||
|
||||
<article>
|
||||
<h3 class="font-semibold text-gray-800 mb-1"><i class="fas fa-shuffle mr-2 text-gray-400" aria-hidden="true"></i>Zapier / n8n / Make</h3>
|
||||
<p class="text-gray-600 text-sm">
|
||||
Connect DocuElevate to thousands of other apps via integration platforms.
|
||||
Use the HTTP/Webhook module to <code class="bg-gray-100 px-1 rounded text-xs">POST</code> files to the upload API.
|
||||
</p>
|
||||
</article>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ═══════════════════════ FAQ ═══════════════════════ -->
|
||||
<section aria-labelledby="faq-heading" class="mb-16">
|
||||
<h2 id="faq-heading" class="text-2xl font-bold text-gray-800 mb-6">
|
||||
<i class="fas fa-circle-question text-blue-500 mr-2" aria-hidden="true"></i>Frequently Asked Questions
|
||||
</h2>
|
||||
<div class="space-y-4" x-data="{ open: null }">
|
||||
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
<button type="button"
|
||||
class="w-full flex justify-between items-center px-6 py-4 text-left font-medium text-gray-800 focus:outline-none focus:ring-2 focus:ring-blue-400 rounded-lg"
|
||||
@click="open = open === 1 ? null : 1"
|
||||
:aria-expanded="open === 1 ? 'true' : 'false'"
|
||||
aria-controls="faq-1">
|
||||
<span>How do I upload documents?</span>
|
||||
<i class="fas fa-chevron-down text-gray-400 transition-transform" :class="{ 'rotate-180': open === 1 }" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div id="faq-1" x-show="open === 1" x-collapse class="px-6 pb-4 text-gray-600 text-sm leading-relaxed">
|
||||
Navigate to the <strong>Upload</strong> page, drag-and-drop your files or click <em>Choose File</em>.
|
||||
DocuElevate converts images and office documents to PDF, runs OCR, and extracts metadata automatically.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
<button type="button"
|
||||
class="w-full flex justify-between items-center px-6 py-4 text-left font-medium text-gray-800 focus:outline-none focus:ring-2 focus:ring-blue-400 rounded-lg"
|
||||
@click="open = open === 2 ? null : 2"
|
||||
:aria-expanded="open === 2 ? 'true' : 'false'"
|
||||
aria-controls="faq-2">
|
||||
<span>Which file formats are supported?</span>
|
||||
<i class="fas fa-chevron-down text-gray-400 transition-transform" :class="{ 'rotate-180': open === 2 }" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div id="faq-2" x-show="open === 2" x-collapse class="px-6 pb-4 text-gray-600 text-sm leading-relaxed">
|
||||
PDF, JPEG, PNG, TIFF, BMP, GIF, DOCX, XLSX, PPTX, ODT, ODS, TXT, RTF, and HTML.
|
||||
Non-PDF files are automatically converted to PDF before processing.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
<button type="button"
|
||||
class="w-full flex justify-between items-center px-6 py-4 text-left font-medium text-gray-800 focus:outline-none focus:ring-2 focus:ring-blue-400 rounded-lg"
|
||||
@click="open = open === 3 ? null : 3"
|
||||
:aria-expanded="open === 3 ? 'true' : 'false'"
|
||||
aria-controls="faq-3">
|
||||
<span>Can I ingest documents from email?</span>
|
||||
<i class="fas fa-chevron-down text-gray-400 transition-transform" :class="{ 'rotate-180': open === 3 }" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div id="faq-3" x-show="open === 3" x-collapse class="px-6 pb-4 text-gray-600 text-sm leading-relaxed">
|
||||
Yes. Go to <strong>Email Ingestion</strong>, add an IMAP account, and DocuElevate will poll for new
|
||||
messages and process attachments automatically.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
<button type="button"
|
||||
class="w-full flex justify-between items-center px-6 py-4 text-left font-medium text-gray-800 focus:outline-none focus:ring-2 focus:ring-blue-400 rounded-lg"
|
||||
@click="open = open === 4 ? null : 4"
|
||||
:aria-expanded="open === 4 ? 'true' : 'false'"
|
||||
aria-controls="faq-4">
|
||||
<span>How do processing pipelines work?</span>
|
||||
<i class="fas fa-chevron-down text-gray-400 transition-transform" :class="{ 'rotate-180': open === 4 }" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div id="faq-4" x-show="open === 4" x-collapse class="px-6 pb-4 text-gray-600 text-sm leading-relaxed">
|
||||
Pipelines let you chain processing steps – OCR, AI extraction, format conversion – and route the
|
||||
result to one or more destinations. Create and manage them from the <strong>Pipelines</strong> page.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
<button type="button"
|
||||
class="w-full flex justify-between items-center px-6 py-4 text-left font-medium text-gray-800 focus:outline-none focus:ring-2 focus:ring-blue-400 rounded-lg"
|
||||
@click="open = open === 5 ? null : 5"
|
||||
:aria-expanded="open === 5 ? 'true' : 'false'"
|
||||
aria-controls="faq-5">
|
||||
<span>Is my data secure?</span>
|
||||
<i class="fas fa-chevron-down text-gray-400 transition-transform" :class="{ 'rotate-180': open === 5 }" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div id="faq-5" x-show="open === 5" x-collapse class="px-6 pb-4 text-gray-600 text-sm leading-relaxed">
|
||||
DocuElevate encrypts credentials at rest, communicates over TLS, and never stores your documents
|
||||
longer than necessary. See the <a href="/privacy" class="text-blue-600 hover:underline">Privacy Notice</a> for full details.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ═══════════════════════ Contact / Support ═══════════════════════ -->
|
||||
<section aria-labelledby="support-heading" class="mb-10">
|
||||
<h2 id="support-heading" class="text-2xl font-bold text-gray-800 mb-6">
|
||||
<i class="fas fa-headset text-blue-500 mr-2" aria-hidden="true"></i>Contact Support
|
||||
</h2>
|
||||
|
||||
<div class="grid gap-6 md:grid-cols-2">
|
||||
|
||||
<!-- Left column: info -->
|
||||
<div class="bg-white shadow rounded-lg p-6 space-y-4">
|
||||
<p class="text-gray-600 text-sm leading-relaxed">
|
||||
Can't find what you're looking for? Our support team is here to help.
|
||||
</p>
|
||||
|
||||
{% if support_email %}
|
||||
<div class="flex items-center text-sm text-gray-700">
|
||||
<i class="fas fa-envelope text-blue-500 mr-2" aria-hidden="true"></i>
|
||||
<a href="mailto:{{ support_email }}" class="text-blue-600 hover:underline">{{ support_email }}</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if zammad_chat_enabled and zammad_url %}
|
||||
<div class="flex items-center text-sm text-gray-700">
|
||||
<i class="fas fa-comments text-blue-500 mr-2" aria-hidden="true"></i>
|
||||
<span>Live chat available – look for the chat bubble in the bottom-right corner.</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Right column: Zammad ticket form -->
|
||||
{% if zammad_form_enabled and zammad_url %}
|
||||
<div class="bg-white shadow rounded-lg p-6">
|
||||
<h3 class="font-semibold text-gray-800 mb-3">
|
||||
<i class="fas fa-ticket mr-2 text-gray-400" aria-hidden="true"></i>Open a Support Ticket
|
||||
</h3>
|
||||
<p class="text-gray-600 text-sm mb-4">
|
||||
Describe your issue below and we'll get back to you as soon as possible.
|
||||
</p>
|
||||
<button id="zammad-feedback-form"
|
||||
type="button"
|
||||
class="inline-flex items-center bg-blue-600 hover:bg-blue-700 text-white font-semibold py-2 px-5 rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-blue-400 min-h-[44px]">
|
||||
<i class="fas fa-paper-plane mr-2" aria-hidden="true"></i>Submit a Ticket
|
||||
</button>
|
||||
</div>
|
||||
{% elif not zammad_form_enabled and not support_email and not zammad_chat_enabled %}
|
||||
<div class="bg-white shadow rounded-lg p-6 flex flex-col items-center justify-center text-center">
|
||||
<i class="fas fa-info-circle text-blue-400 text-2xl mb-2" aria-hidden="true"></i>
|
||||
<p class="text-gray-500 text-sm">
|
||||
Contact your administrator for support information.
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{% if zammad_url and zammad_form_enabled %}
|
||||
<!-- Zammad Feedback Form -->
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"
|
||||
integrity="sha384-vtXRMe3mGCbOeY7l30aIg8H9p3GdeSe4IFlP6G8JMa7o7lXvnz3GFKzPxzJdPfGK"
|
||||
crossorigin="anonymous"></script>
|
||||
<script id="zammad_form_script" src="{{ zammad_url }}/assets/form/form.js"></script>
|
||||
<script>
|
||||
$(function() {
|
||||
$('#zammad-feedback-form').ZammadForm({
|
||||
messageTitle: 'DocuElevate Support',
|
||||
messageSubmit: 'Submit',
|
||||
messageThankYou: 'Thank you for your request (#%s)! We will get back to you shortly.',
|
||||
modal: true
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
{% if zammad_url and zammad_chat_enabled %}
|
||||
<!-- Zammad Live Chat Widget -->
|
||||
<script src="{{ zammad_url }}/assets/chat/chat-no-jquery.min.js"></script>
|
||||
<script>
|
||||
(function() {
|
||||
new ZammadChat({
|
||||
fontSize: '12px',
|
||||
flat: true,
|
||||
chatId: {{ zammad_chat_id | int }},
|
||||
title: '<strong>Chat</strong> with us!'
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
+1
-1
@@ -46,7 +46,7 @@ class TestHealthEndpoints:
|
||||
|
||||
def test_docs_endpoint(self, client: TestClient):
|
||||
"""Test that API documentation is accessible."""
|
||||
response = client.get("/docs")
|
||||
response = client.get("/admin/api-docs")
|
||||
assert response.status_code == 200
|
||||
assert "swagger" in response.text.lower() or "openapi" in response.text.lower()
|
||||
|
||||
|
||||
+116
-29
@@ -1,33 +1,50 @@
|
||||
"""Tests for app/views/help.py module."""
|
||||
"""Tests for app/views/help.py module – user-facing Help Center."""
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestHelpViews:
|
||||
"""Tests for the help documentation view routes."""
|
||||
"""Tests for the Help Center view routes."""
|
||||
|
||||
def test_help_redirect(self, client):
|
||||
"""GET /help should redirect permanently to /help/."""
|
||||
response = client.get("/help", follow_redirects=False)
|
||||
assert response.status_code == 301
|
||||
assert response.headers["location"] in ("/help/", "http://testserver/help/")
|
||||
def test_help_returns_200(self, client):
|
||||
"""GET /help should return the Help Center page."""
|
||||
response = client.get("/help")
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_help_redirect_follows(self, client):
|
||||
"""Following /help redirect leads to /help/ (docs served or 404 if not built)."""
|
||||
# In test environments docs_build/ is not present, so /help/ may 404.
|
||||
# We only verify the initial redirect works; the final landing page depends
|
||||
# on whether the docs have been built (they are built only in Docker images).
|
||||
response = client.get("/help", follow_redirects=False)
|
||||
assert response.status_code == 301
|
||||
# Redirect target must be /help/
|
||||
location = response.headers.get("location", "")
|
||||
assert location.endswith("/help/")
|
||||
def test_help_contains_help_center_title(self, client):
|
||||
"""The page should contain the Help Center heading."""
|
||||
response = client.get("/help")
|
||||
assert b"Help Center" in response.content
|
||||
|
||||
def test_help_contains_quick_start_section(self, client):
|
||||
"""The page should contain the Quick Start section."""
|
||||
response = client.get("/help")
|
||||
assert b"Quick Start" in response.content
|
||||
|
||||
def test_help_contains_sources_section(self, client):
|
||||
"""The page should contain the Sources section."""
|
||||
response = client.get("/help")
|
||||
assert b"Sources" in response.content
|
||||
|
||||
def test_help_contains_destinations_section(self, client):
|
||||
"""The page should contain the Destinations section."""
|
||||
response = client.get("/help")
|
||||
assert b"Destinations" in response.content
|
||||
|
||||
def test_help_contains_faq_section(self, client):
|
||||
"""The page should contain the FAQ section."""
|
||||
response = client.get("/help")
|
||||
assert b"Frequently Asked Questions" in response.content
|
||||
|
||||
def test_help_contains_support_section(self, client):
|
||||
"""The page should contain the Contact Support section."""
|
||||
response = client.get("/help")
|
||||
assert b"Contact Support" in response.content
|
||||
|
||||
def test_help_route_is_registered(self, client):
|
||||
"""Verify the /help route exists in the app router."""
|
||||
# A GET to /help must not return 405 Method Not Allowed
|
||||
response = client.get("/help", follow_redirects=False)
|
||||
response = client.get("/help")
|
||||
assert response.status_code != 405
|
||||
|
||||
|
||||
@@ -35,31 +52,52 @@ class TestHelpViews:
|
||||
class TestHelpViewUnit:
|
||||
"""Unit tests for the help view module."""
|
||||
|
||||
def test_help_redirect_returns_301(self):
|
||||
"""Verify the redirect is HTTP 301 (permanent)."""
|
||||
def test_help_returns_200(self):
|
||||
"""Verify the help center returns HTTP 200."""
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from starlette.middleware.sessions import SessionMiddleware
|
||||
|
||||
from app.views.help import router
|
||||
|
||||
app = FastAPI()
|
||||
app.add_middleware(SessionMiddleware, secret_key="test-secret")
|
||||
app.include_router(router)
|
||||
with TestClient(app) as tc:
|
||||
resp = tc.get("/help", follow_redirects=False)
|
||||
assert resp.status_code == 301
|
||||
resp = tc.get("/help")
|
||||
assert resp.status_code == 200
|
||||
|
||||
def test_help_redirect_target(self):
|
||||
"""Verify the redirect points to /help/."""
|
||||
def test_help_page_has_seo_meta(self):
|
||||
"""Verify SEO meta tags are present in the response."""
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from starlette.middleware.sessions import SessionMiddleware
|
||||
|
||||
from app.views.help import router
|
||||
|
||||
app = FastAPI()
|
||||
app.add_middleware(SessionMiddleware, secret_key="test-secret")
|
||||
app.include_router(router)
|
||||
with TestClient(app) as tc:
|
||||
resp = tc.get("/help", follow_redirects=False)
|
||||
assert resp.headers["location"].rstrip("/").endswith("help") or resp.headers["location"].endswith("/help/")
|
||||
resp = tc.get("/help")
|
||||
assert b'name="description"' in resp.content
|
||||
assert b'name="robots"' in resp.content
|
||||
|
||||
def test_help_page_has_structured_data(self):
|
||||
"""Verify JSON-LD structured data is present."""
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from starlette.middleware.sessions import SessionMiddleware
|
||||
|
||||
from app.views.help import router
|
||||
|
||||
app = FastAPI()
|
||||
app.add_middleware(SessionMiddleware, secret_key="test-secret")
|
||||
app.include_router(router)
|
||||
with TestClient(app) as tc:
|
||||
resp = tc.get("/help")
|
||||
assert b"application/ld+json" in resp.content
|
||||
assert b"FAQPage" in resp.content
|
||||
|
||||
def test_docs_build_path_constant(self):
|
||||
"""Verify _DOCS_BUILD_DIR is resolved relative to the app package."""
|
||||
@@ -71,6 +109,38 @@ class TestHelpViewUnit:
|
||||
# Should point to <repo_root>/docs_build
|
||||
assert _DOCS_BUILD_DIR.name == "docs_build"
|
||||
|
||||
def test_zammad_chat_hidden_when_disabled(self):
|
||||
"""Chat widget markup should not appear when Zammad chat is disabled."""
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from starlette.middleware.sessions import SessionMiddleware
|
||||
|
||||
from app.views.help import router
|
||||
|
||||
app = FastAPI()
|
||||
app.add_middleware(SessionMiddleware, secret_key="test-secret")
|
||||
app.include_router(router)
|
||||
with TestClient(app) as tc:
|
||||
resp = tc.get("/help")
|
||||
# Default settings: zammad_chat_enabled=False → no chat script
|
||||
assert b"ZammadChat" not in resp.content
|
||||
|
||||
def test_zammad_form_hidden_when_disabled(self):
|
||||
"""Ticket form markup should not appear when Zammad form is disabled."""
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from starlette.middleware.sessions import SessionMiddleware
|
||||
|
||||
from app.views.help import router
|
||||
|
||||
app = FastAPI()
|
||||
app.add_middleware(SessionMiddleware, secret_key="test-secret")
|
||||
app.include_router(router)
|
||||
with TestClient(app) as tc:
|
||||
resp = tc.get("/help")
|
||||
# Default settings: zammad_form_enabled=False → no form script
|
||||
assert b"ZammadForm" not in resp.content
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestHelpNavigationLink:
|
||||
@@ -81,12 +151,29 @@ class TestHelpNavigationLink:
|
||||
response = client.get("/about")
|
||||
assert response.status_code == 200
|
||||
# The Help link should be present somewhere in the rendered page
|
||||
assert b"/help/" in response.content
|
||||
assert b"/help" in response.content
|
||||
|
||||
def test_help_link_has_accessible_text(self, client):
|
||||
"""The Help link should have visible text for accessibility."""
|
||||
response = client.get("/about")
|
||||
assert response.status_code == 200
|
||||
content = response.text
|
||||
# Should include the word "Help" associated with /help/
|
||||
# Should include the word "Help" associated with /help
|
||||
assert "Help" in content
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
class TestAdminDocsLinks:
|
||||
"""Tests that admin doc links are in the admin menu markup."""
|
||||
|
||||
def test_api_docs_link_in_admin_menu(self, client):
|
||||
"""The admin menu HTML should contain the API Docs link."""
|
||||
response = client.get("/about")
|
||||
assert response.status_code == 200
|
||||
assert b"/admin/api-docs" in response.content
|
||||
|
||||
def test_developer_docs_link_in_admin_menu(self, client):
|
||||
"""The admin menu HTML should contain the Developer Docs link."""
|
||||
response = client.get("/about")
|
||||
assert response.status_code == 200
|
||||
assert b"/developer-docs/" in response.content
|
||||
|
||||
Reference in New Issue
Block a user