Add controlled service catalogue merge workflow

This commit is contained in:
A R R R Associates
2026-07-17 22:59:02 +05:30
parent 9f51461548
commit 0b58bf182b
4 changed files with 504 additions and 13 deletions
+83 -12
View File
@@ -1,7 +1,5 @@
from __future__ import annotations
from datetime import datetime, timezone
from fastapi import APIRouter, File, Form, Request, UploadFile
from fastapi.responses import FileResponse, RedirectResponse, StreamingResponse
from sqlalchemy import select
@@ -32,7 +30,7 @@ from app.modules.services.due_dates import (
list_due_rules,
parse_optional_date,
)
from app.modules.services.catalogue_export import build_service_catalogue_export
from app.modules.services.catalogue_merge import build_merge_preview, merge_service_catalogues
from app.modules.services.task_documents import (
create_task_document_requirement,
get_task_document_requirement,
@@ -335,8 +333,8 @@ def catalogue_list(request: Request, q: str = '', category_id: int | None = None
db.close()
@router.get('/catalogue/export')
def catalogue_export(request: Request):
@router.get('/catalogue/merge')
def catalogue_merge_page(request: Request, source_id: int | None = None, target_id: int | None = None):
db = CommonSessionLocal()
try:
user = get_current_user(request, db=db)
@@ -344,15 +342,88 @@ def catalogue_export(request: Request):
return RedirectResponse(url='/login', status_code=303)
if not _is_system_admin(db, user):
return _redirect_denied()
require_permission(db, user, 'services.view')
require_permission(db, user, 'services.edit')
workbook_bytes = build_service_catalogue_export(db)
filename = f"service_catalogue_export_{datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')}.xlsx"
return StreamingResponse(
iter([workbook_bytes]),
media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
headers={'Content-Disposition': f'attachment; filename="{filename}"'},
services = db.execute(
select(ServiceCatalogue).order_by(ServiceCatalogue.is_active.desc(), ServiceCatalogue.service_code.asc())
).scalars().all()
preview = None
error = None
if source_id and target_id:
try:
preview = build_merge_preview(db, source_id=source_id, target_id=target_id)
except ValueError as exc:
error = str(exc)
return _render(
request,
'modules/services/templates/services/catalogue_merge.html',
db,
user,
title='Merge Service Catalogue',
services=services,
source_id=source_id,
target_id=target_id,
preview=preview,
error=error,
)
except Exception:
return _redirect_denied()
finally:
db.close()
@router.post('/catalogue/merge')
def catalogue_merge_submit(
request: Request,
source_id: int = Form(...),
target_id: int = Form(...),
confirmation: str = Form(...),
csrf_token: str = Form(...),
):
validate_csrf(request, csrf_token)
db = CommonSessionLocal()
try:
user = get_current_user(request, db=db)
if not user:
return RedirectResponse(url='/login', status_code=303)
if not _is_system_admin(db, user):
return _redirect_denied()
require_permission(db, user, 'services.edit')
preview = build_merge_preview(db, source_id=source_id, target_id=target_id)
expected = f"MERGE {preview.source.service_code} INTO {preview.target.service_code}"
if confirmation.strip().upper() != expected.upper():
return RedirectResponse(
url=f'/services/catalogue/merge?source_id={source_id}&target_id={target_id}&error=confirmation',
status_code=303,
)
result = merge_service_catalogues(
db,
source_id=source_id,
target_id=target_id,
actor_user_id=user.id,
)
db.commit()
write_audit_log(
db,
action='services.catalogue.merged',
entity_type='service_catalogue',
entity_id=target_id,
entity_name=result['target_code'],
actor=user,
request=request,
details=result,
)
return RedirectResponse(url=f'/services/catalogue/{target_id}?merged=1', status_code=303)
except ValueError:
db.rollback()
return RedirectResponse(url='/services/catalogue/merge?error=invalid', status_code=303)
except Exception:
db.rollback()
raise
finally:
db.close()