class DocumentVersion_v1
Model representing a specific version of a controlled document.
/tf/active/vicechatdev/CDocs/models/document.py
43 - 696
moderate
Purpose
Model representing a specific version of a controlled document.
Source Code
class DocumentVersion(BaseModel):
"""Model representing a specific version of a controlled document."""
def __init__(self, data: Optional[Dict[str, Any]] = None, uid: Optional[str] = None):
"""
Initialize a document version.
Args:
data: Dictionary of version properties
uid: Version UID to load from database (if data not provided)
"""
if data is None and uid is not None:
# Fetch document data from database
data = db.get_node_by_uid(uid)
super().__init__(data or {})
self._parent_doc = None # Cached parent document
@classmethod
def create(cls, document_uid: str, version_number: str,
created_by: Union[DocUser, str],
file_paths: Optional[Dict[str, str]] = None,
properties: Optional[Dict[str, Any]] = None) -> Optional['DocumentVersion']:
"""
Create a new document version.
Args:
document_uid: UID of parent document
version_number: Version number string (e.g., "1.0")
created_by: User creating the version or their UID
file_paths: Dictionary containing file paths in FileCloud
Can include 'base', 'word', 'pdf' keys
properties: Additional properties for the version
Returns:
New DocumentVersion instance or None if creation failed
"""
try:
# First fetch the document to check if it has a custom path
doc = ControlledDocument(uid=document_uid)
if not doc:
logger.error(f"Document not found: {document_uid}")
return None
# Prepare properties
props = properties or {}
props.update({
'versionNumber': version_number,
'status': 'DRAFT',
'createdDate': datetime.now(),
})
# Handle file paths based on the document's path structure
if file_paths:
# If base path is directly provided, use it
if 'base' in file_paths:
props['fileCloudPath'] = file_paths['base']
# Otherwise, try to derive it based on custom path or standard structure
elif 'word' in file_paths or 'pdf' in file_paths:
# Get the source file path (prefer word over PDF)
source_path = file_paths.get('word', file_paths.get('pdf'))
if doc.custom_path:
# For custom path, derive base path maintaining the custom path structure
document_folder = f"{doc.custom_path}/{doc.doc_number}"
base_name = f"{doc.doc_number}_v{version_number}"
props['fileCloudPath'] = f"{document_folder}/{base_name}"
else:
# For standard path, use the file path directly
base_path, ext = os.path.splitext(source_path)
props['fileCloudPath'] = base_path
# Get file type from source path
_, ext = os.path.splitext(source_path)
if ext and ext.startswith('.'):
props['file_type'] = ext[1:]
else:
props['file_type'] = 'docx' # Default to docx if extension unclear
else:
# No file paths provided, but we should still set fileCloudPath
# based on document structure for future use
if doc.custom_path:
document_folder = f"{doc.custom_path}/{doc.doc_number}"
base_name = f"{doc.doc_number}_v{version_number}"
props['fileCloudPath'] = f"{document_folder}/{base_name}"
else:
department_folder = f"/{settings.FILECLOUD_ROOT_FOLDER}/{doc.department}"
doc_type_folder = f"{department_folder}/{doc.doc_type}"
document_folder = f"{doc_type_folder}/{doc.doc_number}"
base_name = f"{doc.doc_number}_v{version_number}"
props['fileCloudPath'] = f"{document_folder}/{base_name}"
# Create node in database
version_data = db.create_node(
NodeLabels.DOCUMENT_VERSION,
props,
document_uid,
RelTypes.HAS_VERSION
)
if not version_data:
logger.error(f"Failed to create document version for document {document_uid}")
return None
# Create the version instance
version = cls(version_data)
# Link to author
user_uid = created_by.uid if isinstance(created_by, DocUser) else created_by
db.create_relationship(
version.uid,
user_uid,
RelTypes.AUTHORED_BY
)
# Link to previous version if any exists
previous_versions = db.run_query(
"""
MATCH (d:ControlledDocument {UID: $doc_uid})-[:HAS_VERSION]->(v:DocumentVersion)
WHERE v.UID <> $version_uid
RETURN v
ORDER BY v.versionNumber DESC
LIMIT 1
""",
{"doc_uid": document_uid, "version_uid": version.uid}
)
if previous_versions and 'v' in previous_versions[0]:
prev_version = previous_versions[0]['v']
db.create_relationship(
version.uid,
prev_version['UID'],
RelTypes.PREVIOUS_VERSION
)
return version
except Exception as e:
logger.error(f"Error creating document version: {e}")
return None
@property
def document_uid(self) -> Optional[str]:
"""Get the UID of the parent document."""
if hasattr(self, '_document_uid') and self._document_uid:
return self._document_uid
result = db.run_query(
"""
MATCH (d:ControlledDocument)-[:HAS_VERSION]->(v:DocumentVersion {UID: $uid})
RETURN d.UID as doc_uid
""",
{"uid": self.uid}
)
if result and 'doc_uid' in result[0]:
self._document_uid = result[0]['doc_uid']
return self._document_uid
return None
@property
def document(self) -> Optional['ControlledDocument']:
"""Get the parent document instance."""
if self._parent_doc:
return self._parent_doc
doc_uid = self.document_uid
if doc_uid:
self._parent_doc = ControlledDocument(uid=doc_uid)
return self._parent_doc
return None
@property
def version_number(self) -> str:
"""Get version number."""
return self._data.get('versionNumber', self._data.get('version_number', '1.0'))
@property
def share_id(self) -> str:
"""Get share ID for this version."""
return self._data.get('share_id', '')
@share_id.setter
def share_id(self, value: str) -> None:
"""Set share ID for this version."""
self._data['share_id'] = value
db.update_node(self.uid, {'share_id': value})
@property
def share_url(self) -> str:
"""Get share URL for this version."""
return self._data.get('share_url', self.get_primary_file_path())
@share_url.setter
def share_url(self, value: str) -> None:
"""Set share URL for this version."""
self._data['share_url'] = value
db.update_node(self.uid, {'share_url': value})
@property
def status(self) -> str:
"""Get version status code."""
return self._data.get('status', 'DRAFT')
@status.setter
def status(self, value: str) -> None:
"""Set version status using code."""
# Convert from name to code if needed
status_code = settings.get_document_status_code(value)
# Validate status code
if not settings.is_valid_document_status(status_code):
logger.warning(f"Invalid status: {value}")
return
self._data['status'] = status_code
db.update_node(self.uid, {'status': status_code})
def get_status_name(self) -> str:
"""Get the full status name."""
return settings.get_document_status_name(self.status)
def get_status_color(self) -> str:
"""Get the color for displaying this status."""
return settings.get_status_color(self.status)
@property
def is_published(self) -> bool:
"""
Check if this version is published.
Returns:
bool: True if this version is published, False otherwise
"""
return is_published_status(self.status)
@property
def is_current(self) -> bool:
"""
Check if this version is the current version of its document.
Returns:
bool: True if this is the current version, False otherwise
"""
try:
# Use the document method if we have a document reference
document = self.document
if document:
return document.is_current_version(self.uid)
# Otherwise, query directly using the database helper
result = db.run_query(
"""
MATCH (d:ControlledDocument)-[:CURRENT_VERSION]->(v:DocumentVersion {UID: $version_uid})
RETURN COUNT(d) > 0 as is_current
""",
{"version_uid": self.uid}
)
if result and 'is_current' in result[0]:
return result[0]['is_current']
return False
except Exception as e:
logger.error(f"Error checking if version is current: {e}")
return False
@property
def filecloud_path(self) -> Optional[str]:
"""
Get the base FileCloud path without extension.
This is the central attribute for file references.
Returns:
str: Base path in FileCloud without file extension
"""
return self._data.get('fileCloudPath')
@filecloud_path.setter
def filecloud_path(self, path: str) -> None:
"""
Set the base FileCloud path without extension.
Args:
path: Base path in FileCloud without file extension
"""
self._data['fileCloudPath'] = path
db.update_node(self.uid, {'fileCloudPath': path})
@property
def word_file_path(self) -> Optional[str]:
"""
Get path to editable file in FileCloud.
Dynamically constructs path based on filecloud_path and file_type.
Returns:
str: Path to the editable file or None if base path is not available
"""
# Import here to avoid circular imports
from CDocs.controllers.filecloud_controller import get_filecloud_document_path
# Use the document and version number to get the full path
document = self.document
if not document:
return None
try:
# Get file path using the centralized function
return get_filecloud_document_path(document, self.version_number)
except Exception as e:
logger.error(f"Error getting word file path: {e}")
return None
@property
def pdf_file_path(self) -> Optional[str]:
"""
Get path to PDF file in FileCloud.
Uses centralized path function and checks if PDF exists.
Returns:
str: Path to the PDF file or None if file doesn't exist
"""
# Import here to avoid circular imports
from CDocs.controllers.filecloud_controller import get_filecloud_document_path
# Use the document and version number to get the full path
document = self.document
if not document:
return None
try:
# Get editable file path
file_path = get_filecloud_document_path(document, self.version_number)
# If it's already a PDF, return it
if file_path.lower().endswith('.pdf'):
return file_path
# Otherwise construct PDF path - replace extension with .pdf
base_path = os.path.splitext(file_path)[0]
pdf_path = f"{base_path}.pdf"
# Check if PDF exists in FileCloud
from CDocs.controllers.filecloud_controller import get_filecloud_client
client = get_filecloud_client()
if client.check_file_exists(pdf_path):
return pdf_path
return None
except Exception as e:
logger.error(f"Error getting PDF file path: {e}")
return None
@property
def file_type(self) -> Optional[str]:
"""Get filetype and extension of file"""
return self._data.get('file_type')
@property
def file_name(self) -> Optional[str]:
"""Get filetype and extension of file"""
return self._data.get('file_name')
@pdf_file_path.setter
def pdf_file_path(self, path: str) -> None:
"""
Set the PDF file path and extract base path if not already set.
Args:
path: Full path to the PDF file
"""
if not path:
return
# Extract the base path by removing the extension
base_path, ext = os.path.splitext(path)
# If filecloud_path isn't set yet, set it
if not self.filecloud_path:
self.filecloud_path = base_path
#db.update_node(self.uid, {'fileCloudPath': path})
@property
def created_date(self) -> Optional[datetime]:
"""Get version creation date."""
return self._data.get('created_date')
@property
def effective_date(self) -> Optional[datetime]:
"""Get date when version becomes effective."""
return self._data.get('effective_date')
@effective_date.setter
def effective_date(self, date: datetime) -> None:
"""Set effective date."""
self._data['effectiveDate'] = date
db.update_node(self.uid, {'effective_date': date})
@property
def expiry_date(self) -> Optional[datetime]:
"""Get expiry date."""
return self._data.get('expiryDate')
@expiry_date.setter
def expiry_date(self, date: datetime) -> None:
"""Set expiry date."""
self._data['expiryDate'] = date
db.update_node(self.uid, {'expiryDate': date})
@property
def author(self) -> Optional[DocUser]:
"""Get author of the version."""
result = db.run_query(
"""
MATCH (v:DocumentVersion {UID: $uid})-[:AUTHORED_BY]->(u:User)
RETURN u
""",
{"uid": self.uid}
)
if result and 'u' in result[0]:
return DocUser(result[0]['u'])
return None
@property
def change_summary(self) -> str:
"""Get summary of changes in this version."""
return self._data.get('changeSummary', '')
@change_summary.setter
def change_summary(self, summary: str) -> None:
"""Set change summary."""
self._data['changeSummary'] = summary
db.update_node(self.uid, {'changeSummary': summary})
@property
def hash(self) -> Optional[str]:
"""Get document hash."""
return self._data.get('hash')
@hash.setter
def hash(self, value: str) -> None:
"""Set document hash."""
self._data['hash'] = value
db.update_node(self.uid, {'hash': value})
def calculate_hash(self, file_content: bytes) -> str:
"""
Calculate SHA-256 hash for file content.
Args:
file_content: Content of the file as bytes
Returns:
Hash string
"""
hasher = hashlib.sha256()
hasher.update(file_content)
hash_value = hasher.hexdigest()
# Store hash in the database
self.hash = hash_value
return hash_value
def get_previous_version(self) -> Optional['DocumentVersion']:
"""
Get the previous version of the document.
Returns:
Previous DocumentVersion or None if this is the first version
"""
result = db.run_query(
"""
MATCH (v:DocumentVersion {UID: $uid})-[:PREVIOUS_VERSION]->(prev:DocumentVersion)
RETURN prev
""",
{"uid": self.uid}
)
if result and 'prev' in result[0]:
return DocumentVersion(result[0]['prev'])
return None
def start_review(self, reviewers: List[Union[DocUser, str]],
due_date: Optional[datetime] = None,
instructions: str = '') -> Optional['ReviewCycle']:
"""
Start a review cycle for this version.
Args:
reviewers: List of DocUser instances or UIDs of reviewers
due_date: Date when review should be completed
instructions: Instructions for reviewers
Returns:
ReviewCycle instance or None if creation failed
"""
# Circular import handled by delayed import
from .review import ReviewCycle
# Generate due date if not provided
if not due_date:
due_date = datetime.now() + timedelta(days=settings.DEFAULT_REVIEW_DAYS)
# Create review cycle
return ReviewCycle.create(self.uid, reviewers, due_date, instructions)
def get_active_review(self) -> Optional[Dict[str, Any]]:
"""
Get the active review cycle for this version.
Returns:
Review cycle data or None if no active review
"""
result = db.run_query(
"""
MATCH (v:DocumentVersion {UID: $uid})-[:FOR_REVIEW]->(r:ReviewCycle)
WHERE r.status IN ['PENDING', 'IN_PROGRESS']
RETURN r
ORDER BY r.startDate DESC
LIMIT 1
""",
{"uid": self.uid}
)
if result and 'r' in result[0]:
return result[0]['r']
return None
def get_reviews(self) -> List[Dict[str, Any]]:
"""
Get all review cycles for this version.
Returns:
List of review cycle data
"""
result = db.run_query(
"""
MATCH (v:DocumentVersion {UID: $uid})-[:FOR_REVIEW]->(r:ReviewCycle)
RETURN r
ORDER BY r.startDate DESC
""",
{"uid": self.uid}
)
return [record['r'] for record in result if 'r' in record]
# Add this method to the ControlledDocument class
def save(self) -> bool:
"""Save all changes to the database."""
try:
# If node doesn't exist, create it
if not db.node_exists(self.uid):
created = db.create_node_with_uid(
NodeLabels.CONTROLLED_DOCUMENT,
self._data,
self.uid
)
return created
# Otherwise update existing node
return db.update_node(self.uid, self._data)
except Exception as e:
logger.error(f"Error saving document: {e}")
import traceback
logger.error(traceback.format_exc())
return False
def to_dict(self) -> Dict[str, Any]:
"""Convert to dictionary representation."""
result = super().to_dict()
# Add author information if available
author = self.author
if author:
result['author'] = {
'UID': author.uid,
'name': author.name,
'email': author.email
}
# Add document ID if available
document = self.document
if document:
result['documentId'] = document.doc_number
return result
def get_primary_file_path(self) -> Optional[str]:
"""
Get the primary file path based on document status.
For published/non-editable documents, returns PDF path.
For editable documents, returns editable document path.
Returns:
str: Path to the primary file or None if no files are available
"""
if not self.is_current:
return None
if self.is_published:
return self.pdf_file_path
else:
return self.word_file_path
def has_pdf_version(self) -> bool:
"""
Check if this version has a PDF version.
Returns:
bool: True if version has PDF file
"""
return bool(self.pdf_file_path)
def has_editable_version(self) -> bool:
"""
Check if this version has an editable version.
Returns:
bool: True if version has editable file
"""
return bool(self.word_file_path)
def get_file_extension(self) -> str:
"""
Get the file extension for the primary file.
Returns:
str: File extension (like '.docx', '.pdf')
"""
if self.word_file_path:
# Get extension from path
return os.path.splitext(self.word_file_path)[1].lower()
elif self.pdf_file_path:
return '.pdf'
else:
return ''
def is_editable_format(self) -> bool:
"""
Check if the primary file is in an editable format.
Returns:
bool: True if editable format
"""
ext = self.get_file_extension()
return ext.lower() in ['.docx', '.doc', '.pptx', '.ppt', '.xlsx', '.xls']
Parameters
| Name | Type | Default | Kind |
|---|---|---|---|
bases |
BaseModel | - |
Parameter Details
bases: Parameter of type BaseModel
Return Value
Returns unspecified type
Class Interface
Methods
__init__(self, data, uid)
Purpose: Initialize a document version. Args: data: Dictionary of version properties uid: Version UID to load from database (if data not provided)
Parameters:
data: Type: Optional[Dict[str, Any]]uid: Type: Optional[str]
Returns: None
create(cls, document_uid, version_number, created_by, file_paths, properties) -> Optional['DocumentVersion']
Purpose: Create a new document version. Args: document_uid: UID of parent document version_number: Version number string (e.g., "1.0") created_by: User creating the version or their UID file_paths: Dictionary containing file paths in FileCloud Can include 'base', 'word', 'pdf' keys properties: Additional properties for the version Returns: New DocumentVersion instance or None if creation failed
Parameters:
cls: Parameterdocument_uid: Type: strversion_number: Type: strcreated_by: Type: Union[DocUser, str]file_paths: Type: Optional[Dict[str, str]]properties: Type: Optional[Dict[str, Any]]
Returns: Returns Optional['DocumentVersion']
document_uid(self) -> Optional[str]
property
Purpose: Get the UID of the parent document.
Returns: Returns Optional[str]
document(self) -> Optional['ControlledDocument']
property
Purpose: Get the parent document instance.
Returns: Returns Optional['ControlledDocument']
version_number(self) -> str
property
Purpose: Get version number.
Returns: Returns str
share_id(self) -> str
property
Purpose: Get share ID for this version.
Returns: Returns str
share_id(self, value) -> None
Purpose: Set share ID for this version.
Parameters:
value: Type: str
Returns: Returns None
share_url(self) -> str
property
Purpose: Get share URL for this version.
Returns: Returns str
share_url(self, value) -> None
Purpose: Set share URL for this version.
Parameters:
value: Type: str
Returns: Returns None
status(self) -> str
property
Purpose: Get version status code.
Returns: Returns str
status(self, value) -> None
Purpose: Set version status using code.
Parameters:
value: Type: str
Returns: Returns None
get_status_name(self) -> str
Purpose: Get the full status name.
Returns: Returns str
get_status_color(self) -> str
Purpose: Get the color for displaying this status.
Returns: Returns str
is_published(self) -> bool
property
Purpose: Check if this version is published. Returns: bool: True if this version is published, False otherwise
Returns: Returns bool
is_current(self) -> bool
property
Purpose: Check if this version is the current version of its document. Returns: bool: True if this is the current version, False otherwise
Returns: Returns bool
filecloud_path(self) -> Optional[str]
property
Purpose: Get the base FileCloud path without extension. This is the central attribute for file references. Returns: str: Base path in FileCloud without file extension
Returns: Returns Optional[str]
filecloud_path(self, path) -> None
Purpose: Set the base FileCloud path without extension. Args: path: Base path in FileCloud without file extension
Parameters:
path: Type: str
Returns: Returns None
word_file_path(self) -> Optional[str]
property
Purpose: Get path to editable file in FileCloud. Dynamically constructs path based on filecloud_path and file_type. Returns: str: Path to the editable file or None if base path is not available
Returns: Returns Optional[str]
pdf_file_path(self) -> Optional[str]
property
Purpose: Get path to PDF file in FileCloud. Uses centralized path function and checks if PDF exists. Returns: str: Path to the PDF file or None if file doesn't exist
Returns: Returns Optional[str]
file_type(self) -> Optional[str]
property
Purpose: Get filetype and extension of file
Returns: Returns Optional[str]
file_name(self) -> Optional[str]
property
Purpose: Get filetype and extension of file
Returns: Returns Optional[str]
pdf_file_path(self, path) -> None
Purpose: Set the PDF file path and extract base path if not already set. Args: path: Full path to the PDF file
Parameters:
path: Type: str
Returns: Returns None
created_date(self) -> Optional[datetime]
property
Purpose: Get version creation date.
Returns: Returns Optional[datetime]
effective_date(self) -> Optional[datetime]
property
Purpose: Get date when version becomes effective.
Returns: Returns Optional[datetime]
effective_date(self, date) -> None
Purpose: Set effective date.
Parameters:
date: Type: datetime
Returns: Returns None
expiry_date(self) -> Optional[datetime]
property
Purpose: Get expiry date.
Returns: Returns Optional[datetime]
expiry_date(self, date) -> None
Purpose: Set expiry date.
Parameters:
date: Type: datetime
Returns: Returns None
author(self) -> Optional[DocUser]
property
Purpose: Get author of the version.
Returns: Returns Optional[DocUser]
change_summary(self) -> str
property
Purpose: Get summary of changes in this version.
Returns: Returns str
change_summary(self, summary) -> None
Purpose: Set change summary.
Parameters:
summary: Type: str
Returns: Returns None
hash(self) -> Optional[str]
property
Purpose: Get document hash.
Returns: Returns Optional[str]
hash(self, value) -> None
Purpose: Set document hash.
Parameters:
value: Type: str
Returns: Returns None
calculate_hash(self, file_content) -> str
Purpose: Calculate SHA-256 hash for file content. Args: file_content: Content of the file as bytes Returns: Hash string
Parameters:
file_content: Type: bytes
Returns: Returns str
get_previous_version(self) -> Optional['DocumentVersion']
Purpose: Get the previous version of the document. Returns: Previous DocumentVersion or None if this is the first version
Returns: Returns Optional['DocumentVersion']
start_review(self, reviewers, due_date, instructions) -> Optional['ReviewCycle']
Purpose: Start a review cycle for this version. Args: reviewers: List of DocUser instances or UIDs of reviewers due_date: Date when review should be completed instructions: Instructions for reviewers Returns: ReviewCycle instance or None if creation failed
Parameters:
reviewers: Type: List[Union[DocUser, str]]due_date: Type: Optional[datetime]instructions: Type: str
Returns: Returns Optional['ReviewCycle']
get_active_review(self) -> Optional[Dict[str, Any]]
Purpose: Get the active review cycle for this version. Returns: Review cycle data or None if no active review
Returns: Returns Optional[Dict[str, Any]]
get_reviews(self) -> List[Dict[str, Any]]
Purpose: Get all review cycles for this version. Returns: List of review cycle data
Returns: Returns List[Dict[str, Any]]
save(self) -> bool
Purpose: Save all changes to the database.
Returns: Returns bool
to_dict(self) -> Dict[str, Any]
Purpose: Convert to dictionary representation.
Returns: Returns Dict[str, Any]
get_primary_file_path(self) -> Optional[str]
Purpose: Get the primary file path based on document status. For published/non-editable documents, returns PDF path. For editable documents, returns editable document path. Returns: str: Path to the primary file or None if no files are available
Returns: Returns Optional[str]
has_pdf_version(self) -> bool
Purpose: Check if this version has a PDF version. Returns: bool: True if version has PDF file
Returns: Returns bool
has_editable_version(self) -> bool
Purpose: Check if this version has an editable version. Returns: bool: True if version has editable file
Returns: Returns bool
get_file_extension(self) -> str
Purpose: Get the file extension for the primary file. Returns: str: File extension (like '.docx', '.pdf')
Returns: Returns str
is_editable_format(self) -> bool
Purpose: Check if the primary file is in an editable format. Returns: bool: True if editable format
Returns: Returns bool
Required Imports
import logging
import uuid
import hashlib
from typing import Dict
from typing import List
Usage Example
# Example usage:
# result = DocumentVersion(bases)
Tags
Similar Components
AI-powered semantic similarity - components with related functionality:
-
class DocumentVersion 78.5% similar
-
class ControlledDocument 75.1% similar
-
class Document 67.6% similar
-
class ApprovalCycle_v1 61.9% similar
-
class ApprovalCycle 59.9% similar