diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..bf77ca2 --- /dev/null +++ b/app/main.py @@ -0,0 +1,16 @@ +from fastapi import FastAPI +from fastapi.staticfiles import StaticFiles + +from app.routes import doc_generator + +app = FastAPI(title="Utility App") + +app.mount("/static", StaticFiles(directory="static"), name="static") + +app.include_router(doc_generator.router, prefix="/api/doc-generator", tags=["doc-generator"]) + + +@app.get("/") +def index(): + from fastapi.responses import FileResponse + return FileResponse("static/index.html") diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/routes/__init__.py b/app/routes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/services/__init__.py b/app/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/utils/__init__.py b/app/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/archive/.gitkeep b/archive/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/diagnostics/.gitkeep b/diagnostics/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/exports/.gitkeep b/exports/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/logs/.gitkeep b/logs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/outputs/.gitkeep b/outputs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/scripts/.gitkeep b/scripts/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/settings/.gitkeep b/settings/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/static/.gitkeep b/static/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tests/.gitkeep b/tests/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tools/.gitkeep b/tools/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tools/doc_generator/content/document_types/engineering_report.json b/tools/doc_generator/content/document_types/engineering_report.json new file mode 100644 index 0000000..c92919b --- /dev/null +++ b/tools/doc_generator/content/document_types/engineering_report.json @@ -0,0 +1,12 @@ +{ + "id": "engineering_report", + "name": "Engineering Report", + "description": "General engineering report generated from a Word template.", + "template": "engineering_report.docx", + "outputFilename": "engineering_report_{projectName}_{timestamp_YYYY-MM-DD_HH-mm-ss}.docx", + "fields": [ + {"name": "projectName", "label": "Project Name", "type": "text", "required": true}, + {"name": "preparedBy", "label": "Prepared By", "type": "text", "required": false}, + {"name": "summary", "label": "Summary", "type": "textarea", "required": false} + ] +} diff --git a/tools/doc_generator/content/templates/engineering_report.docx b/tools/doc_generator/content/templates/engineering_report.docx new file mode 100644 index 0000000..4ccd264 Binary files /dev/null and b/tools/doc_generator/content/templates/engineering_report.docx differ diff --git a/tools/doc_generator/logic/__init__.py b/tools/doc_generator/logic/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tools/doc_generator/logic/core_fields.py b/tools/doc_generator/logic/core_fields.py new file mode 100644 index 0000000..65da68b --- /dev/null +++ b/tools/doc_generator/logic/core_fields.py @@ -0,0 +1,22 @@ +import pendulum + + +def get_core_fields() -> dict: + now = pendulum.now() + + return { + "date": now.format("MMMM Do, YYYY"), + "time": now.format("h:mm A"), + "timestamp": now.format("MMMM Do, YYYY h:mm A"), + "date_YYYY-MM-DD": now.format("YYYY-MM-DD"), + "date_MM-DD-YYYY": now.format("MM-DD-YYYY"), + "timestamp_YYYY-MM-DD_HH-mm-ss": now.format("YYYY-MM-DD_HH-mm-ss"), + } + + +def merge_core_fields(data: dict) -> dict: + # App-owned core fields win over submitted form values. + return { + **data, + **get_core_fields(), + } diff --git a/tools/doc_generator/logic/document_types.py b/tools/doc_generator/logic/document_types.py new file mode 100644 index 0000000..07a4490 --- /dev/null +++ b/tools/doc_generator/logic/document_types.py @@ -0,0 +1,25 @@ +import json +from pathlib import Path + +BASE_DIR = Path(__file__).resolve().parent.parent +CONTENT_DIR = BASE_DIR / "content" +DOCUMENT_TYPES_DIR = CONTENT_DIR / "document_types" + + +def list_document_types(): + document_types = [] + + for path in sorted(DOCUMENT_TYPES_DIR.glob("*.json")): + data = json.loads(path.read_text(encoding="utf-8")) + document_types.append(data) + + return document_types + + +def get_document_type(document_type_id: str): + path = DOCUMENT_TYPES_DIR / f"{document_type_id}.json" + + if not path.exists(): + raise FileNotFoundError(f"Document type not found: {document_type_id}") + + return json.loads(path.read_text(encoding="utf-8")) diff --git a/tools/doc_generator/logic/renderer.py b/tools/doc_generator/logic/renderer.py new file mode 100644 index 0000000..13bdddc --- /dev/null +++ b/tools/doc_generator/logic/renderer.py @@ -0,0 +1,90 @@ +import re +from pathlib import Path + +from docx import Document + +from tools.doc_generator.logic.core_fields import merge_core_fields +from tools.doc_generator.logic.document_types import get_document_type + +BASE_DIR = Path(__file__).resolve().parent.parent +CONTENT_DIR = BASE_DIR / "content" +TEMPLATES_DIR = CONTENT_DIR / "templates" +PROJECT_ROOT = BASE_DIR.parent.parent +EXPORTS_DIR = PROJECT_ROOT / "exports" + + +def safe_filename(value: str) -> str: + value = str(value or "document").strip() + value = re.sub(r"[^A-Za-z0-9._ -]+", "", value) + value = re.sub(r"\s+", "_", value) + return value or "document" + + +def render_filename(pattern: str, data: dict) -> str: + filename = pattern + + for key, value in data.items(): + filename = filename.replace("{" + key + "}", safe_filename(value)) + + return safe_filename(filename) + + +def replace_placeholders_in_paragraph(paragraph, data: dict): + full_text = "".join(run.text for run in paragraph.runs) + + new_text = full_text + for key, value in data.items(): + new_text = new_text.replace("{" + key + "}", "" if value is None else str(value)) + + if new_text == full_text: + return + + for run in paragraph.runs: + run.text = "" + + if paragraph.runs: + paragraph.runs[0].text = new_text + else: + paragraph.add_run(new_text) + + +def replace_placeholders_in_table(table, data: dict): + for row in table.rows: + for cell in row.cells: + for paragraph in cell.paragraphs: + replace_placeholders_in_paragraph(paragraph, data) + + for nested_table in cell.tables: + replace_placeholders_in_table(nested_table, data) + + +def generate_docx(document_type_id: str, data: dict) -> Path: + data = merge_core_fields(data) + document_type = get_document_type(document_type_id) + + template_path = TEMPLATES_DIR / document_type["template"] + + if not template_path.exists(): + raise FileNotFoundError(f"Template not found: {template_path}") + + EXPORTS_DIR.mkdir(parents=True, exist_ok=True) + + output_pattern = document_type.get("outputFilename", f"{document_type_id}.docx") + output_filename = render_filename(output_pattern, data) + + if not output_filename.lower().endswith(".docx"): + output_filename += ".docx" + + output_path = EXPORTS_DIR / output_filename + + document = Document(template_path) + + for paragraph in document.paragraphs: + replace_placeholders_in_paragraph(paragraph, data) + + for table in document.tables: + replace_placeholders_in_table(table, data) + + document.save(output_path) + + return output_path