from __future__ import annotations from pathlib import Path from govoplan_core.core.module_guards import drop_table_retirement_provider, persistent_table_uninstall_guard from govoplan_core.core.modules import FrontendModule, MigrationSpec, ModuleContext, ModuleManifest, NavItem, PermissionDefinition, RoleTemplate from govoplan_core.db.base import Base from govoplan_files.backend.db import models as file_models # noqa: F401 - populate Files ORM metadata def _permission(scope: str, label: str, description: str) -> PermissionDefinition: module_id, resource, action = scope.split(":", 2) return PermissionDefinition( scope=scope, label=label, description=description, category="Files", level="tenant", module_id=module_id, resource=resource, action=action, ) PERMISSIONS = ( _permission("files:file:read", "View files", "List, search and preview managed files."), _permission("files:file:download", "Download files", "Download managed files and generated archives."), _permission("files:file:upload", "Upload files", "Upload new managed file versions."), _permission("files:file:organize", "Organize files", "Create folders, rename, move or copy managed files."), _permission("files:file:share", "Share files", "Grant or revoke managed file shares."), _permission("files:file:delete", "Delete files", "Delete or hide managed files and folders where policy allows it."), _permission("files:file:admin", "Administer file spaces", "Administer all file spaces in the tenant."), ) ROLE_TEMPLATES = ( RoleTemplate( slug="file_manager", name="File manager", description="Manage tenant file spaces without campaign delivery rights.", permissions=( "files:file:read", "files:file:download", "files:file:upload", "files:file:organize", "files:file:share", "files:file:delete", ), ), RoleTemplate( slug="file_viewer", name="File viewer", description="Read and download permitted files.", permissions=("files:file:read", "files:file:download"), ), ) def _tenant_summary(session, tenant_id: str) -> dict[str, int]: from govoplan_files.backend.db.models import FileAsset return {"files": session.query(FileAsset).filter(FileAsset.tenant_id == tenant_id).count()} def _veto_group_delete(session, tenant_id: str, group_id: str) -> None: from govoplan_files.backend.db.models import FileAsset, FileFolder, FileShare owned_asset_count = session.query(FileAsset).filter(FileAsset.tenant_id == tenant_id, FileAsset.owner_group_id == group_id).count() owned_folder_count = session.query(FileFolder).filter(FileFolder.tenant_id == tenant_id, FileFolder.owner_group_id == group_id).count() shared_asset_count = session.query(FileShare).filter( FileShare.tenant_id == tenant_id, FileShare.target_type == "group", FileShare.target_id == group_id, ).count() if owned_asset_count or owned_folder_count or shared_asset_count: raise ValueError("Cannot remove the group while it owns files, folders or file shares.") def _files_router(context: ModuleContext): from govoplan_files.backend.runtime import configure_runtime configure_runtime(registry=context.registry, settings=context.settings) from govoplan_files.backend.router import router return router manifest = ModuleManifest( id="files", name="Files", version="0.1.6", dependencies=("access",), optional_dependencies=(), permissions=PERMISSIONS, route_factory=_files_router, role_templates=ROLE_TEMPLATES, tenant_summary_providers=(_tenant_summary,), delete_veto_providers={"group": (_veto_group_delete,)}, nav_items=(NavItem(path="/files", label="Files", icon="folder", required_any=("files:file:read",), order=40),), frontend=FrontendModule( module_id="files", package_name="@govoplan/files-webui", nav_items=(NavItem(path="/files", label="Files", icon="folder", required_any=("files:file:read",), order=40),), ), migration_spec=MigrationSpec( module_id="files", metadata=Base.metadata, script_location=str(Path(__file__).with_name("migrations") / "versions"), retirement_supported=True, retirement_provider=drop_table_retirement_provider( file_models.FileBlob, file_models.FileFolder, file_models.FileAsset, file_models.FileVersion, file_models.FileShare, file_models.CampaignAttachmentUse, label="Files", ), retirement_notes="Destructive retirement drops files-owned database tables after the installer captures a database snapshot.", ), uninstall_guard_providers=( persistent_table_uninstall_guard( file_models.FileBlob, file_models.FileFolder, file_models.FileAsset, file_models.FileVersion, file_models.FileShare, file_models.CampaignAttachmentUse, label="Files", ), ), capability_factories={ "files.campaign_attachments": lambda context: __import__("govoplan_files.backend.capabilities", fromlist=["campaign_capability"]).campaign_capability(context), }, ) def get_manifest() -> ModuleManifest: return manifest