v1.4.0: fix #43, #31, #29, #49, #33 + shared templating

This commit is contained in:
2026-05-20 20:08:25 +03:00
parent ca4d00c895
commit 64991f0228
63 changed files with 5005 additions and 45 deletions
+282
View File
@@ -0,0 +1,282 @@
import os
import re
import markdown
from pathlib import Path
from fastapi import APIRouter, Request, Depends, Form
from fastapi.responses import HTMLResponse, RedirectResponse
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.config import get_settings
from app.services.docs_permissions import load_permissions, save_permissions, discover_docs, can_access_doc, get_allowed_docs
from app.dependencies import get_current_user, require_role
settings = get_settings()
router = APIRouter(prefix="/service/documents", tags=["documents"])
from app.templating import templates
def transliterate_slug(text: str) -> str:
translit_map = {
'а': 'a', 'б': 'b', 'в': 'v', 'г': 'g', 'д': 'd', 'е': 'e', 'ё': 'yo', 'ж': 'zh',
'з': 'z', 'и': 'i', 'й': 'y', 'к': 'k', 'л': 'l', 'м': 'm', 'н': 'n', 'о': 'o',
'п': 'p', 'р': 'r', 'с': 's', 'т': 't', 'у': 'u', 'ф': 'f', 'х': 'kh', 'ц': 'ts',
'ч': 'ch', 'ш': 'sh', 'щ': 'shch', 'ъ': '', 'ы': 'y', 'ь': '', 'э': 'e', 'ю': 'yu',
'я': 'ya', ' ': '_',
}
result = ''.join(translit_map.get(c.lower(), c) for c in text)
result = re.sub(r'[^a-zA-Z0-9_\-]', '', result)
result = re.sub(r'_+', '_', result)
return result.strip('_').lower()
def render_markdown(content: str) -> str:
md = markdown.Markdown(extensions=['tables', 'fenced_code', 'toc', 'attr_list'])
return md.convert(content)
@router.get("/")
async def documents_list(
request: Request,
db: AsyncSession = Depends(get_db),
user: dict = Depends(get_current_user),
):
docs = get_allowed_docs(user["role"])
return templates.TemplateResponse("documents/list.html", {
"request": request, "user": user, "docs": docs,
})
@router.get("/{slug}")
async def document_view(
slug: str,
request: Request,
db: AsyncSession = Depends(get_db),
user: dict = Depends(get_current_user),
):
if not can_access_doc(user["role"], slug, "view"):
return HTMLResponse(content="Доступ запрещён", status_code=403)
docs_dir = settings.DOCS_DIR
perms = discover_docs()
doc = perms.get(slug)
if not doc:
return HTMLResponse(content="Документ не найден", status_code=404)
filepath = os.path.join(docs_dir, doc["filename"])
if not os.path.exists(filepath):
return HTMLResponse(content="Файл не найден", status_code=404)
with open(filepath, "r", encoding="utf-8") as f:
content = f.read()
html_content = render_markdown(content)
return templates.TemplateResponse("documents/view.html", {
"request": request, "user": user, "doc": doc, "content": html_content,
"can_edit": can_access_doc(user["role"], slug, "edit"),
"can_download": user["role"] == "owner",
})
@router.get("/{slug}/edit")
async def document_edit(
slug: str,
request: Request,
db: AsyncSession = Depends(get_db),
user: dict = Depends(get_current_user),
):
if not can_access_doc(user["role"], slug, "edit"):
return HTMLResponse(content="Доступ запрещён", status_code=403)
docs_dir = settings.DOCS_DIR
perms = discover_docs()
doc = perms.get(slug)
if not doc:
return HTMLResponse(content="Документ не найден", status_code=404)
filepath = os.path.join(docs_dir, doc["filename"])
if not os.path.exists(filepath):
return HTMLResponse(content="Файл не найден", status_code=404)
with open(filepath, "r", encoding="utf-8") as f:
content = f.read()
return templates.TemplateResponse("documents/edit.html", {
"request": request, "user": user, "doc": doc, "content": content,
})
@router.post("/{slug}/edit")
async def document_update(
slug: str,
request: Request,
content: str = Form(...),
db: AsyncSession = Depends(get_db),
user: dict = Depends(get_current_user),
):
if not can_access_doc(user["role"], slug, "edit"):
return HTMLResponse(content="Доступ запрещён", status_code=403)
docs_dir = settings.DOCS_DIR
perms = discover_docs()
doc = perms.get(slug)
if not doc:
return HTMLResponse(content="Документ не найден", status_code=404)
filepath = os.path.join(docs_dir, doc["filename"])
with open(filepath, "w", encoding="utf-8") as f:
f.write(content)
return RedirectResponse(url=f"/service/documents/{slug}", status_code=302)
@router.get("/tech-access")
async def tech_access_page(
request: Request,
db: AsyncSession = Depends(get_db),
user: dict = Depends(get_current_user),
):
if user["role"] != "engineer":
return HTMLResponse(content="Доступ только для инженеров", status_code=403)
perms = discover_docs()
editable_docs = [
doc for slug, doc in perms.items()
if doc.get("permissions", {}).get("engineer", {}).get("edit", False)
]
return templates.TemplateResponse("documents/tech_access.html", {
"request": request, "user": user, "docs": editable_docs, "perms": perms,
})
@router.post("/tech-access")
async def tech_access_update(
request: Request,
db: AsyncSession = Depends(get_db),
user: dict = Depends(get_current_user),
):
if user["role"] != "engineer":
return HTMLResponse(content="Доступ только для инженеров", status_code=403)
form = await request.form()
perms = discover_docs()
for slug in perms.keys():
if not perms[slug].get("permissions", {}).get("engineer", {}).get("edit", False):
continue
technician_view = form.get(f"{slug}_technician_view") == "on"
perms[slug]["permissions"]["technician"] = {"view": technician_view}
save_permissions(perms)
return RedirectResponse(url="/service/documents/tech-access", status_code=302)
@router.get("/{slug}/download")
async def document_download(
slug: str,
request: Request,
db: AsyncSession = Depends(get_db),
user: dict = Depends(get_current_user),
):
if user["role"] != "owner":
return HTMLResponse(content="Скачивание доступно только владельцу", status_code=403)
from fastapi.responses import FileResponse
docs_dir = settings.DOCS_DIR
perms = discover_docs()
doc = perms.get(slug)
if not doc:
return HTMLResponse(content="Документ не найден", status_code=404)
filepath = os.path.join(docs_dir, doc["filename"])
if not os.path.exists(filepath):
return HTMLResponse(content="Файл не найден", status_code=404)
return FileResponse(filepath, filename=doc["filename"], media_type="text/markdown")
@router.get("/admin/permissions")
async def permissions_page(
request: Request,
db: AsyncSession = Depends(get_db),
user: dict = Depends(get_current_user),
):
if user["role"] != "owner":
return HTMLResponse(content="Доступ запрещён", status_code=403)
perms = discover_docs()
return templates.TemplateResponse("documents/permissions.html", {
"request": request, "user": user, "perms": perms,
})
@router.post("/admin/permissions")
async def permissions_update(
request: Request,
db: AsyncSession = Depends(get_db),
user: dict = Depends(get_current_user),
):
if user["role"] != "owner":
return HTMLResponse(content="Доступ запрещён", status_code=403)
form = await request.form()
perms = discover_docs()
for slug in perms.keys():
engineer_view = form.get(f"{slug}_engineer_view") == "on"
engineer_edit = form.get(f"{slug}_engineer_edit") == "on"
technician_view = form.get(f"{slug}_technician_view") == "on"
if engineer_edit and not engineer_view:
engineer_view = True
perms[slug]["permissions"]["engineer"] = {
"view": engineer_view,
"edit": engineer_edit,
"cancel": engineer_edit,
}
perms[slug]["permissions"]["technician"] = {
"view": technician_view,
}
save_permissions(perms)
return RedirectResponse(url="/service/documents/admin/permissions", status_code=302)
@router.post("/admin/create")
async def document_create(
request: Request,
title: str = Form(...),
db: AsyncSession = Depends(get_db),
user: dict = Depends(get_current_user),
):
if user["role"] != "owner":
return HTMLResponse(content="Доступ запрещён", status_code=403)
slug = transliterate_slug(title)
if not slug:
return HTMLResponse(content="Некорректное название", status_code=400)
docs_dir = settings.DOCS_DIR
os.makedirs(docs_dir, exist_ok=True)
filename = f"{slug}.md"
filepath = os.path.join(docs_dir, filename)
if os.path.exists(filepath):
return HTMLResponse(content="Документ уже существует", status_code=400)
with open(filepath, "w", encoding="utf-8") as f:
f.write(f"# {title}\n\n")
perms = load_permissions()
perms[slug] = {
"title": title,
"filename": filename,
"sort_order": len(perms),
"permissions": {
"engineer": {"view": False, "edit": False, "cancel": False},
"technician": {"view": False},
},
}
save_permissions(perms)
return RedirectResponse(url=f"/service/documents/{slug}/edit", status_code=302)