function publish_document_v1
Publishes a controlled document by incrementing its version to the next major revision, converting it to a signed PDF, and updating its status to PUBLISHED.
/tf/active/vicechatdev/CDocs/controllers/document_controller.py
1138 - 1369
complex
Purpose
This function orchestrates the complete document publication workflow in a document management system. It validates the document's current state, increments the version number, generates a PDF with audit trail and signatures, uploads it to FileCloud, updates database records, logs the event, and sends notifications. It enforces business rules such as requiring APPROVED status before publishing (unless user has FORCE_PUBLISH_DOCUMENT permission).
Source Code
def publish_document(
user: DocUser,
document_uid: str,
publish_comment: Optional[str] = None
) -> Dict[str, Any]:
"""
Publish a document, changing its status to PUBLISHED.
This includes updating version to the next major revision and creating a PDF with signatures.
Args:
user: User publishing the document
document_uid: UID of document to publish
publish_comment: Optional comment about publishing
Returns:
Dictionary with publish status
Raises:
ResourceNotFoundError: If document not found
ValidationError: If validation fails
PermissionError: If user doesn't have permission
BusinessRuleError: If publishing is not allowed
"""
logger = logging.getLogger('CDocs.controllers.document_controller')
logger.info(f"Publishing document {document_uid} by user {user.username}")
try:
# Get document instance
document = ControlledDocument(uid=document_uid)
if not document:
raise ResourceNotFoundError(f"Document not found: {document_uid}")
# Check if document can be published
if document.status != STATUS_APPROVED:
logger.warning(f"Attempting to publish document with status {document.status}")
if not permissions.user_has_permission(user, "FORCE_PUBLISH_DOCUMENT"):
raise BusinessRuleError(
f"Document must be in APPROVED status to be published. Current status: {document.status}"
)
# Check if document has a current version
current_version = document.current_version
if not current_version:
raise ValidationError("Document has no current version")
# Calculate next major version number
current_revision = document.revision if hasattr(document, 'revision') else current_version.version_number
try:
major_version = int(current_revision.split('.')[0])
next_version = f"{major_version + 1}.0"
except (ValueError, IndexError):
next_version = "1.0" # Default if parsing fails
logger.info(f"Current version: {current_revision}, publishing as: {next_version}")
# Store original status for audit
previous_status = document.status
# Create a temporary directory for processing
with tempfile.TemporaryDirectory() as temp_dir:
try:
# 1. Download current editable document
source_file_path = current_version.word_file_path
if not source_file_path:
raise ValidationError("Document has no editable version")
# Use the filecloud_controller to download the file
file_content = download_document_from_filecloud(
document_uid=document_uid,
user=user,
version=current_version.version_number
)
# Save to temp file
temp_doc_path = os.path.join(temp_dir, f"document.{current_version.file_type}")
with open(temp_doc_path, 'wb') as f:
f.write(file_content)
# 2. UPDATE version number on existing version instead of creating new version
# Update the version number on the current version node
db.run_query(
"""
MATCH (v:DocumentVersion {UID: $version_uid})
SET v.version_number = $new_version
RETURN v
""",
{
"version_uid": current_version.uid,
"new_version": next_version
}
)
# Update the revision number on the document node
db.run_query(
"""
MATCH (d:ControlledDocument {UID: $doc_uid})
SET d.revision = $new_version
RETURN d
""",
{
"doc_uid": document_uid,
"new_version": next_version
}
)
# 3. Add a comment about publishing if provided
if publish_comment:
db.run_query(
"""
MATCH (v:DocumentVersion {UID: $version_uid})
SET v.comment = $comment
RETURN v
""",
{
"version_uid": current_version.uid,
"comment": publish_comment
}
)
# 4. Convert document to PDF with audit trail and signatures
# Prepare the audit data
audit_data = prepare_audit_data_for_document_processor(document, current_version, user)
# Create audit data JSON file
audit_json_path = os.path.join(temp_dir, "audit_data.json")
with open(audit_json_path, 'w') as f:
json.dump(audit_data, f)
# Use DocumentProcessor to create published PDF
from CDocs.document_auditor.src.document_processor import DocumentProcessor
processor = DocumentProcessor()
pdf_output_path = os.path.join(temp_dir, f"{document.doc_number}_v{next_version}.pdf")
final_pdf_path = processor.process_document(
original_doc_path=temp_doc_path,
json_path=audit_json_path,
output_path=pdf_output_path,
include_signatures=True,
convert_to_pdfa=True,
finalize=True
)
# 5. Upload PDF to FileCloud
with open(final_pdf_path, 'rb') as f:
pdf_content = f.read()
# Determine PDF path - should be at same level as document folder
if document.custom_path:
pdf_cloud_path = f"{document.custom_path}/{document.doc_number}_v{next_version}.pdf"
else:
department_folder = f"/{settings.FILECLOUD_ROOT_FOLDER}/{document.department}"
doc_type_folder = f"{department_folder}/{document.doc_type}"
pdf_cloud_path = f"{doc_type_folder}/{document.doc_number}_v{next_version}.pdf"
# Upload the published PDF to FileCloud
upload_result = upload_document_to_filecloud(
user=user,
document=document,
file_content=pdf_content,
file_path=pdf_cloud_path,
version_comment=publish_comment
)
if not upload_result.get('success', False):
raise BusinessRuleError(f"Failed to upload published PDF: {upload_result.get('message')}")
# 6. Update version with PDF path
current_version.pdf_file_path = pdf_cloud_path
# 7. Remove any existing shares
if hasattr(current_version, 'share_id') and current_version.share_id:
try:
from CDocs.controllers.filecloud_controller import get_filecloud_client
client = get_filecloud_client()
client.remove_share(current_version.share_id)
except Exception as share_err:
logger.warning(f"Error removing share {current_version.share_id}: {share_err}")
# 8. Update document and version status to PUBLISHED
document.status = STATUS_PUBLISHED
current_version.status = STATUS_PUBLISHED
# 9. Save all changes
document.save()
current_version.save()
# 10. Log publication event
audit_trail.log_document_lifecycle_event(
event_type="DOCUMENT_PUBLISHED",
user=user,
document_uid=document_uid,
details={
"previous_status": previous_status,
"version_number": next_version,
"comment": publish_comment,
"pdf_path": pdf_cloud_path
}
)
# 11. Notify about publication
notifications.notify_document_update(document, "DOCUMENT_PUBLISHED")
# Return success
return {
"success": True,
"document_uid": document_uid,
"document_number": document.doc_number,
"title": document.title,
"previous_status": previous_status,
"new_status": STATUS_PUBLISHED,
"previous_version": current_revision,
"new_version_number": next_version,
"comment": publish_comment,
"pdf_path": pdf_cloud_path,
"message": f"Document {document.doc_number} published successfully as version {next_version}"
}
except Exception as process_error:
logger.error(f"Error in document publication processing: {process_error}")
logger.error(traceback.format_exc())
raise BusinessRuleError(f"Document publication failed: {str(process_error)}")
except (ResourceNotFoundError, ValidationError, PermissionError) as e:
# Re-raise known errors
raise
except BusinessRuleError:
# Re-raise business rule errors
raise
except Exception as e:
logger.error(f"Unexpected error publishing document {document_uid}: {e}")
logger.error(traceback.format_exc())
raise BusinessRuleError(f"Failed to publish document due to unexpected error: {str(e)}")
Parameters
| Name | Type | Default | Kind |
|---|---|---|---|
user |
DocUser | - | positional_or_keyword |
document_uid |
str | - | positional_or_keyword |
publish_comment |
Optional[str] | None | positional_or_keyword |
Parameter Details
user: DocUser object representing the user performing the publication action. Used for permission checks, audit logging, and FileCloud operations.
document_uid: String containing the unique identifier (UID) of the document to be published. Must correspond to an existing ControlledDocument in the database.
publish_comment: Optional string containing a comment or note about the publication. This comment is stored with the document version and included in audit logs. Can be None if no comment is needed.
Return Value
Type: Dict[str, Any]
Returns a dictionary containing publication results with keys: 'success' (bool), 'document_uid' (str), 'document_number' (str), 'title' (str), 'previous_status' (str), 'new_status' (str - STATUS_PUBLISHED), 'previous_version' (str), 'new_version_number' (str - major version increment like '2.0'), 'comment' (str or None), 'pdf_path' (str - FileCloud path to published PDF), and 'message' (str - success message).
Dependencies
logginguuidostempfiletypingdatetimeiopanelshutiltracebackjsonreCDocs.dbCDocs.config.settingsCDocs.config.permissionsCDocs.models.documentCDocs.models.user_extensionsCDocs.utils.document_processorCDocs.utils.notificationsCDocs.utils.audit_trailCDocs.db.schema_managerCDocs.controllersCDocs.models.approvalCDocs.controllers.filecloud_controllerCDocs.controllers.share_controllerCDocs.db.db_operationsCDocs.utils.document_converterCDocs.models.document_statusCDocs.utils.filecloud_integrationCDocs.utils.notificationsCDocs.models.reviewCDocs.document_auditor.src.document_processorCDocs.controllers.training_controllerCDocs.utils.metadata_catalog
Required Imports
import logging
import os
import tempfile
import json
import traceback
from typing import Dict, Any, Optional
from CDocs import db
from CDocs.config import settings, permissions
from CDocs.models.document import ControlledDocument
from CDocs.models.user_extensions import DocUser
from CDocs.utils import notifications, audit_trail
from CDocs.controllers import ResourceNotFoundError, ValidationError, PermissionError, BusinessRuleError
from CDocs.controllers.filecloud_controller import upload_document_to_filecloud, download_document_from_filecloud, get_filecloud_client
from CDocs.models.document_status import STATUS_APPROVED, STATUS_PUBLISHED
from CDocs.document_auditor.src.document_processor import DocumentProcessor
Conditional/Optional Imports
These imports are only needed under specific conditions:
from CDocs.document_auditor.src.document_processor import DocumentProcessor
Condition: Required for PDF generation with audit trail and signatures during document publication
Required (conditional)from CDocs.controllers.filecloud_controller import get_filecloud_client
Condition: Only used if document version has an existing share_id that needs to be removed
OptionalUsage Example
from CDocs.models.user_extensions import DocUser
from CDocs.controllers.document_controller import publish_document
# Get the user and document UID
user = DocUser.get_by_username('john.doe')
document_uid = 'doc-12345-abcde'
# Publish the document with a comment
try:
result = publish_document(
user=user,
document_uid=document_uid,
publish_comment='Initial release of quality procedure'
)
if result['success']:
print(f"Document {result['document_number']} published successfully")
print(f"Version: {result['new_version_number']}")
print(f"PDF available at: {result['pdf_path']}")
else:
print(f"Publication failed: {result.get('message')}")
except ResourceNotFoundError:
print('Document not found')
except ValidationError as e:
print(f'Validation error: {e}')
except PermissionError:
print('User does not have permission to publish')
except BusinessRuleError as e:
print(f'Business rule violation: {e}')
Best Practices
- Ensure the document is in APPROVED status before calling this function, unless the user has FORCE_PUBLISH_DOCUMENT permission
- Always provide a meaningful publish_comment to maintain audit trail clarity
- This function is wrapped in a transaction decorator, so all database changes will be rolled back if any error occurs
- The function requires PUBLISH_DOCUMENT permission which is checked by the decorator before execution
- Handle all four exception types: ResourceNotFoundError, ValidationError, PermissionError, and BusinessRuleError
- The function creates temporary files during PDF generation - these are automatically cleaned up via tempfile.TemporaryDirectory context manager
- Version numbers are automatically incremented to the next major version (e.g., 1.5 becomes 2.0)
- The published PDF includes audit trail data and signatures, making it tamper-evident
- Existing FileCloud shares are removed during publication to ensure proper access control
- The function logs all actions and sends notifications automatically - no additional logging needed by caller
- PDF files are stored at the document type folder level in FileCloud, not in the document's version subfolder
- The function updates both the DocumentVersion and ControlledDocument nodes in the graph database
Tags
Similar Components
AI-powered semantic similarity - components with related functionality:
-
function publish_document 92.5% similar
-
function create_document_v2 72.1% similar
-
function archive_document 71.2% similar
-
function update_document_v1 70.9% similar
-
function create_document_v1 70.3% similar