2025-08-12 13:33:25 +03:00

497 lines
19 KiB
Python

"""
Analytics app models.
"""
import uuid
import json
from django.db import models
from django.contrib.auth import get_user_model
from django.core.validators import MinValueValidator, MaxValueValidator
from django.utils import timezone
from core.models import Tenant
User = get_user_model()
class Dashboard(models.Model):
"""
Dashboard model for organizing widgets and analytics views.
"""
DASHBOARD_TYPES = [
('EXECUTIVE', 'Executive Dashboard'),
('CLINICAL', 'Clinical Dashboard'),
('OPERATIONAL', 'Operational Dashboard'),
('FINANCIAL', 'Financial Dashboard'),
('QUALITY', 'Quality Dashboard'),
('PATIENT', 'Patient Dashboard'),
('PROVIDER', 'Provider Dashboard'),
('DEPARTMENT', 'Department Dashboard'),
('CUSTOM', 'Custom Dashboard'),
]
dashboard_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE, related_name='dashboards')
name = models.CharField(max_length=200)
description = models.TextField(blank=True)
dashboard_type = models.CharField(max_length=20, choices=DASHBOARD_TYPES)
# Layout and configuration
layout_config = models.JSONField(default=dict, help_text="Dashboard layout configuration")
refresh_interval = models.PositiveIntegerField(default=300, help_text="Refresh interval in seconds")
# Access control
is_public = models.BooleanField(default=False)
allowed_users = models.ManyToManyField(User, blank=True, related_name='accessible_dashboards')
allowed_roles = models.JSONField(default=list, help_text="List of allowed user roles")
# Status
is_active = models.BooleanField(default=True)
is_default = models.BooleanField(default=False)
# Metadata
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
class Meta:
db_table = 'analytics_dashboard'
indexes = [
models.Index(fields=['tenant', 'dashboard_type']),
models.Index(fields=['tenant', 'is_active']),
models.Index(fields=['tenant', 'is_default']),
]
unique_together = [['tenant', 'name']]
def __str__(self):
return f"{self.name} ({self.get_dashboard_type_display()})"
class DashboardWidget(models.Model):
"""
Dashboard widget model for individual analytics components.
"""
WIDGET_TYPES = [
('CHART', 'Chart Widget'),
('TABLE', 'Table Widget'),
('METRIC', 'Metric Widget'),
('GAUGE', 'Gauge Widget'),
('MAP', 'Map Widget'),
('TEXT', 'Text Widget'),
('IMAGE', 'Image Widget'),
('IFRAME', 'IFrame Widget'),
('CUSTOM', 'Custom Widget'),
]
CHART_TYPES = [
('LINE', 'Line Chart'),
('BAR', 'Bar Chart'),
('PIE', 'Pie Chart'),
('DOUGHNUT', 'Doughnut Chart'),
('AREA', 'Area Chart'),
('SCATTER', 'Scatter Plot'),
('HISTOGRAM', 'Histogram'),
('HEATMAP', 'Heat Map'),
('TREEMAP', 'Tree Map'),
('FUNNEL', 'Funnel Chart'),
]
widget_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
dashboard = models.ForeignKey(Dashboard, on_delete=models.CASCADE, related_name='widgets')
# Widget configuration
name = models.CharField(max_length=200)
description = models.TextField(blank=True)
widget_type = models.CharField(max_length=20, choices=WIDGET_TYPES)
chart_type = models.CharField(max_length=20, choices=CHART_TYPES, blank=True)
# Data source
data_source = models.ForeignKey('DataSource', on_delete=models.CASCADE, related_name='widgets')
query_config = models.JSONField(default=dict, help_text="Query configuration for data source")
# Layout
position_x = models.PositiveIntegerField(default=0)
position_y = models.PositiveIntegerField(default=0)
width = models.PositiveIntegerField(default=4, validators=[MinValueValidator(1), MaxValueValidator(12)])
height = models.PositiveIntegerField(default=4, validators=[MinValueValidator(1), MaxValueValidator(12)])
# Display configuration
display_config = models.JSONField(default=dict, help_text="Widget display configuration")
color_scheme = models.CharField(max_length=50, default='default')
# Refresh settings
auto_refresh = models.BooleanField(default=True)
refresh_interval = models.PositiveIntegerField(default=300, help_text="Refresh interval in seconds")
# Status
is_active = models.BooleanField(default=True)
# Metadata
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'analytics_dashboard_widget'
indexes = [
models.Index(fields=['dashboard', 'is_active']),
models.Index(fields=['dashboard', 'position_x', 'position_y']),
]
ordering = ['position_y', 'position_x']
def __str__(self):
return f"{self.name} ({self.get_widget_type_display()})"
class DataSource(models.Model):
"""
Data source model for analytics data connections.
"""
SOURCE_TYPES = [
('DATABASE', 'Database Query'),
('API', 'API Endpoint'),
('FILE', 'File Upload'),
('STREAM', 'Real-time Stream'),
('WEBHOOK', 'Webhook'),
('CUSTOM', 'Custom Source'),
]
CONNECTION_TYPES = [
('POSTGRESQL', 'PostgreSQL'),
('MYSQL', 'MySQL'),
('SQLITE', 'SQLite'),
('MONGODB', 'MongoDB'),
('REDIS', 'Redis'),
('REST_API', 'REST API'),
('GRAPHQL', 'GraphQL'),
('WEBSOCKET', 'WebSocket'),
('CSV', 'CSV File'),
('JSON', 'JSON File'),
('XML', 'XML File'),
]
TEST_STATUS_CHOICES = [
('PENDING', 'Pending'),
('RUNNING', 'Running'),
('SUCCESS', 'Success'),
('FAILURE', 'Failure'),
]
source_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE, related_name='data_sources')
# Source configuration
name = models.CharField(max_length=200)
description = models.TextField(blank=True)
source_type = models.CharField(max_length=20, choices=SOURCE_TYPES)
connection_type = models.CharField(max_length=20, choices=CONNECTION_TYPES)
# Connection details
connection_config = models.JSONField(default=dict, help_text="Connection configuration")
authentication_config = models.JSONField(default=dict, help_text="Authentication configuration")
# Query/endpoint details
query_template = models.TextField(blank=True, help_text="SQL query or API endpoint template")
parameters = models.JSONField(default=dict, help_text="Query parameters")
# Data processing
data_transformation = models.JSONField(default=dict, help_text="Data transformation rules")
cache_duration = models.PositiveIntegerField(default=300, help_text="Cache duration in seconds")
# Health monitoring
is_healthy = models.BooleanField(default=True)
last_health_check = models.DateTimeField(null=True, blank=True)
health_check_interval = models.PositiveIntegerField(default=300, help_text="Health check interval in seconds")
# Status
is_active = models.BooleanField(default=True)
last_test_status = models.CharField(max_length=20, choices=TEST_STATUS_CHOICES, default='PENDING')
last_test_start_at = models.DateTimeField(null=True, blank=True)
last_test_end_at = models.DateTimeField(null=True, blank=True)
last_test_duration_seconds = models.PositiveIntegerField(null=True, blank=True)
last_test_error_message = models.TextField(blank=True)
# Metadata
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
class Meta:
db_table = 'analytics_data_source'
indexes = [
models.Index(fields=['tenant', 'source_type']),
models.Index(fields=['tenant', 'is_active']),
models.Index(fields=['tenant', 'is_healthy']),
]
unique_together = [['tenant', 'name']]
def __str__(self):
return f"{self.name} ({self.get_source_type_display()})"
class Report(models.Model):
"""
Report model for scheduled and ad-hoc reporting.
"""
REPORT_TYPES = [
('OPERATIONAL', 'Operational Report'),
('FINANCIAL', 'Financial Report'),
('CLINICAL', 'Clinical Report'),
('QUALITY', 'Quality Report'),
('COMPLIANCE', 'Compliance Report'),
('PERFORMANCE', 'Performance Report'),
('CUSTOM', 'Custom Report'),
]
OUTPUT_FORMATS = [
('PDF', 'PDF Document'),
('EXCEL', 'Excel Spreadsheet'),
('CSV', 'CSV File'),
('JSON', 'JSON Data'),
('HTML', 'HTML Page'),
('EMAIL', 'Email Report'),
]
SCHEDULE_TYPES = [
('MANUAL', 'Manual Execution'),
('DAILY', 'Daily'),
('WEEKLY', 'Weekly'),
('MONTHLY', 'Monthly'),
('QUARTERLY', 'Quarterly'),
('YEARLY', 'Yearly'),
('CUSTOM', 'Custom Schedule'),
]
report_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE, related_name='reports')
# Report configuration
name = models.CharField(max_length=200)
description = models.TextField(blank=True)
report_type = models.CharField(max_length=20, choices=REPORT_TYPES)
# Data source
data_source = models.ForeignKey(DataSource, on_delete=models.CASCADE, related_name='reports')
query_config = models.JSONField(default=dict, help_text="Query configuration for report")
# Output configuration
output_format = models.CharField(max_length=20, choices=OUTPUT_FORMATS)
template_config = models.JSONField(default=dict, help_text="Report template configuration")
# Scheduling
schedule_type = models.CharField(max_length=20, choices=SCHEDULE_TYPES, default='MANUAL')
schedule_config = models.JSONField(default=dict, help_text="Schedule configuration")
next_execution = models.DateTimeField(null=True, blank=True)
# Distribution
recipients = models.JSONField(default=list, help_text="Report recipients")
distribution_config = models.JSONField(default=dict, help_text="Distribution configuration")
# Status
is_active = models.BooleanField(default=True)
# Metadata
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
class Meta:
db_table = 'analytics_report'
indexes = [
models.Index(fields=['tenant', 'report_type']),
models.Index(fields=['tenant', 'schedule_type']),
models.Index(fields=['tenant', 'next_execution']),
models.Index(fields=['tenant', 'is_active']),
]
unique_together = [['tenant', 'name']]
def __str__(self):
return f"{self.name} ({self.get_report_type_display()})"
class ReportExecution(models.Model):
"""
Report execution model for tracking report runs.
"""
EXECUTION_STATUS = [
('PENDING', 'Pending'),
('RUNNING', 'Running'),
('COMPLETED', 'Completed'),
('FAILED', 'Failed'),
('CANCELLED', 'Cancelled'),
]
execution_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
report = models.ForeignKey(Report, on_delete=models.CASCADE, related_name='executions')
# Execution details
execution_type = models.CharField(max_length=20, choices=[
('MANUAL', 'Manual'),
('SCHEDULED', 'Scheduled'),
('API', 'API Triggered'),
], default='MANUAL')
# Timing
started_at = models.DateTimeField(auto_now_add=True)
completed_at = models.DateTimeField(null=True, blank=True)
duration_seconds = models.PositiveIntegerField(null=True, blank=True)
# Status and results
status = models.CharField(max_length=20, choices=EXECUTION_STATUS, default='PENDING')
error_message = models.TextField(blank=True)
# Output
output_file_path = models.CharField(max_length=500, blank=True)
output_size_bytes = models.PositiveBigIntegerField(null=True, blank=True)
record_count = models.PositiveIntegerField(null=True, blank=True)
# Parameters
execution_parameters = models.JSONField(default=dict, help_text="Execution parameters")
# Metadata
executed_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
class Meta:
db_table = 'analytics_report_execution'
indexes = [
models.Index(fields=['report', 'status']),
models.Index(fields=['report', 'started_at']),
models.Index(fields=['status', 'started_at']),
]
ordering = ['-started_at']
def __str__(self):
return f"{self.report.name} - {self.started_at.strftime('%Y-%m-%d %H:%M')}"
@property
def is_completed(self):
"""Check if execution is completed."""
return self.status in ['COMPLETED', 'FAILED', 'CANCELLED']
class MetricDefinition(models.Model):
"""
Metric definition model for KPI and performance metrics.
"""
METRIC_TYPES = [
('COUNT', 'Count'),
('SUM', 'Sum'),
('AVERAGE', 'Average'),
('PERCENTAGE', 'Percentage'),
('RATIO', 'Ratio'),
('RATE', 'Rate'),
('DURATION', 'Duration'),
('CUSTOM', 'Custom Calculation'),
]
AGGREGATION_PERIODS = [
('REAL_TIME', 'Real-time'),
('HOURLY', 'Hourly'),
('DAILY', 'Daily'),
('WEEKLY', 'Weekly'),
('MONTHLY', 'Monthly'),
('QUARTERLY', 'Quarterly'),
('YEARLY', 'Yearly'),
]
metric_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
tenant = models.ForeignKey(Tenant, on_delete=models.CASCADE, related_name='metric_definitions')
# Metric configuration
name = models.CharField(max_length=200)
description = models.TextField(blank=True)
metric_type = models.CharField(max_length=20, choices=METRIC_TYPES)
# Data source
data_source = models.ForeignKey(DataSource, on_delete=models.CASCADE, related_name='metrics')
calculation_config = models.JSONField(default=dict, help_text="Metric calculation configuration")
# Aggregation
aggregation_period = models.CharField(max_length=20, choices=AGGREGATION_PERIODS)
aggregation_config = models.JSONField(default=dict, help_text="Aggregation configuration")
# Thresholds and targets
target_value = models.DecimalField(max_digits=15, decimal_places=4, null=True, blank=True)
warning_threshold = models.DecimalField(max_digits=15, decimal_places=4, null=True, blank=True)
critical_threshold = models.DecimalField(max_digits=15, decimal_places=4, null=True, blank=True)
# Display configuration
unit_of_measure = models.CharField(max_length=50, blank=True)
decimal_places = models.PositiveIntegerField(default=2, validators=[MaxValueValidator(10)])
display_format = models.CharField(max_length=50, default='number')
# Status
is_active = models.BooleanField(default=True)
# Metadata
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
class Meta:
db_table = 'analytics_metric_definition'
indexes = [
models.Index(fields=['tenant', 'metric_type']),
models.Index(fields=['tenant', 'aggregation_period']),
models.Index(fields=['tenant', 'is_active']),
]
unique_together = [['tenant', 'name']]
def __str__(self):
return f"{self.name} ({self.get_metric_type_display()})"
class MetricValue(models.Model):
"""
Metric value model for storing calculated metric values.
"""
value_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
metric_definition = models.ForeignKey(MetricDefinition, on_delete=models.CASCADE, related_name='values')
# Value details
value = models.DecimalField(max_digits=15, decimal_places=4)
period_start = models.DateTimeField()
period_end = models.DateTimeField()
# Context
dimensions = models.JSONField(default=dict, help_text="Metric dimensions (e.g., department, provider)")
metadata = models.JSONField(default=dict, help_text="Additional metadata")
# Quality indicators
data_quality_score = models.DecimalField(max_digits=5, decimal_places=2, null=True, blank=True,
validators=[MinValueValidator(0), MaxValueValidator(100)])
confidence_level = models.DecimalField(max_digits=5, decimal_places=2, null=True, blank=True,
validators=[MinValueValidator(0), MaxValueValidator(100)])
# Calculation details
calculation_timestamp = models.DateTimeField(auto_now_add=True)
calculation_duration_ms = models.PositiveIntegerField(null=True, blank=True)
class Meta:
db_table = 'analytics_metric_value'
indexes = [
models.Index(fields=['metric_definition', 'period_start']),
models.Index(fields=['metric_definition', 'period_end']),
models.Index(fields=['period_start', 'period_end']),
models.Index(fields=['calculation_timestamp']),
]
unique_together = [['metric_definition', 'period_start', 'period_end']]
ordering = ['-period_start']
def __str__(self):
return f"{self.metric_definition.name}: {self.value} ({self.period_start.date()})"
@property
def is_above_target(self):
"""Check if value is above target."""
if self.metric_definition.target_value:
return self.value >= self.metric_definition.target_value
return None
@property
def threshold_status(self):
"""Get threshold status."""
if self.metric_definition.critical_threshold and self.value >= self.metric_definition.critical_threshold:
return 'CRITICAL'
elif self.metric_definition.warning_threshold and self.value >= self.metric_definition.warning_threshold:
return 'WARNING'
return 'NORMAL'