changes
This commit is contained in:
@@ -17,6 +17,15 @@ from .pensions import (
|
||||
SeparationAgreement, LifeTable, NumberTable, PensionResult
|
||||
)
|
||||
from .templates import DocumentTemplate, DocumentTemplateVersion, TemplateKeyword
|
||||
from .template_variables import (
|
||||
TemplateVariable, VariableContext, VariableAuditLog,
|
||||
VariableTemplate, VariableGroup, VariableType
|
||||
)
|
||||
from .document_workflows import (
|
||||
DocumentWorkflow, WorkflowAction, WorkflowExecution, EventLog,
|
||||
WorkflowTemplate, WorkflowSchedule, WorkflowTriggerType, WorkflowActionType,
|
||||
ExecutionStatus, WorkflowStatus
|
||||
)
|
||||
from .billing import (
|
||||
BillingBatch, BillingBatchFile, StatementTemplate, BillingStatement,
|
||||
BillingStatementItem, StatementPayment, StatementStatus
|
||||
@@ -26,8 +35,14 @@ from .timers import (
|
||||
)
|
||||
from .file_management import (
|
||||
FileStatusHistory, FileTransferHistory, FileArchiveInfo,
|
||||
FileClosureChecklist, FileAlert
|
||||
FileClosureChecklist, FileAlert, FileRelationship
|
||||
)
|
||||
from .jobs import JobRecord
|
||||
from .deadlines import (
|
||||
Deadline, DeadlineReminder, DeadlineTemplate, DeadlineHistory,
|
||||
CourtCalendar, DeadlineType, DeadlinePriority, DeadlineStatus, NotificationFrequency
|
||||
)
|
||||
from .sessions import UserSession, SessionActivity, SessionConfiguration, SessionSecurityEvent
|
||||
from .lookups import (
|
||||
Employee, FileType, FileStatus, TransactionType, TransactionCode,
|
||||
State, GroupLookup, Footer, PlanInfo, FormIndex, FormList,
|
||||
@@ -48,5 +63,14 @@ __all__ = [
|
||||
"BillingStatementItem", "StatementPayment", "StatementStatus",
|
||||
"Timer", "TimeEntry", "TimerSession", "TimerTemplate", "TimerStatus", "TimerType",
|
||||
"FileStatusHistory", "FileTransferHistory", "FileArchiveInfo",
|
||||
"FileClosureChecklist", "FileAlert"
|
||||
"FileClosureChecklist", "FileAlert", "FileRelationship",
|
||||
"Deadline", "DeadlineReminder", "DeadlineTemplate", "DeadlineHistory",
|
||||
"CourtCalendar", "DeadlineType", "DeadlinePriority", "DeadlineStatus", "NotificationFrequency",
|
||||
"JobRecord",
|
||||
"UserSession", "SessionActivity", "SessionConfiguration", "SessionSecurityEvent",
|
||||
"TemplateVariable", "VariableContext", "VariableAuditLog",
|
||||
"VariableTemplate", "VariableGroup", "VariableType",
|
||||
"DocumentWorkflow", "WorkflowAction", "WorkflowExecution", "EventLog",
|
||||
"WorkflowTemplate", "WorkflowSchedule", "WorkflowTriggerType", "WorkflowActionType",
|
||||
"ExecutionStatus", "WorkflowStatus"
|
||||
]
|
||||
388
app/models/audit_enhanced.py
Normal file
388
app/models/audit_enhanced.py
Normal file
@@ -0,0 +1,388 @@
|
||||
"""
|
||||
Enhanced audit logging models for P2 security features
|
||||
"""
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional, Dict, Any
|
||||
from enum import Enum
|
||||
import json
|
||||
|
||||
from sqlalchemy import Column, Integer, String, DateTime, Boolean, ForeignKey, Text, Index
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
|
||||
from app.models.base import BaseModel
|
||||
|
||||
|
||||
class SecurityEventType(str, Enum):
|
||||
"""Security event types for classification"""
|
||||
# Authentication events
|
||||
LOGIN_SUCCESS = "login_success"
|
||||
LOGIN_FAILURE = "login_failure"
|
||||
LOGOUT = "logout"
|
||||
SESSION_EXPIRED = "session_expired"
|
||||
PASSWORD_CHANGE = "password_change"
|
||||
ACCOUNT_LOCKED = "account_locked"
|
||||
|
||||
# Authorization events
|
||||
ACCESS_DENIED = "access_denied"
|
||||
PRIVILEGE_ESCALATION = "privilege_escalation"
|
||||
UNAUTHORIZED_ACCESS = "unauthorized_access"
|
||||
|
||||
# Data access events
|
||||
DATA_READ = "data_read"
|
||||
DATA_WRITE = "data_write"
|
||||
DATA_DELETE = "data_delete"
|
||||
DATA_EXPORT = "data_export"
|
||||
BULK_OPERATION = "bulk_operation"
|
||||
|
||||
# System events
|
||||
CONFIGURATION_CHANGE = "configuration_change"
|
||||
USER_CREATION = "user_creation"
|
||||
USER_MODIFICATION = "user_modification"
|
||||
USER_DELETION = "user_deletion"
|
||||
|
||||
# Security events
|
||||
SUSPICIOUS_ACTIVITY = "suspicious_activity"
|
||||
ATTACK_DETECTED = "attack_detected"
|
||||
SECURITY_VIOLATION = "security_violation"
|
||||
IP_BLOCKED = "ip_blocked"
|
||||
|
||||
# File events
|
||||
FILE_UPLOAD = "file_upload"
|
||||
FILE_DOWNLOAD = "file_download"
|
||||
FILE_DELETION = "file_deletion"
|
||||
FILE_MODIFICATION = "file_modification"
|
||||
|
||||
# Integration events
|
||||
API_ACCESS = "api_access"
|
||||
EXTERNAL_SERVICE = "external_service"
|
||||
IMPORT_OPERATION = "import_operation"
|
||||
|
||||
|
||||
class SecurityEventSeverity(str, Enum):
|
||||
"""Security event severity levels"""
|
||||
CRITICAL = "critical"
|
||||
HIGH = "high"
|
||||
MEDIUM = "medium"
|
||||
LOW = "low"
|
||||
INFO = "info"
|
||||
|
||||
|
||||
class ComplianceStandard(str, Enum):
|
||||
"""Compliance standards for reporting"""
|
||||
SOX = "sox" # Sarbanes-Oxley
|
||||
HIPAA = "hipaa" # Health Insurance Portability and Accountability Act
|
||||
GDPR = "gdpr" # General Data Protection Regulation
|
||||
SOC2 = "soc2" # Service Organization Control 2
|
||||
ISO27001 = "iso27001" # Information Security Management
|
||||
NIST = "nist" # National Institute of Standards and Technology
|
||||
|
||||
|
||||
class EnhancedAuditLog(BaseModel):
|
||||
"""
|
||||
Enhanced audit logging for comprehensive security monitoring
|
||||
"""
|
||||
__tablename__ = "enhanced_audit_logs"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
|
||||
|
||||
# Event identification
|
||||
event_id = Column(String(64), nullable=False, unique=True, index=True)
|
||||
event_type = Column(String(50), nullable=False, index=True)
|
||||
event_category = Column(String(30), nullable=False, index=True) # security, audit, compliance, system
|
||||
severity = Column(String(20), nullable=False, index=True)
|
||||
|
||||
# Event details
|
||||
title = Column(String(255), nullable=False)
|
||||
description = Column(Text, nullable=False)
|
||||
outcome = Column(String(20), nullable=False, index=True) # success, failure, error, blocked
|
||||
|
||||
# User and session context
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=True, index=True)
|
||||
session_id = Column(String(128), nullable=True, index=True)
|
||||
impersonated_user_id = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||||
|
||||
# Network context
|
||||
source_ip = Column(String(45), nullable=True, index=True)
|
||||
user_agent = Column(Text, nullable=True)
|
||||
request_id = Column(String(64), nullable=True, index=True)
|
||||
|
||||
# Geographic context
|
||||
country = Column(String(5), nullable=True)
|
||||
region = Column(String(100), nullable=True)
|
||||
city = Column(String(100), nullable=True)
|
||||
|
||||
# Technical context
|
||||
endpoint = Column(String(255), nullable=True, index=True)
|
||||
http_method = Column(String(10), nullable=True)
|
||||
status_code = Column(Integer, nullable=True)
|
||||
response_time_ms = Column(Integer, nullable=True)
|
||||
|
||||
# Resource context
|
||||
resource_type = Column(String(50), nullable=True, index=True) # file, customer, document, etc.
|
||||
resource_id = Column(String(100), nullable=True, index=True)
|
||||
resource_name = Column(String(255), nullable=True)
|
||||
|
||||
# Data context
|
||||
data_before = Column(Text, nullable=True) # JSON string of previous state
|
||||
data_after = Column(Text, nullable=True) # JSON string of new state
|
||||
data_volume = Column(Integer, nullable=True) # Bytes processed
|
||||
record_count = Column(Integer, nullable=True) # Number of records affected
|
||||
|
||||
# Risk assessment
|
||||
risk_score = Column(Integer, default=0, nullable=False, index=True) # 0-100
|
||||
risk_factors = Column(Text, nullable=True) # JSON array of risk indicators
|
||||
threat_indicators = Column(Text, nullable=True) # JSON array of threat patterns
|
||||
|
||||
# Compliance tracking
|
||||
compliance_standards = Column(Text, nullable=True) # JSON array of applicable standards
|
||||
retention_period_days = Column(Integer, default=2555, nullable=False) # 7 years default
|
||||
|
||||
# Timestamp and tracking
|
||||
timestamp = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False, index=True)
|
||||
processed_at = Column(DateTime(timezone=True), nullable=True)
|
||||
correlation_id = Column(String(64), nullable=True, index=True) # For related events
|
||||
|
||||
# Additional metadata
|
||||
tags = Column(Text, nullable=True) # JSON array of tags for categorization
|
||||
custom_fields = Column(Text, nullable=True) # JSON object for custom data
|
||||
|
||||
# Relationships
|
||||
user = relationship("User", foreign_keys=[user_id])
|
||||
impersonated_user = relationship("User", foreign_keys=[impersonated_user_id])
|
||||
|
||||
def set_data_before(self, data: Dict[str, Any]) -> None:
|
||||
"""Set data before change as JSON"""
|
||||
self.data_before = json.dumps(data, default=str) if data else None
|
||||
|
||||
def set_data_after(self, data: Dict[str, Any]) -> None:
|
||||
"""Set data after change as JSON"""
|
||||
self.data_after = json.dumps(data, default=str) if data else None
|
||||
|
||||
def get_data_before(self) -> Optional[Dict[str, Any]]:
|
||||
"""Get data before change from JSON"""
|
||||
return json.loads(self.data_before) if self.data_before else None
|
||||
|
||||
def get_data_after(self) -> Optional[Dict[str, Any]]:
|
||||
"""Get data after change from JSON"""
|
||||
return json.loads(self.data_after) if self.data_after else None
|
||||
|
||||
def set_risk_factors(self, factors: list) -> None:
|
||||
"""Set risk factors as JSON"""
|
||||
self.risk_factors = json.dumps(factors) if factors else None
|
||||
|
||||
def get_risk_factors(self) -> list:
|
||||
"""Get risk factors from JSON"""
|
||||
return json.loads(self.risk_factors) if self.risk_factors else []
|
||||
|
||||
def set_threat_indicators(self, indicators: list) -> None:
|
||||
"""Set threat indicators as JSON"""
|
||||
self.threat_indicators = json.dumps(indicators) if indicators else None
|
||||
|
||||
def get_threat_indicators(self) -> list:
|
||||
"""Get threat indicators from JSON"""
|
||||
return json.loads(self.threat_indicators) if self.threat_indicators else []
|
||||
|
||||
def set_compliance_standards(self, standards: list) -> None:
|
||||
"""Set compliance standards as JSON"""
|
||||
self.compliance_standards = json.dumps(standards) if standards else None
|
||||
|
||||
def get_compliance_standards(self) -> list:
|
||||
"""Get compliance standards from JSON"""
|
||||
return json.loads(self.compliance_standards) if self.compliance_standards else []
|
||||
|
||||
def set_tags(self, tags: list) -> None:
|
||||
"""Set tags as JSON"""
|
||||
self.tags = json.dumps(tags) if tags else None
|
||||
|
||||
def get_tags(self) -> list:
|
||||
"""Get tags from JSON"""
|
||||
return json.loads(self.tags) if self.tags else []
|
||||
|
||||
def set_custom_fields(self, fields: Dict[str, Any]) -> None:
|
||||
"""Set custom fields as JSON"""
|
||||
self.custom_fields = json.dumps(fields, default=str) if fields else None
|
||||
|
||||
def get_custom_fields(self) -> Optional[Dict[str, Any]]:
|
||||
"""Get custom fields from JSON"""
|
||||
return json.loads(self.custom_fields) if self.custom_fields else None
|
||||
|
||||
# Add indexes for performance
|
||||
__table_args__ = (
|
||||
Index('idx_enhanced_audit_user_timestamp', 'user_id', 'timestamp'),
|
||||
Index('idx_enhanced_audit_event_severity', 'event_type', 'severity'),
|
||||
Index('idx_enhanced_audit_resource', 'resource_type', 'resource_id'),
|
||||
Index('idx_enhanced_audit_ip_timestamp', 'source_ip', 'timestamp'),
|
||||
Index('idx_enhanced_audit_correlation', 'correlation_id'),
|
||||
Index('idx_enhanced_audit_risk_score', 'risk_score'),
|
||||
Index('idx_enhanced_audit_compliance', 'compliance_standards'),
|
||||
)
|
||||
|
||||
|
||||
class SecurityAlert(BaseModel):
|
||||
"""
|
||||
Security alerts for real-time monitoring and incident response
|
||||
"""
|
||||
__tablename__ = "security_alerts"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
|
||||
|
||||
# Alert identification
|
||||
alert_id = Column(String(64), nullable=False, unique=True, index=True)
|
||||
rule_id = Column(String(64), nullable=False, index=True)
|
||||
rule_name = Column(String(255), nullable=False)
|
||||
|
||||
# Alert details
|
||||
title = Column(String(255), nullable=False)
|
||||
description = Column(Text, nullable=False)
|
||||
severity = Column(String(20), nullable=False, index=True)
|
||||
confidence = Column(Integer, default=100, nullable=False) # 0-100 confidence score
|
||||
|
||||
# Context
|
||||
event_count = Column(Integer, default=1, nullable=False) # Number of triggering events
|
||||
time_window_minutes = Column(Integer, nullable=True) # Time window for correlation
|
||||
affected_users = Column(Text, nullable=True) # JSON array of user IDs
|
||||
affected_resources = Column(Text, nullable=True) # JSON array of resource identifiers
|
||||
|
||||
# Response tracking
|
||||
status = Column(String(20), default="open", nullable=False, index=True) # open, investigating, resolved, false_positive
|
||||
assigned_to = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||||
resolved_by = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||||
resolution_notes = Column(Text, nullable=True)
|
||||
|
||||
# Timestamps
|
||||
first_seen = Column(DateTime(timezone=True), nullable=False, index=True)
|
||||
last_seen = Column(DateTime(timezone=True), nullable=False, index=True)
|
||||
acknowledged_at = Column(DateTime(timezone=True), nullable=True)
|
||||
resolved_at = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
# Related audit logs
|
||||
triggering_events = Column(Text, nullable=True) # JSON array of audit log IDs
|
||||
|
||||
# Additional metadata
|
||||
tags = Column(Text, nullable=True) # JSON array of tags
|
||||
custom_fields = Column(Text, nullable=True) # JSON object for custom data
|
||||
|
||||
# Relationships
|
||||
assignee = relationship("User", foreign_keys=[assigned_to])
|
||||
resolver = relationship("User", foreign_keys=[resolved_by])
|
||||
|
||||
|
||||
class ComplianceReport(BaseModel):
|
||||
"""
|
||||
Compliance reporting for various standards
|
||||
"""
|
||||
__tablename__ = "compliance_reports"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
|
||||
|
||||
# Report identification
|
||||
report_id = Column(String(64), nullable=False, unique=True, index=True)
|
||||
standard = Column(String(50), nullable=False, index=True)
|
||||
report_type = Column(String(50), nullable=False, index=True) # periodic, on_demand, incident
|
||||
|
||||
# Report details
|
||||
title = Column(String(255), nullable=False)
|
||||
description = Column(Text, nullable=True)
|
||||
|
||||
# Time range
|
||||
start_date = Column(DateTime(timezone=True), nullable=False, index=True)
|
||||
end_date = Column(DateTime(timezone=True), nullable=False, index=True)
|
||||
|
||||
# Report content
|
||||
summary = Column(Text, nullable=True) # JSON summary of findings
|
||||
details = Column(Text, nullable=True) # JSON detailed findings
|
||||
recommendations = Column(Text, nullable=True) # JSON recommendations
|
||||
|
||||
# Metrics
|
||||
total_events = Column(Integer, default=0, nullable=False)
|
||||
security_events = Column(Integer, default=0, nullable=False)
|
||||
violations = Column(Integer, default=0, nullable=False)
|
||||
high_risk_events = Column(Integer, default=0, nullable=False)
|
||||
|
||||
# Status
|
||||
status = Column(String(20), default="generating", nullable=False, index=True) # generating, ready, delivered, archived
|
||||
generated_by = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
generated_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False)
|
||||
|
||||
# Delivery
|
||||
recipients = Column(Text, nullable=True) # JSON array of recipient emails
|
||||
delivered_at = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
# File storage
|
||||
file_path = Column(String(500), nullable=True) # Path to generated report file
|
||||
file_size = Column(Integer, nullable=True) # File size in bytes
|
||||
|
||||
# Relationships
|
||||
generator = relationship("User", foreign_keys=[generated_by])
|
||||
|
||||
|
||||
class AuditRetentionPolicy(BaseModel):
|
||||
"""
|
||||
Audit log retention policies for compliance
|
||||
"""
|
||||
__tablename__ = "audit_retention_policies"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
|
||||
|
||||
# Policy identification
|
||||
policy_name = Column(String(255), nullable=False, unique=True, index=True)
|
||||
event_types = Column(Text, nullable=True) # JSON array of event types to apply to
|
||||
compliance_standards = Column(Text, nullable=True) # JSON array of applicable standards
|
||||
|
||||
# Retention settings
|
||||
retention_days = Column(Integer, nullable=False) # Days to retain
|
||||
archive_after_days = Column(Integer, nullable=True) # Days before archiving
|
||||
|
||||
# Policy details
|
||||
description = Column(Text, nullable=True)
|
||||
is_active = Column(Boolean, default=True, nullable=False)
|
||||
priority = Column(Integer, default=100, nullable=False) # Higher priority = more specific
|
||||
|
||||
# Timestamps
|
||||
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False)
|
||||
updated_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False)
|
||||
created_by = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
|
||||
# Relationships
|
||||
creator = relationship("User")
|
||||
|
||||
|
||||
class SIEMIntegration(BaseModel):
|
||||
"""
|
||||
SIEM integration configuration and status
|
||||
"""
|
||||
__tablename__ = "siem_integrations"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
|
||||
|
||||
# Integration identification
|
||||
integration_name = Column(String(255), nullable=False, unique=True, index=True)
|
||||
siem_type = Column(String(50), nullable=False, index=True) # splunk, elk, qradar, etc.
|
||||
|
||||
# Configuration
|
||||
endpoint_url = Column(String(500), nullable=True)
|
||||
api_key_hash = Column(String(255), nullable=True) # Hashed API key
|
||||
configuration = Column(Text, nullable=True) # JSON configuration
|
||||
|
||||
# Event filtering
|
||||
event_types = Column(Text, nullable=True) # JSON array of event types to send
|
||||
severity_threshold = Column(String(20), default="medium", nullable=False)
|
||||
|
||||
# Status
|
||||
is_active = Column(Boolean, default=True, nullable=False)
|
||||
is_healthy = Column(Boolean, default=True, nullable=False)
|
||||
last_sync = Column(DateTime(timezone=True), nullable=True)
|
||||
last_error = Column(Text, nullable=True)
|
||||
|
||||
# Statistics
|
||||
events_sent = Column(Integer, default=0, nullable=False)
|
||||
errors_count = Column(Integer, default=0, nullable=False)
|
||||
|
||||
# Timestamps
|
||||
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False)
|
||||
updated_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False)
|
||||
created_by = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
|
||||
# Relationships
|
||||
creator = relationship("User")
|
||||
@@ -8,6 +8,8 @@ from sqlalchemy import Column, Integer, String, DateTime, Boolean, ForeignKey, U
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from app.models.base import BaseModel
|
||||
from sqlalchemy import Text
|
||||
from app.models.audit import LoginAttempt as _AuditLoginAttempt
|
||||
|
||||
|
||||
class RefreshToken(BaseModel):
|
||||
@@ -32,3 +34,8 @@ class RefreshToken(BaseModel):
|
||||
)
|
||||
|
||||
|
||||
"""
|
||||
Expose `LoginAttempt` from `app.models.audit` here for backward compatibility.
|
||||
"""
|
||||
LoginAttempt = _AuditLoginAttempt
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
Base model with common fields
|
||||
"""
|
||||
from sqlalchemy import Column, DateTime, String
|
||||
from sqlalchemy import Column, DateTime, String, event
|
||||
from sqlalchemy.sql import func
|
||||
from app.database.base import Base
|
||||
|
||||
@@ -14,4 +14,41 @@ class TimestampMixin:
|
||||
|
||||
class BaseModel(Base, TimestampMixin):
|
||||
"""Base model class"""
|
||||
__abstract__ = True
|
||||
__abstract__ = True
|
||||
|
||||
|
||||
# Event listeners for adaptive cache integration
|
||||
@event.listens_for(BaseModel, 'after_update', propagate=True)
|
||||
def record_update(mapper, connection, target):
|
||||
"""Record data updates for adaptive cache TTL calculation"""
|
||||
try:
|
||||
from app.services.adaptive_cache import record_data_update
|
||||
table_name = target.__tablename__
|
||||
record_data_update(table_name)
|
||||
except Exception:
|
||||
# Don't fail database operations if cache tracking fails
|
||||
pass
|
||||
|
||||
|
||||
@event.listens_for(BaseModel, 'after_insert', propagate=True)
|
||||
def record_insert(mapper, connection, target):
|
||||
"""Record data inserts for adaptive cache TTL calculation"""
|
||||
try:
|
||||
from app.services.adaptive_cache import record_data_update
|
||||
table_name = target.__tablename__
|
||||
record_data_update(table_name)
|
||||
except Exception:
|
||||
# Don't fail database operations if cache tracking fails
|
||||
pass
|
||||
|
||||
|
||||
@event.listens_for(BaseModel, 'after_delete', propagate=True)
|
||||
def record_delete(mapper, connection, target):
|
||||
"""Record data deletions for adaptive cache TTL calculation"""
|
||||
try:
|
||||
from app.services.adaptive_cache import record_data_update
|
||||
table_name = target.__tablename__
|
||||
record_data_update(table_name)
|
||||
except Exception:
|
||||
# Don't fail database operations if cache tracking fails
|
||||
pass
|
||||
272
app/models/deadlines.py
Normal file
272
app/models/deadlines.py
Normal file
@@ -0,0 +1,272 @@
|
||||
"""
|
||||
Deadline management models for legal practice deadlines and court dates
|
||||
"""
|
||||
from sqlalchemy import Column, Integer, String, DateTime, Date, Text, ForeignKey, Boolean, Enum
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
from enum import Enum as PyEnum
|
||||
from app.models.base import BaseModel
|
||||
|
||||
|
||||
class DeadlineType(PyEnum):
|
||||
"""Types of deadlines in legal practice"""
|
||||
COURT_FILING = "court_filing"
|
||||
COURT_HEARING = "court_hearing"
|
||||
DISCOVERY = "discovery"
|
||||
STATUTE_OF_LIMITATIONS = "statute_of_limitations"
|
||||
CONTRACT = "contract"
|
||||
ADMINISTRATIVE = "administrative"
|
||||
CLIENT_MEETING = "client_meeting"
|
||||
INTERNAL = "internal"
|
||||
OTHER = "other"
|
||||
|
||||
|
||||
class DeadlinePriority(PyEnum):
|
||||
"""Priority levels for deadlines"""
|
||||
CRITICAL = "critical" # Statute of limitations, court filings
|
||||
HIGH = "high" # Court hearings, important discovery
|
||||
MEDIUM = "medium" # Client meetings, administrative
|
||||
LOW = "low" # Internal deadlines, optional items
|
||||
|
||||
|
||||
class DeadlineStatus(PyEnum):
|
||||
"""Status of deadline completion"""
|
||||
PENDING = "pending"
|
||||
COMPLETED = "completed"
|
||||
MISSED = "missed"
|
||||
CANCELLED = "cancelled"
|
||||
EXTENDED = "extended"
|
||||
|
||||
|
||||
class NotificationFrequency(PyEnum):
|
||||
"""How often to send deadline reminders"""
|
||||
NONE = "none"
|
||||
DAILY = "daily"
|
||||
WEEKLY = "weekly"
|
||||
MONTHLY = "monthly"
|
||||
CUSTOM = "custom"
|
||||
|
||||
|
||||
class Deadline(BaseModel):
|
||||
"""
|
||||
Legal deadlines and important dates
|
||||
Tracks court dates, filing deadlines, statute of limitations, etc.
|
||||
"""
|
||||
__tablename__ = "deadlines"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
|
||||
# File association
|
||||
file_no = Column(String(45), ForeignKey("files.file_no"), nullable=True, index=True)
|
||||
client_id = Column(String(80), ForeignKey("rolodex.id"), nullable=True, index=True)
|
||||
|
||||
# Deadline details
|
||||
title = Column(String(200), nullable=False)
|
||||
description = Column(Text)
|
||||
deadline_date = Column(Date, nullable=False, index=True)
|
||||
deadline_time = Column(DateTime(timezone=True), nullable=True) # For specific times
|
||||
|
||||
# Classification
|
||||
deadline_type = Column(Enum(DeadlineType), nullable=False, default=DeadlineType.OTHER)
|
||||
priority = Column(Enum(DeadlinePriority), nullable=False, default=DeadlinePriority.MEDIUM)
|
||||
status = Column(Enum(DeadlineStatus), nullable=False, default=DeadlineStatus.PENDING)
|
||||
|
||||
# Assignment and ownership
|
||||
assigned_to_user_id = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||||
assigned_to_employee_id = Column(String(10), ForeignKey("employees.empl_num"), nullable=True)
|
||||
created_by_user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
|
||||
# Court/external details
|
||||
court_name = Column(String(200)) # Which court if applicable
|
||||
case_number = Column(String(100)) # Court case number
|
||||
judge_name = Column(String(100))
|
||||
opposing_counsel = Column(String(200))
|
||||
|
||||
# Notification settings
|
||||
notification_frequency = Column(Enum(NotificationFrequency), default=NotificationFrequency.WEEKLY)
|
||||
advance_notice_days = Column(Integer, default=7) # Days before deadline to start notifications
|
||||
last_notification_sent = Column(DateTime(timezone=True))
|
||||
|
||||
# Completion tracking
|
||||
completed_date = Column(DateTime(timezone=True))
|
||||
completed_by_user_id = Column(Integer, ForeignKey("users.id"))
|
||||
completion_notes = Column(Text)
|
||||
|
||||
# Extension tracking
|
||||
original_deadline_date = Column(Date) # Track if deadline was extended
|
||||
extension_reason = Column(Text)
|
||||
extension_granted_by = Column(String(100)) # Court, opposing counsel, etc.
|
||||
|
||||
# Metadata
|
||||
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
|
||||
|
||||
# Relationships
|
||||
file = relationship("File", back_populates="deadlines")
|
||||
client = relationship("Rolodex")
|
||||
assigned_to_user = relationship("User", foreign_keys=[assigned_to_user_id])
|
||||
assigned_to_employee = relationship("Employee")
|
||||
created_by = relationship("User", foreign_keys=[created_by_user_id])
|
||||
completed_by = relationship("User", foreign_keys=[completed_by_user_id])
|
||||
reminders = relationship("DeadlineReminder", back_populates="deadline", cascade="all, delete-orphan")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Deadline(id={self.id}, title='{self.title}', date='{self.deadline_date}', status='{self.status.value}')>"
|
||||
|
||||
@property
|
||||
def is_overdue(self) -> bool:
|
||||
"""Check if deadline is overdue"""
|
||||
from datetime import date
|
||||
return self.status == DeadlineStatus.PENDING and self.deadline_date < date.today()
|
||||
|
||||
@property
|
||||
def days_until_deadline(self) -> int:
|
||||
"""Calculate days until deadline (negative if overdue)"""
|
||||
from datetime import date
|
||||
return (self.deadline_date - date.today()).days
|
||||
|
||||
|
||||
class DeadlineReminder(BaseModel):
|
||||
"""
|
||||
Automatic reminders for deadlines
|
||||
Tracks when notifications were sent and their status
|
||||
"""
|
||||
__tablename__ = "deadline_reminders"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
deadline_id = Column(Integer, ForeignKey("deadlines.id"), nullable=False, index=True)
|
||||
|
||||
# Reminder scheduling
|
||||
reminder_date = Column(Date, nullable=False, index=True)
|
||||
reminder_time = Column(DateTime(timezone=True))
|
||||
days_before_deadline = Column(Integer, nullable=False) # How many days before deadline
|
||||
|
||||
# Notification details
|
||||
notification_sent = Column(Boolean, default=False)
|
||||
sent_at = Column(DateTime(timezone=True))
|
||||
notification_method = Column(String(50), default="email") # email, sms, in_app
|
||||
recipient_user_id = Column(Integer, ForeignKey("users.id"))
|
||||
recipient_email = Column(String(255))
|
||||
|
||||
# Message content
|
||||
subject = Column(String(200))
|
||||
message = Column(Text)
|
||||
|
||||
# Status tracking
|
||||
delivery_status = Column(String(50), default="pending") # pending, sent, delivered, failed
|
||||
error_message = Column(Text)
|
||||
|
||||
# Metadata
|
||||
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||
|
||||
# Relationships
|
||||
deadline = relationship("Deadline", back_populates="reminders")
|
||||
recipient = relationship("User")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<DeadlineReminder(id={self.id}, deadline_id={self.deadline_id}, date='{self.reminder_date}', sent={self.notification_sent})>"
|
||||
|
||||
|
||||
class DeadlineTemplate(BaseModel):
|
||||
"""
|
||||
Templates for common deadline types
|
||||
Helps standardize deadline creation for common legal processes
|
||||
"""
|
||||
__tablename__ = "deadline_templates"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
|
||||
# Template details
|
||||
name = Column(String(200), nullable=False, unique=True)
|
||||
description = Column(Text)
|
||||
deadline_type = Column(Enum(DeadlineType), nullable=False)
|
||||
priority = Column(Enum(DeadlinePriority), nullable=False, default=DeadlinePriority.MEDIUM)
|
||||
|
||||
# Default settings
|
||||
default_title_template = Column(String(200)) # Template with placeholders like {file_no}, {client_name}
|
||||
default_description_template = Column(Text)
|
||||
default_advance_notice_days = Column(Integer, default=7)
|
||||
default_notification_frequency = Column(Enum(NotificationFrequency), default=NotificationFrequency.WEEKLY)
|
||||
|
||||
# Timing defaults
|
||||
days_from_file_open = Column(Integer) # Auto-calculate deadline based on file open date
|
||||
days_from_event = Column(Integer) # Days from some triggering event
|
||||
|
||||
# Status and metadata
|
||||
active = Column(Boolean, default=True)
|
||||
created_by_user_id = Column(Integer, ForeignKey("users.id"))
|
||||
created_at = Column(DateTime(timezone=True), default=func.now())
|
||||
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
|
||||
|
||||
# Relationships
|
||||
created_by = relationship("User")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<DeadlineTemplate(id={self.id}, name='{self.name}', type='{self.deadline_type.value}')>"
|
||||
|
||||
|
||||
class DeadlineHistory(BaseModel):
|
||||
"""
|
||||
History of deadline changes and updates
|
||||
Maintains audit trail for deadline modifications
|
||||
"""
|
||||
__tablename__ = "deadline_history"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
deadline_id = Column(Integer, ForeignKey("deadlines.id"), nullable=False, index=True)
|
||||
|
||||
# Change details
|
||||
change_type = Column(String(50), nullable=False) # created, updated, completed, extended, cancelled
|
||||
field_changed = Column(String(100)) # Which field was changed
|
||||
old_value = Column(Text)
|
||||
new_value = Column(Text)
|
||||
|
||||
# Change context
|
||||
change_reason = Column(Text)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
change_date = Column(DateTime(timezone=True), default=func.now())
|
||||
|
||||
# Relationships
|
||||
deadline = relationship("Deadline")
|
||||
user = relationship("User")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<DeadlineHistory(id={self.id}, deadline_id={self.deadline_id}, change='{self.change_type}')>"
|
||||
|
||||
|
||||
class CourtCalendar(BaseModel):
|
||||
"""
|
||||
Court calendar entries and hearing schedules
|
||||
Specialized deadline type for court appearances
|
||||
"""
|
||||
__tablename__ = "court_calendar"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
deadline_id = Column(Integer, ForeignKey("deadlines.id"), nullable=False, unique=True)
|
||||
|
||||
# Court details
|
||||
court_name = Column(String(200), nullable=False)
|
||||
courtroom = Column(String(50))
|
||||
judge_name = Column(String(100))
|
||||
case_number = Column(String(100))
|
||||
|
||||
# Hearing details
|
||||
hearing_type = Column(String(100)) # Motion hearing, trial, conference, etc.
|
||||
estimated_duration = Column(Integer) # Minutes
|
||||
appearance_required = Column(Boolean, default=True)
|
||||
|
||||
# Preparation tracking
|
||||
preparation_deadline = Column(Date) # When prep should be completed
|
||||
documents_filed = Column(Boolean, default=False)
|
||||
client_notified = Column(Boolean, default=False)
|
||||
|
||||
# Outcome tracking
|
||||
hearing_completed = Column(Boolean, default=False)
|
||||
outcome = Column(Text)
|
||||
next_hearing_date = Column(Date)
|
||||
|
||||
# Relationships
|
||||
deadline = relationship("Deadline")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<CourtCalendar(id={self.id}, court='{self.court_name}', hearing='{self.hearing_type}')>"
|
||||
303
app/models/document_workflows.py
Normal file
303
app/models/document_workflows.py
Normal file
@@ -0,0 +1,303 @@
|
||||
"""
|
||||
Document Workflow Automation Models
|
||||
|
||||
This module provides automated document generation workflows triggered by case events,
|
||||
deadlines, file status changes, and other system events.
|
||||
"""
|
||||
from sqlalchemy import Column, Integer, String, Text, ForeignKey, Boolean, JSON, DateTime, Date, Enum
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
from enum import Enum as PyEnum
|
||||
from typing import Dict, Any, List, Optional
|
||||
import json
|
||||
|
||||
from app.models.base import BaseModel
|
||||
|
||||
|
||||
class WorkflowTriggerType(PyEnum):
|
||||
"""Types of events that can trigger workflows"""
|
||||
FILE_STATUS_CHANGE = "file_status_change"
|
||||
DEADLINE_APPROACHING = "deadline_approaching"
|
||||
DEADLINE_OVERDUE = "deadline_overdue"
|
||||
DEADLINE_COMPLETED = "deadline_completed"
|
||||
PAYMENT_RECEIVED = "payment_received"
|
||||
PAYMENT_OVERDUE = "payment_overdue"
|
||||
FILE_OPENED = "file_opened"
|
||||
FILE_CLOSED = "file_closed"
|
||||
DOCUMENT_UPLOADED = "document_uploaded"
|
||||
QDRO_STATUS_CHANGE = "qdro_status_change"
|
||||
TIME_BASED = "time_based"
|
||||
MANUAL_TRIGGER = "manual_trigger"
|
||||
CUSTOM_EVENT = "custom_event"
|
||||
|
||||
|
||||
class WorkflowStatus(PyEnum):
|
||||
"""Workflow execution status"""
|
||||
ACTIVE = "active"
|
||||
INACTIVE = "inactive"
|
||||
PAUSED = "paused"
|
||||
ARCHIVED = "archived"
|
||||
|
||||
|
||||
class WorkflowActionType(PyEnum):
|
||||
"""Types of actions a workflow can perform"""
|
||||
GENERATE_DOCUMENT = "generate_document"
|
||||
SEND_EMAIL = "send_email"
|
||||
CREATE_DEADLINE = "create_deadline"
|
||||
UPDATE_FILE_STATUS = "update_file_status"
|
||||
CREATE_LEDGER_ENTRY = "create_ledger_entry"
|
||||
SEND_NOTIFICATION = "send_notification"
|
||||
EXECUTE_CUSTOM = "execute_custom"
|
||||
|
||||
|
||||
class ExecutionStatus(PyEnum):
|
||||
"""Status of workflow execution"""
|
||||
PENDING = "pending"
|
||||
RUNNING = "running"
|
||||
COMPLETED = "completed"
|
||||
FAILED = "failed"
|
||||
CANCELLED = "cancelled"
|
||||
RETRYING = "retrying"
|
||||
|
||||
|
||||
class DocumentWorkflow(BaseModel):
|
||||
"""
|
||||
Defines automated workflows for document generation and case management
|
||||
"""
|
||||
__tablename__ = "document_workflows"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
|
||||
|
||||
# Basic workflow information
|
||||
name = Column(String(200), nullable=False, index=True)
|
||||
description = Column(Text, nullable=True)
|
||||
status = Column(Enum(WorkflowStatus), default=WorkflowStatus.ACTIVE, nullable=False)
|
||||
|
||||
# Trigger configuration
|
||||
trigger_type = Column(Enum(WorkflowTriggerType), nullable=False, index=True)
|
||||
trigger_conditions = Column(JSON, nullable=True) # JSON conditions for when to trigger
|
||||
|
||||
# Execution settings
|
||||
delay_minutes = Column(Integer, default=0) # Delay before execution
|
||||
max_retries = Column(Integer, default=3)
|
||||
retry_delay_minutes = Column(Integer, default=30)
|
||||
timeout_minutes = Column(Integer, default=60)
|
||||
|
||||
# Filtering conditions
|
||||
file_type_filter = Column(JSON, nullable=True) # Array of file types to include
|
||||
status_filter = Column(JSON, nullable=True) # Array of file statuses to include
|
||||
attorney_filter = Column(JSON, nullable=True) # Array of attorney IDs to include
|
||||
client_filter = Column(JSON, nullable=True) # Array of client IDs to include
|
||||
|
||||
# Schedule settings (for time-based triggers)
|
||||
schedule_cron = Column(String(100), nullable=True) # Cron expression for scheduling
|
||||
schedule_timezone = Column(String(50), default="UTC")
|
||||
next_run_time = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
# Priority and organization
|
||||
priority = Column(Integer, default=5) # 1-10, higher = more important
|
||||
category = Column(String(100), nullable=True, index=True)
|
||||
tags = Column(JSON, nullable=True) # Array of tags for organization
|
||||
|
||||
# Metadata
|
||||
created_by = Column(String(150), ForeignKey("users.username"), nullable=True)
|
||||
created_at = Column(DateTime(timezone=True), default=func.now(), nullable=False)
|
||||
updated_at = Column(DateTime(timezone=True), default=func.now(), onupdate=func.now())
|
||||
last_triggered_at = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
# Statistics
|
||||
execution_count = Column(Integer, default=0)
|
||||
success_count = Column(Integer, default=0)
|
||||
failure_count = Column(Integer, default=0)
|
||||
|
||||
# Relationships
|
||||
actions = relationship("WorkflowAction", back_populates="workflow", cascade="all, delete-orphan")
|
||||
executions = relationship("WorkflowExecution", back_populates="workflow", cascade="all, delete-orphan")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<DocumentWorkflow(id={self.id}, name='{self.name}', trigger='{self.trigger_type.value}')>"
|
||||
|
||||
|
||||
class WorkflowAction(BaseModel):
|
||||
"""
|
||||
Individual actions within a workflow
|
||||
"""
|
||||
__tablename__ = "workflow_actions"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
|
||||
workflow_id = Column(Integer, ForeignKey("document_workflows.id"), nullable=False, index=True)
|
||||
|
||||
# Action configuration
|
||||
action_type = Column(Enum(WorkflowActionType), nullable=False)
|
||||
action_order = Column(Integer, default=1) # Order of execution within workflow
|
||||
action_name = Column(String(200), nullable=True) # Optional descriptive name
|
||||
|
||||
# Action parameters (specific to action type)
|
||||
parameters = Column(JSON, nullable=True)
|
||||
|
||||
# Document generation specific fields
|
||||
template_id = Column(Integer, ForeignKey("document_templates.id"), nullable=True)
|
||||
output_format = Column(String(50), default="DOCX") # DOCX, PDF, HTML
|
||||
custom_filename_template = Column(String(500), nullable=True) # Template for filename
|
||||
|
||||
# Email action specific fields
|
||||
email_template_id = Column(Integer, nullable=True) # Reference to email template
|
||||
email_recipients = Column(JSON, nullable=True) # Array of recipient types/addresses
|
||||
email_subject_template = Column(String(500), nullable=True)
|
||||
|
||||
# Conditional execution
|
||||
condition = Column(JSON, nullable=True) # Conditions for this action to execute
|
||||
continue_on_failure = Column(Boolean, default=False) # Whether to continue if this action fails
|
||||
|
||||
# Relationships
|
||||
workflow = relationship("DocumentWorkflow", back_populates="actions")
|
||||
template = relationship("DocumentTemplate")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<WorkflowAction(id={self.id}, workflow_id={self.workflow_id}, type='{self.action_type.value}')>"
|
||||
|
||||
|
||||
class WorkflowExecution(BaseModel):
|
||||
"""
|
||||
Tracks individual workflow executions
|
||||
"""
|
||||
__tablename__ = "workflow_executions"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
|
||||
workflow_id = Column(Integer, ForeignKey("document_workflows.id"), nullable=False, index=True)
|
||||
|
||||
# Execution context
|
||||
triggered_by_event_id = Column(String(100), nullable=True, index=True) # Reference to triggering event
|
||||
triggered_by_event_type = Column(String(50), nullable=True)
|
||||
context_file_no = Column(String(45), nullable=True, index=True)
|
||||
context_client_id = Column(String(80), nullable=True)
|
||||
context_user_id = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||||
|
||||
# Execution details
|
||||
status = Column(Enum(ExecutionStatus), default=ExecutionStatus.PENDING, nullable=False)
|
||||
started_at = Column(DateTime(timezone=True), nullable=True)
|
||||
completed_at = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
# Input data and context
|
||||
trigger_data = Column(JSON, nullable=True) # Data from the triggering event
|
||||
execution_context = Column(JSON, nullable=True) # Variables and context for execution
|
||||
|
||||
# Results and outputs
|
||||
generated_documents = Column(JSON, nullable=True) # Array of generated document info
|
||||
action_results = Column(JSON, nullable=True) # Results from each action
|
||||
error_message = Column(Text, nullable=True)
|
||||
error_details = Column(JSON, nullable=True)
|
||||
|
||||
# Performance metrics
|
||||
execution_duration_seconds = Column(Integer, nullable=True)
|
||||
retry_count = Column(Integer, default=0)
|
||||
next_retry_at = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
# Relationships
|
||||
workflow = relationship("DocumentWorkflow", back_populates="executions")
|
||||
user = relationship("User")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<WorkflowExecution(id={self.id}, workflow_id={self.workflow_id}, status='{self.status.value}')>"
|
||||
|
||||
|
||||
class WorkflowTemplate(BaseModel):
|
||||
"""
|
||||
Pre-defined workflow templates for common scenarios
|
||||
"""
|
||||
__tablename__ = "workflow_templates"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
|
||||
|
||||
# Template information
|
||||
name = Column(String(200), nullable=False, unique=True)
|
||||
description = Column(Text, nullable=True)
|
||||
category = Column(String(100), nullable=True, index=True)
|
||||
|
||||
# Workflow definition
|
||||
workflow_definition = Column(JSON, nullable=False) # Complete workflow configuration
|
||||
|
||||
# Metadata
|
||||
created_by = Column(String(150), ForeignKey("users.username"), nullable=True)
|
||||
created_at = Column(DateTime(timezone=True), default=func.now(), nullable=False)
|
||||
is_system_template = Column(Boolean, default=False) # Built-in vs user-created
|
||||
usage_count = Column(Integer, default=0) # How many times this template has been used
|
||||
|
||||
# Version control
|
||||
version = Column(String(20), default="1.0.0")
|
||||
template_tags = Column(JSON, nullable=True) # Tags for categorization
|
||||
|
||||
def __repr__(self):
|
||||
return f"<WorkflowTemplate(id={self.id}, name='{self.name}', version='{self.version}')>"
|
||||
|
||||
|
||||
class EventLog(BaseModel):
|
||||
"""
|
||||
Unified event log for workflow triggering
|
||||
"""
|
||||
__tablename__ = "event_log"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
|
||||
|
||||
# Event identification
|
||||
event_id = Column(String(100), nullable=False, unique=True, index=True) # UUID
|
||||
event_type = Column(String(50), nullable=False, index=True)
|
||||
event_source = Column(String(100), nullable=False) # Which system/module generated the event
|
||||
|
||||
# Event context
|
||||
file_no = Column(String(45), nullable=True, index=True)
|
||||
client_id = Column(String(80), nullable=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||||
resource_type = Column(String(50), nullable=True) # deadline, file, payment, etc.
|
||||
resource_id = Column(String(100), nullable=True)
|
||||
|
||||
# Event data
|
||||
event_data = Column(JSON, nullable=True) # Event-specific data
|
||||
previous_state = Column(JSON, nullable=True) # Previous state before event
|
||||
new_state = Column(JSON, nullable=True) # New state after event
|
||||
|
||||
# Workflow processing
|
||||
processed = Column(Boolean, default=False, index=True)
|
||||
processed_at = Column(DateTime(timezone=True), nullable=True)
|
||||
triggered_workflows = Column(JSON, nullable=True) # Array of workflow IDs triggered
|
||||
processing_errors = Column(JSON, nullable=True)
|
||||
|
||||
# Timing
|
||||
occurred_at = Column(DateTime(timezone=True), default=func.now(), nullable=False, index=True)
|
||||
|
||||
# Relationships
|
||||
user = relationship("User")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<EventLog(id={self.id}, event_type='{self.event_type}', file_no='{self.file_no}')>"
|
||||
|
||||
|
||||
class WorkflowSchedule(BaseModel):
|
||||
"""
|
||||
Scheduled workflow executions (for time-based triggers)
|
||||
"""
|
||||
__tablename__ = "workflow_schedules"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
|
||||
workflow_id = Column(Integer, ForeignKey("document_workflows.id"), nullable=False, index=True)
|
||||
|
||||
# Schedule configuration
|
||||
schedule_name = Column(String(200), nullable=True)
|
||||
cron_expression = Column(String(100), nullable=False)
|
||||
timezone = Column(String(50), default="UTC")
|
||||
|
||||
# Execution tracking
|
||||
next_run_time = Column(DateTime(timezone=True), nullable=False, index=True)
|
||||
last_run_time = Column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
# Status
|
||||
active = Column(Boolean, default=True, nullable=False)
|
||||
|
||||
# Metadata
|
||||
created_at = Column(DateTime(timezone=True), default=func.now(), nullable=False)
|
||||
|
||||
# Relationships
|
||||
workflow = relationship("DocumentWorkflow")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<WorkflowSchedule(id={self.id}, workflow_id={self.workflow_id}, next_run='{self.next_run_time}')>"
|
||||
@@ -188,4 +188,33 @@ class FileAlert(BaseModel):
|
||||
|
||||
def __repr__(self):
|
||||
status = "🔔" if self.is_active and not self.is_acknowledged else "✓"
|
||||
return f"<FileAlert({status} {self.alert_type} - {self.file_no} on {self.alert_date})>"
|
||||
return f"<FileAlert({status} {self.alert_type} - {self.file_no} on {self.alert_date})>"
|
||||
|
||||
|
||||
class FileRelationship(BaseModel):
|
||||
"""
|
||||
Track relationships between files (e.g., related, parent/child, duplicate).
|
||||
Enables cross-referencing and conflict checks.
|
||||
"""
|
||||
__tablename__ = "file_relationships"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
source_file_no = Column(String(45), ForeignKey("files.file_no"), nullable=False, index=True)
|
||||
target_file_no = Column(String(45), ForeignKey("files.file_no"), nullable=False, index=True)
|
||||
|
||||
# Relationship metadata
|
||||
relationship_type = Column(String(45), nullable=False) # related, parent, child, duplicate, conflict, referral
|
||||
notes = Column(Text)
|
||||
|
||||
# Who created it (cached for reporting)
|
||||
created_by_user_id = Column(Integer, ForeignKey("users.id"))
|
||||
created_by_name = Column(String(100))
|
||||
|
||||
# Relationships
|
||||
source_file = relationship("File", foreign_keys=[source_file_no])
|
||||
target_file = relationship("File", foreign_keys=[target_file_no])
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
f"<FileRelationship({self.source_file_no} -[{self.relationship_type}]-> {self.target_file_no})>"
|
||||
)
|
||||
@@ -68,4 +68,5 @@ class File(BaseModel):
|
||||
documents = relationship("Document", back_populates="file", cascade="all, delete-orphan")
|
||||
billing_statements = relationship("BillingStatement", back_populates="file", cascade="all, delete-orphan")
|
||||
timers = relationship("Timer", back_populates="file", cascade="all, delete-orphan")
|
||||
time_entries = relationship("TimeEntry", back_populates="file", cascade="all, delete-orphan")
|
||||
time_entries = relationship("TimeEntry", back_populates="file", cascade="all, delete-orphan")
|
||||
deadlines = relationship("Deadline", back_populates="file", cascade="all, delete-orphan")
|
||||
55
app/models/jobs.py
Normal file
55
app/models/jobs.py
Normal file
@@ -0,0 +1,55 @@
|
||||
"""
|
||||
Simple job record schema for tracking synchronous batch operations.
|
||||
"""
|
||||
from sqlalchemy import Column, Integer, String, DateTime, Text, JSON, Index
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
from app.models.base import BaseModel
|
||||
|
||||
|
||||
class JobRecord(BaseModel):
|
||||
"""
|
||||
Minimal job tracking record (no worker/queue yet).
|
||||
|
||||
Used to record outcomes and downloadable bundle info for synchronous jobs
|
||||
such as batch document generation.
|
||||
"""
|
||||
__tablename__ = "jobs"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
|
||||
job_id = Column(String(100), unique=True, nullable=False, index=True)
|
||||
job_type = Column(String(64), nullable=False, index=True) # e.g., documents_batch
|
||||
status = Column(String(32), nullable=False, index=True) # running|completed|failed
|
||||
|
||||
# Request/identity
|
||||
requested_by_username = Column(String(150), nullable=True, index=True)
|
||||
|
||||
# Timing
|
||||
started_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False, index=True)
|
||||
completed_at = Column(DateTime(timezone=True), nullable=True, index=True)
|
||||
|
||||
# Metrics
|
||||
total_requested = Column(Integer, nullable=False, default=0)
|
||||
total_success = Column(Integer, nullable=False, default=0)
|
||||
total_failed = Column(Integer, nullable=False, default=0)
|
||||
|
||||
# Result bundle (if any)
|
||||
result_storage_path = Column(String(512), nullable=True)
|
||||
result_mime_type = Column(String(100), nullable=True)
|
||||
result_size = Column(Integer, nullable=True)
|
||||
|
||||
# Arbitrary details/metadata for easy querying
|
||||
details = Column(JSON, nullable=True)
|
||||
|
||||
__table_args__ = (
|
||||
Index("ix_jobs_type_status", "job_type", "status"),
|
||||
{},
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
f"<JobRecord(job_id={self.job_id}, type='{self.job_type}', status='{self.status}', "
|
||||
f"success={self.total_success}/{self.total_requested})>"
|
||||
)
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@ class TransactionType(BaseModel):
|
||||
t_type = Column(String(1), primary_key=True, index=True) # Transaction type code
|
||||
description = Column(String(100), nullable=False) # Description
|
||||
debit_credit = Column(String(1)) # D=Debit, C=Credit
|
||||
footer_code = Column(String(45), ForeignKey("footers.footer_code")) # Default footer for statements (legacy TRNSTYPE Footer)
|
||||
active = Column(Boolean, default=True) # Is type active
|
||||
|
||||
def __repr__(self):
|
||||
@@ -118,6 +119,7 @@ class GroupLookup(BaseModel):
|
||||
|
||||
group_code = Column(String(45), primary_key=True, index=True) # Group code
|
||||
description = Column(String(200), nullable=False) # Description
|
||||
title = Column(String(200)) # Legacy GRUPLKUP Title
|
||||
active = Column(Boolean, default=True) # Is group active
|
||||
|
||||
def __repr__(self):
|
||||
|
||||
189
app/models/sessions.py
Normal file
189
app/models/sessions.py
Normal file
@@ -0,0 +1,189 @@
|
||||
"""
|
||||
Session management models for advanced security
|
||||
"""
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from typing import Optional
|
||||
from enum import Enum
|
||||
|
||||
from sqlalchemy import Column, Integer, String, DateTime, Boolean, ForeignKey, Text, func
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
import uuid
|
||||
|
||||
from app.models.base import BaseModel
|
||||
|
||||
|
||||
class SessionStatus(str, Enum):
|
||||
"""Session status enumeration"""
|
||||
ACTIVE = "active"
|
||||
EXPIRED = "expired"
|
||||
REVOKED = "revoked"
|
||||
LOCKED = "locked"
|
||||
|
||||
|
||||
class UserSession(BaseModel):
|
||||
"""
|
||||
Enhanced user session tracking for security monitoring
|
||||
"""
|
||||
__tablename__ = "user_sessions"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
|
||||
session_id = Column(String(128), nullable=False, unique=True, index=True) # Secure session identifier
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
|
||||
|
||||
# Session metadata
|
||||
ip_address = Column(String(45), nullable=True, index=True)
|
||||
user_agent = Column(Text, nullable=True)
|
||||
device_fingerprint = Column(String(255), nullable=True) # For device tracking
|
||||
|
||||
# Geographic and security info
|
||||
country = Column(String(5), nullable=True) # ISO country code
|
||||
city = Column(String(100), nullable=True)
|
||||
is_suspicious = Column(Boolean, default=False, nullable=False, index=True)
|
||||
risk_score = Column(Integer, default=0, nullable=False) # 0-100 risk assessment
|
||||
|
||||
# Session lifecycle
|
||||
status = Column(String(20), default=SessionStatus.ACTIVE, nullable=False, index=True)
|
||||
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False)
|
||||
last_activity = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False, index=True)
|
||||
expires_at = Column(DateTime(timezone=True), nullable=False, index=True)
|
||||
|
||||
# Security tracking
|
||||
login_method = Column(String(50), nullable=True) # password, 2fa, sso, etc.
|
||||
locked_at = Column(DateTime(timezone=True), nullable=True)
|
||||
revoked_at = Column(DateTime(timezone=True), nullable=True)
|
||||
revocation_reason = Column(String(100), nullable=True)
|
||||
|
||||
# Relationships
|
||||
user = relationship("User", back_populates="sessions")
|
||||
activities = relationship("SessionActivity", back_populates="session", cascade="all, delete-orphan")
|
||||
|
||||
def is_expired(self) -> bool:
|
||||
"""Check if session is expired"""
|
||||
return datetime.now(timezone.utc) >= self.expires_at
|
||||
|
||||
def is_active(self) -> bool:
|
||||
"""Check if session is currently active"""
|
||||
return self.status == SessionStatus.ACTIVE and not self.is_expired()
|
||||
|
||||
def extend_session(self, duration: timedelta = timedelta(hours=8)) -> None:
|
||||
"""Extend session expiration time"""
|
||||
self.expires_at = datetime.now(timezone.utc) + duration
|
||||
self.last_activity = datetime.now(timezone.utc)
|
||||
|
||||
def revoke_session(self, reason: str = "user_logout") -> None:
|
||||
"""Revoke the session"""
|
||||
self.status = SessionStatus.REVOKED
|
||||
self.revoked_at = datetime.now(timezone.utc)
|
||||
self.revocation_reason = reason
|
||||
|
||||
def lock_session(self, reason: str = "suspicious_activity") -> None:
|
||||
"""Lock the session for security reasons"""
|
||||
self.status = SessionStatus.LOCKED
|
||||
self.locked_at = datetime.now(timezone.utc)
|
||||
self.revocation_reason = reason
|
||||
|
||||
|
||||
class SessionActivity(BaseModel):
|
||||
"""
|
||||
Track user activities within sessions for security analysis
|
||||
"""
|
||||
__tablename__ = "session_activities"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
|
||||
session_id = Column(Integer, ForeignKey("user_sessions.id"), nullable=False, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
|
||||
|
||||
# Activity details
|
||||
activity_type = Column(String(50), nullable=False, index=True) # login, logout, api_call, admin_action, etc.
|
||||
endpoint = Column(String(255), nullable=True) # API endpoint accessed
|
||||
method = Column(String(10), nullable=True) # HTTP method
|
||||
status_code = Column(Integer, nullable=True) # Response status
|
||||
|
||||
# Request details
|
||||
ip_address = Column(String(45), nullable=True, index=True)
|
||||
user_agent = Column(Text, nullable=True)
|
||||
referer = Column(String(255), nullable=True)
|
||||
|
||||
# Security analysis
|
||||
is_suspicious = Column(Boolean, default=False, nullable=False, index=True)
|
||||
risk_factors = Column(Text, nullable=True) # JSON string of detected risks
|
||||
|
||||
# Timing
|
||||
timestamp = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False, index=True)
|
||||
duration_ms = Column(Integer, nullable=True) # Request processing time
|
||||
|
||||
# Additional metadata
|
||||
resource_accessed = Column(String(255), nullable=True) # File, customer, etc.
|
||||
data_volume = Column(Integer, nullable=True) # Bytes transferred
|
||||
|
||||
# Relationships
|
||||
session = relationship("UserSession", back_populates="activities")
|
||||
user = relationship("User")
|
||||
|
||||
|
||||
class SessionConfiguration(BaseModel):
|
||||
"""
|
||||
Configurable session policies and limits
|
||||
"""
|
||||
__tablename__ = "session_configurations"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=True, index=True) # Null for global config
|
||||
|
||||
# Session limits
|
||||
max_concurrent_sessions = Column(Integer, default=3, nullable=False)
|
||||
session_timeout_minutes = Column(Integer, default=480, nullable=False) # 8 hours default
|
||||
idle_timeout_minutes = Column(Integer, default=60, nullable=False) # 1 hour idle
|
||||
|
||||
# Security policies
|
||||
require_session_renewal = Column(Boolean, default=True, nullable=False)
|
||||
renewal_interval_hours = Column(Integer, default=24, nullable=False)
|
||||
force_logout_on_ip_change = Column(Boolean, default=False, nullable=False)
|
||||
suspicious_activity_threshold = Column(Integer, default=5, nullable=False)
|
||||
|
||||
# Geographic restrictions
|
||||
allowed_countries = Column(Text, nullable=True) # JSON array of ISO codes
|
||||
blocked_countries = Column(Text, nullable=True) # JSON array of ISO codes
|
||||
|
||||
# Timestamps
|
||||
created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False)
|
||||
updated_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=func.now(), nullable=False)
|
||||
|
||||
# Relationships
|
||||
user = relationship("User") # If user-specific config
|
||||
|
||||
|
||||
class SessionSecurityEvent(BaseModel):
|
||||
"""
|
||||
Track security events related to sessions
|
||||
"""
|
||||
__tablename__ = "session_security_events"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
|
||||
session_id = Column(Integer, ForeignKey("user_sessions.id"), nullable=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
|
||||
|
||||
# Event details
|
||||
event_type = Column(String(50), nullable=False, index=True) # session_fixation, concurrent_limit, suspicious_login, etc.
|
||||
severity = Column(String(20), nullable=False, index=True) # low, medium, high, critical
|
||||
description = Column(Text, nullable=False)
|
||||
|
||||
# Context
|
||||
ip_address = Column(String(45), nullable=True, index=True)
|
||||
user_agent = Column(Text, nullable=True)
|
||||
country = Column(String(5), nullable=True)
|
||||
|
||||
# Response actions
|
||||
action_taken = Column(String(100), nullable=True) # session_locked, user_notified, admin_alerted, etc.
|
||||
resolved = Column(Boolean, default=False, nullable=False, index=True)
|
||||
resolved_at = Column(DateTime(timezone=True), nullable=True)
|
||||
resolved_by = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||||
|
||||
# Timestamps
|
||||
timestamp = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False, index=True)
|
||||
|
||||
# Relationships
|
||||
session = relationship("UserSession")
|
||||
user = relationship("User", foreign_keys=[user_id])
|
||||
resolver = relationship("User", foreign_keys=[resolved_by])
|
||||
186
app/models/template_variables.py
Normal file
186
app/models/template_variables.py
Normal file
@@ -0,0 +1,186 @@
|
||||
"""
|
||||
Enhanced Template Variable Models with Advanced Features
|
||||
|
||||
This module provides sophisticated variable management for document templates including:
|
||||
- Conditional logic and calculations
|
||||
- Dynamic data source integration
|
||||
- Variable dependencies and validation
|
||||
- Type-safe variable definitions
|
||||
"""
|
||||
from sqlalchemy import Column, Integer, String, Text, ForeignKey, Boolean, JSON, Enum, Float, DateTime, func
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import expression
|
||||
from enum import Enum as PyEnum
|
||||
from typing import Dict, Any, List, Optional
|
||||
import json
|
||||
|
||||
from app.models.base import BaseModel
|
||||
|
||||
|
||||
class VariableType(PyEnum):
|
||||
"""Variable types supported in templates"""
|
||||
STRING = "string"
|
||||
NUMBER = "number"
|
||||
DATE = "date"
|
||||
BOOLEAN = "boolean"
|
||||
CALCULATED = "calculated"
|
||||
CONDITIONAL = "conditional"
|
||||
QUERY = "query"
|
||||
LOOKUP = "lookup"
|
||||
|
||||
|
||||
class TemplateVariable(BaseModel):
|
||||
"""
|
||||
Enhanced template variables with support for complex logic, calculations, and data sources
|
||||
"""
|
||||
__tablename__ = "template_variables"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
|
||||
|
||||
# Basic identification
|
||||
name = Column(String(100), nullable=False, index=True)
|
||||
display_name = Column(String(200), nullable=True)
|
||||
description = Column(Text, nullable=True)
|
||||
|
||||
# Variable type and behavior
|
||||
variable_type = Column(Enum(VariableType), nullable=False, default=VariableType.STRING)
|
||||
required = Column(Boolean, default=False)
|
||||
active = Column(Boolean, default=True, nullable=False)
|
||||
|
||||
# Default and static values
|
||||
default_value = Column(Text, nullable=True)
|
||||
static_value = Column(Text, nullable=True) # When set, always returns this value
|
||||
|
||||
# Advanced features
|
||||
formula = Column(Text, nullable=True) # Mathematical or logical expressions
|
||||
conditional_logic = Column(JSON, nullable=True) # If/then/else rules
|
||||
data_source_query = Column(Text, nullable=True) # SQL query for dynamic data
|
||||
lookup_table = Column(String(100), nullable=True) # Reference table name
|
||||
lookup_key_field = Column(String(100), nullable=True) # Field to match on
|
||||
lookup_value_field = Column(String(100), nullable=True) # Field to return
|
||||
|
||||
# Validation rules
|
||||
validation_rules = Column(JSON, nullable=True) # JSON schema or validation rules
|
||||
format_pattern = Column(String(200), nullable=True) # Regex pattern for formatting
|
||||
|
||||
# Dependencies and relationships
|
||||
depends_on = Column(JSON, nullable=True) # List of variable names this depends on
|
||||
scope = Column(String(50), default="global") # global, template, file, client
|
||||
|
||||
# Metadata
|
||||
created_by = Column(String(150), ForeignKey("users.username"), nullable=True)
|
||||
category = Column(String(100), nullable=True, index=True)
|
||||
tags = Column(JSON, nullable=True) # Array of tags for organization
|
||||
|
||||
# Cache settings for performance
|
||||
cache_duration_minutes = Column(Integer, default=0) # 0 = no cache
|
||||
last_cached_at = Column(DateTime, nullable=True)
|
||||
cached_value = Column(Text, nullable=True)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<TemplateVariable(name='{self.name}', type='{self.variable_type}')>"
|
||||
|
||||
|
||||
class VariableTemplate(BaseModel):
|
||||
"""
|
||||
Association between variables and document templates
|
||||
"""
|
||||
__tablename__ = "variable_templates"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
template_id = Column(Integer, ForeignKey("document_templates.id"), nullable=False, index=True)
|
||||
variable_id = Column(Integer, ForeignKey("template_variables.id"), nullable=False, index=True)
|
||||
|
||||
# Template-specific overrides
|
||||
override_default = Column(Text, nullable=True)
|
||||
override_required = Column(Boolean, nullable=True)
|
||||
display_order = Column(Integer, default=0)
|
||||
group_name = Column(String(100), nullable=True) # For organizing variables in UI
|
||||
|
||||
# Relationships
|
||||
template = relationship("DocumentTemplate")
|
||||
variable = relationship("TemplateVariable")
|
||||
|
||||
|
||||
class VariableContext(BaseModel):
|
||||
"""
|
||||
Context-specific variable values (per file, client, case, etc.)
|
||||
"""
|
||||
__tablename__ = "variable_contexts"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
|
||||
variable_id = Column(Integer, ForeignKey("template_variables.id"), nullable=False, index=True)
|
||||
|
||||
# Context identification
|
||||
context_type = Column(String(50), nullable=False, index=True) # file, client, global, session
|
||||
context_id = Column(String(100), nullable=False, index=True) # The actual ID (file_no, client_id, etc.)
|
||||
|
||||
# Value storage
|
||||
value = Column(Text, nullable=True)
|
||||
computed_value = Column(Text, nullable=True) # Result after formula/logic processing
|
||||
last_computed_at = Column(DateTime, nullable=True)
|
||||
|
||||
# Validation and metadata
|
||||
is_valid = Column(Boolean, default=True)
|
||||
validation_errors = Column(JSON, nullable=True)
|
||||
source = Column(String(100), nullable=True) # manual, computed, imported, etc.
|
||||
|
||||
# Relationships
|
||||
variable = relationship("TemplateVariable")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<VariableContext(variable_id={self.variable_id}, context='{self.context_type}:{self.context_id}')>"
|
||||
|
||||
|
||||
class VariableAuditLog(BaseModel):
|
||||
"""
|
||||
Audit trail for variable value changes
|
||||
"""
|
||||
__tablename__ = "variable_audit_log"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
|
||||
variable_id = Column(Integer, ForeignKey("template_variables.id"), nullable=False, index=True)
|
||||
context_type = Column(String(50), nullable=True, index=True)
|
||||
context_id = Column(String(100), nullable=True, index=True)
|
||||
|
||||
# Change tracking
|
||||
old_value = Column(Text, nullable=True)
|
||||
new_value = Column(Text, nullable=True)
|
||||
change_type = Column(String(50), nullable=False) # created, updated, deleted, computed
|
||||
change_reason = Column(String(200), nullable=True)
|
||||
|
||||
# Metadata
|
||||
changed_by = Column(String(150), ForeignKey("users.username"), nullable=True)
|
||||
changed_at = Column(DateTime, default=func.now(), nullable=False)
|
||||
source_system = Column(String(100), nullable=True) # web, api, import, etc.
|
||||
|
||||
# Relationships
|
||||
variable = relationship("TemplateVariable")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<VariableAuditLog(variable_id={self.variable_id}, change='{self.change_type}')>"
|
||||
|
||||
|
||||
class VariableGroup(BaseModel):
|
||||
"""
|
||||
Logical groupings of variables for better organization
|
||||
"""
|
||||
__tablename__ = "variable_groups"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True, index=True)
|
||||
name = Column(String(100), nullable=False, unique=True, index=True)
|
||||
description = Column(Text, nullable=True)
|
||||
parent_group_id = Column(Integer, ForeignKey("variable_groups.id"), nullable=True)
|
||||
display_order = Column(Integer, default=0)
|
||||
|
||||
# UI configuration
|
||||
icon = Column(String(50), nullable=True)
|
||||
color = Column(String(20), nullable=True)
|
||||
collapsible = Column(Boolean, default=True)
|
||||
|
||||
# Relationships
|
||||
parent_group = relationship("VariableGroup", remote_side=[id])
|
||||
child_groups = relationship("VariableGroup", back_populates="parent_group")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<VariableGroup(name='{self.name}')>"
|
||||
@@ -39,6 +39,7 @@ class User(BaseModel):
|
||||
submitted_tickets = relationship("SupportTicket", foreign_keys="SupportTicket.user_id", back_populates="submitter")
|
||||
timers = relationship("Timer", back_populates="user", cascade="all, delete-orphan")
|
||||
time_entries = relationship("TimeEntry", back_populates="user", cascade="all, delete-orphan")
|
||||
sessions = relationship("UserSession", back_populates="user", cascade="all, delete-orphan")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<User(username='{self.username}', email='{self.email}')>"
|
||||
Reference in New Issue
Block a user